155 lines
10 KiB
PowerShell
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'}
|
|
}
|