mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 09:31:15 +10:00
Speed up runspace and tab initialization (#4793)
* Add startup performance tracing * Lazy initialize non-default tabs * Render install app entries incrementally * Defer and cache toggle status checks * Clean up runspace invocation ownership * Defer GUI runspace pool startup * Defer status taskbar asset rendering * Record final speed verification * Fix deferred install render timer callback * Add dispatcher smoke coverage for install rendering * Replace install render timer with dispatcher callbacks * Gate performance tracing behind compile switch * Clean up analyzer warnings in speed changes * Speed up runspace and tab initialization
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
#===========================================================================
|
||||
# Tests - Asset rendering
|
||||
#===========================================================================
|
||||
|
||||
BeforeAll {
|
||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
}
|
||||
|
||||
Describe "Rendered asset caching" {
|
||||
It "caches rendered bitmap assets by type and size" {
|
||||
$assetScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Invoke-WinUtilAssets.ps1") -Raw
|
||||
|
||||
$assetScript | Should -Match 'RenderedAssetCache'
|
||||
$assetScript | Should -Match '\$cacheKey = "\$\(\(\[string\]\$type\)\.ToLowerInvariant\(\)\)\|\$Size"'
|
||||
$assetScript | Should -Match 'return \$sync\.RenderedAssetCache\[\$cacheKey\]'
|
||||
$assetScript | Should -Match '\$sync\.RenderedAssetCache\[\$cacheKey\] = \$bitmapImage'
|
||||
}
|
||||
|
||||
It "renders only the logo overlay before first paint and defers status overlays" {
|
||||
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
|
||||
|
||||
$mainScript | Should -Match 'Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo \$true -IncludeStatusAssets \$false'
|
||||
$mainScript | Should -Match 'Dispatcher\.BeginInvoke\(\[System\.Windows\.Threading\.DispatcherPriority\]::Background, \[action\]\{ Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo \$false -IncludeStatusAssets \$true \}'
|
||||
$mainScript | Should -Not -Match '\$sync\["checkmarkrender"\] = \(Invoke-WinUtilAssets -Type "checkmark"'
|
||||
$mainScript | Should -Not -Match '\$sync\["warningrender"\] = \(Invoke-WinUtilAssets -Type "warning"'
|
||||
}
|
||||
|
||||
It "lazily creates taskbar overlays before assigning them" {
|
||||
$taskbarScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Set-WinUtilTaskbarItem.ps1") -Raw
|
||||
|
||||
$taskbarScript | Should -Match 'Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo \$true -IncludeStatusAssets \$false'
|
||||
$taskbarScript | Should -Match 'Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo \$false -IncludeStatusAssets \$true'
|
||||
}
|
||||
|
||||
It "records individual taskbar overlay render checkpoints" {
|
||||
$overlayScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilTaskbarOverlayAssets.ps1") -Raw
|
||||
|
||||
$overlayScript | Should -Match 'Taskbar logo asset rendered'
|
||||
$overlayScript | Should -Match 'Taskbar checkmark asset rendered'
|
||||
$overlayScript | Should -Match 'Taskbar warning asset rendered'
|
||||
}
|
||||
}
|
||||
@@ -245,13 +245,14 @@ Describe "Preset config" {
|
||||
Describe "App navigation config" {
|
||||
It "is wired to an existing XAML target grid" {
|
||||
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
|
||||
$tabInitializerScript = Get-Content -Path (Join-Path $script:repoRoot "functions/private/Initialize-WinUtilTabContent.ps1") -Raw
|
||||
$targetGridMatch = [regex]::Match(
|
||||
$mainScript,
|
||||
"$mainScript`n$tabInitializerScript",
|
||||
'Invoke-WPFUIElements\s+-configVariable\s+\$sync\.configs\.appnavigation\s+-targetGridName\s+"([^"]+)"'
|
||||
)
|
||||
|
||||
if (-not $targetGridMatch.Success) {
|
||||
throw "scripts/main.ps1 does not wire appnavigation through Invoke-WPFUIElements."
|
||||
throw "Startup tab initialization does not wire appnavigation through Invoke-WPFUIElements."
|
||||
}
|
||||
|
||||
$xamlText = Get-Content -Path $script:xamlPath -Raw
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
#===========================================================================
|
||||
# Tests - Install tab rendering
|
||||
#===========================================================================
|
||||
|
||||
BeforeAll {
|
||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
}
|
||||
|
||||
Describe "Install app rendering startup contract" {
|
||||
It "queues app entries after creating category containers" {
|
||||
$categoryScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-InstallCategoryAppList.ps1") -Raw
|
||||
|
||||
$categoryScript | Should -Match '\$sync\.InstallAppRenderQueue = \[System\.Collections\.Queue\]::new\(\)'
|
||||
$categoryScript | Should -Match 'Start-WinUtilInstallAppRendering'
|
||||
$categoryScript | Should -Match 'Pre-group apps by category before creating WPF controls'
|
||||
}
|
||||
|
||||
It "renders queued apps through dispatcher callbacks when a form dispatcher exists" {
|
||||
$renderScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Start-WinUtilInstallAppRendering.ps1") -Raw
|
||||
|
||||
$renderScript | Should -Match 'Dispatcher\.BeginInvoke'
|
||||
$renderScript | Should -Match 'Invoke-WinUtilInstallAppRenderNextBatch'
|
||||
$renderScript | Should -Match 'Initialize-InstallAppEntry'
|
||||
$renderScript | Should -Match 'Install app entries rendered'
|
||||
}
|
||||
|
||||
It "does not use dispatcher timers for deferred install rendering" {
|
||||
$renderScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Start-WinUtilInstallAppRendering.ps1") -Raw
|
||||
|
||||
$renderScript | Should -Not -Match 'DispatcherTimer'
|
||||
$renderScript | Should -Not -Match '\$timer'
|
||||
$renderScript | Should -Not -Match '\$dispatcherTimer'
|
||||
$renderScript | Should -Not -Match '\$timer\.Stop\(\)'
|
||||
$renderScript | Should -Not -Match '& \$renderCategory'
|
||||
}
|
||||
|
||||
It "drains queued app batches on the WPF dispatcher without timer scope errors" {
|
||||
Add-Type -AssemblyName WindowsBase
|
||||
. (Join-Path $script:repoRoot "functions\private\Start-WinUtilInstallAppRendering.ps1")
|
||||
|
||||
$previousSync = Get-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
|
||||
$previousInitializeAppEntry = Get-Item -Path Function:\Initialize-InstallAppEntry -ErrorAction SilentlyContinue
|
||||
$previousSearch = Get-Item -Path Function:\Find-AppsByNameOrDescription -ErrorAction SilentlyContinue
|
||||
$previousCheckpoint = Get-Item -Path Function:\Write-WinUtilPerformanceCheckpoint -ErrorAction SilentlyContinue
|
||||
$errorCountBefore = $global:Error.Count
|
||||
|
||||
try {
|
||||
$global:sync = [Hashtable]::Synchronized(@{})
|
||||
$global:sync.currentTab = "Install"
|
||||
$global:sync.SearchBar = [pscustomobject]@{ Text = "" }
|
||||
$global:sync.Form = [pscustomobject]@{ Dispatcher = [System.Windows.Threading.Dispatcher]::CurrentDispatcher }
|
||||
$global:sync.InstallAppRenderQueue = [System.Collections.Queue]::new()
|
||||
|
||||
$renderedApps = [System.Collections.Generic.List[string]]::new()
|
||||
$checkpoints = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
function global:Initialize-InstallAppEntry {
|
||||
param($TargetElement, $AppKey)
|
||||
$renderedApps.Add($AppKey)
|
||||
return "entry:$AppKey"
|
||||
}
|
||||
|
||||
function global:Find-AppsByNameOrDescription {
|
||||
param($SearchString)
|
||||
throw "Search should not run for an empty search box in this test."
|
||||
}
|
||||
|
||||
function global:Write-WinUtilPerformanceCheckpoint {
|
||||
param([string]$Name)
|
||||
$checkpoints.Add($Name)
|
||||
}
|
||||
|
||||
$global:sync.InstallAppRenderQueue.Enqueue([pscustomobject]@{ TargetElement = [pscustomobject]@{}; AppKeys = @("AppA", "AppB") })
|
||||
$global:sync.InstallAppRenderQueue.Enqueue([pscustomobject]@{ TargetElement = [pscustomobject]@{}; AppKeys = @("AppC") })
|
||||
|
||||
$frame = New-Object System.Windows.Threading.DispatcherFrame
|
||||
$timeout = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
Start-WinUtilInstallAppRendering
|
||||
|
||||
$closeTimer = New-Object System.Windows.Threading.DispatcherTimer
|
||||
$closeTimer.Interval = [TimeSpan]::FromMilliseconds(25)
|
||||
$closeTimer.Add_Tick({
|
||||
param($sender)
|
||||
$timer = [System.Windows.Threading.DispatcherTimer]$sender
|
||||
|
||||
if ($global:sync.InstallAppEntriesRendered -or $timeout.Elapsed.TotalSeconds -gt 5) {
|
||||
$timer.Stop()
|
||||
$frame.Continue = $false
|
||||
}
|
||||
})
|
||||
$closeTimer.Start()
|
||||
|
||||
[System.Windows.Threading.Dispatcher]::PushFrame($frame)
|
||||
|
||||
$global:sync.InstallAppEntriesRendered | Should -BeTrue
|
||||
$global:sync.InstallAppRenderQueue.Count | Should -Be 0
|
||||
@($renderedApps) | Should -Be @("AppA", "AppB", "AppC")
|
||||
@($checkpoints) | Should -Contain "Install app entries rendered"
|
||||
$global:Error.Count | Should -Be $errorCountBefore
|
||||
} finally {
|
||||
if ($previousSync) {
|
||||
Set-Variable -Name sync -Value $previousSync.Value -Scope Global
|
||||
} else {
|
||||
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
foreach ($functionBackup in @(
|
||||
@{ Name = "Initialize-InstallAppEntry"; Backup = $previousInitializeAppEntry },
|
||||
@{ Name = "Find-AppsByNameOrDescription"; Backup = $previousSearch },
|
||||
@{ Name = "Write-WinUtilPerformanceCheckpoint"; Backup = $previousCheckpoint }
|
||||
)) {
|
||||
if ($functionBackup.Backup) {
|
||||
Set-Item -Path "Function:\$($functionBackup.Name)" -Value $functionBackup.Backup.ScriptBlock
|
||||
} else {
|
||||
Remove-Item -Path "Function:\$($functionBackup.Name)" -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
It "keeps app-entry metadata lookup independent from the old caller scope" {
|
||||
$entryScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-InstallAppEntry.ps1") -Raw
|
||||
|
||||
$entryScript | Should -Match '\$app = \$sync\.configs\.applicationsHashtable\.\$appKey'
|
||||
$entryScript | Should -Not -Match '\$Apps\.\$appKey'
|
||||
}
|
||||
|
||||
It "restores delayed app checkbox state from selected apps" {
|
||||
$entryScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-InstallAppEntry.ps1") -Raw
|
||||
|
||||
$entryScript | Should -Match '\$sync\.selectedApps -contains \$appKey'
|
||||
$entryScript | Should -Match '\$checkBox\.IsChecked = \$true'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#===========================================================================
|
||||
# Tests - Lazy tab initialization
|
||||
#===========================================================================
|
||||
|
||||
BeforeAll {
|
||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
|
||||
function Invoke-WPFUIElements {
|
||||
param($configVariable, [string]$targetGridName, [int]$columncount)
|
||||
}
|
||||
function Initialize-WPFUI {
|
||||
param([string]$TargetGridName)
|
||||
}
|
||||
function Write-WinUtilPerformanceCheckpoint {
|
||||
param([string]$Name)
|
||||
}
|
||||
function Invoke-WinUtilISOCheckExistingWork { }
|
||||
|
||||
. (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilTabContent.ps1")
|
||||
}
|
||||
|
||||
Describe "Initialize-WinUtilTabContent" {
|
||||
BeforeEach {
|
||||
$script:sync = [Hashtable]::Synchronized(@{
|
||||
configs = @{
|
||||
appnavigation = [pscustomobject]@{}
|
||||
tweaks = [pscustomobject]@{}
|
||||
feature = [pscustomobject]@{}
|
||||
appx = [pscustomobject]@{}
|
||||
}
|
||||
})
|
||||
|
||||
Mock Invoke-WPFUIElements { }
|
||||
Mock Initialize-WPFUI { }
|
||||
Mock Write-WinUtilPerformanceCheckpoint { }
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It "initializes the install tab once" {
|
||||
Initialize-WinUtilTabContent -TabName "Install"
|
||||
Initialize-WinUtilTabContent -TabName "Install"
|
||||
|
||||
Should -Invoke -CommandName Invoke-WPFUIElements -Times 1 -Exactly -ParameterFilter {
|
||||
$targetGridName -eq "appscategory" -and $columncount -eq 1
|
||||
}
|
||||
Should -Invoke -CommandName Initialize-WPFUI -Times 1 -Exactly -ParameterFilter {
|
||||
$TargetGridName -eq "appscategory"
|
||||
}
|
||||
Should -Invoke -CommandName Initialize-WPFUI -Times 1 -Exactly -ParameterFilter {
|
||||
$TargetGridName -eq "appspanel"
|
||||
}
|
||||
$script:sync.InitializedTabs["Install"] | Should -BeTrue
|
||||
}
|
||||
|
||||
It "initializes deferred config-backed tabs once" {
|
||||
Initialize-WinUtilTabContent -TabName "Tweaks"
|
||||
Initialize-WinUtilTabContent -TabName "Config"
|
||||
Initialize-WinUtilTabContent -TabName "AppX"
|
||||
Initialize-WinUtilTabContent -TabName "Tweaks"
|
||||
Initialize-WinUtilTabContent -TabName "Config"
|
||||
Initialize-WinUtilTabContent -TabName "AppX"
|
||||
|
||||
Should -Invoke -CommandName Invoke-WPFUIElements -Times 1 -Exactly -ParameterFilter {
|
||||
$targetGridName -eq "tweakspanel" -and $columncount -eq 2
|
||||
}
|
||||
Should -Invoke -CommandName Invoke-WPFUIElements -Times 1 -Exactly -ParameterFilter {
|
||||
$targetGridName -eq "featurespanel" -and $columncount -eq 2
|
||||
}
|
||||
Should -Invoke -CommandName Invoke-WPFUIElements -Times 1 -Exactly -ParameterFilter {
|
||||
$targetGridName -eq "appxpanel" -and $columncount -eq 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Startup lazy tab wiring" {
|
||||
It "builds only install tab content before first paint" {
|
||||
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
|
||||
$startupRegion = $mainScript.Substring(0, $mainScript.IndexOf("# Store Form Objects In PowerShell"))
|
||||
|
||||
$startupRegion | Should -Match 'Initialize-WinUtilTabContent -TabName "Install"'
|
||||
$startupRegion | Should -Not -Match 'targetGridName "tweakspanel"'
|
||||
$startupRegion | Should -Not -Match 'targetGridName "featurespanel"'
|
||||
$startupRegion | Should -Not -Match 'targetGridName "appxpanel"'
|
||||
}
|
||||
|
||||
It "initializes tab content when a tab is selected" {
|
||||
$tabScript = Get-Content -Path (Join-Path $script:repoRoot "functions\public\Invoke-WPFTab.ps1") -Raw
|
||||
|
||||
$tabScript | Should -Match 'Initialize-WinUtilTabContent -TabName \$sync\.currentTab'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#===========================================================================
|
||||
# Tests - Performance tracing
|
||||
#===========================================================================
|
||||
|
||||
BeforeAll {
|
||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
|
||||
. (Join-Path $script:repoRoot "functions\private\Write-WinUtilLog.ps1")
|
||||
. (Join-Path $script:repoRoot "tools\perf\Test-WinUtilPerformanceTrace.ps1")
|
||||
. (Join-Path $script:repoRoot "tools\perf\Write-WinUtilPerformanceCheckpoint.ps1")
|
||||
. (Join-Path $script:repoRoot "tools\perf\Start-WinUtilPerformanceTrace.ps1")
|
||||
. (Join-Path $script:repoRoot "tools\perf\Stop-WinUtilPerformanceTrace.ps1")
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
& (Join-Path $script:repoRoot "Compile.ps1")
|
||||
}
|
||||
|
||||
Describe "WinUtil performance tracing helpers" {
|
||||
BeforeEach {
|
||||
$script:originalPerfEnv = $env:WINUTIL_PERF_LOG
|
||||
Remove-Item Env:\WINUTIL_PERF_LOG -ErrorAction SilentlyContinue
|
||||
$script:sync = [Hashtable]::Synchronized(@{})
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
if ($null -eq $script:originalPerfEnv) {
|
||||
Remove-Item Env:\WINUTIL_PERF_LOG -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
$env:WINUTIL_PERF_LOG = $script:originalPerfEnv
|
||||
}
|
||||
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name originalPerfEnv -Scope Script -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It "is disabled by default" {
|
||||
Test-WinUtilPerformanceTrace | Should -BeFalse
|
||||
}
|
||||
|
||||
It "can be enabled by environment variable" {
|
||||
$env:WINUTIL_PERF_LOG = "1"
|
||||
|
||||
Test-WinUtilPerformanceTrace | Should -BeTrue
|
||||
}
|
||||
|
||||
It "writes startup checkpoints through the normal WinUtil log helper" {
|
||||
$env:WINUTIL_PERF_LOG = "1"
|
||||
Mock Write-WinUtilLog { }
|
||||
|
||||
Start-WinUtilPerformanceTrace
|
||||
Write-WinUtilPerformanceCheckpoint -Name "XAML loaded"
|
||||
Stop-WinUtilPerformanceTrace
|
||||
|
||||
Should -Invoke -CommandName Write-WinUtilLog -Times 3 -Exactly -ParameterFilter {
|
||||
$Component -eq "StartupPerf" -and $Level -eq "DEBUG"
|
||||
}
|
||||
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||
$Message -like "XAML loaded:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Startup performance checkpoints" {
|
||||
It "keeps performance tracing out of normal compiled output" {
|
||||
& (Join-Path $script:repoRoot "Compile.ps1")
|
||||
|
||||
$compiledScript = Get-Content -Path (Join-Path $script:repoRoot "winutil.ps1") -Raw
|
||||
|
||||
$compiledScript | Should -Not -Match "Test-WinUtilPerformanceTrace"
|
||||
$compiledScript | Should -Not -Match "Start-WinUtilPerformanceTrace"
|
||||
$compiledScript | Should -Not -Match "Write-WinUtilPerformanceCheckpoint"
|
||||
$compiledScript | Should -Not -Match "Stop-WinUtilPerformanceTrace"
|
||||
$compiledScript | Should -Not -Match "PerformanceTraceEnabled"
|
||||
}
|
||||
|
||||
It "adds config-load checkpoints only to trace compiled output" {
|
||||
$compileScript = Get-Content -Path (Join-Path $script:repoRoot "Compile.ps1") -Raw
|
||||
|
||||
$compileScript | Should -Match '\[switch\]\$Trace'
|
||||
$compileScript | Should -Match "Start-WinUtilPerformanceTrace"
|
||||
$compileScript | Should -Match "Config load start"
|
||||
$compileScript | Should -Match "Config load complete"
|
||||
$compileScript | Should -Match "Config .* loaded"
|
||||
|
||||
& (Join-Path $script:repoRoot "Compile.ps1") -Trace
|
||||
$compiledScript = Get-Content -Path (Join-Path $script:repoRoot "winutil.ps1") -Raw
|
||||
|
||||
$compiledScript | Should -Match "function Test-WinUtilPerformanceTrace"
|
||||
$compiledScript | Should -Match '\$sync\.PerformanceTraceEnabled = \$true'
|
||||
$compiledScript | Should -Match "Config load start"
|
||||
$compiledScript | Should -Match "Config load complete"
|
||||
}
|
||||
|
||||
It "adds runtime checkpoints for startup hotspots" {
|
||||
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
|
||||
$lazyTabScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilTabContent.ps1") -Raw
|
||||
$runspaceScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1") -Raw
|
||||
$overlayScript = Get-Content -Path (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilTaskbarOverlayAssets.ps1") -Raw
|
||||
$startupText = "$mainScript`n$lazyTabScript`n$runspaceScript`n$overlayScript"
|
||||
|
||||
foreach ($checkpoint in @(
|
||||
"Runspace pool initialized",
|
||||
"XAML loaded",
|
||||
"Theme applied",
|
||||
"Install UI created",
|
||||
"Tweaks UI created",
|
||||
"Features UI created",
|
||||
"AppX UI created",
|
||||
"Taskbar logo asset rendered",
|
||||
"First content rendered"
|
||||
)) {
|
||||
$startupText | Should -Match ([regex]::Escape($checkpoint))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#===========================================================================
|
||||
# Tests - Runspace lifecycle
|
||||
#===========================================================================
|
||||
|
||||
BeforeAll {
|
||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
. (Join-Path $script:repoRoot "functions\private\Close-WinUtilRunspacePool.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1")
|
||||
}
|
||||
|
||||
Describe "Initialize-WinUtilRunspacePool" {
|
||||
BeforeEach {
|
||||
$script:sync = [Hashtable]::Synchronized(@{})
|
||||
$script:PARAM_OFFLINE = $false
|
||||
|
||||
function Write-WinUtilPerformanceCheckpoint { param($Name) }
|
||||
Mock Write-WinUtilPerformanceCheckpoint { }
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
Close-WinUtilRunspacePool
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name PARAM_OFFLINE -Scope Script -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It "creates and reuses one open runspace pool" {
|
||||
$firstPool = Initialize-WinUtilRunspacePool
|
||||
$secondPool = Initialize-WinUtilRunspacePool
|
||||
|
||||
$firstPool.RunspacePoolStateInfo.State | Should -Be ([System.Management.Automation.Runspaces.RunspacePoolState]::Opened)
|
||||
[object]::ReferenceEquals($firstPool, $secondPool) | Should -BeTrue
|
||||
Should -Invoke -CommandName Write-WinUtilPerformanceCheckpoint -Times 1 -Exactly -ParameterFilter {
|
||||
$Name -eq "Runspace pool initialized"
|
||||
}
|
||||
}
|
||||
|
||||
It "closes and removes the active runspace pool" {
|
||||
$pool = Initialize-WinUtilRunspacePool
|
||||
|
||||
Close-WinUtilRunspacePool
|
||||
|
||||
$pool.RunspacePoolStateInfo.State | Should -Be ([System.Management.Automation.Runspaces.RunspacePoolState]::Closed)
|
||||
$script:sync.ContainsKey("runspace") | Should -BeFalse
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Runspace startup wiring" {
|
||||
It "does not create the GUI runspace pool before automation checks" {
|
||||
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
|
||||
$beforePreset = $mainScript.Substring(0, $mainScript.IndexOf('if ($Preset)'))
|
||||
|
||||
$beforePreset | Should -Not -Match '\[runspacefactory\]::CreateRunspacePool'
|
||||
$beforePreset | Should -Not -Match '\$sync\.runspace\.Open\(\)'
|
||||
}
|
||||
|
||||
It "initializes runspaces synchronously for automation paths and after first render for GUI" {
|
||||
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
|
||||
|
||||
$mainScript | Should -Match 'if \(\$Preset\) \{\s+Initialize-WinUtilRunspacePool'
|
||||
$mainScript | Should -Match 'if \(\$Config\) \{\s+Initialize-WinUtilRunspacePool'
|
||||
$mainScript | Should -Match 'Dispatcher\.BeginInvoke\(\[System\.Windows\.Threading\.DispatcherPriority\]::Background, \[action\]\{ Initialize-WinUtilRunspacePool'
|
||||
$mainScript | Should -Match 'Close-WinUtilRunspacePool'
|
||||
}
|
||||
|
||||
It "creates runspaces on demand before queueing background work" {
|
||||
$runspaceScript = Get-Content -Path (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1") -Raw
|
||||
|
||||
$runspaceScript | Should -Match 'Initialize-WinUtilRunspacePool \| Out-Null'
|
||||
}
|
||||
}
|
||||
+54
-14
@@ -4,6 +4,8 @@
|
||||
|
||||
BeforeAll {
|
||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
. (Join-Path $script:repoRoot "functions\private\Close-WinUtilRunspacePool.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFFeatureInstall.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
|
||||
@@ -18,21 +20,17 @@ BeforeAll {
|
||||
$initialSessionState.Variables.Add($syncVariable)
|
||||
$script:sync.runspace = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
|
||||
$script:sync.runspace.Open()
|
||||
|
||||
function Write-WinUtilPerformanceCheckpoint { param($Name) }
|
||||
}
|
||||
|
||||
function script:Clear-WinUtilRunspaceTestContext {
|
||||
if ($script:powershell) {
|
||||
$script:powershell.Dispose()
|
||||
}
|
||||
|
||||
if ($script:sync -and $script:sync.runspace) {
|
||||
$script:sync.runspace.Close()
|
||||
$script:sync.runspace.Dispose()
|
||||
}
|
||||
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name powershell -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name handle -Scope Script -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function script:Assert-WinUtilAsyncHandle {
|
||||
@@ -54,28 +52,34 @@ Describe "Invoke-WPFRunspace behavior" {
|
||||
}
|
||||
|
||||
It "returns a single async handle with no argument list" {
|
||||
$script:sync.Result = $null
|
||||
|
||||
$handle = Invoke-WPFRunspace -ScriptBlock {
|
||||
Start-Sleep -Milliseconds 100
|
||||
"no-args|$($sync.Marker)"
|
||||
$sync.Result = "no-args|$($sync.Marker)"
|
||||
}
|
||||
|
||||
Assert-WinUtilAsyncHandle -Handle $handle
|
||||
@($script:powershell.EndInvoke($handle))[0] | Should -Be "no-args|shared"
|
||||
$script:sync.Result | Should -Be "no-args|shared"
|
||||
}
|
||||
|
||||
It "passes one named parameter" {
|
||||
$script:sync.Result = $null
|
||||
|
||||
$handle = Invoke-WPFRunspace -ParameterList @(,("Name", "value")) -ScriptBlock {
|
||||
param([string]$Name)
|
||||
|
||||
Start-Sleep -Milliseconds 100
|
||||
"Name=$Name"
|
||||
$sync.Result = "Name=$Name"
|
||||
}
|
||||
|
||||
Assert-WinUtilAsyncHandle -Handle $handle
|
||||
@($script:powershell.EndInvoke($handle))[0] | Should -Be "Name=value"
|
||||
$script:sync.Result | Should -Be "Name=value"
|
||||
}
|
||||
|
||||
It "passes multiple named parameters" {
|
||||
$script:sync.Result = $null
|
||||
|
||||
$handle = Invoke-WPFRunspace -ParameterList @(
|
||||
("First", "alpha"),
|
||||
("Second", "beta")
|
||||
@@ -86,21 +90,57 @@ Describe "Invoke-WPFRunspace behavior" {
|
||||
)
|
||||
|
||||
Start-Sleep -Milliseconds 100
|
||||
"$First|$Second|$($sync.Marker)"
|
||||
$sync.Result = "$First|$Second|$($sync.Marker)"
|
||||
}
|
||||
|
||||
Assert-WinUtilAsyncHandle -Handle $handle
|
||||
@($script:powershell.EndInvoke($handle))[0] | Should -Be "alpha|beta|shared"
|
||||
$script:sync.Result | Should -Be "alpha|beta|shared"
|
||||
}
|
||||
|
||||
It "surfaces scriptblock failures through the owning PowerShell instance" {
|
||||
It "keeps the shared runspace pool usable after scriptblock failures" {
|
||||
$handle = Invoke-WPFRunspace -ScriptBlock {
|
||||
Start-Sleep -Milliseconds 100
|
||||
throw "runspace failure"
|
||||
}
|
||||
|
||||
Assert-WinUtilAsyncHandle -Handle $handle
|
||||
{ $script:powershell.EndInvoke($handle) } | Should -Throw -ExpectedMessage "*runspace failure*"
|
||||
|
||||
$script:sync.Result = $null
|
||||
$secondHandle = Invoke-WPFRunspace -ScriptBlock {
|
||||
$sync.Result = "after-failure"
|
||||
}
|
||||
|
||||
Assert-WinUtilAsyncHandle -Handle $secondHandle
|
||||
$script:sync.Result | Should -Be "after-failure"
|
||||
}
|
||||
|
||||
It "runs multiple queued invocations without shared PowerShell state" {
|
||||
$script:sync.FirstResult = $null
|
||||
$script:sync.SecondResult = $null
|
||||
|
||||
$firstHandle = Invoke-WPFRunspace -ParameterList @(,("Value", "first")) -ScriptBlock {
|
||||
param([string]$Value)
|
||||
|
||||
Start-Sleep -Milliseconds 150
|
||||
$sync.FirstResult = $Value
|
||||
}
|
||||
$secondHandle = Invoke-WPFRunspace -ParameterList @(,("Value", "second")) -ScriptBlock {
|
||||
param([string]$Value)
|
||||
|
||||
$sync.SecondResult = $Value
|
||||
}
|
||||
|
||||
Assert-WinUtilAsyncHandle -Handle $firstHandle
|
||||
Assert-WinUtilAsyncHandle -Handle $secondHandle
|
||||
$script:sync.FirstResult | Should -Be "first"
|
||||
$script:sync.SecondResult | Should -Be "second"
|
||||
}
|
||||
|
||||
It "does not use script-scoped PowerShell or handle state" {
|
||||
$runspaceScript = Get-Content -Path (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1") -Raw
|
||||
|
||||
$runspaceScript | Should -Not -Match '\$script:powershell'
|
||||
$runspaceScript | Should -Not -Match '\$script:handle'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-21
@@ -206,6 +206,10 @@ Describe "Compiled WinUtil sanity" {
|
||||
Describe "Runspace sanity" {
|
||||
BeforeAll {
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\private\Close-WinUtilRunspacePool.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1")
|
||||
|
||||
function Write-WinUtilPerformanceCheckpoint { param($Name) }
|
||||
}
|
||||
|
||||
It "returns a single async handle and runs a scriptblock with arguments in the shared runspace pool" {
|
||||
@@ -216,44 +220,26 @@ Describe "Runspace sanity" {
|
||||
$script:sync.runspace = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
|
||||
$script:sync.runspace.Open()
|
||||
|
||||
$ended = $false
|
||||
try {
|
||||
$script:sync.Result = $null
|
||||
$handle = Invoke-WPFRunspace -ArgumentList "argument" -ParameterList @(,("NamedValue", "parameter")) -ScriptBlock {
|
||||
param($ArgumentValue, [string]$NamedValue)
|
||||
|
||||
Start-Sleep -Milliseconds 200
|
||||
"$ArgumentValue|$NamedValue|$($sync.SmokeValue)"
|
||||
$sync.Result = "$ArgumentValue|$NamedValue|$($sync.SmokeValue)"
|
||||
}
|
||||
|
||||
($handle -is [System.IAsyncResult]) | Should -BeTrue
|
||||
($handle -is [array]) | Should -BeFalse
|
||||
$handle.AsyncWaitHandle.WaitOne(5000) | Should -BeTrue
|
||||
|
||||
$result = $script:powershell.EndInvoke($handle)
|
||||
$ended = $true
|
||||
|
||||
@($result)[0] | Should -Be "argument|parameter|shared"
|
||||
$script:sync.Result | Should -Be "argument|parameter|shared"
|
||||
} finally {
|
||||
if (-not $ended -and $handle -and $handle.IsCompleted -and $script:powershell) {
|
||||
try {
|
||||
$script:powershell.EndInvoke($handle) | Out-Null
|
||||
} catch {
|
||||
# The assertion failure is more useful than cleanup errors here.
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:powershell) {
|
||||
$script:powershell.Dispose()
|
||||
}
|
||||
|
||||
if ($script:sync -and $script:sync.runspace) {
|
||||
$script:sync.runspace.Close()
|
||||
$script:sync.runspace.Dispose()
|
||||
}
|
||||
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name powershell -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name handle -Scope Script -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#===========================================================================
|
||||
# Tests - Toggle status checks
|
||||
#===========================================================================
|
||||
|
||||
BeforeAll {
|
||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
. (Join-Path $script:repoRoot "functions\private\Get-WinUtilToggleStatus.ps1")
|
||||
}
|
||||
|
||||
Describe "Get-WinUtilToggleStatus" {
|
||||
BeforeEach {
|
||||
$script:sync = [Hashtable]::Synchronized(@{
|
||||
configs = @{
|
||||
tweaks = [pscustomobject]@{
|
||||
WPFToggleExample = [pscustomobject]@{
|
||||
registry = @(
|
||||
[pscustomobject]@{
|
||||
Path = "HKCU:\Software\WinUtilToggle"
|
||||
Name = "Enabled"
|
||||
Value = "1"
|
||||
OriginalValue = "0"
|
||||
DefaultState = "true"
|
||||
}
|
||||
)
|
||||
}
|
||||
WPFToggleDisabledByDefault = [pscustomobject]@{
|
||||
registry = @(
|
||||
[pscustomobject]@{
|
||||
Path = "HKCU:\Software\WinUtilToggle"
|
||||
Name = "Enabled"
|
||||
Value = "1"
|
||||
OriginalValue = "0"
|
||||
DefaultState = "false"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Mock Get-PSDrive { [pscustomobject]@{ Name = "HKU" } } -ParameterFilter { $Name -eq "HKU" }
|
||||
Mock New-PSDrive { }
|
||||
Mock New-Item { }
|
||||
Mock Test-Path { $false }
|
||||
Mock Get-ItemProperty { [pscustomobject]@{ Enabled = "1" } }
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It "does not create missing registry paths while reading toggle state" {
|
||||
Get-WinUtilToggleStatus "WPFToggleExample" | Should -BeTrue
|
||||
|
||||
Should -Invoke -CommandName Test-Path -Times 1 -Exactly -ParameterFilter {
|
||||
$Path -eq "HKCU:\Software\WinUtilToggle"
|
||||
}
|
||||
Should -Invoke -CommandName New-Item -Times 0 -Exactly
|
||||
Should -Invoke -CommandName Get-ItemProperty -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It "uses configured false default when the registry path is missing" {
|
||||
Get-WinUtilToggleStatus "WPFToggleDisabledByDefault" | Should -BeFalse
|
||||
|
||||
Should -Invoke -CommandName New-Item -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It "caches toggle results for repeated checks" {
|
||||
Mock Test-Path { $true } -ParameterFilter { $Path -eq "HKCU:\Software\WinUtilToggle" }
|
||||
Mock Get-ItemProperty { [pscustomobject]@{ Enabled = "1" } } -ParameterFilter {
|
||||
$Path -eq "HKCU:\Software\WinUtilToggle"
|
||||
}
|
||||
|
||||
Get-WinUtilToggleStatus "WPFToggleExample" | Should -BeTrue
|
||||
Get-WinUtilToggleStatus "WPFToggleExample" | Should -BeTrue
|
||||
|
||||
Should -Invoke -CommandName Test-Path -Times 1 -Exactly -ParameterFilter {
|
||||
$Path -eq "HKCU:\Software\WinUtilToggle"
|
||||
}
|
||||
Should -Invoke -CommandName Get-ItemProperty -Times 1 -Exactly -ParameterFilter {
|
||||
$Path -eq "HKCU:\Software\WinUtilToggle"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,13 +295,19 @@ Describe "XAML and sync wiring" {
|
||||
"keys",
|
||||
"ContainsKey",
|
||||
"GetEnumerator",
|
||||
"Remove",
|
||||
"logorender",
|
||||
"checkmarkrender",
|
||||
"warningrender",
|
||||
"InitializedTabs",
|
||||
"RenderedAssetCache",
|
||||
"ToggleStatusCache",
|
||||
"InstallAppAreaBorder",
|
||||
"InstallAppAreaScrollViewer",
|
||||
"InstallAppAreaOverlay",
|
||||
"InstallAppAreaOverlayText",
|
||||
"InstallAppRenderQueue",
|
||||
"InstallAppEntriesRendered",
|
||||
"ProgressBar",
|
||||
"progressBarTextBlock",
|
||||
"Win11ISOImageInfo",
|
||||
|
||||
Reference in New Issue
Block a user