scripts\verify-oracle.ps1¶
源码路径:
scripts\verify-oracle.ps1· 188 行
# 移植验收:把 Rust 版 exe 放进 C# 原版所在的目录,跑同样的命令,逐字节 diff。
#
# 用法:
# .\scripts\verify-oracle.ps1 只比对(需先 cargo build)
# .\scripts\verify-oracle.ps1 -Rebuild 先重新编译再比对
# .\scripts\verify-oracle.ps1 -Rebuild -Toolchain stable-x86_64-pc-windows-gnu
#
# 原理:两边 exe 都放在 .oracle\bin\ 下,于是 Paths::detect() 推出同一个程序根,
# 输出里的绝对路径也完全一致 —— 可以直接字节比对,不需要做任何"归一化"。
#
# `.oracle\` 是移植期的脚手架:C# 原版 exe + 离线样本 + 由原版程序生成的标准答案。
# 它不进版本库,但每一条移植出来的命令都应该在这里过一遍。
[CmdletBinding()]
param(
[switch]$Rebuild,
[string]$Toolchain = '',
# 用 C# 原版重新生成「与机器有关」的那几条基准(--versions / --npm-apps)。
# 只在确认是环境变了(而不是移植回归)之后才用。
[switch]$RegenGoldens
)
$ErrorActionPreference = 'Stop'
$ws = Split-Path -Parent $PSScriptRoot
$oracle = Join-Path $ws '.oracle'
# 我们的 exe 放**程序根**(不是 bin\):布局扁平化之后程序根 == exe 目录,
# 而 C# 版的 exe 在 .oracle\bin\ 下、会上溯到 .oracle —— 放根下两边才是同一个根。
$rustExe = Join-Path $oracle 'narms.exe'
if (-not (Test-Path $oracle)) { throw "对照环境不存在:$oracle" }
if ($Rebuild) {
. (Join-Path $ws 'dev.ps1')
Push-Location $ws
# PowerShell 5.1 下 native 命令往 stderr 写进度行会被当成错误记录;
# 配上 ErrorActionPreference='Stop' 就升级成终止性异常。编译期间先放宽,靠退出码判断。
$prevEap = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
if ($Toolchain) { & cargo "+$Toolchain" build --offline 2>&1 | Write-Host }
else { & cargo build --offline 2>&1 | Write-Host }
$buildCode = $LASTEXITCODE
}
finally {
$ErrorActionPreference = $prevEap
Pop-Location
}
if ($buildCode -ne 0) { throw "cargo build 失败 (exit $buildCode)" }
}
# 注意:拷的是 **debug** 产物。这个坑害过一次 ——
# 我改了代码、重编了 release 和 dist,却忘了 debug 还是旧的,
# 于是 oracle 一直在拿旧二进制比,报「逐字节一致」但其实是假的。
Copy-Item (Join-Path $ws 'target\debug\nfarms.exe') $rustExe -Force
# 用「干净环境」跑一条命令:去掉 dev.ps1 留下的 RUSTUP_TOOLCHAIN,
# 以及它前置到 PATH 的工具链目录(工具链自带一份 rustc / gcc)。
#
# 为什么需要:验收脚本经常是在 source 过 dev.ps1 的 shell 里被调用的,
# 而 dev.ps1 把 `...\stable-x86_64-pc-windows-gnu\bin` 塞到了 PATH 最前面 ——
# 于是 --versions 里 `rustc --version` 拿到的是工具链那份的版本号,
# 和「干净 shell」里生成的基准差一个小版本。那不是移植回归,是**测试环境的差异**。
# 只影响子进程,跑完立刻恢复。
function Invoke-CleanEnv {
param([scriptblock]$Body)
$savedToolchain = $env:RUSTUP_TOOLCHAIN
$savedPath = $env:PATH
try {
Remove-Item Env:RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue
$env:PATH = (($env:PATH -split ';') |
Where-Object { $_ -and $_ -notmatch 'windows-gnu|self-contained' }) -join ';'
& $Body
}
finally {
if ($null -eq $savedToolchain) {
Remove-Item Env:RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue
}
else {
$env:RUSTUP_TOOLCHAIN = $savedToolchain
}
$env:PATH = $savedPath
}
}
$sample = Join-Path $oracle 'tests\weather-sample.json'
# Machine = $true 表示「基准与这台机器上装了什么有关」(工具版本、全局 npm 包)。
# 这类基准会因为**环境变了**而失效 —— 比如 rustc 自己升了个小版本,
# 那不是移植回归。脚本会明确提示用 -RegenGoldens 重新生成,别去改 Rust 代码。
$cases = @(
# **有意与原版不同**:用户要求把 ak\ 并进 logs\、live\ 并进 libs\,
# 所以顶层约定目录从 6 个变成 4 个 —— 这条输出**必然**和 C# 原版不一样。
# 基准文件代表原版真值,**不重生成**(抹掉它就等于把差异藏起来)。
@{ Name = 'layout-test'; Args = @('--layout-test'); Golden = 'golden-layout-test.txt'; Machine = $false
Diverges = '顶层目录由 6 个改为 4 个:bak\ 并进 logs\、live\ 并进 libs\(用户要求)' }
@{ Name = 'weather-parse'; Args = @('--weather-parse', $sample); Golden = 'golden-weather-parse.txt'; Machine = $false }
# 拓扑验证器的**离线**自检:不发任何网络请求,输出完全确定 —— 可以逐字节比。
# (带网络那一版含耗时,天然不可比,所以基准只取 --offline。)
@{ Name = 'topo-test'; Args = @('--topo-test', '--offline'); Golden = 'golden-topo-test.txt'; Machine = $false }
# 环境工具检测:同时验证了最难对齐的一环 —— 子进程输出按字节读 + 智能解码 + 噪音过滤。
@{ Name = 'versions'; Args = @('--versions'); Golden = 'golden-versions.txt'; Machine = $true }
@{ Name = 'npm-apps'; Args = @('--npm-apps'); Golden = 'golden-npm-apps.txt'; Machine = $true }
)
# -RegenGoldens:用 **C# 原版**重跑一遍机器相关的那几条,覆盖基准。
# 只有确认「是机器变了,不是代码坏了」之后才该用它。
if ($RegenGoldens) {
$oracleExe = Join-Path $oracle 'bin\NFarmsTools.exe'
if (-not (Test-Path $oracleExe)) { throw "缺少 C# 原版:$oracleExe" }
foreach ($c in ($cases | Where-Object { $_.Machine })) {
$goldenPath = Join-Path $oracle $c.Golden
$quoted = ($c.Args | ForEach-Object { '"{0}"' -f $_ }) -join ' '
Invoke-CleanEnv { cmd /c ('"{0}" {1} > "{2}" 2>&1' -f $oracleExe, $quoted, $goldenPath) | Out-Null }
Write-Host ("[REGEN] {0,-14} 基准已用 C# 原版重新生成" -f $c.Name) -ForegroundColor Yellow
}
Write-Host ''
}
$failed = 0
$diverged = 0
$machineDrift = 0
foreach ($c in $cases) {
$goldenPath = Join-Path $oracle $c.Golden
$rustPath = Join-Path $oracle ("rust-{0}.txt" -f $c.Name)
if (-not (Test-Path $goldenPath)) { throw "缺少基准文件 $goldenPath" }
$quoted = ($c.Args | ForEach-Object { '"{0}"' -f $_ }) -join ' '
if ($c.Machine) {
# 机器相关的那几条要**清掉调用者的环境干扰**再跑:dev.ps1 会把 GNU 工具链的
# bin 前置到 PATH,于是 `rustc` 解析到工具链自带的那一份(和系统默认的版本
# 不一样),--versions 就会和基准差一个版本号 —— 那是**测试环境**的差异,
# 不是移植回归。基准也是在干净环境下生成的,两边一致才可比。
Invoke-CleanEnv { cmd /c ('"{0}" {1} > "{2}" 2>&1' -f $rustExe, $quoted, $rustPath) | Out-Null }
}
else {
cmd /c ('"{0}" {1} > "{2}" 2>&1' -f $rustExe, $quoted, $rustPath) | Out-Null
}
$same = (Get-FileHash $goldenPath).Hash -eq (Get-FileHash $rustPath).Hash
if ($same) {
Write-Host ("[OK] {0,-14} 与 C# 原版输出逐字节一致" -f $c.Name) -ForegroundColor Green
}
elseif ($c.Diverges) {
# **有意的分歧**,不是回归。
#
# 这类条目是「用户明确要求改掉原版行为」造成的:两边输出**必然**不同,
# 而且**不该**去重生成基准 —— 基准代表原版的真值,抹掉它就等于把
# 「我们和原版不一样」这件事藏起来了。所以这里单独报一行,不计入失败。
#
# 判断依据也写出来,方便复核:两边的差异行会打到下面。
Write-Host ("[分歧] {0,-14} 有意与原版不同" -f $c.Name) -ForegroundColor Yellow
Write-Host (" 原因:{0}" -f $c.Diverges) -ForegroundColor Yellow
$diff = Compare-Object (Get-Content $goldenPath) (Get-Content $rustPath)
$diff | Select-Object -First 8 | ForEach-Object {
$mark = if ($_.SideIndicator -eq '<=') { '原版' } else { '本版' }
Write-Host (" {0} {1}" -f $mark, $_.InputObject.Trim()) -ForegroundColor DarkGray
}
$diverged++
}
else {
$failed++
if ($c.Machine) { $machineDrift++ }
Write-Host ("[FAIL] {0,-14} 输出不一致" -f $c.Name) -ForegroundColor Red
Write-Host (" 基准: {0}" -f $goldenPath)
Write-Host (" 新版: {0}" -f $rustPath)
$a = Get-Content $goldenPath -Encoding UTF8
$b = Get-Content $rustPath -Encoding UTF8
Compare-Object $a $b |
ForEach-Object { Write-Host (" {0} {1}" -f $_.SideIndicator, $_.InputObject) }
if ($c.Machine) {
Write-Host " ↑ 这一条与**本机装了什么**有关。先确认是环境变了(比如某个工具升级了)," -ForegroundColor Yellow
Write-Host " 再跑 .\scripts\verify-oracle.ps1 -RegenGoldens 重新生成基准。" -ForegroundColor Yellow
}
}
}
Write-Host ''
if ($failed -eq 0) {
Write-Host "验收通过:$($cases.Count)/$($cases.Count) 条命令与 C# 原版逐字节一致。" -ForegroundColor Green
exit 0
}
if ($machineDrift -eq $failed) {
Write-Host "验收失败:$failed/$($cases.Count) 条不一致 —— 但**全部是机器相关的那几条**," -ForegroundColor Yellow
Write-Host "多半是本机工具版本变了,不是移植回归。确认后用 -RegenGoldens 重新生成基准。" -ForegroundColor Yellow
exit 1
}
Write-Host "验收失败:$failed/$($cases.Count) 条命令输出不一致。" -ForegroundColor Red
exit 1