[CmdletBinding()] param( [string]$BuildPreset = "windows-msvc-default", [string]$Configuration = "Debug", [string[]]$BuildTargets = @("PanoPainter", "pano_cli"), [string]$TestPreset = "desktop-fast", [string]$TestRegex = "", [switch]$Configure, [switch]$SkipBuild, [switch]$SkipTests, [string]$CMakeCommand = "", [string]$CTestCommand = "", [string]$LogDir = "out/logs/quiet-validation", [string]$IgnoreFilterFile = "", [string[]]$IgnorePattern = @(), [int]$FailureTailLines = 0 ) $ErrorActionPreference = "Stop" function Resolve-CMakeCommand { param([string]$Requested) if ($Requested.Length -gt 0) { return $Requested } $vsCmake = "C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" if (Test-Path -LiteralPath $vsCmake) { return $vsCmake } return "cmake" } function Resolve-CTestCommand { param( [string]$Requested, [string]$ResolvedCMake ) if ($Requested.Length -gt 0) { return $Requested } if ($ResolvedCMake.EndsWith("cmake.exe", [System.StringComparison]::OrdinalIgnoreCase)) { $candidate = Join-Path -Path (Split-Path -Parent $ResolvedCMake) -ChildPath "ctest.exe" if (Test-Path -LiteralPath $candidate) { return $candidate } } return "ctest" } function Read-IgnorePatterns { param( [string]$FilterFile, [string[]]$InlinePatterns ) $patterns = @() if ($FilterFile.Length -eq 0) { $defaultFile = Join-Path -Path $PSScriptRoot -ChildPath "quiet-validation-ignore.txt" if (Test-Path -LiteralPath $defaultFile) { $FilterFile = $defaultFile } } if ($FilterFile.Length -gt 0 -and (Test-Path -LiteralPath $FilterFile)) { $patterns += Get-Content -LiteralPath $FilterFile | Where-Object { $_.Trim().Length -gt 0 -and -not $_.TrimStart().StartsWith("#") } } $patterns += $InlinePatterns return @($patterns | Where-Object { $_ -and $_.Length -gt 0 }) } function Expand-ArgumentList { param([string[]]$Values) $expanded = @() foreach ($value in $Values) { if ($null -eq $value) { continue } $expanded += $value -split "[,\s]+" | Where-Object { $_.Length -gt 0 } } return @($expanded) } function Test-IgnoredLine { param( [string]$Line, [string[]]$Patterns ) foreach ($pattern in $Patterns) { if ($Line -match $pattern) { return $true } } return $false } function Measure-Log { param( [string]$Path, [string[]]$IgnorePatterns ) $errorPattern = "(?i)(:\s*(fatal\s+)?error\s+[A-Z0-9]+:|^LINK\s*:\s*fatal error|^CMake Error|Errors while running CTest|Unable to find executable|\*\*\*Failed)" $warningPattern = "(?i)(:\s*warning\s+[A-Z0-9]+:|^LINK\s*:\s*warning\s+[A-Z0-9]+:|warning:)" $ctestSummaryPattern = "(\d+)% tests passed, (\d+) tests failed out of (\d+)" $lineCount = 0 $rawErrors = 0 $rawWarnings = 0 $visibleErrors = 0 $visibleWarnings = 0 $ignoredErrors = 0 $ignoredWarnings = 0 $testsFailed = $null $testsTotal = $null if (Test-Path -LiteralPath $Path) { foreach ($line in Get-Content -LiteralPath $Path) { ++$lineCount $ignored = Test-IgnoredLine -Line $line -Patterns $IgnorePatterns if ($line -match $ctestSummaryPattern) { $testsFailed = [int]$Matches[2] $testsTotal = [int]$Matches[3] } if ($line -match $errorPattern) { ++$rawErrors if ($ignored) { ++$ignoredErrors } else { ++$visibleErrors } } if ($line -match $warningPattern) { ++$rawWarnings if ($ignored) { ++$ignoredWarnings } else { ++$visibleWarnings } } } } return [ordered]@{ lineCount = $lineCount errors = $visibleErrors warnings = $visibleWarnings rawErrors = $rawErrors rawWarnings = $rawWarnings ignoredErrors = $ignoredErrors ignoredWarnings = $ignoredWarnings testsFailed = $testsFailed testsTotal = $testsTotal } } function Invoke-QuietStep { param( [string]$Name, [string]$Command, [string[]]$Arguments, [string]$LogPath, [string[]]$IgnorePatterns, [int]$FailureTailLines ) $started = Get-Date $exitCode = 0 try { & $Command @Arguments *> $LogPath $exitCode = $LASTEXITCODE if ($null -eq $exitCode) { $exitCode = 0 } } catch { $_ | Out-File -LiteralPath $LogPath -Append -Encoding utf8 $exitCode = 1 } $elapsed = [int]((Get-Date) - $started).TotalMilliseconds $summary = Measure-Log -Path $LogPath -IgnorePatterns $IgnorePatterns $result = [ordered]@{ name = $Name exitCode = $exitCode elapsedMs = $elapsed log = $LogPath summary = $summary } if ($exitCode -ne 0 -and $FailureTailLines -gt 0 -and (Test-Path -LiteralPath $LogPath)) { $result.failureTail = @(Get-Content -LiteralPath $LogPath -Tail $FailureTailLines) } return $result } $resolvedCMake = Resolve-CMakeCommand -Requested $CMakeCommand $resolvedCTest = Resolve-CTestCommand -Requested $CTestCommand -ResolvedCMake $resolvedCMake $BuildTargets = @(Expand-ArgumentList -Values $BuildTargets) $IgnorePattern = @(Expand-ArgumentList -Values $IgnorePattern) $ignorePatterns = Read-IgnorePatterns -FilterFile $IgnoreFilterFile -InlinePatterns $IgnorePattern New-Item -ItemType Directory -Force -Path $LogDir | Out-Null $runId = Get-Date -Format "yyyyMMdd-HHmmss" $started = Get-Date $results = @() $overallExitCode = 0 if ($Configure) { $log = Join-Path -Path $LogDir -ChildPath "$runId-configure-$BuildPreset.log" $result = Invoke-QuietStep ` -Name "configure:$BuildPreset" ` -Command $resolvedCMake ` -Arguments @("--preset", $BuildPreset) ` -LogPath $log ` -IgnorePatterns $ignorePatterns ` -FailureTailLines $FailureTailLines $results += $result if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) { $overallExitCode = $result.exitCode } } if (-not $SkipBuild) { $targets = @($BuildTargets | Where-Object { $_ -and $_.Length -gt 0 }) if ($targets.Count -gt 0) { $safeTargets = ($targets -join "_") -replace "[^A-Za-z0-9_.-]", "_" $log = Join-Path -Path $LogDir -ChildPath "$runId-build-$BuildPreset-$Configuration-$safeTargets.log" $buildArgs = @("--build", "--preset", $BuildPreset, "--config", $Configuration, "--target") + $targets $result = Invoke-QuietStep ` -Name ("build:{0}:{1}" -f $BuildPreset, $Configuration) ` -Command $resolvedCMake ` -Arguments $buildArgs ` -LogPath $log ` -IgnorePatterns $ignorePatterns ` -FailureTailLines $FailureTailLines $result.targets = $targets $results += $result if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) { $overallExitCode = $result.exitCode } } } if (-not $SkipTests) { $safeRegex = if ($TestRegex.Length -gt 0) { ($TestRegex -replace "[^A-Za-z0-9_.-]", "_") } else { "all" } $log = Join-Path -Path $LogDir -ChildPath "$runId-test-$TestPreset-$Configuration-$safeRegex.log" $testArgs = @("--preset", $TestPreset, "--build-config", $Configuration, "--output-on-failure") if ($TestRegex.Length -gt 0) { $testArgs += @("-R", $TestRegex) } $result = Invoke-QuietStep ` -Name ("test:{0}:{1}" -f $TestPreset, $Configuration) ` -Command $resolvedCTest ` -Arguments $testArgs ` -LogPath $log ` -IgnorePatterns $ignorePatterns ` -FailureTailLines $FailureTailLines if ($TestRegex.Length -gt 0) { $result.testRegex = $TestRegex } $results += $result if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) { $overallExitCode = $result.exitCode } } $elapsed = [int]((Get-Date) - $started).TotalMilliseconds $summaryPath = Join-Path -Path $LogDir -ChildPath "$runId-summary.json" $payload = [ordered]@{ command = "quiet-validate" exitCode = $overallExitCode elapsedMs = $elapsed buildPreset = $BuildPreset configuration = $Configuration testPreset = $TestPreset logDir = $LogDir summary = $summaryPath ignoreFilterFile = $IgnoreFilterFile ignorePatternCount = $ignorePatterns.Count results = $results } $payload | ConvertTo-Json -Depth 8 | Out-File -LiteralPath $summaryPath -Encoding utf8 $payload | ConvertTo-Json -Compress -Depth 8 exit $overallExitCode