323 lines
10 KiB
PowerShell
323 lines
10 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
|
|
Set-Location $ROOT
|
|
|
|
$LOG = "logs/quality/latest"
|
|
New-Item -ItemType Directory -Path $LOG -Force | Out-Null
|
|
|
|
$OVERALL = $false
|
|
$ERRORS = @()
|
|
$FIXES = @()
|
|
$OWNED_GDSCRIPT_ROOTS = @("player", "simulation", "tests", "tools", "world")
|
|
|
|
# -- tool detection -----------------------------------------------------------
|
|
function Find-Godot {
|
|
if ($env:GODOT_BIN) { return $env:GODOT_BIN }
|
|
try { return (Get-Command godot -ErrorAction Stop).Source } catch {}
|
|
try { return (Get-Command godot4 -ErrorAction Stop).Source } catch {}
|
|
$common = @(
|
|
"$env:ProgramFiles\Godot\godot.exe",
|
|
"${env:ProgramFiles(x86)}\Godot\godot.exe",
|
|
"$env:LOCALAPPDATA\Godot\godot.exe",
|
|
"$env:USERPROFILE\AppData\Local\Godot\godot.exe"
|
|
)
|
|
foreach ($p in $common) { if (Test-Path $p) { return $p } }
|
|
$bundled = Get-ChildItem "${env:ProgramFiles(x86)}\Godot" -Filter "Godot*_console.exe" -ErrorAction SilentlyContinue |
|
|
Sort-Object Name -Descending |
|
|
Select-Object -First 1
|
|
if ($bundled) { return $bundled.FullName }
|
|
return $null
|
|
}
|
|
|
|
function Test-GdFormat {
|
|
if (Get-Command gdformat -ErrorAction SilentlyContinue) { return $true }
|
|
try {
|
|
$null = & python -m gdtoolkit.formatter --help 2>&1
|
|
if ($LASTEXITCODE -eq 0) { return $true }
|
|
} catch {}
|
|
try {
|
|
$null = & python3 -m gdtoolkit.formatter --help 2>&1
|
|
if ($LASTEXITCODE -eq 0) { return $true }
|
|
} catch {}
|
|
return $false
|
|
}
|
|
|
|
function Test-GdLint {
|
|
if (Get-Command gdlint -ErrorAction SilentlyContinue) { return $true }
|
|
try {
|
|
$null = & python -m gdtoolkit.linter --help 2>&1
|
|
if ($LASTEXITCODE -eq 0) { return $true }
|
|
} catch {}
|
|
try {
|
|
$null = & python3 -m gdtoolkit.linter --help 2>&1
|
|
if ($LASTEXITCODE -eq 0) { return $true }
|
|
} catch {}
|
|
return $false
|
|
}
|
|
|
|
function Invoke-GdFormat {
|
|
$args = $args
|
|
if (Get-Command gdformat -ErrorAction SilentlyContinue) {
|
|
& gdformat @args 2>&1
|
|
} else {
|
|
& python -m gdtoolkit.formatter @args 2>&1
|
|
}
|
|
}
|
|
|
|
function Invoke-GdLint {
|
|
$args = $args
|
|
if (Get-Command gdlint -ErrorAction SilentlyContinue) {
|
|
& gdlint @args 2>&1
|
|
} else {
|
|
& python -m gdtoolkit.linter @args 2>&1
|
|
}
|
|
}
|
|
|
|
function Get-ChangedGd {
|
|
$f = git diff --name-only HEAD -- '*.gd' 2>$null
|
|
return @($f | Where-Object { $_ -ne '' })
|
|
}
|
|
|
|
function To-ResPath($p) {
|
|
$p = $p.Replace('\', '/')
|
|
if ($p.StartsWith($ROOT)) { $p = $p.Substring($ROOT.Length + 1) }
|
|
if ($p.StartsWith('./')) { $p = $p.Substring(2) }
|
|
return "res://$p"
|
|
}
|
|
|
|
function Invoke-GodotWithTimeout {
|
|
param([string]$ArgumentString)
|
|
$psi = New-Object Diagnostics.ProcessStartInfo
|
|
$psi.FileName = $GODOT
|
|
$psi.Arguments = $ArgumentString
|
|
$psi.RedirectStandardOutput = $true
|
|
$psi.RedirectStandardError = $true
|
|
$psi.UseShellExecute = $false
|
|
$psi.CreateNoWindow = $true
|
|
$p = [Diagnostics.Process]::Start($psi)
|
|
$outputTask = $p.StandardOutput.ReadToEndAsync()
|
|
$errorTask = $p.StandardError.ReadToEndAsync()
|
|
$exited = $p.WaitForExit(30000)
|
|
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 1
|
|
}
|
|
|
|
# -- 1. gdformat --------------------------------------------------------------
|
|
$fmtAvail = Test-GdFormat
|
|
$fmtResult = "PASS"
|
|
Set-Content -Path "$LOG/gdformat.log" -Value "" -Encoding utf8
|
|
if (-not $fmtAvail) {
|
|
$fmtResult = "SKIPPED"
|
|
Add-Content -Path "$LOG/gdformat.log" -Value "gdformat not found -- install: pip install gdtoolkit" -Encoding utf8
|
|
} 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
|
|
$out | Add-Content -Path "$LOG/gdformat.log" -Encoding utf8
|
|
if ($LASTEXITCODE -ne 0) { $bad++ }
|
|
}
|
|
$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
|
|
$out | Add-Content -Path "$LOG/gdformat.log" -Encoding utf8
|
|
}
|
|
}
|
|
|
|
if (Select-String -SimpleMatch "would reformat" -Path "$LOG/gdformat.log" -Quiet) {
|
|
$fmtResult = "FAIL"
|
|
$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"
|
|
}
|
|
|
|
# -- 2. gdlint ----------------------------------------------------------------
|
|
$lintAvail = Test-GdLint
|
|
$lintResult = "PASS"
|
|
Set-Content -Path "$LOG/gdlint.log" -Value "" -Encoding utf8
|
|
if (-not $lintAvail) {
|
|
$lintResult = "SKIPPED"
|
|
Add-Content -Path "$LOG/gdlint.log" -Value "gdlint not found -- install: pip install gdtoolkit" -Encoding utf8
|
|
} 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
|
|
$out | Add-Content -Path "$LOG/gdlint.log" -Encoding utf8
|
|
}
|
|
}
|
|
} else {
|
|
$out = Invoke-GdLint @OWNED_GDSCRIPT_ROOTS
|
|
$out | Add-Content -Path "$LOG/gdlint.log" -Encoding utf8
|
|
}
|
|
}
|
|
|
|
$lintLines = Select-String -Pattern '^[^ ]+:\d+:\d+:' -Path "$LOG/gdlint.log" | Select-Object -ExpandProperty Line
|
|
if ($lintLines) {
|
|
$lintResult = "FAIL"
|
|
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"
|
|
}
|
|
}
|
|
|
|
# -- 3. 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"
|
|
}
|
|
|
|
# -- 4. 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"
|
|
}
|
|
}
|
|
if ($scenarioResult -eq "FAIL") {
|
|
$FIXES += "Fix failing project scenario tests"
|
|
}
|
|
|
|
# -- 5. GUT (optional) --------------------------------------------------------
|
|
$gutResult = "SKIPPED"
|
|
Set-Content -Path "$LOG/gut.log" -Value "" -Encoding utf8
|
|
if (Test-Path "addons/gut/gut_cmdln.gd") {
|
|
$gutArgs = "--headless --path `"$ROOT`" -s addons/gut/gut_cmdln.gd --quit"
|
|
$gutOutput, $gutExit = Invoke-GodotWithTimeout -ArgumentString $gutArgs
|
|
$gutOutput | Out-File -FilePath "$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"
|
|
}
|
|
}
|
|
|
|
# -- summary ------------------------------------------------------------------
|
|
if ($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 ""
|
|
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
|
|
|
|
gdformat $fmtResult
|
|
gdlint $lintResult
|
|
godot-check $godotResult
|
|
scenarios $scenarioResult
|
|
gut $gutResult
|
|
"@
|
|
Set-Content -Path "$LOG/summary.txt" -Value $summary -Encoding utf8
|
|
|
|
if ($OVERALL) { exit 1 } else { exit 0 }
|