解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
12.58 KiB
X
# 全量单元测试并行跑测脚本
# 构建一次后,把全部测试宿主(4 个工程 × 目标框架,共 10 个)并行跑完,输出汇总表。
# Windows 上 testhost 偶发跑完不退出,脚本带超时兜底(按 PID 递归终止进程树)。
# 仅支持 Windows(Compat-net462 宿主需要 .NET Framework);Windows PowerShell 5.1 与 PowerShell 7(pwsh)均可运行。
#
# 用法:
#   pwsh -File Tools/Scripts/run-tests.ps1                      # 构建 Release 后并行跑全部宿主
#   pwsh -File Tools/Scripts/run-tests.ps1 -Configuration Debug # 指定构建配置
#   pwsh -File Tools/Scripts/run-tests.ps1 -SkipBuild           # 跳过构建(要求各宿主产物已存在)
#   pwsh -File Tools/Scripts/run-tests.ps1 -Only "Core-*"       # 只跑名称匹配的宿主(正则)
#   pwsh -File Tools/Scripts/run-tests.ps1 -TimeoutSec 1200     # 单宿主超时秒数(默认 900)
#   pwsh -File Tools/Scripts/run-tests.ps1 -ThrottleLimit 5     # 限制同时运行的宿主数(默认 0=不限)
#   pwsh -File Tools/Scripts/run-tests.ps1 -Repeat 10           # 稳定性验收:连续跑 10 轮,任一轮失败即退出码非零
#   pwsh -File Tools/Scripts/run-tests.ps1 -List                # 列出全部宿主后退出
#
# 结果:TestResults\<时间戳>\<宿主>.trx(用例明细,可与 CI 矩阵产物对照)+ <宿主>.log(控制台输出)
param(
    [ValidateSet("Debug", "Release")]
    [String]$Configuration = "Release",
    [Switch]$SkipBuild,
    [String]$Only = "",
    [Int32]$TimeoutSec = 900,
    [Int32]$ThrottleLimit = 0,
    [Int32]$Repeat = 1,
    [Switch]$List
)

$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# 脚本位于 Tools\Scripts,仓库根目录在其上两级
$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)

# 异常消息必须为英文(全球化约束)
& (Join-Path $PSScriptRoot "check-english-exceptions.ps1")
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

# 多语言词表必须键集与占位符一致
& (Join-Path $PSScriptRoot "check-locales.ps1")
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

# 宿主表:名称与 .github/workflows/test.yml 的矩阵项一致,便于与 CI 结果对照
$hosts = @(
    [pscustomobject]@{ Name = "Core-net8.0";          Project = "XUnitTest.Core\XUnitTest.Core.csproj";                       Framework = "net8.0";          OutDir = "Bin\UnitTest\net8.0" }
    [pscustomobject]@{ Name = "Core-net8.0-windows";  Project = "XUnitTest.Core\XUnitTest.Core.csproj";                       Framework = "net8.0-windows";  OutDir = "Bin\UnitTest\net8.0-windows" }
    [pscustomobject]@{ Name = "Core-net9.0";          Project = "XUnitTest.Core\XUnitTest.Core.csproj";                       Framework = "net9.0";          OutDir = "Bin\UnitTest\net9.0" }
    [pscustomobject]@{ Name = "Core-net10.0";         Project = "XUnitTest.Core\XUnitTest.Core.csproj";                       Framework = "net10.0";         OutDir = "Bin\UnitTest\net10.0" }
    [pscustomobject]@{ Name = "Core-net10.0-windows"; Project = "XUnitTest.Core\XUnitTest.Core.csproj";                       Framework = "net10.0-windows"; OutDir = "Bin\UnitTest\net10.0-windows" }
    [pscustomobject]@{ Name = "Net6-net6.0";          Project = "XUnitTest.Net6\XUnitTest.Net6.csproj";                       Framework = "net6.0";          OutDir = "Bin\UnitTest6\net6.0" }
    [pscustomobject]@{ Name = "Net6-net6.0-windows";  Project = "XUnitTest.Net6\XUnitTest.Net6.csproj";                       Framework = "net6.0-windows";  OutDir = "Bin\UnitTest6\net6.0-windows" }
    [pscustomobject]@{ Name = "Compat-net462";        Project = "XUnitTest.Compat\XUnitTest.Compat.csproj";                   Framework = "net462";          OutDir = "Bin\UnitTestCompat\net462" }
    [pscustomobject]@{ Name = "Compat-net8.0";        Project = "XUnitTest.Compat\XUnitTest.Compat.csproj";                   Framework = "net8.0";          OutDir = "Bin\UnitTestCompat\net8.0" }
    [pscustomobject]@{ Name = "SourceGenerator";      Project = "XUnitTest.SourceGenerator\XUnitTest.SourceGenerator.csproj"; Framework = "net10.0";         OutDir = "Bin\UnitTestSG\net10.0" }
)

if ($List)
{
    $hosts | Select-Object Name, Framework, OutDir | Format-Table -AutoSize
    return
}

