Files
winutil/pester/configs.Tests.ps1
Chris Titus 93dc23dd66 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
2026-07-01 23:34:59 -05:00

552 lines
21 KiB
PowerShell

#===========================================================================
# Tests - Config Files
#===========================================================================
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$configRoot = Join-Path $PSScriptRoot "..\config"
$functionRoot = Join-Path $repoRoot "functions"
$xamlPath = Join-Path $repoRoot "xaml\inputXML.xaml"
$mainScriptPath = Join-Path $repoRoot "scripts\main.ps1"
$buttonScriptPath = Join-Path $repoRoot "functions\public\Invoke-WPFButton.ps1"
$configCases = @(
Get-ChildItem -Path $configRoot -Filter *.json | ForEach-Object {
@{
Name = $_.Name
Path = $_.FullName
}
}
)
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$script:configRoot = Join-Path $script:repoRoot "config"
$script:functionRoot = Join-Path $script:repoRoot "functions"
$script:xamlPath = Join-Path $script:repoRoot "xaml\inputXML.xaml"
$script:mainScriptPath = Join-Path $script:repoRoot "scripts\main.ps1"
$script:buttonScriptPath = Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1"
function script:Get-WinUtilConfigObject {
param([string]$Name)
Get-Content -Path (Join-Path $script:configRoot "$Name.json") -Raw | ConvertFrom-Json
}
function script:Test-WinUtilHasProperty {
param(
[Parameter(Mandatory)]
[psobject]$Object,
[Parameter(Mandatory)]
[string]$Name
)
return @($Object.PSObject.Properties.Name) -contains $Name
}
function script:Test-WinUtilHasNonEmptyProperty {
param(
[Parameter(Mandatory)]
[psobject]$Object,
[Parameter(Mandatory)]
[string]$Name
)
if (-not (Test-WinUtilHasProperty -Object $Object -Name $Name)) {
return $false
}
$value = $Object.$Name
if ($null -eq $value) {
return $false
}
if ($value -is [string]) {
return -not [string]::IsNullOrWhiteSpace($value)
}
if ($value -is [array]) {
return @($value).Count -gt 0
}
return -not [string]::IsNullOrWhiteSpace([string]$value)
}
function script:Get-WinUtilMissingRequiredFields {
param(
[Parameter(Mandatory)]
[string]$EntryName,
[Parameter(Mandatory)]
[psobject]$Entry,
[Parameter(Mandatory)]
[string[]]$RequiredFields
)
foreach ($field in $RequiredFields) {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $Entry -Name $field)) {
"$EntryName missing $field"
}
}
}
function script:Get-WinUtilTopLevelFunctionNames {
Get-ChildItem -Path $script:functionRoot -Filter *.ps1 -Recurse | ForEach-Object {
$tokens = $null
$syntaxErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$syntaxErrors)
if ($syntaxErrors.Count -ne 0) {
throw ($syntaxErrors | Out-String)
}
$ast.EndBlock.Statements |
Where-Object { $_ -is [System.Management.Automation.Language.FunctionDefinitionAst] } |
ForEach-Object { $_.Name }
} | Sort-Object -Unique
}
function script:Get-WinUtilButtonSwitchNames {
$buttonSource = Get-Content -Path $script:buttonScriptPath -Raw
[regex]::Matches($buttonSource, '"(WPF[A-Za-z0-9_]+)"\s*\{') |
ForEach-Object { $_.Groups[1].Value } |
Sort-Object -Unique
}
}
Describe "Config files" {
foreach ($configCase in $configCases) {
It "imports $($configCase.Name) with no JSON errors" -TestCases $configCase {
param([string]$Name, [string]$Path)
try {
Get-Content -Path $Path -Raw | ConvertFrom-Json | Out-Null
} catch {
throw "Failed to import ${Name}: $_"
}
}
}
}
Describe "Applications config" {
$testCase = @{ Path = (Join-Path $configRoot "applications.json") }
It "contains at least one application" -TestCases $testCase {
param([string]$Path)
$applications = Get-Content -Path $Path -Raw | ConvertFrom-Json
$applicationEntries = @($applications.PSObject.Properties)
if ($applicationEntries.Count -eq 0) {
throw "applications.json does not contain any application entries."
}
}
It "contains required display fields and at least one install source" -TestCases $testCase {
param([string]$Path)
$applications = Get-Content -Path $Path -Raw | ConvertFrom-Json
$requiredFields = @("category", "content", "description", "link")
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $applications.PSObject.Properties) {
$entryFields = @($entry.Value.PSObject.Properties.Name)
foreach ($field in $requiredFields) {
if ($entryFields -notcontains $field -or [string]::IsNullOrWhiteSpace([string]$entry.Value.$field)) {
$invalidEntries.Add("$($entry.Name) missing $field")
}
}
$hasInstallSource = $false
foreach ($sourceField in @("winget", "choco")) {
if ($entryFields -contains $sourceField -and -not [string]::IsNullOrWhiteSpace([string]$entry.Value.$sourceField)) {
$hasInstallSource = $true
}
}
if (-not $hasInstallSource) {
$invalidEntries.Add("$($entry.Name) missing winget/choco install source")
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
}
Describe "Tweaks config" {
$testCase = @{ Path = (Join-Path $configRoot "tweaks.json") }
It "contains undo metadata for registry and service actions" -TestCases $testCase {
param([string]$Path)
$tweaks = Get-Content -Path $Path -Raw | ConvertFrom-Json
$invalidTweaks = New-Object System.Collections.Generic.List[string]
foreach ($tweak in $tweaks.PSObject.Properties) {
foreach ($registryEntry in @($tweak.Value.registry)) {
if ($null -eq $registryEntry) { continue }
if ($registryEntry.PSObject.Properties.Name -notcontains "OriginalValue" -or
[string]::IsNullOrWhiteSpace([string]$registryEntry.OriginalValue)) {
$invalidTweaks.Add("$($tweak.Name),registry")
}
}
foreach ($serviceEntry in @($tweak.Value.service)) {
if ($null -eq $serviceEntry) { continue }
if ($serviceEntry.PSObject.Properties.Name -notcontains "OriginalType" -or
[string]::IsNullOrWhiteSpace([string]$serviceEntry.OriginalType)) {
$invalidTweaks.Add("$($tweak.Name),service")
}
}
}
if ($invalidTweaks.Count -gt 0) {
throw ($invalidTweaks -join "`n")
}
}
}
Describe "Preset config" {
It "references existing config entries or supported actions" {
$preset = Get-WinUtilConfigObject -Name "preset"
$applications = Get-WinUtilConfigObject -Name "applications"
$tweaks = Get-WinUtilConfigObject -Name "tweaks"
$feature = Get-WinUtilConfigObject -Name "feature"
$appx = Get-WinUtilConfigObject -Name "appx"
$validReferences = @(
$tweaks.PSObject.Properties.Name
$feature.PSObject.Properties.Name
$appx.PSObject.Properties.Name
$applications.PSObject.Properties.Name | ForEach-Object { "WPFInstall$_" }
Get-WinUtilButtonSwitchNames
) | Sort-Object -Unique
$invalidReferences = New-Object System.Collections.Generic.List[string]
foreach ($presetEntry in $preset.PSObject.Properties) {
foreach ($reference in @($presetEntry.Value)) {
if ($validReferences -notcontains $reference) {
$invalidReferences.Add("$($presetEntry.Name) references missing item $reference")
}
}
}
if ($invalidReferences.Count -gt 0) {
throw ($invalidReferences -join "`n")
}
}
}
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`n$tabInitializerScript",
'Invoke-WPFUIElements\s+-configVariable\s+\$sync\.configs\.appnavigation\s+-targetGridName\s+"([^"]+)"'
)
if (-not $targetGridMatch.Success) {
throw "Startup tab initialization does not wire appnavigation through Invoke-WPFUIElements."
}
$xamlText = Get-Content -Path $script:xamlPath -Raw
$targetGridName = $targetGridMatch.Groups[1].Value
if ($xamlText -notmatch "Name=`"$([regex]::Escape($targetGridName))`"") {
throw "appnavigation target grid '$targetGridName' was not found in xaml/inputXML.xaml."
}
}
It "contains renderable entries with valid button and radio groups" {
$appnavigation = Get-WinUtilConfigObject -Name "appnavigation"
$feature = Get-WinUtilConfigObject -Name "feature"
$requiredFields = @("Content", "Category", "Type", "Order", "Description")
$allowedTypes = @("Button", "RadioButton", "Note")
$supportedButtons = @(
Get-WinUtilButtonSwitchNames
$feature.PSObject.Properties.Name
) | Sort-Object -Unique
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $appnavigation.PSObject.Properties) {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
if ($allowedTypes -notcontains $entry.Value.Type) {
$invalidEntries.Add("$($entry.Name) has unsupported Type '$($entry.Value.Type)'")
}
if ($entry.Value.Type -eq "Button" -and $supportedButtons -notcontains $entry.Name) {
$invalidEntries.Add("$($entry.Name) is not handled by Invoke-WPFButton or feature config")
}
if ($entry.Value.Type -eq "RadioButton") {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $entry.Value -Name "GroupName")) {
$invalidEntries.Add("$($entry.Name) missing GroupName")
}
if (-not (Test-WinUtilHasProperty -Object $entry.Value -Name "Checked")) {
$invalidEntries.Add("$($entry.Name) missing Checked")
}
}
}
$radioButtons = @($appnavigation.PSObject.Properties | Where-Object { $_.Value.Type -eq "RadioButton" })
foreach ($group in ($radioButtons | Group-Object -Property { $_.Value.GroupName })) {
if ([string]::IsNullOrWhiteSpace($group.Name)) {
$invalidEntries.Add("RadioButton group name is blank")
continue
}
$checkedCount = @($group.Group | Where-Object { $_.Value.Checked -eq $true }).Count
if ($checkedCount -ne 1) {
$invalidEntries.Add("RadioButton group '$($group.Name)' has $checkedCount checked entries")
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
}
Describe "UI-rendered config entries" {
It "contains required AppX fields" {
$appx = Get-WinUtilConfigObject -Name "appx"
$requiredFields = @("Category", "Content", "Description", "Panel", "PackageId")
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $appx.PSObject.Properties) {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
It "contains required DNS fields with parseable IP addresses" {
$dns = Get-WinUtilConfigObject -Name "dns"
$requiredFields = @("Primary", "Secondary", "Primary6", "Secondary6")
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $dns.PSObject.Properties) {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
foreach ($field in $requiredFields) {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $entry.Value -Name $field)) {
continue
}
try {
[System.Net.IPAddress]::Parse([string]$entry.Value.$field) | Out-Null
} catch {
$invalidEntries.Add("$($entry.Name) $field is not a parseable IP address")
}
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
It "contains required feature fields and valid configured functions" {
$feature = Get-WinUtilConfigObject -Name "feature"
$functionNames = Get-WinUtilTopLevelFunctionNames
$requiredFields = @("Content", "category", "panel", "link")
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $feature.PSObject.Properties) {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
if ($entry.Value.Type -and $entry.Value.Type -ne "Button") {
$invalidEntries.Add("$($entry.Name) has unsupported Type '$($entry.Value.Type)'")
}
if ($entry.Value.Type -eq "Button") {
if (-not $entry.Value.function -and -not $entry.Value.InvokeScript) {
$invalidEntries.Add("$($entry.Name) button missing function or InvokeScript")
}
} else {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $entry.Value -Name "Description")) {
$invalidEntries.Add("$($entry.Name) missing Description")
}
if (-not $entry.Value.feature -and -not $entry.Value.InvokeScript) {
$invalidEntries.Add("$($entry.Name) missing feature or InvokeScript action")
}
}
if ($entry.Value.function -and $functionNames -notcontains $entry.Value.function) {
$invalidEntries.Add("$($entry.Name) references missing function $($entry.Value.function)")
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
It "contains required tweak fields and valid action metadata" {
$tweaks = Get-WinUtilConfigObject -Name "tweaks"
$requiredFields = @("Content", "category", "panel", "link")
$allowedTypes = @("Button", "Combobox", "Toggle", "ToggleButton")
$supportedButtons = Get-WinUtilButtonSwitchNames
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $tweaks.PSObject.Properties) {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
if ($entry.Value.Type -and $allowedTypes -notcontains $entry.Value.Type) {
$invalidEntries.Add("$($entry.Name) has unsupported Type '$($entry.Value.Type)'")
}
if ($entry.Value.Type -eq "Button") {
if ($supportedButtons -notcontains $entry.Name) {
$invalidEntries.Add("$($entry.Name) is not handled by Invoke-WPFButton")
}
} elseif ($entry.Value.Type -eq "Combobox") {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $entry.Value -Name "ComboItems")) {
$invalidEntries.Add("$($entry.Name) combobox missing ComboItems")
}
} else {
if (-not (Test-WinUtilHasNonEmptyProperty -Object $entry.Value -Name "Description")) {
$invalidEntries.Add("$($entry.Name) missing Description")
}
if (-not $entry.Value.registry -and -not $entry.Value.service -and -not $entry.Value.InvokeScript -and -not $entry.Value.appx) {
$invalidEntries.Add("$($entry.Name) missing registry, service, InvokeScript, or appx action")
}
}
foreach ($registryEntry in @($entry.Value.registry)) {
if ($null -eq $registryEntry) { continue }
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName "$($entry.Name),registry" -Entry $registryEntry -RequiredFields @("Path", "Name", "Type", "Value", "OriginalValue"))) {
$invalidEntries.Add($missingField)
}
}
foreach ($serviceEntry in @($entry.Value.service)) {
if ($null -eq $serviceEntry) { continue }
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName "$($entry.Name),service" -Entry $serviceEntry -RequiredFields @("Name", "StartupType", "OriginalType"))) {
$invalidEntries.Add($missingField)
}
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
It "defines theme resources required by XAML rendering" {
$themes = Get-WinUtilConfigObject -Name "themes"
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($themeName in @("shared", "Light", "Dark")) {
if (-not (Test-WinUtilHasProperty -Object $themes -Name $themeName)) {
$invalidEntries.Add("themes.json missing $themeName")
continue
}
foreach ($property in $themes.$themeName.PSObject.Properties) {
if ([string]::IsNullOrWhiteSpace([string]$property.Value)) {
$invalidEntries.Add("$themeName.$($property.Name) is blank")
}
}
}
$lightKeys = @($themes.Light.PSObject.Properties.Name)
$darkKeys = @($themes.Dark.PSObject.Properties.Name)
foreach ($key in $lightKeys) {
if ($darkKeys -notcontains $key) {
$invalidEntries.Add("Dark theme missing $key")
}
}
foreach ($key in $darkKeys) {
if ($lightKeys -notcontains $key) {
$invalidEntries.Add("Light theme missing $key")
}
}
$xamlText = Get-Content -Path $script:xamlPath -Raw
$dynamicResourceNames = @(
[regex]::Matches($xamlText, '\{DynamicResource\s+([^\}\s]+)') |
ForEach-Object { $_.Groups[1].Value }
) | Sort-Object -Unique
$xamlDefinedResourceNames = @(
[regex]::Matches($xamlText, 'x:Key="([^"]+)"') |
ForEach-Object { $_.Groups[1].Value }
) | Sort-Object -Unique
$themeResourceNames = @(
$themes.shared.PSObject.Properties.Name
$themes.Light.PSObject.Properties.Name
$themes.Dark.PSObject.Properties.Name
"CBorderColor"
"CButtonBackgroundMouseoverColor"
) | Sort-Object -Unique
foreach ($resourceName in $dynamicResourceNames) {
if ($xamlDefinedResourceNames -notcontains $resourceName -and $themeResourceNames -notcontains $resourceName) {
$invalidEntries.Add("XAML DynamicResource '$resourceName' is not defined in themes.json or XAML resources")
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
}
Describe "Embedded config scripts" {
It "parse as PowerShell scriptblocks" {
$invalidScripts = New-Object System.Collections.Generic.List[string]
foreach ($configFile in (Get-ChildItem -Path $script:configRoot -Filter *.json)) {
$config = Get-Content -Path $configFile.FullName -Raw | ConvertFrom-Json
foreach ($entry in $config.PSObject.Properties) {
foreach ($field in @("InvokeScript", "UndoScript")) {
if (-not (Test-WinUtilHasProperty -Object $entry.Value -Name $field)) {
continue
}
$index = 0
foreach ($scriptText in @($entry.Value.$field)) {
$index++
if ([string]::IsNullOrWhiteSpace([string]$scriptText)) {
continue
}
try {
[scriptblock]::Create([string]$scriptText) | Out-Null
} catch {
$invalidScripts.Add("$($configFile.Name):$($entry.Name).$field[$index] $($psitem.Exception.Message)")
}
}
}
}
}
if ($invalidScripts.Count -gt 0) {
throw ($invalidScripts -join "`n")
}
}
}