mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-07-16 00:40:39 +00:00
* Clean up analyzer warnings * Align analyzer warning cleanup policy * Run PSScriptAnalyzer directly in unittests workflow * Bind lazy-rendered buttons to click handlers * Remove WinUtil performance tracing * Fix Win11 ISO creator temp directory reuse
257 lines
13 KiB
PowerShell
257 lines
13 KiB
PowerShell
#===========================================================================
|
|
# Tests - Win11 Creator
|
|
#===========================================================================
|
|
|
|
Describe "Win11 Creator setup media" {
|
|
BeforeAll {
|
|
$script:repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
|
$script:isoWorkflowPath = Join-Path $script:repoRoot "functions\private\Invoke-WinUtilISO.ps1"
|
|
$script:isoScriptPath = Join-Path $script:repoRoot "functions\private\Invoke-WinUtilISOScript.ps1"
|
|
$script:autoUnattendPath = Join-Path $script:repoRoot "tools\autounattend.xml"
|
|
|
|
function Get-WinUtilFunctionText {
|
|
param (
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$FunctionName
|
|
)
|
|
|
|
$tokens = $null
|
|
$errors = $null
|
|
$ast = [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $Path), [ref]$tokens, [ref]$errors)
|
|
if ($errors.Count -gt 0) {
|
|
throw "Unable to parse $Path`: $($errors[0].Message)"
|
|
}
|
|
|
|
$functionAst = $ast.Find({
|
|
param($node)
|
|
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
|
|
$node.Name -eq $FunctionName
|
|
}, $true)
|
|
|
|
if (-not $functionAst) {
|
|
throw "Unable to find function $FunctionName in $Path."
|
|
}
|
|
|
|
return $functionAst.Extent.Text
|
|
}
|
|
|
|
$script:modifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOModify"
|
|
$script:editionIdFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Get-WinUtilEditionIdFromName"
|
|
$script:addDriversFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Add-DriversToImage"
|
|
$script:answerFileChildElementFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Get-WinUtilISOScriptChildElement"
|
|
$script:answerFileConversionFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "ConvertTo-WinUtilISOAnswerFile"
|
|
$script:editionConfigFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Write-WinUtilISOEditionConfig"
|
|
}
|
|
|
|
It "autounattend template does not force a product key" {
|
|
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
|
|
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
|
|
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
|
|
|
|
$productKeyCount = $xml.SelectNodes("//u:ProductKey", $nsMgr).Count
|
|
if ($productKeyCount -ne 0) {
|
|
throw "Expected no ProductKey nodes, found $productKeyCount."
|
|
}
|
|
}
|
|
|
|
It "ISO script accepts selected edition setup metadata" {
|
|
$isoScriptPath = Join-Path $PSScriptRoot "..\functions\private\Invoke-WinUtilISOScript.ps1"
|
|
$content = Get-Content -Path $isoScriptPath -Raw
|
|
|
|
foreach ($pattern in @(
|
|
'\[string\]\$InstallEditionId',
|
|
'\[int\]\$InstallImageIndex',
|
|
'sources\\ei\.cfg',
|
|
'PID\.txt'
|
|
)) {
|
|
if ($content -notmatch $pattern) {
|
|
throw "Expected Invoke-WinUtilISOScript.ps1 to match pattern: $pattern"
|
|
}
|
|
}
|
|
}
|
|
|
|
It "starts each new ISO modification in a fresh working directory" {
|
|
foreach ($expectedText in @(
|
|
'$workDir = Join-Path $env:TEMP "WinUtil_Win11ISO_$(Get-Date -Format ''yyyyMMdd_HHmmss'')"',
|
|
'$workDir = Join-Path $env:TEMP "WinUtil_Win11ISO_$(Get-Date -Format ''yyyyMMdd_HHmmss'')_$(([guid]::NewGuid()).ToString(''N'').Substring(0, 8))"'
|
|
)) {
|
|
$script:modifyFunction | Should -Match ([regex]::Escape($expectedText))
|
|
}
|
|
|
|
$script:modifyFunction | Should -Not -Match ([regex]::Escape("Reusing existing temp directory"))
|
|
}
|
|
|
|
It "mounts the copied image file that was verified from the ISO" {
|
|
foreach ($expectedText in @(
|
|
'$sourceImageFileName = Split-Path $wimPath -Leaf',
|
|
'$localWim = Join-Path $isoContents "sources\$sourceImageFileName"',
|
|
'Copied ISO image file not found: sources\$sourceImageFileName'
|
|
)) {
|
|
$script:modifyFunction | Should -Match ([regex]::Escape($expectedText))
|
|
}
|
|
}
|
|
|
|
It "maps Windows edition names to setup edition IDs" {
|
|
. ([scriptblock]::Create($script:editionIdFunction))
|
|
|
|
$cases = @{
|
|
"Windows 11 Home Single Language" = "CoreSingleLanguage"
|
|
"Windows 11 Home N" = "CoreN"
|
|
"Windows 11 Home" = "Core"
|
|
"Windows 11 Pro for Workstations N" = "ProfessionalWorkstationN"
|
|
"Windows 11 Pro for Workstations" = "ProfessionalWorkstation"
|
|
"Windows 11 Pro Education N" = "ProfessionalEducationN"
|
|
"Windows 11 Pro Education" = "ProfessionalEducation"
|
|
"Windows 11 Pro N" = "ProfessionalN"
|
|
"Windows 11 Pro" = "Professional"
|
|
"Windows 11 Education N" = "EducationN"
|
|
"Windows 11 Education" = "Education"
|
|
"Windows 11 Enterprise LTSC N" = "EnterpriseSN"
|
|
"Windows 11 Enterprise LTSC" = "EnterpriseS"
|
|
"Windows 11 Enterprise N" = "EnterpriseN"
|
|
"Windows 11 Enterprise" = "Enterprise"
|
|
}
|
|
|
|
foreach ($case in $cases.GetEnumerator()) {
|
|
Get-WinUtilEditionIdFromName -EditionName $case.Key | Should -Be $case.Value
|
|
}
|
|
|
|
Get-WinUtilEditionIdFromName -EditionName "Windows 11 Unknown Edition" | Should -Be ""
|
|
}
|
|
|
|
It "writes ei.cfg and removes stale PID.txt for selected editions" {
|
|
. ([scriptblock]::Create($script:editionConfigFunction))
|
|
|
|
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
|
|
$sourcesDir = Join-Path $contentRoot "sources"
|
|
$logs = [System.Collections.Generic.List[string]]::new()
|
|
$logger = { param($message) $logs.Add([string]$message) }
|
|
|
|
try {
|
|
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
|
|
Set-Content -Path (Join-Path $sourcesDir "PID.txt") -Value "stale-key" -Encoding UTF8
|
|
|
|
Write-WinUtilISOEditionConfig -ContentRoot $contentRoot -EditionId "Professional" -Logger $logger
|
|
|
|
Test-Path (Join-Path $sourcesDir "PID.txt") | Should -BeFalse
|
|
Test-Path (Join-Path $sourcesDir "ei.cfg") | Should -BeTrue
|
|
(Get-Content -Path (Join-Path $sourcesDir "ei.cfg")) -join "|" |
|
|
Should -Be "[EditionID]|Professional|[Channel]|Retail|[VL]|0"
|
|
($logs -join "|") | Should -Match "Removed sources\\PID\.txt"
|
|
($logs -join "|") | Should -Match "Written sources\\ei\.cfg"
|
|
} finally {
|
|
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
It "skips ei.cfg when selected edition ID is unknown" {
|
|
. ([scriptblock]::Create($script:editionConfigFunction))
|
|
|
|
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
|
|
$logs = [System.Collections.Generic.List[string]]::new()
|
|
$logger = { param($message) $logs.Add([string]$message) }
|
|
|
|
try {
|
|
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
|
|
|
|
Write-WinUtilISOEditionConfig -ContentRoot $contentRoot -EditionId "" -Logger $logger
|
|
|
|
Test-Path (Join-Path $contentRoot "sources\ei.cfg") | Should -BeFalse
|
|
($logs -join "|") | Should -Match "selected edition ID is unknown"
|
|
} finally {
|
|
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
It "converts autounattend setup metadata without preserving placeholder product keys" {
|
|
. ([scriptblock]::Create($script:answerFileChildElementFunction))
|
|
. ([scriptblock]::Create($script:answerFileConversionFunction))
|
|
|
|
[xml]$templateXml = Get-Content -Path $script:autoUnattendPath -Raw
|
|
$unattendNs = "urn:schemas-microsoft-com:unattend"
|
|
$nsMgr = New-Object System.Xml.XmlNamespaceManager($templateXml.NameTable)
|
|
$nsMgr.AddNamespace("u", $unattendNs)
|
|
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
|
|
|
|
$setupComponent = $templateXml.SelectSingleNode('//u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]', $nsMgr)
|
|
$userData = $templateXml.CreateElement("UserData", $unattendNs)
|
|
$productKey = $templateXml.CreateElement("ProductKey", $unattendNs)
|
|
$key = $templateXml.CreateElement("Key", $unattendNs)
|
|
$key.InnerText = "00000-00000-00000-00000-00000"
|
|
[void]$productKey.AppendChild($key)
|
|
[void]$userData.AppendChild($productKey)
|
|
[void]$setupComponent.AppendChild($userData)
|
|
|
|
$originalSetupFileCount = $templateXml.SelectNodes("//sg:File", $nsMgr).Count
|
|
[xml]$converted = ConvertTo-WinUtilISOAnswerFile -XmlContent $templateXml.OuterXml -ImageIndex 3
|
|
$convertedNsMgr = New-Object System.Xml.XmlNamespaceManager($converted.NameTable)
|
|
$convertedNsMgr.AddNamespace("u", $unattendNs)
|
|
$convertedNsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
|
|
|
|
$converted.DocumentElement.NamespaceURI | Should -Be $unattendNs
|
|
$converted.DocumentElement.GetAttribute("xmlns:wcm") | Should -Be "http://schemas.microsoft.com/WMIConfig/2002/State"
|
|
$converted.SelectNodes('//u:component[@name="Microsoft-Windows-Setup"]/u:UserData/u:ProductKey', $convertedNsMgr).Count | Should -Be 0
|
|
$converted.SelectSingleNode('//u:ImageInstall/u:OSImage/u:InstallFrom/u:MetaData[u:Key="/IMAGE/INDEX"]/u:Value', $convertedNsMgr).InnerText | Should -Be "3"
|
|
$converted.SelectNodes("//sg:File", $convertedNsMgr).Count | Should -Be $originalSetupFileCount
|
|
}
|
|
|
|
It "logs DISM output for driver injection through a mocked command" {
|
|
. ([scriptblock]::Create($script:addDriversFunction))
|
|
|
|
$script:dismArguments = @()
|
|
function dism {
|
|
param([Parameter(ValueFromRemainingArguments)]$Arguments)
|
|
$script:dismArguments = @($Arguments)
|
|
"driver one"
|
|
"driver two"
|
|
}
|
|
|
|
$logs = [System.Collections.Generic.List[string]]::new()
|
|
Add-DriversToImage -MountPath "C:\Mount" -DriverDir "C:\Drivers" -Label "install" -Logger {
|
|
param($message)
|
|
$logs.Add([string]$message)
|
|
}
|
|
|
|
($script:dismArguments -join "|") | Should -Be "/English|/image:C:\Mount|/Add-Driver|/Driver:C:\Drivers|/Recurse"
|
|
($logs -join "|") | Should -Be " dism[install]: driver one| dism[install]: driver two"
|
|
}
|
|
|
|
It "keeps driver export and boot.wim injection behind the selected branch" {
|
|
$content = Get-Content -Path $script:isoScriptPath -Raw
|
|
|
|
foreach ($expectedText in @(
|
|
'if ($InjectCurrentSystemDrivers)',
|
|
'Export-WindowsDriver -Online -Destination $driverExportRoot',
|
|
'Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log',
|
|
'Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log',
|
|
'Warning: boot.wim not found - skipping boot.wim driver injection.',
|
|
'Driver injection skipped.'
|
|
)) {
|
|
$content | Should -Match ([regex]::Escape($expectedText))
|
|
}
|
|
}
|
|
|
|
It "attempts winget oscdimg install and exits before export when fallback fails" {
|
|
$content = Get-Content -Path $script:isoWorkflowPath -Raw
|
|
|
|
foreach ($expectedText in @(
|
|
'oscdimg.exe not found. Attempting to install via winget...',
|
|
'Install-WinUtilWinget',
|
|
'Get-Command winget',
|
|
'install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements',
|
|
'oscdimg.exe still not found after install attempt.',
|
|
'oscdimg Not Found'
|
|
)) {
|
|
$content | Should -Match ([regex]::Escape($expectedText))
|
|
}
|
|
|
|
$fallbackIndex = $content.IndexOf('oscdimg.exe not found. Attempting to install via winget...')
|
|
$notFoundDialogIndex = $content.IndexOf('oscdimg Not Found', $fallbackIndex)
|
|
$runspaceIndex = $content.IndexOf('[Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()', $fallbackIndex)
|
|
|
|
$fallbackIndex | Should -BeGreaterThan -1
|
|
$notFoundDialogIndex | Should -BeGreaterThan $fallbackIndex
|
|
$runspaceIndex | Should -BeGreaterThan $notFoundDialogIndex
|
|
}
|
|
}
|