Skip to content
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b1d7d0f
feat: Add Android-specific Adb and SauceLabs providers
tustanivsky Nov 25, 2025
86e48f9
Fix comments and logs
tustanivsky Nov 25, 2025
927b225
Update Connect-Device func to use env var
tustanivsky Nov 26, 2025
4a55e0a
Add fallback for connect device func if target is empty
tustanivsky Nov 26, 2025
bd38cdc
Add SauceLabs session name override
tustanivsky Nov 26, 2025
26c1d8e
Update adb install command params (remove -r flag)
tustanivsky Nov 26, 2025
a3e4fce
Enforce intent format validation
tustanivsky Nov 26, 2025
7635aaa
Rework android package name extraction
tustanivsky Nov 26, 2025
da7d0da
Rename
tustanivsky Nov 26, 2025
c50bc0c
Fix adb devices discovery
tustanivsky Nov 26, 2025
cba2075
Fix some adb commands output checks
tustanivsky Nov 26, 2025
42c246a
Add unit tests for AndroidHelpers functions
tustanivsky Nov 27, 2025
7c0bb74
Fix formatting
tustanivsky Nov 27, 2025
3d5dd3e
Fix screenshot
tustanivsky Nov 27, 2025
842b08d
Add basic Android integration tests (needs pre-built .apk)
tustanivsky Nov 27, 2025
d9e7839
Allow intent arguments with "--"
limbonaut Nov 27, 2025
db0f6b1
Remove unnecessary initialWaitSeconds
limbonaut Nov 27, 2025
ec9f09d
Fix WaitForProcess PID gathering
limbonaut Nov 27, 2025
c1f33de
Rename "pid" var because analyzer complains about reserved name
limbonaut Nov 27, 2025
d3b0813
Add screenshot test
tustanivsky Nov 27, 2025
ff45311
Align naming
tustanivsky Nov 27, 2025
c0d3757
Add Android test app fixture
tustanivsky Nov 27, 2025
f41f9fe
Refactor SauceLabs provider (ios impl placeholder) + tests
tustanivsky Nov 28, 2025
8578335
Rename test
tustanivsky Nov 28, 2025
e9551b8
Rename adb provider
tustanivsky Nov 28, 2025
a715195
Clean up
tustanivsky Nov 28, 2025
b207c34
Add SauceLabs tests execution to CI
tustanivsky Nov 28, 2025
73d96cb
Fix secrets
tustanivsky Nov 28, 2025
92d93c4
Update docs
tustanivsky Nov 28, 2025
b2ff8d1
Fix readme
tustanivsky Nov 28, 2025
e05a67f
Remove unused force-stop command for Adb provider
tustanivsky Dec 2, 2025
31b012a
Update README.md
tustanivsky Dec 2, 2025
295c5d8
Move hardcoded time intervals to Timeouts property
tustanivsky Dec 2, 2025
baa451e
Fix adb command invocations
tustanivsky Dec 2, 2025
312965a
Remove command duplication for adb screenshot capture
tustanivsky Dec 2, 2025
922266c
Remove redundant screenshot command
tustanivsky Dec 2, 2025
cbb67b4
Rework Android package name extraction helper func
tustanivsky Dec 2, 2025
a2a1654
Remove package name helper fallback tests
tustanivsky Dec 2, 2025
4b229ff
Update app-runner/Private/DeviceProviders/AdbProvider.ps1
tustanivsky Dec 2, 2025
1957e89
Update app-runner/Private/DeviceProviders/SauceLabsProvider.ps1
tustanivsky Dec 2, 2025
5fd78f6
Update app-runner/Private/DeviceProviders/AdbProvider.ps1
tustanivsky Dec 2, 2025
b31283e
Update app-runner/Public/Connect-Device.ps1
tustanivsky Dec 2, 2025
5d2c793
Fix comment
tustanivsky Dec 2, 2025
da4faeb
Fix PR suggestion
tustanivsky Dec 2, 2025
d66a5cd
Update app-runner/Private/DeviceProviders/SauceLabsProvider.ps1
tustanivsky Dec 2, 2025
b88b632
Make SauceLabs tests mandatory in CI
tustanivsky Dec 2, 2025
e1a9413
Add Adb provider tests to CI
tustanivsky Dec 2, 2025
f7e5f5c
Update app-runner/Private/DeviceProviders/AdbProvider.ps1
tustanivsky Dec 2, 2025
c4971d8
Update app-runner/README.md
tustanivsky Dec 2, 2025
1ec6480
Add null check for response
tustanivsky Dec 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/app-runner.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: AppRunner
permissions: read-all

