Expand Pester coverage across install, tweaks, and UI workflows (#4792)

* Harden Pester CI and discard runspace return values

* Add prioritized test backlog

* Add WinUtil action logging

* Add config integrity tests

* Add XAML control wiring tests

* Add registry and service helper tests

* Add package manager tests

* Add runspace behavior tests

* Add tweak orchestration tests

* Add install workflow tests

* Add update profile tests

* Add AppX removal tests

* Log package installer output

* Add UI state helper tests

* Pin Pester test runner version

* Expand Win11 Creator tests

* Add preferences and theme tests

* Add search filter tests

* Add compile contract tests

* Use single WinUtil session log

* Remove installer output logging helper

* Handle locked WinUtil transcript log

* Avoid appending to active transcript log

* Log install uninstall package identities
This commit is contained in:
Chris Titus
2026-07-01 21:49:15 -05:00
committed by GitHub
parent c3b4f85fce
commit 2cb20893fc
44 changed files with 4586 additions and 36 deletions
@@ -0,0 +1,28 @@
function Get-WinUtilPackageLogSummary {
param(
[Parameter(Mandatory = $true)]
[object[]]$Packages,
[Parameter(Mandatory = $true)]
[PackageManagers]$Preference
)
@($Packages | ForEach-Object {
$package = $_
$packageName = @($package.Name, $package.Description, $package.winget, $package.choco) |
Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) -and $_ -ne "na" } |
Select-Object -First 1
if ([string]::IsNullOrWhiteSpace([string]$packageName)) {
$packageName = "Unknown package"
}
if ($Preference -eq [PackageManagers]::Choco -and -not [string]::IsNullOrWhiteSpace([string]$package.choco) -and $package.choco -ne "na") {
"$packageName (choco: $($package.choco))"
} elseif (-not [string]::IsNullOrWhiteSpace([string]$package.winget) -and $package.winget -ne "na") {
"$packageName (winget: $($package.winget))"
} else {
"$packageName (no package id)"
}
})
}
@@ -21,17 +21,32 @@ function Get-WinUtilSelectedPackages {
$packages[[PackageManagers]::Winget] = $packagesWinget
$packages[[PackageManagers]::Choco] = $packagesChoco
function Add-PackageId {
param(
[System.Collections.ArrayList]$Target,
$PackageId
)
if ([string]::IsNullOrWhiteSpace([string]$PackageId) -or $PackageId -eq "na") {
return
}
if (-not $Target.Contains($PackageId)) {
$null = $Target.Add($PackageId)
}
}
foreach ($package in $PackageList) {
switch ($Preference) {
"Choco" {
if ($package.choco -eq "na") {
$null = $packagesWinget.add($package.winget)
if ([string]::IsNullOrWhiteSpace([string]$package.choco) -or $package.choco -eq "na") {
Add-PackageId -Target $packagesWinget -PackageId $package.winget
} else {
$null = $packagesChoco.add($package.choco)
Add-PackageId -Target $packagesChoco -PackageId $package.choco
}
}
"Winget" {
$null = $packagesWinget.add($package.winget)
Add-PackageId -Target $packagesWinget -PackageId $package.winget
}
}
}
@@ -9,8 +9,12 @@ function Install-WinUtilProgramChoco {
)
if ($Action -eq 'Install') {
Start-Process -FilePath choco -ArgumentList "install $Programs -y" -NoNewWindow -Wait
$arguments = "install $Programs -y"
} else {
Start-Process -FilePath choco -ArgumentList "uninstall $Programs -y" -NoNewWindow -Wait
$arguments = "uninstall $Programs -y"
}
Write-WinUtilLog -Component "Package" -Message "$Action choco package(s): $($Programs -join ', ')"
$process = Start-Process -FilePath choco -ArgumentList $arguments -NoNewWindow -Wait -PassThru
Write-WinUtilLog -Component "Package" -Message "$Action choco package(s) completed: $($Programs -join ', ') (exit code: $($process.ExitCode))"
}
@@ -20,9 +20,13 @@ Function Install-WinUtilProgramWinget {
}
if ($Action -eq 'Install') {
Start-Process -FilePath winget -ArgumentList @("install", "--id", $program, "--accept-package-agreements", "--accept-source-agreements", "--source", $source, "--silent") -NoNewWindow -Wait
$arguments = @("install", "--id", $program, "--accept-package-agreements", "--accept-source-agreements", "--source", $source, "--silent")
} else {
Start-Process -FilePath winget -ArgumentList @("uninstall", "--id", $program, "--source", $source, "--silent") -NoNewWindow -Wait
$arguments = @("uninstall", "--id", $program, "--source", $source, "--silent")
}
Write-WinUtilLog -Component "Package" -Message "$Action winget package: $program (source: $source)"
$process = Start-Process -FilePath winget -ArgumentList $arguments -NoNewWindow -Wait -PassThru
Write-WinUtilLog -Component "Package" -Message "$Action winget package completed: $program (exit code: $($process.ExitCode))"
}
}
@@ -1,16 +1,22 @@
function Invoke-WinUtilFeatureInstall ($CheckBox) {
Write-WinUtilLog -Component "Feature" -Message "Applying feature action: $CheckBox"
if ($sync.configs.feature.$CheckBox.feature) {
foreach ($feature in $sync.configs.feature.$CheckBox.feature) {
Write-Host "Installing $feature"
Write-WinUtilLog -Component "Feature" -Message "Enabling Windows optional feature: $feature"
Enable-WindowsOptionalFeature -Online -FeatureName $feature -All -NoRestart -ErrorAction Stop
Write-WinUtilLog -Component "Feature" -Message "Enabled Windows optional feature: $feature"
}
}
if ($sync.configs.feature.$CheckBox.InvokeScript) {
foreach ($script in $sync.configs.feature.$CheckBox.InvokeScript) {
Write-Host "Running Script for $CheckBox"
Write-WinUtilLog -Component "Feature" -Message "Running feature script for: $CheckBox"
Invoke-Command -ScriptBlock ([scriptblock]::Create($script)) -ErrorAction Stop
Write-WinUtilLog -Component "Feature" -Message "Completed feature script for: $CheckBox"
}
}
Write-WinUtilLog -Component "Feature" -Message "Feature action completed: $CheckBox"
}
@@ -22,23 +22,30 @@ function Invoke-WinUtilScript {
try {
Write-Host "Running Script for $Name"
Write-WinUtilLog -Component "Script" -Message "Running script for $Name"
Invoke-Command $scriptblock -ErrorAction Stop
Write-WinUtilLog -Component "Script" -Message "Completed script for $Name"
} catch [System.Management.Automation.CommandNotFoundException] {
Write-Warning "The specified command was not found."
Write-Warning $PSItem.Exception.message
Write-WinUtilLog -Level "ERROR" -Component "Script" -Message "Command not found while running script for $Name`: $($PSItem.Exception.Message)"
} catch [System.Management.Automation.RuntimeException] {
Write-Warning "A runtime exception occurred."
Write-Warning $PSItem.Exception.message
Write-WinUtilLog -Level "ERROR" -Component "Script" -Message "Runtime exception while running script for $Name`: $($PSItem.Exception.Message)"
} catch [System.Security.SecurityException] {
Write-Warning "A security exception occurred."
Write-Warning $PSItem.Exception.message
Write-WinUtilLog -Level "ERROR" -Component "Script" -Message "Security exception while running script for $Name`: $($PSItem.Exception.Message)"
} catch [System.UnauthorizedAccessException] {
Write-Warning "Access denied. You do not have permission to perform this operation."
Write-Warning $PSItem.Exception.message
Write-WinUtilLog -Level "ERROR" -Component "Script" -Message "Access denied while running script for $Name`: $($PSItem.Exception.Message)"
} catch {
# Generic catch block to handle any other type of exception
Write-Warning "Unable to run script for $Name due to unhandled exception."
Write-Warning $psitem.Exception.StackTrace
Write-WinUtilLog -Level "ERROR" -Component "Script" -Message "Unhandled exception while running script for $Name`: $($psitem.Exception.Message)"
}
}
@@ -21,6 +21,9 @@ function Invoke-WinUtilTweaks {
$KeepServiceStartup = $true
)
$action = if ($undo) { "Undo" } else { "Apply" }
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak: $CheckBox"
if ($undo) {
$Values = @{
Registry = "OriginalValue"
@@ -77,4 +80,5 @@ function Invoke-WinUtilTweaks {
}
}
}
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak completed: $CheckBox"
}
+2
View File
@@ -16,6 +16,8 @@ function Remove-WinUtilAPPX {
)
Write-Host "Removing $Name"
Write-WinUtilLog -Component "AppX" -Message "Removing AppX package pattern: $Name"
Get-AppxPackage $Name -AllUsers | Remove-AppxPackage -AllUsers
Get-AppxProvisionedPackage -Online | Where-Object DisplayName -like $Name | Remove-AppxProvisionedPackage -Online
Write-WinUtilLog -Component "AppX" -Message "AppX removal completed for package pattern: $Name"
}
+10 -1
View File
@@ -12,22 +12,31 @@ function Set-WinUtilDNS {
#>
param($DNSProvider)
if($DNSProvider -eq "Default") {return}
if($DNSProvider -eq "Default") {
Write-WinUtilLog -Component "DNS" -Message "DNS provider is Default; no DNS changes applied."
return
}
try {
$Adapters = Get-NetAdapter | Where-Object {$_.Status -eq "Up"}
Write-Host "Ensuring DNS is set to $DNSProvider on the following interfaces:"
Write-Host $($Adapters | Out-String)
Write-WinUtilLog -Component "DNS" -Message "Setting DNS provider to $DNSProvider for $(@($Adapters).Count) active adapter(s)."
Foreach ($Adapter in $Adapters) {
if($DNSProvider -eq "DHCP") {
Write-WinUtilLog -Component "DNS" -Message "Resetting DNS to DHCP on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex))."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ResetServerAddresses
} else {
Write-WinUtilLog -Component "DNS" -Message "Setting IPv4 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($sync.configs.dns.$DNSProvider.Primary), $($sync.configs.dns.$DNSProvider.Secondary)."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ("$($sync.configs.dns.$DNSProvider.Primary)", "$($sync.configs.dns.$DNSProvider.Secondary)")
Write-WinUtilLog -Component "DNS" -Message "Setting IPv6 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($sync.configs.dns.$DNSProvider.Primary6), $($sync.configs.dns.$DNSProvider.Secondary6)."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ("$($sync.configs.dns.$DNSProvider.Primary6)", "$($sync.configs.dns.$DNSProvider.Secondary6)")
}
}
Write-WinUtilLog -Component "DNS" -Message "DNS provider change completed: $DNSProvider"
} catch {
Write-Warning "Unable to set DNS Provider due to an unhandled exception."
Write-Warning $psitem.Exception.StackTrace
Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "Unable to set DNS provider $DNSProvider`: $($psitem.Exception.Message)"
}
}
@@ -32,25 +32,32 @@ function Set-WinUtilRegistry {
If (!(Test-Path $Path)) {
Write-Host "$Path was not found. Creating..."
Write-WinUtilLog -Component "Registry" -Message "Creating registry path: $Path"
New-Item -Path $Path -Force -ErrorAction Stop | Out-Null
}
if ($Value -ne "<RemoveEntry>") {
Write-Host "Set $Path\$Name to $Value"
Write-WinUtilLog -Component "Registry" -Message "Setting $Path\$Name ($Type) to $Value"
Set-ItemProperty -Path $Path -Name $Name -Type $Type -Value $Value -Force -ErrorAction Stop | Out-Null
}
else{
Write-Host "Remove $Path\$Name"
Write-WinUtilLog -Component "Registry" -Message "Removing $Path\$Name"
Remove-ItemProperty -Path $Path -Name $Name -Force -ErrorAction Stop | Out-Null
}
} catch [System.Security.SecurityException] {
Write-Warning "Unable to set $Path\$Name to $Value due to a Security Exception."
Write-WinUtilLog -Level "ERROR" -Component "Registry" -Message "Security exception while changing $Path\$Name to $Value`: $($psitem.Exception.Message)"
} catch [System.Management.Automation.ItemNotFoundException] {
Write-Warning $psitem.Exception.ErrorRecord
Write-WinUtilLog -Level "ERROR" -Component "Registry" -Message "Registry item not found while changing $Path\$Name`: $($psitem.Exception.Message)"
} catch [System.UnauthorizedAccessException] {
Write-Warning $psitem.Exception.Message
Write-WinUtilLog -Level "ERROR" -Component "Registry" -Message "Unauthorized while changing $Path\$Name`: $($psitem.Exception.Message)"
} catch {
Write-Warning "Unable to set $Name due to unhandled exception."
Write-Warning $psitem.Exception.StackTrace
Write-WinUtilLog -Level "ERROR" -Component "Registry" -Message "Unhandled exception while changing $Path\$Name`: $($psitem.Exception.Message)"
}
}
+16 -4
View File
@@ -20,21 +20,33 @@ Function Set-WinUtilService {
)
try {
Write-Host "Setting Service $Name to $StartupType"
Write-WinUtilLog -Component "Service" -Message "Setting service $Name startup type to $StartupType"
# Check if the service exists
$service = Get-Service -Name $Name -ErrorAction Stop
if (($service.PSObject.Properties.Name -contains "StartType") -and ([string]$service.StartType -eq [string]$StartupType) ) {
Write-Host "Service $Name is already set to $StartupType"
Write-WinUtilLog -Component "Service" -Message "Service $Name startup type is already $StartupType; no change needed."
return
}
# Service exists, proceed with changing properties -- while handling auto delayed start for PWSH 5
if (($PSVersionTable.PSVersion.Major -lt 7) -and ($StartupType -eq "AutomaticDelayedStart")) {
sc.exe config $Name start=delayed-auto
} else {
$service | Set-Service -StartupType $StartupType -ErrorAction Stop
}
} catch [System.ServiceProcess.ServiceNotFoundException] {
Write-Warning "Service $Name was not found."
Write-WinUtilLog -Component "Service" -Message "Service $Name startup type set to $StartupType"
} catch {
Write-Warning "Unable to set $Name due to unhandled exception."
Write-Warning $_.Exception.Message
if ($_.FullyQualifiedErrorId -like "NoServiceFoundForGivenName,*") {
Write-Warning "Service $Name was not found."
Write-WinUtilLog -Level "WARN" -Component "Service" -Message "Service $Name was not found."
} else {
Write-Warning "Unable to set $Name due to unhandled exception."
Write-Warning $_.Exception.Message
Write-WinUtilLog -Level "ERROR" -Component "Service" -Message "Unable to set service $Name to $StartupType`: $($_.Exception.Message)"
}
}
}
+14
View File
@@ -0,0 +1,14 @@
function Show-WinUtilMessage {
<#
.SYNOPSIS
Shows a WinUtil message box and returns the selected result.
#>
param (
[string]$Message,
[string]$Title = "Winutil",
$Button = "OK",
$Icon = "Information"
)
[System.Windows.MessageBox]::Show($Message, $Title, $Button, $Icon)
}
+81
View File
@@ -0,0 +1,81 @@
function Write-WinUtilLog {
<#
.SYNOPSIS
Writes a timestamped WinUtil log entry to the active session log.
.PARAMETER Message
The message to write.
.PARAMETER Level
The severity level for the log entry.
.PARAMETER Component
The WinUtil component producing the log entry.
#>
param (
[Parameter(Mandatory = $true)]
[string]$Message,
[ValidateSet("INFO", "WARN", "ERROR", "DEBUG")]
[string]$Level = "INFO",
[string]$Component = "WinUtil"
)
try {
$logPath = $null
$transcriptPath = $null
if ($null -ne $sync -and $sync.ContainsKey("logPath")) {
$logPath = $sync.logPath
}
if ($null -ne $sync -and $sync.ContainsKey("transcriptPath")) {
$transcriptPath = $sync.transcriptPath
}
if ([string]::IsNullOrWhiteSpace($logPath) -and -not [string]::IsNullOrWhiteSpace($transcriptPath)) {
$logPath = $transcriptPath
}
if ([string]::IsNullOrWhiteSpace($logPath) -and $null -ne $sync -and $sync.ContainsKey("winutildir")) {
$logDirectory = Join-Path $sync.winutildir "logs"
$logPath = Join-Path $logDirectory "winutil_$(Get-Date -Format "yyyy-MM-dd_HH-mm-ss").log"
$sync.logPath = $logPath
}
if ([string]::IsNullOrWhiteSpace($logPath) -and -not [string]::IsNullOrWhiteSpace($env:LocalAppData)) {
if ([string]::IsNullOrWhiteSpace($script:WinUtilLogPath)) {
$logDirectory = Join-Path (Join-Path $env:LocalAppData "winutil") "logs"
$script:WinUtilLogPath = Join-Path $logDirectory "winutil_$(Get-Date -Format "yyyy-MM-dd_HH-mm-ss").log"
}
$logPath = $script:WinUtilLogPath
}
if ([string]::IsNullOrWhiteSpace($logPath)) {
return
}
$logDirectory = Split-Path -Path $logPath -Parent
if (-not (Test-Path $logDirectory)) {
New-Item -Path $logDirectory -ItemType Directory -Force | Out-Null
}
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
$line = "[$timestamp] [$Level] [$Component] $Message"
if (-not [string]::IsNullOrWhiteSpace($transcriptPath) -and $logPath -eq $transcriptPath) {
Write-Host $line
return
}
try {
Add-Content -Path $logPath -Value $line -Encoding UTF8 -ErrorAction Stop
} catch [System.IO.IOException] {
Write-Host $line
}
} catch {
Write-Warning "Unable to write WinUtil log entry: $($_.Exception.Message)"
}
}