From 1b5466deb2a153e6be779bf609608e3a04ad43bc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 8 Sep 2026 10:59:45 +0200 Subject: [PATCH 1/4] [ci] Make logcat capture non-gating Bound adb device discovery and logcat collection inside a reusable helper so stalled diagnostics preserve partial output without setting the job to SucceededWithIssues. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scripts/CaptureLogcat.Tests.ps1 | 178 +++++++++++++++ .../automation/scripts/CaptureLogcat.ps1 | 203 ++++++++++++++++++ .../yaml-templates/apk-instrumentation.yaml | 22 +- .../yaml-templates/stage-package-tests.yaml | 4 + 4 files changed, 391 insertions(+), 16 deletions(-) create mode 100644 build-tools/automation/scripts/CaptureLogcat.Tests.ps1 create mode 100644 build-tools/automation/scripts/CaptureLogcat.ps1 diff --git a/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 b/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 new file mode 100644 index 00000000000..b75be82b26d --- /dev/null +++ b/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 @@ -0,0 +1,178 @@ +$ErrorActionPreference = 'Stop' + +$captureScript = Join-Path $PSScriptRoot 'CaptureLogcat.ps1' +$powerShellExe = (Get-Process -Id $PID).Path +if (-not $powerShellExe) { + throw 'Could not determine the current PowerShell executable.' +} + +function Assert-True { + param ( + [bool] $Condition, + [string] $Message + ) + + if (-not $Condition) { + throw $Message + } +} + +function Invoke-CaptureTest { + param ( + [string] $Name, + [string] $DevicesMode, + [string] $LogcatMode, + [int] $DeviceTimeoutSeconds = 10, + [int] $LogcatTimeoutSeconds = 10 + ) + + $destination = Join-Path $testDirectory "$Name.txt" + $env:FAKE_ADB_DEVICES_MODE = $DevicesMode + $env:FAKE_ADB_LOGCAT_MODE = $LogcatMode + $stopwatch = [Diagnostics.Stopwatch]::StartNew() + $output = & $powerShellExe ` + -NoLogo ` + -NoProfile ` + -File $captureScript ` + -Destination $destination ` + -AdbPath $fakeAdb ` + -DeviceTimeoutSeconds $DeviceTimeoutSeconds ` + -LogcatTimeoutSeconds $LogcatTimeoutSeconds ` + -TerminationTimeoutSeconds 1 2>&1 + $exitCode = $LASTEXITCODE + $stopwatch.Stop() + + return [PSCustomObject] @{ + Destination = $destination + Elapsed = $stopwatch.Elapsed + ExitCode = $exitCode + Output = ($output | ForEach-Object { "$_" }) -join "`n" + } +} + +$testDirectory = Join-Path ([IO.Path]::GetTempPath()) "CaptureLogcat.Tests-$([IO.Path]::GetRandomFileName())" +$fakeAdbScript = Join-Path $testDirectory 'FakeAdb.ps1' +New-Item -ItemType Directory -Force -Path $testDirectory | Out-Null + +try { + @' +if ($args.Count -eq 0) { + exit 64 +} + +switch ($args[0]) { + 'devices' { + switch ($env:FAKE_ADB_DEVICES_MODE) { + 'device' { + [Console]::Out.WriteLine('List of devices attached') + [Console]::Out.WriteLine('emulator-5570 device product:sdk_gphone64_arm64') + [Console]::Out.Flush() + exit 0 + } + 'none' { + [Console]::Out.WriteLine('List of devices attached') + [Console]::Out.Flush() + exit 0 + } + 'fail' { + [Console]::Error.WriteLine('device probe failed') + [Console]::Error.Flush() + exit 17 + } + 'hang' { + Start-Sleep -Seconds 30 + exit 0 + } + default { + exit 65 + } + } + } + 'logcat' { + switch ($env:FAKE_ADB_LOGCAT_MODE) { + 'success' { + [Console]::Out.WriteLine('complete log line') + [Console]::Out.Flush() + exit 0 + } + 'fail' { + [Console]::Error.WriteLine('logcat failed') + [Console]::Error.Flush() + exit 23 + } + 'hang' { + [Console]::Out.WriteLine('partial log line') + [Console]::Out.Flush() + Start-Sleep -Seconds 30 + exit 0 + } + default { + exit 66 + } + } + } + default { + exit 67 + } +} +'@ | Set-Content -LiteralPath $fakeAdbScript -Encoding ASCII + + if ([Environment]::OSVersion.Platform -eq 'Unix') { + $fakeAdb = Join-Path $testDirectory 'adb' + $wrapper = @' +#!/bin/sh +exec "__POWERSHELL__" -NoLogo -NoProfile -File "__SCRIPT__" "$@" +'@ + $wrapper = $wrapper.Replace('__POWERSHELL__', $powerShellExe.Replace('"', '\"')).Replace('__SCRIPT__', $fakeAdbScript.Replace('"', '\"')) + $wrapper | Set-Content -LiteralPath $fakeAdb -Encoding ASCII + & chmod +x $fakeAdb + if ($LASTEXITCODE -ne 0) { + throw 'Could not make the fake adb executable.' + } + } else { + $fakeAdb = Join-Path $testDirectory 'adb.cmd' + $wrapper = @' +@echo off +"__POWERSHELL__" -NoLogo -NoProfile -File "__SCRIPT__" %* +'@ + $wrapper = $wrapper.Replace('__POWERSHELL__', $powerShellExe).Replace('__SCRIPT__', $fakeAdbScript) + $wrapper | Set-Content -LiteralPath $fakeAdb -Encoding ASCII + } + + $result = Invoke-CaptureTest -Name 'success' -DevicesMode 'device' -LogcatMode 'success' + Assert-True ($result.ExitCode -eq 0) "Successful capture exited with $($result.ExitCode)." + Assert-True ($result.Output -match 'logcat capture completed') "Successful capture did not report completion: $($result.Output)" + Assert-True ((Get-Content -LiteralPath $result.Destination -Raw) -match 'complete log line') 'Successful capture did not preserve logcat output.' + + $result = Invoke-CaptureTest -Name 'no-device' -DevicesMode 'none' -LogcatMode 'success' + Assert-True ($result.ExitCode -eq 0) "No-device capture exited with $($result.ExitCode)." + Assert-True ($result.Output -match 'logcat capture skipped: no connected device') "No-device capture did not report the skip: $($result.Output)" + Assert-True (-not (Test-Path -LiteralPath $result.Destination)) 'No-device capture unexpectedly created a logcat file.' + + $result = Invoke-CaptureTest -Name 'probe-failure' -DevicesMode 'fail' -LogcatMode 'success' + Assert-True ($result.ExitCode -eq 0) "Failed device probe exited with $($result.ExitCode)." + Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*adb devices exited with code 17') "Failed device probe did not emit the expected warning: $($result.Output)" + Assert-True ($result.Output -match 'device probe failed') "Failed device probe did not include stderr: $($result.Output)" + + $result = Invoke-CaptureTest -Name 'probe-timeout' -DevicesMode 'hang' -LogcatMode 'success' -DeviceTimeoutSeconds 1 + Assert-True ($result.ExitCode -eq 0) "Timed-out device probe exited with $($result.ExitCode)." + Assert-True ($result.Elapsed.TotalSeconds -lt 15) "Timed-out device probe took $($result.Elapsed.TotalSeconds) seconds." + Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*adb devices timed out after 1 seconds') "Timed-out device probe did not emit the expected warning: $($result.Output)" + + $result = Invoke-CaptureTest -Name 'logcat-failure' -DevicesMode 'device' -LogcatMode 'fail' + Assert-True ($result.ExitCode -eq 0) "Failed logcat capture exited with $($result.ExitCode)." + Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*logcat capture exited with code 23') "Failed logcat capture did not emit the expected warning: $($result.Output)" + Assert-True ($result.Output -match 'logcat failed') "Failed logcat capture did not include stderr: $($result.Output)" + + $result = Invoke-CaptureTest -Name 'logcat-timeout' -DevicesMode 'device' -LogcatMode 'hang' -LogcatTimeoutSeconds 1 + Assert-True ($result.ExitCode -eq 0) "Timed-out logcat capture exited with $($result.ExitCode)." + Assert-True ($result.Elapsed.TotalSeconds -lt 15) "Timed-out logcat capture took $($result.Elapsed.TotalSeconds) seconds." + Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*logcat capture timed out after 1 seconds') "Timed-out logcat capture did not emit the expected warning: $($result.Output)" + Assert-True ((Get-Content -LiteralPath $result.Destination -Raw) -match 'partial log line') 'Timed-out capture did not preserve partial logcat output.' + + Write-Host 'CaptureLogcat tests passed.' +} finally { + Remove-Item Env:FAKE_ADB_DEVICES_MODE -ErrorAction Ignore + Remove-Item Env:FAKE_ADB_LOGCAT_MODE -ErrorAction Ignore + Remove-Item -LiteralPath $testDirectory -Recurse -Force -ErrorAction Ignore +} diff --git a/build-tools/automation/scripts/CaptureLogcat.ps1 b/build-tools/automation/scripts/CaptureLogcat.ps1 new file mode 100644 index 00000000000..6f802322f81 --- /dev/null +++ b/build-tools/automation/scripts/CaptureLogcat.ps1 @@ -0,0 +1,203 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [string] $Destination, + [string] $AdbPath = 'adb', + [int] $DeviceTimeoutSeconds = 10, + [int] $LogcatTimeoutSeconds = 45, + [int] $TerminationTimeoutSeconds = 2 +) + +$ErrorActionPreference = 'Stop' + +function Write-CaptureWarning { + param ( + [Parameter(Mandatory = $true)] + [string] $Message + ) + + $escapedMessage = $Message.Replace('%', '%AZP25').Replace("`r", '%0D').Replace("`n", '%0A') + Write-Host "##vso[task.logissue type=warning]$escapedMessage" +} + +function Read-ProcessOutput { + param ( + [Parameter(Mandatory = $true)] + [string] $Path + ) + + if (-not (Test-Path -LiteralPath $Path)) { + return '' + } + + $content = Get-Content -LiteralPath $Path -Raw + if ($null -eq $content) { + return '' + } + + return $content.TrimEnd() +} + +function Invoke-BoundedProcess { + param ( + [Parameter(Mandatory = $true)] + [string] $FilePath, + [Parameter(Mandatory = $true)] + [string[]] $Arguments, + [Parameter(Mandatory = $true)] + [string] $StandardOutputPath, + [Parameter(Mandatory = $true)] + [string] $StandardErrorPath, + [Parameter(Mandatory = $true)] + [int] $TimeoutSeconds, + [Parameter(Mandatory = $true)] + [int] $TerminationTimeoutSeconds + ) + + $process = $null + try { + $process = Start-Process -FilePath $FilePath ` + -ArgumentList $Arguments ` + -NoNewWindow ` + -PassThru ` + -RedirectStandardOutput $StandardOutputPath ` + -RedirectStandardError $StandardErrorPath + + $exited = $process.WaitForExit([int] [TimeSpan]::FromSeconds($TimeoutSeconds).TotalMilliseconds) + if ($exited) { + return [PSCustomObject] @{ + ExitCode = $process.ExitCode + TimedOut = $false + TerminationTimedOut = $false + KillError = '' + } + } + + $killError = '' + try { + $process.Kill($true) + } catch [InvalidOperationException] { + # The process exited between the timeout and the kill request. + } catch { + $killError = $_.Exception.Message + } + + $terminated = $process.WaitForExit([int] [TimeSpan]::FromSeconds($TerminationTimeoutSeconds).TotalMilliseconds) + return [PSCustomObject] @{ + ExitCode = $null + TimedOut = $true + TerminationTimedOut = -not $terminated + KillError = $killError + } + } finally { + if ($null -ne $process) { + $process.Dispose() + } + } +} + +function Get-FailureDetails { + param ( + [string] $StandardError, + [string] $KillError, + [bool] $TerminationTimedOut + ) + + $details = @() + if (-not [string]::IsNullOrWhiteSpace($StandardError)) { + $details += "stderr: $StandardError" + } + if (-not [string]::IsNullOrWhiteSpace($KillError)) { + $details += "kill failed: $KillError" + } + if ($TerminationTimedOut) { + $details += "process did not exit within $TerminationTimeoutSeconds seconds after termination" + } + + if ($details.Count -eq 0) { + return '' + } + + return '; ' + ($details -join '; ') +} + +$temporaryPaths = @() + +try { + $devicesOutputPath = [IO.Path]::GetTempFileName() + $devicesErrorPath = [IO.Path]::GetTempFileName() + $logcatErrorPath = [IO.Path]::GetTempFileName() + $temporaryPaths = @($devicesOutputPath, $devicesErrorPath, $logcatErrorPath) + + $destinationDirectory = Split-Path -Parent $Destination + if ([string]::IsNullOrEmpty($destinationDirectory)) { + $destinationDirectory = (Get-Location).Path + } + New-Item -ItemType Directory -Force -Path $destinationDirectory | Out-Null + + $devicesResult = Invoke-BoundedProcess ` + -FilePath $AdbPath ` + -Arguments @('devices') ` + -StandardOutputPath $devicesOutputPath ` + -StandardErrorPath $devicesErrorPath ` + -TimeoutSeconds $DeviceTimeoutSeconds ` + -TerminationTimeoutSeconds $TerminationTimeoutSeconds + $devicesOutput = Read-ProcessOutput -Path $devicesOutputPath + $devicesError = Read-ProcessOutput -Path $devicesErrorPath + + if (-not [string]::IsNullOrWhiteSpace($devicesOutput)) { + Write-Host $devicesOutput + } + if ($devicesResult.TimedOut) { + $details = Get-FailureDetails -StandardError $devicesError -KillError $devicesResult.KillError -TerminationTimedOut $devicesResult.TerminationTimedOut + Write-CaptureWarning "logcat capture skipped: adb devices timed out after $DeviceTimeoutSeconds seconds$details" + exit 0 + } + if ($devicesResult.ExitCode -ne 0) { + $details = Get-FailureDetails -StandardError $devicesError -KillError '' -TerminationTimedOut $false + Write-CaptureWarning "logcat capture skipped: adb devices exited with code $($devicesResult.ExitCode)$details" + exit 0 + } + if (-not [string]::IsNullOrWhiteSpace($devicesError)) { + Write-Host $devicesError + } + + $connectedDevice = $devicesOutput -split '\r?\n' | Where-Object { $_ -match '^\S+\s+device(?:\s|$)' } | Select-Object -First 1 + if ($null -eq $connectedDevice) { + Write-Host 'logcat capture skipped: no connected device' + exit 0 + } + + $logcatResult = Invoke-BoundedProcess ` + -FilePath $AdbPath ` + -Arguments @('logcat', '-d') ` + -StandardOutputPath $Destination ` + -StandardErrorPath $logcatErrorPath ` + -TimeoutSeconds $LogcatTimeoutSeconds ` + -TerminationTimeoutSeconds $TerminationTimeoutSeconds + $logcatError = Read-ProcessOutput -Path $logcatErrorPath + + if ($logcatResult.TimedOut) { + $details = Get-FailureDetails -StandardError $logcatError -KillError $logcatResult.KillError -TerminationTimedOut $logcatResult.TerminationTimedOut + Write-CaptureWarning "logcat capture timed out after $LogcatTimeoutSeconds seconds; partial output was retained at $Destination$details" + exit 0 + } + if ($logcatResult.ExitCode -ne 0) { + $details = Get-FailureDetails -StandardError $logcatError -KillError '' -TerminationTimedOut $false + Write-CaptureWarning "logcat capture exited with code $($logcatResult.ExitCode); partial output was retained at $Destination$details" + exit 0 + } + if (-not [string]::IsNullOrWhiteSpace($logcatError)) { + Write-Host $logcatError + } + + Write-Host "logcat capture completed: $Destination" +} catch { + Write-CaptureWarning "logcat capture failed: $($_.Exception.Message)" +} finally { + if ($temporaryPaths.Count -gt 0) { + Remove-Item -LiteralPath $temporaryPaths -Force -ErrorAction Ignore + } +} + +exit 0 diff --git a/build-tools/automation/yaml-templates/apk-instrumentation.yaml b/build-tools/automation/yaml-templates/apk-instrumentation.yaml index 65f4ae1292c..33d8d20acd4 100644 --- a/build-tools/automation/yaml-templates/apk-instrumentation.yaml +++ b/build-tools/automation/yaml-templates/apk-instrumentation.yaml @@ -82,24 +82,14 @@ steps: # earlier step failed or the job was canceled - in particular a failed -t:Install # (which now fails the lane fast) or a hung/timed-out test run. Losing logcat in # exactly those cases would defeat the diagnostics this template exists for; the - # capture is best-effort and time-bounded. See dotnet/android#11830. -- script: | - DEST="$(Build.StagingDirectory)/Test${{ parameters.configuration }}/${{ parameters.artifactFolder }}/" - mkdir -p "$DEST" - ADB_DEVICES="$(adb devices 2>&1)" - ADB_STATUS=$? - echo "$ADB_DEVICES" - if [ "$ADB_STATUS" -ne 0 ]; then - echo "logcat capture failed: adb devices exited with code $ADB_STATUS" - elif echo "$ADB_DEVICES" | awk 'NR > 1 && $2 == "device" { found=1 } END { exit !found }'; then - adb logcat -d > "$DEST/logcat-${{ parameters.testName }}.txt" || echo "logcat capture failed" - else - echo "logcat capture skipped: no connected device" - fi + # capture is best-effort, internally time-bounded, and does not change the job + # status when diagnostics cannot be collected. See dotnet/android#11830 and + # dotnet/android#12704. +- powershell: | + $destination = "$(Build.StagingDirectory)/Test${{ parameters.configuration }}/${{ parameters.artifactFolder }}/logcat-${{ parameters.testName }}.txt" + & "${{ parameters.xaSourcePath }}/build-tools/automation/scripts/CaptureLogcat.ps1" -Destination $destination displayName: capture logcat ${{ parameters.testName }} condition: always() - continueOnError: true - timeoutInMinutes: 1 - task: PublishTestResults@2 displayName: publish ${{ parameters.testName }} results diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml index caf98d3d1e4..ea80a6d95c5 100644 --- a/build-tools/automation/yaml-templates/stage-package-tests.yaml +++ b/build-tools/automation/yaml-templates/stage-package-tests.yaml @@ -33,6 +33,10 @@ stages: parameters: use1ESTemplate: ${{ parameters.use1ESTemplate }} + - powershell: | + & "$(System.DefaultWorkingDirectory)/build-tools/automation/scripts/CaptureLogcat.Tests.ps1" + displayName: test bounded logcat capture + - task: DownloadPipelineArtifact@2 inputs: artifactName: $(TestAssembliesArtifactName) From fae713e07b808d765d6cb184c16434f146296619 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 8 Sep 2026 12:52:18 +0200 Subject: [PATCH 2/4] [ci] Drain logcat output before disposal Copy redirected adb streams explicitly and give them a bounded completion window after process exit or termination so complete and partial captures are not truncated. Add repeated burst-output and timeout-partial regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scripts/CaptureLogcat.Tests.ps1 | 34 ++++- .../automation/scripts/CaptureLogcat.ps1 | 141 ++++++++++++++---- 2 files changed, 139 insertions(+), 36 deletions(-) diff --git a/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 b/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 index b75be82b26d..a5633e00e85 100644 --- a/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 +++ b/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 @@ -38,7 +38,8 @@ function Invoke-CaptureTest { -AdbPath $fakeAdb ` -DeviceTimeoutSeconds $DeviceTimeoutSeconds ` -LogcatTimeoutSeconds $LogcatTimeoutSeconds ` - -TerminationTimeoutSeconds 1 2>&1 + -TerminationTimeoutSeconds 1 ` + -OutputDrainTimeoutSeconds 2 2>&1 $exitCode = $LASTEXITCODE $stopwatch.Stop() @@ -95,13 +96,24 @@ switch ($args[0]) { [Console]::Out.Flush() exit 0 } + 'burst' { + for ($i = 1; $i -le 2000; $i++) { + [Console]::Out.WriteLine("burst log line $i") + } + [Console]::Out.Flush() + [Console]::Error.WriteLine('burst stderr marker') + [Console]::Error.Flush() + exit 0 + } 'fail' { [Console]::Error.WriteLine('logcat failed') [Console]::Error.Flush() exit 23 } 'hang' { - [Console]::Out.WriteLine('partial log line') + for ($i = 1; $i -le 100; $i++) { + [Console]::Out.WriteLine("partial log line $i") + } [Console]::Out.Flush() Start-Sleep -Seconds 30 exit 0 @@ -144,6 +156,18 @@ exec "__POWERSHELL__" -NoLogo -NoProfile -File "__SCRIPT__" "$@" Assert-True ($result.Output -match 'logcat capture completed') "Successful capture did not report completion: $($result.Output)" Assert-True ((Get-Content -LiteralPath $result.Destination -Raw) -match 'complete log line') 'Successful capture did not preserve logcat output.' + for ($iteration = 1; $iteration -le 5; $iteration++) { + $result = Invoke-CaptureTest -Name "burst-$iteration" -DevicesMode 'device' -LogcatMode 'burst' + Assert-True ($result.ExitCode -eq 0) "Burst capture $iteration exited with $($result.ExitCode)." + Assert-True ($result.Output -match 'logcat capture completed') "Burst capture $iteration did not report completion: $($result.Output)" + Assert-True ($result.Output -match 'burst stderr marker') "Burst capture $iteration did not preserve stderr: $($result.Output)" + $burstLines = @(Get-Content -LiteralPath $result.Destination) + Assert-True ($burstLines.Count -eq 2000) "Burst capture $iteration preserved $($burstLines.Count) of 2000 lines." + for ($line = 1; $line -le 2000; $line++) { + Assert-True ($burstLines[$line - 1] -eq "burst log line $line") "Burst capture $iteration had unexpected output at line $line." + } + } + $result = Invoke-CaptureTest -Name 'no-device' -DevicesMode 'none' -LogcatMode 'success' Assert-True ($result.ExitCode -eq 0) "No-device capture exited with $($result.ExitCode)." Assert-True ($result.Output -match 'logcat capture skipped: no connected device') "No-device capture did not report the skip: $($result.Output)" @@ -168,7 +192,11 @@ exec "__POWERSHELL__" -NoLogo -NoProfile -File "__SCRIPT__" "$@" Assert-True ($result.ExitCode -eq 0) "Timed-out logcat capture exited with $($result.ExitCode)." Assert-True ($result.Elapsed.TotalSeconds -lt 15) "Timed-out logcat capture took $($result.Elapsed.TotalSeconds) seconds." Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*logcat capture timed out after 1 seconds') "Timed-out logcat capture did not emit the expected warning: $($result.Output)" - Assert-True ((Get-Content -LiteralPath $result.Destination -Raw) -match 'partial log line') 'Timed-out capture did not preserve partial logcat output.' + $partialLines = @(Get-Content -LiteralPath $result.Destination) + Assert-True ($partialLines.Count -eq 100) "Timed-out capture preserved $($partialLines.Count) of 100 partial lines." + for ($line = 1; $line -le 100; $line++) { + Assert-True ($partialLines[$line - 1] -eq "partial log line $line") "Timed-out capture had unexpected partial output at line $line." + } Write-Host 'CaptureLogcat tests passed.' } finally { diff --git a/build-tools/automation/scripts/CaptureLogcat.ps1 b/build-tools/automation/scripts/CaptureLogcat.ps1 index 6f802322f81..87d9a1858ec 100644 --- a/build-tools/automation/scripts/CaptureLogcat.ps1 +++ b/build-tools/automation/scripts/CaptureLogcat.ps1 @@ -5,7 +5,8 @@ param ( [string] $AdbPath = 'adb', [int] $DeviceTimeoutSeconds = 10, [int] $LogcatTimeoutSeconds = 45, - [int] $TerminationTimeoutSeconds = 2 + [int] $TerminationTimeoutSeconds = 2, + [int] $OutputDrainTimeoutSeconds = 5 ) $ErrorActionPreference = 'Stop' @@ -38,6 +39,36 @@ function Read-ProcessOutput { return $content.TrimEnd() } +function Wait-ForOutputDrain { + param ( + [Parameter(Mandatory = $true)] + [Threading.Tasks.Task[]] $Tasks, + [Parameter(Mandatory = $true)] + [int] $TimeoutSeconds + ) + + $drainTask = [Threading.Tasks.Task]::WhenAll($Tasks) + try { + if (-not $drainTask.Wait([int] [TimeSpan]::FromSeconds($TimeoutSeconds).TotalMilliseconds)) { + return [PSCustomObject] @{ + TimedOut = $true + Error = '' + } + } + } catch [AggregateException] { + $errorMessage = ($_.Exception.Flatten().InnerExceptions | ForEach-Object { $_.Message }) -join '; ' + return [PSCustomObject] @{ + TimedOut = $false + Error = $errorMessage + } + } + + return [PSCustomObject] @{ + TimedOut = $false + Error = '' + } +} + function Invoke-BoundedProcess { param ( [Parameter(Mandatory = $true)] @@ -51,45 +82,69 @@ function Invoke-BoundedProcess { [Parameter(Mandatory = $true)] [int] $TimeoutSeconds, [Parameter(Mandatory = $true)] - [int] $TerminationTimeoutSeconds + [int] $TerminationTimeoutSeconds, + [Parameter(Mandatory = $true)] + [int] $OutputDrainTimeoutSeconds ) $process = $null + $standardOutput = $null + $standardError = $null try { - $process = Start-Process -FilePath $FilePath ` - -ArgumentList $Arguments ` - -NoNewWindow ` - -PassThru ` - -RedirectStandardOutput $StandardOutputPath ` - -RedirectStandardError $StandardErrorPath + $startInfo = [Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = $FilePath + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + foreach ($argument in $Arguments) { + $startInfo.ArgumentList.Add($argument) + } - $exited = $process.WaitForExit([int] [TimeSpan]::FromSeconds($TimeoutSeconds).TotalMilliseconds) - if ($exited) { - return [PSCustomObject] @{ - ExitCode = $process.ExitCode - TimedOut = $false - TerminationTimedOut = $false - KillError = '' - } + $standardOutput = [IO.File]::Open($StandardOutputPath, [IO.FileMode]::Create, [IO.FileAccess]::Write, [IO.FileShare]::Read) + $standardError = [IO.File]::Open($StandardErrorPath, [IO.FileMode]::Create, [IO.FileAccess]::Write, [IO.FileShare]::Read) + $process = [Diagnostics.Process]::new() + $process.StartInfo = $startInfo + if (-not $process.Start()) { + throw "Failed to start '$FilePath'." } + $standardOutputTask = $process.StandardOutput.BaseStream.CopyToAsync($standardOutput) + $standardErrorTask = $process.StandardError.BaseStream.CopyToAsync($standardError) + + $exited = $process.WaitForExit([int] [TimeSpan]::FromSeconds($TimeoutSeconds).TotalMilliseconds) $killError = '' - try { - $process.Kill($true) - } catch [InvalidOperationException] { - # The process exited between the timeout and the kill request. - } catch { - $killError = $_.Exception.Message + $terminationTimedOut = $false + if (-not $exited) { + try { + $process.Kill($true) + } catch [InvalidOperationException] { + # The process exited between the timeout and the kill request. + } catch { + $killError = $_.Exception.Message + } + + $terminationTimedOut = -not $process.WaitForExit([int] [TimeSpan]::FromSeconds($TerminationTimeoutSeconds).TotalMilliseconds) } - $terminated = $process.WaitForExit([int] [TimeSpan]::FromSeconds($TerminationTimeoutSeconds).TotalMilliseconds) + $outputDrain = Wait-ForOutputDrain ` + -Tasks @($standardOutputTask, $standardErrorTask) ` + -TimeoutSeconds $OutputDrainTimeoutSeconds return [PSCustomObject] @{ - ExitCode = $null - TimedOut = $true - TerminationTimedOut = -not $terminated + ExitCode = if ($exited) { $process.ExitCode } else { $null } + TimedOut = -not $exited + TerminationTimedOut = $terminationTimedOut KillError = $killError + OutputDrainTimedOut = $outputDrain.TimedOut + OutputDrainError = $outputDrain.Error } } finally { + if ($null -ne $standardOutput) { + $standardOutput.Dispose() + } + if ($null -ne $standardError) { + $standardError.Dispose() + } if ($null -ne $process) { $process.Dispose() } @@ -100,7 +155,9 @@ function Get-FailureDetails { param ( [string] $StandardError, [string] $KillError, - [bool] $TerminationTimedOut + [bool] $TerminationTimedOut, + [bool] $OutputDrainTimedOut, + [string] $OutputDrainError ) $details = @() @@ -113,6 +170,12 @@ function Get-FailureDetails { if ($TerminationTimedOut) { $details += "process did not exit within $TerminationTimeoutSeconds seconds after termination" } + if ($OutputDrainTimedOut) { + $details += "output did not finish draining within $OutputDrainTimeoutSeconds seconds" + } + if (-not [string]::IsNullOrWhiteSpace($OutputDrainError)) { + $details += "output drain failed: $OutputDrainError" + } if ($details.Count -eq 0) { return '' @@ -141,7 +204,8 @@ try { -StandardOutputPath $devicesOutputPath ` -StandardErrorPath $devicesErrorPath ` -TimeoutSeconds $DeviceTimeoutSeconds ` - -TerminationTimeoutSeconds $TerminationTimeoutSeconds + -TerminationTimeoutSeconds $TerminationTimeoutSeconds ` + -OutputDrainTimeoutSeconds $OutputDrainTimeoutSeconds $devicesOutput = Read-ProcessOutput -Path $devicesOutputPath $devicesError = Read-ProcessOutput -Path $devicesErrorPath @@ -149,15 +213,20 @@ try { Write-Host $devicesOutput } if ($devicesResult.TimedOut) { - $details = Get-FailureDetails -StandardError $devicesError -KillError $devicesResult.KillError -TerminationTimedOut $devicesResult.TerminationTimedOut + $details = Get-FailureDetails -StandardError $devicesError -KillError $devicesResult.KillError -TerminationTimedOut $devicesResult.TerminationTimedOut -OutputDrainTimedOut $devicesResult.OutputDrainTimedOut -OutputDrainError $devicesResult.OutputDrainError Write-CaptureWarning "logcat capture skipped: adb devices timed out after $DeviceTimeoutSeconds seconds$details" exit 0 } if ($devicesResult.ExitCode -ne 0) { - $details = Get-FailureDetails -StandardError $devicesError -KillError '' -TerminationTimedOut $false + $details = Get-FailureDetails -StandardError $devicesError -KillError '' -TerminationTimedOut $false -OutputDrainTimedOut $devicesResult.OutputDrainTimedOut -OutputDrainError $devicesResult.OutputDrainError Write-CaptureWarning "logcat capture skipped: adb devices exited with code $($devicesResult.ExitCode)$details" exit 0 } + if ($devicesResult.OutputDrainTimedOut -or -not [string]::IsNullOrWhiteSpace($devicesResult.OutputDrainError)) { + $details = Get-FailureDetails -StandardError $devicesError -KillError '' -TerminationTimedOut $false -OutputDrainTimedOut $devicesResult.OutputDrainTimedOut -OutputDrainError $devicesResult.OutputDrainError + Write-CaptureWarning "logcat capture skipped: adb devices output was incomplete$details" + exit 0 + } if (-not [string]::IsNullOrWhiteSpace($devicesError)) { Write-Host $devicesError } @@ -174,19 +243,25 @@ try { -StandardOutputPath $Destination ` -StandardErrorPath $logcatErrorPath ` -TimeoutSeconds $LogcatTimeoutSeconds ` - -TerminationTimeoutSeconds $TerminationTimeoutSeconds + -TerminationTimeoutSeconds $TerminationTimeoutSeconds ` + -OutputDrainTimeoutSeconds $OutputDrainTimeoutSeconds $logcatError = Read-ProcessOutput -Path $logcatErrorPath if ($logcatResult.TimedOut) { - $details = Get-FailureDetails -StandardError $logcatError -KillError $logcatResult.KillError -TerminationTimedOut $logcatResult.TerminationTimedOut + $details = Get-FailureDetails -StandardError $logcatError -KillError $logcatResult.KillError -TerminationTimedOut $logcatResult.TerminationTimedOut -OutputDrainTimedOut $logcatResult.OutputDrainTimedOut -OutputDrainError $logcatResult.OutputDrainError Write-CaptureWarning "logcat capture timed out after $LogcatTimeoutSeconds seconds; partial output was retained at $Destination$details" exit 0 } if ($logcatResult.ExitCode -ne 0) { - $details = Get-FailureDetails -StandardError $logcatError -KillError '' -TerminationTimedOut $false + $details = Get-FailureDetails -StandardError $logcatError -KillError '' -TerminationTimedOut $false -OutputDrainTimedOut $logcatResult.OutputDrainTimedOut -OutputDrainError $logcatResult.OutputDrainError Write-CaptureWarning "logcat capture exited with code $($logcatResult.ExitCode); partial output was retained at $Destination$details" exit 0 } + if ($logcatResult.OutputDrainTimedOut -or -not [string]::IsNullOrWhiteSpace($logcatResult.OutputDrainError)) { + $details = Get-FailureDetails -StandardError $logcatError -KillError '' -TerminationTimedOut $false -OutputDrainTimedOut $logcatResult.OutputDrainTimedOut -OutputDrainError $logcatResult.OutputDrainError + Write-CaptureWarning "logcat capture output was incomplete; partial output was retained at $Destination$details" + exit 0 + } if (-not [string]::IsNullOrWhiteSpace($logcatError)) { Write-Host $logcatError } From 24d2a3c26ade3a76f52cfc8f0298f7ed05df8334 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 8 Sep 2026 21:48:28 +0200 Subject: [PATCH 3/4] [ci] Test inherited logcat output pipes Cover a fake adb descendant that inherits stdout after its parent exits, proving the output drain remains bounded and preserves output written before the pipe stays open. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scripts/CaptureLogcat.Tests.ps1 | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 b/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 index a5633e00e85..e6ef55bbca3 100644 --- a/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 +++ b/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 @@ -53,6 +53,7 @@ function Invoke-CaptureTest { $testDirectory = Join-Path ([IO.Path]::GetTempPath()) "CaptureLogcat.Tests-$([IO.Path]::GetRandomFileName())" $fakeAdbScript = Join-Path $testDirectory 'FakeAdb.ps1' +$descendantPidPath = Join-Path $testDirectory 'descendant.pid' New-Item -ItemType Directory -Force -Path $testDirectory | Out-Null try { @@ -105,6 +106,26 @@ switch ($args[0]) { [Console]::Error.Flush() exit 0 } + 'inherited-pipe' { + $descendantInfo = [Diagnostics.ProcessStartInfo]::new() + $descendantInfo.FileName = (Get-Process -Id $PID).Path + $descendantInfo.UseShellExecute = $false + $descendantInfo.CreateNoWindow = $true + $descendantInfo.ArgumentList.Add('-NoLogo') + $descendantInfo.ArgumentList.Add('-NoProfile') + $descendantInfo.ArgumentList.Add('-Command') + $descendantInfo.ArgumentList.Add('[Console]::Out.WriteLine(''descendant inherited output''); [Console]::Out.Flush(); Start-Sleep -Seconds 30') + $descendant = [Diagnostics.Process]::new() + $descendant.StartInfo = $descendantInfo + if (-not $descendant.Start()) { + exit 68 + } + Set-Content -LiteralPath $env:FAKE_ADB_DESCENDANT_PID_PATH -Value $descendant.Id -Encoding ASCII + $descendant.Dispose() + [Console]::Out.WriteLine('parent output before inherited-pipe exit') + [Console]::Out.Flush() + exit 0 + } 'fail' { [Console]::Error.WriteLine('logcat failed') [Console]::Error.Flush() @@ -168,6 +189,14 @@ exec "__POWERSHELL__" -NoLogo -NoProfile -File "__SCRIPT__" "$@" } } + $env:FAKE_ADB_DESCENDANT_PID_PATH = $descendantPidPath + $result = Invoke-CaptureTest -Name 'inherited-pipe' -DevicesMode 'device' -LogcatMode 'inherited-pipe' + Assert-True ($result.ExitCode -eq 0) "Inherited-pipe capture exited with $($result.ExitCode)." + Assert-True ($result.Elapsed.TotalSeconds -lt 15) "Inherited-pipe capture took $($result.Elapsed.TotalSeconds) seconds." + Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*logcat capture output was incomplete') "Inherited-pipe capture did not emit the expected warning: $($result.Output)" + Assert-True ($result.Output -match 'output did not finish draining within 2 seconds') "Inherited-pipe capture did not exercise the bounded drain timeout: $($result.Output)" + Assert-True ((Get-Content -LiteralPath $result.Destination -Raw) -match 'parent output before inherited-pipe exit') 'Inherited-pipe capture did not preserve output written before the parent exited.' + $result = Invoke-CaptureTest -Name 'no-device' -DevicesMode 'none' -LogcatMode 'success' Assert-True ($result.ExitCode -eq 0) "No-device capture exited with $($result.ExitCode)." Assert-True ($result.Output -match 'logcat capture skipped: no connected device') "No-device capture did not report the skip: $($result.Output)" @@ -202,5 +231,25 @@ exec "__POWERSHELL__" -NoLogo -NoProfile -File "__SCRIPT__" "$@" } finally { Remove-Item Env:FAKE_ADB_DEVICES_MODE -ErrorAction Ignore Remove-Item Env:FAKE_ADB_LOGCAT_MODE -ErrorAction Ignore + Remove-Item Env:FAKE_ADB_DESCENDANT_PID_PATH -ErrorAction Ignore + if (Test-Path -LiteralPath $descendantPidPath) { + $descendantProcessId = [int] (Get-Content -LiteralPath $descendantPidPath -Raw) + $descendantProcess = $null + try { + $descendantProcess = [Diagnostics.Process]::GetProcessById($descendantProcessId) + if (-not $descendantProcess.HasExited) { + $descendantProcess.Kill($true) + $descendantProcess.WaitForExit(5000) | Out-Null + } + } catch [ArgumentException] { + # The descendant already exited after its inherited pipe was closed. + } catch [InvalidOperationException] { + # The descendant exited between checking its state and terminating it. + } finally { + if ($null -ne $descendantProcess) { + $descendantProcess.Dispose() + } + } + } Remove-Item -LiteralPath $testDirectory -Recurse -Force -ErrorAction Ignore } From dfd031c88f94af3ae9f8dba97989d8380efdd63f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 10 Sep 2026 11:50:09 +0200 Subject: [PATCH 4/4] [ci] Remove standalone logcat helper tests The production helper runs in every APK instrumentation lane, so avoid adding a separate fake-adb test step to each package-test pipeline run. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scripts/CaptureLogcat.Tests.ps1 | 255 ------------------ .../yaml-templates/stage-package-tests.yaml | 4 - 2 files changed, 259 deletions(-) delete mode 100644 build-tools/automation/scripts/CaptureLogcat.Tests.ps1 diff --git a/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 b/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 deleted file mode 100644 index e6ef55bbca3..00000000000 --- a/build-tools/automation/scripts/CaptureLogcat.Tests.ps1 +++ /dev/null @@ -1,255 +0,0 @@ -$ErrorActionPreference = 'Stop' - -$captureScript = Join-Path $PSScriptRoot 'CaptureLogcat.ps1' -$powerShellExe = (Get-Process -Id $PID).Path -if (-not $powerShellExe) { - throw 'Could not determine the current PowerShell executable.' -} - -function Assert-True { - param ( - [bool] $Condition, - [string] $Message - ) - - if (-not $Condition) { - throw $Message - } -} - -function Invoke-CaptureTest { - param ( - [string] $Name, - [string] $DevicesMode, - [string] $LogcatMode, - [int] $DeviceTimeoutSeconds = 10, - [int] $LogcatTimeoutSeconds = 10 - ) - - $destination = Join-Path $testDirectory "$Name.txt" - $env:FAKE_ADB_DEVICES_MODE = $DevicesMode - $env:FAKE_ADB_LOGCAT_MODE = $LogcatMode - $stopwatch = [Diagnostics.Stopwatch]::StartNew() - $output = & $powerShellExe ` - -NoLogo ` - -NoProfile ` - -File $captureScript ` - -Destination $destination ` - -AdbPath $fakeAdb ` - -DeviceTimeoutSeconds $DeviceTimeoutSeconds ` - -LogcatTimeoutSeconds $LogcatTimeoutSeconds ` - -TerminationTimeoutSeconds 1 ` - -OutputDrainTimeoutSeconds 2 2>&1 - $exitCode = $LASTEXITCODE - $stopwatch.Stop() - - return [PSCustomObject] @{ - Destination = $destination - Elapsed = $stopwatch.Elapsed - ExitCode = $exitCode - Output = ($output | ForEach-Object { "$_" }) -join "`n" - } -} - -$testDirectory = Join-Path ([IO.Path]::GetTempPath()) "CaptureLogcat.Tests-$([IO.Path]::GetRandomFileName())" -$fakeAdbScript = Join-Path $testDirectory 'FakeAdb.ps1' -$descendantPidPath = Join-Path $testDirectory 'descendant.pid' -New-Item -ItemType Directory -Force -Path $testDirectory | Out-Null - -try { - @' -if ($args.Count -eq 0) { - exit 64 -} - -switch ($args[0]) { - 'devices' { - switch ($env:FAKE_ADB_DEVICES_MODE) { - 'device' { - [Console]::Out.WriteLine('List of devices attached') - [Console]::Out.WriteLine('emulator-5570 device product:sdk_gphone64_arm64') - [Console]::Out.Flush() - exit 0 - } - 'none' { - [Console]::Out.WriteLine('List of devices attached') - [Console]::Out.Flush() - exit 0 - } - 'fail' { - [Console]::Error.WriteLine('device probe failed') - [Console]::Error.Flush() - exit 17 - } - 'hang' { - Start-Sleep -Seconds 30 - exit 0 - } - default { - exit 65 - } - } - } - 'logcat' { - switch ($env:FAKE_ADB_LOGCAT_MODE) { - 'success' { - [Console]::Out.WriteLine('complete log line') - [Console]::Out.Flush() - exit 0 - } - 'burst' { - for ($i = 1; $i -le 2000; $i++) { - [Console]::Out.WriteLine("burst log line $i") - } - [Console]::Out.Flush() - [Console]::Error.WriteLine('burst stderr marker') - [Console]::Error.Flush() - exit 0 - } - 'inherited-pipe' { - $descendantInfo = [Diagnostics.ProcessStartInfo]::new() - $descendantInfo.FileName = (Get-Process -Id $PID).Path - $descendantInfo.UseShellExecute = $false - $descendantInfo.CreateNoWindow = $true - $descendantInfo.ArgumentList.Add('-NoLogo') - $descendantInfo.ArgumentList.Add('-NoProfile') - $descendantInfo.ArgumentList.Add('-Command') - $descendantInfo.ArgumentList.Add('[Console]::Out.WriteLine(''descendant inherited output''); [Console]::Out.Flush(); Start-Sleep -Seconds 30') - $descendant = [Diagnostics.Process]::new() - $descendant.StartInfo = $descendantInfo - if (-not $descendant.Start()) { - exit 68 - } - Set-Content -LiteralPath $env:FAKE_ADB_DESCENDANT_PID_PATH -Value $descendant.Id -Encoding ASCII - $descendant.Dispose() - [Console]::Out.WriteLine('parent output before inherited-pipe exit') - [Console]::Out.Flush() - exit 0 - } - 'fail' { - [Console]::Error.WriteLine('logcat failed') - [Console]::Error.Flush() - exit 23 - } - 'hang' { - for ($i = 1; $i -le 100; $i++) { - [Console]::Out.WriteLine("partial log line $i") - } - [Console]::Out.Flush() - Start-Sleep -Seconds 30 - exit 0 - } - default { - exit 66 - } - } - } - default { - exit 67 - } -} -'@ | Set-Content -LiteralPath $fakeAdbScript -Encoding ASCII - - if ([Environment]::OSVersion.Platform -eq 'Unix') { - $fakeAdb = Join-Path $testDirectory 'adb' - $wrapper = @' -#!/bin/sh -exec "__POWERSHELL__" -NoLogo -NoProfile -File "__SCRIPT__" "$@" -'@ - $wrapper = $wrapper.Replace('__POWERSHELL__', $powerShellExe.Replace('"', '\"')).Replace('__SCRIPT__', $fakeAdbScript.Replace('"', '\"')) - $wrapper | Set-Content -LiteralPath $fakeAdb -Encoding ASCII - & chmod +x $fakeAdb - if ($LASTEXITCODE -ne 0) { - throw 'Could not make the fake adb executable.' - } - } else { - $fakeAdb = Join-Path $testDirectory 'adb.cmd' - $wrapper = @' -@echo off -"__POWERSHELL__" -NoLogo -NoProfile -File "__SCRIPT__" %* -'@ - $wrapper = $wrapper.Replace('__POWERSHELL__', $powerShellExe).Replace('__SCRIPT__', $fakeAdbScript) - $wrapper | Set-Content -LiteralPath $fakeAdb -Encoding ASCII - } - - $result = Invoke-CaptureTest -Name 'success' -DevicesMode 'device' -LogcatMode 'success' - Assert-True ($result.ExitCode -eq 0) "Successful capture exited with $($result.ExitCode)." - Assert-True ($result.Output -match 'logcat capture completed') "Successful capture did not report completion: $($result.Output)" - Assert-True ((Get-Content -LiteralPath $result.Destination -Raw) -match 'complete log line') 'Successful capture did not preserve logcat output.' - - for ($iteration = 1; $iteration -le 5; $iteration++) { - $result = Invoke-CaptureTest -Name "burst-$iteration" -DevicesMode 'device' -LogcatMode 'burst' - Assert-True ($result.ExitCode -eq 0) "Burst capture $iteration exited with $($result.ExitCode)." - Assert-True ($result.Output -match 'logcat capture completed') "Burst capture $iteration did not report completion: $($result.Output)" - Assert-True ($result.Output -match 'burst stderr marker') "Burst capture $iteration did not preserve stderr: $($result.Output)" - $burstLines = @(Get-Content -LiteralPath $result.Destination) - Assert-True ($burstLines.Count -eq 2000) "Burst capture $iteration preserved $($burstLines.Count) of 2000 lines." - for ($line = 1; $line -le 2000; $line++) { - Assert-True ($burstLines[$line - 1] -eq "burst log line $line") "Burst capture $iteration had unexpected output at line $line." - } - } - - $env:FAKE_ADB_DESCENDANT_PID_PATH = $descendantPidPath - $result = Invoke-CaptureTest -Name 'inherited-pipe' -DevicesMode 'device' -LogcatMode 'inherited-pipe' - Assert-True ($result.ExitCode -eq 0) "Inherited-pipe capture exited with $($result.ExitCode)." - Assert-True ($result.Elapsed.TotalSeconds -lt 15) "Inherited-pipe capture took $($result.Elapsed.TotalSeconds) seconds." - Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*logcat capture output was incomplete') "Inherited-pipe capture did not emit the expected warning: $($result.Output)" - Assert-True ($result.Output -match 'output did not finish draining within 2 seconds') "Inherited-pipe capture did not exercise the bounded drain timeout: $($result.Output)" - Assert-True ((Get-Content -LiteralPath $result.Destination -Raw) -match 'parent output before inherited-pipe exit') 'Inherited-pipe capture did not preserve output written before the parent exited.' - - $result = Invoke-CaptureTest -Name 'no-device' -DevicesMode 'none' -LogcatMode 'success' - Assert-True ($result.ExitCode -eq 0) "No-device capture exited with $($result.ExitCode)." - Assert-True ($result.Output -match 'logcat capture skipped: no connected device') "No-device capture did not report the skip: $($result.Output)" - Assert-True (-not (Test-Path -LiteralPath $result.Destination)) 'No-device capture unexpectedly created a logcat file.' - - $result = Invoke-CaptureTest -Name 'probe-failure' -DevicesMode 'fail' -LogcatMode 'success' - Assert-True ($result.ExitCode -eq 0) "Failed device probe exited with $($result.ExitCode)." - Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*adb devices exited with code 17') "Failed device probe did not emit the expected warning: $($result.Output)" - Assert-True ($result.Output -match 'device probe failed') "Failed device probe did not include stderr: $($result.Output)" - - $result = Invoke-CaptureTest -Name 'probe-timeout' -DevicesMode 'hang' -LogcatMode 'success' -DeviceTimeoutSeconds 1 - Assert-True ($result.ExitCode -eq 0) "Timed-out device probe exited with $($result.ExitCode)." - Assert-True ($result.Elapsed.TotalSeconds -lt 15) "Timed-out device probe took $($result.Elapsed.TotalSeconds) seconds." - Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*adb devices timed out after 1 seconds') "Timed-out device probe did not emit the expected warning: $($result.Output)" - - $result = Invoke-CaptureTest -Name 'logcat-failure' -DevicesMode 'device' -LogcatMode 'fail' - Assert-True ($result.ExitCode -eq 0) "Failed logcat capture exited with $($result.ExitCode)." - Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*logcat capture exited with code 23') "Failed logcat capture did not emit the expected warning: $($result.Output)" - Assert-True ($result.Output -match 'logcat failed') "Failed logcat capture did not include stderr: $($result.Output)" - - $result = Invoke-CaptureTest -Name 'logcat-timeout' -DevicesMode 'device' -LogcatMode 'hang' -LogcatTimeoutSeconds 1 - Assert-True ($result.ExitCode -eq 0) "Timed-out logcat capture exited with $($result.ExitCode)." - Assert-True ($result.Elapsed.TotalSeconds -lt 15) "Timed-out logcat capture took $($result.Elapsed.TotalSeconds) seconds." - Assert-True ($result.Output -match '##vso\[task.logissue type=warning\].*logcat capture timed out after 1 seconds') "Timed-out logcat capture did not emit the expected warning: $($result.Output)" - $partialLines = @(Get-Content -LiteralPath $result.Destination) - Assert-True ($partialLines.Count -eq 100) "Timed-out capture preserved $($partialLines.Count) of 100 partial lines." - for ($line = 1; $line -le 100; $line++) { - Assert-True ($partialLines[$line - 1] -eq "partial log line $line") "Timed-out capture had unexpected partial output at line $line." - } - - Write-Host 'CaptureLogcat tests passed.' -} finally { - Remove-Item Env:FAKE_ADB_DEVICES_MODE -ErrorAction Ignore - Remove-Item Env:FAKE_ADB_LOGCAT_MODE -ErrorAction Ignore - Remove-Item Env:FAKE_ADB_DESCENDANT_PID_PATH -ErrorAction Ignore - if (Test-Path -LiteralPath $descendantPidPath) { - $descendantProcessId = [int] (Get-Content -LiteralPath $descendantPidPath -Raw) - $descendantProcess = $null - try { - $descendantProcess = [Diagnostics.Process]::GetProcessById($descendantProcessId) - if (-not $descendantProcess.HasExited) { - $descendantProcess.Kill($true) - $descendantProcess.WaitForExit(5000) | Out-Null - } - } catch [ArgumentException] { - # The descendant already exited after its inherited pipe was closed. - } catch [InvalidOperationException] { - # The descendant exited between checking its state and terminating it. - } finally { - if ($null -ne $descendantProcess) { - $descendantProcess.Dispose() - } - } - } - Remove-Item -LiteralPath $testDirectory -Recurse -Force -ErrorAction Ignore -} diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml index ea80a6d95c5..caf98d3d1e4 100644 --- a/build-tools/automation/yaml-templates/stage-package-tests.yaml +++ b/build-tools/automation/yaml-templates/stage-package-tests.yaml @@ -33,10 +33,6 @@ stages: parameters: use1ESTemplate: ${{ parameters.use1ESTemplate }} - - powershell: | - & "$(System.DefaultWorkingDirectory)/build-tools/automation/scripts/CaptureLogcat.Tests.ps1" - displayName: test bounded logcat capture - - task: DownloadPipelineArtifact@2 inputs: artifactName: $(TestAssembliesArtifactName)