on:
push:
Expand All @@ -13,8 +14,15 @@ on:
jobs:
test:
uses: ./.github/workflows/test-powershell-module.yml
secrets: inherit
with:
module-name: SentryAppRunner
module-path: app-runner
test-path: Tests
exclude-path: Adb.Tests.ps1
settings-path: PSScriptAnalyzerSettings.psd1

test-adb:
uses: ./.github/workflows/test-adb-provider.yml
with:
module-path: app-runner
64 changes: 64 additions & 0 deletions .github/workflows/test-adb-provider.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Test ADB Provider
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a separate file? the other one (test-powershell-module.yml) exists just because it's used for multiple modules.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adb.Tests.ps1 requires setup that’s specific to these integration tests (Android built tools installation, KVM config, emulator-runner action) which would unnecessarily clutter the generic workflow.

permissions:
contents: read

on:
workflow_call:
inputs:
module-path:
description: 'Path to the module directory'
required: true
type: string

jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.module-path }}
shell: pwsh

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Enable KVM group permissions
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Setup Android directories
run: |
mkdir -p $HOME/.android/avd
touch $HOME/.android/repositories.cfg
- name: Install Android SDK Build Tools
shell: bash
run: |
${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "build-tools;35.0.0"
echo "${ANDROID_HOME}/build-tools/35.0.0" >> $GITHUB_PATH
- name: Run ADB Provider Integration Tests
uses: reactivecircus/android-emulator-runner@d94c3fbe4fe6a29e4a5ba47c12fb47677c73656b
timeout-minutes: 45
with:
api-level: 35
target: 'google_apis'
arch: x86_64
force-avd-creation: true
disable-animations: true
disable-spellchecker: true
emulator-options: >
-no-window
-no-snapshot-save
-gpu swiftshader_indirect
-noaudio
-no-boot-anim
-camera-back none
-camera-front none
script: |
adb wait-for-device
echo "Android emulator is ready"
adb devices
cd ${{ inputs.module-path }} && pwsh -Command "Invoke-Pester Tests/Adb.Tests.ps1 -CI"
Comment on lines +61 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
adb wait-for-device
echo "Android emulator is ready"
adb devices
cd ${{ inputs.module-path }} && pwsh -Command "Invoke-Pester Tests/Adb.Tests.ps1 -CI"
adb wait-for-device
echo "Android emulator is ready"
adb devices
cd ${{ inputs.module-path }}
pwsh -Command "Invoke-Pester Tests/Adb.Tests.ps1 -CI"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that each line in the script: block is executed as a separate shell command, so we need to use && to chain them and run everything in a single session.

19 changes: 19 additions & 0 deletions .github/workflows/test-powershell-module.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ on:
required: false
type: string
default: ''
exclude-path:
description: 'Comma-separated list of test file paths to exclude (relative to test-path)'
required: false
type: string
default: ''
settings-path:
description: 'Path to PSScriptAnalyzer settings file (relative to repo root)'
required: false
Expand Down Expand Up @@ -74,6 +79,10 @@ jobs:
run:
working-directory: ${{ inputs.module-path }}
shell: pwsh
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
SAUCE_REGION: us-west-1

steps:
- name: Checkout repository
Expand All @@ -94,6 +103,16 @@ jobs:
$config.Filter.ExcludeTag = $excludeTags.Split(',').Trim()
}

$excludePath = "${{ inputs.exclude-path }}"
if ($excludePath) {
$testPath = "${{ inputs.test-path }}"
$config.Run.ExcludePath = $excludePath.Split(',').Trim() | ForEach-Object {
$relativePath = Join-Path $testPath $_
# ExcludePath requires absolute paths
Join-Path (Get-Location) $relativePath
}
}

$testResults = Invoke-Pester -Configuration $config

Write-Host "Test Summary:" -ForegroundColor Cyan
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ Currently supported:
- Windows
- macOS
- Linux

Future support planned:

- Mobile platforms (iOS, Android)
- **Mobile Platforms:**
- Android (via ADB or SauceLabs Real Device Cloud)
- iOS (via SauceLabs - coming soon)

## Requirements

Expand Down
188 changes: 188 additions & 0 deletions app-runner/Private/AndroidHelpers.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Android Helper Functions
# Shared utilities for Android device providers (ADB and SauceLabs)

<#
.SYNOPSIS
Converts an Android activity path into package name and activity name components.

.DESCRIPTION
Converts the ExecutablePath format used by Android apps: "package.name/activity.name"
Returns a hashtable with PackageName and ActivityName properties.

.PARAMETER ExecutablePath
The full activity path in format "package.name/activity.name"

