This commit is contained in:
xyou
2026-07-31 13:53:49 +08:00
parent 5ea84fac97
commit f52157b7af
241 changed files with 26843 additions and 8155 deletions
+232
View File
@@ -0,0 +1,232 @@
#requires -Version 5.1
<#
.SYNOPSIS
boot-regression — 启动链路回归测试(docs/RE/test-cases/boot-regression.md)
.DESCRIPTION
场景覆盖:
B1 首次启动 firstInit.txt="0\n0" UTSM → DSK → mouseInit → netman → FirstInit 向导(自动按键) → desktop
B2 正常启动 firstInit.txt="1\n0" UTSM → DSK → login(skip) → cmd(AUTOEXEC=ver/exit) → desktop
B3 dev_mode firstInit.txt="1\n1" DSK → shell dev_tests → Esc → cmd → desktop
B4 无 SATA 盘 UTSM DSK 加载回退 Limine boot module,DSK 降级运行不崩溃
B5 无 NIC e1000/virtio_net not found,系统继续启动到 desktop
B6 Linux 兼容层 见 linux-compat.ps1
退出码:全部通过 0,任一 FAIL 1。
#>
param(
[switch]$Build,
[string[]]$Scenarios = @('B1', 'B2', 'B3', 'B4', 'B5')
)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
Initialize-QemuTest -Build:$Build
$result = New-TestCaseResult 'boot-regression'
# ----------------------------------------------------------------
# FirstInit 设置向导自动填表:
# 账户卡片(3 字段) → 偏好页(5×Enter) → 网络页(3×Enter) → 完成
# 每步都等串口锚点再发键,字段验证失败自动重试。
# ----------------------------------------------------------------
function Complete-FirstInitWizard {
param([Parameter(Mandatory)]$Session)
$hit = Wait-QemuLog -Session $Session -Patterns @('[FI] fadein done') -TimeoutSeconds 150
if (-not $hit) { Add-TestCheck $result 'B1: FirstInit 账户卡片就绪' 'FAIL' 'missing [FI] fadein done'; return $false }
$done = $false
for ($attempt = 1; $attempt -le 3 -and -not $done; $attempt++) {
Send-QemuText -Session $Session -Text 'testpc' -Enter
Start-Sleep -Milliseconds 400
Send-QemuText -Session $Session -Text 'tester' -Enter
Start-Sleep -Milliseconds 400
Send-QemuText -Session $Session -Text 'pass123' -Enter
$hit = Wait-QemuLog -Session $Session -Patterns @('[FI] all fields done', '[FI] validation failed') -TimeoutSeconds 30
if ($hit -eq '[FI] all fields done') { $done = $true }
else { Add-TestNote $result "B1: FirstInit 字段验证失败,重试第 $attempt 次" }
}
if (-not $done) { Add-TestCheck $result 'B1: FirstInit 账户字段填写' 'FAIL' 'validation did not pass after 3 attempts'; return $false }
Add-TestCheck $result 'B1: FirstInit 账户字段填写' 'PASS'
Start-Sleep -Milliseconds 600
Send-QemuKeys -Session $Session -Keys @('ret', 'ret', 'ret', 'ret', 'ret') -DelayMs 350
$hit = Wait-QemuLog -Session $Session -Patterns @('[FirstInit] entering network setup page') -TimeoutSeconds 40
if (-not $hit) { Add-TestCheck $result 'B1: FirstInit 偏好页' 'FAIL' 'did not reach network setup page'; return $false }
Add-TestCheck $result 'B1: FirstInit 偏好页(5xEnter)' 'PASS'
Start-Sleep -Milliseconds 400
Send-QemuKeys -Session $Session -Keys @('ret', 'ret', 'ret') -DelayMs 350
$hit = Wait-QemuLog -Session $Session -Patterns @('[FirstInit] user setup finished') -TimeoutSeconds 40
if (-not $hit) { Add-TestCheck $result 'B1: FirstInit 网络页' 'FAIL' 'setup did not finish'; return $false }
Add-TestCheck $result 'B1: FirstInit 网络页(3xEnter)' 'PASS'
return $true
}
# ----------------------------------------------------------------
function Invoke-ScenarioB1 {
Write-Host "`n--- B1 首次启动 (firstInit=0) ---"
Set-DeshabFirstInit -First 0 -DevMode 0
$s = Start-QemuSession -Name 'bootreg-b1'
try {
if (-not (Complete-FirstInitWizard -Session $s)) { return }
$hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 120
$log = Read-QemuLogText $s.LogPath
$result.LogPath = $s.LogPath
Assert-QemuLog $result $log -Ordered @(
'[UTSM] SELFTEST PASS',
'[DSK] boot',
'[DSK] firstInit=0',
'[DSK:SCHED] decision: is_first=1 -> mouseInit -> netman -> FirstInit -> desktop',
'[DSK] loading mouseInit',
'[DSK] loading netman',
'[DSK] loading FirstInit',
'[FirstInit] boot',
'[FirstInit] user setup finished',
'[DSK] FirstInit returned, persisting user.conf',
'[DSK] writing USER.CONF',
'[DSK] firstInit.txt flip rc=',
'[DSK] loading desktop',
'desktop ready'
) -MustNotContain @('[PANIC]')
if ($hit) { Add-TestCheck $result 'B1: desktop ready' 'PASS' }
else { Add-TestCheck $result 'B1: desktop ready' 'FAIL' 'timeout waiting desktop ready' }
} finally {
Stop-QemuSession $s
}
}
function Invoke-ScenarioB2 {
Write-Host "`n--- B2 正常启动 (firstInit=1, AUTOEXEC=ver/exit) ---"
Set-DeshabFirstInit -First 1 -DevMode 0
Set-DeshabAutoexec -Lines @('ver', 'exit')
$s = Start-QemuSession -Name 'bootreg-b2'
try {
$hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
$log = Read-QemuLogText $s.LogPath
$result.LogPath = $s.LogPath
Assert-QemuLog $result $log -Ordered @(
'[UTSM] SELFTEST PASS',
'[UTSM] loading DSK',
'[DSK] boot',
'[DSK] context ok',
'[DSK] firstInit != 0',
'[DSK:SCHED] decision: is_first=0 -> login then desktop',
'[DSK:SCHED] login skipped by FUCK config',
'[DSK:SCHED] default_shell=cmd -> cmd.elf',
'[cmd] boot',
'[cmd] running AUTOEXEC.BAT',
'[DSK] cmd.elf returned, falling back to desktop',
'[DSK:SCHED] loading desktop',
'desktop ready'
) -MustNotContain @('[PANIC]')
if ($hit) { Add-TestCheck $result 'B2: desktop ready' 'PASS' }
else { Add-TestCheck $result 'B2: desktop ready' 'FAIL' 'timeout waiting desktop ready' }
} finally {
Stop-QemuSession $s
}
}
function Invoke-ScenarioB3 {
Write-Host "`n--- B3 dev_mode (firstInit=1\1) ---"
Set-DeshabFirstInit -First 1 -DevMode 1
Set-DeshabAutoexec -Lines @('ver', 'exit')
$s = Start-QemuSession -Name 'bootreg-b3'
try {
$hit = Wait-QemuLog -Session $s -Patterns @('[8] rm TEST.TXT') -TimeoutSeconds 150
if (-not $hit) {
$result.LogPath = $s.LogPath
Add-TestCheck $result 'B3: shell dev_tests 执行' 'FAIL' 'missing [8] rm TEST.TXT'
return
}
Start-Sleep -Seconds 2
Send-QemuKeys -Session $s -Keys @('esc')
$hit2 = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
$log = Read-QemuLogText $s.LogPath
$result.LogPath = $s.LogPath
Assert-QemuLog $result $log -Ordered @(
'[DSK] dev_mode=1 (developer auto-test)',
'[DSK:SCHED] decision: dev_mode -> shell.elf',
'[shell] boot',
'[1] ls',
'[4] cp TEST.TXT COPY.TXT',
'[8] rm TEST.TXT',
'[shell] esc -> return to DSK',
'[DSK] shell auto-test returned',
'[DSK:SCHED] default_shell=cmd -> cmd.elf',
'desktop ready'
) -MustNotContain @('[PANIC]')
if ($hit2) { Add-TestCheck $result 'B3: Esc 返回后到 desktop' 'PASS' }
else { Add-TestCheck $result 'B3: Esc 返回后到 desktop' 'FAIL' 'timeout waiting desktop ready' }
} finally {
Stop-QemuSession $s
}
}
function Invoke-ScenarioB4 {
Write-Host "`n--- B4 无 SATA 盘 (DSK 回退 Limine module) ---"
$s = Start-QemuSession -Name 'bootreg-b4' -NoSata
try {
$hit = Wait-QemuLog -Session $s -Patterns @('[DSK] firstInit.txt not found', '[DSK] desktop load failed', '[PANIC]') -TimeoutSeconds 120
Start-Sleep -Seconds 3
$log = Read-QemuLogText $s.LogPath
$result.LogPath = $s.LogPath
Assert-QemuLog $result $log -Ordered @(
'[UTSM] SELFTEST PASS',
'[UTSM] loading DSK',
'[UTSM] FAT32 block path unavailable; trying Limine module',
'[DSK] boot',
'[DSK] context ok',
'[DSK] firstInit.txt not found, assuming first boot'
) -MustContain @('[DSK] mouseInit not found') -MustNotContain @('[PANIC]')
if ($hit -eq '[PANIC]') { Add-TestCheck $result 'B4: 无盘降级不崩溃' 'FAIL' 'kernel panicked' }
else { Add-TestCheck $result 'B4: 无盘降级不崩溃' 'PASS' }
} finally {
Stop-QemuSession $s
}
}
function Invoke-ScenarioB5 {
Write-Host "`n--- B5 无 NIC (e1000/virtio_net not found, 继续启动) ---"
Set-DeshabFirstInit -First 1 -DevMode 0
Set-DeshabAutoexec -Lines @('ver', 'exit')
$s = Start-QemuSession -Name 'bootreg-b5' -NoNet
try {
$hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
$log = Read-QemuLogText $s.LogPath
$result.LogPath = $s.LogPath
Assert-QemuLog $result $log -MustContain @(
'[e1000] device not found',
'[virtio_net] virtio-net device not found'
) -Ordered @(
'[UTSM] SELFTEST PASS',
'[DSK] boot',
'[DSK] firstInit != 0',
'desktop ready'
) -MustNotContain @('[PANIC]')
if ($hit) { Add-TestCheck $result 'B5: 无 NIC 到 desktop' 'PASS' }
else { Add-TestCheck $result 'B5: 无 NIC 到 desktop' 'FAIL' 'timeout waiting desktop ready' }
} finally {
Stop-QemuSession $s
}
}
# ----------------------------------------------------------------
try {
foreach ($sc in $Scenarios) {
switch ($sc.ToUpper()) {
'B1' { Invoke-ScenarioB1 }
'B2' { Invoke-ScenarioB2 }
'B3' { Invoke-ScenarioB3 }
'B4' { Invoke-ScenarioB4 }
'B5' { Invoke-ScenarioB5 }
default { Add-TestNote $result "未知场景 $sc,跳过" }
}
}
Add-TestNote $result 'B6 (Linux 兼容层启用) 由 linux-compat.ps1 覆盖'
} finally {
Restore-DeshabScenario
}
Write-TestCaseResult $result
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
+85
View File
@@ -0,0 +1,85 @@
#requires -Version 5.1
<#
.SYNOPSIS
driver-matrix — DKM 驱动功能矩阵(docs/RE/test-cases/driver-matrix.md)
.DESCRIPTION
单次 QEMU 会话挂全设备(SATA 测试盘 + NVMe 测试盘 + xHCI USB 盘 + e1000),
验证 4 个 stage 共 16 个清单驱动的加载顺序与功能锚点:
stage0: timer / apic / acpi / pci
stage1: console_fb / ahci / nvme / xhci / bootfs
stage2: vfs / fat32 / devfs
stage3: e1000 / virtio_net(无设备,降级) / ath9k(FUCK 禁用) / ps2kbd
已知限制(与文档一致):
- virtio_net 在无 virtio-net 设备时走 not-found 降级路径(断言降级锚点)
- ath9k 被 FUCK [drivers] ath9k=0 禁用(断言 DSM 禁用锚点;QEMU 无 WiFi 设备)
- xhci USB MSC 枚举在当前实现下可能失败,仅断言驱动就绪
退出码:全部通过 0,任一 FAIL 1。
#>
param([switch]$Build)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
Initialize-QemuTest -Build:$Build
$result = New-TestCaseResult 'driver-matrix'
Write-Host "`n--- 全设备单会话驱动矩阵 ---"
$s = Start-QemuSession -Name 'driver-matrix'
try {
$hit = Wait-QemuLog -Session $s -Patterns @('[DSK] boot') -TimeoutSeconds 150
Start-Sleep -Seconds 2
$log = Read-QemuLogText $s.LogPath
$result.LogPath = $s.LogPath
# ---- stage 加载顺序(driver ready 锚点按 manifest 顺序) ----
Assert-QemuLog $result $log -Ordered @(
'[timer] driver ready',
'[apic] driver ready',
'[acpi] driver ready',
'[pci] driver ready',
'[console_fb] ready',
'[ahci] driver ready',
'[nvme] driver ready',
'[xhci] driver ready',
'[bootfs] driver ready',
'[vfs] driver ready',
'[fat32] driver ready',
'[devfs] driver ready',
'[e1000] driver ready',
'[virtio_net] driver ready',
'[kbd] driver ready',
'[UTSM] SELFTEST PASS'
)
# ---- 功能验证点 ----
Assert-QemuLog $result $log -MustContain @(
'[ahci] block provider index=', # ahci: ahci0 block provider 注册
'[nvme] block provider index=', # nvme: block provider 注册
'[fat32] using block provider image', # fat32: 经块设备解析测试镜像
'[e1000] netdev registered', # e1000: 注册 netdev
'[e1000] link=up', # e1000: 链路就绪
'[kbd] IRQ1 registered', # ps2kbd: IRQ1 注册
'[virtio_net] virtio-net device not found', # virtio_net: e1000-only 拓扑降级
'[DSM] driver disabled by FUCK' # ath9k: FUCK 禁用路径
)
# ---- DSM 装载统计:16 个 manifest 驱动中 15 个 active(ath9k 禁用) ----
Assert-QemuLog $result $log -CountPattern '[DKM] external driver active' -MinCount 15
# ---- 4 个 stage 均完成 ----
Assert-QemuLog $result $log -CountPattern '[DSM] stage end' -MinCount 4
if ($hit) { Add-TestCheck $result '系统到达 DSK' 'PASS' }
else { Add-TestCheck $result '系统到达 DSK' 'FAIL' 'timeout waiting [DSK] boot' }
Add-TestNote $result 'ath9k: QEMU 无 WiFi 设备模拟,FUCK ath9k=0,走 DSM 禁用路径(文档第 14 行 ⏭)'
Add-TestNote $result 'xhci: USB MSC 枚举当前可能失败(已知限制),仅断言 [xhci] driver ready'
} finally {
Stop-QemuSession $s
}
Write-TestCaseResult $result
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
+704
View File
@@ -0,0 +1,704 @@
#requires -Version 5.1
<#
.SYNOPSIS
Deshab QEMU 自动化测试公共库(tests/qemu/lib/QemuTest.ps1)
.DESCRIPTION
提供:
- QEMU 会话管理(启动 / 计时杀进程 / 串口日志收集)
- 日志断言引擎(必须匹配 / 禁止匹配 / 顺序匹配 / 计数匹配)
- QEMU monitor TCP 客户端(sendkey / mouse_move / screendump)
- 场景准备(firstInit.txt 与 AUTOEXEC.BAT 备份-替换-恢复 + SATA 镜像重建)
- 用例结果报告(PASS/FAIL/MANUAL/SKIP,失败时输出日志尾部上下文)
兼容 PowerShell 5.1。所有临时文件写入 .build_tmp\tests\。
#>
# ---------------- 路径 ----------------
$script:RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
$script:BuildTmp = Join-Path $script:RepoRoot '.build_tmp'
$script:TestWorkDir = Join-Path $script:BuildTmp 'tests'
$script:IsoDir = Join-Path $script:RepoRoot 'ISO'
$script:SystemDir = Join-Path $script:RepoRoot 'SYSTEM'
$script:Config = $null
# 场景备份(模块级,保证只备份一次、恢复幂等)
$script:FirstInitBackup = $null
$script:AutoexecBackup = $null
# ================================================================
# 环境初始化
# ================================================================
function Initialize-QemuTest {
param([switch]$Build)
# --- 定位 QEMU ---
$qemu = $null
$cmd = Get-Command 'qemu-system-x86_64' -ErrorAction SilentlyContinue
if ($cmd) { $qemu = $cmd.Source }
foreach ($c in @('C:\Program Files\qemu\qemu-system-x86_64.exe',
'C:\Program Files (x86)\qemu\qemu-system-x86_64.exe',
'C:\msys64\mingw64\bin\qemu-system-x86_64.exe',
'C:\msys64\ucrt64\bin\qemu-system-x86_64.exe')) {
if (-not $qemu -and (Test-Path $c)) { $qemu = $c }
}
if (-not $qemu) { throw 'qemu-system-x86_64 not found (install QEMU or add to PATH)' }
# --- 定位 OVMF ---
$ovmf = $null
foreach ($c in @('C:\Program Files\qemu\share\edk2-x86_64-code.fd',
'C:\Program Files\qemu\share\OVMF_CODE.fd',
'C:\msys64\mingw64\share\edk2\ovmf\OVMF_CODE.fd',
'C:\msys64\ucrt64\share\edk2\ovmf\OVMF_CODE.fd')) {
if (Test-Path $c) { $ovmf = $c; break }
}
if (-not $ovmf) { throw 'OVMF UEFI firmware not found under QEMU install directory' }
New-Item -ItemType Directory -Force -Path $script:TestWorkDir | Out-Null
$img = Join-Path $script:IsoDir 'deshab.img'
$sata = Join-Path $script:BuildTmp 'sata_fat32_dsk.img'
$nvme = Join-Path $script:BuildTmp 'nvme_test.img'
$usb = Join-Path $script:BuildTmp 'usb_test.img'
if ($Build -or -not (Test-Path $img)) {
Write-Host '[init] building images via build.ps1 ...'
Stop-ResidualQemu
& powershell -ExecutionPolicy Bypass -File (Join-Path $script:RepoRoot 'build.ps1') | Out-Null
if ($LASTEXITCODE -ne 0) { throw "build.ps1 failed with exit code $LASTEXITCODE" }
}
if (-not (Test-Path $img)) { throw "image missing: $img (run with -Build)" }
if (-not (Test-Path $sata)) {
Write-Host '[init] sata_fat32_dsk.img missing, regenerating via mkfat32.ps1 ...'
Invoke-MkFat32
}
if (-not (Test-Path $sata)) { throw "sata image missing: $sata" }
$nvmeOk = $null; if (Test-Path $nvme) { $nvmeOk = $nvme }
$usbOk = $null; if (Test-Path $usb) { $usbOk = $usb }
$script:Config = [pscustomobject]@{
QemuExe = $qemu
Ovmf = $ovmf
Img = $img
SataImg = $sata
NvmeImg = $nvmeOk
UsbImg = $usbOk
}
Write-Host "[init] QEMU: $qemu"
Write-Host "[init] OVMF: $ovmf"
Write-Host "[init] IMG: $img"
return $script:Config
}
# ================================================================
# 进程管理
# ================================================================
# 清理持有本项目镜像的残留 QEMU(deshab.img 被锁会导致 build/启动失败)。
# 只杀命令行中引用本仓库路径的 qemu 进程,避免误伤他人虚拟机。
function Stop-ResidualQemu {
$procs = Get-CimInstance Win32_Process -Filter "Name='qemu-system-x86_64.exe'" -ErrorAction SilentlyContinue
foreach ($p in $procs) {
if ($p.CommandLine -and $p.CommandLine.ToLower().Contains('deshab')) {
Write-Host "[qemu] killing residual qemu pid=$($p.ProcessId)"
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
}
}
}
function Get-FreeMonitorPort {
for ($port = 4444; $port -lt 4544; $port++) {
$listener = $null
try {
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $port)
$listener.Start()
$listener.Stop()
return $port
} catch {
if ($listener) { try { $listener.Stop() } catch {} }
}
}
throw 'no free TCP port available for QEMU monitor'
}
# ================================================================
# QEMU 会话
# ================================================================
function Start-QemuSession {
param(
[Parameter(Mandatory)][string]$Name,
[string]$Accel = 'whpx',
[switch]$NoSata,
[switch]$NoNvme,
[switch]$NoUsb,
[switch]$NoNet,
[string]$NetModel = 'e1000',
[switch]$NoMonitor,
[string]$Cpu = '',
[string[]]$ExtraArgs = @()
)
if (-not $script:Config) { throw 'call Initialize-QemuTest first' }
$cfg = $script:Config
$tag = "deshabtest-$Name-" + [guid]::NewGuid().ToString('N').Substring(0, 8)
$stamp = Get-Date -Format 'HHmmss'
$log = Join-Path $script:TestWorkDir "$Name-$stamp.log"
$bat = Join-Path $script:TestWorkDir "$Name-$stamp.bat"
# WHPX 不支持 nested VMX:默认关闭 guest VMX CPUID(与 ISO/run_qemu.bat 一致)。
# -Cpu 可显式覆盖(如 'max' 用于探测 nested VMX 可用性)。
$cpu = 'qemu64'
if ($Accel -eq 'whpx') { $cpu = 'qemu64,-vmx' }
if ($Cpu -ne '') { $cpu = $Cpu }
$qargs = @(
'-accel', $Accel,
'-machine', 'q35',
'-m', '2G',
'-cpu', $cpu,
'-serial', 'stdio',
'-display', 'none',
'-no-reboot',
'-name', $tag,
'-drive', ('if=pflash,format=raw,readonly=on,file="{0}"' -f $cfg.Ovmf),
'-drive', ('format=raw,file="{0}",if=virtio' -f $cfg.Img)
)
$port = 0
if (-not $NoMonitor) {
$port = Get-FreeMonitorPort
$qargs += @('-monitor', "tcp:127.0.0.1:$port,server,nowait")
}
if (-not $NoSata) {
$qargs += @('-drive', ('id=sata0,format=raw,file="{0}",if=none' -f $cfg.SataImg),
'-device', 'ide-hd,drive=sata0,bus=ide.0')
}
if (-not $NoNvme -and $cfg.NvmeImg) {
$qargs += @('-drive', ('if=none,id=nvme0,file="{0}",format=raw' -f $cfg.NvmeImg),
'-device', 'nvme,drive=nvme0,serial=deadbeef')
}
if (-not $NoUsb -and $cfg.UsbImg) {
$qargs += @('-device', 'qemu-xhci,id=xhci0',
'-drive', ('id=usb0,if=none,file="{0}",format=raw' -f $cfg.UsbImg),
'-device', 'usb-storage,bus=xhci0.0,drive=usb0')
}
if (-not $NoNet) {
$qargs += @('-netdev', 'user,id=net0')
if ($NetModel -eq 'e1000') {
$qargs += @('-device', 'e1000,netdev=net0,mac=52:54:00:12:34:56')
} elseif ($NetModel -eq 'virtio-net-pci') {
$qargs += @('-device', 'virtio-net-pci,netdev=net0')
}
}
foreach ($x in $ExtraArgs) { $qargs += $x }
$qargs += @('-boot', 'menu=on')
# 已知坑:Start-Process 下 -serial file: 不生效;用 cmd 批处理重定向 stdio。
$cmdline = '"' + $cfg.QemuExe + '" ' + ($qargs -join ' ')
$batContent = "@echo off`r`n$cmdline > `"$log`" 2>&1`r`n"
[System.IO.File]::WriteAllText($bat, $batContent)
Stop-ResidualQemu
Start-Process -FilePath $bat -WindowStyle Hidden
$session = [pscustomobject]@{
Name = $Name
Tag = $tag
BatPath = $bat
LogPath = $log
MonitorPort = $port
Accel = $Accel
StartTime = Get-Date
}
Write-Host "[qemu] session '$Name' started (monitor=$port, log=$log)"
return $session
}
function Test-QemuSessionExited {
param([Parameter(Mandatory)]$Session)
$procs = Get-CimInstance Win32_Process -Filter "Name='qemu-system-x86_64.exe'" -ErrorAction SilentlyContinue
foreach ($p in $procs) {
if ($p.CommandLine -and $p.CommandLine.Contains($Session.Tag)) { return $false }
}
return $true
}
function Stop-QemuSession {
param([Parameter(Mandatory)]$Session)
$procs = Get-CimInstance Win32_Process -Filter "Name='qemu-system-x86_64.exe'" -ErrorAction SilentlyContinue
foreach ($p in $procs) {
if ($p.CommandLine -and $p.CommandLine.Contains($Session.Tag)) {
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
}
}
Start-Sleep -Milliseconds 500
}
# ================================================================
# 串口日志
# ================================================================
function Read-QemuLogText {
param([Parameter(Mandatory)][string]$Path)
if (-not (Test-Path $Path)) { return '' }
$fs = $null
try {
$fs = [System.IO.File]::Open($Path, 'Open', 'Read', 'ReadWrite')
$sr = New-Object System.IO.StreamReader($fs, [System.Text.Encoding]::UTF8)
$text = $sr.ReadToEnd()
$sr.Close()
return $text
} catch {
return ''
} finally {
if ($fs) { $fs.Close() }
}
}
# 轮询串口日志直到任一 pattern 出现 / QEMU 退出 / 超时。返回匹配到的 pattern 或 $null。
function Wait-QemuLog {
param(
[Parameter(Mandatory)]$Session,
[Parameter(Mandatory)][string[]]$Patterns,
[int]$TimeoutSeconds = 90,
[int]$PollMs = 1000
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
$text = Read-QemuLogText $Session.LogPath
foreach ($p in $Patterns) {
if ($text.Contains($p)) { return $p }
}
if (Test-QemuSessionExited $Session) {
Write-Host '[qemu] session exited unexpectedly'
return $null
}
Start-Sleep -Milliseconds $PollMs
}
Write-Host "[qemu] wait timeout after ${TimeoutSeconds}s (patterns: $($Patterns -join ' | '))"
return $null
}
# ================================================================
# QEMU monitor(TCP)
# ================================================================
function Invoke-QemuMonitor {
param(
[Parameter(Mandatory)]$Session,
[Parameter(Mandatory)][string]$Command,
[int]$SettleMs = 250
)
if (-not $Session.MonitorPort) { throw 'session started with -NoMonitor' }
$client = New-Object System.Net.Sockets.TcpClient
$client.ReceiveTimeout = 3000
$client.SendTimeout = 3000
$client.Connect('127.0.0.1', [int]$Session.MonitorPort)
try {
$stream = $client.GetStream()
$buf = New-Object byte[] 8192
Start-Sleep -Milliseconds 200
while ($stream.DataAvailable) { [void]$stream.Read($buf, 0, $buf.Length) } # banner
$writer = New-Object System.IO.StreamWriter($stream)
$writer.AutoFlush = $true
$writer.WriteLine($Command)
Start-Sleep -Milliseconds $SettleMs
$resp = ''
while ($stream.DataAvailable) {
$n = $stream.Read($buf, 0, $buf.Length)
$resp += [System.Text.Encoding]::ASCII.GetString($buf, 0, $n)
}
return $resp
} finally {
$client.Close()
}
}
function ConvertTo-QemuKeyName {
param([Parameter(Mandatory)][char]$C)
$i = [int]$C
if ($i -ge 97 -and $i -le 122) { return [string]$C } # a-z
if ($i -ge 65 -and $i -le 90) { return 'shift-' + ([string]$C).ToLower() } # A-Z
if ($i -ge 48 -and $i -le 57) { return [string]$C } # 0-9
switch ($C) {
' ' { return 'spc' }
'.' { return 'dot' }
',' { return 'comma' }
'/' { return 'slash' }
'\' { return 'backslash' }
'-' { return 'minus' }
'=' { return 'equal' }
';' { return 'semicolon' }
':' { return 'shift-semicolon' }
'>' { return 'shift-dot' }
'<' { return 'shift-comma' }
'_' { return 'shift-minus' }
'!' { return 'shift-1' }
'"' { return 'shift-apostrophe' }
'''' { return 'apostrophe' }
'(' { return 'shift-9' }
')' { return 'shift-0' }
'?' { return 'shift-slash' }
'*' { return 'shift-8' }
'+' { return 'shift-equal' }
'@' { return 'shift-2' }
'#' { return 'shift-3' }
'$' { return 'shift-4' }
'%' { return 'shift-5' }
'^' { return 'shift-6' }
'&' { return 'shift-7' }
'|' { return 'shift-backslash' }
'~' { return 'shift-grave_accent' }
}
return $null
}
# 通过 monitor 发送一串按键(单 TCP 连接内完成)
function Send-QemuKeys {
param(
[Parameter(Mandatory)]$Session,
[Parameter(Mandatory)][string[]]$Keys,
[int]$DelayMs = 120
)
if (-not $Session.MonitorPort) { throw 'session started with -NoMonitor' }
$client = New-Object System.Net.Sockets.TcpClient
$client.Connect('127.0.0.1', [int]$Session.MonitorPort)
try {
$stream = $client.GetStream()
$buf = New-Object byte[] 4096
Start-Sleep -Milliseconds 200
while ($stream.DataAvailable) { [void]$stream.Read($buf, 0, $buf.Length) }
$writer = New-Object System.IO.StreamWriter($stream)
$writer.AutoFlush = $true
foreach ($k in $Keys) {
$writer.WriteLine("sendkey $k")
Start-Sleep -Milliseconds $DelayMs
}
} finally {
$client.Close()
}
}
# 输入一段文本(自动映射大小写/符号),可选追加回车
function Send-QemuText {
param(
[Parameter(Mandatory)]$Session,
[Parameter(Mandatory)][string]$Text,
[switch]$Enter,
[int]$DelayMs = 90
)
$keys = New-Object System.Collections.ArrayList
foreach ($c in $Text.ToCharArray()) {
$k = ConvertTo-QemuKeyName $c
if ($k) { [void]$keys.Add($k) }
}
if ($Enter) { [void]$keys.Add('ret') }
Send-QemuKeys -Session $Session -Keys ($keys.ToArray()) -DelayMs $DelayMs
}
function Send-QemuMouseMove {
param(
[Parameter(Mandatory)]$Session,
[int]$Dx = 0,
[int]$Dy = 0,
[string]$Button = ''
)
$cmd = "mouse_move $Dx $Dy"
[void](Invoke-QemuMonitor -Session $Session -Command $cmd)
if ($Button) {
[void](Invoke-QemuMonitor -Session $Session -Command "mouse_button $Button")
}
}
function Get-QemuScreenshot {
param(
[Parameter(Mandatory)]$Session,
[Parameter(Mandatory)][string]$OutPath
)
$resp = Invoke-QemuMonitor -Session $Session -Command "screendump `"$OutPath`"" -SettleMs 600
return (Test-Path $OutPath)
}
# ================================================================
# 场景准备(firstInit.txt / AUTOEXEC.BAT + SATA 镜像重建)
# ================================================================
function Invoke-MkFat32 {
$mk = Join-Path $script:BuildTmp 'mkfat32.ps1'
if (-not (Test-Path $mk)) { throw "mkfat32.ps1 not found: $mk (run build.ps1 once)" }
& powershell -ExecutionPolicy Bypass -File $mk | Out-Null
if ($LASTEXITCODE -ne 0) { throw "mkfat32.ps1 failed with exit code $LASTEXITCODE" }
}
# 写 firstInit.txt(格式 "<first>`n<dev_mode>",LF)并重建 SATA 镜像;原文件自动备份
function Set-DeshabFirstInit {
param(
[Parameter(Mandatory)][int]$First,
[Parameter(Mandatory)][int]$DevMode
)
$path = Join-Path $script:SystemDir 'system\user\use\firstInit.txt'
if ($null -eq $script:FirstInitBackup) {
$script:FirstInitBackup = [System.IO.File]::ReadAllBytes($path)
}
[System.IO.File]::WriteAllText($path, "$First`n$DevMode")
Invoke-MkFat32
}
function Restore-DeshabFirstInit {
$path = Join-Path $script:SystemDir 'system\user\use\firstInit.txt'
if ($null -ne $script:FirstInitBackup) {
[System.IO.File]::WriteAllBytes($path, $script:FirstInitBackup)
$script:FirstInitBackup = $null
Invoke-MkFat32
}
}
# 替换 AUTOEXEC.BAT 内容(cmd.elf 启动时逐行自动执行)并重建 SATA 镜像;原文件自动备份
function Set-DeshabAutoexec {
param([Parameter(Mandatory)][string[]]$Lines)
$path = Join-Path $script:SystemDir 'bin\AUTOEXEC.BAT'
if ($null -eq $script:AutoexecBackup) {
$script:AutoexecBackup = [System.IO.File]::ReadAllBytes($path)
}
[System.IO.File]::WriteAllText($path, (($Lines -join "`r`n") + "`r`n"))
Invoke-MkFat32
}
function Restore-DeshabAutoexec {
$path = Join-Path $script:SystemDir 'bin\AUTOEXEC.BAT'
if ($null -ne $script:AutoexecBackup) {
[System.IO.File]::WriteAllBytes($path, $script:AutoexecBackup)
$script:AutoexecBackup = $null
Invoke-MkFat32
}
}
function Restore-DeshabScenario {
Restore-DeshabFirstInit
Restore-DeshabAutoexec
}
# ================================================================
# 结果与断言
# ================================================================
function New-TestCaseResult {
param([Parameter(Mandatory)][string]$Case)
return [pscustomobject]@{
Case = $Case
Status = 'PASS'
StartTime = Get-Date
DurationSec = 0.0
LogPath = ''
Checks = New-Object System.Collections.ArrayList
Notes = New-Object System.Collections.ArrayList
}
}
function Add-TestCheck {
param(
[Parameter(Mandatory)]$Result,
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$Status, # PASS / FAIL / MANUAL / SKIP
[string]$Detail = ''
)
[void]$Result.Checks.Add([pscustomobject]@{ Name = $Name; Status = $Status; Detail = $Detail })
if ($Status -eq 'FAIL') { $Result.Status = 'FAIL' }
}
function Add-TestNote {
param([Parameter(Mandatory)]$Result, [Parameter(Mandatory)][string]$Note)
[void]$Result.Notes.Add($Note)
}
# 日志断言引擎:必须包含 / 禁止包含 / 按顺序包含 / 出现次数下限
function Assert-QemuLog {
param(
[Parameter(Mandatory)]$Result,
[Parameter(Mandatory)][string]$LogText,
[string[]]$MustContain = @(),
[string[]]$MustNotContain = @(),
[string[]]$Ordered = @(),
[string]$CountPattern = '',
[int]$MinCount = 0
)
foreach ($p in $MustContain) {
if ($LogText.Contains($p)) {
Add-TestCheck $Result "锚点: $p" 'PASS'
} else {
Add-TestCheck $Result "锚点: $p" 'FAIL' 'pattern not found in serial log'
}
}
foreach ($p in $MustNotContain) {
if ($LogText.Contains($p)) {
Add-TestCheck $Result "禁止: $p" 'FAIL' 'forbidden pattern found in serial log'
} else {
Add-TestCheck $Result "禁止: $p" 'PASS'
}
}
if ($Ordered.Count -gt 0) {
$pos = 0
$failAt = ''
foreach ($p in $Ordered) {
$idx = $LogText.IndexOf($p, $pos)
if ($idx -lt 0) { $failAt = $p; break }
$pos = $idx + $p.Length
}
if ($failAt -eq '') {
Add-TestCheck $Result ("顺序链({0} 锚点)" -f $Ordered.Count) 'PASS' ($Ordered -join ' -> ')
} else {
Add-TestCheck $Result ("顺序链({0} 锚点)" -f $Ordered.Count) 'FAIL' "missing/out-of-order: $failAt"
}
}
if ($CountPattern -ne '' -and $MinCount -gt 0) {
$count = 0
$pos = 0
while ($true) {
$idx = $LogText.IndexOf($CountPattern, $pos)
if ($idx -lt 0) { break }
$count++
$pos = $idx + $CountPattern.Length
}
if ($count -ge $MinCount) {
Add-TestCheck $Result "计数: '$CountPattern' x$count (>= $MinCount)" 'PASS'
} else {
Add-TestCheck $Result "计数: '$CountPattern' x$count (>= $MinCount)" 'FAIL' "only $count occurrences"
}
}
}
# 输出用例结果;FAIL 时附带日志尾部上下文
function Write-TestCaseResult {
param([Parameter(Mandatory)]$Result)
$Result.DurationSec = [math]::Round(((Get-Date) - $Result.StartTime).TotalSeconds, 1)
Write-Host ''
Write-Host ("=== [{0}] {1} ({2}s) ===" -f $Result.Case, $Result.Status, $Result.DurationSec)
foreach ($c in $Result.Checks) {
$mark = '[PASS]'
if ($c.Status -eq 'FAIL') { $mark = '[FAIL] ' }
if ($c.Status -eq 'MANUAL') { $mark = '[MANUAL]' }
if ($c.Status -eq 'SKIP') { $mark = '[SKIP] ' }
Write-Host (" {0} {1}" -f $mark, $c.Name)
if ($c.Detail -and $c.Status -ne 'PASS') {
Write-Host (" -> {0}" -f $c.Detail)
}
}
foreach ($n in $Result.Notes) { Write-Host " [note] $n" }
if ($Result.Status -eq 'FAIL' -and $Result.LogPath -and (Test-Path $Result.LogPath)) {
Write-Host ' --- serial log tail (25 lines) ---'
Get-Content $Result.LogPath -Tail 25 | ForEach-Object { Write-Host " | $_" }
}
}
# ================================================================
# FirstInit 设置向导自动填表(首启流程公共助手)
# 账户卡片(3 字段) → 偏好页(5xEnter) → 网络页(3xEnter) → 完成
# 每步等串口锚点再发键;字段验证失败自动重试 3 次。
# ================================================================
function Complete-DeshabFirstInitWizard {
param(
[Parameter(Mandatory)]$Session,
[Parameter(Mandatory)]$Result,
[string]$Prefix = 'FirstInit'
)
$hit = Wait-QemuLog -Session $Session -Patterns @('[FI] fadein done') -TimeoutSeconds 150
if (-not $hit) { Add-TestCheck $Result "${Prefix}: 账户卡片就绪" 'FAIL' 'missing [FI] fadein done'; return $false }
$done = $false
for ($attempt = 1; $attempt -le 3 -and -not $done; $attempt++) {
Send-QemuText -Session $Session -Text 'testpc' -Enter
Start-Sleep -Milliseconds 400
Send-QemuText -Session $Session -Text 'tester' -Enter
Start-Sleep -Milliseconds 400
Send-QemuText -Session $Session -Text 'pass123' -Enter
$hit = Wait-QemuLog -Session $Session -Patterns @('[FI] all fields done', '[FI] validation failed') -TimeoutSeconds 30
if ($hit -eq '[FI] all fields done') { $done = $true }
else { Add-TestNote $Result "${Prefix}: 字段验证失败,重试第 $attempt 次" }
}
if (-not $done) { Add-TestCheck $Result "${Prefix}: 账户字段填写" 'FAIL' 'validation did not pass after 3 attempts'; return $false }
Add-TestCheck $Result "${Prefix}: 账户字段填写" 'PASS'
Start-Sleep -Milliseconds 600
Send-QemuKeys -Session $Session -Keys @('ret', 'ret', 'ret', 'ret', 'ret') -DelayMs 350
$hit = Wait-QemuLog -Session $Session -Patterns @('[FirstInit] entering network setup page') -TimeoutSeconds 40
if (-not $hit) { Add-TestCheck $Result "${Prefix}: 偏好页" 'FAIL' 'did not reach network setup page'; return $false }
Add-TestCheck $Result "${Prefix}: 偏好页(5xEnter)" 'PASS'
Start-Sleep -Milliseconds 400
Send-QemuKeys -Session $Session -Keys @('ret', 'ret', 'ret') -DelayMs 350
$hit = Wait-QemuLog -Session $Session -Patterns @('[FirstInit] user setup finished') -TimeoutSeconds 40
if (-not $hit) { Add-TestCheck $Result "${Prefix}: 网络页" 'FAIL' 'setup did not finish'; return $false }
Add-TestCheck $Result "${Prefix}: 网络页(3xEnter)" 'PASS'
return $true
}
# ================================================================
# 极简 HTTP 测试服务器(network-e2e 用)
# slirp 网关 10.0.2.2 → 主机 127.0.0.1;guest curl 经 slirp 访问主机服务。
# 后台 Job 接收连接,把每个请求首行写入 $LogPath(主机侧端到端证据),
# 回复固定 HTTP/1.0 200 + 标记体后关闭。
# ================================================================
function Start-TestHttpServer {
param(
[int]$Port = 8000,
[Parameter(Mandatory)][string]$LogPath,
[string]$Marker = 'DESHAB-NET-E2E-MARKER'
)
if (Test-Path $LogPath) { Remove-Item $LogPath -Force }
$job = Start-Job -ScriptBlock {
param($Port, $LogPath, $Marker)
$listener = $null
try {
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port)
$listener.Start()
$body = "<html><body><h1>$Marker</h1></body></html>"
$deadline = (Get-Date).AddMinutes(30)
while ((Get-Date) -lt $deadline) {
if (-not $listener.Pending()) { Start-Sleep -Milliseconds 200; continue }
$client = $listener.AcceptTcpClient()
try {
$stream = $client.GetStream()
$stream.ReadTimeout = 5000
$buf = New-Object byte[] 4096
$req = ''
while ($stream.DataAvailable -or $req -eq '') {
$n = 0
try { $n = $stream.Read($buf, 0, $buf.Length) } catch { break }
if ($n -le 0) { break }
$req += [System.Text.Encoding]::ASCII.GetString($buf, 0, $n)
if ($req.Contains("`r`n`r`n")) { break }
}
$firstLine = ($req -split "`r`n")[0]
[System.IO.File]::AppendAllText($LogPath, "$(Get-Date -Format 'HH:mm:ss') $firstLine`r`n")
$resp = "HTTP/1.0 200 OK`r`nContent-Type: text/html`r`nContent-Length: $($body.Length)`r`nConnection: close`r`n`r`n$body"
$bytes = [System.Text.Encoding]::ASCII.GetBytes($resp)
$stream.Write($bytes, 0, $bytes.Length)
$stream.Flush()
} finally {
$client.Close()
}
}
} finally {
if ($listener) { $listener.Stop() }
}
} -ArgumentList $Port, $LogPath, $Marker
Start-Sleep -Milliseconds 800
if ($job.State -ne 'Running') {
$err = Receive-Job $job 2>&1 | Out-String
throw "http test server failed to start on port ${Port}: $err"
}
Write-Host "[http] test server listening on 127.0.0.1:$Port (log=$LogPath)"
return $job
}
function Stop-TestHttpServer {
param($Job)
if ($Job) {
Stop-Job $Job -ErrorAction SilentlyContinue
Remove-Job $Job -Force -ErrorAction SilentlyContinue
}
}
+167
View File
@@ -0,0 +1,167 @@
#requires -Version 5.1
<#
.SYNOPSIS
linux-compat — UTSM+Linux 双内核兼容层端到端(docs/RE/test-cases/linux-compat.md)
.DESCRIPTION
动态检测 VMM/Linux guest 可用性,自动分流两条验证路径:
路径 A(guest VMX 可用,真机/KVM):
[UTSM] VMM init ok → VMM self-test PASS → bzImage 解析 → IPC shm →
vmlaunch → Linux version → initramfs init → daemon park →
[LNXC] service ready → DSK → shell dev_tests → dsl 命令实测
路径 B(QEMU WHPX/TCG 无 nested VMX —— 本环境预期路径):
[UTSM] VMM unavailable → Linux loader 跳过 → DSK 正常启动不崩溃 →
shell 收到 linux_compat 服务表 → dsl 报告运行态不可用 → desktop。
A5.1~A5.12 标记 SKIP(环境限制,非产品缺陷)。
依据 ISO/run_qemu.bat 说明:WHPX 下 VMXON 会触发致命 hypervisor exit,
因此框架默认 -cpu qemu64,-vmx 关闭 guest VMX CPUID,UTSM 走优雅降级。
若未来 WHPX/KVM 支持 nested VMX,可用 -Cpu 'max' 复测路径 A。
退出码:全部通过 0,任一 FAIL 1。
#>
param(
[switch]$Build,
[string]$Cpu = ''
)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
Initialize-QemuTest -Build:$Build
$result = New-TestCaseResult 'linux-compat'
Set-DeshabFirstInit -First 1 -DevMode 1
Set-DeshabAutoexec -Lines @('ver', 'exit')
$s = Start-QemuSession -Name 'linux-compat' -Cpu $Cpu
try {
# ---- 分流点:VMM 自检通过 还是 VMM 不可用 ----
$fork = Wait-QemuLog -Session $s -Patterns @(
'[UTSM] VMM self-test PASS',
'[UTSM] VMM unavailable'
) -TimeoutSeconds 180
if ($fork -eq '[UTSM] VMM self-test PASS') {
# ================= 路径 A:VMX 可用,完整双内核流程 =================
Write-Host "`n--- 路径 A: guest VMX 可用,验证完整 Linux 兼容层 ---"
$hit = Wait-QemuLog -Session $s -Patterns @('=== 自动测试完成 ===', '[PANIC]') -TimeoutSeconds 300
$log = Read-QemuLogText $s.LogPath
$result.LogPath = $s.LogPath
# A5.1 ~ A5.7 启动链
Assert-QemuLog $result $log -Ordered @(
'[UTSM] VMM init ok',
'[UTSM] VMM self-test PASS',
'[LINUX] found bzImage module:',
'[UTSM] Linux loader init ok',
'[UTSM] IPC shm init ok',
'[VMM] launching guest',
'Linux version',
'[utsm-linux] init started',
'[LINUX] guest parked (daemon ready)',
'[LNXC] service ready',
'[DSK] boot',
'[shell] linux_compat service ok'
) -MustNotContain @('[PANIC]')
if (-not $hit -or $hit -eq '[PANIC]') {
Add-TestCheck $result 'A5.7: DSK/shell 接管' 'FAIL' 'dev_tests did not complete'
} else {
Add-TestCheck $result 'A5.7: DSK/shell 接管' 'PASS'
}
# A5.8 ~ A5.12 shell dsl 交互(dev_mode 串口镜像开启)
if ($hit -eq '=== 自动测试完成 ===') {
Start-Sleep -Seconds 1
Send-QemuText -Session $s -Text 'dsl' -Enter
$h1 = Wait-QemuLog -Session $s -Patterns @('Linux 兼容层已就绪') -TimeoutSeconds 60
if ($h1) { Add-TestCheck $result 'A5.8: dsl 状态查询' 'PASS' }
else { Add-TestCheck $result 'A5.8: dsl 状态查询' 'FAIL' 'missing 兼容层已就绪' }
Send-QemuText -Session $s -Text 'dsl ls' -Enter
$h2 = Wait-QemuLog -Session $s -Patterns @('bin', 'etc', 'deshab#') -TimeoutSeconds 60
if ($h2) { Add-TestCheck $result 'A5.9: dsl ls 目录列表' 'PASS' }
else { Add-TestCheck $result 'A5.9: dsl ls 目录列表' 'FAIL' 'no listing output' }
Send-QemuText -Session $s -Text 'dsl uname -a' -Enter
$h3 = Wait-QemuLog -Session $s -Patterns @('Linux') -TimeoutSeconds 60
if ($h3) { Add-TestCheck $result 'A5.10: dsl uname -a' 'PASS' }
else { Add-TestCheck $result 'A5.10: dsl uname -a' 'FAIL' 'no uname output' }
Send-QemuText -Session $s -Text 'dsl nosuchprog' -Enter
$h4 = Wait-QemuLog -Session $s -Patterns @('deshab# ') -TimeoutSeconds 60
Start-Sleep -Seconds 1
$log2 = Read-QemuLogText $s.LogPath
if ($h4 -and -not $log2.Contains('[PANIC]')) {
Add-TestCheck $result 'A5.12: dsl 错误路径不崩溃' 'PASS'
} else {
Add-TestCheck $result 'A5.12: dsl 错误路径不崩溃' 'FAIL' 'shell did not recover'
}
Send-QemuKeys -Session $s -Keys @('esc')
$h5 = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
if ($h5) { Add-TestCheck $result 'A5.x: Esc 后到 desktop' 'PASS' }
else { Add-TestCheck $result 'A5.x: Esc 后到 desktop' 'FAIL' 'no desktop ready' }
}
Add-TestNote $result '路径 A 执行:guest VMX 可用(真机/KVM 环境)'
}
elseif ($fork -eq '[UTSM] VMM unavailable') {
# ================= 路径 B:无 nested VMX,优雅降级 =================
Write-Host "`n--- 路径 B: guest 无 VMX(QEMU 预期),验证优雅降级 ---"
$hit = Wait-QemuLog -Session $s -Patterns @('=== 自动测试完成 ===', '[PANIC]') -TimeoutSeconds 240
$log = Read-QemuLogText $s.LogPath
$result.LogPath = $s.LogPath
# 降级锚点 + 系统继续正常启动
Assert-QemuLog $result $log -MustContain @('[UTSM] VMM unavailable')
Assert-QemuLog $result $log -Ordered @(
'[UTSM] SELFTEST PASS',
'[UTSM] VMM unavailable',
'[DSK] boot',
'[DSK] dev_mode=1 (developer auto-test)',
'[shell] boot',
'[shell] linux_compat service ok'
) -MustNotContain @('[PANIC]')
if ($hit -eq '=== 自动测试完成 ===') {
Add-TestCheck $result '降级路径: dev_tests 完成(Linux 缺失不阻塞启动)' 'PASS'
} else {
Add-TestCheck $result '降级路径: dev_tests 完成(Linux 缺失不阻塞启动)' 'FAIL' "hit=$hit"
}
# dsl 运行态不可用报告(服务表已传递,is_available()=0)
if ($hit -eq '=== 自动测试完成 ===') {
Start-Sleep -Seconds 1
Send-QemuText -Session $s -Text 'dsl' -Enter
$h = Wait-QemuLog -Session $s -Patterns @('linux_compat: not initialized') -TimeoutSeconds 40
if ($h) { Add-TestCheck $result '降级路径: dsl 报告运行态不可用' 'PASS' }
else { Add-TestCheck $result '降级路径: dsl 报告运行态不可用' 'FAIL' 'missing linux_compat: not initialized' }
Send-QemuKeys -Session $s -Keys @('esc')
$h2 = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
if ($h2) { Add-TestCheck $result '降级路径: Esc 后经 cmd 到 desktop' 'PASS' }
else { Add-TestCheck $result '降级路径: Esc 后经 cmd 到 desktop' 'FAIL' 'no desktop ready' }
}
# A5.1~A5.12 环境性跳过
foreach ($t in @('A5.1 VMM self-test', 'A5.2 bzImage 加载', 'A5.3 EPT+vmlaunch',
'A5.4 Linux 内核启动', 'A5.5 init 脚本', 'A5.6 daemon park',
'A5.8 dsl 状态就绪', 'A5.9 dsl ls', 'A5.10 dsl uname',
'A5.11 dsl cat hostname', 'A5.12 dsl 错误路径')) {
Add-TestCheck $result $t 'SKIP' 'QEMU WHPX/TCG 无 nested VMX(见 run_qemu.bat 注释),需真机或 KVM 复测'
}
Add-TestCheck $result 'A5.7: Linux 缺失时 DSK 正常接管' 'PASS'
Add-TestNote $result '路径 B 执行:QEMU 无 nested VMX,Linux guest 不可用属预期环境限制'
Add-TestNote $result '如需完整验证:真机(VT-x)或 Linux KVM (-enable-kvm -cpu host) 下以 -Cpu max 重跑'
}
else {
$result.LogPath = $s.LogPath
Add-TestCheck $result 'VMM 分流探测' 'FAIL' 'neither VMM self-test PASS nor VMM unavailable within 180s'
}
} finally {
Stop-QemuSession $s
Restore-DeshabScenario
}
Write-TestCaseResult $result
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
+84
View File
@@ -0,0 +1,84 @@
#requires -Version 5.1
<#
.SYNOPSIS
pe-compat — PE/EXE 兼容层测试(docs/RE/test-cases/pe-compat.md)
.DESCRIPTION
正常启动到 cmd.elf,用 AUTOEXEC.BAT 自动执行 PE 命令序列:
pe HELLO64.EXE → PE32+ 原生 Ring0 执行,shim_WriteFile 输出串口
pe HELLO32.EXE → PE32 x86emu32 解释执行,shim32 输出串口
pe NONEXIST.EXE → 错误路径:不产生 [cmd] PE run begin
exit → 返回 DSK → desktop
串口可断言锚点:
[cmd] PE run begin / [cmd] cmdline: ... / [cmd] PE run end (logl/swrite, 始终上串口)
[PE] PE32+ (64-bit) / [PE] running PE32+ natively (pe_service)
[PE] running PE32 via x86emu32 (pe_service)
Hello from PE32+! / Hello from PE32! (shim WriteFile → 串口)
peinfo / 无参用法提示只写帧缓冲(cmd 非 dev_mode 无串口镜像),标 MANUAL。
退出码:全部通过 0,任一 FAIL 1。
#>
param([switch]$Build)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
Initialize-QemuTest -Build:$Build
$result = New-TestCaseResult 'pe-compat'
Set-DeshabFirstInit -First 1 -DevMode 0
Set-DeshabAutoexec -Lines @('pe HELLO64.EXE', 'pe HELLO32.EXE', 'pe NONEXIST.EXE', 'exit')
$s = Start-QemuSession -Name 'pe-compat'
try {
$hit = Wait-QemuLog -Session $s -Patterns @('[DSK] cmd.elf returned') -TimeoutSeconds 180
Start-Sleep -Seconds 2
$log = Read-QemuLogText $s.LogPath
$result.LogPath = $s.LogPath
# ---- B2.1 PE32+ 加载执行 ----
Assert-QemuLog $result $log -Ordered @(
'[cmd] running AUTOEXEC.BAT',
'[cmd] PE run begin',
'[cmd] cmdline: pe HELLO64.EXE',
'[PE] running PE32+ natively',
'Hello from PE32+!',
'[cmd] PE run end'
)
# ---- B2.2 PE32 解释器执行 ----
Assert-QemuLog $result $log -Ordered @(
'Hello from PE32+!',
'[cmd] cmdline: pe HELLO32.EXE',
'[PE] running PE32 via x86emu32',
'Hello from PE32!',
'[cmd] PE run end'
)
# ---- B2.4 文件不存在错误路径:NONEXIST 不得产生 PE run begin ----
Assert-QemuLog $result $log -MustNotContain @('[cmd] cmdline: pe NONEXIST.EXE')
# 恰好两次 PE 执行(HELLO64 + HELLO32)
Assert-QemuLog $result $log -CountPattern '[cmd] PE run begin' -MinCount 2
# ---- B2.6 ExitProcess 返回:cmd 接管继续执行后续命令并最终返回 DSK ----
if ($hit) { Add-TestCheck $result 'B2.6: ExitProcess 后 cmd 返回 DSK' 'PASS' }
else { Add-TestCheck $result 'B2.6: ExitProcess 后 cmd 返回 DSK' 'FAIL' 'missing [DSK] cmd.elf returned' }
# ---- ABI 验证(源码级,日志侧证) ----
Assert-QemuLog $result $log -MustContain @('[cmd] PE service ok') # ctx->reserved[4] + PE_SERVICE_MAGIC 校验通过
# ---- 帧缓冲限定项(无串口输出,标 MANUAL) ----
Add-TestCheck $result 'B2.3: peinfo HELLO64.EXE 头解析' 'MANUAL' 'peinfo 输出仅帧缓冲(cmd 非 dev_mode 无串口镜像),手动验证 Machine: 0x8664'
Add-TestCheck $result 'B2.7: peinfo HELLO32.EXE 头解析' 'MANUAL' '同上,预期 Machine: 0x14c / image_base=0x400000'
Add-TestCheck $result 'B2.5: pe 无参用法提示' 'MANUAL' '用法提示仅帧缓冲输出'
Assert-QemuLog $result $log -MustNotContain @('[PANIC]')
} finally {
Stop-QemuSession $s
Restore-DeshabScenario
}
Write-TestCaseResult $result
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
+177
View File
@@ -0,0 +1,177 @@
#requires -Version 5.1
<#
.SYNOPSIS
userapps-matrix — 用户态程序功能矩阵(docs/RE/test-cases/userapps-matrix.md)
.DESCRIPTION
dev_mode 单会话覆盖 shell 内建命令与工具程序(串口镜像开启,全程可断言):
1) dev_tests 硬编码序列自动执行(ls/echo/cat/cp/mv/rm,SATA 镜像无 test.bas)
2) 交互阶段经 monitor 按键驱动:
help / id / pci(存活验证)→
ping (PATH 查找 /bin/PING.ELF,输入 10.0.2.2,Esc 退出) →
run EDITOR.ELF / FILEMAN.ELF / BROWSER.ELF(root 目录加载,Esc 退出)→
Esc 回 DSK → cmd(AUTOEXEC) → desktop
串口可断言锚点:
[shell] path: running /bin/ tool / [shell] path: tool returned
[shell] run: jumping to tool / [shell] run: tool returned
[ping] boot|exit [editor] boot|exit [fileman] boot|exit [browser] boot|exit
工具内部 UI 输出(编辑器内容、fileman 列表、ping RTT)仅帧缓冲,标 MANUAL。
desktop 双击/拖动/任务栏无串口锚点,标 MANUAL。
退出码:全部通过 0,任一 FAIL 1。
#>
param([switch]$Build)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
Initialize-QemuTest -Build:$Build
$result = New-TestCaseResult 'userapps-matrix'
Set-DeshabFirstInit -First 1 -DevMode 1
Set-DeshabAutoexec -Lines @('ver', 'exit')
$s = Start-QemuSession -Name 'userapps-matrix'
try {
# ================= 阶段 1: dev_tests 自动序列 =================
$hit = Wait-QemuLog -Session $s -Patterns @('=== 自动测试完成 ===', '[PANIC]') -TimeoutSeconds 240
$log = Read-QemuLogText $s.LogPath
$result.LogPath = $s.LogPath
Assert-QemuLog $result $log -Ordered @(
'[DSK:SCHED] decision: dev_mode -> shell.elf',
'[shell] boot',
'[shell] test.bas not found, fallback to hardcoded',
'[1] ls',
'[2] echo "Deshab dev test" > TEST.TXT',
'[3] cat TEST.TXT',
'[4] cp TEST.TXT COPY.TXT',
'[6] mv COPY.TXT MOVED.TXT',
'[8] rm TEST.TXT',
'[10] ls'
) -MustNotContain @('[PANIC]')
if ($hit -eq '=== 自动测试完成 ===') {
Add-TestCheck $result 'shell: dev_tests 完成(ls/echo/cat/cp/mv/rm)' 'PASS'
} else {
Add-TestCheck $result 'shell: dev_tests 完成(ls/echo/cat/cp/mv/rm)' 'FAIL' "hit=$hit"
}
# ================= 阶段 2: 交互命令 =================
if ($hit -eq '=== 自动测试完成 ===') {
# --- help ---
Start-Sleep -Seconds 1
Send-QemuText -Session $s -Text 'help' -Enter
$h = Wait-QemuLog -Session $s -Patterns @('dsl push <') -TimeoutSeconds 30
if ($h) { Add-TestCheck $result 'shell: help 命令列表' 'PASS' }
else { Add-TestCheck $result 'shell: help 命令列表' 'FAIL' 'missing help output' }
# --- id(uid/gid 输出为 ASCII 强锚点) ---
Send-QemuText -Session $s -Text 'id' -Enter
$h = Wait-QemuLog -Session $s -Patterns @('uid=0(root) gid=0(root)') -TimeoutSeconds 30
if ($h) { Add-TestCheck $result 'shell: id/whoami 用户查询' 'PASS' }
else { Add-TestCheck $result 'shell: id/whoami 用户查询' 'FAIL' 'missing uid=0(root)' }
# --- pci(输出含中文,验证存活:pci 后 ver 仍有响应,锚点取 ver 独有行) ---
Send-QemuText -Session $s -Text 'pci' -Enter
Start-Sleep -Seconds 3
Send-QemuText -Session $s -Text 'ver' -Enter
$h = Wait-QemuLog -Session $s -Patterns @('Architecture: x86_64') -TimeoutSeconds 30
if ($h) { Add-TestCheck $result 'shell: pci 枚举后 shell 存活' 'PASS' }
else { Add-TestCheck $result 'shell: pci 枚举后 shell 存活' 'FAIL' 'no response after pci' }
# --- ping 工具(PATH → /bin/PING.ELF) ---
Send-QemuText -Session $s -Text 'ping' -Enter
$h = Wait-QemuLog -Session $s -Patterns @('[ping] boot') -TimeoutSeconds 40
if ($h) {
Add-TestCheck $result 'ping.elf: PATH 加载启动' 'PASS'
Start-Sleep -Milliseconds 800
Send-QemuText -Session $s -Text '10.0.2.2' -Enter
Start-Sleep -Seconds 12 # 4 次 ICMP echo(每次超时上限 2s)
Send-QemuKeys -Session $s -Keys @('esc')
$h2 = Wait-QemuLog -Session $s -Patterns @('[ping] exit', '[shell] path: tool returned') -TimeoutSeconds 30
$logp = Read-QemuLogText $s.LogPath
if ($h2 -and $logp.Contains('[ping] exit') -and $logp.Contains('[shell] path: tool returned')) {
Add-TestCheck $result 'ping.elf: Esc 退出返回 shell' 'PASS'
} else {
Add-TestCheck $result 'ping.elf: Esc 退出返回 shell' 'FAIL' 'missing [ping] exit / tool returned'
}
} else {
Add-TestCheck $result 'ping.elf: PATH 加载启动' 'FAIL' 'missing [ping] boot'
}
# --- run EDITOR.ELF ---
Send-QemuText -Session $s -Text 'run EDITOR.ELF' -Enter
$h = Wait-QemuLog -Session $s -Patterns @('[editor] boot') -TimeoutSeconds 40
if ($h) {
Add-TestCheck $result 'editor.elf: run 加载启动' 'PASS'
Start-Sleep -Seconds 2
Send-QemuKeys -Session $s -Keys @('esc')
$h2 = Wait-QemuLog -Session $s -Patterns @('[editor] exit') -TimeoutSeconds 30
$loge = Read-QemuLogText $s.LogPath
if ($h2 -and $loge.Contains('[shell] run: tool returned')) {
Add-TestCheck $result 'editor.elf: Esc 退出返回 shell' 'PASS'
} else {
Add-TestCheck $result 'editor.elf: Esc 退出返回 shell' 'FAIL' 'missing [editor] exit / run returned'
}
} else {
Add-TestCheck $result 'editor.elf: run 加载启动' 'FAIL' 'missing [editor] boot'
}
# --- run FILEMAN.ELF(已知问题观察项:曾触发 QEMU 退出) ---
Send-QemuText -Session $s -Text 'run FILEMAN.ELF' -Enter
$h = Wait-QemuLog -Session $s -Patterns @('[fileman] boot') -TimeoutSeconds 40
if ($h) {
Add-TestCheck $result 'fileman.elf: run 加载启动' 'PASS'
Start-Sleep -Seconds 2
Send-QemuKeys -Session $s -Keys @('esc')
$h2 = Wait-QemuLog -Session $s -Patterns @('[fileman] exit') -TimeoutSeconds 30
if ($h2) { Add-TestCheck $result 'fileman.elf: Esc 退出返回 shell(未复现 QEMU 退出)' 'PASS' }
else { Add-TestCheck $result 'fileman.elf: Esc 退出返回 shell(未复现 QEMU 退出)' 'FAIL' 'missing [fileman] exit(疑似已知问题复现)' }
} else {
Add-TestCheck $result 'fileman.elf: run 加载启动' 'FAIL' 'missing [fileman] boot(疑似已知问题复现:QEMU 退出)'
}
# --- run BROWSER.ELF ---
Send-QemuText -Session $s -Text 'run BROWSER.ELF' -Enter
$h = Wait-QemuLog -Session $s -Patterns @('[browser] boot') -TimeoutSeconds 40
if ($h) {
Add-TestCheck $result 'browser.elf: run 加载启动' 'PASS'
Start-Sleep -Seconds 2
Send-QemuKeys -Session $s -Keys @('esc')
$h2 = Wait-QemuLog -Session $s -Patterns @('[browser] exit') -TimeoutSeconds 30
if ($h2) { Add-TestCheck $result 'browser.elf: Esc 退出返回 shell' 'PASS' }
else { Add-TestCheck $result 'browser.elf: Esc 退出返回 shell' 'FAIL' 'missing [browser] exit' }
} else {
Add-TestCheck $result 'browser.elf: run 加载启动' 'FAIL' 'missing [browser] boot'
}
# --- Esc 回 DSK → cmd → desktop ---
Send-QemuKeys -Session $s -Keys @('esc')
$h = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
$logf = Read-QemuLogText $s.LogPath
Assert-QemuLog $result $logf -Ordered @(
'[shell] esc -> return to DSK',
'[DSK] shell auto-test returned'
) -MustNotContain @('[PANIC]')
if ($h) { Add-TestCheck $result '链路: Esc → cmd → desktop ready' 'PASS' }
else { Add-TestCheck $result '链路: Esc → cmd → desktop ready' 'FAIL' 'no desktop ready' }
}
# ================= MANUAL / 引用项 =================
Add-TestCheck $result 'FirstInit 向导(程序 1)' 'MANUAL' '由 boot-regression.ps1 B1 自动化覆盖(monitor 按键填表)'
Add-TestCheck $result 'login(程序 2)' 'SKIP' 'FUCK [dsk] skip_login=1 跳过;USER.CONF 解密路径需人工验证'
Add-TestCheck $result 'desktop 双击/窗口拖动/任务栏(程序 3)' 'MANUAL' '无串口锚点,需人工或 screendump 验证'
Add-TestCheck $result 'cmd pe 兼容(程序 5)' 'MANUAL' '由 pe-compat.ps1 自动化覆盖'
Add-TestCheck $result 'netman(程序 6)' 'MANUAL' '由 network-e2e.ps1 场景 N-A 自动化覆盖(netman 仅首启调度)'
Add-TestCheck $result 'mouseInit(程序 7)' 'MANUAL' '由 boot-regression.ps1 B1 首启链路覆盖'
Add-TestCheck $result 'curl.elf HTTP(程序 11)' 'MANUAL' '由 network-e2e.ps1 场景 N-B 自动化覆盖(主机侧 HTTP 服务器断言)'
Add-TestCheck $result 'editor 保存/fileman 浏览/ping RTT' 'MANUAL' '工具内部输出仅帧缓冲,无串口锚点'
} finally {
Stop-QemuSession $s
Restore-DeshabScenario
}
Write-TestCaseResult $result
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }