From 7387a51b1c5af7f8998e2dea13a30ad63c889b21 Mon Sep 17 00:00:00 2001 From: Martijn Verburg Date: Tue, 10 Feb 2026 15:46:06 +1300 Subject: [PATCH 1/2] Added Mac OS X Build and test check on PR submission --- .github/workflows/macos-ci.yml | 162 +++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 .github/workflows/macos-ci.yml diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml new file mode 100644 index 0000000..20513ee --- /dev/null +++ b/.github/workflows/macos-ci.yml @@ -0,0 +1,162 @@ +name: macOS CI + +on: + pull_request: + branches: [ "main" ] + +jobs: + build-and-interactive-test: + runs-on: macos-latest # Apple silicon only + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # macOS runners have cmake, make, and ncurses out of the box via Xcode CLT. + # No brew installs needed. + + - name: Configure CMake + run: | + cmake -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER=clang + + - name: Build + run: | + cmake --build build --parallel + + # Yes we use perl here, no apologies :-) + - name: Basic smoke test (startup does not crash) + shell: bash + run: | + set -Eeuo pipefail + + cd build + export TERM=vt100 + + echo "Verifying hack binary exists and is executable..." + test -x ./hack || { echo "❌ hack binary not found"; exit 1; } + test -x ./hackdir/mklev || { echo "❌ mklev not found in hackdir"; exit 1; } + + echo "Running quick startup test (expect it to time out after 3s)..." + # macOS has no GNU timeout; use perl alarm as a portable substitute + set +e + out="$(perl -e 'alarm 3; exec @ARGV' ./hack 2>&1 | head -40)" + rc=$? + set -e + echo "$out" | sed 's/^/│ /' + + if echo "$out" | grep -q "Cannot cd"; then + echo "❌ FAILED: hack could not chdir to HACKDIR" + exit 1 + fi + echo "✓ Basic startup smoke test passed (rc=$rc)" + + - name: Interactive map rendering test (expect) + shell: bash + run: | + set -Eeuo pipefail + + cd build + export TERM=vt100 + + LOGFILE="${RUNNER_TEMP}/hack-expect.log" + + echo "Running interactive expect test..." + + # Outer safety timeout (60s) in case expect itself hangs + set +e + perl -e 'alarm 60; exec @ARGV' \ + expect -f "${GITHUB_WORKSPACE}/.github/workflows/hack-interactive-test.exp" + EXPECT_RC=$? + set -e + + # Copy log to a stable location + if [ -f /tmp/hack-expect.log ]; then + cp /tmp/hack-expect.log "$LOGFILE" 2>/dev/null || true + fi + + echo "" + echo "===== Expect session log =====" + if [ -f "$LOGFILE" ]; then + cat "$LOGFILE" + else + echo "(no log file found)" + fi + echo "===== End log =====" + echo "" + + if [ "$EXPECT_RC" -ne 0 ]; then + echo "❌ Interactive test failed (expect exit code: $EXPECT_RC)" + exit 1 + fi + + echo "✓ Interactive test: game started successfully" + + - name: Verify map was rendered + shell: bash + run: | + set -Eeuo pipefail + + LOGFILE="${RUNNER_TEMP}/hack-expect.log" + + if [ ! -f "$LOGFILE" ]; then + echo "❌ No expect log file — cannot verify map output" + exit 1 + fi + + PASS=true + + echo "Checking expect log for map rendering evidence..." + + # Check for player character + if grep -q '@' "$LOGFILE"; then + echo " ✓ Found player character '@'" + else + echo " ✗ Missing player character '@'" + PASS=false + fi + + # Check for wall characters (horizontal or vertical) + if grep -qE '[-|]' "$LOGFILE"; then + echo " ✓ Found wall characters" + else + echo " ✗ Missing wall characters (- or |)" + PASS=false + fi + + # Check for floor/corridor characters + if grep -qE '[.#]' "$LOGFILE"; then + echo " ✓ Found floor/corridor characters" + else + echo " ✗ Missing floor/corridor characters (. or #)" + PASS=false + fi + + # Check for the quit prompt (proves game loop was running) + if grep -q 'Really quit?' "$LOGFILE"; then + echo " ✓ Found 'Really quit?' prompt (game loop was responsive)" + else + echo " ✗ Missing 'Really quit?' prompt" + PASS=false + fi + + # Check for the welcome screen prompt + if grep -q 'Hit space to continue' "$LOGFILE"; then + echo " ✓ Found welcome screen prompt" + else + echo " ✗ Missing welcome screen prompt" + PASS=false + fi + + echo "" + if [ "$PASS" = true ]; then + echo "✅ Map rendered successfully — found player (@), walls, floor/corridors, and interactive quit." + else + echo "❌ Map rendering verification failed. See log above for details." + echo "" + echo "===== Full expect log =====" + cat "$LOGFILE" + echo "===== End log =====" + exit 1 + fi From 397aa13be3bc49843b024be47c53422c9c54609d Mon Sep 17 00:00:00 2001 From: Martijn Verburg Date: Tue, 10 Feb 2026 15:57:28 +1300 Subject: [PATCH 2/2] Inline the test script --- .github/workflows/macos-ci.yml | 87 +++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index 20513ee..bc353ab 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -52,6 +52,89 @@ jobs: fi echo "✓ Basic startup smoke test passed (rc=$rc)" + - name: Write expect test script + shell: python3 {0} + run: | + import os + script = os.path.join(os.environ["RUNNER_TEMP"], "hack-test.exp") + with open(script, "w") as f: + f.write(r'''set timeout 30 + log_file -noappend /tmp/hack-expect.log + match_max 50000 + + spawn ./hack + + expect { + -exact "--Hit space to continue--" { + send_log "\n>>> Found welcome prompt, sending space...\n" + } + timeout { + send_log "\n>>> TIMEOUT waiting for welcome screen\n" + exit 1 + } + eof { + send_log "\n>>> EOF before welcome screen -- game crashed?\n" + exit 2 + } + } + + send " " + + expect { + "@" { + send_log "\n>>> Found player @ on map!\n" + } + timeout { + send_log "\n>>> TIMEOUT waiting for map to render\n" + exit 3 + } + eof { + send_log "\n>>> EOF during map render -- game crashed after welcome?\n" + exit 4 + } + } + + sleep 1 + + send "Q" + + expect { + "Really quit?" { + send_log "\n>>> Got quit confirmation prompt\n" + } + timeout { + send_log "\n>>> TIMEOUT waiting for quit prompt\n" + exit 5 + } + eof { + send_log "\n>>> EOF after sending Q -- unexpected exit\n" + exit 6 + } + } + + send "y" + + expect { + eof { + send_log "\n>>> Game exited cleanly\n" + } + timeout { + send_log "\n>>> TIMEOUT waiting for game to exit after quit\n" + exit 7 + } + } + + lassign [wait] pid spawnid os_error value + if {$os_error != 0} { + send_log "\n>>> Game exited with OS error: $os_error\n" + exit 8 + } + + send_log "\n>>> All interactive checks passed!\n" + exit 0 + ''') + print(f"Wrote expect script to {script}") + - name: Interactive map rendering test (expect) shell: bash run: | @@ -61,13 +144,13 @@ jobs: export TERM=vt100 LOGFILE="${RUNNER_TEMP}/hack-expect.log" + SCRIPT="${RUNNER_TEMP}/hack-test.exp" echo "Running interactive expect test..." # Outer safety timeout (60s) in case expect itself hangs set +e - perl -e 'alarm 60; exec @ARGV' \ - expect -f "${GITHUB_WORKSPACE}/.github/workflows/hack-interactive-test.exp" + perl -e 'alarm 60; exec @ARGV' expect -f "$SCRIPT" EXPECT_RC=$? set -e