if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { throw "未找到 dotnet 命令" }
if ($env:OS -ne "Windows_NT") { Write-Warning "当前非 Windows 环境,Compat-net462 宿主不可用,可用 -Only 过滤排除" }

if ($Only)
{
    $hosts = @($hosts | Where-Object { $_.Name -match $Only })
    if ($hosts.Count -eq 0) { throw "-Only '$Only' 未匹配到宿主,可用 -List 查看宿主名" }
}

# 阶段一:构建一次(逐工程;工程内多 TFM 由 MSBuild 并行)。
# 不做并发构建:同工程多 TFM 并发 restore/build 会竞争 obj 下的共享中间文件。
if (-not $SkipBuild)
{
    Write-Host "== 构建($Configuration)=="
    foreach ($project in @($hosts | Select-Object -ExpandProperty Project -Unique))
    {
        Write-Host "-> $project"
        & dotnet build (Join-Path $root $project) -c $Configuration -m --nologo -v q
        if ($LASTEXITCODE -ne 0) { throw "构建失败(退出码 $LASTEXITCODE):$project" }
    }
}
else
{
    foreach ($h in $hosts)
    {
        if (-not (Test-Path (Join-Path $root $h.OutDir))) { throw "-SkipBuild 但产物目录不存在:$($h.OutDir),请先去掉 -SkipBuild 构建一次" }
    }
}

# 递归终止进程树:宿主进程为 cmd → dotnet test → testhost(.dll/.exe),必须沿子进程递归清理
function Stop-ProcessTree([Int32]$ProcessId)
{
    foreach ($child in @(Get-CimInstance Win32_Process -Filter "ParentProcessId=$ProcessId" -ErrorAction SilentlyContinue))
    {
        Stop-ProcessTree $child.ProcessId
    }
    Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
}

function Start-HostRun($h)
{
    $log = Join-Path $resultDir "$($h.Name).log"

    # 经 cmd 把输出重定向到文件(.NET 启动方式没有“重定向到文件”能力,用管道又有缓冲区死锁风险),
    # 并用 [Process]::Start 启动:Windows PowerShell 5.1 的 Start-Process -PassThru 取不到 ExitCode,
    # 会导致按退出码判定宿主成败失效。chcp 65001 让子进程按 UTF-8 输出,日志中文可读。
    $inner = 'chcp 65001>nul & dotnet test "' + (Join-Path $root $h.Project) + '" -f "' + $h.Framework + '" -c "' + $Configuration + '"' +
    ' --no-build --no-restore --nologo --logger "trx;LogFileName=' + $h.Name + '.trx" --results-directory "' + $resultDir + '"' +
    ' 1> "' + $log + '" 2> "' + $log + '.err"'

    $psi = New-Object System.Diagnostics.ProcessStartInfo
    $psi.FileName = if ($env:ComSpec) { $env:ComSpec } else { 'cmd.exe' }
    $psi.Arguments = '/d /s /c "' + $inner + '"'
    $psi.UseShellExecute = $false
    $psi.CreateNoWindow = $true
    $psi.WorkingDirectory = $root
    $proc = [System.Diagnostics.Process]::Start($psi)

    [pscustomobject]@{
        Host      = $h
        Process   = $proc
        Log       = $log
        StartTime = Get-Date
        Done      = $false
        ExitCode  = $null
        TimedOut  = $false
        Duration  = [TimeSpan]::Zero
    }
}

# 解析 dotnet test 写入的 trx 结果;缺文件(如超时被杀)返回 $null
function Get-TrxCounters([String]$Path)
{
    if (-not (Test-Path $Path)) { return $null }
    try
    {
        $xml = New-Object System.Xml.XmlDocument
        $xml.Load($Path)
        $c = $xml.TestRun.ResultSummary.Counters
        if (-not $c) { return $null }
        # xunit 跳过用例记为 outcome=NotExecuted,且不填 Counters@notExecuted,需按结果节点单独统计
        $skipped = 0
        foreach ($r in $xml.GetElementsByTagName("UnitTestResult"))
        {
            if ($r.outcome -eq "NotExecuted") { $skipped++ }
        }
        [pscustomobject]@{
            Total   = [Int32]$c.total
            Passed  = [Int32]$c.passed
            Failed  = [Int32]$c.failed
            Skipped = $skipped
        }
    }
    catch { $null }
}

