#!/usr/bin/env python3 """Run required speech regressions, or check each commit tip in a Git push.""" import json import os from pathlib import Path import subprocess import sys import tempfile from native_test_session import run_private_session requiredTests = { "TestSpeechWorkerRadioSelectionDoesNotCancelLabel", "TestPrivateWineReviewUpdatesPreserveQueuedTitleSpeech", "TestPrivateWineTitleSurvivesReviewFocusBurst", "TestAutomaticTitleSurvivesMovementRepeatsBeforeAndAfterDelivery", "TestAutomaticTitleStillStopsForExplicitInput", "TestTitleProtectionDoesNotChangeOrdinaryReading", "TestRepeatTrackerRecognizesNativeSequences", "TestTypingEchoModeCombinations", "TestTypingEchoVoiceCombinations", "TestTypingEchoLineBreakBeforeExistingText", "TestTypingEchoLineBreakKeepsPriorCaretContext", "TestEchoSpeakerSwitchesBothVoicesTransactionally", "TestSpeechWorkerKeyEchoCompletesBeforeCaretSpeech", "TestSpeechWorkerKeyEchoWaitStillAllowsInputInterruption", } def run_checks(sourceRoot: Path) -> None: testEnv = os.environ.copy() # Git exports repository paths to hooks. Tests run against the snapshot. for name in tuple(testEnv): if name.startswith("GIT_"): del testEnv[name] testEnv.pop("DISPLAY", None) testEnv.pop("WAYLAND_DISPLAY", None) testEnv["CTHULHU_WINE_TITLE_DBUS"] = "1" command = [ "dbus-run-session", "--", "go", "test", "-json", "-race", "-count=1", "-timeout=60s", "-run", "^Test.*(Title|Repeat|Interrupt|Echo)|^Test(SpeechWorker|Cancel|WineReview)", "./internal/app", "./internal/input", "./internal/presentation", "./internal/speech", "./internal/wine", ] print("Checking speech queues, key echo, Wine metadata and key interruption...", flush=True) result = run_private_session(command, cwd=sourceRoot, env=testEnv, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) passedTests = set() diagnostics = [] for line in result.stdout.splitlines(): try: event = json.loads(line) except ValueError: diagnostics.append(line + "\n") continue if "Output" in event: diagnostics.append(event["Output"]) if event.get("Action") == "pass" and "Test" in event: passedTests.add(event["Test"]) missingTests = requiredTests - passedTests if result.returncode or missingTests: print("".join(diagnostics), end="", file=sys.stderr) if missingTests: print("Required tests did not pass (missing or skipped): " + ", ".join(sorted(missingTests)), file=sys.stderr) raise RuntimeError("speech regression check failed") print("Speech regression checks passed.", flush=True) def check_push(repoRoot: Path) -> None: checkedCommits = set() for line in sys.stdin: localRef, localCommit, remoteRef, remoteCommit = line.split() if set(localCommit) == {"0"} or localCommit in checkedCommits: continue checkedCommits.add(localCommit) print(f"Checking {localRef} ({localCommit[:12]}) before push...", flush=True) with tempfile.TemporaryDirectory(prefix="cthulhu-push-") as directory: sourceRoot = Path(directory) with tempfile.TemporaryFile() as archive: subprocess.run(["git", "archive", "--format=tar", localCommit], cwd=repoRoot, stdout=archive, check=True) archive.seek(0) subprocess.run(["tar", "-xf", "-", "-C", str(sourceRoot)], stdin=archive, check=True) run_checks(sourceRoot) def main() -> int: repoRoot = Path(__file__).resolve().parents[1] try: if len(sys.argv) > 1 and sys.argv[1] == "--pre-push": check_push(repoRoot) else: run_checks(repoRoot) except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error: print(f"Speech check blocked: {error}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": sys.exit(main())