543 lines
20 KiB
PowerShell
543 lines
20 KiB
PowerShell
#!/usr/bin/env pwsh
|
|
# quality.ps1 — Local quality gate for GDScript projects (Windows).
|
|
# Usage: pwsh ./tools/quality.ps1 [-Changed] [--changed]
|
|
param([switch]$Changed)
|
|
if ($args -contains '--changed') { $Changed = $true }
|
|
|
|
$ROOT = Split-Path -Parent $PSScriptRoot
|
|
$script:PreviousLocation = Get-Location
|
|
Set-Location $ROOT
|
|
|
|
$LOG = "logs/quality/latest"
|
|
New-Item -ItemType Directory -Path $LOG -Force | Out-Null
|
|
$GODOT_PROFILE = Join-Path $ROOT "logs/quality/godot_profile"
|
|
New-Item -ItemType Directory -Path $GODOT_PROFILE -Force | Out-Null
|
|
|
|
$OVERALL = $false
|
|
$ERRORS = @()
|
|
$FIXES = @()
|
|
$OWNED_GDSCRIPT_ROOTS = @("player", "simulation", "tests", "tools", "world")
|
|
$CROSS_PLATFORM_PLUGIN_FILES = @(
|
|
"addons/terrain_3d/terrain.gdextension",
|
|
"addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug",
|
|
"addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release",
|
|
"addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll",
|
|
"addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll"
|
|
)
|
|
$REQUIRED_GODOT_SERIES = "4.7"
|
|
|
|
function Exit-Quality {
|
|
param([int]$ExitCode)
|
|
Set-Location $script:PreviousLocation
|
|
exit $ExitCode
|
|
}
|
|
|
|
# -- tool detection -----------------------------------------------------------
|
|
function ConvertTo-ConsoleGodotPath($Path) {
|
|
if (-not $Path) { return $null }
|
|
$separator = [IO.Path]::DirectorySeparatorChar
|
|
$normalized = $Path.Replace([char]47, $separator).Replace([char]92, $separator).Trim('"')
|
|
if ($normalized.EndsWith(".exe")) {
|
|
$consolePath = $normalized -replace '\.exe$', '_console.exe'
|
|
if (Test-Path $consolePath) { return $consolePath }
|
|
}
|
|
if (Test-Path $normalized) { return $normalized }
|
|
return $null
|
|
}
|
|
|
|
function Find-ProjectGodot {
|
|
$metadata = Join-Path $ROOT ".godot/editor/project_metadata.cfg"
|
|
if (-not (Test-Path $metadata)) { return $null }
|
|
$line = Select-String -Path $metadata -Pattern '^executable_path=' | Select-Object -First 1
|
|
if (-not $line) { return $null }
|
|
$rawPath = ($line.Line -replace '^executable_path=', '').Trim().Trim('"')
|
|
return ConvertTo-ConsoleGodotPath $rawPath
|
|
}
|
|
|
|
function Find-Godot {
|
|
if ($env:GODOT_BIN) { return (ConvertTo-ConsoleGodotPath $env:GODOT_BIN) }
|
|
$projectGodot = Find-ProjectGodot
|
|
if ($projectGodot) { return $projectGodot }
|
|
foreach ($name in @("godot", "godot4")) {
|
|
$command = Get-Command $name -ErrorAction SilentlyContinue
|
|
if (-not $command) { continue }
|
|
$candidate = ConvertTo-ConsoleGodotPath $command.Source
|
|
if ($candidate) { return $candidate }
|
|
}
|
|
|
|
$common = @()
|
|
if ($env:ProgramFiles) { $common += Join-Path $env:ProgramFiles "Godot/godot.exe" }
|
|
if (${env:ProgramFiles(x86)}) { $common += Join-Path ${env:ProgramFiles(x86)} "Godot/godot.exe" }
|
|
if ($env:LOCALAPPDATA) {
|
|
$common += Join-Path $env:LOCALAPPDATA "Godot/godot.exe"
|
|
$common += Join-Path $env:LOCALAPPDATA "Programs/Godot/godot.exe"
|
|
}
|
|
if ($env:USERPROFILE) { $common += Join-Path $env:USERPROFILE "scoop/apps/godot/current/godot.exe" }
|
|
foreach ($path in $common) {
|
|
$candidate = ConvertTo-ConsoleGodotPath $path
|
|
if ($candidate) { return $candidate }
|
|
}
|
|
|
|
$searchRoots = @()
|
|
if ($env:ProgramFiles) { $searchRoots += Join-Path $env:ProgramFiles "Godot" }
|
|
if (${env:ProgramFiles(x86)}) { $searchRoots += Join-Path ${env:ProgramFiles(x86)} "Godot" }
|
|
if ($env:LOCALAPPDATA) {
|
|
$searchRoots += Join-Path $env:LOCALAPPDATA "Godot"
|
|
$searchRoots += Join-Path $env:LOCALAPPDATA "Programs/Godot"
|
|
}
|
|
foreach ($searchRoot in $searchRoots) {
|
|
$bundled = Get-ChildItem $searchRoot -Filter "Godot*_console.exe" -File -ErrorAction SilentlyContinue |
|
|
Sort-Object Name -Descending |
|
|
Select-Object -First 1
|
|
if ($bundled) { return $bundled.FullName }
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Resolve-GdTool {
|
|
param(
|
|
[string]$ExecutableName,
|
|
[string]$ModuleName
|
|
)
|
|
$localTool = Join-Path $ROOT ".venv/Scripts/$ExecutableName.exe"
|
|
if (Test-Path $localTool) {
|
|
return [pscustomobject]@{ Command = $localTool; Prefix = @() }
|
|
}
|
|
|
|
$directTool = Get-Command $ExecutableName -ErrorAction SilentlyContinue
|
|
if ($directTool) {
|
|
return [pscustomobject]@{ Command = $directTool.Source; Prefix = @() }
|
|
}
|
|
|
|
$localPython = Join-Path $ROOT ".venv/Scripts/python.exe"
|
|
$pythonCandidates = @(
|
|
[pscustomobject]@{ Command = $localPython; Prefix = @() },
|
|
[pscustomobject]@{ Command = "python"; Prefix = @() },
|
|
[pscustomobject]@{ Command = "python3"; Prefix = @() },
|
|
[pscustomobject]@{ Command = "py"; Prefix = @("-3") }
|
|
)
|
|
foreach ($candidate in $pythonCandidates) {
|
|
$isLocalPython = $candidate.Command -eq $localPython
|
|
if ($isLocalPython) {
|
|
if (-not (Test-Path $candidate.Command)) { continue }
|
|
$command = $candidate.Command
|
|
} else {
|
|
$resolved = Get-Command $candidate.Command -ErrorAction SilentlyContinue
|
|
if (-not $resolved) { continue }
|
|
$command = $resolved.Source
|
|
}
|
|
|
|
$prefix = @($candidate.Prefix)
|
|
$null = @(& $command @prefix -m $ModuleName --help 2>&1)
|
|
if ($LASTEXITCODE -eq 0) {
|
|
return [pscustomobject]@{ Command = $command; Prefix = $prefix + @("-m", $ModuleName) }
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
$script:GdFormatRunner = $null
|
|
$script:GdLintRunner = $null
|
|
|
|
function Test-GdFormat {
|
|
$script:GdFormatRunner = Resolve-GdTool "gdformat" "gdtoolkit.formatter"
|
|
return $null -ne $script:GdFormatRunner
|
|
}
|
|
|
|
function Test-GdLint {
|
|
$script:GdLintRunner = Resolve-GdTool "gdlint" "gdtoolkit.linter"
|
|
return $null -ne $script:GdLintRunner
|
|
}
|
|
|
|
$script:GdToolExitCode = 0
|
|
|
|
function Invoke-GdFormat {
|
|
$command = $script:GdFormatRunner.Command
|
|
$prefix = @($script:GdFormatRunner.Prefix)
|
|
& $command @prefix @args 2>&1
|
|
$script:GdToolExitCode = $LASTEXITCODE
|
|
}
|
|
|
|
function Invoke-GdLint {
|
|
$command = $script:GdLintRunner.Command
|
|
$prefix = @($script:GdLintRunner.Prefix)
|
|
& $command @prefix @args 2>&1
|
|
$script:GdToolExitCode = $LASTEXITCODE
|
|
}
|
|
|
|
function Get-ChangedGd {
|
|
$changed = @(git diff --name-only HEAD -- '*.gd' 2>$null)
|
|
$untracked = @(git ls-files --others --exclude-standard -- '*.gd' 2>$null)
|
|
return @((@($changed) + @($untracked)) | Where-Object { $_ -ne '' } | Sort-Object -Unique)
|
|
}
|
|
|
|
function To-ResPath($p) {
|
|
$p = $p.Replace('\', '/')
|
|
$normalizedRoot = $ROOT.Replace('\', '/').TrimEnd('/')
|
|
$rootPrefix = "$normalizedRoot/"
|
|
if ($p.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
$p = $p.Substring($rootPrefix.Length)
|
|
}
|
|
if ($p.StartsWith('./')) { $p = $p.Substring(2) }
|
|
return "res://$p"
|
|
}
|
|
|
|
function Invoke-GodotWithTimeout {
|
|
param(
|
|
[string]$ArgumentString,
|
|
[int]$TimeoutSeconds = 30
|
|
)
|
|
$psi = New-Object Diagnostics.ProcessStartInfo
|
|
$psi.FileName = $GODOT
|
|
$psi.Arguments = $ArgumentString
|
|
$psi.RedirectStandardOutput = $true
|
|
$psi.RedirectStandardError = $true
|
|
$psi.UseShellExecute = $false
|
|
$psi.CreateNoWindow = $true
|
|
$previousAppData = $env:APPDATA
|
|
$previousLocalAppData = $env:LOCALAPPDATA
|
|
$env:APPDATA = $GODOT_PROFILE
|
|
$env:LOCALAPPDATA = $GODOT_PROFILE
|
|
try {
|
|
$p = [Diagnostics.Process]::Start($psi)
|
|
} catch {
|
|
return "Failed to start Godot: $($_.Exception.Message)", $null
|
|
} finally {
|
|
$env:APPDATA = $previousAppData
|
|
$env:LOCALAPPDATA = $previousLocalAppData
|
|
}
|
|
$outputTask = $p.StandardOutput.ReadToEndAsync()
|
|
$errorTask = $p.StandardError.ReadToEndAsync()
|
|
$exited = $p.WaitForExit($TimeoutSeconds * 1000)
|
|
if (-not $exited) {
|
|
$p.Kill()
|
|
$p.WaitForExit()
|
|
$output = $outputTask.Result
|
|
$err = $errorTask.Result
|
|
return ($output + $err), $null
|
|
}
|
|
$output = $outputTask.Result
|
|
$err = $errorTask.Result
|
|
return ($output + $err), $p.ExitCode
|
|
}
|
|
|
|
# -- godot --------------------------------------------------------------------
|
|
$GODOT = Find-Godot
|
|
if (-not $GODOT) {
|
|
Write-Host "ERROR: Godot binary not found. Set GODOT_BIN or add godot/godot4 to PATH."
|
|
Exit-Quality 1
|
|
}
|
|
$godotVersionOutput = @(& $GODOT --version 2>&1)
|
|
$godotVersionExit = $LASTEXITCODE
|
|
if ($godotVersionExit -ne 0 -or $godotVersionOutput.Count -eq 0) {
|
|
Write-Host "ERROR: Could not query the Godot version from '$GODOT'."
|
|
Exit-Quality 1
|
|
}
|
|
$GODOT_VERSION = ([string]$godotVersionOutput[0]).Trim()
|
|
$requiredVersionPattern = '^' + [regex]::Escape($REQUIRED_GODOT_SERIES) + '([.-]|$)'
|
|
if ($GODOT_VERSION -notmatch $requiredVersionPattern) {
|
|
Write-Host "ERROR: Godot $REQUIRED_GODOT_SERIES.x is required; found '$GODOT_VERSION'."
|
|
Exit-Quality 1
|
|
}
|
|
|
|
# -- 0. Cross-platform plugin payload -----------------------------------------
|
|
$platformAssetsResult = "PASS"
|
|
Set-Content -Path "$LOG/platform-assets.log" -Value "" -Encoding utf8
|
|
foreach ($pluginFile in $CROSS_PLATFORM_PLUGIN_FILES) {
|
|
if (-not (Test-Path $pluginFile -PathType Leaf)) {
|
|
$platformAssetsResult = "FAIL"
|
|
Add-Content -Path "$LOG/platform-assets.log" -Value "Missing $pluginFile" -Encoding utf8
|
|
$ERRORS += "platform-assets: missing res://$pluginFile"
|
|
}
|
|
}
|
|
if ($platformAssetsResult -eq "FAIL") {
|
|
$FIXES += "Restore the complete pinned Terrain3D 1.0.2 package"
|
|
}
|
|
|
|
# -- 1. Godot import bootstrap ------------------------------------------------
|
|
$importResult = "PASS"
|
|
Set-Content -Path "$LOG/godot-import.log" -Value "" -Encoding utf8
|
|
$classCache = Join-Path $ROOT ".godot/global_script_class_cache.cfg"
|
|
$hasProjectClasses = (Test-Path $classCache) -and
|
|
(Select-String -Path $classCache -SimpleMatch '"class": &"SimNPC"' -Quiet)
|
|
$hasGutClasses = (Test-Path $classCache) -and
|
|
(Select-String -Path $classCache -SimpleMatch '"class": &"GutTest"' -Quiet)
|
|
$changedGd = @(Get-ChangedGd)
|
|
$needsImport = (-not $hasProjectClasses) -or
|
|
((Test-Path "addons/gut/gut_cmdln.gd") -and (-not $hasGutClasses)) -or
|
|
($changedGd.Count -gt 0)
|
|
if ($needsImport) {
|
|
$importArgs = "--headless --path `"$ROOT`" --import"
|
|
$importOutput, $importExit = Invoke-GodotWithTimeout -ArgumentString $importArgs -TimeoutSeconds 60
|
|
$importOutput | Add-Content -Path "$LOG/godot-import.log" -Encoding utf8
|
|
if ($importExit -eq $null -or $importExit -ne 0) {
|
|
$importResult = "FAIL"
|
|
if ($importExit -eq $null) {
|
|
Add-Content -Path "$LOG/godot-import.log" -Value "[timed out after 60s]" -Encoding utf8
|
|
}
|
|
}
|
|
}
|
|
$hasProjectClasses = (Test-Path $classCache) -and
|
|
(Select-String -Path $classCache -SimpleMatch '"class": &"SimNPC"' -Quiet)
|
|
$hasGutClasses = (Test-Path $classCache) -and
|
|
(Select-String -Path $classCache -SimpleMatch '"class": &"GutTest"' -Quiet)
|
|
if (-not $hasProjectClasses -or ((Test-Path "addons/gut/gut_cmdln.gd") -and (-not $hasGutClasses))) {
|
|
$importResult = "FAIL"
|
|
}
|
|
$importLoadError = Select-String -Path "$LOG/godot-import.log" -Pattern 'SCRIPT ERROR:|Parse Error:|Failed to load script' -Quiet
|
|
if ($importLoadError) {
|
|
$importResult = "FAIL"
|
|
}
|
|
if ($importResult -eq "FAIL") {
|
|
$ERRORS += "godot-import: failed to build the global class cache"
|
|
$FIXES += "Fix Godot import errors before running headless checks"
|
|
}
|
|
|
|
# -- 2. gdformat --------------------------------------------------------------
|
|
$fmtAvail = Test-GdFormat
|
|
$fmtResult = "PASS"
|
|
$fmtFailed = $false
|
|
Set-Content -Path "$LOG/gdformat.log" -Value "" -Encoding utf8
|
|
if (-not $fmtAvail) {
|
|
$fmtResult = "FAIL"
|
|
Add-Content -Path "$LOG/gdformat.log" -Value "gdformat not found -- install requirements-dev.txt (see docs/local_quality_gate.md)" -Encoding utf8
|
|
$ERRORS += "gdformat: required formatter not found"
|
|
$FIXES += "Install the pinned dev tools from requirements-dev.txt"
|
|
} else {
|
|
if ($Changed) {
|
|
$files = Get-ChangedGd
|
|
if ($files.Count -eq 0) {
|
|
Add-Content -Path "$LOG/gdformat.log" -Value "No changed .gd files to check." -Encoding utf8
|
|
} else {
|
|
$total = 0; $bad = 0
|
|
foreach ($f in $files) {
|
|
if (-not (Test-Path $f)) { continue }
|
|
$total++
|
|
$out = Invoke-GdFormat "--check" $f
|
|
$formatExit = $script:GdToolExitCode
|
|
$out | Add-Content -Path "$LOG/gdformat.log" -Encoding utf8
|
|
if ($formatExit -ne 0) { $bad++; $fmtFailed = $true }
|
|
}
|
|
$msg = "($bad/$total files need formatting)"
|
|
Add-Content -Path "$LOG/gdformat.log" -Value $msg -Encoding utf8
|
|
}
|
|
} else {
|
|
$formatArgs = @("--check") + $OWNED_GDSCRIPT_ROOTS
|
|
$out = Invoke-GdFormat @formatArgs
|
|
$formatExit = $script:GdToolExitCode
|
|
$out | Add-Content -Path "$LOG/gdformat.log" -Encoding utf8
|
|
if ($formatExit -ne 0) { $fmtFailed = $true }
|
|
}
|
|
}
|
|
|
|
if ($fmtFailed) { $fmtResult = "FAIL" }
|
|
$formatDiagnostics = Select-String -SimpleMatch "would reformat" -Path "$LOG/gdformat.log" -Quiet
|
|
if ($formatDiagnostics) {
|
|
$lines = Select-String -SimpleMatch "would reformat" -Path "$LOG/gdformat.log" | Select-Object -ExpandProperty Line
|
|
foreach ($line in $lines) {
|
|
$p = ($line -replace '^would reformat ', '').Trim()
|
|
$rp = To-ResPath $p
|
|
$ERRORS += "$rp needs formatting"
|
|
}
|
|
$FIXES += "Run gdformat to auto-format files"
|
|
} elseif ($fmtFailed) {
|
|
$ERRORS += "gdformat: formatter exited non-zero; see $LOG/gdformat.log"
|
|
$FIXES += "Fix the gdformat error before continuing"
|
|
}
|
|
|
|
# -- 3. gdlint ----------------------------------------------------------------
|
|
$lintAvail = Test-GdLint
|
|
$lintResult = "PASS"
|
|
$lintFailed = $false
|
|
Set-Content -Path "$LOG/gdlint.log" -Value "" -Encoding utf8
|
|
if (-not $lintAvail) {
|
|
$lintResult = "FAIL"
|
|
Add-Content -Path "$LOG/gdlint.log" -Value "gdlint not found -- install requirements-dev.txt (see docs/local_quality_gate.md)" -Encoding utf8
|
|
$ERRORS += "gdlint: required linter not found"
|
|
$FIXES += "Install the pinned dev tools from requirements-dev.txt"
|
|
} else {
|
|
if ($Changed) {
|
|
$files = Get-ChangedGd
|
|
if ($files.Count -eq 0) {
|
|
Add-Content -Path "$LOG/gdlint.log" -Value "No changed .gd files to lint." -Encoding utf8
|
|
} else {
|
|
foreach ($f in $files) {
|
|
if (-not (Test-Path $f)) { continue }
|
|
$out = Invoke-GdLint $f
|
|
$lintExit = $script:GdToolExitCode
|
|
$out | Add-Content -Path "$LOG/gdlint.log" -Encoding utf8
|
|
if ($lintExit -ne 0) { $lintFailed = $true }
|
|
}
|
|
}
|
|
} else {
|
|
$out = Invoke-GdLint @OWNED_GDSCRIPT_ROOTS
|
|
$lintExit = $script:GdToolExitCode
|
|
$out | Add-Content -Path "$LOG/gdlint.log" -Encoding utf8
|
|
if ($lintExit -ne 0) { $lintFailed = $true }
|
|
}
|
|
}
|
|
|
|
if ($lintFailed) { $lintResult = "FAIL" }
|
|
$lintLines = Select-String -Pattern '^[^ ]+:\d+:\d+:' -Path "$LOG/gdlint.log" | Select-Object -ExpandProperty Line
|
|
if ($lintLines) {
|
|
foreach ($line in $lintLines) {
|
|
$parts = $line -split ':', 4
|
|
if ($parts.Count -lt 4) { continue }
|
|
$file = $parts[0]; $lineno = $parts[1]; $msg = $parts[3]
|
|
$rp = To-ResPath $file
|
|
$ERRORS += "$rp`:$lineno $msg"
|
|
}
|
|
foreach ($line in $lintLines) {
|
|
$parts = $line -split ':', 5
|
|
if ($parts.Count -lt 5) { continue }
|
|
$rule = $parts[3]; $fname = Split-Path -Leaf $parts[0]
|
|
$FIXES += "Fix $rule in $fname"
|
|
}
|
|
} elseif ($lintFailed) {
|
|
$ERRORS += "gdlint: linter exited non-zero; see $LOG/gdlint.log"
|
|
$FIXES += "Fix the gdlint error before continuing"
|
|
}
|
|
|
|
# -- 4. godot headless check --------------------------------------------------
|
|
$godotResult = "PASS"
|
|
Set-Content -Path "$LOG/godot-check.log" -Value "" -Encoding utf8
|
|
$godotArgs = "--headless --path `"$ROOT`" --script res://tests/simulation_definitions_test.gd"
|
|
$godotOutput, $godotExit = Invoke-GodotWithTimeout -ArgumentString $godotArgs
|
|
$godotOutput | Out-File -FilePath "$LOG/godot-check.log" -Encoding utf8
|
|
if ($godotExit -eq $null) {
|
|
$godotResult = "FAIL"
|
|
Add-Content -Path "$LOG/godot-check.log" -Value "[timed out after 30s]" -Encoding utf8
|
|
} elseif ($godotExit -eq 0) {
|
|
$godotResult = "PASS"
|
|
} else {
|
|
$godotResult = "FAIL"
|
|
$godotOutput | Select-String -Pattern 'error|warning|parse|syntax' | Select-Object -First 20 | ForEach-Object {
|
|
$ERRORS += "godot: $_"
|
|
}
|
|
$FIXES += "Fix Godot parser errors"
|
|
}
|
|
$godotLoadError = Select-String -Path "$LOG/godot-check.log" -Pattern 'SCRIPT ERROR:|Parse Error:|Failed to load script' -Quiet
|
|
if ($godotLoadError) {
|
|
$godotResult = "FAIL"
|
|
$ERRORS += "godot: project check reported a script load or parse error"
|
|
$FIXES += "Fix Godot parser errors"
|
|
}
|
|
|
|
# -- 5. project scenario tests ------------------------------------------------
|
|
$scenarioResult = "PASS"
|
|
Set-Content -Path "$LOG/scenarios.log" -Value "" -Encoding utf8
|
|
$scenarioTests = Get-ChildItem "tests" -Filter "*_test.gd" | Sort-Object Name
|
|
foreach ($test in $scenarioTests) {
|
|
Add-Content -Path "$LOG/scenarios.log" -Value "[RUN] $($test.Name)" -Encoding utf8
|
|
$testArgs = "--headless --path `"$ROOT`" --script res://tests/$($test.Name)"
|
|
$testOutput, $testExit = Invoke-GodotWithTimeout -ArgumentString $testArgs
|
|
$testOutput | Add-Content -Path "$LOG/scenarios.log" -Encoding utf8
|
|
if ($testExit -eq $null -or $testExit -ne 0) {
|
|
$scenarioResult = "FAIL"
|
|
$ERRORS += "scenario: $($test.Name) failed or timed out"
|
|
}
|
|
}
|
|
$scenarioLoadError = Select-String -Path "$LOG/scenarios.log" -Pattern 'SCRIPT ERROR:|Parse Error:|Failed to load script' -Quiet
|
|
if ($scenarioLoadError) {
|
|
$scenarioResult = "FAIL"
|
|
$ERRORS += "scenario: Godot reported a script load or parse error"
|
|
}
|
|
if ($scenarioResult -eq "FAIL") {
|
|
$FIXES += "Fix failing project scenario tests"
|
|
}
|
|
|
|
# -- 6. GUT -------------------------------------------------------------------
|
|
$gutResult = "PASS"
|
|
Set-Content -Path "$LOG/gut.log" -Value "" -Encoding utf8
|
|
if (-not (Test-Path "addons/gut/gut_cmdln.gd")) {
|
|
$gutResult = "FAIL"
|
|
$ERRORS += "gut: pinned addon is missing"
|
|
$FIXES += "Restore addons/gut from pinned GUT v9.7.1"
|
|
} else {
|
|
$gutArgs = "--headless --path `"$ROOT`" -s addons/gut/gut_cmdln.gd -gexit -gdisable_colors"
|
|
$gutOutput, $gutExit = Invoke-GodotWithTimeout -ArgumentString $gutArgs
|
|
$gutOutput | Add-Content -Path "$LOG/gut.log" -Encoding utf8
|
|
if ($gutExit -eq $null) {
|
|
$gutResult = "FAIL"
|
|
Add-Content -Path "$LOG/gut.log" -Value "[timed out after 30s]" -Encoding utf8
|
|
$ERRORS += "gut: GUT test timed out after 30 seconds"
|
|
$FIXES += "Fix GUT tests (timeout)"
|
|
} elseif ($gutExit -eq 0) {
|
|
$gutResult = "PASS"
|
|
} else {
|
|
$gutResult = "FAIL"
|
|
$gutOutput | Select-String -Pattern 'fail|error|assert' | Select-Object -First 20 | ForEach-Object {
|
|
$ERRORS += "gut: $_"
|
|
}
|
|
$FIXES += "Fix failing GUT tests"
|
|
}
|
|
$gutLoadError = Select-String -Path "$LOG/gut.log" -Pattern 'Some GUT class_names have not been imported|SCRIPT ERROR:|Parse Error:|Failed to load script' -Quiet
|
|
if ($gutLoadError) {
|
|
$gutResult = "FAIL"
|
|
$ERRORS += "gut: addon or unit tests failed to load"
|
|
$FIXES += "Run Godot 4.7 headless import and fix GUT load errors"
|
|
}
|
|
}
|
|
|
|
# -- summary ------------------------------------------------------------------
|
|
if ($platformAssetsResult -eq 'FAIL' -or $importResult -eq 'FAIL' -or
|
|
$fmtResult -eq 'FAIL' -or $lintResult -eq 'FAIL' -or $godotResult -eq 'FAIL' -or
|
|
$scenarioResult -eq 'FAIL' -or $gutResult -eq 'FAIL') {
|
|
$OVERALL = $true
|
|
}
|
|
|
|
Write-Host ""
|
|
if ($OVERALL) { Write-Host "QUALITY RESULT: FAIL" } else { Write-Host "QUALITY RESULT: PASS" }
|
|
Write-Host "Godot: $GODOT_VERSION"
|
|
Write-Host ""
|
|
Write-Host " platform-assets $platformAssetsResult"
|
|
Write-Host " godot-import $importResult"
|
|
Write-Host " gdformat $fmtResult"
|
|
Write-Host " gdlint $lintResult"
|
|
Write-Host " godot-check $godotResult"
|
|
Write-Host " scenarios $scenarioResult"
|
|
Write-Host " gut $gutResult"
|
|
|
|
$seen = @{}
|
|
foreach ($e in $ERRORS) {
|
|
if ($seen.ContainsKey($e)) { continue }
|
|
$seen[$e] = $true
|
|
Write-Host " $e"
|
|
}
|
|
|
|
if ($OVERALL -and $FIXES.Count -gt 0) {
|
|
Write-Host ""
|
|
Write-Host " NEXT FIX:"
|
|
$seenFix = @{}
|
|
$idx = 1
|
|
foreach ($fix in $FIXES) {
|
|
if ($seenFix.ContainsKey($fix)) { continue }
|
|
$seenFix[$fix] = $true
|
|
Write-Host " $idx. $fix"
|
|
$idx++
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "Full logs:"
|
|
Write-Host " $LOG/"
|
|
Write-Host ""
|
|
|
|
# save summary
|
|
if ($OVERALL) { $resultText = "FAIL" } else { $resultText = "PASS" }
|
|
$summary = @"
|
|
QUALITY RESULT: $resultText
|
|
Godot: $GODOT_VERSION
|
|
|
|
platform-assets $platformAssetsResult
|
|
godot-import $importResult
|
|
gdformat $fmtResult
|
|
gdlint $lintResult
|
|
godot-check $godotResult
|
|
scenarios $scenarioResult
|
|
gut $gutResult
|
|
"@
|
|
Set-Content -Path "$LOG/summary.txt" -Value $summary -Encoding utf8
|
|
|
|
if ($OVERALL) { Exit-Quality 1 } else { Exit-Quality 0 }
|