#!/usr/bin/env bash set -eo pipefail mkdir -p artifacts/coverage if [[ "${TUI2_SMOKE_GATE:-0}" == "1" ]]; then ./scripts/tui2_check_smoke.sh fi # Pick an interpreter that can actually import pytest, not merely the first # one named "python" on PATH. On Windows the MSYS2 toolchain is prepended to # PATH so native wheels can build, and MSYS2 ships its own python.exe -- so a # bare `python` resolved to that one and the whole suite died with "No module # named pytest", after the determinism gate had already passed using the # `pytest` executable, which MSYS2 does not provide. candidates=() [[ -x ".venv/bin/python" ]] && candidates+=(".venv/bin/python") # setup-python exports pythonLocation but does not necessarily put it first on # PATH. On Windows the MSYS2 toolchain is prepended so native wheels can # build, and it ships its own python.exe AND python3 -- so BOTH bare names # resolve to an interpreter with no pytest, and the toolchain that has to be # on PATH is the very thing that hides the interpreter we want. if [[ -n "${pythonLocation:-}" ]]; then candidates+=("${pythonLocation}/python" "${pythonLocation}/python.exe" "${pythonLocation}/bin/python") fi candidates+=(python python3) # Last resort: pytest itself is resolvable here (the determinism gate runs it # that way), so its own directory locates a usable interpreter. if command -v pytest >/dev/null 2>&1; then pytest_dir="$(dirname "$(command -v pytest)")" candidates+=("$pytest_dir/python" "$pytest_dir/python.exe" "$pytest_dir/../python" "$pytest_dir/../python.exe") fi py_bin="" for candidate in "${candidates[@]}"; do if "$candidate" -c "import pytest" >/dev/null 2>&1; then py_bin="$candidate" break fi done if [[ -z "$py_bin" ]]; then echo "no interpreter can import pytest; tried:" >&2 printf ' %s\n' "${candidates[@]}" >&2 exit 1 fi echo "test interpreter: $py_bin" if [[ "${DETERMINISM_GATE:-1}" == "1" ]]; then ./scripts/run_determinism_tests.sh fi pytest_args=(-vv) if [[ -n "${STRESS_ARGS:-}" ]]; then pytest_args+=(${STRESS_ARGS}) fi if [[ "${RUNNER_OS:-}" == "Windows" ]]; then pytest_args+=(-n 1) fi pytest_args+=( --cov=src --cov-report=xml --cov-report=term-missing --cov-report=json:artifacts/coverage/coverage.json --cov-fail-under=20 src/tests ) timeout_bin="timeout" if ! command -v "$timeout_bin" >/dev/null 2>&1; then if command -v gtimeout >/dev/null 2>&1; then timeout_bin="gtimeout" else timeout_bin="" fi fi if [[ -n "$timeout_bin" ]]; then $timeout_bin 15m "$py_bin" -m pytest "${pytest_args[@]}" 2>&1 | tee pytest.log status=${PIPESTATUS[0]} else echo "timeout command not found; running tests without timeout" >&2 "$py_bin" -m pytest "${pytest_args[@]}" 2>&1 | tee pytest.log status=${PIPESTATUS[0]} fi if [[ $status -eq 124 ]]; then echo "::error::Tests exceeded 15-minute limit" tail -n 20 pytest.log exit 1 fi # The critical-coverage floor is a property of the codebase, not of a # platform, so it is measured on one. src/seedpass/core/manager.py holds # POSIX-only paths that cannot execute on Windows at all: it measures 60.74% # on Linux and 59.80% on Windows against a 60.00% floor -- and Windows runs # MORE tests, not fewer (1239 against 1223), so the gap is code that is # unreachable there rather than tests that did not run. A floor checked on # Windows measures platform reachability instead of test quality, and fails # by two tenths of a point for reasons no test author can act on. if [[ $status -eq 0 && "${CRITICAL_COVERAGE_GATE:-1}" == "1" ]]; then if [[ "${RUNNER_OS:-Linux}" == "Linux" ]]; then "$py_bin" scripts/check_critical_coverage.py \ artifacts/coverage/coverage.json \ --json-output artifacts/coverage/critical_gate.full.json else echo "critical coverage gate: not run on ${RUNNER_OS:-this platform}" \ "(the floor is measured on Linux -- see the comment above)" fi fi if [[ $status -eq 0 && "${TUI2_COVERAGE_GATE:-0}" == "1" ]]; then ./scripts/tui2_coverage_gate.sh fi if [[ $status -eq 0 && "${TUI3_COVERAGE_GATE:-1}" == "1" ]]; then ./scripts/tui3_coverage_gate.sh fi exit $status