From e5fe9efb43f4f30d5751691a86b94ead41330add Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 8 Sep 2026 11:39:33 +0200 Subject: [PATCH 1/4] [flaky-ci] Recover MAUI R2R Helix devices Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scripts/RunMauiR2RHelix.Tests.ps1 | 309 ++++++++++ .../automation/scripts/RunMauiR2RHelix.ps1 | 581 ++++++++++++++++++ .../run-maui-r2r-helix-matrix.yaml | 48 +- 3 files changed, 900 insertions(+), 38 deletions(-) create mode 100644 build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 create mode 100644 build-tools/automation/scripts/RunMauiR2RHelix.ps1 diff --git a/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 b/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 new file mode 100644 index 00000000000..c516eff20e2 --- /dev/null +++ b/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 @@ -0,0 +1,309 @@ +$ErrorActionPreference = 'Stop' + +$runnerScript = Join-Path $PSScriptRoot 'RunMauiR2RHelix.ps1' +$powerShellExe = (Get-Process -Id $PID).Path +if (-not $powerShellExe) { + $powerShellExe = 'powershell.exe' +} + +function Assert-Equal ($Expected, $Actual, [string] $Message) +{ + if ($Expected -ne $Actual) { + throw "$Message Expected '$Expected', but found '$Actual'." + } +} + +function Assert-Contains ([string []] $Lines, [string] $Pattern, [string] $Message) +{ + if (-not ($Lines | Where-Object { $_ -match $Pattern })) { + throw "$Message Pattern '$Pattern' was not found." + } +} + +function Assert-NotContains ([string []] $Lines, [string] $Pattern, [string] $Message) +{ + if ($Lines | Where-Object { $_ -match $Pattern }) { + throw "$Message Pattern '$Pattern' was unexpectedly found." + } +} + +function Assert-MatchCount ([string []] $Lines, [string] $Pattern, [int] $Expected, [string] $Message) +{ + $actual = @($Lines | Where-Object { $_ -match $Pattern }).Count + Assert-Equal $Expected $actual $Message +} + +$testRoot = Join-Path ([IO.Path]::GetTempPath()) "Maui R2R $([IO.Path]::GetRandomFileName())" +$fakeAdb = Join-Path $testRoot 'FakeAdb.ps1' +$apk = Join-Path $testRoot 'MauiR2R-Signed.apk' + +try { + New-Item -ItemType Directory -Path $testRoot | Out-Null + Set-Content -LiteralPath $apk -Value 'test APK placeholder' -Encoding ASCII + + @' +$ErrorActionPreference = 'Stop' + +$command = $args -join ' ' +$transcriptPath = $env:FAKE_ADB_TRANSCRIPT +$scenario = $env:FAKE_ADB_SCENARIO +$previousCommands = @() +if (Test-Path -LiteralPath $transcriptPath) { + $previousCommands = @(Get-Content -LiteralPath $transcriptPath) +} +Add-Content -LiteralPath $transcriptPath -Value $command -Encoding ASCII + +$serverRestarted = [bool]($previousCommands | Where-Object { $_ -eq 'start-server' }) +$deviceRebooted = [bool]($previousCommands | Where-Object { $_ -match '^-s test-device reboot$' }) +$targetUninstalled = [bool]($previousCommands | Where-Object { $_ -match '^-s test-device uninstall com\.xamarin\.mauir2r\.' }) + +if ($command -eq 'version') { + Write-Output 'Android Debug Bridge version 1.0.41' + exit 0 +} + +if ($command -eq 'devices -l') { + Write-Output 'List of devices attached' + switch ($scenario) { + 'recover-device' { + if ($serverRestarted) { + Write-Output 'test-device device product:test model:Test_Device device:test transport_id:1' + } else { + Write-Output 'test-device unauthorized transport_id:1' + } + } + 'unauthorized' { + Write-Output 'test-device unauthorized transport_id:1' + } + 'no-device' { + } + 'multiple-devices' { + Write-Output 'test-device device product:test model:Test_Device device:test transport_id:1' + Write-Output 'other-device device product:test model:Other_Device device:test transport_id:2' + } + default { + Write-Output 'test-device device product:test model:Test_Device device:test transport_id:1' + } + } + exit 0 +} + +if ($command -eq 'kill-server' -or $command -eq 'start-server') { + exit 0 +} + +if ($command -match '^-s test-device shell getprop sys\.boot_completed$') { + Write-Output '1' + exit 0 +} + +if ($command -match '^-s test-device shell pm path android$') { + Write-Output 'package:/system/framework/framework-res.apk' + exit 0 +} + +if ($command -match '^-s test-device shell getprop$') { + Write-Output '[ro.build.fingerprint]: [test/fingerprint]' + exit 0 +} + +if ($command -match '^-s test-device shell df ') { + Write-Output '/dev/block/test 100G 20G 80G 20% /data' + exit 0 +} + +if ($command -match '^-s test-device shell dumpsys (diskstats|storaged)') { + Write-Output 'diagnostic output' + exit 0 +} + +if ($command -match '^-s test-device shell pm list packages -3 -U$') { + Write-Output 'package:com.xamarin.mauir2r.arm64.baseline uid:10123' + exit 0 +} + +if ($command -match '^-s test-device shell pm path com\.xamarin\.mauir2r\.') { + Write-Output 'package:/data/app/maui/base.apk' + exit 0 +} + +if ($command -match '^-s test-device shell dumpsys package com\.xamarin\.mauir2r\.') { + Write-Output 'Package diagnostic output' + exit 0 +} + +if ($command -match '^-s test-device install -r ') { + switch ($scenario) { + 'uid-recovery' { + if (-not $deviceRebooted) { + [Console]::Error.WriteLine('Failure [INSTALL_FAILED_INSUFFICIENT_STORAGE: Scanning Failed.: Package com.xamarin.mauir2r.arm64.baseline could not be assigned a valid UID]') + exit 1 + } + } + 'uid-persistent' { + [Console]::Error.WriteLine('Failure [INSTALL_FAILED_INSUFFICIENT_STORAGE: Scanning Failed.: Package com.xamarin.mauir2r.arm64.baseline could not be assigned a valid UID]') + exit 1 + } + 'update-incompatible' { + if (-not $targetUninstalled) { + [Console]::Error.WriteLine('Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package signatures do not match previously installed version]') + exit 1 + } + } + 'unknown-install-failure' { + [Console]::Error.WriteLine('Failure [INSTALL_FAILED_INVALID_APK: Package is invalid]') + exit 1 + } + } + + Write-Output 'Success' + exit 0 +} + +if ($command -match '^-s test-device uninstall com\.xamarin\.mauir2r\.') { + Write-Output 'Success' + exit 0 +} + +if ($command -eq '-s test-device reboot') { + exit 0 +} + +if ($command -match '^-s test-device shell am force-stop com\.xamarin\.mauir2r\.') { + exit 0 +} + +if ($command -match '^-s test-device shell pm clear com\.xamarin\.mauir2r\.') { + Write-Output 'Success' + exit 0 +} + +if ($command -eq '-s test-device logcat -c') { + exit 0 +} + +if ($command -match '^-s test-device shell monkey -p com\.xamarin\.mauir2r\.') { + Write-Output 'Events injected: 1' + exit 0 +} + +if ($command -match '^-s test-device shell pidof com\.xamarin\.mauir2r\.') { + if ($scenario -eq 'pid-missing') { + exit 0 + } + Write-Output '1234' + exit 0 +} + +if ($command -eq '-s test-device logcat -d -b all') { + Write-Output 'test logcat' + exit 0 +} + +[Console]::Error.WriteLine("Unexpected fake adb command: $command") +exit 2 +'@ | Set-Content -LiteralPath $fakeAdb -Encoding ASCII + + function Invoke-TestCase ([string] $Name, [int] $ExpectedExitCode, [bool] $SkipDiagnostics = $true) + { + $caseDirectory = Join-Path $testRoot $Name + New-Item -ItemType Directory -Path $caseDirectory | Out-Null + $transcriptPath = Join-Path $caseDirectory 'adb-transcript.log' + $outputPath = Join-Path $caseDirectory 'runner-output.log' + + $env:FAKE_ADB_SCENARIO = $Name + $env:FAKE_ADB_TRANSCRIPT = $transcriptPath + $env:HELIX_WORKITEM_UPLOAD_ROOT = $caseDirectory + + $optionalArguments = @() + if ($SkipDiagnostics) { + $optionalArguments += '-SkipExtendedDiagnostics' + } + + & $powerShellExe -NoLogo -NoProfile -File $runnerScript ` + -AdbPath $fakeAdb ` + -ApkPath $apk ` + -PackageName 'com.xamarin.mauir2r.arm64.baseline' ` + -ScenarioName $Name ` + -ConfigurationName $Name ` + -RuntimeIdentifier 'android-arm64' ` + -DeviceRecoveryTimeoutSeconds 0 ` + -AndroidReadyTimeoutSeconds 0 ` + -PollIntervalMilliseconds 0 ` + -RebootInitialDelayMilliseconds 0 ` + -LaunchWaitMilliseconds 0 ` + @optionalArguments *> $outputPath + + $actualExitCode = $LASTEXITCODE + $output = @(Get-Content -LiteralPath $outputPath) + if ($actualExitCode -ne $ExpectedExitCode) { + throw "Test case '$Name' exited with '$actualExitCode' instead of '$ExpectedExitCode'. Output:`n$($output -join [Environment]::NewLine)" + } + + return [pscustomobject]@{ + Name = $Name + Transcript = @(Get-Content -LiteralPath $transcriptPath) + Output = $output + Directory = $caseDirectory + } + } + + $healthy = Invoke-TestCase 'healthy' 0 $false + Assert-NotContains $healthy.Transcript ' uninstall ' 'Healthy device flow must not uninstall the package.' + Assert-NotContains $healthy.Transcript ' reboot$' 'Healthy device flow must not reboot the device.' + Assert-Contains $healthy.Transcript ' shell pm list packages -3 -U$' 'Healthy device flow must capture package UID diagnostics.' + Assert-MatchCount $healthy.Transcript ' shell pm clear com\.xamarin\.mauir2r\.arm64\.baseline$' 2 'Healthy device flow must clear package data before and after the smoke test.' + Assert-MatchCount $healthy.Transcript ' shell am force-stop com\.xamarin\.mauir2r\.arm64\.baseline$' 2 'Healthy device flow must force-stop the package before and after the smoke test.' + + $recoveredDevice = Invoke-TestCase 'recover-device' 0 + Assert-MatchCount $recoveredDevice.Transcript '^kill-server$' 1 'Device enumeration recovery must restart the ADB server only once.' + Assert-MatchCount $recoveredDevice.Transcript '^start-server$' 1 'Device enumeration recovery must restart the ADB server only once.' + Assert-MatchCount $recoveredDevice.Transcript ' install -r ' 1 'Recovered device flow must install once.' + + $unauthorized = Invoke-TestCase 'unauthorized' 1 + Assert-MatchCount $unauthorized.Transcript '^kill-server$' 1 'Unauthorized device flow must attempt one ADB server restart.' + Assert-NotContains $unauthorized.Transcript ' install -r ' 'Persistent unauthorized device flow must fail before installation.' + Assert-Contains $unauthorized.Output 'device-lab owner must attach, authorize, or quarantine' 'Unauthorized failure must identify the service-owner action.' + + $noDevice = Invoke-TestCase 'no-device' 1 + Assert-MatchCount $noDevice.Transcript '^kill-server$' 1 'Missing-device flow must attempt one ADB server restart.' + Assert-NotContains $noDevice.Transcript ' install -r ' 'Persistent missing-device flow must fail before installation.' + Assert-Contains $noDevice.Output 'device-lab owner must attach, authorize, or quarantine' 'Missing-device failure must identify the service-owner action.' + + $multipleDevices = Invoke-TestCase 'multiple-devices' 1 + Assert-NotContains $multipleDevices.Transcript '^kill-server$' 'Multiple-device flow must fail without restarting the ADB server.' + Assert-NotContains $multipleDevices.Transcript ' install -r ' 'Multiple-device flow must fail before installation.' + Assert-Contains $multipleDevices.Output 'expected exactly one Android device' 'Multiple-device failure must identify the ambiguous device state.' + + $uidRecovery = Invoke-TestCase 'uid-recovery' 0 + Assert-MatchCount $uidRecovery.Transcript ' install -r ' 2 'UID exhaustion recovery must retry installation exactly once.' + Assert-MatchCount $uidRecovery.Transcript '^-s test-device reboot$' 1 'UID exhaustion recovery must reboot exactly once.' + Assert-NotContains $uidRecovery.Transcript ' uninstall ' 'UID exhaustion recovery must not uninstall unrelated or target packages.' + + $uidPersistent = Invoke-TestCase 'uid-persistent' 1 + Assert-MatchCount $uidPersistent.Transcript ' install -r ' 2 'Persistent UID exhaustion must stop after one retry.' + Assert-MatchCount $uidPersistent.Transcript '^-s test-device reboot$' 1 'Persistent UID exhaustion must reboot exactly once.' + Assert-Contains $uidPersistent.Output 'UID exhaustion persisted after one device reboot' 'Persistent UID exhaustion must identify the Helix-owner action.' + + $updateIncompatible = Invoke-TestCase 'update-incompatible' 0 + Assert-MatchCount $updateIncompatible.Transcript ' install -r ' 2 'Incompatible signature recovery must retry installation exactly once.' + Assert-MatchCount $updateIncompatible.Transcript '^-s test-device uninstall com\.xamarin\.mauir2r\.arm64\.baseline$' 1 'Incompatible signature recovery must uninstall only the selected package.' + Assert-NotContains $updateIncompatible.Transcript ' reboot$' 'Incompatible signature recovery must not reboot the device.' + + $unknownFailure = Invoke-TestCase 'unknown-install-failure' 1 + Assert-MatchCount $unknownFailure.Transcript ' install -r ' 1 'Unknown install failures must not be retried.' + Assert-NotContains $unknownFailure.Transcript ' uninstall ' 'Unknown install failures must not uninstall packages.' + Assert-NotContains $unknownFailure.Transcript ' reboot$' 'Unknown install failures must not reboot the device.' + Assert-Contains $unknownFailure.Output 'no retry was attempted' 'Unknown install failures must preserve explicit no-retry behavior.' + + $pidMissing = Invoke-TestCase 'pid-missing' 1 + Assert-Contains $pidMissing.Output 'did not remain running after launch' "Empty pidof output must report the smoke-test failure instead of a null-reference error. Output: $($pidMissing.Output -join ' | ')" + Assert-NotContains $pidMissing.Output 'null-valued expression' 'Empty adb output must remain a non-null string.' + + Write-Host 'MAUI R2R Helix recovery tests passed.' +} finally { + Remove-Item Env:FAKE_ADB_SCENARIO -ErrorAction Ignore + Remove-Item Env:FAKE_ADB_TRANSCRIPT -ErrorAction Ignore + Remove-Item Env:HELIX_WORKITEM_UPLOAD_ROOT -ErrorAction Ignore + Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction Ignore +} diff --git a/build-tools/automation/scripts/RunMauiR2RHelix.ps1 b/build-tools/automation/scripts/RunMauiR2RHelix.ps1 new file mode 100644 index 00000000000..140937c9d86 --- /dev/null +++ b/build-tools/automation/scripts/RunMauiR2RHelix.ps1 @@ -0,0 +1,581 @@ +param ( + [Parameter(Mandatory = $true)] + [string] $AdbPath, + [Parameter(Mandatory = $true)] + [string] $ApkPath, + [Parameter(Mandatory = $true)] + [string] $PackageName, + [Parameter(Mandatory = $true)] + [string] $ScenarioName, + [Parameter(Mandatory = $true)] + [string] $ConfigurationName, + [Parameter(Mandatory = $true)] + [string] $RuntimeIdentifier, + [int] $DeviceRecoveryTimeoutSeconds = 30, + [int] $AndroidReadyTimeoutSeconds = 120, + [int] $PollIntervalMilliseconds = 2000, + [int] $RebootInitialDelayMilliseconds = 5000, + [int] $LaunchWaitMilliseconds = 10000, + [switch] $SkipExtendedDiagnostics +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 2.0 + +$allowedPackages = @( + 'com.xamarin.mauir2r.arm64.baseline', + 'com.xamarin.mauir2r.arm64.fullr2r', + 'com.xamarin.mauir2r.arm.baseline', + 'com.xamarin.mauir2r.arm.fullr2r' +) + +$powerShellExe = (Get-Process -Id $PID).Path +if (-not $powerShellExe) { + $powerShellExe = 'powershell.exe' +} + +$uploadDirectory = $env:HELIX_WORKITEM_UPLOAD_ROOT +if (-not $uploadDirectory) { + $uploadDirectory = $PSScriptRoot +} + +$selectedSerial = $null +$applicationInstalled = $false +$failureDiagnosticsCaptured = $false +$exitCode = 0 + +function ConvertTo-ProcessArgument ([string] $Argument) +{ + if ($Argument -notmatch '[\s"]') { + return $Argument + } + + return '"' + $Argument.Replace('"', '\"') + '"' +} + +function Get-CombinedOutput ($Result) +{ + return (@($Result.StandardOutput, $Result.StandardError) | + Where-Object { $_ } | + ForEach-Object { $_.Trim() } | + Where-Object { $_ }) -join [Environment]::NewLine +} + +function Test-ContainsIgnoreCase ([string] $Value, [string] $Expected) +{ + return $Value.IndexOf($Expected, [StringComparison]::OrdinalIgnoreCase) -ge 0 +} + +function Write-CommandResult ([string] $Name, [string []] $Arguments, $Result) +{ + $safeName = $Name -replace '[^A-Za-z0-9_.-]', '-' + $path = Join-Path $uploadDirectory "$safeName.log" + $content = @( + "Command: adb $($Arguments -join ' ')", + "ExitCode: $($Result.ExitCode)", + "TimedOut: $($Result.TimedOut)", + '', + '===== stdout =====', + $Result.StandardOutput, + '===== stderr =====', + $Result.StandardError + ) -join [Environment]::NewLine + Set-Content -LiteralPath $path -Value $content -Encoding ASCII +} + +function Invoke-Adb ([string []] $Arguments, [int] $TimeoutSeconds = 30, [switch] $Quiet) +{ + $stdoutPath = Join-Path ([IO.Path]::GetTempPath()) "$([IO.Path]::GetRandomFileName()).stdout" + $stderrPath = Join-Path ([IO.Path]::GetTempPath()) "$([IO.Path]::GetRandomFileName()).stderr" + $process = $null + $timedOut = $false + + if ($AdbPath.EndsWith('.ps1', [StringComparison]::OrdinalIgnoreCase)) { + $filePath = $powerShellExe + $processArguments = @('-NoLogo', '-NoProfile', '-File', $AdbPath) + $Arguments + } else { + $filePath = $AdbPath + $processArguments = $Arguments + } + + $argumentList = @($processArguments | ForEach-Object { ConvertTo-ProcessArgument $_ }) + if (-not $Quiet) { + Write-Host "adb $($Arguments -join ' ')" + } + + try { + $process = Start-Process -FilePath $filePath -ArgumentList $argumentList -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -NoNewWindow -PassThru + # Windows PowerShell requires the process handle to be cached before waiting when output is redirected. + $null = $process.Handle + $timeoutMilliseconds = [Math]::Max(1, $TimeoutSeconds * 1000) + if (-not $process.WaitForExit($timeoutMilliseconds)) { + $timedOut = $true + try { + $process.Kill() + $process.WaitForExit() + } catch { + Write-Warning "Could not terminate timed-out adb process: $($_.Exception.Message)" + } + } else { + $process.WaitForExit() + } + + [string] $standardOutput = if (Test-Path $stdoutPath) { Get-Content -LiteralPath $stdoutPath -Raw } else { "" } + [string] $standardError = if (Test-Path $stderrPath) { Get-Content -LiteralPath $stderrPath -Raw } else { "" } + if ($null -eq $standardOutput) { + $standardOutput = "" + } + if ($null -eq $standardError) { + $standardError = "" + } + $commandExitCode = if ($timedOut) { -1 } else { $process.ExitCode } + + if (-not $Quiet) { + if ($standardOutput) { + Write-Host $standardOutput.TrimEnd() + } + if ($standardError) { + Write-Host $standardError.TrimEnd() + } + } + + return [pscustomobject]@{ + ExitCode = $commandExitCode + StandardOutput = $standardOutput + StandardError = $standardError + TimedOut = $timedOut + } + } finally { + Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction Ignore + if ($process) { + $process.Dispose() + } + } +} + +function Get-AdbDeviceSnapshot ([string] $Name) +{ + $arguments = @('devices', '-l') + $result = Invoke-Adb $arguments + Write-CommandResult "adb-devices-$ConfigurationName-$Name" $arguments $result + + $devices = @() + foreach ($line in ($result.StandardOutput -split '\r?\n')) { + if (-not $line -or $line.StartsWith('List of devices attached', [StringComparison]::OrdinalIgnoreCase)) { + continue + } + if ($line -match '^\s*(\S+)\s+(\S+)(?:\s+.*)?$') { + $devices += [pscustomobject]@{ + Serial = $Matches[1] + State = $Matches[2].ToLowerInvariant() + Line = $line.Trim() + } + } + } + + return [pscustomobject]@{ + Result = $result + Devices = @($devices) + } +} + +function Select-HelixDevice ([string] $Name) +{ + $snapshot = Get-AdbDeviceSnapshot $Name + $devices = @($snapshot.Devices) + + if ($snapshot.Result.ExitCode -ne 0) { + return [pscustomobject]@{ + Ready = $false + Recoverable = $true + Serial = $null + Reason = "adb devices failed with exit code $($snapshot.Result.ExitCode)" + } + } + + if ($devices.Count -eq 0) { + return [pscustomobject]@{ + Ready = $false + Recoverable = $true + Serial = $null + Reason = 'no attached Android device was detected' + } + } + + if ($devices.Count -ne 1) { + return [pscustomobject]@{ + Ready = $false + Recoverable = $false + Serial = $null + Reason = "expected exactly one Android device, but found $($devices.Count): $($devices.Line -join '; ')" + } + } + + $device = $devices[0] + if ($device.State -ne 'device') { + return [pscustomobject]@{ + Ready = $false + Recoverable = $true + Serial = $device.Serial + Reason = "Android device '$($device.Serial)' is '$($device.State)', not authorized and online" + } + } + + return [pscustomobject]@{ + Ready = $true + Recoverable = $false + Serial = $device.Serial + Reason = "Android device '$($device.Serial)' is ready" + } +} + +function Restart-AdbServer +{ + Write-Host 'Restarting the ADB server once to recover device enumeration.' + $killArguments = @('kill-server') + $killResult = Invoke-Adb $killArguments -TimeoutSeconds 15 + Write-CommandResult "adb-kill-server-$ConfigurationName" $killArguments $killResult + + $startArguments = @('start-server') + $startResult = Invoke-Adb $startArguments -TimeoutSeconds 30 + Write-CommandResult "adb-start-server-$ConfigurationName" $startArguments $startResult + if ($startResult.ExitCode -ne 0) { + throw "MAUI R2R Helix infrastructure failure: adb start-server failed with exit code $($startResult.ExitCode)." + } +} + +function Wait-ForReadyDevice ([int] $TimeoutSeconds, [string] $Name, [string] $ExpectedSerial = "") +{ + $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) + do { + $selection = Select-HelixDevice $Name + if ($selection.Ready) { + if (-not $ExpectedSerial -or $selection.Serial -eq $ExpectedSerial) { + return $selection + } + return [pscustomobject]@{ + Ready = $false + Recoverable = $false + Serial = $selection.Serial + Reason = "expected Android device '$ExpectedSerial', but '$($selection.Serial)' was attached" + } + } + if (-not $selection.Recoverable) { + return $selection + } + if ([DateTime]::UtcNow -ge $deadline) { + return $selection + } + Start-Sleep -Milliseconds $PollIntervalMilliseconds + } while ($true) +} + +function Resolve-HelixDevice +{ + $selection = Select-HelixDevice 'initial' + if ($selection.Ready) { + return $selection.Serial + } + if (-not $selection.Recoverable) { + throw "MAUI R2R Helix infrastructure failure: $($selection.Reason)." + } + + Write-Warning "$($selection.Reason); attempting one bounded ADB server recovery." + Restart-AdbServer + $selection = Wait-ForReadyDevice $DeviceRecoveryTimeoutSeconds 'post-adb-restart' $selection.Serial + if (-not $selection.Ready) { + throw "MAUI R2R Helix infrastructure failure after one ADB server restart: $($selection.Reason). The Helix device-lab owner must attach, authorize, or quarantine this device." + } + + return $selection.Serial +} + +function Wait-ForAndroidReady ([string] $Serial, [string] $Name) +{ + $deadline = [DateTime]::UtcNow.AddSeconds($AndroidReadyTimeoutSeconds) + do { + $bootArguments = @('-s', $Serial, 'shell', 'getprop', 'sys.boot_completed') + $bootResult = Invoke-Adb $bootArguments -TimeoutSeconds 15 -Quiet + $packageArguments = @('-s', $Serial, 'shell', 'pm', 'path', 'android') + $packageResult = Invoke-Adb $packageArguments -TimeoutSeconds 20 -Quiet + + $bootCompleted = $bootResult.ExitCode -eq 0 -and $bootResult.StandardOutput.Trim() -eq '1' + $packageManagerReady = $packageResult.ExitCode -eq 0 -and $packageResult.StandardOutput.Trim().StartsWith('package:', [StringComparison]::Ordinal) + if ($bootCompleted -and $packageManagerReady) { + Write-CommandResult "android-boot-$ConfigurationName-$Name" $bootArguments $bootResult + Write-CommandResult "android-package-manager-$ConfigurationName-$Name" $packageArguments $packageResult + return $true + } + + if ([DateTime]::UtcNow -ge $deadline) { + Write-CommandResult "android-boot-$ConfigurationName-$Name" $bootArguments $bootResult + Write-CommandResult "android-package-manager-$ConfigurationName-$Name" $packageArguments $packageResult + return $false + } + Start-Sleep -Milliseconds $PollIntervalMilliseconds + } while ($true) +} + +function Capture-DeviceDiagnostics ([string] $Serial, [string] $Name) +{ + Write-Host "Capturing Android device diagnostics: $Name" + $snapshot = Get-AdbDeviceSnapshot "diagnostics-$Name" + $onlineDevice = @($snapshot.Devices | Where-Object { $_.Serial -eq $Serial -and $_.State -eq 'device' }) + if ($onlineDevice.Count -ne 1) { + Write-Warning "Skipping shell diagnostics because Android device '$Serial' is not online." + return + } + if ($SkipExtendedDiagnostics) { + return + } + + $commands = @( + @{ Title = 'getprop'; Arguments = @('-s', $Serial, 'shell', 'getprop') }, + @{ Title = 'df-data'; Arguments = @('-s', $Serial, 'shell', 'df', '/data') }, + @{ Title = 'df-emulated-storage'; Arguments = @('-s', $Serial, 'shell', 'df', '/storage/emulated/0') }, + @{ Title = 'dumpsys-diskstats'; Arguments = @('-s', $Serial, 'shell', 'dumpsys', 'diskstats') }, + @{ Title = 'dumpsys-storaged'; Arguments = @('-s', $Serial, 'shell', 'dumpsys', 'storaged') }, + @{ Title = 'third-party-packages'; Arguments = @('-s', $Serial, 'shell', 'pm', 'list', 'packages', '-3', '-U') }, + @{ Title = 'target-package-path'; Arguments = @('-s', $Serial, 'shell', 'pm', 'path', $PackageName) }, + @{ Title = 'target-package-details'; Arguments = @('-s', $Serial, 'shell', 'dumpsys', 'package', $PackageName) } + ) + + $content = @() + foreach ($command in $commands) { + $result = Invoke-Adb $command.Arguments -TimeoutSeconds 15 -Quiet + $content += @( + "===== $($command.Title) =====", + "Command: adb $($command.Arguments -join ' ')", + "ExitCode: $($result.ExitCode)", + $Result.StandardOutput, + $Result.StandardError, + '' + ) + } + + $safeName = $Name -replace '[^A-Za-z0-9_.-]', '-' + Set-Content -LiteralPath (Join-Path $uploadDirectory "device-state-$ConfigurationName-$safeName.log") -Value ($content -join [Environment]::NewLine) -Encoding ASCII +} + +function Restart-AndroidDevice ([string] $Serial) +{ + Write-Warning "Rebooting Android device '$Serial' once to recover package UID allocation." + $arguments = @('-s', $Serial, 'reboot') + $result = Invoke-Adb $arguments -TimeoutSeconds 30 + Write-CommandResult "adb-reboot-$ConfigurationName" $arguments $result + if ($result.ExitCode -ne 0) { + throw "MAUI R2R Helix infrastructure failure: adb reboot failed with exit code $($result.ExitCode)." + } + + Start-Sleep -Milliseconds $RebootInitialDelayMilliseconds + $selection = Wait-ForReadyDevice $AndroidReadyTimeoutSeconds 'post-device-reboot' $Serial + if (-not $selection.Ready) { + throw "MAUI R2R Helix infrastructure failure: Android device '$Serial' did not reconnect after its one recovery reboot. The Helix device-lab owner must repair or quarantine it." + } + if (-not (Wait-ForAndroidReady $Serial 'post-device-reboot')) { + throw "MAUI R2R Helix infrastructure failure: Android device '$Serial' did not finish booting with a responsive package manager after its one recovery reboot." + } +} + +function Install-MauiR2RApk ([string] $Serial) +{ + $updateRecoveryUsed = $false + $uidRecoveryUsed = $false + $attempt = 0 + + while ($true) { + $attempt++ + $arguments = @('-s', $Serial, 'install', '-r', $ApkPath) + $result = Invoke-Adb $arguments -TimeoutSeconds 180 + Write-CommandResult "install-$ConfigurationName-attempt-$attempt" $arguments $result + if ($result.ExitCode -eq 0) { + return + } + + $output = Get-CombinedOutput $result + $isUpdateSignatureFailure = Test-ContainsIgnoreCase $output 'INSTALL_FAILED_UPDATE_INCOMPATIBLE' + $isUidExhaustion = (Test-ContainsIgnoreCase $output 'INSTALL_FAILED_INSUFFICIENT_STORAGE') -and + (Test-ContainsIgnoreCase $output 'could not be assigned a valid UID') + + if ($isUpdateSignatureFailure -and -not $updateRecoveryUsed) { + $updateRecoveryUsed = $true + Write-Warning "The existing MAUI R2R package has an incompatible signature; uninstalling only '$PackageName' before one install retry." + $uninstallArguments = @('-s', $Serial, 'uninstall', $PackageName) + $uninstallResult = Invoke-Adb $uninstallArguments -TimeoutSeconds 60 + Write-CommandResult "uninstall-$ConfigurationName-update-signature" $uninstallArguments $uninstallResult + if ($uninstallResult.ExitCode -ne 0) { + throw "APK replacement failed with an incompatible signature, and cleanup of the known package '$PackageName' failed with exit code $($uninstallResult.ExitCode)." + } + continue + } + + if ($isUidExhaustion -and -not $uidRecoveryUsed) { + $uidRecoveryUsed = $true + Capture-DeviceDiagnostics $Serial 'uid-exhaustion-before-reboot' + Restart-AndroidDevice $Serial + Capture-DeviceDiagnostics $Serial 'uid-exhaustion-after-reboot' + continue + } + + if ($isUpdateSignatureFailure) { + Capture-DeviceDiagnostics $Serial 'update-signature-failure' + $script:failureDiagnosticsCaptured = $true + throw "APK replacement still failed with an incompatible signature after uninstalling only '$PackageName' and retrying once. Last install output: $output" + } + + if ($isUidExhaustion) { + $script:failureDiagnosticsCaptured = $true + throw "MAUI R2R Helix infrastructure failure: Android package UID exhaustion persisted after one device reboot. The Helix device-lab owner must reprovision or quarantine this device. Last install output: $output" + } + + Capture-DeviceDiagnostics $Serial 'install-failure' + $script:failureDiagnosticsCaptured = $true + if ($result.TimedOut) { + throw "APK installation timed out; no retry was attempted because the failure did not match a recognized infrastructure recovery signature." + } + throw "APK installation failed without a recognized infrastructure recovery signature; no retry was attempted. Last install output: $output" + } +} + +function Reset-MauiR2RPackage ([string] $Serial) +{ + $forceStopArguments = @('-s', $Serial, 'shell', 'am', 'force-stop', $PackageName) + $forceStopResult = Invoke-Adb $forceStopArguments -TimeoutSeconds 30 + Write-CommandResult "force-stop-$ConfigurationName-before-launch" $forceStopArguments $forceStopResult + if ($forceStopResult.ExitCode -ne 0) { + throw "Could not force-stop the installed MAUI R2R package '$PackageName'." + } + + $clearArguments = @('-s', $Serial, 'shell', 'pm', 'clear', $PackageName) + $clearResult = Invoke-Adb $clearArguments -TimeoutSeconds 60 + Write-CommandResult "clear-package-$ConfigurationName-before-launch" $clearArguments $clearResult + if ($clearResult.ExitCode -ne 0) { + throw "Could not clear the installed MAUI R2R package '$PackageName' before launch." + } + if (-not (Test-ContainsIgnoreCase $clearResult.StandardOutput 'Success')) { + Write-Warning "Package data clear returned exit code 0 without the usual 'Success' output." + } +} + +function Verify-MauiR2RPackage ([string] $Serial) +{ + $pathArguments = @('-s', $Serial, 'shell', 'pm', 'path', $PackageName) + $pathResult = Invoke-Adb $pathArguments -TimeoutSeconds 30 + Write-CommandResult "pm-path-command-$ConfigurationName" $pathArguments $pathResult + Set-Content -LiteralPath (Join-Path $uploadDirectory "pm-path-$ConfigurationName.log") -Value $pathResult.StandardOutput -Encoding ASCII + if ($pathResult.ExitCode -ne 0 -or -not $pathResult.StandardOutput.Trim().StartsWith('package:', [StringComparison]::Ordinal)) { + throw "The MAUI R2R package '$PackageName' was not present after installation." + } + + $dumpsysArguments = @('-s', $Serial, 'shell', 'dumpsys', 'package', $PackageName) + $dumpsysResult = Invoke-Adb $dumpsysArguments -TimeoutSeconds 30 -Quiet + Write-CommandResult "dumpsys-package-command-$ConfigurationName" $dumpsysArguments $dumpsysResult + Set-Content -LiteralPath (Join-Path $uploadDirectory "dumpsys-package-$ConfigurationName.log") -Value $dumpsysResult.StandardOutput -Encoding ASCII + if ($dumpsysResult.ExitCode -ne 0) { + Write-Warning "Could not capture dumpsys details for installed package '$PackageName'." + } +} + +function Stop-MauiR2RPackage ([string] $Serial) +{ + $forceStopArguments = @('-s', $Serial, 'shell', 'am', 'force-stop', $PackageName) + $forceStopResult = Invoke-Adb $forceStopArguments -TimeoutSeconds 30 -Quiet + Write-CommandResult "force-stop-$ConfigurationName-after-run" $forceStopArguments $forceStopResult + + $clearArguments = @('-s', $Serial, 'shell', 'pm', 'clear', $PackageName) + $clearResult = Invoke-Adb $clearArguments -TimeoutSeconds 60 -Quiet + Write-CommandResult "clear-package-$ConfigurationName-after-run" $clearArguments $clearResult + + if ($forceStopResult.ExitCode -ne 0 -or $clearResult.ExitCode -ne 0) { + Write-Warning "Best-effort package cleanup failed for '$PackageName'; the package was intentionally not uninstalled to avoid Android UID churn." + } +} + +try { + if (-not (Test-Path -LiteralPath $AdbPath)) { + throw "Could not find adb at '$AdbPath'." + } + if (-not (Test-Path -LiteralPath $ApkPath)) { + throw "Could not find the MAUI R2R APK at '$ApkPath'." + } + if ($PackageName -notin $allowedPackages) { + throw "Refusing to operate on unexpected package '$PackageName'." + } + + New-Item -ItemType Directory -Force -Path $uploadDirectory | Out-Null + $scenarioPath = Join-Path $uploadDirectory "scenario-$ConfigurationName.txt" + @( + "Scenario: $ScenarioName", + "Configuration: $ConfigurationName", + "RuntimeIdentifier: $RuntimeIdentifier", + "PackageName: $PackageName", + "APK: $ApkPath", + "Machine: $env:COMPUTERNAME" + ) | Set-Content -LiteralPath $scenarioPath -Encoding ASCII + + $versionArguments = @('version') + $versionResult = Invoke-Adb $versionArguments + Write-CommandResult "adb-version-$ConfigurationName" $versionArguments $versionResult + if ($versionResult.ExitCode -ne 0) { + throw "adb version failed with exit code $($versionResult.ExitCode)." + } + + $selectedSerial = Resolve-HelixDevice + Write-Host "Using Android device '$selectedSerial'." + if (-not (Wait-ForAndroidReady $selectedSerial 'initial')) { + throw "MAUI R2R Helix infrastructure failure: Android device '$selectedSerial' did not report boot completion with a responsive package manager within $AndroidReadyTimeoutSeconds seconds." + } + + Capture-DeviceDiagnostics $selectedSerial 'initial' + Install-MauiR2RApk $selectedSerial + $applicationInstalled = $true + Reset-MauiR2RPackage $selectedSerial + Verify-MauiR2RPackage $selectedSerial + + $clearLogcatArguments = @('-s', $selectedSerial, 'logcat', '-c') + $clearLogcatResult = Invoke-Adb $clearLogcatArguments -TimeoutSeconds 30 + Write-CommandResult "logcat-clear-$ConfigurationName" $clearLogcatArguments $clearLogcatResult + + $launchArguments = @('-s', $selectedSerial, 'shell', 'monkey', '-p', $PackageName, '-c', 'android.intent.category.LAUNCHER', '1') + $launchResult = Invoke-Adb $launchArguments -TimeoutSeconds 60 + Write-CommandResult "launch-$ConfigurationName" $launchArguments $launchResult + if ($launchResult.ExitCode -ne 0) { + throw "Launching the MAUI R2R APK failed with exit code $($launchResult.ExitCode)." + } + + Start-Sleep -Milliseconds $LaunchWaitMilliseconds + $pidArguments = @('-s', $selectedSerial, 'shell', 'pidof', $PackageName) + $pidResult = Invoke-Adb $pidArguments -TimeoutSeconds 30 + Write-CommandResult "pidof-$ConfigurationName" $pidArguments $pidResult + + $logcatArguments = @('-s', $selectedSerial, 'logcat', '-d', '-b', 'all') + $logcatResult = Invoke-Adb $logcatArguments -TimeoutSeconds 60 -Quiet + Write-CommandResult "logcat-command-$ConfigurationName" $logcatArguments $logcatResult + Set-Content -LiteralPath (Join-Path $uploadDirectory "logcat-$ConfigurationName.log") -Value $logcatResult.StandardOutput -Encoding ASCII + + if ($pidResult.ExitCode -ne 0 -or -not $pidResult.StandardOutput.Trim()) { + throw "The MAUI R2R APK did not remain running after launch." + } + + Write-Host "MAUI R2R smoke test passed for $ScenarioName." +} catch { + $exitCode = 1 + Write-Error $_.Exception.Message -ErrorAction Continue + if ($selectedSerial -and -not $failureDiagnosticsCaptured) { + try { + Capture-DeviceDiagnostics $selectedSerial 'final-failure' + } catch { + Write-Warning "Could not capture final Android diagnostics: $($_.Exception.Message)" + } + } +} finally { + if ($applicationInstalled -and $selectedSerial) { + try { + Stop-MauiR2RPackage $selectedSerial + } catch { + Write-Warning "Could not complete best-effort MAUI R2R package cleanup: $($_.Exception.Message)" + } + } +} + +exit $exitCode diff --git a/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml b/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml index db39c7c841c..a3f2806d942 100644 --- a/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml +++ b/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml @@ -153,6 +153,7 @@ steps: - ${{ each testCase in parameters.testCases }}: - powershell: | + $xaSourcePath = '${{ parameters.xaSourcePath }}' $projectDirectory = '${{ parameters.matrixDirectory }}/${{ testCase.name }}' $payloadDirectory = '${{ parameters.payloadDirectory }}/${{ testCase.name }}' $runtimeIdentifier = '${{ testCase.runtimeIdentifier }}' @@ -187,46 +188,13 @@ steps: New-Item -ItemType Directory -Force -Path $payloadPlatformTools | Out-Null Copy-Item -Path (Join-Path $platformTools '*') -Destination $payloadPlatformTools -Recurse -Force + $runnerScript = Join-Path $xaSourcePath 'build-tools/automation/scripts/RunMauiR2RHelix.ps1' + Copy-Item -LiteralPath $runnerScript -Destination (Join-Path $payloadDirectory 'RunMauiR2RHelix.ps1') -Force + $script = @" @echo on - setlocal - set "UPLOAD_DIR=%HELIX_WORKITEM_UPLOAD_ROOT%" - if "%UPLOAD_DIR%"=="" set "UPLOAD_DIR=%~dp0" - if not exist "%UPLOAD_DIR%" mkdir "%UPLOAD_DIR%" - echo Scenario: ${{ testCase.displayName }} > "%UPLOAD_DIR%\scenario-${{ testCase.name }}.txt" - echo RuntimeIdentifier: $runtimeIdentifier >> "%UPLOAD_DIR%\scenario-${{ testCase.name }}.txt" - echo PackageName: $packageName >> "%UPLOAD_DIR%\scenario-${{ testCase.name }}.txt" - echo APK: $apkName >> "%UPLOAD_DIR%\scenario-${{ testCase.name }}.txt" - set "ADB=%~dp0platform-tools\adb.exe" - if not exist "%ADB%" ( - echo Could not find payload adb.exe at "%ADB%" - dir "%~dp0" /s - exit /b 1 - ) - "%ADB%" version || exit /b 1 - "%ADB%" devices -l > "%UPLOAD_DIR%\adb-devices-${{ testCase.name }}.log" - type "%UPLOAD_DIR%\adb-devices-${{ testCase.name }}.log" - "%ADB%" wait-for-device || exit /b 1 - "%ADB%" shell getprop > "%UPLOAD_DIR%\getprop-${{ testCase.name }}.log" - "%ADB%" shell getprop ro.product.cpu.abi - "%ADB%" shell getprop ro.product.cpu.abilist - "%ADB%" shell getprop ro.build.fingerprint - "%ADB%" uninstall $packageName - "%ADB%" install -r "%~dp0$apkName" || exit /b 1 - "%ADB%" shell pm path $packageName > "%UPLOAD_DIR%\pm-path-${{ testCase.name }}.log" - "%ADB%" shell dumpsys package $packageName > "%UPLOAD_DIR%\dumpsys-package-${{ testCase.name }}.log" - "%ADB%" shell logcat -c - "%ADB%" shell monkey -p $packageName -c android.intent.category.LAUNCHER 1 || exit /b 1 - ping -n 11 127.0.0.1 > nul - "%ADB%" shell pidof $packageName - if errorlevel 1 ( - "%ADB%" logcat -d -b all > "%UPLOAD_DIR%\logcat-${{ testCase.name }}.log" - "%ADB%" uninstall $packageName - exit /b 1 - ) - "%ADB%" logcat -d -b all > "%UPLOAD_DIR%\logcat-${{ testCase.name }}.log" - "%ADB%" uninstall $packageName - exit /b 0 + powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0RunMauiR2RHelix.ps1" -AdbPath "%~dp0platform-tools\adb.exe" -ApkPath "%~dp0$apkName" -PackageName "$packageName" -ScenarioName "${{ testCase.displayName }}" -ConfigurationName "${{ testCase.name }}" -RuntimeIdentifier "$runtimeIdentifier" + exit /b %ERRORLEVEL% "@ Set-Content -LiteralPath (Join-Path $payloadDirectory 'run-maui-r2r.cmd') -Value $script -Encoding ASCII @@ -246,6 +214,10 @@ steps: displayName: Restore MAUI R2R Helix project continueOnError: false +- powershell: | + & '${{ parameters.xaSourcePath }}/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1' + displayName: Test MAUI R2R Helix device recovery + - powershell: | & '${{ parameters.xaSourcePath }}/build-tools/automation/scripts/ConfigureMauiR2RHelixEnvironment.Tests.ps1' displayName: Test MAUI R2R Helix environment From 418d3bbb79a1b24e4cafe44c7596ff74a8574a67 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 8 Sep 2026 12:53:18 +0200 Subject: [PATCH 2/4] [flaky-ci] Verify Helix test process exit codes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scripts/RunMauiR2RHelix.Tests.Process.ps1 | 50 +++++++++++++++++++ .../scripts/RunMauiR2RHelix.Tests.ps1 | 13 +++++ .../run-maui-r2r-helix-matrix.yaml | 2 +- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 build-tools/automation/scripts/RunMauiR2RHelix.Tests.Process.ps1 diff --git a/build-tools/automation/scripts/RunMauiR2RHelix.Tests.Process.ps1 b/build-tools/automation/scripts/RunMauiR2RHelix.Tests.Process.ps1 new file mode 100644 index 00000000000..30db141be9a --- /dev/null +++ b/build-tools/automation/scripts/RunMauiR2RHelix.Tests.Process.ps1 @@ -0,0 +1,50 @@ +$ErrorActionPreference = 'Stop' + +$testScript = Join-Path $PSScriptRoot 'RunMauiR2RHelix.Tests.ps1' +$powerShell = Get-Command 'pwsh' -ErrorAction Stop +$testRoot = Join-Path ([IO.Path]::GetTempPath()) "Maui R2R process $([IO.Path]::GetRandomFileName())" +$testsPassed = $false + +function Invoke-TestProcess ([string] $Name, [string []] $AdditionalArguments) +{ + $stdoutPath = Join-Path $testRoot "$Name.out.log" + $stderrPath = Join-Path $testRoot "$Name.err.log" + $arguments = @('-NoLogo', '-NoProfile', '-File', "`"$testScript`"") + $AdditionalArguments + $process = Start-Process -FilePath $powerShell.Source -ArgumentList $arguments -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -NoNewWindow -PassThru + # Windows PowerShell requires the process handle to be cached before waiting when output is redirected. + $null = $process.Handle + $process.WaitForExit() + + return [pscustomobject]@{ + ExitCode = $process.ExitCode + StandardOutput = [string](Get-Content -LiteralPath $stdoutPath -Raw -ErrorAction Ignore) + StandardError = [string](Get-Content -LiteralPath $stderrPath -Raw -ErrorAction Ignore) + } +} + +try { + New-Item -ItemType Directory -Path $testRoot | Out-Null + + $success = Invoke-TestProcess 'success' @() + if ($success.ExitCode -ne 0) { + throw "The successful MAUI R2R Helix suite exited with code $($success.ExitCode). Output:`n$($success.StandardOutput)`n$($success.StandardError)" + } + Write-Host $success.StandardOutput.TrimEnd() + + $failure = Invoke-TestProcess 'injected-failure' @('-InjectAssertionFailure') + if ($failure.ExitCode -eq 0) { + throw 'The injected MAUI R2R Helix assertion failure unexpectedly exited with code 0.' + } + if ($failure.StandardError -notmatch 'Injected process-level assertion failure') { + throw "The injected MAUI R2R Helix assertion failure did not report the expected error. Output:`n$($failure.StandardOutput)`n$($failure.StandardError)" + } + + Write-Host 'MAUI R2R Helix process exit-code tests passed.' + $testsPassed = $true +} finally { + Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction Ignore +} + +if ($testsPassed) { + exit 0 +} diff --git a/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 b/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 index c516eff20e2..124e55a4624 100644 --- a/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 +++ b/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 @@ -1,3 +1,7 @@ +param ( + [switch] $InjectAssertionFailure +) + $ErrorActionPreference = 'Stop' $runnerScript = Join-Path $PSScriptRoot 'RunMauiR2RHelix.ps1' @@ -36,10 +40,14 @@ function Assert-MatchCount ([string []] $Lines, [string] $Pattern, [int] $Expect $testRoot = Join-Path ([IO.Path]::GetTempPath()) "Maui R2R $([IO.Path]::GetRandomFileName())" $fakeAdb = Join-Path $testRoot 'FakeAdb.ps1' $apk = Join-Path $testRoot 'MauiR2R-Signed.apk' +$suitePassed = $false try { New-Item -ItemType Directory -Path $testRoot | Out-Null Set-Content -LiteralPath $apk -Value 'test APK placeholder' -Encoding ASCII + if ($InjectAssertionFailure) { + Assert-Equal 'expected' 'injected failure' 'Injected process-level assertion failure.' + } @' $ErrorActionPreference = 'Stop' @@ -301,9 +309,14 @@ exit 2 Assert-NotContains $pidMissing.Output 'null-valued expression' 'Empty adb output must remain a non-null string.' Write-Host 'MAUI R2R Helix recovery tests passed.' + $suitePassed = $true } finally { Remove-Item Env:FAKE_ADB_SCENARIO -ErrorAction Ignore Remove-Item Env:FAKE_ADB_TRANSCRIPT -ErrorAction Ignore Remove-Item Env:HELIX_WORKITEM_UPLOAD_ROOT -ErrorAction Ignore Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction Ignore } + +if ($suitePassed) { + exit 0 +} diff --git a/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml b/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml index a3f2806d942..b14f94fa066 100644 --- a/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml +++ b/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml @@ -215,7 +215,7 @@ steps: continueOnError: false - powershell: | - & '${{ parameters.xaSourcePath }}/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1' + & '${{ parameters.xaSourcePath }}/build-tools/automation/scripts/RunMauiR2RHelix.Tests.Process.ps1' displayName: Test MAUI R2R Helix device recovery - powershell: | From a6b842f5047a863909972d9f7e2480015eba18e8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 8 Sep 2026 21:53:29 +0200 Subject: [PATCH 3/4] [flaky-ci] Bound Helix device recovery Cap the complete MAUI R2R work item at nine minutes so repository recovery and final error handling finish before Helix's ten-minute timeout. Preserve every repeated ADB device snapshot and retain the command transcript that consumes the overall deadline. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scripts/RunMauiR2RHelix.Tests.ps1 | 25 ++++++- .../automation/scripts/RunMauiR2RHelix.ps1 | 70 +++++++++++++++++-- .../run-maui-r2r-helix-matrix.yaml | 2 +- 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 b/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 index 124e55a4624..9dbc2f2be84 100644 --- a/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 +++ b/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 @@ -64,8 +64,12 @@ Add-Content -LiteralPath $transcriptPath -Value $command -Encoding ASCII $serverRestarted = [bool]($previousCommands | Where-Object { $_ -eq 'start-server' }) $deviceRebooted = [bool]($previousCommands | Where-Object { $_ -match '^-s test-device reboot$' }) $targetUninstalled = [bool]($previousCommands | Where-Object { $_ -match '^-s test-device uninstall com\.xamarin\.mauir2r\.' }) +$deviceQueryCount = @($previousCommands | Where-Object { $_ -eq 'devices -l' }).Count if ($command -eq 'version') { + if ($scenario -eq 'overall-timeout') { + Start-Sleep -Seconds 5 + } Write-Output 'Android Debug Bridge version 1.0.41' exit 0 } @@ -80,6 +84,13 @@ if ($command -eq 'devices -l') { Write-Output 'test-device unauthorized transport_id:1' } } + 'recover-device-after-polls' { + if ($serverRestarted -and $deviceQueryCount -ge 3) { + Write-Output 'test-device device product:test model:Test_Device device:test transport_id:1' + } else { + Write-Output 'test-device unauthorized transport_id:1' + } + } 'unauthorized' { Write-Output 'test-device unauthorized transport_id:1' } @@ -212,7 +223,7 @@ if ($command -eq '-s test-device logcat -d -b all') { exit 2 '@ | Set-Content -LiteralPath $fakeAdb -Encoding ASCII - function Invoke-TestCase ([string] $Name, [int] $ExpectedExitCode, [bool] $SkipDiagnostics = $true) + function Invoke-TestCase ([string] $Name, [int] $ExpectedExitCode, [bool] $SkipDiagnostics = $true, [int] $DeviceTimeoutSeconds = 0, [int] $OverallTimeoutSeconds = 540) { $caseDirectory = Join-Path $testRoot $Name New-Item -ItemType Directory -Path $caseDirectory | Out-Null @@ -235,8 +246,9 @@ exit 2 -ScenarioName $Name ` -ConfigurationName $Name ` -RuntimeIdentifier 'android-arm64' ` - -DeviceRecoveryTimeoutSeconds 0 ` + -DeviceRecoveryTimeoutSeconds $DeviceTimeoutSeconds ` -AndroidReadyTimeoutSeconds 0 ` + -OverallTimeoutSeconds $OverallTimeoutSeconds ` -PollIntervalMilliseconds 0 ` -RebootInitialDelayMilliseconds 0 ` -LaunchWaitMilliseconds 0 ` @@ -268,6 +280,10 @@ exit 2 Assert-MatchCount $recoveredDevice.Transcript '^start-server$' 1 'Device enumeration recovery must restart the ADB server only once.' Assert-MatchCount $recoveredDevice.Transcript ' install -r ' 1 'Recovered device flow must install once.' + $recoveredAfterPolls = Invoke-TestCase 'recover-device-after-polls' 0 $true 5 + $pollSnapshots = @(Get-ChildItem -LiteralPath $recoveredAfterPolls.Directory -Filter 'adb-devices-recover-device-after-polls-post-adb-restart*.log') + Assert-Equal 3 $pollSnapshots.Count 'Each device recovery poll must preserve a separate adb devices snapshot.' + $unauthorized = Invoke-TestCase 'unauthorized' 1 Assert-MatchCount $unauthorized.Transcript '^kill-server$' 1 'Unauthorized device flow must attempt one ADB server restart.' Assert-NotContains $unauthorized.Transcript ' install -r ' 'Persistent unauthorized device flow must fail before installation.' @@ -308,6 +324,11 @@ exit 2 Assert-Contains $pidMissing.Output 'did not remain running after launch' "Empty pidof output must report the smoke-test failure instead of a null-reference error. Output: $($pidMissing.Output -join ' | ')" Assert-NotContains $pidMissing.Output 'null-valued expression' 'Empty adb output must remain a non-null string.' + $overallTimeout = Invoke-TestCase 'overall-timeout' 1 $true 0 1 + Assert-MatchCount $overallTimeout.Transcript '^version$' 1 'The end-to-end deadline must stop the hanging adb command without starting later operations.' + Assert-Contains $overallTimeout.Output 'reached its 1-second end-to-end deadline' 'The end-to-end deadline must fail explicitly before the outer Helix timeout.' + Assert-Equal 1 @(Get-ChildItem -LiteralPath $overallTimeout.Directory -Filter 'overall-deadline-overall-timeout.log').Count 'The command that consumes the end-to-end deadline must retain its diagnostic transcript.' + Write-Host 'MAUI R2R Helix recovery tests passed.' $suitePassed = $true } finally { diff --git a/build-tools/automation/scripts/RunMauiR2RHelix.ps1 b/build-tools/automation/scripts/RunMauiR2RHelix.ps1 index 140937c9d86..cfdb3f77d7b 100644 --- a/build-tools/automation/scripts/RunMauiR2RHelix.ps1 +++ b/build-tools/automation/scripts/RunMauiR2RHelix.ps1 @@ -13,6 +13,8 @@ param ( [string] $RuntimeIdentifier, [int] $DeviceRecoveryTimeoutSeconds = 30, [int] $AndroidReadyTimeoutSeconds = 120, + [ValidateRange(1, 540)] + [int] $OverallTimeoutSeconds = 540, [int] $PollIntervalMilliseconds = 2000, [int] $RebootInitialDelayMilliseconds = 5000, [int] $LaunchWaitMilliseconds = 10000, @@ -43,6 +45,8 @@ $selectedSerial = $null $applicationInstalled = $false $failureDiagnosticsCaptured = $false $exitCode = 0 +$workItemDeadline = [DateTime]::UtcNow.AddSeconds($OverallTimeoutSeconds) +$commandResultCounts = @{} function ConvertTo-ProcessArgument ([string] $Argument) { @@ -66,9 +70,49 @@ function Test-ContainsIgnoreCase ([string] $Value, [string] $Expected) return $Value.IndexOf($Expected, [StringComparison]::OrdinalIgnoreCase) -ge 0 } +function Get-RemainingWorkItemMilliseconds +{ + $remaining = [Math]::Floor(($workItemDeadline - [DateTime]::UtcNow).TotalMilliseconds) + if ($remaining -le 0) { + return 0 + } + return [int][Math]::Min($remaining, [int]::MaxValue) +} + +function Throw-WorkItemDeadlineExceeded ([string] $Operation) +{ + throw "MAUI R2R Helix work item reached its $OverallTimeoutSeconds-second end-to-end deadline while $Operation. No additional recovery was attempted, leaving time for final error handling before the 10-minute Helix timeout." +} + +function Start-BoundedSleep ([int] $Milliseconds, [string] $Operation) +{ + if ($Milliseconds -le 0) { + return + } + + $remaining = Get-RemainingWorkItemMilliseconds + if ($remaining -le 0) { + Throw-WorkItemDeadlineExceeded $Operation + } + + $sleepMilliseconds = [Math]::Min($Milliseconds, $remaining) + Start-Sleep -Milliseconds $sleepMilliseconds + if ($sleepMilliseconds -lt $Milliseconds) { + Throw-WorkItemDeadlineExceeded $Operation + } +} + function Write-CommandResult ([string] $Name, [string []] $Arguments, $Result) { $safeName = $Name -replace '[^A-Za-z0-9_.-]', '-' + $count = 1 + if ($commandResultCounts.ContainsKey($safeName)) { + $count = $commandResultCounts[$safeName] + 1 + } + $script:commandResultCounts[$safeName] = $count + if ($count -gt 1) { + $safeName = "$safeName-attempt-$count" + } $path = Join-Path $uploadDirectory "$safeName.log" $content = @( "Command: adb $($Arguments -join ' ')", @@ -89,6 +133,7 @@ function Invoke-Adb ([string []] $Arguments, [int] $TimeoutSeconds = 30, [switch $stderrPath = Join-Path ([IO.Path]::GetTempPath()) "$([IO.Path]::GetRandomFileName()).stderr" $process = $null $timedOut = $false + $deadlineLimited = $false if ($AdbPath.EndsWith('.ps1', [StringComparison]::OrdinalIgnoreCase)) { $filePath = $powerShellExe @@ -104,10 +149,16 @@ function Invoke-Adb ([string []] $Arguments, [int] $TimeoutSeconds = 30, [switch } try { + if ((Get-RemainingWorkItemMilliseconds) -le 0) { + Throw-WorkItemDeadlineExceeded "starting 'adb $($Arguments -join ' ')'" + } $process = Start-Process -FilePath $filePath -ArgumentList $argumentList -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -NoNewWindow -PassThru # Windows PowerShell requires the process handle to be cached before waiting when output is redirected. $null = $process.Handle - $timeoutMilliseconds = [Math]::Max(1, $TimeoutSeconds * 1000) + $requestedTimeoutMilliseconds = [Math]::Max(1, $TimeoutSeconds * 1000) + $remainingMilliseconds = Get-RemainingWorkItemMilliseconds + $deadlineLimited = $remainingMilliseconds -lt $requestedTimeoutMilliseconds + $timeoutMilliseconds = [Math]::Max(1, [Math]::Min($requestedTimeoutMilliseconds, $remainingMilliseconds)) if (-not $process.WaitForExit($timeoutMilliseconds)) { $timedOut = $true try { @@ -138,13 +189,17 @@ function Invoke-Adb ([string []] $Arguments, [int] $TimeoutSeconds = 30, [switch Write-Host $standardError.TrimEnd() } } - - return [pscustomobject]@{ + $result = [pscustomobject]@{ ExitCode = $commandExitCode StandardOutput = $standardOutput StandardError = $standardError TimedOut = $timedOut } + if ($timedOut -and $deadlineLimited) { + Write-CommandResult "overall-deadline-$ConfigurationName" $Arguments $result + Throw-WorkItemDeadlineExceeded "running 'adb $($Arguments -join ' ')'" + } + return $result } finally { Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction Ignore if ($process) { @@ -266,7 +321,7 @@ function Wait-ForReadyDevice ([int] $TimeoutSeconds, [string] $Name, [string] $E if ([DateTime]::UtcNow -ge $deadline) { return $selection } - Start-Sleep -Milliseconds $PollIntervalMilliseconds + Start-BoundedSleep $PollIntervalMilliseconds 'waiting for an authorized Android device' } while ($true) } @@ -312,7 +367,7 @@ function Wait-ForAndroidReady ([string] $Serial, [string] $Name) Write-CommandResult "android-package-manager-$ConfigurationName-$Name" $packageArguments $packageResult return $false } - Start-Sleep -Milliseconds $PollIntervalMilliseconds + Start-BoundedSleep $PollIntervalMilliseconds 'waiting for Android and its package manager' } while ($true) } @@ -367,7 +422,7 @@ function Restart-AndroidDevice ([string] $Serial) throw "MAUI R2R Helix infrastructure failure: adb reboot failed with exit code $($result.ExitCode)." } - Start-Sleep -Milliseconds $RebootInitialDelayMilliseconds + Start-BoundedSleep $RebootInitialDelayMilliseconds 'waiting for the recovery reboot to begin' $selection = Wait-ForReadyDevice $AndroidReadyTimeoutSeconds 'post-device-reboot' $Serial if (-not $selection.Ready) { throw "MAUI R2R Helix infrastructure failure: Android device '$Serial' did not reconnect after its one recovery reboot. The Helix device-lab owner must repair or quarantine it." @@ -510,6 +565,7 @@ try { "RuntimeIdentifier: $RuntimeIdentifier", "PackageName: $PackageName", "APK: $ApkPath", + "OverallTimeoutSeconds: $OverallTimeoutSeconds", "Machine: $env:COMPUTERNAME" ) | Set-Content -LiteralPath $scenarioPath -Encoding ASCII @@ -543,7 +599,7 @@ try { throw "Launching the MAUI R2R APK failed with exit code $($launchResult.ExitCode)." } - Start-Sleep -Milliseconds $LaunchWaitMilliseconds + Start-BoundedSleep $LaunchWaitMilliseconds 'waiting for the launched MAUI R2R application' $pidArguments = @('-s', $selectedSerial, 'shell', 'pidof', $PackageName) $pidResult = Invoke-Adb $pidArguments -TimeoutSeconds 30 Write-CommandResult "pidof-$ConfigurationName" $pidArguments $pidResult diff --git a/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml b/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml index b14f94fa066..bfc12ab7b2e 100644 --- a/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml +++ b/build-tools/automation/yaml-templates/run-maui-r2r-helix-matrix.yaml @@ -193,7 +193,7 @@ steps: $script = @" @echo on - powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0RunMauiR2RHelix.ps1" -AdbPath "%~dp0platform-tools\adb.exe" -ApkPath "%~dp0$apkName" -PackageName "$packageName" -ScenarioName "${{ testCase.displayName }}" -ConfigurationName "${{ testCase.name }}" -RuntimeIdentifier "$runtimeIdentifier" + powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0RunMauiR2RHelix.ps1" -AdbPath "%~dp0platform-tools\adb.exe" -ApkPath "%~dp0$apkName" -PackageName "$packageName" -ScenarioName "${{ testCase.displayName }}" -ConfigurationName "${{ testCase.name }}" -RuntimeIdentifier "$runtimeIdentifier" -OverallTimeoutSeconds 540 exit /b %ERRORLEVEL% "@ From c7fa676c55b1797a77241c0f47c440240aa9a3a8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 10 Sep 2026 14:14:25 +0200 Subject: [PATCH 4/4] [flaky-ci] Preserve Helix device diagnostics Ignore ADB daemon startup banners when selecting the attached device and keep ADB output UTF-8 through capture and uploaded diagnostics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scripts/RunMauiR2RHelix.Tests.ps1 | 48 +++++++++++++++++-- .../automation/scripts/RunMauiR2RHelix.ps1 | 23 +++++---- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 b/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 index 9dbc2f2be84..164be6ba2a3 100644 --- a/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 +++ b/build-tools/automation/scripts/RunMauiR2RHelix.Tests.ps1 @@ -76,6 +76,10 @@ if ($command -eq 'version') { if ($command -eq 'devices -l') { Write-Output 'List of devices attached' + if ($scenario -eq 'daemon-noise') { + Write-Output '* daemon not running; starting now at tcp:5037' + Write-Output '* daemon started successfully' + } switch ($scenario) { 'recover-device' { if ($serverRestarted) { @@ -122,7 +126,11 @@ if ($command -match '^-s test-device shell pm path android$') { } if ($command -match '^-s test-device shell getprop$') { - Write-Output '[ro.build.fingerprint]: [test/fingerprint]' + if ($scenario -eq 'unicode-diagnostics') { + Write-Output ('[ro.product.name]: [caf' + [string][char]0x00E9 + '-' + [string][char]0x6F22 + ']') + } else { + Write-Output '[ro.build.fingerprint]: [test/fingerprint]' + } exit 0 } @@ -142,12 +150,20 @@ if ($command -match '^-s test-device shell pm list packages -3 -U$') { } if ($command -match '^-s test-device shell pm path com\.xamarin\.mauir2r\.') { - Write-Output 'package:/data/app/maui/base.apk' + if ($scenario -eq 'unicode-diagnostics') { + Write-Output ('package:/data/app/caf' + [string][char]0x00E9 + '-' + [string][char]0x6F22 + '/base.apk') + } else { + Write-Output 'package:/data/app/maui/base.apk' + } exit 0 } if ($command -match '^-s test-device shell dumpsys package com\.xamarin\.mauir2r\.') { - Write-Output 'Package diagnostic output' + if ($scenario -eq 'unicode-diagnostics') { + Write-Output ('Package label: caf' + [string][char]0x00E9 + '-' + [string][char]0x6F22) + } else { + Write-Output 'Package diagnostic output' + } exit 0 } @@ -215,7 +231,11 @@ if ($command -match '^-s test-device shell pidof com\.xamarin\.mauir2r\.') { } if ($command -eq '-s test-device logcat -d -b all') { - Write-Output 'test logcat' + if ($scenario -eq 'unicode-diagnostics') { + Write-Output ('FATAL EXCEPTION: caf' + [string][char]0x00E9 + '-' + [string][char]0x6F22) + } else { + Write-Output 'test logcat' + } exit 0 } @@ -284,6 +304,10 @@ exit 2 $pollSnapshots = @(Get-ChildItem -LiteralPath $recoveredAfterPolls.Directory -Filter 'adb-devices-recover-device-after-polls-post-adb-restart*.log') Assert-Equal 3 $pollSnapshots.Count 'Each device recovery poll must preserve a separate adb devices snapshot.' + $daemonNoise = Invoke-TestCase 'daemon-noise' 0 + Assert-NotContains $daemonNoise.Transcript '^kill-server$' 'ADB daemon startup messages must not be treated as extra or unauthorized devices.' + Assert-MatchCount $daemonNoise.Transcript ' install -r ' 1 'ADB daemon startup messages must not block installation on the one real device.' + $unauthorized = Invoke-TestCase 'unauthorized' 1 Assert-MatchCount $unauthorized.Transcript '^kill-server$' 1 'Unauthorized device flow must attempt one ADB server restart.' Assert-NotContains $unauthorized.Transcript ' install -r ' 'Persistent unauthorized device flow must fail before installation.' @@ -324,6 +348,22 @@ exit 2 Assert-Contains $pidMissing.Output 'did not remain running after launch' "Empty pidof output must report the smoke-test failure instead of a null-reference error. Output: $($pidMissing.Output -join ' | ')" Assert-NotContains $pidMissing.Output 'null-valued expression' 'Empty adb output must remain a non-null string.' + $unicodeDiagnostics = Invoke-TestCase 'unicode-diagnostics' 0 $false + $unicodeText = 'caf' + [string][char]0x00E9 + '-' + [string][char]0x6F22 + $unicodeArtifacts = @( + 'device-state-unicode-diagnostics-initial.log', + 'pm-path-command-unicode-diagnostics.log', + 'pm-path-unicode-diagnostics.log', + 'dumpsys-package-command-unicode-diagnostics.log', + 'dumpsys-package-unicode-diagnostics.log', + 'logcat-command-unicode-diagnostics.log', + 'logcat-unicode-diagnostics.log' + ) + foreach ($artifact in $unicodeArtifacts) { + $content = Get-Content -LiteralPath (Join-Path $unicodeDiagnostics.Directory $artifact) -Raw -Encoding UTF8 + Assert-Contains @($content) ([Regex]::Escape($unicodeText)) "ADB diagnostic artifact '$artifact' must preserve UTF-8 output." + } + $overallTimeout = Invoke-TestCase 'overall-timeout' 1 $true 0 1 Assert-MatchCount $overallTimeout.Transcript '^version$' 1 'The end-to-end deadline must stop the hanging adb command without starting later operations.' Assert-Contains $overallTimeout.Output 'reached its 1-second end-to-end deadline' 'The end-to-end deadline must fail explicitly before the outer Helix timeout.' diff --git a/build-tools/automation/scripts/RunMauiR2RHelix.ps1 b/build-tools/automation/scripts/RunMauiR2RHelix.ps1 index cfdb3f77d7b..c0ee6c2e26c 100644 --- a/build-tools/automation/scripts/RunMauiR2RHelix.ps1 +++ b/build-tools/automation/scripts/RunMauiR2RHelix.ps1 @@ -124,7 +124,7 @@ function Write-CommandResult ([string] $Name, [string []] $Arguments, $Result) '===== stderr =====', $Result.StandardError ) -join [Environment]::NewLine - Set-Content -LiteralPath $path -Value $content -Encoding ASCII + Set-Content -LiteralPath $path -Value $content -Encoding UTF8 } function Invoke-Adb ([string []] $Arguments, [int] $TimeoutSeconds = 30, [switch] $Quiet) @@ -171,8 +171,8 @@ function Invoke-Adb ([string []] $Arguments, [int] $TimeoutSeconds = 30, [switch $process.WaitForExit() } - [string] $standardOutput = if (Test-Path $stdoutPath) { Get-Content -LiteralPath $stdoutPath -Raw } else { "" } - [string] $standardError = if (Test-Path $stderrPath) { Get-Content -LiteralPath $stderrPath -Raw } else { "" } + [string] $standardOutput = if (Test-Path $stdoutPath) { Get-Content -LiteralPath $stdoutPath -Raw -Encoding UTF8 } else { "" } + [string] $standardError = if (Test-Path $stderrPath) { Get-Content -LiteralPath $stderrPath -Raw -Encoding UTF8 } else { "" } if ($null -eq $standardOutput) { $standardOutput = "" } @@ -216,14 +216,17 @@ function Get-AdbDeviceSnapshot ([string] $Name) $devices = @() foreach ($line in ($result.StandardOutput -split '\r?\n')) { - if (-not $line -or $line.StartsWith('List of devices attached', [StringComparison]::OrdinalIgnoreCase)) { + $trimmedLine = $line.Trim() + if (-not $trimmedLine -or + $trimmedLine.StartsWith('List of devices attached', [StringComparison]::OrdinalIgnoreCase) -or + $trimmedLine.StartsWith('*', [StringComparison]::Ordinal)) { continue } - if ($line -match '^\s*(\S+)\s+(\S+)(?:\s+.*)?$') { + if ($trimmedLine -match '^(\S+)\s+(\S+)(?:\s+.*)?$') { $devices += [pscustomobject]@{ Serial = $Matches[1] State = $Matches[2].ToLowerInvariant() - Line = $line.Trim() + Line = $trimmedLine } } } @@ -409,7 +412,7 @@ function Capture-DeviceDiagnostics ([string] $Serial, [string] $Name) } $safeName = $Name -replace '[^A-Za-z0-9_.-]', '-' - Set-Content -LiteralPath (Join-Path $uploadDirectory "device-state-$ConfigurationName-$safeName.log") -Value ($content -join [Environment]::NewLine) -Encoding ASCII + Set-Content -LiteralPath (Join-Path $uploadDirectory "device-state-$ConfigurationName-$safeName.log") -Value ($content -join [Environment]::NewLine) -Encoding UTF8 } function Restart-AndroidDevice ([string] $Serial) @@ -517,7 +520,7 @@ function Verify-MauiR2RPackage ([string] $Serial) $pathArguments = @('-s', $Serial, 'shell', 'pm', 'path', $PackageName) $pathResult = Invoke-Adb $pathArguments -TimeoutSeconds 30 Write-CommandResult "pm-path-command-$ConfigurationName" $pathArguments $pathResult - Set-Content -LiteralPath (Join-Path $uploadDirectory "pm-path-$ConfigurationName.log") -Value $pathResult.StandardOutput -Encoding ASCII + Set-Content -LiteralPath (Join-Path $uploadDirectory "pm-path-$ConfigurationName.log") -Value $pathResult.StandardOutput -Encoding UTF8 if ($pathResult.ExitCode -ne 0 -or -not $pathResult.StandardOutput.Trim().StartsWith('package:', [StringComparison]::Ordinal)) { throw "The MAUI R2R package '$PackageName' was not present after installation." } @@ -525,7 +528,7 @@ function Verify-MauiR2RPackage ([string] $Serial) $dumpsysArguments = @('-s', $Serial, 'shell', 'dumpsys', 'package', $PackageName) $dumpsysResult = Invoke-Adb $dumpsysArguments -TimeoutSeconds 30 -Quiet Write-CommandResult "dumpsys-package-command-$ConfigurationName" $dumpsysArguments $dumpsysResult - Set-Content -LiteralPath (Join-Path $uploadDirectory "dumpsys-package-$ConfigurationName.log") -Value $dumpsysResult.StandardOutput -Encoding ASCII + Set-Content -LiteralPath (Join-Path $uploadDirectory "dumpsys-package-$ConfigurationName.log") -Value $dumpsysResult.StandardOutput -Encoding UTF8 if ($dumpsysResult.ExitCode -ne 0) { Write-Warning "Could not capture dumpsys details for installed package '$PackageName'." } @@ -607,7 +610,7 @@ try { $logcatArguments = @('-s', $selectedSerial, 'logcat', '-d', '-b', 'all') $logcatResult = Invoke-Adb $logcatArguments -TimeoutSeconds 60 -Quiet Write-CommandResult "logcat-command-$ConfigurationName" $logcatArguments $logcatResult - Set-Content -LiteralPath (Join-Path $uploadDirectory "logcat-$ConfigurationName.log") -Value $logcatResult.StandardOutput -Encoding ASCII + Set-Content -LiteralPath (Join-Path $uploadDirectory "logcat-$ConfigurationName.log") -Value $logcatResult.StandardOutput -Encoding UTF8 if ($pidResult.ExitCode -ne 0 -or -not $pidResult.StandardOutput.Trim()) { throw "The MAUI R2R APK did not remain running after launch."