feat: B7 APIC/IOAPIC接管 + NVMe写路径 + virtio_net virtqueue + PE兼容层修复 + QEMU测试套件全自动化
中断路由 (B7): - apic.drv: LAPIC enable + IOAPIC重定向表编程(MADT ISO感知) + apic_route/apic_route_legacy/apic_diag开关 - UTSM: LAPIC EOI钩子(register_apic_eoi, ABI尾部追加) + IDT 0-255全覆盖 + spurious 0xFF特判 - FUCK: apic_route=0默认PIC(零回归) / =1 IOAPIC->LAPIC接管(失败完整降级) / legacy放开IRQ1/12/11 存储: - nvme.drv: 写路径 + PRP list + 读回校验 + MSI/MSI-X中断驱动(nvme_msi开关, 回退纯轮询) - build.ps1: apic.drv纳入构建(原为预置二进制) 网络: - virtio_net: virtqueue RX/TX + netdev注册 + DHCP自测(slirp真实OFFER) PE兼容层: - pe_shim: guest->host指针转换(g_emu_mem_base + gp()) + shim fn32列补齐 - gen_pe.py: PE32+ RVA偏置修复(BUG-004) + cmd.exe生成 - desktop_app.h: da_mouse_poll先判AUX位 + da_mouse_init冲刷残留(BUG-003/008) - shell: Ctrl修饰键跟踪(BUG-001) + g_run_image扩至1MB(BUG-002) QEMU测试套件: - 9用例全自动化(Run-All.ps1): boot-regression/driver-matrix/linux-compat/pe-compat/deaicup-e2e/userapps-matrix/network-e2e/performance-baseline/x86emu32-coverage - QemuTest.ps1公共库: 串口断言/monitor sendkey/screendump/HTTP服务器/并发隔离 文档: - CLAUDE.md: APIC/IOAPIC能力 + kernel_api暴露 + IRQ后端升级状态 - LESSONS_LEARNED.md: B7 LAPIC/IOAPIC经验教训(2条) - Bug记录.txt: BUG-20260801-001~008 + BUG-20260803-009(9条) - test-cases/*.md: 全部补充自动化脚本小节 - 桌面设计.md: .lnk快捷方式 + LINUXAPP.CNF Linux应用图标 - 系统开发策划.md: NVMe/virtio_net标完成 + B2/B7标已解决 + M2进度列
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run-All — Deshab QEMU 自动化测试套件总运行器
|
||||
|
||||
.DESCRIPTION
|
||||
顺序执行 tests/qemu 下全部 9 个用例脚本,每用例独立 powershell 子进程
|
||||
(独立 QEMU 会话 + firstInit/AUTOEXEC 场景隔离),汇总 PASS/FAIL/耗时。
|
||||
|
||||
用例顺序(依赖与耗时综合考虑,快用例靠后):
|
||||
1. boot-regression.ps1 B1 首启向导 / B2 正常 / B3 dev_mode / B4 无盘 / B5 无 NIC
|
||||
2. driver-matrix.ps1 16 清单驱动加载矩阵 + NVMe 写路径回归
|
||||
3. linux-compat.ps1 VMM 分流(QEMU 走 VMM unavailable 优雅降级路径)
|
||||
4. pe-compat.ps1 PE32+ 原生 + PE32 x86emu32 + 错误路径
|
||||
5. deaicup-e2e.ps1 Deaicup GUI PE 端到端(首帧像素/键鼠消息/Esc 退出链)
|
||||
6. userapps-matrix.ps1 shell dev_tests + ping/editor/fileman/browser 工具链
|
||||
7. network-e2e.ps1 N-A netman 全链路 + N-B curl 主机侧端到端
|
||||
8. performance-baseline.ps1
|
||||
启动阶段墙上计时 → .build_tmp/perf_baseline.json
|
||||
9. x86emu32-coverage.ps1 x86emu32 静态枚举 + hello32 动态解释执行
|
||||
|
||||
.PARAMETER Only
|
||||
只跑指定用例(逗号分隔的脚本基名,大小写不敏感):
|
||||
.\Run-All.ps1 -Only network-e2e,x86emu32-coverage
|
||||
|
||||
.PARAMETER Build
|
||||
先执行一次 build.ps1(全量编译+打包镜像)再跑用例。
|
||||
|
||||
.NOTES
|
||||
退出码:全部用例 PASS → 0;任一 FAIL → 1。
|
||||
每用例完整输出存档 .build_tmp\tests\runall\<case>.log。
|
||||
#>
|
||||
param(
|
||||
[string[]]$Only = @(),
|
||||
[switch]$Build
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$suiteRoot = $PSScriptRoot
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $suiteRoot '..\..'))
|
||||
$outDir = Join-Path $repoRoot '.build_tmp\tests\runall'
|
||||
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
||||
|
||||
# ---- 跑前清理:只杀本套件历史会话残留的 QEMU(-name deshabtest- 标记)。
|
||||
# 绝不全局清理——可能有其他代理/人工在并发跑 QEMU(如 monitor 45454 的调试会话)。
|
||||
# 本套件所有会话默认 -snapshot(镜像只读),无需靠杀进程解决镜像锁。 ----
|
||||
. (Join-Path $suiteRoot 'lib\QemuTest.ps1')
|
||||
Stop-ResidualQemu
|
||||
|
||||
# ---- 可选:先构建一次 ----
|
||||
if ($Build) {
|
||||
Write-Host '[runall] running build.ps1 ...'
|
||||
& powershell -ExecutionPolicy Bypass -File (Join-Path $repoRoot 'build.ps1')
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[runall] build.ps1 FAILED (exit $LASTEXITCODE)"; exit 1 }
|
||||
}
|
||||
|
||||
$cases = @(
|
||||
'boot-regression',
|
||||
'driver-matrix',
|
||||
'linux-compat',
|
||||
'pe-compat',
|
||||
'deaicup-e2e',
|
||||
'userapps-matrix',
|
||||
'network-e2e',
|
||||
'performance-baseline',
|
||||
'x86emu32-coverage'
|
||||
)
|
||||
if ($Only.Count -gt 0) {
|
||||
# -File 调用下逗号列表常作为单个字符串传入,统一按逗号再切一次
|
||||
$want = @($Only | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ })
|
||||
$cases = @($cases | Where-Object { $want -contains $_.ToLower() })
|
||||
if ($cases.Count -eq 0) { Write-Host "[runall] -Only 过滤后无用例可跑: $($Only -join ',')"; exit 1 }
|
||||
}
|
||||
|
||||
$rows = New-Object System.Collections.ArrayList
|
||||
$suiteStart = Get-Date
|
||||
foreach ($case in $cases) {
|
||||
$script = Join-Path $suiteRoot "$case.ps1"
|
||||
$log = Join-Path $outDir "$case.log"
|
||||
Write-Host ''
|
||||
Write-Host ("================ [runall] {0} ================" -f $case)
|
||||
$t0 = Get-Date
|
||||
& powershell -ExecutionPolicy Bypass -File $script 2>&1 | Tee-Object -FilePath $log | Out-Host
|
||||
$rc = $LASTEXITCODE
|
||||
$dur = [math]::Round(((Get-Date) - $t0).TotalSeconds, 1)
|
||||
|
||||
$outText = ''
|
||||
if (Test-Path $log) { $outText = Get-Content $log -Raw }
|
||||
$manualCount = ([regex]::Matches($outText, '\[MANUAL\]')).Count
|
||||
$skipCount = ([regex]::Matches($outText, '\[SKIP\]')).Count
|
||||
$status = 'PASS'
|
||||
if ($rc -ne 0) { $status = 'FAIL' }
|
||||
|
||||
[void]$rows.Add([pscustomobject]@{
|
||||
Case = $case
|
||||
Status = $status
|
||||
Duration = "${dur}s"
|
||||
Manual = $manualCount
|
||||
Skip = $skipCount
|
||||
ExitCode = $rc
|
||||
Log = $log
|
||||
})
|
||||
}
|
||||
|
||||
# ---- 汇总表 ----
|
||||
$totalDur = [math]::Round(((Get-Date) - $suiteStart).TotalMinutes, 1)
|
||||
Write-Host ''
|
||||
Write-Host ("================ 测试套件汇总 ({0} min) ================" -f $totalDur)
|
||||
$rows | Format-Table Case, Status, Duration, Manual, Skip, ExitCode -AutoSize | Out-Host
|
||||
|
||||
$failCount = @($rows | Where-Object { $_.Status -eq 'FAIL' }).Count
|
||||
$passCount = @($rows | Where-Object { $_.Status -eq 'PASS' }).Count
|
||||
Write-Host ("PASS: {0} FAIL: {1} TOTAL: {2}" -f $passCount, $failCount, $rows.Count)
|
||||
Write-Host ("日志目录: {0}" -f $outDir)
|
||||
|
||||
if ($failCount -gt 0) { exit 1 } else { exit 0 }
|
||||
@@ -1,4 +1,4 @@
|
||||
#requires -Version 5.1
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
boot-regression — 启动链路回归测试(docs/RE/test-cases/boot-regression.md)
|
||||
@@ -67,7 +67,7 @@ function Complete-FirstInitWizard {
|
||||
function Invoke-ScenarioB1 {
|
||||
Write-Host "`n--- B1 首次启动 (firstInit=0) ---"
|
||||
Set-DeshabFirstInit -First 0 -DevMode 0
|
||||
$s = Start-QemuSession -Name 'bootreg-b1'
|
||||
$s = Start-QemuSession -Name 'bootreg-b1' -MonitorPort 45501
|
||||
try {
|
||||
if (-not (Complete-FirstInitWizard -Session $s)) { return }
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 120
|
||||
@@ -100,7 +100,7 @@ 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'
|
||||
$s = Start-QemuSession -Name 'bootreg-b2' -MonitorPort 45501
|
||||
try {
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
|
||||
$log = Read-QemuLogText $s.LogPath
|
||||
@@ -131,7 +131,7 @@ 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'
|
||||
$s = Start-QemuSession -Name 'bootreg-b3' -MonitorPort 45501
|
||||
try {
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @('[8] rm TEST.TXT') -TimeoutSeconds 150
|
||||
if (-not $hit) {
|
||||
@@ -165,7 +165,7 @@ function Invoke-ScenarioB3 {
|
||||
|
||||
function Invoke-ScenarioB4 {
|
||||
Write-Host "`n--- B4 无 SATA 盘 (DSK 回退 Limine module) ---"
|
||||
$s = Start-QemuSession -Name 'bootreg-b4' -NoSata
|
||||
$s = Start-QemuSession -Name 'bootreg-b4' -NoSata -MonitorPort 45501
|
||||
try {
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @('[DSK] firstInit.txt not found', '[DSK] desktop load failed', '[PANIC]') -TimeoutSeconds 120
|
||||
Start-Sleep -Seconds 3
|
||||
@@ -190,7 +190,7 @@ 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
|
||||
$s = Start-QemuSession -Name 'bootreg-b5' -NoNet -MonitorPort 45501
|
||||
try {
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
|
||||
$log = Read-QemuLogText $s.LogPath
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
deaicup-e2e - Deaicup GUI PE end-to-end test (docs/RE/test-cases/pe-compat.md)
|
||||
|
||||
.DESCRIPTION
|
||||
AUTOEXEC runs `pe DEAICUP.EXE` (Rust/egui original Win32 API PE64 GUI app),
|
||||
then the script drives the app through the QEMU monitor and asserts on the
|
||||
serial log plus screendump pixel analysis:
|
||||
|
||||
1. boot chain: 9-anchor ordered chain
|
||||
(cmdline -> native run -> mainCRTStartup -> RegisterClassExW ->
|
||||
CreateWindowExW -> CreateDIBSection -> Context::default ->
|
||||
BitBlt first frame -> bitblt ok)
|
||||
2. screendump pixel analysis of the rendered GUI frame:
|
||||
not-all-black, distinct color threshold, GUI window rectangle
|
||||
detection (window body color / count / bounding box / fill ratio)
|
||||
3. mouse: AUX init + mouse_move -> mpkt (WM_MOUSEMOVE),
|
||||
mouse_button -> LDOWN/LUP edges (WM_LBUTTONDOWN/WM_LBUTTONUP),
|
||||
cursor-region pixel diff between two screendumps
|
||||
4. keyboard: sendkey -> KEYDOWN/CHAR/KEYUP (WM_KEYDOWN/WM_CHAR/WM_KEYUP)
|
||||
5. Esc -> WM_CLOSE -> ExitProcess(0) -> [cmd] PE run end clean exit chain
|
||||
(also regresses BUG-006 long-run return chain and BUG-005 noise)
|
||||
|
||||
Forbidden patterns over the whole session:
|
||||
[PANIC] / [IDT] exception / Unimplemented handler
|
||||
|
||||
WHPX note: native PE32+ execution enables SSE via CR4 and WHPX may kill
|
||||
QEMU with "Unexpected VP exit code 4" (platform limitation, not a guest
|
||||
crash). If the first-frame anchor is missed under the requested
|
||||
accelerator the session is rerun once with -Accel tcg
|
||||
(pass -Accel tcg to skip WHPX directly).
|
||||
|
||||
NOTE: keep this file ASCII-only. PS 5.1 reads BOM-less UTF-8 as GBK;
|
||||
CJK comment bytes can swallow newlines and merge code into comments.
|
||||
|
||||
Exit code: 0 = all checks PASS, 1 = any FAIL.
|
||||
#>
|
||||
param(
|
||||
[switch]$Build,
|
||||
[string]$Accel = 'whpx',
|
||||
[int]$MonitorPort = 45454
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
|
||||
|
||||
Initialize-QemuTest -Build:$Build
|
||||
$result = New-TestCaseResult 'deaicup-e2e'
|
||||
|
||||
# ================================================================
|
||||
# PPM helpers. WARNING: [byte] -shl N truncates back to byte in
|
||||
# PS 5.1, always cast to [int] before shifting (the scratch
|
||||
# analyze script lost R/G channels that way).
|
||||
# ================================================================
|
||||
function Read-PpmFile {
|
||||
param([Parameter(Mandatory)][string]$Path)
|
||||
$b = [System.IO.File]::ReadAllBytes($Path)
|
||||
$p = 0
|
||||
$tok = @()
|
||||
while ($tok.Count -lt 4) {
|
||||
while ($p -lt $b.Length) {
|
||||
$c = $b[$p]
|
||||
if ($c -eq 0x23) { while ($p -lt $b.Length -and $b[$p] -ne 0x0A) { $p++ } }
|
||||
elseif ($c -le 0x20) { $p++ }
|
||||
else { break }
|
||||
}
|
||||
$s = $p
|
||||
while ($p -lt $b.Length -and $b[$p] -gt 0x20) { $p++ }
|
||||
$tok += [Text.Encoding]::ASCII.GetString($b, $s, $p - $s)
|
||||
}
|
||||
$p++
|
||||
if ($tok[0] -ne 'P6') { throw "not a P6 ppm: $($tok[0])" }
|
||||
return @{ data = $b; off = $p; w = [int]$tok[1]; h = [int]$tok[2] }
|
||||
}
|
||||
|
||||
# Pass 1: histogram + non-black count. Pass 2: window rectangle.
|
||||
# Window body color = most common color with mid-dark luminance
|
||||
# (excludes black console background and bright text/cursor/highlight).
|
||||
function Get-PpmStats {
|
||||
param([Parameter(Mandatory)]$F)
|
||||
$npix = $F.w * $F.h
|
||||
$colors = @{}
|
||||
$nonBlack = 0
|
||||
for ($i = 0; $i -lt $npix; $i++) {
|
||||
$o = $F.off + $i * 3
|
||||
$r = [int]$F.data[$o]; $g = [int]$F.data[$o + 1]; $b = [int]$F.data[$o + 2]
|
||||
$key = ($r -shl 16) -bor ($g -shl 8) -bor $b
|
||||
if ($colors.ContainsKey($key)) { $colors[$key]++ } else { $colors[$key] = 1 }
|
||||
if ($r -ge 8 -or $g -ge 8 -or $b -ge 8) { $nonBlack++ }
|
||||
}
|
||||
$winKey = -1; $winCount = 0
|
||||
foreach ($e in ($colors.GetEnumerator() | Sort-Object Value -Descending)) {
|
||||
$k = [int]$e.Key
|
||||
$r = ($k -shr 16) -band 0xFF; $g = ($k -shr 8) -band 0xFF; $b = $k -band 0xFF
|
||||
$lum = 0.299 * $r + 0.587 * $g + 0.114 * $b
|
||||
if ($lum -ge 15 -and $lum -le 90) { $winKey = $k; $winCount = [int]$e.Value; break }
|
||||
}
|
||||
# Window rectangle: the GUI window body is nearly solid window-color, while
|
||||
# anti-aliased console text rows only contain a sparse sprinkle of it.
|
||||
# So: per-row window-color count -> longest contiguous run of "solid" rows
|
||||
# (>= 50% of the max row count) -> x extent + fill ratio inside that run.
|
||||
$box = $null
|
||||
if ($winKey -ge 0) {
|
||||
$rowCnt = New-Object int[] $F.h
|
||||
$rowFirst = New-Object int[] $F.h
|
||||
$rowLast = New-Object int[] $F.h
|
||||
for ($y = 0; $y -lt $F.h; $y++) {
|
||||
$rowOff = $F.off + $y * $F.w * 3
|
||||
$cnt = 0; $fx = -1; $lx = -1
|
||||
for ($x = 0; $x -lt $F.w; $x++) {
|
||||
$o = $rowOff + $x * 3
|
||||
$key = ([int]$F.data[$o] -shl 16) -bor ([int]$F.data[$o + 1] -shl 8) -bor [int]$F.data[$o + 2]
|
||||
if ($key -eq $winKey) {
|
||||
$cnt++
|
||||
if ($fx -lt 0) { $fx = $x }
|
||||
$lx = $x
|
||||
}
|
||||
}
|
||||
$rowCnt[$y] = $cnt; $rowFirst[$y] = $fx; $rowLast[$y] = $lx
|
||||
}
|
||||
$maxRow = 0
|
||||
for ($y = 0; $y -lt $F.h; $y++) { if ($rowCnt[$y] -gt $maxRow) { $maxRow = $rowCnt[$y] } }
|
||||
$rowThresh = [int]($maxRow * 0.5)
|
||||
$bestY0 = -1; $bestY1 = -1; $curY0 = -1
|
||||
for ($y = 0; $y -le $F.h; $y++) {
|
||||
$inRun = ($y -lt $F.h) -and ($rowCnt[$y] -ge $rowThresh)
|
||||
if ($inRun -and $curY0 -lt 0) { $curY0 = $y }
|
||||
if (-not $inRun -and $curY0 -ge 0) {
|
||||
if (($y - 1 - $curY0) -gt ($bestY1 - $bestY0)) { $bestY0 = $curY0; $bestY1 = $y - 1 }
|
||||
$curY0 = -1
|
||||
}
|
||||
}
|
||||
if ($bestY0 -ge 0) {
|
||||
$minX = $F.w; $maxX = -1; $inside = 0
|
||||
for ($y = $bestY0; $y -le $bestY1; $y++) {
|
||||
if ($rowCnt[$y] -gt 0) {
|
||||
if ($rowFirst[$y] -lt $minX) { $minX = $rowFirst[$y] }
|
||||
if ($rowLast[$y] -gt $maxX) { $maxX = $rowLast[$y] }
|
||||
$inside += $rowCnt[$y]
|
||||
}
|
||||
}
|
||||
$bw = $maxX - $minX + 1; $bh = $bestY1 - $bestY0 + 1
|
||||
$box = @{ X0 = $minX; Y0 = $bestY0; X1 = $maxX; Y1 = $bestY1; W = $bw; H = $bh
|
||||
Fill = [math]::Round($inside / [double]($bw * $bh), 4) }
|
||||
}
|
||||
}
|
||||
return @{ Colors = $colors.Count; NonBlack = $nonBlack; Pixels = $npix
|
||||
WinKey = $winKey; WinCount = $winCount; WinBox = $box }
|
||||
}
|
||||
|
||||
# Sampled diff (every 2nd pixel) between two frames over a region.
|
||||
# A pixel counts as changed when |dR|+|dG|+|dB| > 30.
|
||||
function Compare-PpmRegion {
|
||||
param([Parameter(Mandatory)]$A, [Parameter(Mandatory)]$B,
|
||||
[int]$X0, [int]$Y0, [int]$X1, [int]$Y1)
|
||||
$changed = 0
|
||||
for ($y = $Y0; $y -le $Y1; $y += 2) {
|
||||
$rowA = $A.off + $y * $A.w * 3
|
||||
$rowB = $B.off + $y * $B.w * 3
|
||||
for ($x = $X0; $x -le $X1; $x += 2) {
|
||||
$oa = $rowA + $x * 3
|
||||
$ob = $rowB + $x * 3
|
||||
$d = [Math]::Abs([int]$A.data[$oa] - [int]$B.data[$ob]) +
|
||||
[Math]::Abs([int]$A.data[$oa + 1] - [int]$B.data[$ob + 1]) +
|
||||
[Math]::Abs([int]$A.data[$oa + 2] - [int]$B.data[$ob + 2])
|
||||
if ($d -gt 30) { $changed++ }
|
||||
}
|
||||
}
|
||||
return $changed
|
||||
}
|
||||
|
||||
function Convert-PpmToPng {
|
||||
param([Parameter(Mandatory)][string]$PpmPath, [Parameter(Mandatory)][string]$PngPath)
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bytes = [System.IO.File]::ReadAllBytes($PpmPath)
|
||||
# ASCII decode: 1 byte == 1 char, so string index == byte offset.
|
||||
# QEMU screendump header is "P6\n<w> <h>\n255\n" + single whitespace byte.
|
||||
$hdr = [System.Text.Encoding]::ASCII.GetString($bytes, 0, [Math]::Min(64, $bytes.Length))
|
||||
$m = [regex]::Match($hdr, '^P6\s+(\d+)\s+(\d+)\s+(\d+)\s')
|
||||
if (-not $m.Success) { throw 'bad ppm header' }
|
||||
$w = [int]$m.Groups[1].Value; $h = [int]$m.Groups[2].Value; $max = [int]$m.Groups[3].Value
|
||||
if ($max -ne 255) { throw "unsupported maxval $max" }
|
||||
$pos = $m.Length
|
||||
$bmp = New-Object System.Drawing.Bitmap $w, $h, ([System.Drawing.Imaging.PixelFormat]::Format24bppRgb)
|
||||
$rect = New-Object System.Drawing.Rectangle 0, 0, $w, $h
|
||||
$data = $bmp.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::WriteOnly, $bmp.PixelFormat)
|
||||
try {
|
||||
$rowBytes = $w * 3
|
||||
$rgb = New-Object byte[] ($rowBytes)
|
||||
for ($y = 0; $y -lt $h; $y++) {
|
||||
$srcOff = $pos + $y * $rowBytes
|
||||
for ($x = 0; $x -lt $w; $x++) {
|
||||
$rgb[$x*3+0] = $bytes[$srcOff + $x*3 + 2] # B
|
||||
$rgb[$x*3+1] = $bytes[$srcOff + $x*3 + 1] # G
|
||||
$rgb[$x*3+2] = $bytes[$srcOff + $x*3 + 0] # R
|
||||
}
|
||||
[System.Runtime.InteropServices.Marshal]::Copy($rgb, 0, [IntPtr]($data.Scan0.ToInt64() + $y * $data.Stride), $rowBytes)
|
||||
}
|
||||
} finally { $bmp.UnlockBits($data) }
|
||||
$bmp.Save($PngPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
$bmp.Dispose()
|
||||
}
|
||||
|
||||
$script:UsedAccel = $Accel
|
||||
$s = $null
|
||||
try {
|
||||
Set-DeshabFirstInit -First 1 -DevMode 0
|
||||
Set-DeshabAutoexec -Lines @('pe DEAICUP.EXE')
|
||||
|
||||
$s = Start-QemuSession -Name "deaicup-$Accel" -Accel $Accel -MonitorPort $MonitorPort
|
||||
$result.LogPath = $s.LogPath
|
||||
|
||||
# ---- 1. boot chain + first frame (WHPX miss -> TCG rerun once) ----
|
||||
$waitFirst = 150
|
||||
if ($Accel -eq 'tcg') { $waitFirst = 360 } # TCG full emulation is much slower
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @('[shim] BitBlt first frame on screen', '[PANIC]', '[IDT] exception') -TimeoutSeconds $waitFirst
|
||||
|
||||
if ($hit -ne '[shim] BitBlt first frame on screen' -and $Accel -eq 'whpx') {
|
||||
$why = 'first-frame anchor missed under WHPX'
|
||||
$early = Read-QemuLogText $s.LogPath
|
||||
if ($early.Contains('WHPX: Unexpected VP exit')) { $why = 'WHPX terminated QEMU at native PE execution (Unexpected VP exit, platform limitation)' }
|
||||
Add-TestNote $result "$why; rerunning once with TCG"
|
||||
Stop-QemuSession $s
|
||||
$s = $null
|
||||
$script:UsedAccel = 'tcg'
|
||||
$s = Start-QemuSession -Name 'deaicup-tcg' -Accel 'tcg' -MonitorPort $MonitorPort
|
||||
$result.LogPath = $s.LogPath
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @('[shim] BitBlt first frame on screen', '[PANIC]', '[IDT] exception') -TimeoutSeconds 360
|
||||
}
|
||||
|
||||
$log = Read-QemuLogText $s.LogPath
|
||||
if ($hit -ne '[shim] BitBlt first frame on screen') {
|
||||
Add-TestCheck $result 'first frame: BitBlt first frame on screen' 'FAIL' "hit=$hit"
|
||||
throw 'first-frame anchor missing'
|
||||
}
|
||||
Add-TestCheck $result 'first frame: BitBlt first frame on screen' 'PASS'
|
||||
Assert-QemuLog $result $log -Ordered @(
|
||||
'[cmd] cmdline: DEAICUP.EXE',
|
||||
'[PE] running PE32+ natively',
|
||||
'[deaicup] mainCRTStartup',
|
||||
'[shim] RegisterClassExW ok',
|
||||
'[shim] CreateWindowExW ok',
|
||||
'[shim] CreateDIBSection ok',
|
||||
'[dc] Context::default ok',
|
||||
'[shim] BitBlt first frame on screen',
|
||||
'[dc] bitblt ok'
|
||||
) -MustNotContain @('[PANIC]', '[IDT] exception', 'Unimplemented handler')
|
||||
|
||||
# ---- 2. screendump A: pixel analysis of the rendered GUI frame ----
|
||||
# QEMU human monitor parses backslashes as escapes inside quoted strings
|
||||
# (\C etc. illegal -> screendump silently fails); OutPath must use '/'.
|
||||
Start-Sleep -Seconds 2
|
||||
$ppmA = ($script:TestWorkDir -replace '\\', '/') + '/deaicup-frame-a.ppm'
|
||||
$ppmB = ($script:TestWorkDir -replace '\\', '/') + '/deaicup-frame-b.ppm'
|
||||
$png = Join-Path $script:TestWorkDir 'deaicup-frame.png'
|
||||
$fa = $null
|
||||
$statsA = $null
|
||||
if (Get-QemuScreenshot -Session $s -OutPath $ppmA) {
|
||||
try {
|
||||
Convert-PpmToPng -PpmPath $ppmA -PngPath $png
|
||||
Add-TestCheck $result "screenshot: evidence frame -> $png" 'PASS'
|
||||
} catch {
|
||||
Add-TestCheck $result 'screenshot: PPM->PNG convert' 'FAIL' $_.Exception.Message
|
||||
}
|
||||
$fa = Read-PpmFile $ppmA
|
||||
$statsA = Get-PpmStats $fa
|
||||
$boxDesc = 'none'
|
||||
if ($statsA.WinBox) {
|
||||
$boxDesc = ("({0},{1})-({2},{3}) {4}x{5} fill={6:P1}" -f $statsA.WinBox.X0, $statsA.WinBox.Y0,
|
||||
$statsA.WinBox.X1, $statsA.WinBox.Y1, $statsA.WinBox.W, $statsA.WinBox.H, $statsA.WinBox.Fill)
|
||||
}
|
||||
Add-TestNote $result ("frame {0}x{1}: distinctColors={2} nonBlack={3}/{4} winColor=#{5:X6} winCount={6} winBox={7}" -f
|
||||
$fa.w, $fa.h, $statsA.Colors, $statsA.NonBlack, $statsA.Pixels, $statsA.WinKey, $statsA.WinCount, $boxDesc)
|
||||
|
||||
if ($statsA.NonBlack -ge 100000) {
|
||||
Add-TestCheck $result "pixels: not all black (nonBlack=$($statsA.NonBlack) >= 100000)" 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'pixels: not all black' 'FAIL' "nonBlack=$($statsA.NonBlack) (< 100000)"
|
||||
}
|
||||
if ($statsA.Colors -ge 32) {
|
||||
Add-TestCheck $result "pixels: distinct colors >= 32 ($($statsA.Colors))" 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'pixels: distinct colors >= 32' 'FAIL' "only $($statsA.Colors)"
|
||||
}
|
||||
$wb = $statsA.WinBox
|
||||
if ($statsA.WinKey -ge 0 -and $statsA.WinCount -ge 300000 -and $wb -and
|
||||
$wb.W -ge 600 -and $wb.W -le 900 -and $wb.H -ge 450 -and $wb.H -le 700 -and $wb.Fill -ge 0.75) {
|
||||
Add-TestCheck $result "pixels: GUI window rect ($($wb.W)x$($wb.H) fill=$($wb.Fill.ToString('P1')))" 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'pixels: GUI window rect' 'FAIL' ("winKey=#{0:X6} count={1} box={2}" -f $statsA.WinKey, $statsA.WinCount, $boxDesc)
|
||||
}
|
||||
} else {
|
||||
Add-TestCheck $result 'screenshot: screendump A' 'FAIL' 'ppm not produced'
|
||||
}
|
||||
|
||||
# ---- 3. mouse: mouse_move -> WM_MOUSEMOVE (mpkt) + cursor pixel diff ----
|
||||
$mouseInitOk = $log.Contains('[shim] mouse init ok (AUX streaming, polled)')
|
||||
Send-QemuMouseMove -Session $s -Dx 120 -Dy 60
|
||||
Start-Sleep -Milliseconds 700
|
||||
Send-QemuMouseMove -Session $s -Dx 40 -Dy 20
|
||||
$hitMv = Wait-QemuLog -Session $s -Patterns @('[shim] mpkt dx=', '[PANIC]', '[IDT] exception') -TimeoutSeconds 60
|
||||
if ($mouseInitOk -and $hitMv -eq '[shim] mpkt dx=') {
|
||||
Add-TestCheck $result 'mouse: AUX init + mouse_move -> WM_MOUSEMOVE (mpkt)' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'mouse: AUX init + mouse_move -> WM_MOUSEMOVE (mpkt)' 'FAIL' "init=$mouseInitOk hit=$hitMv"
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
$cursorDiff = -1
|
||||
if ($fa -and (Get-QemuScreenshot -Session $s -OutPath $ppmB)) {
|
||||
$fb = Read-PpmFile $ppmB
|
||||
$x0 = 4; $y0 = 4; $x1 = $fa.w - 5; $y1 = $fa.h - 5
|
||||
if ($statsA.WinBox) {
|
||||
$x0 = $statsA.WinBox.X0 + 4; $y0 = $statsA.WinBox.Y0 + 4
|
||||
$x1 = $statsA.WinBox.X1 - 4; $y1 = $statsA.WinBox.Y1 - 4
|
||||
}
|
||||
$cursorDiff = Compare-PpmRegion -A $fa -B $fb -X0 $x0 -Y0 $y0 -X1 $x1 -Y1 $y1
|
||||
Add-TestNote $result "cursor-region diff (sampled, region ($x0,$y0)-($x1,$y1)): $cursorDiff px"
|
||||
}
|
||||
if ($cursorDiff -ge 8 -and $cursorDiff -le 60000) {
|
||||
Add-TestCheck $result "mouse: cursor-region pixel diff after move ($cursorDiff px)" 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'mouse: cursor-region pixel diff after move' 'FAIL' "diff=$cursorDiff (expect 8..60000)"
|
||||
}
|
||||
|
||||
# ---- 4. mouse buttons: mouse_button -> WM_LBUTTONDOWN/UP edges ----
|
||||
[void](Invoke-QemuMonitor -Session $s -Command 'mouse_button 1' -SettleMs 500)
|
||||
[void](Invoke-QemuMonitor -Session $s -Command 'mouse_button 0' -SettleMs 500)
|
||||
$hitUp = Wait-QemuLog -Session $s -Patterns @('[shim] LUP', '[PANIC]', '[IDT] exception') -TimeoutSeconds 60
|
||||
$log = Read-QemuLogText $s.LogPath
|
||||
if ($hitUp -eq '[shim] LUP' -and $log.Contains('[shim] LDOWN lp=')) {
|
||||
Add-TestCheck $result 'mouse: mouse_button -> WM_LBUTTONDOWN/UP edges' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'mouse: mouse_button -> WM_LBUTTONDOWN/UP edges' 'FAIL' "hit=$hitUp ldown=$($log.Contains('[shim] LDOWN lp='))"
|
||||
}
|
||||
|
||||
# ---- 5. keyboard: sendkey -> WM_KEYDOWN / WM_CHAR / WM_KEYUP ----
|
||||
Send-QemuKeys -Session $s -Keys @('a')
|
||||
$hitK = Wait-QemuLog -Session $s -Patterns @('[shim] CHAR ch=', '[PANIC]', '[IDT] exception') -TimeoutSeconds 60
|
||||
$log = Read-QemuLogText $s.LogPath
|
||||
if ($hitK -eq '[shim] CHAR ch=' -and $log.Contains('[shim] KEYDOWN vk=') -and $log.Contains('[shim] KEYUP vk=')) {
|
||||
Add-TestCheck $result 'keyboard: sendkey -> WM_KEYDOWN/WM_CHAR/WM_KEYUP' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'keyboard: sendkey -> WM_KEYDOWN/WM_CHAR/WM_KEYUP' 'FAIL' "hit=$hitK"
|
||||
}
|
||||
|
||||
# ---- 6. Esc -> WM_CLOSE -> ExitProcess(0) -> cmd continues ----
|
||||
Send-QemuKeys -Session $s -Keys @('esc')
|
||||
$hitQ = Wait-QemuLog -Session $s -Patterns @('[cmd] PE run end', '[PANIC]', '[IDT] exception') -TimeoutSeconds 90
|
||||
$log = Read-QemuLogText $s.LogPath
|
||||
if ($hitQ -eq '[cmd] PE run end') {
|
||||
Add-TestCheck $result 'exit chain: Esc -> WM_CLOSE -> ExitProcess(0) -> cmd continues' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'exit chain: Esc -> WM_CLOSE -> ExitProcess(0) -> cmd continues' 'FAIL' "hit=$hitQ"
|
||||
}
|
||||
Assert-QemuLog $result $log -Ordered @(
|
||||
'[deaicup] quit, ExitProcess(0)',
|
||||
'[PE] ExitProcess code=',
|
||||
'[cmd] PE run end'
|
||||
) -MustNotContain @('[PANIC]', '[IDT] exception', 'Unimplemented handler')
|
||||
|
||||
Add-TestNote $result "accelerator: $script:UsedAccel"
|
||||
} finally {
|
||||
if ($s) { Stop-QemuSession $s }
|
||||
Restore-DeshabScenario
|
||||
}
|
||||
|
||||
Write-TestCaseResult $result
|
||||
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
|
||||
@@ -1,4 +1,4 @@
|
||||
#requires -Version 5.1
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
driver-matrix — DKM 驱动功能矩阵(docs/RE/test-cases/driver-matrix.md)
|
||||
@@ -27,7 +27,7 @@ Initialize-QemuTest -Build:$Build
|
||||
$result = New-TestCaseResult 'driver-matrix'
|
||||
|
||||
Write-Host "`n--- 全设备单会话驱动矩阵 ---"
|
||||
$s = Start-QemuSession -Name 'driver-matrix'
|
||||
$s = Start-QemuSession -Name 'driver-matrix' -MonitorPort 45502
|
||||
try {
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @('[DSK] boot') -TimeoutSeconds 150
|
||||
Start-Sleep -Seconds 2
|
||||
@@ -66,6 +66,21 @@ try {
|
||||
'[DSM] driver disabled by FUCK' # ath9k: FUCK 禁用路径
|
||||
)
|
||||
|
||||
# ---- NVMe 数据面回归(写路径):签名盘自检 + 写/刷/读回 + provider 序号 ----
|
||||
Assert-QemuLog $result $log -MustContain @(
|
||||
'[nvme] LBA0 signature OK', # LBA0 DESHABNVME0 签名识别(测试盘)
|
||||
'[nvme] write+flush+readback verify OK', # 写→FLUSH→读回逐字节校验(PRP2 + PRP list 两趟)
|
||||
'[nvme] LBA0 sig intact after write tests' # 写测试后 LBA0 签名未损坏
|
||||
)
|
||||
# ahci0=0 → nvme0=1 注册顺序([BLOCK] device registered → 名称 → index 三联行)
|
||||
Assert-QemuLog $result $log -Ordered @(
|
||||
'[INFO] ahci0',
|
||||
'[BLOCK] index=0x0000000000000000',
|
||||
'[INFO] nvme0',
|
||||
'[BLOCK] index=0x0000000000000001'
|
||||
)
|
||||
Assert-QemuLog $result $log -CountPattern '[nvme] write+flush+readback verify OK' -MinCount 2
|
||||
|
||||
# ---- DSM 装载统计:16 个 manifest 驱动中 15 个 active(ath9k 禁用) ----
|
||||
Assert-QemuLog $result $log -CountPattern '[DKM] external driver active' -MinCount 15
|
||||
|
||||
|
||||
+163
-33
@@ -1,4 +1,4 @@
|
||||
#requires -Version 5.1
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Deshab QEMU 自动化测试公共库(tests/qemu/lib/QemuTest.ps1)
|
||||
@@ -20,6 +20,14 @@ $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'
|
||||
# 并发隔离布局(另一代理并发跑 QEMU/build,共享路径一律只读,写入全进私有区):
|
||||
# .build_tmp\tests\img\ 私有镜像副本(deshab/nvme/usb,QEMU 只挂副本)
|
||||
# .build_tmp\tests\SYSTEM\ SYSTEM 镜像树(robocopy /MIR;场景文件只翻这里)
|
||||
# .build_tmp\tests\CODE\...\deaicup.exe mkfat32 引用的单文件副本
|
||||
# .build_tmp\tests\fat32gen\ mkfat32.ps1 副本 + 私有 SATA 镜像输出
|
||||
$script:SuiteImgDir = Join-Path $script:TestWorkDir 'img'
|
||||
$script:MkFat32Dir = Join-Path $script:TestWorkDir 'fat32gen'
|
||||
$script:MirrorSystem = Join-Path $script:TestWorkDir 'SYSTEM'
|
||||
$script:Config = $null
|
||||
|
||||
# 场景备份(模块级,保证只备份一次、恢复幂等)
|
||||
@@ -56,60 +64,147 @@ function Initialize-QemuTest {
|
||||
if (-not $ovmf) { throw 'OVMF UEFI firmware not found under QEMU install directory' }
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $script:TestWorkDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $script:SuiteImgDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $script:MkFat32Dir | 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'
|
||||
$imgSrc = Join-Path $script:IsoDir 'deshab.img'
|
||||
$nvmeSrc = Join-Path $script:BuildTmp 'nvme_test.img'
|
||||
$usbSrc = Join-Path $script:BuildTmp 'usb_test.img'
|
||||
|
||||
if ($Build -or -not (Test-Path $img)) {
|
||||
if ($Build -or -not (Test-Path $imgSrc)) {
|
||||
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" }
|
||||
if (-not (Test-Path $imgSrc)) { throw "image missing: $imgSrc (run with -Build)" }
|
||||
|
||||
$nvmeOk = $null; if (Test-Path $nvme) { $nvmeOk = $nvme }
|
||||
$usbOk = $null; if (Test-Path $usb) { $usbOk = $usb }
|
||||
# --- 并发隔离:SYSTEM 镜像树 + 私有 SATA 生成器 + 私有镜像副本 ---
|
||||
Sync-SuiteTree
|
||||
Invoke-MkFat32
|
||||
$sata = Join-Path $script:MkFat32Dir 'sata_fat32_dsk.img'
|
||||
if (-not (Test-Path $sata)) { throw "private sata image not generated: $sata" }
|
||||
|
||||
$img = Copy-SuiteImage -Source $imgSrc -FileName 'deshab.img' -Mandatory
|
||||
$nvme = Copy-SuiteImage -Source $nvmeSrc -FileName 'nvme_test.img'
|
||||
$usb = Copy-SuiteImage -Source $usbSrc -FileName 'usb_test.img'
|
||||
|
||||
$script:Config = [pscustomobject]@{
|
||||
QemuExe = $qemu
|
||||
Ovmf = $ovmf
|
||||
Img = $img
|
||||
SataImg = $sata
|
||||
NvmeImg = $nvmeOk
|
||||
UsbImg = $usbOk
|
||||
NvmeImg = $nvme
|
||||
UsbImg = $usb
|
||||
}
|
||||
Write-Host "[init] QEMU: $qemu"
|
||||
Write-Host "[init] OVMF: $ovmf"
|
||||
Write-Host "[init] IMG: $img"
|
||||
Write-Host "[init] IMG: $img (private copy)"
|
||||
return $script:Config
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# 并发隔离:共享文件只读复制到套件私有区
|
||||
# ================================================================
|
||||
|
||||
# share-tolerant 复制:源可能被并发代理的 QEMU/打包器短暂占用,重试至多 60s。
|
||||
# 目标不变(mtime+size 相同)时跳过,避免每用例重复拷 768MB。
|
||||
function Copy-SuiteImage {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Source,
|
||||
[Parameter(Mandatory)][string]$FileName,
|
||||
[switch]$Mandatory
|
||||
)
|
||||
if (-not (Test-Path $Source)) {
|
||||
if ($Mandatory) { throw "image missing: $Source (run with -Build)" }
|
||||
return $null
|
||||
}
|
||||
$dest = Join-Path $script:SuiteImgDir $FileName
|
||||
$srcItem = Get-Item $Source
|
||||
if ((Test-Path $dest)) {
|
||||
$dstItem = Get-Item $dest
|
||||
if ($dstItem.Length -eq $srcItem.Length -and $dstItem.LastWriteTimeUtc -eq $srcItem.LastWriteTimeUtc) {
|
||||
return $dest
|
||||
}
|
||||
}
|
||||
$deadline = (Get-Date).AddSeconds(60)
|
||||
while ($true) {
|
||||
$in = $null; $out = $null
|
||||
try {
|
||||
$in = [System.IO.File]::Open($Source, 'Open', 'Read', [System.IO.FileShare]::ReadWrite -bor [System.IO.FileShare]::Delete)
|
||||
$out = [System.IO.File]::Open($dest, 'Create', 'Write', [System.IO.FileShare]::None)
|
||||
$in.CopyTo($out, 1048576)
|
||||
$out.Close(); $in.Close()
|
||||
[System.IO.File]::SetLastWriteTimeUtc($dest, $srcItem.LastWriteTimeUtc)
|
||||
Write-Host "[init] private copy: $FileName ($([math]::Round($srcItem.Length/1MB))MB)"
|
||||
return $dest
|
||||
} catch [System.IO.IOException] {
|
||||
if ($out) { try { $out.Close() } catch {} }
|
||||
if ($in) { try { $in.Close() } catch {} }
|
||||
if ((Get-Date) -gt $deadline) {
|
||||
throw "cannot copy $Source (locked by concurrent process for >60s)"
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
} catch {
|
||||
if ($out) { try { $out.Close() } catch {} }
|
||||
if ($in) { try { $in.Close() } catch {} }
|
||||
throw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# SYSTEM 镜像树 + deaicup.exe 单文件 + mkfat32.ps1 生成器副本。
|
||||
# 场景翻转(firstInit/AUTOEXEC)只作用于镜像树,共享工作树绝不写入。
|
||||
function Sync-SuiteTree {
|
||||
& robocopy $script:SystemDir $script:MirrorSystem /MIR /R:2 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null
|
||||
if ($LASTEXITCODE -ge 8) { throw "robocopy SYSTEM mirror failed (rc=$LASTEXITCODE)" }
|
||||
|
||||
$deaSrc = Join-Path $script:RepoRoot 'CODE\Deaicup Software\pe\target\x86_64-pc-windows-msvc\release\deaicup.exe'
|
||||
if (Test-Path $deaSrc) {
|
||||
$deaDst = Join-Path $script:TestWorkDir 'CODE\Deaicup Software\pe\target\x86_64-pc-windows-msvc\release\deaicup.exe'
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $deaDst) | Out-Null
|
||||
$srcItem = Get-Item $deaSrc
|
||||
$need = $true
|
||||
if (Test-Path $deaDst) {
|
||||
$dstItem = Get-Item $deaDst
|
||||
if ($dstItem.Length -eq $srcItem.Length -and $dstItem.LastWriteTimeUtc -eq $srcItem.LastWriteTimeUtc) { $need = $false }
|
||||
}
|
||||
if ($need) { Copy-Item $deaSrc $deaDst -Force }
|
||||
}
|
||||
|
||||
$mkSrc = Join-Path $script:BuildTmp 'mkfat32.ps1'
|
||||
if (-not (Test-Path $mkSrc)) { throw "mkfat32.ps1 not found: $mkSrc (run build.ps1 once)" }
|
||||
Copy-Item $mkSrc (Join-Path $script:MkFat32Dir 'mkfat32.ps1') -Force
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# 进程管理
|
||||
# ================================================================
|
||||
|
||||
# 清理持有本项目镜像的残留 QEMU(deshab.img 被锁会导致 build/启动失败)。
|
||||
# 只杀命令行中引用本仓库路径的 qemu 进程,避免误伤他人虚拟机。
|
||||
# 清理本套件历史会话残留的 QEMU(Start-QemuSession 以 -name deshabtest-... 标记)。
|
||||
# 只按 deshabtest- 标记精确匹配,绝不动其他代理/人工启动的 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)"
|
||||
if ($p.CommandLine -and $p.CommandLine.Contains('deshabtest-')) {
|
||||
Write-Host "[qemu] killing residual suite qemu pid=$($p.ProcessId)"
|
||||
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# monitor 端口:45501-45508 按用例分配,deaicup-e2e 固定 45454
|
||||
# (避开人工调试 4444 与其他套件 456xx/457xx)。
|
||||
# -Preferred 传入用例固定端口;被占用(如 TIME_WAIT)时扫描段内其余端口,
|
||||
# 最后兜底 45509-45599。段内端口被他人占用时只换端口,绝不杀占用进程。
|
||||
function Get-FreeMonitorPort {
|
||||
for ($port = 4444; $port -lt 4544; $port++) {
|
||||
param([int]$Preferred = 0)
|
||||
$candidates = New-Object System.Collections.ArrayList
|
||||
if (($Preferred -ge 45501 -and $Preferred -le 45508) -or $Preferred -eq 45454) { [void]$candidates.Add($Preferred) }
|
||||
foreach ($p in 45501..45508) { if ($p -ne $Preferred) { [void]$candidates.Add($p) } }
|
||||
foreach ($p in 45509..45599) { [void]$candidates.Add($p) }
|
||||
foreach ($port in $candidates) {
|
||||
$listener = $null
|
||||
try {
|
||||
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $port)
|
||||
@@ -137,6 +232,8 @@ function Start-QemuSession {
|
||||
[switch]$NoNet,
|
||||
[string]$NetModel = 'e1000',
|
||||
[switch]$NoMonitor,
|
||||
[int]$MonitorPort = 0,
|
||||
[switch]$NoSnapshot,
|
||||
[string]$Cpu = '',
|
||||
[string[]]$ExtraArgs = @()
|
||||
)
|
||||
@@ -167,9 +264,12 @@ function Start-QemuSession {
|
||||
'-drive', ('format=raw,file="{0}",if=virtio' -f $cfg.Img)
|
||||
)
|
||||
|
||||
# -snapshot 默认开:镜像只读 + 写入落临时 overlay,避免镜像写锁与并发代理冲突。
|
||||
if (-not $NoSnapshot) { $qargs += '-snapshot' }
|
||||
|
||||
$port = 0
|
||||
if (-not $NoMonitor) {
|
||||
$port = Get-FreeMonitorPort
|
||||
$port = Get-FreeMonitorPort -Preferred $MonitorPort
|
||||
$qargs += @('-monitor', "tcp:127.0.0.1:$port,server,nowait")
|
||||
}
|
||||
if (-not $NoSata) {
|
||||
@@ -192,13 +292,21 @@ function Start-QemuSession {
|
||||
} elseif ($NetModel -eq 'virtio-net-pci') {
|
||||
$qargs += @('-device', 'virtio-net-pci,netdev=net0')
|
||||
}
|
||||
} else {
|
||||
# 已知坑3:不显式禁网时 QEMU 会自动补一块默认 e1000 网卡(q35 默认型号),
|
||||
# -NoNet 场景必须用 -nic none 把默认网络整个关掉。
|
||||
$qargs += @('-nic', 'none')
|
||||
}
|
||||
foreach ($x in $ExtraArgs) { $qargs += $x }
|
||||
$qargs += @('-boot', 'menu=on')
|
||||
|
||||
# 已知坑:Start-Process 下 -serial file: 不生效;用 cmd 批处理重定向 stdio。
|
||||
# 已知坑1:Start-Process 下 -serial file: 不生效;用 cmd 批处理重定向 stdio。
|
||||
# 已知坑2:-snapshot 会在 %TEMP% 创建临时 overlay,Windows 用户名含非 ASCII
|
||||
# (如中文)时 QEMU 创建失败;把 TEMP/TMP 重定向到仓库内 ASCII 目录。
|
||||
$qemuTmp = Join-Path $script:BuildTmp 'qemu-tmp'
|
||||
New-Item -ItemType Directory -Force -Path $qemuTmp | Out-Null
|
||||
$cmdline = '"' + $cfg.QemuExe + '" ' + ($qargs -join ' ')
|
||||
$batContent = "@echo off`r`n$cmdline > `"$log`" 2>&1`r`n"
|
||||
$batContent = "@echo off`r`nset `"TEMP=$qemuTmp`"`r`nset `"TMP=$qemuTmp`"`r`n$cmdline > `"$log`" 2>&1`r`n"
|
||||
[System.IO.File]::WriteAllText($bat, $batContent)
|
||||
|
||||
Stop-ResidualQemu
|
||||
@@ -213,6 +321,24 @@ function Start-QemuSession {
|
||||
Accel = $Accel
|
||||
StartTime = Get-Date
|
||||
}
|
||||
|
||||
# 早夭自愈:WHPX 分区被强杀后偶发重建异常,guest 启动即三重故障
|
||||
# (实测日志仅一行 "Ignoring request for interrupt vector 0")。
|
||||
# 启动后观察 3s:进程已死且日志近乎为空 → 同一 bat 重开一次
|
||||
# (tag/monitor 端口不变,旧进程已死无冲突;bat 重定向会重建日志)。
|
||||
Start-Sleep -Seconds 3
|
||||
if (Test-QemuSessionExited $session) {
|
||||
$logSize = 0
|
||||
if (Test-Path $log) { $logSize = (Get-Item $log).Length }
|
||||
if ($logSize -lt 4096) {
|
||||
Write-Host "[qemu] session '$Name' died at launch (log ${logSize}B, WHPX startup flake), relaunching once"
|
||||
Start-Sleep -Milliseconds 800
|
||||
Start-Process -FilePath $bat -WindowStyle Hidden
|
||||
} else {
|
||||
Write-Host "[qemu] session '$Name' exited early (log ${logSize}B)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[qemu] session '$Name' started (monitor=$port, log=$log)"
|
||||
return $session
|
||||
}
|
||||
@@ -426,20 +552,24 @@ function Get-QemuScreenshot {
|
||||
# 场景准备(firstInit.txt / AUTOEXEC.BAT + SATA 镜像重建)
|
||||
# ================================================================
|
||||
|
||||
# 私有 SATA 镜像生成:运行 fat32gen\ 下的 mkfat32.ps1 副本。
|
||||
# 副本的 PSScriptRoot=fat32gen → 输出 fat32gen\sata_fat32_dsk.img(私有),
|
||||
# 输入 tests\SYSTEM 镜像树 + tests\CODE\...\deaicup.exe,不读写任何共享路径。
|
||||
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)" }
|
||||
$mk = Join-Path $script:MkFat32Dir 'mkfat32.ps1'
|
||||
if (-not (Test-Path $mk)) { throw "mkfat32.ps1 copy not found: $mk (Initialize-QemuTest runs Sync-SuiteTree)" }
|
||||
& 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 镜像;原文件自动备份
|
||||
# 写镜像树 firstInit.txt(格式 "<first>`n<dev_mode>",LF)并重建私有 SATA 镜像;原内容自动备份。
|
||||
# 只写 .build_tmp\tests\SYSTEM 镜像树,共享 SYSTEM 工作树保持不变(并发代理可能正在翻转它)。
|
||||
function Set-DeshabFirstInit {
|
||||
param(
|
||||
[Parameter(Mandatory)][int]$First,
|
||||
[Parameter(Mandatory)][int]$DevMode
|
||||
)
|
||||
$path = Join-Path $script:SystemDir 'system\user\use\firstInit.txt'
|
||||
$path = Join-Path $script:MirrorSystem 'system\user\use\firstInit.txt'
|
||||
if ($null -eq $script:FirstInitBackup) {
|
||||
$script:FirstInitBackup = [System.IO.File]::ReadAllBytes($path)
|
||||
}
|
||||
@@ -448,7 +578,7 @@ function Set-DeshabFirstInit {
|
||||
}
|
||||
|
||||
function Restore-DeshabFirstInit {
|
||||
$path = Join-Path $script:SystemDir 'system\user\use\firstInit.txt'
|
||||
$path = Join-Path $script:MirrorSystem 'system\user\use\firstInit.txt'
|
||||
if ($null -ne $script:FirstInitBackup) {
|
||||
[System.IO.File]::WriteAllBytes($path, $script:FirstInitBackup)
|
||||
$script:FirstInitBackup = $null
|
||||
@@ -456,10 +586,10 @@ function Restore-DeshabFirstInit {
|
||||
}
|
||||
}
|
||||
|
||||
# 替换 AUTOEXEC.BAT 内容(cmd.elf 启动时逐行自动执行)并重建 SATA 镜像;原文件自动备份
|
||||
# 替换镜像树 AUTOEXEC.BAT 内容(cmd.elf 启动时逐行自动执行)并重建私有 SATA 镜像;原内容自动备份
|
||||
function Set-DeshabAutoexec {
|
||||
param([Parameter(Mandatory)][string[]]$Lines)
|
||||
$path = Join-Path $script:SystemDir 'bin\AUTOEXEC.BAT'
|
||||
$path = Join-Path $script:MirrorSystem 'bin\AUTOEXEC.BAT'
|
||||
if ($null -eq $script:AutoexecBackup) {
|
||||
$script:AutoexecBackup = [System.IO.File]::ReadAllBytes($path)
|
||||
}
|
||||
@@ -468,7 +598,7 @@ function Set-DeshabAutoexec {
|
||||
}
|
||||
|
||||
function Restore-DeshabAutoexec {
|
||||
$path = Join-Path $script:SystemDir 'bin\AUTOEXEC.BAT'
|
||||
$path = Join-Path $script:MirrorSystem 'bin\AUTOEXEC.BAT'
|
||||
if ($null -ne $script:AutoexecBackup) {
|
||||
[System.IO.File]::WriteAllBytes($path, $script:AutoexecBackup)
|
||||
$script:AutoexecBackup = $null
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#requires -Version 5.1
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
linux-compat — UTSM+Linux 双内核兼容层端到端(docs/RE/test-cases/linux-compat.md)
|
||||
@@ -35,7 +35,7 @@ $result = New-TestCaseResult 'linux-compat'
|
||||
Set-DeshabFirstInit -First 1 -DevMode 1
|
||||
Set-DeshabAutoexec -Lines @('ver', 'exit')
|
||||
|
||||
$s = Start-QemuSession -Name 'linux-compat' -Cpu $Cpu
|
||||
$s = Start-QemuSession -Name 'linux-compat' -Cpu $Cpu -MonitorPort 45503
|
||||
try {
|
||||
# ---- 分流点:VMM 自检通过 还是 VMM 不可用 ----
|
||||
$fork = Wait-QemuLog -Session $s -Patterns @(
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
network-e2e — 网络端到端测试(docs/RE/test-cases/network-e2e.md)
|
||||
|
||||
.DESCRIPTION
|
||||
场景 N-A(netman 全链路,首启调度):
|
||||
firstInit="0\n0" 首启,挂 e1000(net0) + virtio-net(net1) 双网卡。
|
||||
DSK 首启链 mouseInit → netman → FirstInit,netman 读取 conf.conf(mode=dhcp),
|
||||
在 index=0 的 e1000 上执行 DHCP DISCOVER/OFFER/REQUEST/ACK → ARP 网关 → DNS example.com。
|
||||
断言锚点(netman log_hex 为 16 位定宽小写十六进制,同行输出):
|
||||
[virtio_net] selftest PASS: RX path ok (stage3 驱动自测,真实 slirp 后端 DHCP OFFER)
|
||||
[netman] netdev_count=0x0000000000000002 (e1000 + virtio_net 双设备)
|
||||
[netman] DHCP OFFER ip= / DHCP ACK, leased ip= / DHCP final rc=0x0...0
|
||||
[netman] ARP: gateway resolved / ARP final rc=0x0...0
|
||||
[netman] DNS example.com= / DNS final rc=0x0...0
|
||||
[netman] network final rc=0x0000000000000000
|
||||
netman 之后停在 FirstInit 向导即杀会话(向导自动化由 boot-regression B1 覆盖)。
|
||||
|
||||
场景 N-B(curl.elf 主机侧端到端,dev_mode shell):
|
||||
firstInit="1\n1" → shell dev_tests → 交互 `curl`(PATH → /bin/CURL.ELF)。
|
||||
主机侧 Start-TestHttpServer 监听 127.0.0.1:8000,guest 输入
|
||||
`http://10.0.2.2:8000/` 经 slirp 网关访问主机服务;
|
||||
断言主机侧 HTTP 服务器日志收到 "GET / HTTP/1.0"(端到端最强证据),
|
||||
guest 侧断言 [curl] boot / [curl] exit / [shell] path: tool returned。
|
||||
响应体只写帧缓冲(无串口),标 MANUAL。
|
||||
|
||||
退出码:全部通过 0,任一 FAIL 1。
|
||||
#>
|
||||
param(
|
||||
[switch]$Build,
|
||||
[string[]]$Scenarios = @('NA', 'NB')
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
|
||||
|
||||
Initialize-QemuTest -Build:$Build
|
||||
$result = New-TestCaseResult 'network-e2e'
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
function Invoke-ScenarioNA {
|
||||
Write-Host "`n--- N-A netman 全链路 (首启, e1000 + virtio-net 双网卡) ---"
|
||||
Set-DeshabFirstInit -First 0 -DevMode 0
|
||||
# run_qemu.bat 默认仅 e1000;追加第二网卡 virtio-net-pci 挂独立 slirp 后端 net1。
|
||||
$s = Start-QemuSession -Name 'nete2e-na' -MonitorPort 45506 -ExtraArgs @(
|
||||
'-netdev', 'user,id=net1',
|
||||
'-device', 'virtio-net-pci,netdev=net1'
|
||||
)
|
||||
try {
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @(
|
||||
'[netman] network final rc=',
|
||||
'[netman] DHCP: no OFFER received',
|
||||
'[netman] ARP: timeout',
|
||||
'[netman] DNS: timeout',
|
||||
'[PANIC]'
|
||||
) -TimeoutSeconds 240
|
||||
Start-Sleep -Seconds 2
|
||||
$log = Read-QemuLogText $s.LogPath
|
||||
$result.LogPath = $s.LogPath
|
||||
|
||||
# ---- N1/N2: 双网卡驱动与设备注册 ----
|
||||
Assert-QemuLog $result $log -Ordered @(
|
||||
'[e1000] netdev registered, index=0x0000000000000000',
|
||||
'[virtio_net] netdev registered, index=',
|
||||
'[virtio_net] selftest: TX path ok',
|
||||
'[virtio_net] selftest: DHCP OFFER yiaddr=',
|
||||
'[virtio_net] selftest PASS: RX path ok'
|
||||
)
|
||||
# ---- netman 全链路(N3 DHCP / N4 ARP / N5 DNS) ----
|
||||
Assert-QemuLog $result $log -Ordered @(
|
||||
'[DSK] loading netman',
|
||||
'[netman] boot',
|
||||
'[netman] netdev_count=0x0000000000000002',
|
||||
'[netman] DHCP: sending DISCOVER',
|
||||
'[netman] DHCP OFFER ip=',
|
||||
'[netman] DHCP: sending REQUEST',
|
||||
'[netman] DHCP ACK, leased ip=',
|
||||
'[netman] DHCP final rc=0x0000000000000000',
|
||||
'[netman] ARP: gateway resolved',
|
||||
'[netman] ARP final rc=0x0000000000000000',
|
||||
'[netman] DNS example.com=',
|
||||
'[netman] DNS final rc=0x0000000000000000',
|
||||
'[netman] network final rc=0x0000000000000000'
|
||||
) -MustNotContain @(
|
||||
'[PANIC]',
|
||||
'[netman] DHCP: no OFFER received',
|
||||
'[netman] ARP: timeout',
|
||||
'[netman] DNS: timeout'
|
||||
)
|
||||
|
||||
if ($hit -eq '[netman] network final rc=') {
|
||||
Add-TestCheck $result 'N-A: netman DHCP+ARP+DNS 全链路 rc=0' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'N-A: netman DHCP+ARP+DNS 全链路 rc=0' 'FAIL' "hit=$hit"
|
||||
}
|
||||
Add-TestNote $result 'N-A: netman 仅首启调度;后续 FirstInit 向导自动化由 boot-regression.ps1 B1 覆盖,本场景断言后即结束会话'
|
||||
} finally {
|
||||
Stop-QemuSession $s
|
||||
}
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
function Invoke-ScenarioNB {
|
||||
Write-Host "`n--- N-B curl.elf 主机侧端到端 (dev_mode shell → http://10.0.2.2:8000/) ---"
|
||||
Set-DeshabFirstInit -First 1 -DevMode 1
|
||||
Set-DeshabAutoexec -Lines @('ver', 'exit')
|
||||
|
||||
$httpLog = Join-Path $script:TestWorkDir 'nete2e-http-requests.log'
|
||||
$httpJob = Start-TestHttpServer -Port 8000 -LogPath $httpLog
|
||||
$s = Start-QemuSession -Name 'nete2e-nb' -MonitorPort 45506
|
||||
try {
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @('=== 自动测试完成 ===', '[PANIC]') -TimeoutSeconds 240
|
||||
if ($hit -ne '=== 自动测试完成 ===') {
|
||||
$result.LogPath = $s.LogPath
|
||||
Add-TestCheck $result 'N-B: shell dev_tests 完成' 'FAIL' "hit=$hit"
|
||||
return
|
||||
}
|
||||
Add-TestCheck $result 'N-B: shell dev_tests 完成' 'PASS'
|
||||
|
||||
# --- 启动 curl 工具(PATH → /bin/CURL.ELF) ---
|
||||
Start-Sleep -Seconds 1
|
||||
Send-QemuText -Session $s -Text 'curl' -Enter
|
||||
$h = Wait-QemuLog -Session $s -Patterns @('[curl] boot') -TimeoutSeconds 60
|
||||
if (-not $h) { Add-TestCheck $result 'N-B: curl.elf PATH 加载启动' 'FAIL' 'missing [curl] boot'; return }
|
||||
Add-TestCheck $result 'N-B: curl.elf PATH 加载启动' 'PASS'
|
||||
|
||||
# --- 输入 URL:经 slirp 网关 10.0.2.2 访问主机 127.0.0.1:8000 ---
|
||||
Start-Sleep -Milliseconds 800
|
||||
Send-QemuText -Session $s -Text 'http://10.0.2.2:8000/' -Enter -DelayMs 110
|
||||
|
||||
# 主机侧端到端证据:HTTP 服务器收到 guest 的 GET 请求
|
||||
$deadline = (Get-Date).AddSeconds(45)
|
||||
$hostGot = $false
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if ((Test-Path $httpLog) -and ((Get-Content $httpLog -Raw -ErrorAction SilentlyContinue) -match 'GET / HTTP/1\.0')) {
|
||||
$hostGot = $true; break
|
||||
}
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
if ($hostGot) {
|
||||
Add-TestCheck $result 'N-B: 主机 HTTP 服务器收到 guest GET /(TCP+IP+ETH 全链路)' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'N-B: 主机 HTTP 服务器收到 guest GET /(TCP+IP+ETH 全链路)' 'FAIL' 'no request logged by host http server within 45s'
|
||||
}
|
||||
|
||||
# --- Esc 退出 curl 返回 shell ---
|
||||
Start-Sleep -Seconds 2
|
||||
Send-QemuKeys -Session $s -Keys @('esc')
|
||||
$h2 = Wait-QemuLog -Session $s -Patterns @('[curl] exit') -TimeoutSeconds 30
|
||||
$logc = Read-QemuLogText $s.LogPath
|
||||
if ($h2 -and $logc.Contains('[shell] path: tool returned')) {
|
||||
Add-TestCheck $result 'N-B: curl Esc 退出返回 shell' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'N-B: curl Esc 退出返回 shell' 'FAIL' 'missing [curl] exit / tool returned'
|
||||
}
|
||||
|
||||
# --- Esc 回 DSK → cmd → desktop ---
|
||||
Send-QemuKeys -Session $s -Keys @('esc')
|
||||
$h3 = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
|
||||
$log = Read-QemuLogText $s.LogPath
|
||||
$result.LogPath = $s.LogPath
|
||||
Assert-QemuLog $result $log -MustNotContain @('[PANIC]')
|
||||
if ($h3) { Add-TestCheck $result 'N-B: Esc → cmd → desktop ready' 'PASS' }
|
||||
else { Add-TestCheck $result 'N-B: Esc → cmd → desktop ready' 'FAIL' 'no desktop ready' }
|
||||
|
||||
Add-TestCheck $result 'N-B: HTTP 200 响应体与 DESHAB-NET-E2E-MARKER 渲染' 'MANUAL' 'curl 响应输出仅帧缓冲(无串口锚点);主机服务器固定回复 marker 页面,可 screendump 人工核对'
|
||||
Add-TestNote $result 'N6/N11 ping 链路:由 userapps-matrix.ps1 交互阶段覆盖(ping 工具 10.0.2.2 四报文)'
|
||||
Add-TestNote $result 'N7/N9 外网访问:依赖主机联网,不在自动化断言范围(文档标 🔶)'
|
||||
} finally {
|
||||
Stop-QemuSession $s
|
||||
Stop-TestHttpServer $httpJob
|
||||
}
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
try {
|
||||
foreach ($sc in $Scenarios) {
|
||||
switch ($sc.ToUpper()) {
|
||||
'NA' { Invoke-ScenarioNA }
|
||||
'NB' { Invoke-ScenarioNB }
|
||||
default { Add-TestNote $result "未知场景 $sc,跳过" }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Restore-DeshabScenario
|
||||
}
|
||||
|
||||
Write-TestCaseResult $result
|
||||
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
|
||||
+163
-38
@@ -1,15 +1,23 @@
|
||||
#requires -Version 5.1
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
pe-compat — PE/EXE 兼容层测试(docs/RE/test-cases/pe-compat.md)
|
||||
|
||||
.DESCRIPTION
|
||||
正常启动到 cmd.elf,用 AUTOEXEC.BAT 自动执行 PE 命令序列:
|
||||
正常启动到 cmd.elf,用 AUTOEXEC.BAT 自动执行 PE 命令序列。
|
||||
因两个已登记系统 bug(详见 docs/RE/Bug记录.txt),拆三个独立会话:
|
||||
|
||||
pe HELLO64.EXE → PE32+ 原生 Ring0 执行,shim_WriteFile 输出串口
|
||||
pe HELLO32.EXE → PE32 x86emu32 解释执行,shim32 输出串口
|
||||
pe NONEXIST.EXE → 错误路径:不产生 [cmd] PE run begin
|
||||
exit → 返回 DSK → desktop
|
||||
会话 A (pe64):pe HELLO64.EXE → PE32+ 原生 Ring0 执行,shim_WriteFile 输出串口。
|
||||
BUG-20260801-006 已修复(setjmp 下沉 pe_service_run 本体):
|
||||
ExitProcess longjmp 干净返回,"[cmd] PE run end" 稳定出现。
|
||||
会话 B (pe32):pe HELLO32.EXE → PE32 x86emu32 解释执行。
|
||||
BUG-20260801-007 已修复(gp() guest→host 指针转换 +
|
||||
fn64 调用 typedef 显式 ms_abi):"Hello from PE32!" 稳定输出。
|
||||
会话 D (fault64):pe FAULT64.EXE → BUG-20260801-005 验收:DSK 阶段故意
|
||||
#PF 必须打出 [IDT] exception 完整现场(vector=0xe, err=0x2,
|
||||
cr2=0x40000000000)。修复前 halt_all 静默吞异常零日志。
|
||||
会话 C (peerr):pe NONEXIST.EXE + exit → 错误路径:不产生 [cmd] PE run begin;
|
||||
exit 正常返回 DSK("[DSK] cmd.elf returned")。
|
||||
|
||||
串口可断言锚点:
|
||||
[cmd] PE run begin / [cmd] cmdline: ... / [cmd] PE run end (logl/swrite, 始终上串口)
|
||||
@@ -17,66 +25,183 @@
|
||||
[PE] running PE32 via x86emu32 (pe_service)
|
||||
Hello from PE32+! / Hello from PE32! (shim WriteFile → 串口)
|
||||
|
||||
注意:cmd_pe 传 full_cmdline=args(不含 "pe " 前缀),
|
||||
所以串口回显为 "[cmd] cmdline: HELLO64.EXE" 而不是 "pe HELLO64.EXE"。
|
||||
|
||||
WHPX 已知问题:pe_service 为原生 PE32+ 执行写 CR4 启用 SSE 后,
|
||||
WHPX 可能以 "Unexpected VP exit code 4" 提前终止 QEMU(WHPX 平台限制,
|
||||
非 guest 崩溃)。任一会话在 WHPX 下未命中锚点时自动以 -Accel tcg 重跑
|
||||
(可用 -Accel tcg 直接指定跳过 WHPX)。
|
||||
|
||||
peinfo / 无参用法提示只写帧缓冲(cmd 非 dev_mode 无串口镜像),标 MANUAL。
|
||||
|
||||
退出码:全部通过 0,任一 FAIL 1。
|
||||
#>
|
||||
param([switch]$Build)
|
||||
param(
|
||||
[switch]$Build,
|
||||
[string]$Accel = 'whpx'
|
||||
)
|
||||
$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')
|
||||
# ----------------------------------------------------------------
|
||||
# 单会话执行一轮 AUTOEXEC:正常启动 → cmd → 命令序列 → 等锚点/会话结束
|
||||
# 填充 $script:RunLog / RunLogPath;返回命中的锚点或 $null。
|
||||
# ----------------------------------------------------------------
|
||||
$script:RunLog = ''
|
||||
$script:RunLogPath = ''
|
||||
function Invoke-PeSession {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Name,
|
||||
[Parameter(Mandatory)][string]$AccelMode,
|
||||
[Parameter(Mandatory)][string[]]$Autoexec,
|
||||
[Parameter(Mandatory)][string[]]$Anchors
|
||||
)
|
||||
Set-DeshabFirstInit -First 1 -DevMode 0
|
||||
Set-DeshabAutoexec -Lines $Autoexec
|
||||
|
||||
$s = Start-QemuSession -Name 'pe-compat'
|
||||
$timeout = 150
|
||||
if ($AccelMode -eq 'tcg') { $timeout = 360 } # TCG 全模拟显著更慢
|
||||
$s = Start-QemuSession -Name "$Name-$AccelMode" -Accel $AccelMode -MonitorPort 45504
|
||||
try {
|
||||
$hit = Wait-QemuLog -Session $s -Patterns $Anchors -TimeoutSeconds $timeout
|
||||
Start-Sleep -Seconds 2
|
||||
$script:RunLog = Read-QemuLogText $s.LogPath
|
||||
$script:RunLogPath = $s.LogPath
|
||||
return $hit
|
||||
} finally {
|
||||
Stop-QemuSession $s
|
||||
}
|
||||
}
|
||||
|
||||
# WHPX 未命中锚点 → TCG 兜底重跑一轮
|
||||
function Invoke-PeSessionWithFallback {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Name,
|
||||
[Parameter(Mandatory)][string[]]$Autoexec,
|
||||
[Parameter(Mandatory)][string[]]$Anchors
|
||||
)
|
||||
$hit = Invoke-PeSession -Name $Name -AccelMode $Accel -Autoexec $Autoexec -Anchors $Anchors
|
||||
if (-not $hit -and $Accel -eq 'whpx') {
|
||||
$why = 'WHPX 下未命中锚点'
|
||||
if ($script:RunLog.Contains('WHPX: Unexpected VP exit')) { $why = 'WHPX 在 PE 执行处终止 QEMU(Unexpected VP exit,WHPX 平台限制)' }
|
||||
Add-TestNote $result "${Name}: $why,自动以 TCG 重跑"
|
||||
$script:UsedAccel = 'tcg'
|
||||
$hit = Invoke-PeSession -Name $Name -AccelMode 'tcg' -Autoexec $Autoexec -Anchors $Anchors
|
||||
}
|
||||
return $hit
|
||||
}
|
||||
|
||||
$script:UsedAccel = $Accel
|
||||
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
|
||||
# ================================================================
|
||||
# 会话 A:B2.1 PE32+ 原生加载执行(BUG-006 已修复,ExitProcess 干净返航)
|
||||
# ================================================================
|
||||
$hitA = Invoke-PeSessionWithFallback -Name 'pe64' -Autoexec @('pe HELLO64.EXE') -Anchors @('Hello from PE32+!', '[PANIC]')
|
||||
$logA = $script:RunLog
|
||||
$result.LogPath = $script:RunLogPath
|
||||
|
||||
# ---- B2.1 PE32+ 加载执行 ----
|
||||
Assert-QemuLog $result $log -Ordered @(
|
||||
Assert-QemuLog $result $logA -Ordered @(
|
||||
'[cmd] running AUTOEXEC.BAT',
|
||||
'[cmd] PE run begin',
|
||||
'[cmd] cmdline: pe HELLO64.EXE',
|
||||
'[cmd] cmdline: HELLO64.EXE',
|
||||
'[PE] PE32+ (64-bit)',
|
||||
'[PE] running PE32+ natively',
|
||||
'Hello from PE32+!',
|
||||
'[cmd] PE run end'
|
||||
'Hello from PE32+!'
|
||||
) -MustNotContain @('[PANIC]')
|
||||
|
||||
# ---- ABI 验证:cmd 启动时 ctx->reserved[4] + PE_SERVICE_MAGIC 校验 ----
|
||||
Assert-QemuLog $result $logA -MustContain @('[cmd] PE service ok')
|
||||
|
||||
# ---- B2.6 ExitProcess 返回链:BUG-006 已修复,转硬断言(回归守护) ----
|
||||
if ($logA.Contains('[cmd] PE run end')) {
|
||||
Add-TestCheck $result 'B2.6: ExitProcess 后 cmd 继续(PE32+ 返回链)' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'B2.6: ExitProcess 后 cmd 继续(PE32+ 返回链)' 'FAIL' 'missing [cmd] PE run end(BUG-20260801-006 回归?)'
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# 会话 B:B2.2 PE32 x86emu32 解释执行(BUG-007 已修复,shim 指针转换)
|
||||
# ================================================================
|
||||
$hitB = Invoke-PeSessionWithFallback -Name 'pe32' -Autoexec @('pe HELLO32.EXE') -Anchors @('Hello from PE32!', '[PANIC]', '[PE32] emulator error')
|
||||
$logB = $script:RunLog
|
||||
|
||||
Assert-QemuLog $result $logB -Ordered @(
|
||||
'[cmd] PE run begin',
|
||||
'[cmd] cmdline: HELLO32.EXE',
|
||||
'[PE] running PE32 via x86emu32'
|
||||
) -MustNotContain @(
|
||||
'[PANIC]',
|
||||
'[PE32] emulator error',
|
||||
'[PE32] emu init failed',
|
||||
'[PE] unimplemented import',
|
||||
'[PE32] unimplemented import',
|
||||
'[PE] unimplemented API called'
|
||||
)
|
||||
|
||||
# ---- 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.2 shim 输出:BUG-007 已修复,转硬断言(回归守护) ----
|
||||
if ($logB.Contains('Hello from PE32!')) {
|
||||
Add-TestCheck $result 'B2.2: PE32 解释执行 shim 输出' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'B2.2: PE32 解释执行 shim 输出' 'FAIL' 'missing Hello from PE32!(BUG-20260801-007 回归?)'
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# 会话 D:BUG-20260801-005 验收 — DSK 阶段故意 #PF 必须打出完整异常现场
|
||||
# FAULT64.EXE: movabs rax,0x400000000000 ; mov [rax],1 ; jmp $
|
||||
# 预期 vector=0xe / err=0x2(supervisor write non-present) /
|
||||
# cr2=0x0000400000000000;修复前 halt_all 静默吞掉零日志。
|
||||
# 异常后 idt_handler 安全停机(hlt 循环),QEMU 存活 → 锚点命中即返回。
|
||||
# 若异常投递失效则三重故障 → -no-reboot QEMU 退出 → hit=$null → FAIL。
|
||||
# ================================================================
|
||||
$hitD = Invoke-PeSessionWithFallback -Name 'fault64' -Autoexec @('pe FAULT64.EXE') -Anchors @('[IDT] exception', '[PANIC]')
|
||||
$logD = $script:RunLog
|
||||
|
||||
if (-not $hitD) {
|
||||
Add-TestCheck $result 'BUG-005: DSK 阶段 #PF 异常投递' 'FAIL' 'session exited without [IDT] exception(异常被吞/三重故障)'
|
||||
} else {
|
||||
Add-TestCheck $result 'BUG-005: DSK 阶段 #PF 异常投递' 'PASS'
|
||||
}
|
||||
Assert-QemuLog $result $logD -Ordered @(
|
||||
'[cmd] cmdline: FAULT64.EXE',
|
||||
'[PE] running PE32+ natively',
|
||||
'[IDT] exception',
|
||||
'[IDT] vector=0x000000000000000e',
|
||||
'[IDT] err=0x0000000000000002',
|
||||
'[IDT] cr2=0x0000400000000000'
|
||||
)
|
||||
|
||||
# ---- 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
|
||||
# ================================================================
|
||||
# 会话 C:B2.4 文件不存在错误路径 + cmd exit 正常返回 DSK
|
||||
# ================================================================
|
||||
$hitC = Invoke-PeSessionWithFallback -Name 'peerr' -Autoexec @('pe NONEXIST.EXE', 'exit') -Anchors @('[DSK] cmd.elf returned', '[PANIC]')
|
||||
$logC = $script:RunLog
|
||||
|
||||
# ---- 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' }
|
||||
# NONEXIST 不得进入 PE 执行路径
|
||||
if ($logC.Contains('[cmd] cmdline: NONEXIST.EXE') -or $logC.Contains('[cmd] PE run begin')) {
|
||||
Add-TestCheck $result 'B2.4: NONEXIST.EXE 不进入 PE 执行路径' 'FAIL' 'unexpected PE run begin / cmdline echo'
|
||||
} else {
|
||||
Add-TestCheck $result 'B2.4: NONEXIST.EXE 不进入 PE 执行路径' 'PASS'
|
||||
}
|
||||
Assert-QemuLog $result $logC -MustNotContain @('[PANIC]')
|
||||
|
||||
# ---- ABI 验证(源码级,日志侧证) ----
|
||||
Assert-QemuLog $result $log -MustContain @('[cmd] PE service ok') # ctx->reserved[4] + PE_SERVICE_MAGIC 校验通过
|
||||
if ($hitC -eq '[DSK] cmd.elf returned') {
|
||||
Add-TestCheck $result '链路: cmd exit 正常返回 DSK' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result '链路: cmd exit 正常返回 DSK' 'FAIL' "hit=$hitC(missing [DSK] cmd.elf returned)"
|
||||
}
|
||||
|
||||
Add-TestNote $result "加速方案: $script:UsedAccel"
|
||||
Add-TestNote $result 'BUG-006/007 已修复,返回链断言已转硬断言;会话仍按故障域拆分(单会话全序列合并留作后续优化)'
|
||||
|
||||
# ---- 帧缓冲限定项(无串口输出,标 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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
performance-baseline — 启动性能基线(docs/RE/test-cases/performance-baseline.md)
|
||||
|
||||
.DESCRIPTION
|
||||
正常启动场景(firstInit="1\n0", AUTOEXEC=ver/exit),按墙上时钟记录
|
||||
关键阶段锚点在串口日志中首次出现的时刻,计算阶段时间差:
|
||||
|
||||
t0 QEMU 进程启动(含 OVMF + Limine bootloader)
|
||||
[UTSM] boot UTSM 内核入口
|
||||
[UTSM] SELFTEST PASS
|
||||
UTSM 自检完成(位于 4 stage 共 16 驱动装载之后,
|
||||
即"驱动加载完"时刻)
|
||||
[DSK] boot DSK 主内核接管
|
||||
desktop ready 桌面就绪(login skipped → cmd(AUTOEXEC) → desktop)
|
||||
|
||||
输出 .build_tmp\perf_baseline.json(锚点绝对时刻 + 阶段毫秒数 + 环境信息)。
|
||||
本用例只测量展示、不做阈值断言(文档标 P2,QEMU 数值仅作回归参考)。
|
||||
多次测量取中位数:默认 3 轮,可用 -Runs 调整。
|
||||
|
||||
退出码:测量完成 0;会话/锚点异常 1。
|
||||
#>
|
||||
param(
|
||||
[switch]$Build,
|
||||
[int]$Runs = 3
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
|
||||
|
||||
Initialize-QemuTest -Build:$Build
|
||||
$result = New-TestCaseResult 'performance-baseline'
|
||||
|
||||
# 阶段锚点定义(顺序即启动顺序)
|
||||
$anchorDefs = @(
|
||||
@{ Key = 'utsm_boot'; Pattern = '[UTSM] boot' },
|
||||
@{ Key = 'utsm_selftest'; Pattern = '[UTSM] SELFTEST PASS' },
|
||||
@{ Key = 'dsk_boot'; Pattern = '[DSK] boot' },
|
||||
@{ Key = 'desktop_ready'; Pattern = 'desktop ready' }
|
||||
)
|
||||
|
||||
# 轮询串口日志,记录每个锚点首次出现的墙上时刻;返回 hashtable 或 $null
|
||||
function Measure-BootPhases {
|
||||
param([Parameter(Mandatory)]$Session, [int]$TimeoutSeconds = 240)
|
||||
$marks = @{}
|
||||
foreach ($a in $anchorDefs) { $marks[$a.Key] = $null }
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
$remaining = $anchorDefs.Count
|
||||
while ((Get-Date) -lt $deadline -and $remaining -gt 0) {
|
||||
$text = Read-QemuLogText $Session.LogPath
|
||||
foreach ($a in $anchorDefs) {
|
||||
if ($null -eq $marks[$a.Key] -and $text.Contains($a.Pattern)) {
|
||||
$marks[$a.Key] = Get-Date
|
||||
$remaining--
|
||||
}
|
||||
}
|
||||
if (Test-QemuSessionExited $Session) { break }
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
return $marks
|
||||
}
|
||||
|
||||
$allRuns = New-Object System.Collections.ArrayList
|
||||
try {
|
||||
for ($run = 1; $run -le $Runs; $run++) {
|
||||
Write-Host "`n--- perf run $run/$Runs (正常启动 → desktop ready) ---"
|
||||
Set-DeshabFirstInit -First 1 -DevMode 0
|
||||
Set-DeshabAutoexec -Lines @('ver', 'exit')
|
||||
|
||||
$t0 = Get-Date
|
||||
$s = Start-QemuSession -Name "perf-run$run" -MonitorPort 45507
|
||||
try {
|
||||
$marks = Measure-BootPhases -Session $s -TimeoutSeconds 240
|
||||
$result.LogPath = $s.LogPath
|
||||
} finally {
|
||||
Stop-QemuSession $s
|
||||
}
|
||||
|
||||
if ($null -eq $marks['desktop_ready']) {
|
||||
Add-TestCheck $result "run${run}: 锚点采集完整" 'FAIL' 'desktop ready not observed within timeout'
|
||||
continue
|
||||
}
|
||||
Add-TestCheck $result "run${run}: 锚点采集完整" 'PASS'
|
||||
|
||||
$ms = [ordered]@{
|
||||
ovmf_bootloader_ms = [int]($marks['utsm_boot'] - $t0).TotalMilliseconds
|
||||
utsm_drivers_ms = [int]($marks['utsm_selftest'] - $marks['utsm_boot']).TotalMilliseconds
|
||||
selftest_to_dsk_ms = [int]($marks['dsk_boot'] - $marks['utsm_selftest']).TotalMilliseconds
|
||||
dsk_to_desktop_ms = [int]($marks['desktop_ready'] - $marks['dsk_boot']).TotalMilliseconds
|
||||
total_boot_ms = [int]($marks['desktop_ready'] - $t0).TotalMilliseconds
|
||||
}
|
||||
[void]$allRuns.Add([pscustomobject]@{
|
||||
run = $run
|
||||
started_at = $t0.ToString('yyyy-MM-dd HH:mm:ss')
|
||||
metrics = $ms
|
||||
})
|
||||
Write-Host (" ovmf={0}ms utsm+drivers={1}ms dsk_load={2}ms dsk->desktop={3}ms total={4}ms" -f `
|
||||
$ms.ovmf_bootloader_ms, $ms.utsm_drivers_ms, $ms.selftest_to_dsk_ms, $ms.dsk_to_desktop_ms, $ms.total_boot_ms)
|
||||
}
|
||||
} finally {
|
||||
Restore-DeshabScenario
|
||||
}
|
||||
|
||||
# ---- 中位数汇总 + JSON 输出 ----
|
||||
if ($allRuns.Count -gt 0) {
|
||||
$median = [ordered]@{}
|
||||
foreach ($k in @('ovmf_bootloader_ms', 'utsm_drivers_ms', 'selftest_to_dsk_ms', 'dsk_to_desktop_ms', 'total_boot_ms')) {
|
||||
$vals = @($allRuns | ForEach-Object { [int]$_.metrics[$k] } | Sort-Object)
|
||||
$median[$k] = $vals[[int]($vals.Count / 2)]
|
||||
}
|
||||
|
||||
$out = [ordered]@{
|
||||
generated_at = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
|
||||
method = 'host wall-clock polling of serial log anchors (200ms poll, +/-200ms 精度)'
|
||||
environment = [ordered]@{
|
||||
accel = 'whpx'
|
||||
cpu = 'qemu64,-vmx'
|
||||
machine = 'q35'
|
||||
memory = '2G'
|
||||
scenario = 'firstInit=1 dev_mode=0 AUTOEXEC=ver,exit -> desktop ready'
|
||||
snapshot = $true
|
||||
}
|
||||
median_ms = $median
|
||||
runs = $allRuns
|
||||
}
|
||||
$jsonPath = Join-Path $script:BuildTmp 'perf_baseline.json'
|
||||
($out | ConvertTo-Json -Depth 6) | Out-File -Encoding utf8 $jsonPath
|
||||
Add-TestNote $result "基线 JSON: $jsonPath"
|
||||
|
||||
Add-TestCheck $result ("P1: UTSM 启动+驱动装载 (utsm_boot→SELFTEST) = {0} ms (目标<500ms QEMU, 仅展示)" -f $median.utsm_drivers_ms) 'PASS'
|
||||
Add-TestCheck $result ("DSK 加载 (SELFTEST→[DSK] boot) = {0} ms" -f $median.selftest_to_dsk_ms) 'PASS'
|
||||
Add-TestCheck $result ("DSK→desktop ready = {0} ms" -f $median.dsk_to_desktop_ms) 'PASS'
|
||||
Add-TestCheck $result ("总启动 (QEMU 启动→desktop ready) = {0} ms" -f $median.total_boot_ms) 'PASS'
|
||||
Add-TestNote $result '本用例不做阈值断言;QEMU WHPX 数值仅作回归参考(文档:QEMU≠真机,固定加速方案)'
|
||||
Add-TestNote $result 'P2/P3/P6(单 ELF 加载计时)、P4 ping RTT、P7 桌面帧率、P8-P11(PE/VMM/Linux)需内核 TSC 打点日志,当前串口无对应锚点,保持 PENDING(见建议锚点清单)'
|
||||
}
|
||||
|
||||
Write-TestCaseResult $result
|
||||
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
|
||||
+196
-137
@@ -1,25 +1,31 @@
|
||||
#requires -Version 5.1
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
userapps-matrix — 用户态程序功能矩阵(docs/RE/test-cases/userapps-matrix.md)
|
||||
|
||||
.DESCRIPTION
|
||||
dev_mode 单会话覆盖 shell 内建命令与工具程序(串口镜像开启,全程可断言):
|
||||
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
|
||||
会话 A(唯一):dev_tests 硬编码序列 → help → id → pci 存活 →
|
||||
ping(PATH 加载 → 10.0.2.2 四报文 → Esc 退出)→
|
||||
run EDITOR.ELF / FILEMAN.ELF / BROWSER.ELF(加载 + 按键 + 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
|
||||
历史背景(详见 docs/RE/Bug记录.txt,均已 FIXED,相关降级已全部回收):
|
||||
|
||||
工具内部 UI 输出(编辑器内容、fileman 列表、ping RTT)仅帧缓冲,标 MANUAL。
|
||||
desktop 双击/拖动/任务栏无串口锚点,标 MANUAL。
|
||||
BUG-20260801-001 [shell] y/i/0 吞键(未跟踪 Ctrl 修饰键):
|
||||
已修复(0x1D/0x9D 跟踪 Ctrl,快捷键仅在 Ctrl 按下时生效)
|
||||
→ id/ping 等含 i 命令恢复自动化断言。
|
||||
|
||||
BUG-20260801-002 [shell] g_run_image 仅 256KB,editor(496KB) 拒绝加载:
|
||||
已修复(g_run_image 扩至 1MB,与 desktop 宿主对齐)
|
||||
→ run EDITOR.ELF 断言升级为 [editor] boot + [editor] exit。
|
||||
|
||||
BUG-20260801-003 [desktop_app.h] da_mouse_poll 吞键盘扫描码 +
|
||||
BUG-20260801-008 [desktop_app.h] da_mouse_init 配置字污染:
|
||||
均已修复(先判 AUX 位再读 0x60;init 首尾 da_ps2_drain + 读全响应)
|
||||
→ fileman/editor/browser 键盘路径恢复,Esc 退出断言全部自动化;
|
||||
工具不再占住会话,三会合并为单会话顺序执行。
|
||||
|
||||
退出码:全部通过 0,任一 FAIL 1。
|
||||
#>
|
||||
@@ -30,136 +36,191 @@ $ErrorActionPreference = 'Stop'
|
||||
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"
|
||||
# ----------------------------------------------------------------
|
||||
# monitor sendkey 在 guest 重负载(AHCI 读盘刷屏、帧缓冲重绘)下会丢扫描码。
|
||||
# 对策:每键 150ms + "发送→等锚点→重试 ≤3 次",重试前先 backspace 清行。
|
||||
# 注意:backspace(0x0E) 不在 shell Ctrl+U/W/K 快捷键扫描码列表内,清行可靠。
|
||||
# ----------------------------------------------------------------
|
||||
function Invoke-ShellCmdRetry {
|
||||
param(
|
||||
[Parameter(Mandatory)]$Session,
|
||||
[Parameter(Mandatory)][string]$Text,
|
||||
[Parameter(Mandatory)][string[]]$Anchors,
|
||||
[int]$TimeoutSeconds = 20,
|
||||
[int]$MaxAttempts = 3
|
||||
)
|
||||
for ($a = 1; $a -le $MaxAttempts; $a++) {
|
||||
if ($a -gt 1) {
|
||||
$bk = @(); for ($i = 0; $i -lt 24; $i++) { $bk += 'backspace' }
|
||||
Send-QemuKeys -Session $Session -Keys $bk -DelayMs 50
|
||||
Start-Sleep -Milliseconds 400
|
||||
Add-TestNote $result "输入 '$Text' 第 $($a-1) 次未命中锚点,清行重试"
|
||||
}
|
||||
Send-QemuText -Session $Session -Text $Text -Enter -DelayMs 150
|
||||
$h = Wait-QemuLog -Session $Session -Patterns $Anchors -TimeoutSeconds $TimeoutSeconds
|
||||
if ($h) { return $h }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# ================= 阶段 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' }
|
||||
# Esc 退出重试:只在锚点未出现时补发,锚点出现即停(避免多余 Esc 漏进 shell/cmd)
|
||||
function Send-EscRetry {
|
||||
param(
|
||||
[Parameter(Mandatory)]$Session,
|
||||
[Parameter(Mandatory)][string[]]$Anchors,
|
||||
[int]$TimeoutSeconds = 15,
|
||||
[int]$MaxAttempts = 2
|
||||
)
|
||||
for ($a = 1; $a -le $MaxAttempts; $a++) {
|
||||
Send-QemuKeys -Session $Session -Keys @('esc')
|
||||
$h = Wait-QemuLog -Session $Session -Patterns $Anchors -TimeoutSeconds $TimeoutSeconds
|
||||
if ($h) { return $h }
|
||||
if ($a -lt $MaxAttempts) { Add-TestNote $result "Esc 第 $a 次未命中($($Anchors -join ' | ')),补发" }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- 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)' }
|
||||
try {
|
||||
# ================================================================
|
||||
# 会话 A(唯一):shell 内建命令 + ping/editor/fileman/browser
|
||||
# 工具链全交互 + Esc→cmd→desktop 链路
|
||||
# ================================================================
|
||||
Set-DeshabFirstInit -First 1 -DevMode 1
|
||||
Set-DeshabAutoexec -Lines @('ver', 'exit')
|
||||
|
||||
# --- 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' }
|
||||
$s = Start-QemuSession -Name 'userapps-matrix' -MonitorPort 45505
|
||||
try {
|
||||
$hit = Wait-QemuLog -Session $s -Patterns @('=== 自动测试完成 ===', '[PANIC]') -TimeoutSeconds 240
|
||||
$log = Read-QemuLogText $s.LogPath
|
||||
$result.LogPath = $s.LogPath
|
||||
|
||||
# --- 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'
|
||||
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 ($h) { Add-TestCheck $result '链路: Esc → cmd → desktop ready' 'PASS' }
|
||||
else { Add-TestCheck $result '链路: Esc → cmd → desktop ready' 'FAIL' 'no desktop ready' }
|
||||
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"
|
||||
}
|
||||
|
||||
if ($hit -eq '=== 自动测试完成 ===') {
|
||||
# --- help ---
|
||||
Start-Sleep -Seconds 1
|
||||
$h = Invoke-ShellCmdRetry -Session $s -Text 'help' -Anchors @('dsl push <') -TimeoutSeconds 20
|
||||
if ($h) { Add-TestCheck $result 'shell: help 命令列表' 'PASS' }
|
||||
else { Add-TestCheck $result 'shell: help 命令列表' 'FAIL' 'missing help output' }
|
||||
|
||||
# --- id(BUG-001 修复回归守护:i 不再被吞,输出上串口) ---
|
||||
$h = Invoke-ShellCmdRetry -Session $s -Text 'id' -Anchors @('uid=0(root) gid=0(root)') -TimeoutSeconds 20
|
||||
if ($h) { Add-TestCheck $result 'shell: id 用户查询(BUG-001 修复后 i 可键入)' 'PASS' }
|
||||
else { Add-TestCheck $result 'shell: id 用户查询(BUG-001 修复后 i 可键入)' 'FAIL' 'missing uid=0(root) output' }
|
||||
|
||||
# --- pci(验证存活:pci 后 ver 仍有响应,锚点取 ver 独有行) ---
|
||||
Send-QemuText -Session $s -Text 'pci' -Enter -DelayMs 150
|
||||
Start-Sleep -Seconds 3
|
||||
$h = Invoke-ShellCmdRetry -Session $s -Text 'ver' -Anchors @('Architecture: x86_64') -TimeoutSeconds 20
|
||||
if ($h) { Add-TestCheck $result 'shell: pci 枚举后 shell 存活' 'PASS' }
|
||||
else { Add-TestCheck $result 'shell: pci 枚举后 shell 存活' 'FAIL' 'no response after pci' }
|
||||
|
||||
# --- ping:BUG-001 修复后可键入;PATH → /bin/PING.ELF → 10.0.2.2 四报文 → Esc 退出 ---
|
||||
$h = Invoke-ShellCmdRetry -Session $s -Text 'ping' -Anchors @('[ping] boot') -TimeoutSeconds 30
|
||||
if ($h) {
|
||||
Add-TestCheck $result 'ping.elf: PATH 加载启动([ping] boot)' 'PASS'
|
||||
Start-Sleep -Milliseconds 800
|
||||
# 4 个 ICMP Echo(每报文超时上限 2s);reply/统计仅帧缓冲,串口无锚点
|
||||
Send-QemuText -Session $s -Text '10.0.2.2' -Enter -DelayMs 150
|
||||
Start-Sleep -Seconds 10
|
||||
$h2 = Send-EscRetry -Session $s -Anchors @('[ping] exit') -TimeoutSeconds 15 -MaxAttempts 2
|
||||
$logp = Read-QemuLogText $s.LogPath
|
||||
if ($h2 -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 / path: tool returned'
|
||||
}
|
||||
} else {
|
||||
Add-TestCheck $result 'ping.elf: PATH 加载启动([ping] boot)' 'FAIL' 'missing [ping] boot'
|
||||
}
|
||||
|
||||
# --- editor:BUG-002 已修复(g_run_image 扩至 1MB),run 加载 → 按键插入 → Esc 退出 ---
|
||||
$h = Invoke-ShellCmdRetry -Session $s -Text 'run EDITOR.ELF' -Anchors @('[editor] boot') -TimeoutSeconds 30
|
||||
if ($h) {
|
||||
Add-TestCheck $result 'editor.elf: run 加载启动([editor] boot,BUG-002 修复后行为)' 'PASS'
|
||||
Start-Sleep -Seconds 1
|
||||
Send-QemuText -Session $s -Text 'abc' -DelayMs 150 # 编辑模式按键插入(驱动键盘路径,无串口锚点)
|
||||
$h2 = Send-EscRetry -Session $s -Anchors @('[editor] exit') -TimeoutSeconds 15 -MaxAttempts 2
|
||||
$loge = Read-QemuLogText $s.LogPath
|
||||
if ($h2 -and $loge.Contains('[shell] run: tool returned')) {
|
||||
Add-TestCheck $result 'editor.elf: Esc 退出返回 shell([editor] exit)' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'editor.elf: Esc 退出返回 shell([editor] exit)' 'FAIL' 'missing [editor] exit / run: tool returned'
|
||||
}
|
||||
} else {
|
||||
Add-TestCheck $result 'editor.elf: run 加载启动([editor] boot,BUG-002 修复后行为)' 'FAIL' 'missing [editor] boot'
|
||||
}
|
||||
|
||||
# --- fileman:BUG-003/008 修复后键盘可用;方向键移动选区 + Esc 退出 ---
|
||||
$h = Invoke-ShellCmdRetry -Session $s -Text 'run FILEMAN.ELF' -Anchors @('[fileman] boot') -TimeoutSeconds 30
|
||||
if ($h) {
|
||||
Add-TestCheck $result 'fileman.elf: run 加载启动(未复现 QEMU 退出)' 'PASS'
|
||||
Start-Sleep -Seconds 2 # 等 da_mouse_init + 目录加载 + 首帧渲染
|
||||
Send-QemuKeys -Session $s -Keys @('down', 'down', 'up') -DelayMs 200 # 选区移动(仅帧缓冲,驱动键盘路径)
|
||||
$h2 = Send-EscRetry -Session $s -Anchors @('[fileman] exit') -TimeoutSeconds 15 -MaxAttempts 2
|
||||
$logfm = Read-QemuLogText $s.LogPath
|
||||
if ($h2 -and $logfm.Contains('[shell] run: tool returned')) {
|
||||
Add-TestCheck $result 'fileman.elf: 方向键 + Esc 退出返回 shell(BUG-003/008 修复后键盘路径)' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'fileman.elf: 方向键 + Esc 退出返回 shell(BUG-003/008 修复后键盘路径)' 'FAIL' 'missing [fileman] exit / run: tool returned'
|
||||
}
|
||||
} else {
|
||||
Add-TestCheck $result 'fileman.elf: run 加载启动(未复现 QEMU 退出)' 'FAIL' 'missing [fileman] boot'
|
||||
}
|
||||
|
||||
# --- browser:启动 + Esc 退出 ---
|
||||
$h = Invoke-ShellCmdRetry -Session $s -Text 'run BROWSER.ELF' -Anchors @('[browser] boot') -TimeoutSeconds 30
|
||||
if ($h) {
|
||||
Add-TestCheck $result 'browser.elf: run 加载启动' 'PASS'
|
||||
Start-Sleep -Seconds 2
|
||||
$h2 = Send-EscRetry -Session $s -Anchors @('[browser] exit') -TimeoutSeconds 15 -MaxAttempts 2
|
||||
$logbr = Read-QemuLogText $s.LogPath
|
||||
if ($h2 -and $logbr.Contains('[shell] run: tool returned')) {
|
||||
Add-TestCheck $result 'browser.elf: Esc 退出返回 shell([browser] exit)' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result 'browser.elf: Esc 退出返回 shell([browser] exit)' 'FAIL' 'missing [browser] exit / run: tool returned'
|
||||
}
|
||||
} else {
|
||||
Add-TestCheck $result 'browser.elf: run 加载启动' 'FAIL' 'missing [browser] boot'
|
||||
}
|
||||
|
||||
# --- Esc 回 DSK → cmd(AUTOEXEC) → desktop ---
|
||||
# 只重试到中间锚点 '[shell] esc -> return to DSK' 出现即停,
|
||||
# 避免多余 Esc 漏进 cmd/desktop;desktop ready 纯被动等待。
|
||||
$h = Send-EscRetry -Session $s -Anchors @('[shell] esc -> return to DSK') -TimeoutSeconds 20 -MaxAttempts 2
|
||||
$hitDesk = 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 ($hitDesk) { Add-TestCheck $result '链路: Esc → cmd → desktop ready' 'PASS' }
|
||||
else { Add-TestCheck $result '链路: Esc → cmd → desktop ready' 'FAIL' 'no desktop ready' }
|
||||
}
|
||||
} finally {
|
||||
Stop-QemuSession $s
|
||||
}
|
||||
|
||||
# ================= MANUAL / 引用项 =================
|
||||
Add-TestCheck $result 'ping.elf: ICMP reply/RTT 与四报文统计' 'MANUAL' 'reply/统计输出仅帧缓冲(无串口锚点);10.0.2.2 输入与 [ping] boot/exit 链已自动化'
|
||||
Add-TestCheck $result 'editor.elf: 编辑内容渲染与保存' 'MANUAL' '编辑区/保存结果仅帧缓冲与磁盘(无串口锚点);boot/按键插入/exit 链已自动化'
|
||||
Add-TestCheck $result 'fileman 浏览内容(列表渲染/进入子目录)' 'MANUAL' '列表渲染仅帧缓冲(无串口锚点);方向键/Esc 键盘路径已自动化'
|
||||
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 验证'
|
||||
@@ -167,9 +228,7 @@ try {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
x86emu32-coverage — x86emu32 解释器覆盖率(docs/RE/test-cases/x86emu32-coverage.md)
|
||||
|
||||
.DESCRIPTION
|
||||
两层验证:
|
||||
|
||||
1) 静态枚举(源码侧):
|
||||
解析 CODE/UTSM/pe/x86emu32.c,统计 switch case 0xNN 标签数
|
||||
(= 已实现操作码处理分支),并校验 hello32.exe 头
|
||||
(machine=0x14C, optional magic=0x10B, 确为 PE32 目标样本)。
|
||||
|
||||
2) 动态执行(QEMU 侧):
|
||||
正常启动 firstInit="1\n0" → cmd.elf,AUTOEXEC.BAT 自动执行:
|
||||
pe HELLO32.EXE → PE32 走 x86emu32 解释器
|
||||
exit → 返回 DSK → desktop
|
||||
断言锚点(BUG-20260801-007 已修复,全链路硬断言):
|
||||
[cmd] PE run begin / [cmd] cmdline: HELLO32.EXE
|
||||
[PE] running PE32 via x86emu32 (解释器接管证据)
|
||||
Hello from PE32! (解释执行 + shim IAT 拦截证据)
|
||||
[cmd] PE run end / [DSK] cmd.elf returned
|
||||
已知系统侧阻塞(2026-08-04 monitor 取证定案):末锚点当前必缺失——
|
||||
cmd run_autoexec 共享 f32_data 缓冲被首行 pe 文件加载覆盖,exit 行
|
||||
丢失,cmd 停留交互主循环(无异常、无三重故障,guest 存活轮询键盘)。
|
||||
解释器与 shim 链路本身无恙;待 cmd 侧修复后末锚点自动转绿。
|
||||
注意:[PE] PE32 (32-bit) 仅 pe_load_image(PE32+ 原生路径)输出,
|
||||
解释器路径(pe32_load_into)不打印该行,不可作锚点。
|
||||
禁止锚点:
|
||||
[PE32] emulator error / [PE] unimplemented import /
|
||||
[PE] unimplemented API called / [PANIC]
|
||||
(出现 unimplemented 即说明 hello32 触到未实现指令/导入,覆盖率倒退)
|
||||
|
||||
历史:BUG-20260801-007(shim fn32 全缺,fn64 回退把 guest 指针
|
||||
0x401017 当 host 指针解引用 → #PF → 三重故障)曾截断于首个 shim
|
||||
调用;修复后全链路可断言。故障在 host 侧 shim 而非解释器:
|
||||
0x68 push / FF /2 call [disp32] / IAT 合成地址拦截链路均正确,
|
||||
故"无 emulator error"仍为有效覆盖证据。
|
||||
|
||||
指令级逐条覆盖(文档矩阵 test_arith/test_logic/test_control/test_string)
|
||||
依赖尚未存在的分类测试 PE,标 MANUAL。
|
||||
|
||||
WHPX 已知问题:guest 在 PE 兼容层执行阶段可能触发
|
||||
"Unexpected VP exit code 4" 直接终止 QEMU(WHPX 平台限制)。
|
||||
检测到该特征时自动以 -Accel tcg 重跑一轮(可用 -Accel tcg 直接指定)。
|
||||
|
||||
退出码:全部通过 0,任一 FAIL 1。
|
||||
#>
|
||||
param(
|
||||
[switch]$Build,
|
||||
[string]$Accel = 'whpx'
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
|
||||
|
||||
Initialize-QemuTest -Build:$Build
|
||||
$result = New-TestCaseResult 'x86emu32-coverage'
|
||||
|
||||
# ================= 静态枚举 =================
|
||||
$emuSrc = Join-Path $script:RepoRoot 'CODE\UTSM\pe\x86emu32.c'
|
||||
$src = Get-Content $emuSrc -Raw
|
||||
$caseCount = ([regex]::Matches($src, 'case\s+0x[0-9A-Fa-f]+')).Count
|
||||
Add-TestCheck $result "静态: x86emu32.c 已实现操作码分支 case 0xNN × $caseCount" 'PASS' '仅枚举计数,供覆盖率分母参考'
|
||||
Add-TestNote $result "覆盖率分母: x86emu32.c 共 $caseCount 个 case 0xNN 操作码分支(含 0F 扩展页)"
|
||||
|
||||
$h32 = [System.IO.File]::ReadAllBytes((Join-Path $script:SystemDir 'bin\hello32.exe'))
|
||||
$peOff = [BitConverter]::ToInt32($h32, 0x3C)
|
||||
$mach = [BitConverter]::ToUInt16($h32, $peOff + 4)
|
||||
$optm = [BitConverter]::ToUInt16($h32, $peOff + 24)
|
||||
if ($mach -eq 0x14C -and $optm -eq 0x10B) {
|
||||
Add-TestCheck $result '静态: hello32.exe 为 PE32 (machine=0x14C, magic=0x10B)' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result '静态: hello32.exe 为 PE32 (machine=0x14C, magic=0x10B)' 'FAIL' ("actual machine=0x{0:X} magic=0x{1:X}" -f $mach, $optm)
|
||||
}
|
||||
|
||||
# ================= 动态执行 =================
|
||||
# 单轮执行:正常启动 → cmd → AUTOEXEC(pe HELLO32.EXE / exit) → 返回 DSK
|
||||
# 返回 $true=完整跑完;$false=会话提前退出(WHPX 终止等)
|
||||
function Invoke-EmuRun {
|
||||
param([Parameter(Mandatory)][string]$AccelMode)
|
||||
Set-DeshabFirstInit -First 1 -DevMode 0
|
||||
Set-DeshabAutoexec -Lines @('pe HELLO32.EXE', 'exit')
|
||||
|
||||
$timeout = 180
|
||||
if ($AccelMode -eq 'tcg') { $timeout = 420 } # TCG 全模拟显著更慢
|
||||
$s = Start-QemuSession -Name "x86emu32-$AccelMode" -Accel $AccelMode -MonitorPort 45508
|
||||
try {
|
||||
$script:RunHit = Wait-QemuLog -Session $s -Patterns @('[DSK] cmd.elf returned', '[PANIC]') -TimeoutSeconds $timeout
|
||||
Start-Sleep -Seconds 2
|
||||
$script:RunLog = Read-QemuLogText $s.LogPath
|
||||
$script:RunLogPath = $s.LogPath
|
||||
if (-not $script:RunHit) { return $false }
|
||||
return $true
|
||||
} finally {
|
||||
Stop-QemuSession $s
|
||||
}
|
||||
}
|
||||
|
||||
$script:RunLog = ''
|
||||
$script:RunLogPath = ''
|
||||
$script:RunHit = $null
|
||||
|
||||
try {
|
||||
$usedAccel = $Accel
|
||||
$ok = Invoke-EmuRun -AccelMode $Accel
|
||||
if (-not $ok -and $Accel -eq 'whpx' -and $script:RunLog.Contains('WHPX: Unexpected VP exit')) {
|
||||
Add-TestNote $result "WHPX 在 PE 兼容层执行处终止 QEMU(Unexpected VP exit,WHPX 平台限制),自动以 TCG 重跑"
|
||||
$usedAccel = 'tcg'
|
||||
$ok = Invoke-EmuRun -AccelMode 'tcg'
|
||||
}
|
||||
$log = $script:RunLog
|
||||
$result.LogPath = $script:RunLogPath
|
||||
Add-TestNote $result "加速方案: $usedAccel"
|
||||
$hit = $script:RunHit
|
||||
|
||||
# ---- 解释器接管链(BUG-007 已修复,全链路可稳定断言) ----
|
||||
Assert-QemuLog $result $log -Ordered @(
|
||||
'[cmd] running AUTOEXEC.BAT',
|
||||
'[cmd] PE run begin',
|
||||
'[cmd] cmdline: HELLO32.EXE',
|
||||
'[PE] running PE32 via x86emu32'
|
||||
) -MustNotContain @(
|
||||
'[PANIC]',
|
||||
'[PE32] emulator error',
|
||||
'[PE32] emu init failed',
|
||||
'[PE] unimplemented import',
|
||||
'[PE] unimplemented API called'
|
||||
)
|
||||
|
||||
# ---- shim 输出与完整返回链:BUG-007/006 已修复,转硬断言(回归守护) ----
|
||||
if ($log.Contains('Hello from PE32!')) {
|
||||
Add-TestCheck $result '动态: 解释执行 + shim IAT 拦截输出(Hello from PE32!)' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result '动态: 解释执行 + shim IAT 拦截输出(Hello from PE32!)' 'FAIL' 'missing Hello from PE32!(BUG-20260801-007 回归?)'
|
||||
}
|
||||
if ($log.Contains('[cmd] PE run end') -and $log.Contains('[DSK] cmd.elf returned')) {
|
||||
Add-TestCheck $result '动态: ExitProcess 回 cmd,AUTOEXEC 跑完返回 DSK' 'PASS'
|
||||
} else {
|
||||
# 已知系统侧阻塞(2026-08-04 诊断定案,非 BUG-006/007 回归):
|
||||
# cmd run_autoexec 的 data/size 直指共享静态缓冲 f32_data(fat32_io.h),
|
||||
# 首行 `pe HELLO32.EXE` 加载 PE 即覆盖剩余脚本,exit 行丢失 → cmd 停留
|
||||
# 交互主循环(monitor 取证 RIP=cmd.elf 内 inb 0x60 轮询点,无异常投递)。
|
||||
# pe-compat 会话 C(pe NONEXIST.EXE + exit)通过反证:文件未找到不覆盖
|
||||
# 缓冲时 exit 返回链正常。待 cmd 侧修复(脚本行先拷入私有缓冲再执行)。
|
||||
Add-TestCheck $result '动态: ExitProcess 回 cmd,AUTOEXEC 跑完返回 DSK' 'FAIL' 'missing [DSK] cmd.elf returned(已知系统 bug:cmd run_autoexec 共享 f32_data 被 pe 文件加载覆盖,exit 行丢失)'
|
||||
}
|
||||
|
||||
# 解释器侧覆盖判定:hello32 全指令流(含 shim IAT 拦截与参数读取)
|
||||
# 均正确执行——无 emulator error / unimplemented 日志即解释器覆盖率
|
||||
# 不记倒退。
|
||||
if (-not $log.Contains('[PE] unimplemented') -and -not $log.Contains('[PE32] emulator') -and $log.Contains('[PE] running PE32 via x86emu32')) {
|
||||
Add-TestCheck $result '动态: hello32 已执行指令全部命中已实现集(无 unimplemented/emulator error)' 'PASS'
|
||||
} else {
|
||||
Add-TestCheck $result '动态: hello32 已执行指令全部命中已实现集(无 unimplemented/emulator error)' 'FAIL' 'unimplemented/emulator error anchor present or interpreter not reached'
|
||||
}
|
||||
|
||||
# ---- 指令级分类覆盖(依赖尚不存在的分类测试 PE) ----
|
||||
foreach ($t in @('数据传送簇 (MOV/PUSH/POP 逐操作数形态)', '算术簇 (ADD/SUB/INC/DEC/MUL/DIV 逐标志验证)',
|
||||
'逻辑簇 (AND/OR/XOR/NOT)', '控制流簇 (JMP/CALL/RET/Jcc)',
|
||||
'字符串簇 (MOVSB/STOSB/REP 前缀)', '堆栈帧 (ENTER/LEAVE) + INT3 断点路径')) {
|
||||
Add-TestCheck $result "指令级: $t" 'MANUAL' '需 CODE/tools/pe-samples/ 下分类测试 PE(test_arith.exe 等),当前未构建;hello32 仅综合冒烟'
|
||||
}
|
||||
Add-TestNote $result "pe-compat.ps1 已覆盖 PE32+ 原生对照组(HELLO64.EXE)与 PE 服务 ABI 锚点"
|
||||
} finally {
|
||||
Restore-DeshabScenario
|
||||
}
|
||||
|
||||
Write-TestCaseResult $result
|
||||
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
|
||||
Reference in New Issue
Block a user