param( [ValidateSet('T0', 'ALL')] [string]$Tier = 'T0', [string]$OutputDir = '' ) $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $manifestPath = Join-Path $PSScriptRoot 'night-run.manifest.json' $manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json if (-not $OutputDir) { $OutputDir = Join-Path ([System.IO.Path]::GetTempPath()) ('jiapuapp-night-run-' + (Get-Date -Format 'yyyyMMdd-HHmmss')) } New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null $inventory = @( Get-ChildItem -LiteralPath $PSScriptRoot -File -Recurse | Where-Object { $_.Extension -in '.ps1', '.js' -and $_.Name -ne 'night-run.ps1' } | ForEach-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') } | Sort-Object ) $powerShellCount = @($inventory | Where-Object { $_.EndsWith('.ps1') }).Count $nodeCount = @($inventory | Where-Object { $_.EndsWith('.js') }).Count $newExecutableTests = $inventory.Count - $manifest.baselineInventory.powershell - $manifest.baselineInventory.node if ($newExecutableTests -lt 0) { throw "夜跑 inventory 少于冻结基线:actual=$($inventory.Count) baseline=$($manifest.baselineInventory.powershell + $manifest.baselineInventory.node)" } $expectedBlocked = @{} foreach ($entry in @($manifest.expectedBlocked)) { $expectedBlocked[$entry.script] = [string]$entry.marker } $h5ChromeRuntimeTests = @{} foreach ($script in @($manifest.h5ChromeRuntimeTests)) { $h5ChromeRuntimeTests[[string]$script] = $true } if ($Tier -eq 'T0') { $selected = @($manifest.t0 | Sort-Object -Unique) } else { $selected = $inventory } $unknown = @($selected | Where-Object { $_ -notin $inventory }) if ($unknown.Count -gt 0) { throw "夜跑清单引用不存在的测试:$($unknown -join '、')" } function Test-H5ChromeRuntime { try { $pages = Invoke-RestMethod -Uri 'http://127.0.0.1:9222/json/list' -TimeoutSec 2 return @($pages | Where-Object { $_.type -eq 'page' -and $_.url -like 'http://localhost:5173*' }).Count -gt 0 } catch { return $false } } $needsH5ChromeRuntime = @($selected | Where-Object { $h5ChromeRuntimeTests.ContainsKey($_) }).Count -gt 0 $h5ChromeRuntimeReady = -not $needsH5ChromeRuntime -or (Test-H5ChromeRuntime) function Test-ExactLine { param([string]$Text, [string]$Marker) return [regex]::IsMatch($Text, "(?m)^$([regex]::Escape($Marker))`r?$") } function Invoke-ManagedTest { param([string]$RelativePath) $startedAt = Get-Date if ($h5ChromeRuntimeTests.ContainsKey($RelativePath) -and -not $h5ChromeRuntimeReady) { return [pscustomobject]@{ script = $RelativePath startedAt = $startedAt.ToString('o') finishedAt = (Get-Date).ToString('o') durationMs = 0 exitCode = -2 checkResult = 'INFRA_ERROR' expectedBlockedMarker = $null infraReason = 'H5_CHROME_RUNTIME_UNAVAILABLE' outputPath = $null errorPath = $null } } $absolutePath = Join-Path $root $RelativePath.Replace('/', '\') $outputPath = Join-Path $OutputDir (($RelativePath -replace '[\\/]', '__') + '.stdout.txt') $errorPath = Join-Path $OutputDir (($RelativePath -replace '[\\/]', '__') + '.stderr.txt') $fileName = if ($RelativePath.EndsWith('.ps1')) { 'powershell.exe' } else { 'node.exe' } $arguments = if ($RelativePath.EndsWith('.ps1')) { @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $absolutePath) } else { @($absolutePath) } $startInfo = [System.Diagnostics.ProcessStartInfo]::new() $startInfo.FileName = $fileName $startInfo.Arguments = (($arguments | ForEach-Object { '"' + ([string]$_).Replace('"', '\"') + '"' }) -join ' ') $startInfo.UseShellExecute = $false $startInfo.CreateNoWindow = $true $startInfo.RedirectStandardOutput = $true $startInfo.RedirectStandardError = $true $process = [System.Diagnostics.Process]::new() $process.StartInfo = $startInfo if (-not $process.Start()) { throw "无法启动夜跑子进程:$RelativePath" } $standardOutputTask = $process.StandardOutput.ReadToEndAsync() $standardErrorTask = $process.StandardError.ReadToEndAsync() $timedOut = -not $process.WaitForExit([int]$manifest.defaultTimeoutSeconds * 1000) if ($timedOut) { $process.Kill() $process.WaitForExit() } $exitCode = if ($timedOut) { -1 } else { [int]$process.ExitCode } $standardOutput = $standardOutputTask.GetAwaiter().GetResult() $standardError = $standardErrorTask.GetAwaiter().GetResult() [System.IO.File]::WriteAllText($outputPath, $standardOutput, [System.Text.UTF8Encoding]::new($false)) [System.IO.File]::WriteAllText($errorPath, $standardError, [System.Text.UTF8Encoding]::new($false)) $text = ($standardOutput + "`n" + $standardError).Trim() $marker = $expectedBlocked[$RelativePath] $result = if ($timedOut) { 'INFRA_ERROR' } elseif ($marker) { $passMarker = $marker -replace ' BLOCKED$', ' PASS' if ($exitCode -eq 0 -and (Test-ExactLine $text $passMarker)) { 'PASS' } elseif ($exitCode -ne 0 -and (Test-ExactLine $text $marker)) { 'EXPECTED_BLOCKED' } else { 'FAIL' } } elseif ($exitCode -eq 0) { 'PASS' } else { 'FAIL' } return [pscustomobject]@{ script = $RelativePath startedAt = $startedAt.ToString('o') finishedAt = (Get-Date).ToString('o') durationMs = [int]((Get-Date) - $startedAt).TotalMilliseconds exitCode = $exitCode checkResult = $result expectedBlockedMarker = $marker outputPath = $outputPath errorPath = $errorPath } } $records = @() foreach ($test in $selected) { $record = Invoke-ManagedTest $test $records += $record $checkpoint = [pscustomobject]@{ baselineInventoryTests = [int]$manifest.baselineInventory.powershell + [int]$manifest.baselineInventory.node newExecutableTests = $newExecutableTests inventoryTests = $inventory.Count selectedTests = $selected.Count records = $records } $checkpoint | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $OutputDir 'checkpoint.json') -Encoding UTF8 Write-Output ("{0} {1} exit={2}" -f $record.checkResult, $record.script, $record.exitCode) } $summary = [pscustomobject]@{ baselineInventoryTests = [int]$manifest.baselineInventory.powershell + [int]$manifest.baselineInventory.node newExecutableTests = $newExecutableTests inventoryTests = $inventory.Count scheduledTests = $selected.Count executedTests = $records.Count passTests = @($records | Where-Object checkResult -eq 'PASS').Count failTests = @($records | Where-Object checkResult -eq 'FAIL').Count expectedBlockedTests = @($records | Where-Object checkResult -eq 'EXPECTED_BLOCKED').Count infraErrorTests = @($records | Where-Object checkResult -eq 'INFRA_ERROR').Count timedOutTests = @($records | Where-Object { $_.exitCode -eq -1 }).Count notRunScheduledTests = 0 notRunTests = $inventory.Count - $selected.Count outputDir = $OutputDir } $summary | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $OutputDir 'summary.json') -Encoding UTF8 Write-Output ('NIGHT-RUN-SUMMARY ' + ($summary | ConvertTo-Json -Compress)) if ($summary.infraErrorTests -gt 0) { exit 4 } if ($summary.failTests -gt 0) { exit 2 } exit 0