diff --git a/.github/workflows/repo-guards.yml b/.github/workflows/repo-guards.yml index db90102..53ef4f9 100644 --- a/.github/workflows/repo-guards.yml +++ b/.github/workflows/repo-guards.yml @@ -33,6 +33,9 @@ jobs: run: python -m pip install pyqt6 requests flask pyinstaller - name: Run test suites on Windows run: python tests/run_all.py + - name: Test stale-binary detector (check_stale.ps1) + shell: pwsh + run: ./tests/test_check_stale.ps1 - name: Build hyperwall.exe (no mpv DLL needed for a build-only smoke test) shell: pwsh run: | diff --git a/launch.bat b/launch.bat index fe1aa23..120ca83 100644 --- a/launch.bat +++ b/launch.bat @@ -13,14 +13,20 @@ if not exist "hyperwall.exe" ( exit /b %errorlevel% ) -:: Stale binary check: if exe is older than hyperwall.py, warn -for %%F in (hyperwall.exe) do set EXE_TS=%%~tF -for %%F in (hyperwall.py) do set SRC_TS=%%~tF +echo === Hyperwall === -:: Simple check: if hyperwall.py is newer than today, assume it changed -for /f "tokens=2 delims==" %%I in ('wmic os get localdatetime /value') do set NOW=%%I +:: Stale-binary check: warn (don't block) if hyperwall.py is newer than the exe. +:: Delegated to scripts\check_stale.ps1 (exit 2 = stale) so the comparison is +:: real and unit-tested in CI, rather than computing timestamps and ignoring them. +if exist "scripts\check_stale.ps1" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\check_stale.ps1" ^ + -Source "hyperwall.py" -Exe "hyperwall.exe" + if errorlevel 2 ( + echo Continuing with the existing exe. Press Ctrl+C to abort and rebuild. + timeout /t 3 >nul + ) +) -echo === Hyperwall === echo Launching hyperwall.exe... echo. diff --git a/scripts/check_stale.ps1 b/scripts/check_stale.ps1 new file mode 100644 index 0000000..0b6c81f --- /dev/null +++ b/scripts/check_stale.ps1 @@ -0,0 +1,41 @@ +<# + check_stale.ps1 — Hyperwall stale-binary detector. + + Compares the last-write time of the source entry point against the built exe. + Used by launch.bat so an out-of-date exe (source edited, forgot to rebuild) + is flagged instead of silently launched. + + Exit codes (so callers can branch): + 0 = exe is current (or exe/source missing → nothing to warn about here; + launch.bat handles the missing-exe case on its own) + 2 = exe is STALE (source is newer than exe) — a warning is printed + + Params let the tests point at fixture files. +#> +[CmdletBinding()] +param( + [string]$Source = "hyperwall.py", + [string]$Exe = "hyperwall.exe" +) + +if (-not (Test-Path -LiteralPath $Exe)) { + # No exe to compare — not this script's job to complain. + exit 0 +} +if (-not (Test-Path -LiteralPath $Source)) { + # No source next to the exe (e.g. a frozen-only deployment) — can't compare. + exit 0 +} + +$srcTime = (Get-Item -LiteralPath $Source).LastWriteTimeUtc +$exeTime = (Get-Item -LiteralPath $Exe).LastWriteTimeUtc + +if ($srcTime -gt $exeTime) { + $msg = "hyperwall.exe is STALE: $Source (modified $($srcTime.ToString('u'))) " + + "is newer than the exe (built $($exeTime.ToString('u'))). " + + "Rebuild with build.bat to pick up your latest changes." + Write-Warning $msg + exit 2 +} + +exit 0 diff --git a/tests/test_check_stale.ps1 b/tests/test_check_stale.ps1 new file mode 100644 index 0000000..5f5b524 --- /dev/null +++ b/tests/test_check_stale.ps1 @@ -0,0 +1,61 @@ +<# + test_check_stale.ps1 — verifies scripts/check_stale.ps1 exit codes. + + No Pester dependency: creates temp fixture files with controlled write times + and asserts the detector's exit code. Run on the Windows CI job. + + Exit 0 = all cases pass; exit 1 = a failure. +#> +$ErrorActionPreference = "Stop" +$script = Join-Path $PSScriptRoot "..\scripts\check_stale.ps1" +$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("hwstale_" + [guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Path $tmp | Out-Null + +$fails = 0 +function Check($name, $expected, $actual) { + if ($expected -eq $actual) { + Write-Host " PASS $name" + } else { + Write-Host " FAIL $name (expected exit $expected, got $actual)" + $script:fails++ + } +} + +try { + $src = Join-Path $tmp "hyperwall.py" + $exe = Join-Path $tmp "hyperwall.exe" + + # Case 1: exe newer than source → current → exit 0 + Set-Content -LiteralPath $src -Value "src" + Start-Sleep -Milliseconds 50 + Set-Content -LiteralPath $exe -Value "exe" + & powershell -NoProfile -ExecutionPolicy Bypass -File $script -Source $src -Exe $exe + Check "fresh_exe_exit0" 0 $LASTEXITCODE + + # Case 2: source newer than exe → stale → exit 2 + Start-Sleep -Milliseconds 50 + Set-Content -LiteralPath $src -Value "src-edited" + & powershell -NoProfile -ExecutionPolicy Bypass -File $script -Source $src -Exe $exe + Check "stale_exe_exit2" 2 $LASTEXITCODE + + # Case 3: exe missing → nothing to warn → exit 0 + Remove-Item -LiteralPath $exe + & powershell -NoProfile -ExecutionPolicy Bypass -File $script -Source $src -Exe $exe + Check "missing_exe_exit0" 0 $LASTEXITCODE + + # Case 4: source missing → can't compare → exit 0 + Set-Content -LiteralPath $exe -Value "exe" + Remove-Item -LiteralPath $src + & powershell -NoProfile -ExecutionPolicy Bypass -File $script -Source $src -Exe $exe + Check "missing_src_exit0" 0 $LASTEXITCODE +} +finally { + Remove-Item -Recurse -Force -LiteralPath $tmp -ErrorAction SilentlyContinue +} + +if ($fails -gt 0) { + Write-Host "`n$fails failed." + exit 1 +} +Write-Host "`nAll stale-detector cases passed." +exit 0