Under set -e a missing TAP summary aborted run_site without a refuse message; the explicit refuse checks now report it. The site's test script pins the TAP reporter so the summary lines exist on Node 23+.
307 lines
11 KiB
Bash
Executable File
307 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Phase 11.2 fail-closed gate (summercms.io Alpha 0.1 landing page).
|
|
#
|
|
# The phase spans four repositories: summercms.go (this one, the framework),
|
|
# sm-summercmsio-app (the application, a sibling directory), its site plugin
|
|
# submodule plugins/golem15/summercms and its Nuxt site submodule
|
|
# vue-summercmsio-app. Each stage runs one repository's checks. Go tests run
|
|
# with -json through a detector that requires every named test to PASS: a
|
|
# failure, a skip, a missing or renamed test, zero matched tests and "no
|
|
# tests to run" all refuse.
|
|
set -euo pipefail
|
|
|
|
SCRATCH=()
|
|
cleanup() {
|
|
[ "${#SCRATCH[@]}" -eq 0 ] || rm -rf "${SCRATCH[@]}"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
ROOT="${PHASE11_2_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
APP="${PHASE11_2_APP:-$ROOT/../sm-summercmsio-app}"
|
|
PLUG="$APP/plugins/golem15/summercms"
|
|
SITE="$APP/vue-summercmsio-app"
|
|
APP_PKG="git.golem15.com/golem15/sm-summercmsio-app"
|
|
# Statement coverage floors (SC5).
|
|
PLUGIN_COVERAGE_MIN=90.0
|
|
DOCSITE_COVERAGE_MIN=85.0
|
|
|
|
usage() {
|
|
cat >&2 <<'EOF'
|
|
usage:
|
|
check-phase11.2.sh --framework
|
|
check-phase11.2.sh --plugin
|
|
check-phase11.2.sh --app
|
|
check-phase11.2.sh --site
|
|
check-phase11.2.sh --built
|
|
check-phase11.2.sh --smoke
|
|
check-phase11.2.sh --deploy
|
|
check-phase11.2.sh --terminal [--verbatim]
|
|
check-phase11.2.sh --full
|
|
check-phase11.2.sh --all
|
|
EOF
|
|
exit 2
|
|
}
|
|
|
|
refuse() {
|
|
echo "refuse: $*" >&2
|
|
return 1
|
|
}
|
|
|
|
need_dir() {
|
|
[ -d "$1" ] || refuse "$1 not found (the phase expects sm-summercmsio-app next to summercms.go)"
|
|
}
|
|
|
|
# detect_json reads a go test -json log. It refuses a build failure, any
|
|
# failed test or package, any skipped test, "no tests to run", a run with
|
|
# zero passing tests, and any required "<package> <Test>" pair (from
|
|
# PHASE11_2_REQUIRE, newline separated) that did not PASS.
|
|
detect_json() {
|
|
python3 - "$1" <<'PY'
|
|
import json, os, sys
|
|
path = sys.argv[1]
|
|
require = [r.strip() for r in os.environ.get("PHASE11_2_REQUIRE", "").splitlines() if r.strip()]
|
|
passed = set()
|
|
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
for raw in fh:
|
|
line = raw.strip()
|
|
if not line.startswith("{"):
|
|
continue
|
|
try:
|
|
ev = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
print("refuse: non-json test output", file=sys.stderr)
|
|
sys.exit(4)
|
|
action, test, pkg = ev.get("Action"), ev.get("Test") or "", ev.get("Package") or ""
|
|
if action == "build-fail" or (action == "fail" and ev.get("FailedBuild")):
|
|
print(f"refuse: build failed in {pkg}", file=sys.stderr)
|
|
sys.exit(1)
|
|
if action == "output" and "no tests to run" in (ev.get("Output") or ""):
|
|
print(f"refuse: no tests to run in {pkg}", file=sys.stderr)
|
|
sys.exit(3)
|
|
if action == "skip" and test:
|
|
print(f"refuse: skipped {pkg} {test}", file=sys.stderr)
|
|
sys.exit(2)
|
|
if action == "fail":
|
|
print(f"refuse: failed {pkg} {test}".rstrip(), file=sys.stderr)
|
|
sys.exit(1)
|
|
if action == "pass" and test:
|
|
passed.add(f"{pkg} {test}")
|
|
if not passed:
|
|
print("refuse: zero tests", file=sys.stderr)
|
|
sys.exit(3)
|
|
missing = [r for r in require if r not in passed]
|
|
if missing:
|
|
print("refuse: named tests did not pass (missing, renamed or filtered out): " + ", ".join(missing), file=sys.stderr)
|
|
sys.exit(5)
|
|
print(f"named tests passed: {len(require)} required, {len(passed)} passing (subtests included)")
|
|
PY
|
|
}
|
|
|
|
# named_tests DIR PKG NAME... runs `go -C DIR test -json -count=1 PKG -run
|
|
# '^(NAME|...)$'` and requires every named test to PASS. Environment set on
|
|
# the call (VAR=1 named_tests ...) reaches go test; run it in a subshell to
|
|
# unset a variable for one call.
|
|
named_tests() {
|
|
local dir="$1" pkg="$2" log import names name require="" code=0
|
|
shift 2
|
|
import="$(go -C "$dir" list -f '{{.ImportPath}}' "$pkg")" || refuse "go list $pkg in $dir failed" || return 1
|
|
names="$(IFS='|'; echo "$*")"
|
|
for name in "$@"; do
|
|
require+="$import $name"$'\n'
|
|
done
|
|
log="$(mktemp)"
|
|
go -C "$dir" test -json -count=1 "$pkg" -run "^($names)\$" >"$log" 2>&1 || code=$?
|
|
if ! PHASE11_2_REQUIRE="$require" detect_json "$log"; then
|
|
rm -f "$log"
|
|
return 1
|
|
fi
|
|
rm -f "$log"
|
|
[ "$code" -eq 0 ] || refuse "go test $pkg in $dir exited $code"
|
|
}
|
|
|
|
# coverage_at_least DIR MIN PKG... prints the total statement coverage of
|
|
# the packages and refuses when it is below MIN. Build-gated tests skip
|
|
# here (their variables are cleared), so the figure comes from tests that
|
|
# need no build output.
|
|
coverage_at_least() {
|
|
local dir="$1" min="$2" profile total
|
|
shift 2
|
|
profile="$(mktemp)"
|
|
SCRATCH+=("$profile")
|
|
env -u SUMMERCMS_REQUIRE_BUILD -u SUMMERCMS_TERMINAL_CHECK -u SUMMERCMS_CHECK_EXTERNAL \
|
|
go -C "$dir" test -count=1 -coverprofile="$profile" "$@" >/dev/null || refuse "coverage run in $dir failed" || return 1
|
|
total="$(go -C "$dir" tool cover -func="$profile" | awk '/^total:/ {sub("%", "", $NF); print $NF}')"
|
|
[ -n "$total" ] || refuse "no coverage total in $dir" || return 1
|
|
echo "coverage: $total% of statements in $dir (minimum $min%)"
|
|
awk -v t="$total" -v m="$min" 'BEGIN { exit !(t + 0 >= m + 0) }' || refuse "coverage $total% is below $min% in $dir"
|
|
}
|
|
|
|
# Framework tests of the D-41/D-46 site link, plus the docs checker.
|
|
DOCSITE_TESTS=(TestParseSite TestCheckSiteURL TestSiteLabel TestSiteURLPrecedence TestSiteLink)
|
|
SUMMER_TESTS=(TestDocsTree TestDocsBuildRealTree TestDocsBuildSiteFlags TestDocsSiteFlagsInHelp)
|
|
|
|
run_framework() {
|
|
(cd "$ROOT" && go vet ./...)
|
|
named_tests "$ROOT" ./internal/docsite "${DOCSITE_TESTS[@]}" || refuse "framework: internal/docsite named tests"
|
|
named_tests "$ROOT" ./cmd/summer "${SUMMER_TESTS[@]}" || refuse "framework: cmd/summer named tests"
|
|
coverage_at_least "$ROOT" "$DOCSITE_COVERAGE_MIN" ./internal/docsite
|
|
"$ROOT/scripts/check-phase11.1.sh" --docs
|
|
"$ROOT/scripts/check-phase11.1.sh" --forbidden
|
|
echo "phase11.2 framework passed"
|
|
}
|
|
|
|
PLUGIN_TESTS=(TestStaticSmoke TestStaticSite TestStaticDocs TestStaticConditionalAndRange TestStaticNoBlockingHeaders
|
|
TestStaticRedirectLocations TestStaticMissing404Page TestNewHandlersMissingIndex TestContentType TestSiteImmutable
|
|
TestRoutesAssemble TestRoutesFailClosed TestRoutesCoexistWithAdminPatterns TestPluginIdentity TestPluginEmbeddedTree
|
|
TestPageLinks TestResolve)
|
|
|
|
run_plugin() {
|
|
need_dir "$PLUG"
|
|
go -C "$PLUG" vet ./...
|
|
(unset SUMMERCMS_REQUIRE_BUILD && named_tests "$PLUG" . "${PLUGIN_TESTS[@]}") || refuse "plugin: named tests"
|
|
coverage_at_least "$PLUG" "$PLUGIN_COVERAGE_MIN" ./...
|
|
echo "phase11.2 plugin passed"
|
|
}
|
|
|
|
# The D-40 terminal-check helpers (the gated TestTerminalCommands runs in
|
|
# the terminal stage).
|
|
APP_TESTS=(TestLoadTerminal TestTerminalScript TestTerminalEnv)
|
|
|
|
run_app() {
|
|
local pkgs
|
|
need_dir "$APP"
|
|
go -C "$APP" vet ./...
|
|
pkgs="$(go -C "$APP" list ./...)"
|
|
[ "$pkgs" = "$APP_PKG" ] || refuse "app: go list ./... printed '$pkgs', want only $APP_PKG"
|
|
(unset SUMMERCMS_TERMINAL_CHECK && named_tests "$APP" . "${APP_TESTS[@]}") || refuse "app: named tests"
|
|
echo "phase11.2 app passed"
|
|
}
|
|
|
|
# run_site installs from the lockfile, generates the static site and runs
|
|
# the node:test suites (output, terminal and scroll-spy tests).
|
|
run_site() {
|
|
local log
|
|
need_dir "$SITE"
|
|
command -v pnpm >/dev/null || refuse "site: pnpm not found"
|
|
pnpm -C "$SITE" install --frozen-lockfile
|
|
pnpm -C "$SITE" run generate
|
|
log="$(mktemp)"
|
|
SCRATCH+=("$log")
|
|
pnpm -C "$SITE" test >"$log" 2>&1 || {
|
|
cat "$log" >&2
|
|
refuse "site: pnpm test failed"
|
|
}
|
|
# Informational; a missing TAP summary is reported by the refuse checks below.
|
|
grep -E '^# (tests|pass|fail|skipped|todo|cancelled) ' "$log" || true
|
|
grep -qx '# fail 0' "$log" || refuse "site: no '# fail 0' line"
|
|
grep -qE '^# pass [1-9][0-9]*$' "$log" || refuse "site: no passing tests"
|
|
grep -qx '# skipped 0' "$log" || refuse "site: skipped tests"
|
|
grep -qx '# todo 0' "$log" || refuse "site: todo tests"
|
|
echo "phase11.2 site passed"
|
|
}
|
|
|
|
# Build-dependent plugin tests: they skip without SUMMERCMS_REQUIRE_BUILD
|
|
# and fail (not skip) on a missing build with it, so the gate sets it.
|
|
BUILT_TESTS=(TestLandingLinks TestTerminalCommandsInPage TestDocsHeaderSiteLink)
|
|
|
|
# run_built builds bin/summercms-io and the embedded trees, then requires the
|
|
# build-dependent plugin tests to PASS against them. The build is a release
|
|
# build when the framework checkout build.sh uses has the v0.1.0 tag (D-42),
|
|
# else a dev build from the framework HEAD.
|
|
run_built() {
|
|
local fw="${SUMMERCMS_FRAMEWORK:-$ROOT}" mode=dev
|
|
need_dir "$APP"
|
|
if git -C "$fw" rev-parse -q --verify refs/tags/v0.1.0 >/dev/null; then
|
|
mode=release
|
|
echo "built: v0.1.0 found in $fw, release build"
|
|
else
|
|
echo "built: no v0.1.0 tag in $fw, dev build (framework HEAD)"
|
|
fi
|
|
"$APP/scripts/build.sh" "$mode"
|
|
(export SUMMERCMS_REQUIRE_BUILD=1 && named_tests "$PLUG" . "${BUILT_TESTS[@]}") || refuse "built: build-dependent plugin tests"
|
|
echo "phase11.2 built passed"
|
|
}
|
|
|
|
# expect_line CMD... runs a script and requires its own success line ($1).
|
|
expect_line() {
|
|
local want="$1" log code=0
|
|
shift
|
|
log="$(mktemp)"
|
|
SCRATCH+=("$log")
|
|
"$@" >"$log" 2>&1 || code=$?
|
|
cat "$log"
|
|
[ "$code" -eq 0 ] || refuse "$1 exited $code"
|
|
grep -qx -- "$want" "$log" || refuse "$1 printed no '$want' line"
|
|
}
|
|
|
|
run_smoke() {
|
|
expect_line "smoke: ok" "$APP/scripts/smoke.sh"
|
|
echo "phase11.2 smoke passed"
|
|
}
|
|
|
|
run_deploy() {
|
|
expect_line "check-deploy: ok" "$APP/scripts/check-deploy.sh"
|
|
echo "phase11.2 deploy passed"
|
|
}
|
|
|
|
# run_terminal runs the landing page's six commands from a fresh shell
|
|
# (D-40) with TestTerminalCommands required to PASS. By default the clone
|
|
# URL is this checkout, because the public repository may not exist yet;
|
|
# --verbatim leaves the page's URL untouched, the cutover run after D-38.
|
|
# The variables are set in subshells on purpose: they apply to one run only.
|
|
# shellcheck disable=SC2030,SC2031
|
|
run_terminal() {
|
|
need_dir "$APP"
|
|
if [ "${1:-}" = --verbatim ]; then
|
|
echo "terminal: verbatim, cloning the page's URL"
|
|
(unset SUMMERCMS_CLONE_URL && export SUMMERCMS_TERMINAL_CHECK=1 &&
|
|
named_tests "$APP" . TestTerminalCommands) || refuse "terminal: TestTerminalCommands (verbatim)"
|
|
else
|
|
echo "terminal: clone override $ROOT"
|
|
(export SUMMERCMS_TERMINAL_CHECK=1 SUMMERCMS_CLONE_URL="$ROOT" &&
|
|
named_tests "$APP" . TestTerminalCommands) || refuse "terminal: TestTerminalCommands"
|
|
fi
|
|
echo "phase11.2 terminal passed"
|
|
}
|
|
|
|
# run_full is the framework's whole suite, the Docker-backed database
|
|
# suites included.
|
|
run_full() {
|
|
(cd "$ROOT" && go vet ./... && go test ./... -count=1)
|
|
echo "phase11.2 full passed"
|
|
}
|
|
|
|
MODE="${1:-}"
|
|
if [ "$MODE" = --terminal ]; then
|
|
if [ $# -gt 2 ] || { [ $# -eq 2 ] && [ "$2" != --verbatim ]; }; then
|
|
usage
|
|
fi
|
|
elif [ $# -ne 1 ]; then
|
|
usage
|
|
fi
|
|
|
|
case "$MODE" in
|
|
--framework) run_framework ;;
|
|
--plugin) run_plugin ;;
|
|
--app) run_app ;;
|
|
--site) run_site ;;
|
|
--built) run_built ;;
|
|
--smoke) run_smoke ;;
|
|
--deploy) run_deploy ;;
|
|
--terminal) run_terminal "${2:-}" ;;
|
|
--full) run_full ;;
|
|
--all)
|
|
run_framework
|
|
run_plugin
|
|
run_app
|
|
run_site
|
|
run_built
|
|
run_smoke
|
|
run_deploy
|
|
run_terminal
|
|
run_full
|
|
echo "phase11.2 all passed"
|
|
;;
|
|
*) usage ;;
|
|
esac
|