# 单轮跑测:并行执行全部宿主,返回失败宿主数。每轮生成独立的结果目录
function Invoke-TestRound([Int32]$RoundIndex)
{
    $stamp = Get-Date -Format "yyyyMMdd-HHmmss"
    if ($Repeat -gt 1) { $stamp += "-r$RoundIndex" }
    $resultDir = Join-Path $root "TestResults\$stamp"
    New-Item -ItemType Directory -Force -Path $resultDir | Out-Null

# 阶段二:并行跑测(窗口调度:默认全部并发,-ThrottleLimit 限流)
$limit = if ($ThrottleLimit -gt 0) { [Math]::Min($ThrottleLimit, $hosts.Count) } else { $hosts.Count }
Write-Host "== 并行跑测(宿主 $($hosts.Count) 个,并发上限 $limit,单宿主超时 ${TimeoutSec}s)=="
Write-Host "结果目录:$resultDir"

$scriptStart = Get-Date
$runs = New-Object System.Collections.Generic.List[object]
$pending = New-Object System.Collections.Queue
foreach ($h in $hosts) { $pending.Enqueue($h) }

while ($pending.Count -gt 0 -or @($runs | Where-Object { -not $_.Done }).Count -gt 0)
{
    while ($pending.Count -gt 0 -and @($runs | Where-Object { -not $_.Done }).Count -lt $limit)
    {
        $h = $pending.Dequeue()
        $run = Start-HostRun $h
        $runs.Add($run)
        Write-Host ("  启动 {0}({1},PID {2})" -f $h.Name, $h.Framework, $run.Process.Id)
    }

    $waiting = $false
    foreach ($run in @($runs | Where-Object { -not $_.Done }))
    {
        if ($run.Process.HasExited)
        {
            $run.Duration = (Get-Date) - $run.StartTime
            $run.ExitCode = $run.Process.ExitCode
            $run.Done = $true
            $run.Process.Dispose()
            $status = if ($run.ExitCode -eq 0) { "PASS" } else { "FAIL" }
            Write-Host ("  [{0}] {1},用时 {2:N1}s" -f $status, $run.Host.Name, $run.Duration.TotalSeconds)
        }
        elseif (((Get-Date) - $run.StartTime).TotalSeconds -gt $TimeoutSec)
        {
            Write-Warning ("  [{0}] 超时({1}s),终止进程树(PID {2})" -f $run.Host.Name, $TimeoutSec, $run.Process.Id)
            Stop-ProcessTree $run.Process.Id
            $run.Duration = (Get-Date) - $run.StartTime
            $run.TimedOut = $true
            $run.Done = $true
            try { $run.Process.Dispose() } catch { }
        }
        else { $waiting = $true }
    }

    if ($waiting) { Start-Sleep -Seconds 2 }
}

# 汇总
$wall = (Get-Date) - $scriptStart
Write-Host ""
Write-Host ("== 汇总({0},总墙钟 {1:N1}s)==" -f $Configuration, $wall.TotalSeconds)

$rows = New-Object System.Collections.Generic.List[object]
$failed = 0
foreach ($run in $runs)
{
    $verdict = if ($run.TimedOut) { "TIMEOUT" } elseif ($run.ExitCode -ne 0) { "FAIL" } else { "PASS" }
    if ($verdict -ne "PASS") { $failed++ }

    $total = "-"; $passed = "-"; $trxFailed = "-"; $skipped = "-"
    $counters = Get-TrxCounters (Join-Path $resultDir "$($run.Host.Name).trx")
    if ($counters)
    {
        $total = $counters.Total; $passed = $counters.Passed; $trxFailed = $counters.Failed; $skipped = $counters.Skipped
    }

    $rows.Add([pscustomobject]@{
        Host    = $run.Host.Name
        Result  = $verdict
        Total   = $total
        Passed  = $passed
        Failed  = $trxFailed
        Skipped = $skipped
        Seconds = [Math]::Round($run.Duration.TotalSeconds, 1)
    })
}
$rows | Format-Table -AutoSize | Out-Host

foreach ($run in $runs)
{
    $verdict = if ($run.TimedOut) { "TIMEOUT" } elseif ($run.ExitCode -ne 0) { "FAIL" } else { "PASS" }
    if ($verdict -ne "PASS")
    {
        Write-Host ""
        Write-Host ("---- {0}({1})控制台日志尾部:{2} ----" -f $run.Host.Name, $verdict, $run.Log)
        Get-Content $run.Log -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ }
    }
}

    if ($failed -gt 0)
    {
        Write-Warning "有 $failed 个宿主未通过,trx 与日志见 $resultDir"
    }
    else
    {
        Write-Host "全部 $($runs.Count) 个宿主通过,trx 与日志见 $resultDir"
    }

    return $failed
}

# 阶段三:轮次驱动。Repeat>1 为稳定性验收模式:任一轮出现失败,整体退出码非零
$roundsFailed = 0
for ($round = 1; $round -le $Repeat; $round++)
{
    if ($Repeat -gt 1) { Write-Host ""; Write-Host "######## 第 $round/$Repeat 轮 ########" }

    $roundFailed = Invoke-TestRound $round
    if ($roundFailed -gt 0) { $roundsFailed++ }
}

if ($Repeat -gt 1)
{
    Write-Host ""
    if ($roundsFailed -eq 0)
    {
        Write-Host ("== 稳定性验收通过:{0} 轮全部零失败 ==" -f $Repeat)
    }
    else
    {
        Write-Warning ("== 稳定性验收失败:{0} 轮中 {1} 轮出现失败 ==" -f $Repeat, $roundsFailed)
    }
}

if ($roundsFailed -gt 0) { exit 1 }
exit 0