#!/usr/bin/env bash # Every runner-visible contract has one capability classification in # tests/contracts.manifest. The manifest is deliberately complete: callers can # decide what is safe to run without rediscovering test behaviour themselves. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" manifest="$repo_dir/tests/contracts.manifest" discover_contracts() { discovered_contracts=() # Byte order, exactly as the runner discovers them. A UTF-8 collation folds # the punctuation away and reorders the pairs that differ only by `-` and # `_`, so a manifest correct here would be wrong on a machine with a # different LANG. while IFS= read -r path; do [[ -x "$path" || "$path" == *_test.py ]] || continue discovered_contracts+=("tests/${path#"$repo_dir/tests/"}") done < <(find "$repo_dir/tests" -type f \ -not -path '*/fixtures/*' -not -path '*__pycache__*' | LC_ALL=C sort) } validate_manifest() { local candidate="$1" local -n expected_contracts="$2" # Byte order, for the same reason discover_contracts sorts in it. local LC_ALL=C local line capabilities path extra previous_comment="" previous_was_comment=0 local -a capability_list=() local -A manifest_paths=() capability_counts=() local previous_path="" validation_findings=() validation_note() { validation_findings+=("$1"); } while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$line" =~ ^[[:space:]]*# ]]; then previous_comment="${line#*#}" previous_comment="${previous_comment#"${previous_comment%%[![:space:]]*}"}" previous_comment="${previous_comment%"${previous_comment##*[![:space:]]}"}" previous_was_comment=1 continue fi if [[ "$line" =~ ^[[:space:]]*$ ]]; then previous_comment="" previous_was_comment=0 continue fi IFS=$' \t' read -r capabilities path extra <<<"$line" if [[ -z "${capabilities:-}" || -z "${path:-}" || -n "${extra:-}" ]]; then validation_note "manifest line is not exactly two fields: $line" previous_comment="" previous_was_comment=0 continue fi if [[ -n "$previous_path" && "$path" < "$previous_path" ]]; then validation_note 'paths are not lexicographically sorted' fi previous_path="$path" if [[ -n "${manifest_paths[$path]:-}" ]]; then validation_note "duplicate path $path" fi manifest_paths["$path"]=1 IFS=',' read -r -a capability_list <<<"$capabilities" local -A line_capabilities=() local capability for capability in "${capability_list[@]}"; do if [[ -z "$capability" ]]; then validation_note "empty capability on $path" continue fi if [[ -n "${line_capabilities[$capability]:-}" ]]; then validation_note "duplicate capability $capability on $path" fi line_capabilities["$capability"]=1 case "$capability" in hermetic|live-host|live-compositor|live-desktop|network|privileged) capability_counts["$capability"]=1 ;; *) validation_note "unknown capability $capability on $path" ;; esac done if [[ -n "${line_capabilities[hermetic]:-}" && ${#line_capabilities[@]} -ne 1 ]]; then validation_note "hermetic must appear alone on $path" fi if [[ "$capabilities" != hermetic ]]; then if [[ "$previous_was_comment" -ne 1 ]]; then validation_note "$path is non-hermetic but lacks a directly preceding comment" elif [[ -z "$previous_comment" ]]; then validation_note "$path is non-hermetic but lacks a non-empty directly preceding comment" fi fi previous_comment="" previous_was_comment=0 done < "$candidate" local expected for expected in "${!expected_contracts[@]}"; do [[ -n "${manifest_paths[$expected]:-}" ]] || validation_note "missing contract $expected" done for path in "${!manifest_paths[@]}"; do [[ -n "${expected_contracts[$path]:-}" ]] || validation_note "stale manifest path $path" done for capability in live-host live-compositor live-desktop network; do [[ -n "${capability_counts[$capability]:-}" ]] || validation_note "manifest has no $capability contract" done if (( ${#validation_findings[@]} > 0 )); then printf 'contract manifest: %d finding(s)\n' "${#validation_findings[@]}" >&2 printf ' - %s\n' "${validation_findings[@]}" >&2 return 1 fi return 0 } cleanup_fixture() { [[ -n "${fixture:-}" ]] && rm -f -- "$fixture" } run_parser_fixture() { local label="$1" expected_message="$2" contents="$3" output fixture="" shift 3 local -A fixture_paths=() local fixture_path for fixture_path in "$@"; do fixture_paths["$fixture_path"]=1 done fixture="$(mktemp)" trap cleanup_fixture EXIT trap 'cleanup_fixture; exit 130' INT trap 'cleanup_fixture; exit 143' TERM printf '%s' "$contents" > "$fixture" if output="$(validate_manifest "$fixture" fixture_paths 2>&1)"; then printf 'contract manifest: parser fixture %s unexpectedly passed\n' "$label" >&2 cleanup_fixture trap - EXIT INT TERM return 1 fi cleanup_fixture trap - EXIT INT TERM if ! grep -Fq "$expected_message" <<<"$output"; then printf 'contract manifest: parser fixture %s did not name %q: %s\n' \ "$label" "$expected_message" "$output" >&2 return 1 fi } run_parser_fixtures() { run_parser_fixture missing-contract 'missing contract tests/b' \ $'hermetic tests/a\n' tests/a tests/b || return 1 run_parser_fixture stale-path 'stale manifest path tests/stale' \ $'hermetic tests/a\nhermetic tests/stale\n' tests/a || return 1 run_parser_fixture duplicate-path 'duplicate path tests/a' \ $'hermetic tests/a\nhermetic tests/a\n' tests/a || return 1 run_parser_fixture unknown-capability 'unknown capability unknown on tests/a' \ $'# Reads an external thing.\nunknown tests/a\n' tests/a || return 1 run_parser_fixture mixed-hermetic 'hermetic must appear alone on tests/a' \ $'# Uses the network.\nhermetic,network tests/a\n' tests/a || return 1 run_parser_fixture unsorted-paths 'paths are not lexicographically sorted' \ $'hermetic tests/b\nhermetic tests/a\n' tests/a tests/b || return 1 run_parser_fixture uncommented-non-hermetic \ 'tests/a is non-hermetic but lacks a directly preceding comment' \ $'network tests/a\n' tests/a || return 1 run_parser_fixture blank-comment \ 'tests/a is non-hermetic but lacks a non-empty directly preceding comment' \ $'#\nnetwork tests/a\n' tests/a || return 1 } [[ -r "$manifest" ]] || { printf 'contract manifest: %s is missing\n' "$manifest" >&2 exit 1 } discover_contracts declare -A discovered_paths=() for path in "${discovered_contracts[@]}"; do discovered_paths["$path"]=1 done status=0 validate_manifest "$manifest" discovered_paths || status=1 run_parser_fixtures || status=1 (( status == 0 )) || exit 1 printf 'contract manifest: PASS (%d discovered; %d manifested)\n' \ "${#discovered_contracts[@]}" "${#discovered_paths[@]}"