Files
home-network-k3s-impl/conformance/run.sh
T
jakman85andClaude Opus 4.8 416e16bdbf fix(conformance): pass absolute --report-path so run.sh is CWD-robust
run.sh exited non-zero (breaking the L2 gate) when invoked from the repo root:
chainsaw's config report.path is relative (_artifacts), so it resolved against
$PWD and failed to write ('open _artifacts/chainsaw-report.xml: no such file')
even though all tests passed. Pass --report-path ${ARTIFACTS} (absolute) +
mkdir -p first. Verified: R-NET-01 from repo root now exits 0 with report written.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 07:40:03 -05:00

195 lines
8.0 KiB
Bash
Executable File

#!/usr/bin/env bash
###############################################################################
# conformance/run.sh — the single-command conformance runner (R-TEST-02/03/04)
#
# ONE command runs the suite (or a requirement-/layer-scoped subset) and always
# emits JUnit + a requirement-coverage report. Local runs and the in-cluster CI
# Chainsaw Job invoke this same script so results are identical (reproducibility).
#
# ./run.sh # full suite; skips L1/L2/L3 if no cluster
# ./run.sh --requirements R-NET-01,R-NET-06 # merge-gate subset (tagged)
# ./run.sh --layer l2 # one layer
# ./run.sh --requirements R-NET-01,R-NET-02 --layer l2 # intersect: tagged AND in layer
# ./run.sh --coverage-only # regenerate coverage report only (no cluster)
# ./run.sh --lint # structural-validate test defs (no cluster)
# ./run.sh --strict-coverage # hygiene gate: fail on tag/annotation drift
# ./run.sh --requirements R-NET-01 # ALSO asserts those IDs are covered (merge gate)
# ./run.sh --require-all # phase-exit: every requirement must be covered
#
# Artifacts (always written under conformance/_artifacts/, gitignored):
# _artifacts/report.junit.xml JUnit (Chainsaw output, or skipped-scaffold shim)
# _artifacts/coverage/coverage.md human coverage report
# _artifacts/coverage/coverage.json machine coverage report
#
# Phase 0: no cluster exists. The behavior tests are valid scaffolds that cannot
# execute yet, so run.sh emits a *skipped* JUnit and a coverage report, and exits
# green — the harness is proven reproducible while later phases fill in behavior.
###############################################################################
set -euo pipefail
CONF_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export CONF_ROOT
# shellcheck source=lib/common.sh
source "${CONF_ROOT}/lib/common.sh"
# shellcheck source=versions.env
source "${CONF_ROOT}/versions.env"
ARTIFACTS="${CONF_ROOT}/_artifacts"
COVERAGE_OUT="${ARTIFACTS}/coverage"
JUNIT_OUT="${ARTIFACTS}/report.junit.xml"
PYTHON="${PYTHON:-python3}"
LAYER="all"
REQS=""
MODE="run" # run | coverage-only | lint
STRICT_COVERAGE=0
REQUIRE_ALL=0
FORCE_NO_CLUSTER=0
# Documented, out-of-scope coverage gaps exempt from the completeness gate: a
# safety-critical requirement owned by another workstream that WS-TEST does not yet
# scaffold. R-STATE-02 (node-loss) is delivered with Longhorn/infra (WS-STORAGE).
# Listed here so the exemption is explicit and reviewable, not silent.
DOCUMENTED_GAPS="R-STATE-02"
usage() { sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
while [ $# -gt 0 ]; do
case "$1" in
--layer) LAYER="$2"; shift 2 ;;
--requirements) REQS="$(conf_normalize_reqs "$2")"; shift 2 ;;
--coverage-only) MODE="coverage-only"; shift ;;
--lint) MODE="lint"; shift ;;
--strict-coverage) STRICT_COVERAGE=1; shift ;;
--require-all) REQUIRE_ALL=1; shift ;;
--no-cluster) FORCE_NO_CLUSTER=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown arg: $1" >&2; usage; exit 2 ;;
esac
done
mkdir -p "${ARTIFACTS}" "${COVERAGE_OUT}"
log() { printf '\033[1m[run.sh]\033[0m %s\n' "$*"; }
# --- Coverage report is always produced (cluster-independent, R-TEST-04) --------------
gen_coverage() {
local flags=(--allow-gap "${DOCUMENTED_GAPS}")
[ "${STRICT_COVERAGE}" = 1 ] && flags+=(--strict)
# A tagged subset run is also the merge gate: the touched requirements MUST be covered.
[ -n "${REQS}" ] && flags+=(--require "${REQS}")
[ "${REQUIRE_ALL}" = 1 ] && flags+=(--require-all)
"${PYTHON}" "${CONF_ROOT}/lib/coverage.py" \
--registry "${CONF_ROOT}/requirements/registry.yaml" \
--tests "${CONF_ROOT}/tests" \
--out "${COVERAGE_OUT}" \
"${flags[@]}"
}
# --- Structural lint (no cluster): parse each test def and check the tag convention ----
lint_tests() {
log "Linting test definitions (structural, no cluster)"
local rc=0 f
if command -v chainsaw >/dev/null 2>&1; then
# Prefer chainsaw's own linter when the pinned binary is available.
while IFS= read -r f; do
chainsaw lint test --file "$f" >/dev/null || { echo "LINT FAIL: $f" >&2; rc=1; }
done < <(conf_all_test_files)
else
log "chainsaw not on PATH; falling back to structural check"
# Minimal YAML-shape check: kind Test + at least one requirement label per file.
while IFS= read -r f; do
grep -Eq '^kind:\s*Test' "$f" || { echo "LINT FAIL (no kind: Test): $f" >&2; rc=1; }
grep -Eq "^[[:space:]]*${CONF_LABEL_PREFIX}/R-[A-Z0-9]+-[0-9]+:" "$f" \
|| { echo "LINT FAIL (no requirement label): $f" >&2; rc=1; }
done < <(conf_all_test_files)
fi
return $rc
}
# --- Select which test directories Chainsaw should run --------------------------------
select_dirs() {
# --requirements AND --layer together => intersection (tests carrying the tag
# AND living in that layer subtree). Either alone keeps its prior meaning, so
# this is backward compatible (LAYER defaults to "all"). Both helpers emit
# sorted, de-duplicated dirs, so `comm -12` yields the intersection.
if [ -n "${REQS}" ] && [ "${LAYER}" != all ]; then
comm -12 \
<(conf_dirs_for_requirements "${REQS}") \
<(conf_dirs_for_layer "${LAYER}")
elif [ -n "${REQS}" ]; then
conf_dirs_for_requirements "${REQS}"
else
conf_dirs_for_layer "${LAYER}"
fi
}
# --- Execute the Chainsaw suite against a live cluster ---------------------------------
run_chainsaw() {
local -a dirs
mapfile -t dirs < <(select_dirs)
if [ "${#dirs[@]}" -eq 0 ]; then
log "No tests match the selection (layer=${LAYER} reqs='${REQS}')."
return 0
fi
local -a test_dir_args=()
for d in "${dirs[@]}"; do test_dir_args+=(--test-dir "$d"); done
log "Running Chainsaw over ${#dirs[@]} test dir(s) with pinned ${CHAINSAW_VERSION}"
# Chainsaw writes JUnit per config/chainsaw-config.yaml (report.* under _artifacts).
# --report-path is passed ABSOLUTE (overriding the config's relative `_artifacts`) so the report
# write succeeds regardless of the caller's CWD — otherwise chainsaw's relative path resolves
# against $PWD and fails ("open _artifacts/chainsaw-report.xml: no such file") when run.sh is
# invoked from the repo root instead of conformance/.
mkdir -p "${ARTIFACTS}"
chainsaw test \
--config "${CONF_ROOT}/config/chainsaw-config.yaml" \
--report-path "${ARTIFACTS}" \
"${test_dir_args[@]}"
# Normalize whatever chainsaw named the report to a stable path CI collects.
local produced
produced="$(find "${ARTIFACTS}" -maxdepth 1 -name 'chainsaw-report*.xml' -o \
-name 'report*.xml' 2>/dev/null | grep -v report.junit.xml | head -1 || true)"
if [ -n "${produced}" ] && [ "${produced}" != "${JUNIT_OUT}" ]; then
cp "${produced}" "${JUNIT_OUT}"
fi
}
###############################################################################
# Main
###############################################################################
log "Coverage report first (always)"
gen_coverage
case "${MODE}" in
coverage-only)
log "coverage-only: done. See ${COVERAGE_OUT}/coverage.md"
exit 0
;;
lint)
lint_tests
log "lint: done."
exit 0
;;
esac
# run mode: need a cluster for L1/L2/L3. Without one (Phase 0), emit skipped JUnit.
if [ "${FORCE_NO_CLUSTER}" = 1 ] || ! conf_cluster_reachable; then
log "No reachable cluster — emitting SKIPPED JUnit scaffold (Phase 0 behavior)."
log "Also running structural lint so scaffolds stay valid."
lint_tests || { log "lint failed"; exit 1; }
"${PYTHON}" "${CONF_ROOT}/lib/emit_skipped_junit.py" \
--tests "${CONF_ROOT}/tests" --out "${JUNIT_OUT}"
log "Done (scaffold mode). JUnit: ${JUNIT_OUT} | Coverage: ${COVERAGE_OUT}/coverage.md"
exit 0
fi
command -v chainsaw >/dev/null 2>&1 || {
echo "ERROR: cluster reachable but 'chainsaw' (${CHAINSAW_VERSION}) not on PATH." >&2
echo "Install the pinned binary or run via the in-cluster Job (incluster/)." >&2
exit 3
}
run_chainsaw
log "Done. JUnit: ${JUNIT_OUT} | Coverage: ${COVERAGE_OUT}/coverage.md"