From 2a05c8412db43de1479905607bbca3e1d9d9bd74 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Wed, 9 Sep 2026 13:14:10 +0200 Subject: [PATCH 1/3] fix(devices): Report power operations accurately --- .../Private/DeviceProviders/AdbProvider.ps1 | 8 +- .../DeviceProviders/DeviceProvider.ps1 | 36 +++- .../DeviceProviders/MockDeviceProvider.ps1 | 6 +- .../DeviceProviders/SauceLabsProvider.ps1 | 8 +- .../DeviceProviders/SwitchProvider.ps1 | 9 +- .../Private/DeviceProviders/XboxProvider.ps1 | 8 +- .../DeviceProviders/iOSSimulatorProvider.ps1 | 10 +- app-runner/Public/Disconnect-Device.ps1 | 10 +- app-runner/Public/Start-Device.ps1 | 10 +- app-runner/Tests/PowerReporting.Tests.ps1 | 161 ++++++++++++++++++ 10 files changed, 236 insertions(+), 30 deletions(-) create mode 100644 app-runner/Tests/PowerReporting.Tests.ps1 diff --git a/app-runner/Private/DeviceProviders/AdbProvider.ps1 b/app-runner/Private/DeviceProviders/AdbProvider.ps1 index 3d6ce28..b5ea42c 100644 --- a/app-runner/Private/DeviceProviders/AdbProvider.ps1 +++ b/app-runner/Private/DeviceProviders/AdbProvider.ps1 @@ -420,12 +420,12 @@ class AdbProvider : DeviceProvider { return $this.DeviceSerial } - [void] StartDevice() { - Write-Warning "$($this.Platform): StartDevice is not supported for ADB devices" + [DevicePowerResult] StartDevice() { + return [DevicePowerResult]::NotSupported } - [void] StopDevice() { - Write-Warning "$($this.Platform): StopDevice is not supported for ADB devices" + [DevicePowerResult] StopDevice() { + return [DevicePowerResult]::NotSupported } [void] RestartDevice() { diff --git a/app-runner/Private/DeviceProviders/DeviceProvider.ps1 b/app-runner/Private/DeviceProviders/DeviceProvider.ps1 index 92e27b4..0c30b91 100644 --- a/app-runner/Private/DeviceProviders/DeviceProvider.ps1 +++ b/app-runner/Private/DeviceProviders/DeviceProvider.ps1 @@ -1,6 +1,13 @@ # Device Provider Base Class # Provides shared functionality for all device providers +enum DevicePowerResult { + NotSupported + PoweredOn + PoweredOff + Sleeping + Failed +} <# .SYNOPSIS @@ -234,8 +241,11 @@ class DeviceProvider { } [object] InvokeCommand([string]$action, [object[]]$parameters) { - # Build command once and check if it's a no-op $builtCommand = $this.BuildCommand($action, $parameters) + return $this.InvokeBuiltCommand($action, $builtCommand) + } + + [object] InvokeBuiltCommand([string]$action, [BuiltCommand]$builtCommand) { if ($builtCommand.IsNoOp()) { return $null } @@ -348,16 +358,32 @@ class DeviceProvider { } # Device lifecycle management (shared implementation) - [void] StartDevice() { + [DevicePowerResult] StartDevice() { Write-Debug "$($this.Platform): Starting device" - $this.InvokeCommand('poweron', @()) + if ($this.Commands.ContainsKey('poweron') -and $null -eq $this.Commands['poweron']) { + return [DevicePowerResult]::NotSupported + } + $command = $this.BuildCommand('poweron', @()) + if ($command.IsNoOp()) { + return [DevicePowerResult]::NotSupported + } + $this.InvokeBuiltCommand('poweron', $command) + return [DevicePowerResult]::PoweredOn } - [void] StopDevice() { + [DevicePowerResult] StopDevice() { Write-Debug "$($this.Platform): Stopping device" - Write-Output $this.InvokeCommand('poweroff', @()) + if ($this.Commands.ContainsKey('poweroff') -and $null -eq $this.Commands['poweroff']) { + return [DevicePowerResult]::NotSupported + } + $command = $this.BuildCommand('poweroff', @()) + if ($command.IsNoOp()) { + return [DevicePowerResult]::NotSupported + } + $this.InvokeBuiltCommand('poweroff', $command) + return [DevicePowerResult]::PoweredOff } [void] RestartDevice() { diff --git a/app-runner/Private/DeviceProviders/MockDeviceProvider.ps1 b/app-runner/Private/DeviceProviders/MockDeviceProvider.ps1 index 40c4033..f93fff6 100644 --- a/app-runner/Private/DeviceProviders/MockDeviceProvider.ps1 +++ b/app-runner/Private/DeviceProviders/MockDeviceProvider.ps1 @@ -137,15 +137,17 @@ class MockDeviceProvider : DeviceProvider { return $this.IsConnected } - [void] StartDevice() { + [DevicePowerResult] StartDevice() { Write-Debug "Mock: Starting device" $this.MockConfig.PowerState = "On" + return [DevicePowerResult]::PoweredOn } - [void] StopDevice() { + [DevicePowerResult] StopDevice() { Write-Debug "Mock: Stopping device" $this.MockConfig.PowerState = "Off" $this.MockConfig.AppRunning = $false + return [DevicePowerResult]::PoweredOff } [void] RestartDevice() { diff --git a/app-runner/Private/DeviceProviders/SauceLabsProvider.ps1 b/app-runner/Private/DeviceProviders/SauceLabsProvider.ps1 index 59f27f8..e9939c7 100644 --- a/app-runner/Private/DeviceProviders/SauceLabsProvider.ps1 +++ b/app-runner/Private/DeviceProviders/SauceLabsProvider.ps1 @@ -639,12 +639,12 @@ class SauceLabsProvider : DeviceProvider { return $this.DeviceName } - [void] StartDevice() { - Write-Warning "$($this.Platform): StartDevice is not applicable for SauceLabs cloud devices" + [DevicePowerResult] StartDevice() { + return [DevicePowerResult]::NotSupported } - [void] StopDevice() { - Write-Warning "$($this.Platform): StopDevice is not applicable for SauceLabs cloud devices" + [DevicePowerResult] StopDevice() { + return [DevicePowerResult]::NotSupported } [void] RestartDevice() { diff --git a/app-runner/Private/DeviceProviders/SwitchProvider.ps1 b/app-runner/Private/DeviceProviders/SwitchProvider.ps1 index 5e0c819..796e654 100644 --- a/app-runner/Private/DeviceProviders/SwitchProvider.ps1 +++ b/app-runner/Private/DeviceProviders/SwitchProvider.ps1 @@ -137,9 +137,12 @@ class SwitchProvider : DeviceProvider { } # override StopDevice because it exits immediately but actually takes a while to finish - [void] StopDevice() { - ([DeviceProvider] $this).StopDevice(); - Start-Sleep -Seconds 3 + [DevicePowerResult] StopDevice() { + $result = ([DeviceProvider] $this).StopDevice() + if ($result -eq [DevicePowerResult]::PoweredOff) { + Start-Sleep -Seconds 3 + } + return $result } # Override TakeScreenshot to pass --target when a target is specified diff --git a/app-runner/Private/DeviceProviders/XboxProvider.ps1 b/app-runner/Private/DeviceProviders/XboxProvider.ps1 index 321a1e8..80718b6 100644 --- a/app-runner/Private/DeviceProviders/XboxProvider.ps1 +++ b/app-runner/Private/DeviceProviders/XboxProvider.ps1 @@ -91,18 +91,20 @@ class XboxProvider : DeviceProvider { } # Override StartDevice to support connected standby - [void] StartDevice() { + [DevicePowerResult] StartDevice() { Write-Debug "$($this.Platform): Starting device" $this.InvokePowerOn() + return [DevicePowerResult]::PoweredOn } # Override StopDevice to support connected standby - [void] StopDevice() { + [DevicePowerResult] StopDevice() { $powerState = $this.InvokeCommand('powerState', @()) if ($powerState -match 'Allows Instant On') { $this.InvokeCommand('sleep', @()) + return [DevicePowerResult]::Sleeping } else { - $this.InvokeCommand('poweroff', @()) + return ([DeviceProvider] $this).StopDevice() } } diff --git a/app-runner/Private/DeviceProviders/iOSSimulatorProvider.ps1 b/app-runner/Private/DeviceProviders/iOSSimulatorProvider.ps1 index 7c676bd..06bb1d4 100644 --- a/app-runner/Private/DeviceProviders/iOSSimulatorProvider.ps1 +++ b/app-runner/Private/DeviceProviders/iOSSimulatorProvider.ps1 @@ -398,26 +398,32 @@ class iOSSimulatorProvider : DeviceProvider { return $this.SimulatorUUID } - [void] StartDevice() { + [DevicePowerResult] StartDevice() { Write-Debug "$($this.Platform): Starting simulator" if ($this.SimulatorUUID) { $this.BootSimulator() + return [DevicePowerResult]::PoweredOn } else { Write-Warning "$($this.Platform): No simulator selected. Call Connect() first." + return [DevicePowerResult]::Failed } } - [void] StopDevice() { + [DevicePowerResult] StopDevice() { Write-Debug "$($this.Platform): Stopping simulator" if ($this.SimulatorUUID) { try { $this.InvokeCommand('shutdown', @($this.SimulatorUUID)) + return [DevicePowerResult]::PoweredOff } catch { Write-Warning "Failed to shutdown simulator: $_" } + } else { + Write-Warning "$($this.Platform): No simulator selected. Call Connect() first." } + return [DevicePowerResult]::Failed } [void] RestartDevice() { diff --git a/app-runner/Public/Disconnect-Device.ps1 b/app-runner/Public/Disconnect-Device.ps1 index 9a2c406..9c217f6 100644 --- a/app-runner/Public/Disconnect-Device.ps1 +++ b/app-runner/Public/Disconnect-Device.ps1 @@ -37,8 +37,12 @@ function Disconnect-Device { # Power off device first if requested if ($PowerOff) { Write-Debug "Powering off device before disconnect" - $provider.StopDevice() - Write-Output "Device powered off" + switch ($provider.StopDevice()) { + ([DevicePowerResult]::PoweredOff) { Write-Output "Device powered off" } + ([DevicePowerResult]::Sleeping) { Write-Output "Device put to sleep" } + ([DevicePowerResult]::NotSupported) { Write-Output "Device power off not supported. Skipped." } + ([DevicePowerResult]::Failed) { Write-Warning "Device power off failed." } + } } $provider.Disconnect() } finally { @@ -56,4 +60,4 @@ function Disconnect-Device { Write-Debug "Successfully disconnected from $platform device" Write-Output "Disconnected from device" } -} \ No newline at end of file +} diff --git a/app-runner/Public/Start-Device.ps1 b/app-runner/Public/Start-Device.ps1 index f73bd42..5128890 100644 --- a/app-runner/Public/Start-Device.ps1 +++ b/app-runner/Public/Start-Device.ps1 @@ -21,7 +21,9 @@ function Start-Device { # Use the provider to start the device $provider = $script:CurrentSession.Provider - $provider.StartDevice() - - Write-Output "Device started successfully" -} \ No newline at end of file + switch ($provider.StartDevice()) { + ([DevicePowerResult]::PoweredOn) { Write-Output "Device started successfully" } + ([DevicePowerResult]::NotSupported) { Write-Output "Device power on not supported. Skipped." } + ([DevicePowerResult]::Failed) { Write-Warning "Device power on failed." } + } +} diff --git a/app-runner/Tests/PowerReporting.Tests.ps1 b/app-runner/Tests/PowerReporting.Tests.ps1 new file mode 100644 index 0000000..9fc5d7b --- /dev/null +++ b/app-runner/Tests/PowerReporting.Tests.ps1 @@ -0,0 +1,161 @@ +$ErrorActionPreference = 'Stop' + +BeforeAll { + Import-Module "$PSScriptRoot\..\SentryAppRunner.psd1" -Force +} + +AfterAll { + Remove-Module SentryAppRunner -Force -ErrorAction SilentlyContinue +} + +Describe 'Device power reporting' -Tag 'Unit' { + BeforeEach { + $script:savedExitCode = $global:LASTEXITCODE + & (Get-Module SentryAppRunner) { + $provider = [DeviceProvider]::new() + $provider.Platform = 'Test' + $provider.Commands = @{ + poweron = [BuiltCommand]::NoOp() + poweroff = [BuiltCommand]::NoOp() + getstatus = [BuiltCommand]::NoOp() + disconnect = [BuiltCommand]::NoOp() + } + $script:CurrentSession = @{ + Provider = $provider + Platform = 'Test' + Identifier = 'test device' + Mutex = $null + } + } + } + + AfterEach { + & (Get-Module SentryAppRunner) { $script:CurrentSession = $null } + $global:LASTEXITCODE = $script:savedExitCode + } + + It 'Reports skipped power-on for an explicit no-op' { + $output = @(Start-Device 3>&1) + $output | Should -Be @('Device power on not supported. Skipped.') + } + + It 'Reports skipped power-off for an explicit no-op and still disconnects' { + $output = @(Disconnect-Device -PowerOff 3>&1) + $output | Should -Be @('Device power off not supported. Skipped.', 'Disconnected from device') + Get-DeviceSession | Should -BeNullOrEmpty + } + + It 'Reports unavailable power commands without duplicate warnings' { + & (Get-Module SentryAppRunner) { + $script:CurrentSession.Provider.Commands['poweron'] = $null + $script:CurrentSession.Provider.Commands['poweroff'] = $null + } + + @(Start-Device 3>&1) | Should -Be @('Device power on not supported. Skipped.') + @(Disconnect-Device -PowerOff 3>&1) | Should -Be @('Device power off not supported. Skipped.', 'Disconnected from device') + } + + It 'Reports successful power commands after executing them' { + & (Get-Module SentryAppRunner) { + $command = [BuiltCommand]::new('& { $global:LASTEXITCODE = 0 }', $null) + $script:CurrentSession.Provider.Commands['poweron'] = $command + $script:CurrentSession.Provider.Commands['poweroff'] = $command + } + + @(Start-Device) | Should -Be @('Device started successfully') + @(Disconnect-Device -PowerOff) | Should -Be @('Device powered off', 'Disconnected from device') + } + + It 'Propagates power-command failures and still clears the session on disconnect' { + & (Get-Module SentryAppRunner) { + $command = [BuiltCommand]::new('& { throw "Power command failed" }', $null) + $script:CurrentSession.Provider.Commands['poweron'] = $command + $script:CurrentSession.Provider.Commands['poweroff'] = $command + } + + { Start-Device } | Should -Throw '*Power command failed*' + { Disconnect-Device -PowerOff } | Should -Throw '*Power command failed*' + Get-DeviceSession | Should -BeNullOrEmpty + } + + It 'Reports Xbox sleep instead of power-off' { + & (Get-Module SentryAppRunner) { + $provider = & { [XboxProvider]::new() } 3>$null + $provider.TimeoutSeconds = 0 + $provider.Commands['powerState'] = [BuiltCommand]::new('& { $global:LASTEXITCODE = 0; "Allows Instant On" }', $null) + $provider.Commands['sleep'] = [BuiltCommand]::new('& { $global:LASTEXITCODE = 0 }', $null) + $script:CurrentSession.Provider = $provider + } + + $output = @(Disconnect-Device -PowerOff 3>&1) + $output | Should -Be @('Device put to sleep', 'Disconnected from device') + } + + It 'Reports unsupported Xbox power-off when Instant On is unavailable' { + & (Get-Module SentryAppRunner) { + $provider = & { [XboxProvider]::new() } 3>$null + $provider.TimeoutSeconds = 0 + $provider.Commands['powerState'] = [BuiltCommand]::new('& { $global:LASTEXITCODE = 0; "Energy saving" }', $null) + $script:CurrentSession.Provider = $provider + } + + $output = @(Disconnect-Device -PowerOff 3>&1) + $output | Should -Be @('Device power off not supported. Skipped.', 'Disconnected from device') + Get-DeviceSession | Should -BeNullOrEmpty + } + + It 'Reports unsupported ADB power operations from its method overrides' { + & (Get-Module SentryAppRunner) { + $provider = [AdbProvider]::new() + $provider.Commands['getstatus'] = [BuiltCommand]::NoOp() + $script:CurrentSession.Provider = $provider + } + + $startOutput = @(Start-Device 3>&1) + $stopOutput = @(Disconnect-Device -PowerOff 3>&1) + $startOutput | Should -Be @('Device power on not supported. Skipped.') + $stopOutput | Should -Be @('Device power off not supported. Skipped.', 'Disconnected from device') + } + + It 'Identifies failed power operations alongside the missing-simulator warning' { + Mock Assert-DeviceSession -ModuleName SentryAppRunner {} + & (Get-Module SentryAppRunner) { + $provider = [System.Runtime.CompilerServices.RuntimeHelpers]::GetUninitializedObject([iOSSimulatorProvider]) + $provider.Platform = 'iOSSimulator' + $script:CurrentSession.Provider = $provider + } + + $startOutput = @(Start-Device 3>&1 | ForEach-Object { "$_" }) + $stopOutput = @(Disconnect-Device -PowerOff 3>&1 | ForEach-Object { "$_" }) + $startOutput | Should -Be @( + 'iOSSimulator: No simulator selected. Call Connect() first.' + 'Device power on failed.' + ) + $stopOutput | Should -Be @( + 'iOSSimulator: No simulator selected. Call Connect() first.' + 'Device power off failed.' + 'Disconnected from device' + ) + } + + It 'Reports power-off failure alongside the simulator shutdown error' { + & (Get-Module SentryAppRunner) { + # Bypass host and SDK checks to exercise failure reporting without a simulator. + $provider = [System.Runtime.CompilerServices.RuntimeHelpers]::GetUninitializedObject([iOSSimulatorProvider]) + $provider.Platform = 'iOSSimulator' + $provider.SimulatorUUID = 'test-simulator' + $provider.Commands = @{ shutdown = [BuiltCommand]::new('& { throw "Shutdown failed" }', $null) } + $provider.Timeouts = @{} + $script:CurrentSession.Provider = $provider + } + + $output = @(Disconnect-Device -PowerOff 3>&1) + $output[0] | Should -BeOfType ([System.Management.Automation.WarningRecord]) + "$($output[0])" | Should -BeLike '*Shutdown failed*' + $output[1] | Should -BeOfType ([System.Management.Automation.WarningRecord]) + "$($output[1])" | Should -Be 'Device power off failed.' + $output | Should -Not -Contain 'Device powered off' + $output | Should -Not -Contain 'Device power off not supported. Skipped.' + Get-DeviceSession | Should -BeNullOrEmpty + } +} From 084ec140bd1857ca00b5f501cca56269eba4a07e Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Wed, 9 Sep 2026 13:23:26 +0200 Subject: [PATCH 2/3] fix: Report Xbox sleep as powered off --- app-runner/Private/DeviceProviders/DeviceProvider.ps1 | 1 - app-runner/Private/DeviceProviders/XboxProvider.ps1 | 2 +- app-runner/Public/Disconnect-Device.ps1 | 1 - app-runner/Tests/PowerReporting.Tests.ps1 | 5 +++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app-runner/Private/DeviceProviders/DeviceProvider.ps1 b/app-runner/Private/DeviceProviders/DeviceProvider.ps1 index 0c30b91..ad4f165 100644 --- a/app-runner/Private/DeviceProviders/DeviceProvider.ps1 +++ b/app-runner/Private/DeviceProviders/DeviceProvider.ps1 @@ -5,7 +5,6 @@ enum DevicePowerResult { NotSupported PoweredOn PoweredOff - Sleeping Failed } diff --git a/app-runner/Private/DeviceProviders/XboxProvider.ps1 b/app-runner/Private/DeviceProviders/XboxProvider.ps1 index 80718b6..e887b9f 100644 --- a/app-runner/Private/DeviceProviders/XboxProvider.ps1 +++ b/app-runner/Private/DeviceProviders/XboxProvider.ps1 @@ -102,7 +102,7 @@ class XboxProvider : DeviceProvider { $powerState = $this.InvokeCommand('powerState', @()) if ($powerState -match 'Allows Instant On') { $this.InvokeCommand('sleep', @()) - return [DevicePowerResult]::Sleeping + return [DevicePowerResult]::PoweredOff } else { return ([DeviceProvider] $this).StopDevice() } diff --git a/app-runner/Public/Disconnect-Device.ps1 b/app-runner/Public/Disconnect-Device.ps1 index 9c217f6..9baacbf 100644 --- a/app-runner/Public/Disconnect-Device.ps1 +++ b/app-runner/Public/Disconnect-Device.ps1 @@ -39,7 +39,6 @@ function Disconnect-Device { Write-Debug "Powering off device before disconnect" switch ($provider.StopDevice()) { ([DevicePowerResult]::PoweredOff) { Write-Output "Device powered off" } - ([DevicePowerResult]::Sleeping) { Write-Output "Device put to sleep" } ([DevicePowerResult]::NotSupported) { Write-Output "Device power off not supported. Skipped." } ([DevicePowerResult]::Failed) { Write-Warning "Device power off failed." } } diff --git a/app-runner/Tests/PowerReporting.Tests.ps1 b/app-runner/Tests/PowerReporting.Tests.ps1 index 9fc5d7b..89dd5fa 100644 --- a/app-runner/Tests/PowerReporting.Tests.ps1 +++ b/app-runner/Tests/PowerReporting.Tests.ps1 @@ -78,7 +78,7 @@ Describe 'Device power reporting' -Tag 'Unit' { Get-DeviceSession | Should -BeNullOrEmpty } - It 'Reports Xbox sleep instead of power-off' { + It 'Reports Xbox sleep as successful power-off' { & (Get-Module SentryAppRunner) { $provider = & { [XboxProvider]::new() } 3>$null $provider.TimeoutSeconds = 0 @@ -88,7 +88,7 @@ Describe 'Device power reporting' -Tag 'Unit' { } $output = @(Disconnect-Device -PowerOff 3>&1) - $output | Should -Be @('Device put to sleep', 'Disconnected from device') + $output | Should -Be @('Device powered off', 'Disconnected from device') } It 'Reports unsupported Xbox power-off when Instant On is unavailable' { @@ -107,6 +107,7 @@ Describe 'Device power reporting' -Tag 'Unit' { It 'Reports unsupported ADB power operations from its method overrides' { & (Get-Module SentryAppRunner) { $provider = [AdbProvider]::new() + # Let Start-Device's session health check pass without invoking real ADB. $provider.Commands['getstatus'] = [BuiltCommand]::NoOp() $script:CurrentSession.Provider = $provider } From 2a92182ac0718b91f9b399eab698d1bfaa7d74ed Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Wed, 9 Sep 2026 13:27:38 +0200 Subject: [PATCH 3/3] test: Use a mock provider for power reporting tests --- app-runner/Tests/PowerReporting.Tests.ps1 | 166 ++++++---------------- 1 file changed, 40 insertions(+), 126 deletions(-) diff --git a/app-runner/Tests/PowerReporting.Tests.ps1 b/app-runner/Tests/PowerReporting.Tests.ps1 index 89dd5fa..28faa59 100644 --- a/app-runner/Tests/PowerReporting.Tests.ps1 +++ b/app-runner/Tests/PowerReporting.Tests.ps1 @@ -10,153 +10,67 @@ AfterAll { Describe 'Device power reporting' -Tag 'Unit' { BeforeEach { - $script:savedExitCode = $global:LASTEXITCODE + $script:provider = [PSCustomObject]@{ + StartResult = $null + StopResult = $null + Disconnected = $false + } + $script:provider | Add-Member ScriptMethod StartDevice { return $this.StartResult } + $script:provider | Add-Member ScriptMethod StopDevice { return $this.StopResult } + $script:provider | Add-Member ScriptMethod TestConnection { return $true } + $script:provider | Add-Member ScriptMethod Disconnect { $this.Disconnected = $true } + & (Get-Module SentryAppRunner) { - $provider = [DeviceProvider]::new() - $provider.Platform = 'Test' - $provider.Commands = @{ - poweron = [BuiltCommand]::NoOp() - poweroff = [BuiltCommand]::NoOp() - getstatus = [BuiltCommand]::NoOp() - disconnect = [BuiltCommand]::NoOp() - } + param($provider) $script:CurrentSession = @{ Provider = $provider Platform = 'Test' Identifier = 'test device' Mutex = $null } - } + } $script:provider } AfterEach { & (Get-Module SentryAppRunner) { $script:CurrentSession = $null } - $global:LASTEXITCODE = $script:savedExitCode - } - - It 'Reports skipped power-on for an explicit no-op' { - $output = @(Start-Device 3>&1) - $output | Should -Be @('Device power on not supported. Skipped.') - } - - It 'Reports skipped power-off for an explicit no-op and still disconnects' { - $output = @(Disconnect-Device -PowerOff 3>&1) - $output | Should -Be @('Device power off not supported. Skipped.', 'Disconnected from device') - Get-DeviceSession | Should -BeNullOrEmpty - } - - It 'Reports unavailable power commands without duplicate warnings' { - & (Get-Module SentryAppRunner) { - $script:CurrentSession.Provider.Commands['poweron'] = $null - $script:CurrentSession.Provider.Commands['poweroff'] = $null - } - - @(Start-Device 3>&1) | Should -Be @('Device power on not supported. Skipped.') - @(Disconnect-Device -PowerOff 3>&1) | Should -Be @('Device power off not supported. Skipped.', 'Disconnected from device') } - It 'Reports successful power commands after executing them' { - & (Get-Module SentryAppRunner) { - $command = [BuiltCommand]::new('& { $global:LASTEXITCODE = 0 }', $null) - $script:CurrentSession.Provider.Commands['poweron'] = $command - $script:CurrentSession.Provider.Commands['poweroff'] = $command + It 'Reports power operations' -TestCases @( + @{ + Name = 'failed'; StartResult = 'Failed'; StopResult = 'Failed' + StartMessage = 'Device power on failed.'; StopMessage = 'Device power off failed.' + OutputType = [System.Management.Automation.WarningRecord] } - - @(Start-Device) | Should -Be @('Device started successfully') - @(Disconnect-Device -PowerOff) | Should -Be @('Device powered off', 'Disconnected from device') - } - - It 'Propagates power-command failures and still clears the session on disconnect' { - & (Get-Module SentryAppRunner) { - $command = [BuiltCommand]::new('& { throw "Power command failed" }', $null) - $script:CurrentSession.Provider.Commands['poweron'] = $command - $script:CurrentSession.Provider.Commands['poweroff'] = $command - } - - { Start-Device } | Should -Throw '*Power command failed*' - { Disconnect-Device -PowerOff } | Should -Throw '*Power command failed*' - Get-DeviceSession | Should -BeNullOrEmpty - } - - It 'Reports Xbox sleep as successful power-off' { - & (Get-Module SentryAppRunner) { - $provider = & { [XboxProvider]::new() } 3>$null - $provider.TimeoutSeconds = 0 - $provider.Commands['powerState'] = [BuiltCommand]::new('& { $global:LASTEXITCODE = 0; "Allows Instant On" }', $null) - $provider.Commands['sleep'] = [BuiltCommand]::new('& { $global:LASTEXITCODE = 0 }', $null) - $script:CurrentSession.Provider = $provider + @{ + Name = 'unsupported'; StartResult = 'NotSupported'; StopResult = 'NotSupported' + StartMessage = 'Device power on not supported. Skipped.'; StopMessage = 'Device power off not supported. Skipped.' + OutputType = [string] } - - $output = @(Disconnect-Device -PowerOff 3>&1) - $output | Should -Be @('Device powered off', 'Disconnected from device') - } - - It 'Reports unsupported Xbox power-off when Instant On is unavailable' { - & (Get-Module SentryAppRunner) { - $provider = & { [XboxProvider]::new() } 3>$null - $provider.TimeoutSeconds = 0 - $provider.Commands['powerState'] = [BuiltCommand]::new('& { $global:LASTEXITCODE = 0; "Energy saving" }', $null) - $script:CurrentSession.Provider = $provider + @{ + Name = 'successful'; StartResult = 'PoweredOn'; StopResult = 'PoweredOff' + StartMessage = 'Device started successfully'; StopMessage = 'Device powered off' + OutputType = [string] } + ) { + param($StartResult, $StopResult, $StartMessage, $StopMessage, $OutputType) - $output = @(Disconnect-Device -PowerOff 3>&1) - $output | Should -Be @('Device power off not supported. Skipped.', 'Disconnected from device') - Get-DeviceSession | Should -BeNullOrEmpty - } - - It 'Reports unsupported ADB power operations from its method overrides' { & (Get-Module SentryAppRunner) { - $provider = [AdbProvider]::new() - # Let Start-Device's session health check pass without invoking real ADB. - $provider.Commands['getstatus'] = [BuiltCommand]::NoOp() - $script:CurrentSession.Provider = $provider - } + param($StartResult, $StopResult) + $script:CurrentSession.Provider.StartResult = [DevicePowerResult]$StartResult + $script:CurrentSession.Provider.StopResult = [DevicePowerResult]$StopResult + } $StartResult $StopResult $startOutput = @(Start-Device 3>&1) - $stopOutput = @(Disconnect-Device -PowerOff 3>&1) - $startOutput | Should -Be @('Device power on not supported. Skipped.') - $stopOutput | Should -Be @('Device power off not supported. Skipped.', 'Disconnected from device') - } - - It 'Identifies failed power operations alongside the missing-simulator warning' { - Mock Assert-DeviceSession -ModuleName SentryAppRunner {} - & (Get-Module SentryAppRunner) { - $provider = [System.Runtime.CompilerServices.RuntimeHelpers]::GetUninitializedObject([iOSSimulatorProvider]) - $provider.Platform = 'iOSSimulator' - $script:CurrentSession.Provider = $provider - } + $startOutput.Count | Should -Be 1 + $startOutput[0] | Should -BeOfType $OutputType + "$($startOutput[0])" | Should -Be $StartMessage - $startOutput = @(Start-Device 3>&1 | ForEach-Object { "$_" }) - $stopOutput = @(Disconnect-Device -PowerOff 3>&1 | ForEach-Object { "$_" }) - $startOutput | Should -Be @( - 'iOSSimulator: No simulator selected. Call Connect() first.' - 'Device power on failed.' - ) - $stopOutput | Should -Be @( - 'iOSSimulator: No simulator selected. Call Connect() first.' - 'Device power off failed.' - 'Disconnected from device' - ) - } - - It 'Reports power-off failure alongside the simulator shutdown error' { - & (Get-Module SentryAppRunner) { - # Bypass host and SDK checks to exercise failure reporting without a simulator. - $provider = [System.Runtime.CompilerServices.RuntimeHelpers]::GetUninitializedObject([iOSSimulatorProvider]) - $provider.Platform = 'iOSSimulator' - $provider.SimulatorUUID = 'test-simulator' - $provider.Commands = @{ shutdown = [BuiltCommand]::new('& { throw "Shutdown failed" }', $null) } - $provider.Timeouts = @{} - $script:CurrentSession.Provider = $provider - } - - $output = @(Disconnect-Device -PowerOff 3>&1) - $output[0] | Should -BeOfType ([System.Management.Automation.WarningRecord]) - "$($output[0])" | Should -BeLike '*Shutdown failed*' - $output[1] | Should -BeOfType ([System.Management.Automation.WarningRecord]) - "$($output[1])" | Should -Be 'Device power off failed.' - $output | Should -Not -Contain 'Device powered off' - $output | Should -Not -Contain 'Device power off not supported. Skipped.' + $stopOutput = @(Disconnect-Device -PowerOff 3>&1) + $stopOutput.Count | Should -Be 2 + $stopOutput[0] | Should -BeOfType $OutputType + "$($stopOutput[0])" | Should -Be $StopMessage + $stopOutput[1] | Should -Be 'Disconnected from device' + $script:provider.Disconnected | Should -BeTrue Get-DeviceSession | Should -BeNullOrEmpty } }