From 4f21465dbfdfa6910223d4bf982e77524ad44a54 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 25 Feb 2026 12:47:58 +0100 Subject: [PATCH] feat(ci): test runner scripts and Makefile integration (#270) Six test runner scripts at tests/: run-rust, run-godot, run-ipc-fixtures, run-ipc-protocol, run-ipc-integration, run-all. Plus run-ipc-benchmark for Layer 3 timing. All produce structured JSON stdout, support --filter, and exit 0/non-zero. Makefile targets updated to delegate to scripts. Co-Authored-By: Claude Opus 4.6 --- Makefile | 34 ++++++++++---- docs/DEVOPS.md | 22 +++++++-- tests/run-all | 77 ++++++++++++++++++++++++++++++ tests/run-godot | 88 +++++++++++++++++++++++++++++++++++ tests/run-ipc-benchmark | 59 +++++++++++++++++++++++ tests/run-ipc-fixtures | 98 +++++++++++++++++++++++++++++++++++++++ tests/run-ipc-integration | 72 ++++++++++++++++++++++++++++ tests/run-ipc-protocol | 50 ++++++++++++++++++++ tests/run-rust | 54 +++++++++++++++++++++ 9 files changed, 542 insertions(+), 12 deletions(-) create mode 100755 tests/run-all create mode 100755 tests/run-godot create mode 100755 tests/run-ipc-benchmark create mode 100755 tests/run-ipc-fixtures create mode 100755 tests/run-ipc-integration create mode 100755 tests/run-ipc-protocol create mode 100755 tests/run-rust diff --git a/Makefile b/Makefile index 6e71150ba..95294d5ee 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,8 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null) pre-pr-server pre-pr-client pre-pr-content \ fixtures-client golden-diff golden-update \ checklist-validate checklist-generate \ - perf-baseline debug-schedule + perf-baseline debug-schedule \ + test-ipc-fixtures test-ipc-protocol test-ipc-integration test-ipc-benchmark # --- Configuration --- @@ -23,11 +24,15 @@ help: @echo " make stop Stop any running server instance" @echo " make client Run the Godot client (test mode)" @echo " make server Run the Rust simulation server" - @echo " make test Run all tests" - @echo " make lint Run all linters" - @echo " make ci Run full CI pipeline locally" - @echo " make ci-client Run client CI checks" - @echo " make ci-server Run server CI checks" + @echo " make test Run all tests" + @echo " make test-ipc-fixtures Layer 1: IPC serialization fixtures" + @echo " make test-ipc-protocol Layer 2: mock IPC protocol tests" + @echo " make test-ipc-integration Layer 3: real subprocess round-trip" + @echo " make test-ipc-benchmark IPC latency benchmark (blocked: #555/#556)" + @echo " make lint Run all linters" + @echo " make ci Run full CI pipeline locally" + @echo " make ci-client Run client CI checks" + @echo " make ci-server Run server CI checks" @echo " make check-protocol Verify server/client protocol versions match" @echo " make clean Remove build artifacts and caches" @echo "" @@ -128,7 +133,7 @@ stop: test: test-server test-client test-server: - cd server && cargo nextest run + tests/run-rust fixtures: cd server && cargo test --test gen_fixtures -- --ignored @@ -169,8 +174,19 @@ golden-update: @echo "Review with: git diff --cached -- server/tests/golden/" test-client: - @test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; } - $(GODOT) --headless --path client -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://tests/ + tests/run-godot + +test-ipc-fixtures: + tests/run-ipc-fixtures + +test-ipc-protocol: + tests/run-ipc-protocol + +test-ipc-integration: + tests/run-ipc-integration + +test-ipc-benchmark: + tests/run-ipc-benchmark # --- Lint --- diff --git a/docs/DEVOPS.md b/docs/DEVOPS.md index 21d44a440..1e3659119 100644 --- a/docs/DEVOPS.md +++ b/docs/DEVOPS.md @@ -62,11 +62,27 @@ The server must be running before the client connects (subprocess launch will be ### Test ```bash -make test # Run all tests -make test-server # cargo test in server/ -make test-client # gdUnit4 tests (headless runner pending) +make test # Run all tests (test-server + test-client) +make test-server # Rust tests via tests/run-rust (cargo nextest, JSON summary) +make test-client # Godot tests via tests/run-godot (gdUnit4 headless, JSON summary) ``` +The IPC test layers (D-030) have dedicated targets: + +```bash +make test-ipc-fixtures # Layer 1: serialization round-trip fixtures +make test-ipc-protocol # Layer 2: mock LocalBridge protocol tests +make test-ipc-integration # Layer 3: real subprocess round-trip (+ benchmark when ready) +make test-ipc-benchmark # IPC latency benchmark (blocked: #555/#556 handshake) +``` + +Each `tests/run-*` script outputs a JSON summary to stdout and streams progress to stderr: +```json +{"suite":"rust","total":42,"passed":42,"failed":0,"duration_ms":1230} +``` + +All scripts accept `--filter ` to run a subset of tests. They are whitelistable for agent use (no TTY prompts, no interactive input). + Server tests use Rust's built-in test framework with `#[cfg(test)]` inline tests and `tests/` integration tests (D-030). Client tests use gdUnit4 (D-030). ### Cross-Encoder Fixtures diff --git a/tests/run-all b/tests/run-all new file mode 100755 index 000000000..b296cfb22 --- /dev/null +++ b/tests/run-all @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# tests/run-all: Run all test suites in order (D-030) +# Invokes run-rust, run-godot, run-ipc-fixtures, run-ipc-protocol, run-ipc-integration. +# Exit: 0 = all suites pass, non-zero = any suite failed +# Stdout: {"suite":"all","total":N,"passed":N,"failed":N,"duration_ms":N,"suites":[...]} +set -euo pipefail + +FILTER="" +while [[ $# -gt 0 ]]; do + case "$1" in + --filter) FILTER="${2:-}"; shift 2 ;; + --filter=*) FILTER="${1#--filter=}"; shift ;; + *) echo "Unknown argument: $1" >&2; exit 2 ;; + esac +done + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +PASS_ARGS=() +[[ -n "$FILTER" ]] && PASS_ARGS+=(--filter "$FILTER") + +SUITES=( + run-rust + run-godot + run-ipc-fixtures + run-ipc-protocol + run-ipc-integration +) + +START_MS=$(date +%s%3N) + +OVERALL_TOTAL=0 +OVERALL_PASSED=0 +OVERALL_FAILED=0 +OVERALL_EXIT=0 +SUITE_RESULTS="" + +for suite in "${SUITES[@]}"; do + script="$SCRIPT_DIR/$suite" + if [[ ! -x "$script" ]]; then + echo "Warning: $script not found or not executable — skipping" >&2 + continue + fi + + SUITE_OUT=$(mktemp) + set +e + "$script" "${PASS_ARGS[@]}" >"$SUITE_OUT" + SUITE_EXIT=$? + set -e + + SUITE_JSON=$(cat "$SUITE_OUT") + rm -f "$SUITE_OUT" + + # Accumulate totals from the suite's JSON output + S_TOTAL=$(echo "$SUITE_JSON" | grep -oE '"total":[0-9]+' | grep -oE '[0-9]+' || echo 0) + S_PASSED=$(echo "$SUITE_JSON" | grep -oE '"passed":[0-9]+' | grep -oE '[0-9]+' || echo 0) + S_FAILED=$(echo "$SUITE_JSON" | grep -oE '"failed":[0-9]+' | grep -oE '[0-9]+' || echo 0) + + OVERALL_TOTAL=$(( OVERALL_TOTAL + ${S_TOTAL:-0} )) + OVERALL_PASSED=$(( OVERALL_PASSED + ${S_PASSED:-0} )) + OVERALL_FAILED=$(( OVERALL_FAILED + ${S_FAILED:-0} )) + [[ $SUITE_EXIT -ne 0 ]] && OVERALL_EXIT=1 + + # Build suites array for JSON + if [[ -n "$SUITE_RESULTS" ]]; then + SUITE_RESULTS="$SUITE_RESULTS,$SUITE_JSON" + else + SUITE_RESULTS="$SUITE_JSON" + fi +done + +END_MS=$(date +%s%3N) +DURATION_MS=$((END_MS - START_MS)) + +printf '{"suite":"all","total":%d,"passed":%d,"failed":%d,"duration_ms":%d,"suites":[%s]}\n' \ + "$OVERALL_TOTAL" "$OVERALL_PASSED" "$OVERALL_FAILED" "$DURATION_MS" "$SUITE_RESULTS" +exit $OVERALL_EXIT diff --git a/tests/run-godot b/tests/run-godot new file mode 100755 index 000000000..ee323e275 --- /dev/null +++ b/tests/run-godot @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# tests/run-godot: Run Godot client test suite via gdUnit4 (D-030) +# Exit: 0 = all pass, non-zero = failure +# Stdout: {"suite":"godot","total":N,"passed":N,"failed":N,"duration_ms":N} +# +# --filter: accepts a test filename stem (e.g. "test_protocol" → runs test_protocol.gd only) +set -euo pipefail + +FILTER="" +while [[ $# -gt 0 ]]; do + case "$1" in + --filter) FILTER="${2:-}"; shift 2 ;; + --filter=*) FILTER="${1#--filter=}"; shift ;; + *) echo "Unknown argument: $1" >&2; exit 2 ;; + esac +done + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +GODOT=$(command -v godot4 2>/dev/null || command -v godot 2>/dev/null || echo "") +if [[ -z "$GODOT" ]]; then + printf '{"suite":"godot","total":0,"passed":0,"failed":0,"duration_ms":0,"error":"godot not found in PATH"}\n' + exit 1 +fi + +# Resolve the test target: directory or specific file +if [[ -n "$FILTER" ]]; then + # Support bare name (test_protocol) or full path (test_protocol.gd) + if [[ "$FILTER" == res://* ]]; then + TEST_TARGET="$FILTER" + elif [[ "$FILTER" == *.gd ]]; then + TEST_TARGET="res://tests/$FILTER" + else + TEST_TARGET="res://tests/${FILTER}.gd" + fi +else + TEST_TARGET="res://tests/" +fi + +START_MS=$(date +%s%3N) +TMPOUT=$(mktemp) + +set +e +"$GODOT" --headless --path "$REPO_ROOT/client" \ + -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd \ + --ignoreHeadlessMode \ + -c \ + -a "$TEST_TARGET" \ + 2>&1 | tee "$TMPOUT" >&2 +EXIT_CODE=${PIPESTATUS[0]} +set -e + +END_MS=$(date +%s%3N) +DURATION_MS=$((END_MS - START_MS)) + +_extract_num() { + local haystack="$1" pattern="$2" + echo "$haystack" | grep -oiE "[0-9]+ $pattern" | grep -oE '^[0-9]+' || echo 0 +} + +# gdUnit4 outputs per-suite statistics: "N test cases | X errors | Y failures | ..." +# and a summary: "Executed test cases : (X/N)" or "Executed test cases : (X/N), Z skipped" +TOTAL=0; PASSED=0; FAILED=0 + +# Sum errors + failures across all suite statistics lines +STATS_LINES=$(grep -oE "[0-9]+ test cases \| [0-9]+ errors \| [0-9]+ failures" "$TMPOUT" || true) +if [[ -n "$STATS_LINES" ]]; then + TOTAL=$(echo "$STATS_LINES" | grep -oE '^[0-9]+' | awk '{s+=$1} END {print s}') + ERRORS=$(echo "$STATS_LINES" | grep -oE '[0-9]+ errors' | grep -oE '^[0-9]+' | awk '{s+=$1} END {print s}') + FAILURES=$(echo "$STATS_LINES" | grep -oE '[0-9]+ failures' | grep -oE '^[0-9]+' | awk '{s+=$1} END {print s}') + FAILED=$(( ${ERRORS:-0} + ${FAILURES:-0} )) + PASSED=$(( TOTAL - FAILED )) +fi + +# Fallback: parse "Executed test cases : (X/N)" for total if stats parse failed +if [[ "$TOTAL" -eq 0 ]]; then + EXEC_LINE=$(grep -oE "Executed test cases : \([0-9]+/[0-9]+\)" "$TMPOUT" | tail -1 || true) + if [[ -n "$EXEC_LINE" ]]; then + TOTAL=$(echo "$EXEC_LINE" | grep -oE '/[0-9]+\)' | grep -oE '[0-9]+') + PASSED=$(echo "$EXEC_LINE" | grep -oE '\([0-9]+/' | grep -oE '[0-9]+') + FAILED=$(( TOTAL - PASSED )) + fi +fi + +rm -f "$TMPOUT" +printf '{"suite":"godot","total":%d,"passed":%d,"failed":%d,"duration_ms":%d}\n' \ + "${TOTAL:-0}" "${PASSED:-0}" "${FAILED:-0}" "$DURATION_MS" +exit $EXIT_CODE diff --git a/tests/run-ipc-benchmark b/tests/run-ipc-benchmark new file mode 100755 index 000000000..dc638e83f --- /dev/null +++ b/tests/run-ipc-benchmark @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# tests/run-ipc-benchmark: IPC round-trip latency benchmark (#342, D-020) +# +# Runs server/tests/ipc_bench.rs via `cargo test --release --test ipc_bench`. +# Parses IPC_BENCH_RESULT:{json} from output and outputs the result JSON. +# +# Latency budget: p99 <= 5ms (D-020: "~1-5ms serialization latency per tick"). +# +# NOTE (#342): Handshake step is stubbed in ipc_bench.rs pending #555 (server +# protocol handshake) and #556 (client handshake). Full clean timing requires +# a working handshake before the measurement loop starts. +# +# Exit: 0 = benchmark passed (p99 within threshold), non-zero = failure +# Stdout: {"p50_ms":N,"p95_ms":N,"p99_ms":N,"threshold_ms":5,"passed":true,"rounds":100} +set -euo pipefail + +ITERATIONS=100 +THRESHOLD_MS=5 + +while [[ $# -gt 0 ]]; do + case "$1" in + --iterations) ITERATIONS="${2:-100}"; shift 2 ;; + --iterations=*) ITERATIONS="${1#--iterations=}"; shift ;; + --threshold-ms) THRESHOLD_MS="${2:-5}"; shift 2 ;; + --filter) shift 2 ;; # ignored — benchmark has no test filter + --filter=*) shift ;; + *) echo "Unknown argument: $1" >&2; exit 2 ;; + esac +done + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +START_MS=$(date +%s%3N) +TMPOUT=$(mktemp) + +set +e +cd "$REPO_ROOT/server" && \ + cargo test --release --test ipc_bench -- --ignored --nocapture 2>&1 | tee "$TMPOUT" >&2 +EXIT_CODE=${PIPESTATUS[0]} +set -e + +END_MS=$(date +%s%3N) +DURATION_MS=$((END_MS - START_MS)) + +# Extract IPC_BENCH_RESULT:{json} line from output +RESULT_LINE=$(grep "^IPC_BENCH_RESULT:" "$TMPOUT" | tail -1 || true) +rm -f "$TMPOUT" + +if [[ -n "$RESULT_LINE" ]]; then + # Strip the prefix and output the JSON + echo "${RESULT_LINE#IPC_BENCH_RESULT:}" +else + # No result line — test failed to produce output + printf '{"p50_ms":0,"p95_ms":0,"p99_ms":0,"threshold_ms":%d,"passed":false,"error":"no benchmark output — server binary may not be built (run make build-server)"}\n' \ + "$THRESHOLD_MS" + EXIT_CODE=1 +fi + +exit $EXIT_CODE diff --git a/tests/run-ipc-fixtures b/tests/run-ipc-fixtures new file mode 100755 index 000000000..6d76a073e --- /dev/null +++ b/tests/run-ipc-fixtures @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# tests/run-ipc-fixtures: Layer 1 IPC fixture tests (D-030) +# Runs Rust serialization round-trip tests + GDScript fixture validation. +# GDScript side is skipped if client/tests/test_ipc_fixtures.gd doesn't exist yet (#271). +# Exit: 0 = all pass, non-zero = any failure +# Stdout: {"suite":"ipc-fixtures","total":N,"passed":N,"failed":N,"duration_ms":N} +set -euo pipefail + +FILTER="" +while [[ $# -gt 0 ]]; do + case "$1" in + --filter) FILTER="${2:-}"; shift 2 ;; + --filter=*) FILTER="${1#--filter=}"; shift ;; + *) echo "Unknown argument: $1" >&2; exit 2 ;; + esac +done + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +_extract_num() { + local haystack="$1" pattern="$2" + echo "$haystack" | grep -oE "[0-9]+ $pattern" | grep -oE '^[0-9]+' || echo 0 +} + +_parse_nextest_summary() { + local tmpout="$1" + local summary total passed failed + summary=$(grep -E "^\s*(Summary|Finished)" "$tmpout" | tail -1 || true) + if [[ -n "$summary" ]]; then + total=$(_extract_num "$summary" "tests? run") + passed=$(_extract_num "$summary" "passed") + failed=$(_extract_num "$summary" "failed") + else + total=0; passed=0; failed=0 + fi + echo "$total $passed $failed" +} + +# --- Layer 1a: Rust serialization tests --- +START_MS=$(date +%s%3N) + +cd "$REPO_ROOT/server" +TMPOUT=$(mktemp) +NEXTEST_ARGS=(nextest run --color never --test serialization) +[[ -n "$FILTER" ]] && NEXTEST_ARGS+=(-E "test(~${FILTER})") + +set +e +cargo "${NEXTEST_ARGS[@]}" 2>&1 | tee "$TMPOUT" >&2 +RUST_EXIT=${PIPESTATUS[0]} +set -e + +read -r RUST_TOTAL RUST_PASSED RUST_FAILED < <(_parse_nextest_summary "$TMPOUT") +rm -f "$TMPOUT" + +# --- Layer 1b: GDScript fixture tests (optional until #271 lands) --- +GDS_FIXTURE="$REPO_ROOT/client/tests/test_ipc_fixtures.gd" +GDS_TOTAL=0; GDS_PASSED=0; GDS_FAILED=0; GDS_EXIT=0 + +if [[ -f "$GDS_FIXTURE" ]]; then + GODOT=$(command -v godot4 2>/dev/null || command -v godot 2>/dev/null || echo "") + if [[ -z "$GODOT" ]]; then + echo "Warning: test_ipc_fixtures.gd found but godot not in PATH — skipping GDScript layer" >&2 + else + GDTMP=$(mktemp) + set +e + "$GODOT" --headless --path "$REPO_ROOT/client" \ + -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd \ + --ignoreHeadlessMode -c \ + -a res://tests/test_ipc_fixtures.gd \ + 2>&1 | tee "$GDTMP" >&2 + GDS_EXIT=${PIPESTATUS[0]} + set -e + + STATS=$(grep -oE "[0-9]+ test cases \| [0-9]+ errors \| [0-9]+ failures" "$GDTMP" || true) + if [[ -n "$STATS" ]]; then + GDS_TOTAL=$(echo "$STATS" | grep -oE '^[0-9]+' | awk '{s+=$1} END {print s}') + ERRS=$(echo "$STATS" | grep -oE '[0-9]+ errors' | grep -oE '^[0-9]+' | awk '{s+=$1} END {print s}') + FAILS=$(echo "$STATS" | grep -oE '[0-9]+ failures' | grep -oE '^[0-9]+' | awk '{s+=$1} END {print s}') + GDS_FAILED=$(( ${ERRS:-0} + ${FAILS:-0} )) + GDS_PASSED=$(( GDS_TOTAL - GDS_FAILED )) + fi + rm -f "$GDTMP" + fi +fi + +END_MS=$(date +%s%3N) +DURATION_MS=$((END_MS - START_MS)) + +TOTAL=$(( RUST_TOTAL + GDS_TOTAL )) +PASSED=$(( RUST_PASSED + GDS_PASSED )) +FAILED=$(( RUST_FAILED + GDS_FAILED )) + +# Overall exit: fail if either side failed +EXIT_CODE=$(( RUST_EXIT != 0 || GDS_EXIT != 0 ? 1 : 0 )) + +printf '{"suite":"ipc-fixtures","total":%d,"passed":%d,"failed":%d,"duration_ms":%d}\n' \ + "$TOTAL" "$PASSED" "$FAILED" "$DURATION_MS" +exit $EXIT_CODE diff --git a/tests/run-ipc-integration b/tests/run-ipc-integration new file mode 100755 index 000000000..5f0696e71 --- /dev/null +++ b/tests/run-ipc-integration @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# tests/run-ipc-integration: Layer 3 real-subprocess integration tests (D-030) +# Spawns the server binary as a real child process, runs IPC round-trip. +# Also invokes tests/run-ipc-benchmark when that script exists (#342). +# Exit: 0 = all pass, non-zero = any failure +# Stdout: {"suite":"ipc-integration","total":N,"passed":N,"failed":N,"duration_ms":N} +set -euo pipefail + +FILTER="" +while [[ $# -gt 0 ]]; do + case "$1" in + --filter) FILTER="${2:-}"; shift 2 ;; + --filter=*) FILTER="${1#--filter=}"; shift ;; + *) echo "Unknown argument: $1" >&2; exit 2 ;; + esac +done + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT/server" + +START_MS=$(date +%s%3N) +TMPOUT=$(mktemp) + +NEXTEST_ARGS=(nextest run --color never --test layer3) +[[ -n "$FILTER" ]] && NEXTEST_ARGS+=(-E "test(~${FILTER})") + +set +e +cargo "${NEXTEST_ARGS[@]}" 2>&1 | tee "$TMPOUT" >&2 +LAYER3_EXIT=${PIPESTATUS[0]} +set -e + +_extract_num() { + local haystack="$1" pattern="$2" + echo "$haystack" | grep -oE "[0-9]+ $pattern" | grep -oE '^[0-9]+' || echo 0 +} + +SUMMARY=$(grep -E "^\s*(Summary|Finished)" "$TMPOUT" | tail -1 || true) +TOTAL=0; PASSED=0; FAILED=0 +if [[ -n "$SUMMARY" ]]; then + TOTAL=$(_extract_num "$SUMMARY" "tests? run") + PASSED=$(_extract_num "$SUMMARY" "passed") + FAILED=$(_extract_num "$SUMMARY" "failed") +fi +rm -f "$TMPOUT" + +# Run IPC benchmark if it exists (#342 — requires handshake from #555/#556) +BENCH_SCRIPT="$REPO_ROOT/tests/run-ipc-benchmark" +BENCH_EXIT=0 +if [[ -x "$BENCH_SCRIPT" ]]; then + BENCH_ARGS=() + [[ -n "$FILTER" ]] && BENCH_ARGS+=(--filter "$FILTER") + set +e + "$BENCH_SCRIPT" "${BENCH_ARGS[@]}" >&2 + BENCH_EXIT=$? + set -e + if [[ $BENCH_EXIT -ne 0 ]]; then + FAILED=$(( FAILED + 1 )) + TOTAL=$(( TOTAL + 1 )) + else + PASSED=$(( PASSED + 1 )) + TOTAL=$(( TOTAL + 1 )) + fi +fi + +END_MS=$(date +%s%3N) +DURATION_MS=$((END_MS - START_MS)) + +EXIT_CODE=$(( LAYER3_EXIT != 0 || BENCH_EXIT != 0 ? 1 : 0 )) + +printf '{"suite":"ipc-integration","total":%d,"passed":%d,"failed":%d,"duration_ms":%d}\n' \ + "$TOTAL" "$PASSED" "$FAILED" "$DURATION_MS" +exit $EXIT_CODE diff --git a/tests/run-ipc-protocol b/tests/run-ipc-protocol new file mode 100755 index 000000000..166a05a53 --- /dev/null +++ b/tests/run-ipc-protocol @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# tests/run-ipc-protocol: Layer 2 mock IPC protocol tests (D-030) +# Runs LocalBridge Unix-socket round-trip tests (no real subprocess). +# Exit: 0 = all pass, non-zero = failure +# Stdout: {"suite":"ipc-protocol","total":N,"passed":N,"failed":N,"duration_ms":N} +set -euo pipefail + +FILTER="" +while [[ $# -gt 0 ]]; do + case "$1" in + --filter) FILTER="${2:-}"; shift 2 ;; + --filter=*) FILTER="${1#--filter=}"; shift ;; + *) echo "Unknown argument: $1" >&2; exit 2 ;; + esac +done + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT/server" + +START_MS=$(date +%s%3N) +TMPOUT=$(mktemp) + +NEXTEST_ARGS=(nextest run --color never --test bridge_ipc) +[[ -n "$FILTER" ]] && NEXTEST_ARGS+=(-E "test(~${FILTER})") + +set +e +cargo "${NEXTEST_ARGS[@]}" 2>&1 | tee "$TMPOUT" >&2 +EXIT_CODE=${PIPESTATUS[0]} +set -e + +END_MS=$(date +%s%3N) +DURATION_MS=$((END_MS - START_MS)) + +_extract_num() { + local haystack="$1" pattern="$2" + echo "$haystack" | grep -oE "[0-9]+ $pattern" | grep -oE '^[0-9]+' || echo 0 +} + +SUMMARY=$(grep -E "^\s*(Summary|Finished)" "$TMPOUT" | tail -1 || true) +TOTAL=0; PASSED=0; FAILED=0 +if [[ -n "$SUMMARY" ]]; then + TOTAL=$(_extract_num "$SUMMARY" "tests? run") + PASSED=$(_extract_num "$SUMMARY" "passed") + FAILED=$(_extract_num "$SUMMARY" "failed") +fi + +rm -f "$TMPOUT" +printf '{"suite":"ipc-protocol","total":%d,"passed":%d,"failed":%d,"duration_ms":%d}\n' \ + "${TOTAL:-0}" "${PASSED:-0}" "${FAILED:-0}" "$DURATION_MS" +exit $EXIT_CODE diff --git a/tests/run-rust b/tests/run-rust new file mode 100755 index 000000000..453a00b1b --- /dev/null +++ b/tests/run-rust @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# tests/run-rust: Run Rust test suite via cargo nextest (D-030) +# Exit: 0 = all pass, non-zero = failure +# Stdout: {"suite":"rust","total":N,"passed":N,"failed":N,"duration_ms":N} +set -euo pipefail + +FILTER="" +while [[ $# -gt 0 ]]; do + case "$1" in + --filter) FILTER="${2:-}"; shift 2 ;; + --filter=*) FILTER="${1#--filter=}"; shift ;; + *) echo "Unknown argument: $1" >&2; exit 2 ;; + esac +done + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT/server" + +START_MS=$(date +%s%3N) +TMPOUT=$(mktemp) + +NEXTEST_ARGS=(nextest run --color never) +if [[ -n "$FILTER" ]]; then + NEXTEST_ARGS+=(-E "test(~${FILTER})") +fi + +set +e +cargo "${NEXTEST_ARGS[@]}" 2>&1 | tee "$TMPOUT" >&2 +EXIT_CODE=${PIPESTATUS[0]} +set -e + +END_MS=$(date +%s%3N) +DURATION_MS=$((END_MS - START_MS)) + +# Parse nextest summary: " Summary [ 0.123s] N tests run: X passed[, Y failed], Z skipped" +# (older nextest uses "Finished", newer uses "Summary" — match both) +SUMMARY=$(grep -E "^\s*(Summary|Finished)" "$TMPOUT" | tail -1 || true) + +_extract_num() { + local haystack="$1" pattern="$2" + echo "$haystack" | grep -oE "[0-9]+ $pattern" | grep -oE '^[0-9]+' || echo 0 +} + +TOTAL=0; PASSED=0; FAILED=0 +if [[ -n "$SUMMARY" ]]; then + TOTAL=$(_extract_num "$SUMMARY" "tests? run") + PASSED=$(_extract_num "$SUMMARY" "passed") + FAILED=$(_extract_num "$SUMMARY" "failed") +fi + +rm -f "$TMPOUT" +printf '{"suite":"rust","total":%d,"passed":%d,"failed":%d,"duration_ms":%d}\n' \ + "${TOTAL:-0}" "${PASSED:-0}" "${FAILED:-0}" "$DURATION_MS" +exit $EXIT_CODE