Files

155 lines
10 KiB
PowerShell

[CmdletBinding()]
param()
$ErrorActionPreference='Stop'
$root=Split-Path -Parent $PSScriptRoot
. (Join-Path $root 'scripts\Project.Common.ps1')
Assert-PowerShell51
$dependencies=Get-BuildDependencies
. (Join-Path $root 'scripts\Builder.Workflow.ps1')
function Assert-EnvironmentFailure {
param([scriptblock]$Action,[string]$Pattern)
$failed=$false
try { & $Action | Out-Null }
catch { $failed=$true; if ($_.Exception.Message -notmatch $Pattern) { throw } }
if (-not $failed) { throw ('Expected environment failure: '+$Pattern) }
}
& {
$global:PveEnvironmentTestState=@{}
$global:PveEnvironmentTestState.Mode='ok'
$global:PveEnvironmentTestState.Switches=@([PSCustomObject]@{Name='external';SwitchType='External'})
$global:PveEnvironmentTestState.Prompts=0
$oscdimg=Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe'
function Assert-PowerShell51 { if ($global:PveEnvironmentTestState.Mode -eq 'powershell') { throw 'Wrong shell.' } }
function Assert-Administrator { if ($global:PveEnvironmentTestState.Mode -eq 'administrator') { throw 'Not elevated.' } }
function Get-Module {
param($Name,[switch]$ListAvailable)
if ($Name -eq 'Hyper-V' -and $ListAvailable) {
if ($global:PveEnvironmentTestState.Mode -ne 'module_missing') { [PSCustomObject]@{Name='Hyper-V'} }
return
}
Microsoft.PowerShell.Core\Get-Module -Name $Name
}
function Import-Module {
param($Name,$ErrorAction)
if ($Name -ne 'Hyper-V') { throw 'Unexpected module import.' }
if ($global:PveEnvironmentTestState.Mode -eq 'module_broken') { throw 'Broken module.' }
}
function Get-Service {
param($Name,$ErrorAction)
if ($Name -ne 'vmms') { throw 'Unexpected service query.' }
if ($global:PveEnvironmentTestState.Mode -eq 'service_missing') { return }
$status='Running'
if ($global:PveEnvironmentTestState.Mode -eq 'service_stopped') { $status='Stopped' }
[PSCustomObject]@{Name=$Name;Status=$status}
}
function Get-CimInstance {
param($ClassName,$ErrorAction)
if ($ClassName -ne 'Win32_ComputerSystem') { throw 'Unexpected hardware query.' }
[PSCustomObject]@{HypervisorPresent=($global:PveEnvironmentTestState.Mode -ne 'hypervisor');NumberOfLogicalProcessors=8;TotalPhysicalMemory=32GB}
}
function Get-VMHost {
param($ErrorAction)
if ($global:PveEnvironmentTestState.Mode -eq 'permission') { throw 'Access denied.' }
[PSCustomObject]@{Name='Mock build host'}
}
function Get-VMSwitch { param($ErrorAction) $global:PveEnvironmentTestState.Switches }
function Test-Path {
param($LiteralPath,$PathType)
if ($LiteralPath -eq $oscdimg) { return $global:PveEnvironmentTestState.Mode -ne 'adk' }
if ($PathType) { return Microsoft.PowerShell.Management\Test-Path -LiteralPath $LiteralPath -PathType $PathType }
Microsoft.PowerShell.Management\Test-Path -LiteralPath $LiteralPath
}
function Get-Item {
param($LiteralPath)
if ($LiteralPath -eq $oscdimg) { return [PSCustomObject]@{VersionInfo=[PSCustomObject]@{FileVersion='10.0.26100.1'}} }
Microsoft.PowerShell.Management\Get-Item -LiteralPath $LiteralPath
}
function Read-Host { param($Prompt) $global:PveEnvironmentTestState.Prompts++; throw 'An invalid environment must stop before prompting.' }
$cases=@{
powershell='Windows PowerShell 5.1';administrator='管理员权限';module_missing='启用 Hyper-V';
module_broken='无法加载 Hyper-V';service_missing='vmms';service_stopped='vmms';
hypervisor='BIOS/UEFI';permission='无法管理 Hyper-V';adk='Deployment Tools.*https://learn.microsoft.com'
}
foreach ($mode in $cases.Keys) {
$global:PveEnvironmentTestState.Mode=$mode
Assert-EnvironmentFailure { Get-BuilderHost -AllowMissingExternalSwitch } $cases[$mode]
}
$global:PveEnvironmentTestState.Mode='ok'
$hostInfo=Get-BuilderHost
if ($hostInfo.Switches[0].Name -ne 'external' -or $hostInfo.LogicalProcessors -ne 8 -or $hostInfo.MemoryGiB -ne 32 -or -not $hostInfo.Adk.OscdimgPath) { throw 'Environment inventory is incorrect.' }
$global:PveEnvironmentTestState.Switches=@()
if (@((Get-BuilderHost -AllowMissingExternalSwitch).Switches).Count) { throw 'Missing switch was invented.' }
Assert-EnvironmentFailure { Get-BuilderHost } '外部交换机'
$global:PveEnvironmentTestState.Switches=@([PSCustomObject]@{Name='external';SwitchType='External'})
foreach ($mode in @('module_missing','adk')) {
$global:PveEnvironmentTestState.Mode=$mode
Assert-EnvironmentFailure { Invoke-ImageBuilderWizard } $cases[$mode]
}
if ($global:PveEnvironmentTestState.Prompts) { throw 'Wizard continued to ask for resources in an invalid environment.' }
# Exercise the actual -WhatIf entry in an isolated fixture. Native environment
# queries are mocked above; any attempt to build or mount an ISO is rejected.
$fixture=Join-Path $root ('local\environment tests '+[guid]::NewGuid().ToString('N').Substring(0,8))
foreach ($directory in @('scripts','config','local','assets','resources')) { New-Item -ItemType Directory -Path (Join-Path $fixture $directory) -Force | Out-Null }
foreach ($file in @('Build-Image.ps1','scripts\Build.Validation.ps1','scripts\Builder.Workflow.ps1','scripts\Configuration.Common.ps1','scripts\Package.Validation.ps1','scripts\ProductKeys.Common.ps1','scripts\Software.Selection.ps1','config\Kms.ClientKeys.psd1')) {
Copy-Item -LiteralPath (Join-Path $root $file) -Destination (Join-Path $fixture $file)
}
$common=@'
$ProjectRoot=Split-Path -Parent $PSScriptRoot
function Get-ProjectSettings { Get-Content -Raw -LiteralPath (Join-Path $ProjectRoot 'local\settings.json') | ConvertFrom-Json }
function Get-BuildDependencies { Get-Content -Raw -LiteralPath (Join-Path $ProjectRoot 'local\dependencies.json') | ConvertFrom-Json }
'@
[IO.File]::WriteAllText((Join-Path $fixture 'scripts\Project.Common.ps1'),$common)
$import=@'
param($Settings)
function Get-WindowsImageConfig { param($ConfigFilePath) Get-Content -Raw -LiteralPath $ConfigFilePath | ConvertFrom-Json }
function Get-WimFileImagesInfo { param($WimFilePath) [PSCustomObject]@{ImageName='Windows 11 Pro';ImageVersion=[version]'10.0.26100.1'} }
function New-WindowsOnlineImage { throw 'Preflight must never start a build.' }
function Mount-DiskImage { throw 'This WIM-only test must not mount an ISO.' }
'@
[IO.File]::WriteAllText((Join-Path $fixture 'scripts\Import-ImagingTools.ps1'),$import)
foreach ($file in @('install.wim','Windows.iso','virtio.iso','qemu-img.exe')) { [IO.File]::WriteAllText((Join-Path $fixture ('assets\'+$file)),'Fixture; never executed.') }
[IO.File]::WriteAllText((Join-Path $fixture 'resources\virtio-win-guest-tools.exe'),'Unsigned fixture; filename only.')
[IO.File]::WriteAllText((Join-Path $fixture 'resources\software-selection.json'),(@{SchemaVersion=1;InstallChrome=$false;InstallVSCode=$false;ServicingIsoPath=(Join-Path $fixture 'assets\Windows.iso')}|ConvertTo-Json))
$dependencies.Qemu.ExecutableSHA256=(Get-FileHash -LiteralPath (Join-Path $fixture 'assets\qemu-img.exe')).Hash
[IO.File]::WriteAllText((Join-Path $fixture 'local\dependencies.json'),($dependencies|ConvertTo-Json -Depth 4))
$manifest=Join-Path $fixture 'local\tool.json'
[IO.File]::WriteAllText($manifest,(@{Version=$dependencies.Qemu.Version;PublishedHashVerified=$true;ExecutablePath=(Join-Path $fixture 'assets\qemu-img.exe')}|ConvertTo-Json))
$settings=@{ProjectRoot=$fixture;AssetsRoot=(Join-Path $fixture 'assets');UpstreamRoot=(Join-Path $root 'vendor\windows-imaging-tools');GitPath=(Get-Command git.exe).Source;QemuManifestPath=$manifest}
[IO.File]::WriteAllText((Join-Path $fixture 'local\settings.json'),($settings|ConvertTo-Json))
$config=@{wim_file_path=(Join-Path $fixture 'assets\install.wim');virtio_iso_path=(Join-Path $fixture 'assets\virtio.iso');image_path=(Join-Path $fixture 'assets\preflight.qcow2');virtual_disk_format='QCOW2';image_type='KVM';disk_layout='UEFI';custom_resources_path=(Join-Path $fixture 'resources');external_switch='external';cpu_count=4;ram_size=4GB;disk_size=64GB;image_name='Windows 11 Pro'}
$configPath=Join-Path $fixture 'local\config.json'
[IO.File]::WriteAllText($configPath,($config|ConvertTo-Json))
function Get-PSDrive {
param($Name,$PSProvider)
$free=1TB
if ($global:PveEnvironmentTestState.Mode -eq 'disk_space') { $free=1GB }
[PSCustomObject]@{Free=$free}
}
foreach ($mode in @('module_missing','adk')) {
$global:PveEnvironmentTestState.Mode=$mode
Assert-EnvironmentFailure { & (Join-Path $fixture 'Build-Image.ps1') -ConfigPath $configPath -WhatIf } $cases[$mode]
}
$global:PveEnvironmentTestState.Mode='ok'
$preflight=& (Join-Path $fixture 'Build-Image.ps1') -ConfigPath $configPath -WhatIf
if ($preflight.HostChecks -ne 'passed') { throw 'WhatIf skipped host checks.' }
$config.cpu_count=9
[IO.File]::WriteAllText($configPath,($config|ConvertTo-Json))
Assert-EnvironmentFailure { & (Join-Path $fixture 'Build-Image.ps1') -ConfigPath $configPath -WhatIf } 'CPU or memory'
$config.cpu_count=4
$config.ram_size=64GB
[IO.File]::WriteAllText($configPath,($config|ConvertTo-Json))
Assert-EnvironmentFailure { & (Join-Path $fixture 'Build-Image.ps1') -ConfigPath $configPath -WhatIf } 'CPU or memory'
$config.ram_size=4GB
[IO.File]::WriteAllText($configPath,($config|ConvertTo-Json))
$global:PveEnvironmentTestState.Mode='disk_space'
Assert-EnvironmentFailure { & (Join-Path $fixture 'Build-Image.ps1') -ConfigPath $configPath -WhatIf } '空间不足'
$global:PveEnvironmentTestState.Mode='ok'
$config.image_name='Missing edition'
[IO.File]::WriteAllText($configPath,($config|ConvertTo-Json))
Assert-EnvironmentFailure { & (Join-Path $fixture 'Build-Image.ps1') -ConfigPath $configPath -WhatIf } 'edition was not found'
if (Test-Path -LiteralPath $config.image_path) { throw 'Preflight created an image.' }
if (Test-Path -LiteralPath (Join-Path $fixture 'assets\logs')) { throw 'Preflight entered the build stage.' }
[PSCustomObject]@{EnvironmentFailures=$cases.Count;MissingSwitch='prompt allowed only after environment checks';Wizard='stops before resources';WhatIfHostChecks='passed; missing dependencies rejected';CpuMemoryDiskAndEdition='checked; invalid inputs rejected';BuildSideEffects='none';EnvironmentQueries='mocked'}
}