.EXAMPLE
ConvertFrom-AndroidActivityPath "io.sentry.unreal.sample/com.epicgames.unreal.GameActivity"
Returns: @{ PackageName = "io.sentry.unreal.sample"; ActivityName = "com.epicgames.unreal.GameActivity" }

.EXAMPLE
ConvertFrom-AndroidActivityPath "com.example.app/.MainActivity"
Returns: @{ PackageName = "com.example.app"; ActivityName = ".MainActivity" }
#>
function ConvertFrom-AndroidActivityPath {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ExecutablePath
)

if ($ExecutablePath -notmatch '^([^/]+)/(.+)$') {
throw "ExecutablePath must be in format 'package.name/activity.name'. Got: $ExecutablePath"
}

return @{
PackageName = $matches[1]
ActivityName = $matches[2]
}
}

<#
.SYNOPSIS
Validates that Android Intent extras are in the correct format.

.DESCRIPTION
Android Intent extras should be passed in the format understood by `am start`.
This function validates and optionally formats the arguments string.

Common Intent extra formats:
-e key value String extra
-es key value String extra (explicit)
-ez key true|false Boolean extra
-ei key value Integer extra
-el key value Long extra

.PARAMETER Arguments
The arguments string to validate/format

.EXAMPLE
Test-IntentExtrasFormat "-e cmdline -crash-capture"
Returns: $true

.EXAMPLE
Test-IntentExtrasFormat "-e test true -ez debug false"
Returns: $true
#>
function Test-IntentExtrasFormat {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$Arguments
)

if ([string]::IsNullOrWhiteSpace($Arguments)) {
return $true
}

# Intent extras must start with flags: -e, -es, -ez, -ei, -el, -ef, -eu, etc.
# Followed by at least one whitespace and additional content
if ($Arguments -notmatch '^--?[a-z]{1,2}\s+') {
throw "Invalid Intent extras format: '$Arguments'. Must start with flags like -e, -es, -ez, -ei, -el, etc. followed by key-value pairs."
}

return $true
}

<#
.SYNOPSIS
Extracts the package name from an APK file using aapt.

.DESCRIPTION
Extracts the real package name from an APK file using aapt (Android Asset Packaging Tool).
Requires aapt or aapt2 to be available in PATH.

.PARAMETER ApkPath
Path to the APK file

.EXAMPLE
Get-ApkPackageName "MyApp.apk"
Returns: "com.example.myapp" (actual package name from AndroidManifest.xml)

.EXAMPLE
Get-ApkPackageName "SentryPlayground.apk"
Returns: "io.sentry.sample"

.NOTES
Requires aapt or aapt2 to be in PATH or Android SDK to be installed.
Throws an error if aapt is not available or if the package name cannot be extracted.
#>
function Get-ApkPackageName {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ApkPath
)

if (-not (Test-Path $ApkPath)) {
throw "APK file not found: $ApkPath"
}

if ($ApkPath -notlike '*.apk') {
throw "File must be an .apk file. Got: $ApkPath"
}

# Find aapt or aapt2
$aaptCmd = Get-Command aapt -ErrorAction SilentlyContinue
if (-not $aaptCmd) {
$aaptCmd = Get-Command aapt2 -ErrorAction SilentlyContinue
}

if (-not $aaptCmd) {
throw "aapt or aapt2 not found in PATH. Please install Android SDK Build Tools and ensure aapt is available in PATH."
}

Write-Debug "Using $($aaptCmd.Name) to extract package name from APK"

try {
$PSNativeCommandUseErrorActionPreference = $false
$output = & $aaptCmd.Name dump badging $ApkPath 2>&1

# Parse output for package name: package: name='com.example.app'
foreach ($line in $output) {
if ($line -match "package:\s+name='([^']+)'") {
$packageName = $matches[1]
Write-Debug "Extracted package name: $packageName"
return $packageName
}
}

throw "Failed to extract package name from APK using aapt. APK may be corrupted or invalid."
}
finally {
$PSNativeCommandUseErrorActionPreference = $true
}
}

<#
.SYNOPSIS
Parses logcat output into structured format.

.DESCRIPTION
Converts raw logcat output (array of strings) into a consistent format
that can be used by test utilities like Get-EventIds.

.PARAMETER LogcatOutput
Array of logcat log lines (raw output from adb or SauceLabs)

.EXAMPLE
$logs = adb -s emulator-5554 logcat -d
Format-LogcatOutput $logs
#>
function Format-LogcatOutput {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[object[]]$LogcatOutput
)

if ($null -eq $LogcatOutput -or $LogcatOutput.Count -eq 0) {
return @()
}

# Ensure output is an array of strings
return @($LogcatOutput | ForEach-Object {
if ($null -ne $_) {
$_.ToString()
}
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
}
Loading