mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-07-16 00:40:39 +00:00
Harden Pester CI and discard runspace return values
This commit is contained in:
4
.github/workflows/unittests.yaml
vendored
4
.github/workflows/unittests.yaml
vendored
@@ -27,13 +27,13 @@ jobs:
|
||||
- name: Install Pester
|
||||
run: |
|
||||
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
|
||||
Install-Module -Name Pester -Force -SkipPublisherCheck -AllowClobber
|
||||
Install-Module -Name Pester -MinimumVersion 5.0.0 -Force -SkipPublisherCheck -AllowClobber
|
||||
shell: pwsh
|
||||
|
||||
- name: Run Pester tests
|
||||
run: |
|
||||
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
|
||||
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed
|
||||
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed -CI
|
||||
|
||||
shell: pwsh
|
||||
env:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ Microsoft.PowerShell.ConsoleHost.dll
|
||||
winutil.exe.config
|
||||
winutil.ps1
|
||||
binary/
|
||||
testResults.xml
|
||||
|
||||
# general software/os specific
|
||||
desktop.ini
|
||||
|
||||
@@ -34,11 +34,11 @@ function Invoke-WPFRunspace {
|
||||
$script:powershell = [powershell]::Create()
|
||||
|
||||
# Add Scriptblock and Arguments to runspace
|
||||
$script:powershell.AddScript($ScriptBlock)
|
||||
$script:powershell.AddArgument($ArgumentList)
|
||||
[void]$script:powershell.AddScript($ScriptBlock)
|
||||
[void]$script:powershell.AddArgument($ArgumentList)
|
||||
|
||||
foreach ($parameter in $ParameterList) {
|
||||
$script:powershell.AddParameter($parameter[0], $parameter[1])
|
||||
[void]$script:powershell.AddParameter($parameter[0], $parameter[1])
|
||||
}
|
||||
|
||||
$script:powershell.RunspacePool = $sync.runspace
|
||||
|
||||
201
pester/sanity.Tests.ps1
Normal file
201
pester/sanity.Tests.ps1
Normal file
@@ -0,0 +1,201 @@
|
||||
#===========================================================================
|
||||
# Tests - General Sanity
|
||||
#===========================================================================
|
||||
|
||||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
|
||||
BeforeAll {
|
||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
|
||||
function script:Test-WinUtilParser {
|
||||
param([string]$Path)
|
||||
|
||||
$tokens = $null
|
||||
$syntaxErrors = $null
|
||||
[System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$syntaxErrors) | Out-Null
|
||||
|
||||
if ($syntaxErrors.Count -ne 0) {
|
||||
throw ($syntaxErrors | Out-String)
|
||||
}
|
||||
}
|
||||
|
||||
function script:Invoke-WindowsPowerShellParser {
|
||||
param([string[]]$Path)
|
||||
|
||||
$windowsPowerShell = Get-Command powershell.exe -ErrorAction SilentlyContinue
|
||||
if (-not $windowsPowerShell) {
|
||||
Set-ItResult -Skipped -Because "powershell.exe is not available on this platform."
|
||||
return
|
||||
}
|
||||
|
||||
$previousPaths = $env:WINUTIL_TEST_PARSE_PATHS
|
||||
try {
|
||||
$env:WINUTIL_TEST_PARSE_PATHS = @($Path) -join [Environment]::NewLine
|
||||
$parseScript = @'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$paths = $env:WINUTIL_TEST_PARSE_PATHS -split "`r?`n" | Where-Object { $_ }
|
||||
$failed = @()
|
||||
|
||||
foreach ($path in $paths) {
|
||||
$tokens = $null
|
||||
$syntaxErrors = $null
|
||||
[System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$syntaxErrors) | Out-Null
|
||||
|
||||
if ($syntaxErrors.Count -ne 0) {
|
||||
$messages = $syntaxErrors | ForEach-Object { $_.Message }
|
||||
$failed += "[$path] $($messages -join '; ')"
|
||||
}
|
||||
}
|
||||
|
||||
if ($failed.Count -gt 0) {
|
||||
$failed -join [Environment]::NewLine
|
||||
exit 1
|
||||
}
|
||||
'@
|
||||
|
||||
$output = & $windowsPowerShell.Source -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command $parseScript 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Windows PowerShell parser failed:`n$($output | Out-String)"
|
||||
}
|
||||
} finally {
|
||||
if ($null -eq $previousPaths) {
|
||||
Remove-Item Env:\WINUTIL_TEST_PARSE_PATHS -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
$env:WINUTIL_TEST_PARSE_PATHS = $previousPaths
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "PowerShell source sanity" {
|
||||
$scriptCases = @(
|
||||
"Compile.ps1",
|
||||
"windev.ps1",
|
||||
"scripts\start.ps1",
|
||||
"scripts\main.ps1"
|
||||
) | ForEach-Object {
|
||||
@{
|
||||
Name = $_
|
||||
Path = Join-Path $repoRoot $_
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($scriptCase in $scriptCases) {
|
||||
It "parses $($scriptCase.Name) with the current PowerShell parser" -TestCases $scriptCase {
|
||||
param([string]$Path)
|
||||
|
||||
Test-WinUtilParser -Path $Path
|
||||
}
|
||||
}
|
||||
|
||||
It "parses WinUtil source files with Windows PowerShell when available" {
|
||||
$sourcePaths = @(
|
||||
Join-Path $script:repoRoot "Compile.ps1"
|
||||
Join-Path $script:repoRoot "windev.ps1"
|
||||
Join-Path $script:repoRoot "scripts\start.ps1"
|
||||
Join-Path $script:repoRoot "scripts\main.ps1"
|
||||
Get-ChildItem -Path (Join-Path $script:repoRoot "functions") -Filter *.ps1 -Recurse | Select-Object -ExpandProperty FullName
|
||||
)
|
||||
|
||||
Invoke-WindowsPowerShellParser -Path $sourcePaths
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Compiled WinUtil sanity" {
|
||||
BeforeAll {
|
||||
$script:compiledPath = Join-Path $script:repoRoot "winutil.ps1"
|
||||
|
||||
Push-Location $script:repoRoot
|
||||
try {
|
||||
& (Join-Path $script:repoRoot "Compile.ps1")
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
It "generates winutil.ps1" {
|
||||
Test-Path -Path $script:compiledPath | Should -BeTrue
|
||||
}
|
||||
|
||||
It "parses compiled winutil.ps1 with the current PowerShell parser" {
|
||||
Test-WinUtilParser -Path $script:compiledPath
|
||||
}
|
||||
|
||||
It "parses compiled winutil.ps1 with Windows PowerShell when available" {
|
||||
Invoke-WindowsPowerShellParser -Path $script:compiledPath
|
||||
}
|
||||
|
||||
It "contains embedded configs, XAML, autounattend XML, and runspace bootstrap" {
|
||||
$content = Get-Content -Path $script:compiledPath -Raw
|
||||
$requiredSnippets = @(
|
||||
('$sync.configs.applications = @' + "'"),
|
||||
('$inputXML = @' + "'"),
|
||||
('$WinUtilAutounattendXml = @' + "'"),
|
||||
"SessionStateVariableEntry -ArgumentList 'sync'",
|
||||
"SessionStateFunctionEntry",
|
||||
"[runspacefactory]::CreateRunspacePool",
|
||||
"function Invoke-WPFRunspace"
|
||||
)
|
||||
|
||||
foreach ($snippet in $requiredSnippets) {
|
||||
if (-not $content.Contains($snippet)) {
|
||||
throw "Compiled script is missing expected content: $snippet"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Runspace sanity" {
|
||||
BeforeAll {
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1")
|
||||
}
|
||||
|
||||
It "returns a single async handle and runs a scriptblock with arguments in the shared runspace pool" {
|
||||
$script:sync = [Hashtable]::Synchronized(@{ SmokeValue = "shared" })
|
||||
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
|
||||
$syncVariable = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList "sync", $script:sync, $null
|
||||
$initialSessionState.Variables.Add($syncVariable)
|
||||
$script:sync.runspace = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
|
||||
$script:sync.runspace.Open()
|
||||
|
||||
$ended = $false
|
||||
try {
|
||||
$handle = Invoke-WPFRunspace -ArgumentList "argument" -ParameterList @(,("NamedValue", "parameter")) -ScriptBlock {
|
||||
param($ArgumentValue, [string]$NamedValue)
|
||||
|
||||
Start-Sleep -Milliseconds 200
|
||||
"$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"
|
||||
} 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user