mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 17:18:49 -04:00
feat(addons): add the add-on runtime, navigation and first-party add-ons
Add the add-on runtime to LocalAI: manifests, the host operation broker, admin gating by account role, the add-on UI bundle builder and the SDK for add-on pages. Move the navigation to Chat, Models, Add-ons and Settings, with pinned add-ons in the sidebar and an Add-ons drawer. Ship the first-party add-ons (voice, face, fine-tuning, quantization) as packages for linux/amd64, linux/arm64 and darwin/arm64. Intel macOS (darwin/amd64) is enabled in the platform files. Squashed from the review branch. The tree is unchanged; the pre-squash history is kept on a local backup branch. Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
733 files changed
+149964
-323
No files matched your search
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Prints what one workflow event may publish, as key=value lines:
|
||||
# publish, reason, immutable, aliases, channel, signer_ref, version
|
||||
#
|
||||
# sh scripts/addons/channel.sh --event <name> --ref <full ref> --sha <40 hex>
|
||||
# [--draft] [--prerelease]
|
||||
#
|
||||
# Pull requests and branch pushes never publish, and master alone publishes
|
||||
# development. A tag push prepares the immutable version and moves no alias.
|
||||
# A published stable release moves latest.
|
||||
# Anything the rules do not name fails.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
if [ -n "${ADDONPKG:-}" ]; then
|
||||
exec "$ADDONPKG" channel "$@"
|
||||
fi
|
||||
exec go run ./scripts/addons/cmd/addonpkg channel "$@"
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Command addonpkg is the command line of pkg/addons/packaging: build, verify
|
||||
// and publish first-party add-on OCI packages. scripts/addons/package.sh and
|
||||
// the add-on package workflow call it.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAI/pkg/addons/packaging"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := packaging.Run(os.Args[1:], os.Stdout); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "addonpkg: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs the add-on UI runtime checks and prints one JSON case line per test:
|
||||
# 1. the node unit tests of the SDK, loader, style scoping and registry;
|
||||
# 2. the Go specs of the immutable asset route, the apps UI entry and the
|
||||
# owner's lifecycle rules, through real stored keys and sessions;
|
||||
# 3. the Playwright spec against the UI test server running with real
|
||||
# authentication and a real add-on owner, which installs the packages of
|
||||
# tests/addons/fixture/ui AFTER the host build, from a catalog on
|
||||
# loopback, with the protocol fixture as their process.
|
||||
# verify.sh compares the lines with the exact inventory of the esm-ui suite and
|
||||
# rejects a skipped, flaky or missing case.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
ui="$root/core/http/react-ui"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
|
||||
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
|
||||
# 1. Pure SDK, loader, style scoping and registry rules.
|
||||
(cd "$ui" && node tests/unit/run.mjs esm.test.mjs)
|
||||
|
||||
# 2. Go: ginkgo specs of this feature, reported as case lines. The label
|
||||
# "assets" selects them; every other spec of these packages belongs to the
|
||||
# suites that own them.
|
||||
i=0
|
||||
for pkg in ./core/services/addons/apps ./core/services/addons ./core/services/addons/gateway ./core/http/endpoints/addons; do
|
||||
i=$((i + 1))
|
||||
go_report="$cache/logs/esm-ui-go-$i.json"
|
||||
rm -f "$go_report"
|
||||
go test -count=1 -p 2 -tags auth "$pkg" -ginkgo.label-filter=assets -ginkgo.json-report="$go_report" >"$cache/logs/esm-ui-go-$i.log" 2>&1 || { tail -40 "$cache/logs/esm-ui-go-$i.log" >&2; exit 1; }
|
||||
python3 -I - "$go_report" <<'PY'
|
||||
import json, sys
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It':
|
||||
continue
|
||||
labels = set(spec.get('LeafNodeLabels') or [])
|
||||
for group in spec.get('ContainerHierarchyLabels') or []:
|
||||
labels.update(group or [])
|
||||
# A spec the label filter left out is another suite's, not skipped here.
|
||||
if 'assets' not in labels:
|
||||
continue
|
||||
status = {'passed': 'passed', 'skipped': 'skipped'}.get(spec['State'], 'failed')
|
||||
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
|
||||
PY
|
||||
done
|
||||
|
||||
# 3. The real owner and the browser.
|
||||
fixture_exe="$cache/tmp/fixture-addon"
|
||||
go build -p 2 -tags auth -o "$fixture_exe" ./tests/addons/fixture/cmd
|
||||
port=$(python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])')
|
||||
creds="$cache/logs/esm-ui-credentials.json"
|
||||
rm -f "$creds"
|
||||
log="$cache/logs/esm-ui-server.log"
|
||||
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$port" \
|
||||
--addons-fixture="$root/tests/addons/fixture/ui" --addon-executable="$fixture_exe" --fixture-out="$creds" \
|
||||
--fixture-enable=uifixture@1.0.0,cssb@1.0.0,bare@1.0.0 >"$log" 2>&1 &
|
||||
server=$!
|
||||
trap 'kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true' EXIT HUP INT TERM
|
||||
i=0
|
||||
until [ -s "$creds" ] && python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
|
||||
i=$((i + 1))
|
||||
kill -0 "$server" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$log" >&2; exit 1; }
|
||||
[ "$i" -lt 120 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
report="$cache/logs/esm-ui-report.json"
|
||||
rm -f "$report"
|
||||
status=0
|
||||
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$port" ADDONS_FIXTURE="$creds" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.esm.config.js >"$cache/logs/esm-ui-playwright.log" 2>&1) || status=$?
|
||||
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
|
||||
exit "$status"
|
||||
Executable
+212
@@ -0,0 +1,212 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs the add-ons foundation acceptance gate and prints one JSON case line per
|
||||
# check:
|
||||
# 1. Go: the integration specs of tests/addons/foundation. They start the
|
||||
# real owner and two frontends as separate server processes with a shared
|
||||
# account store, the real mTLS carrier and the protocol fixture as the
|
||||
# add-on process, and drive them through real sessions and API keys;
|
||||
# 2. regression: the existing model and backend gallery specs and the process
|
||||
# adapter specs, which the add-on lifecycle must not disturb. They run
|
||||
# offline: core/gallery (gallery, backend gallery, OCI install paths),
|
||||
# core/services/galleryop (the operations the gallery routes enqueue) and
|
||||
# pkg/model (the process adapter in process_runtime.go);
|
||||
# 3. the Playwright spec against the same three servers, which proves server
|
||||
# authorization from the browser.
|
||||
# verify.sh compares the lines with the exact inventory of the
|
||||
# foundation-acceptance suite and rejects a skipped, flaky or missing case.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
ui="$root/core/http/react-ui"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
|
||||
for file in tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
[ -f "$ui/dist/index.html" ] || { echo 'missing UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
|
||||
work=$(mktemp -d "$cache/tmp/foundation-gate.XXXXXX")
|
||||
cluster_pid=
|
||||
. "$root/scripts/addons/leaks.sh"
|
||||
leaked=0
|
||||
cleanup() {
|
||||
code=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
if [ -n "$cluster_pid" ]; then
|
||||
# The cluster tears its servers down on SIGTERM; wait for it, bounded.
|
||||
kill "$cluster_pid" 2>/dev/null || true
|
||||
n=0
|
||||
while kill -0 "$cluster_pid" 2>/dev/null && [ "$n" -lt 60 ]; do
|
||||
n=$((n + 1))
|
||||
sleep 0.5
|
||||
done
|
||||
kill -KILL "$cluster_pid" 2>/dev/null || true
|
||||
wait "$cluster_pid" 2>/dev/null || true
|
||||
fi
|
||||
# Nothing that names the work or scratch directories may survive. Whatever
|
||||
# does is killed, and the gate fails.
|
||||
reap_leaks "$work" foundation || leaked=1
|
||||
reap_leaks "$cache/tmp/" foundation paths-only || leaked=1
|
||||
find "$work" -type d -exec chmod u+rwx {} + 2>/dev/null || true
|
||||
rm -rf "$work"
|
||||
if [ "$leaked" -ne 0 ] && [ "$code" -eq 0 ]; then
|
||||
code=1
|
||||
fi
|
||||
exit "$code"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 143' HUP INT TERM
|
||||
|
||||
# The binaries are built here, never taken from an earlier build: the server
|
||||
# embeds the UI build and every other part of the fixture.
|
||||
go build -p 2 -tags auth -o "$work/ui-test-server" ./tests/e2e-ui
|
||||
go build -p 2 -tags auth -o "$work/fixture-addon" ./tests/addons/fixture/cmd
|
||||
go build -p 2 -tags auth -o "$work/foundation-cluster" ./tests/addons/foundation/cmd/foundation-cluster
|
||||
|
||||
# One case line per spec of a Ginkgo report. $2 prefixes the case.
|
||||
report_cases() {
|
||||
python3 -I - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It':
|
||||
continue
|
||||
status = {'passed': 'passed', 'skipped': 'skipped'}.get(spec['State'], 'failed')
|
||||
print(json.dumps({'case': sys.argv[2] + spec['LeafNodeText'], 'status': status}))
|
||||
PY
|
||||
}
|
||||
|
||||
# 1. The integration specs.
|
||||
go_report="$cache/logs/foundation-go.json"
|
||||
rm -f "$go_report"
|
||||
FOUNDATION_SERVER="$work/ui-test-server" FOUNDATION_FIXTURE="$work/fixture-addon" FOUNDATION_MOCK="$root/tests/e2e/mock-backend/mock-backend" \
|
||||
go test -count=1 -p 2 -tags auth -timeout 20m ./tests/addons/foundation -ginkgo.json-report="$go_report" >"$cache/logs/foundation-go.log" 2>&1 || {
|
||||
# Name the failed specs for the runner, then show the end of the log.
|
||||
report_cases "$go_report" 'go: ' 2>/dev/null | grep -v '"status": "passed"\|"status": "skipped"' || true
|
||||
tail -60 "$cache/logs/foundation-go.log" >&2
|
||||
exit 1
|
||||
}
|
||||
report_cases "$go_report" 'go: '
|
||||
|
||||
# 2. The existing gallery and process adapter specs. A package case passes when
|
||||
# its suite passed with at least one spec and no failed or pending one. The only
|
||||
# skips allowed are the specs that say they need macOS: they cannot run here.
|
||||
# core/gallery gets its fixture directory, so its model install specs run.
|
||||
# A package may name one spec to run alone (see below).
|
||||
regression() {
|
||||
pkg=$1
|
||||
label=$2
|
||||
isolate=${3:-}
|
||||
base="$cache/logs/foundation-regression-$(printf %s "$pkg" | tr '/.' '--')"
|
||||
rm -f "$base.json" "$base.main.json" "$base.isolated.json" "$base.plan.json"
|
||||
if [ -z "$isolate" ]; then
|
||||
FIXTURES="$root/tests/fixtures" go test -count=1 -p 2 -tags auth -timeout 15m "$pkg" -ginkgo.json-report="$base.json" >"$base.log" 2>&1 || { tail -40 "$base.log" >&2; exit 1; }
|
||||
python3 -I - "$base.json" "" "$label" <<'PY'
|
||||
import json, sys
|
||||
specs = platform = failed = 0
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It':
|
||||
continue
|
||||
specs += 1
|
||||
if spec['State'] == 'passed':
|
||||
continue
|
||||
message = (spec.get('Failure') or {}).get('Message', '').lower()
|
||||
if spec['State'] == 'skipped' and 'darwin' in message:
|
||||
platform += 1
|
||||
else:
|
||||
failed += 1
|
||||
ok = specs > 0 and failed == 0
|
||||
print(json.dumps({'case': sys.argv[3], 'status': 'passed' if ok else 'failed', 'specs': specs, 'platformSkipped': platform}))
|
||||
PY
|
||||
return
|
||||
fi
|
||||
# One spec of the package replaces the process-wide logger. Specs that log
|
||||
# from their own goroutines at the same time make it fail at random, so it
|
||||
# runs alone, once, and the rest of the package runs without it. A dry run
|
||||
# fixes the inventory: the two runs must account for every spec exactly
|
||||
# once, the isolated run for exactly one.
|
||||
FIXTURES="$root/tests/fixtures" go test -count=1 -tags auth "$pkg" -ginkgo.dry-run -ginkgo.json-report="$base.plan.json" >"$base.plan.log" 2>&1 || { tail -40 "$base.plan.log" >&2; exit 1; }
|
||||
FIXTURES="$root/tests/fixtures" go test -count=1 -p 2 -tags auth -timeout 15m "$pkg" -ginkgo.skip="$isolate" -ginkgo.json-report="$base.main.json" >"$base.main.log" 2>&1 || { tail -40 "$base.main.log" >&2; exit 1; }
|
||||
FIXTURES="$root/tests/fixtures" go test -count=1 -p 2 -tags auth -timeout 15m "$pkg" -ginkgo.focus="$isolate" -ginkgo.json-report="$base.isolated.json" >"$base.isolated.log" 2>&1 || { tail -40 "$base.isolated.log" >&2; exit 1; }
|
||||
python3 -I - "$base" "$isolate" "$label" <<'PY'
|
||||
import json, sys
|
||||
base, isolate, label = sys.argv[1:4]
|
||||
def its(path):
|
||||
return [s for r in json.load(open(path)) for s in r['SpecReports'] if s['LeafNodeType'] == 'It']
|
||||
plan = its(base + '.plan.json')
|
||||
main = its(base + '.main.json')
|
||||
alone = its(base + '.isolated.json')
|
||||
isolated = [s for s in plan if isolate in s['LeafNodeText']]
|
||||
problems = []
|
||||
if len(isolated) != 1:
|
||||
problems.append('the isolated spec must match exactly one spec, matched %d' % len(isolated))
|
||||
if len(main) != len(plan) or len(alone) != len(plan):
|
||||
problems.append('a run reported %d and %d specs for a plan of %d' % (len(main), len(alone), len(plan)))
|
||||
platform = ran = 0
|
||||
for spec in main:
|
||||
if isolate in spec['LeafNodeText']:
|
||||
if spec['State'] != 'skipped':
|
||||
problems.append('the isolated spec ran in the main run')
|
||||
continue
|
||||
if spec['State'] == 'passed':
|
||||
ran += 1
|
||||
continue
|
||||
message = (spec.get('Failure') or {}).get('Message', '').lower()
|
||||
if spec['State'] == 'skipped' and 'darwin' in message:
|
||||
platform += 1
|
||||
ran += 1
|
||||
else:
|
||||
problems.append('%s: %s' % (spec['State'], spec['LeafNodeText']))
|
||||
once = 0
|
||||
for spec in alone:
|
||||
if isolate in spec['LeafNodeText']:
|
||||
if spec['State'] == 'passed':
|
||||
once += 1
|
||||
else:
|
||||
problems.append('isolated spec %s' % spec['State'])
|
||||
elif spec['State'] != 'skipped':
|
||||
problems.append('a spec outside the isolation ran in the isolated run')
|
||||
if once != 1 or ran != len(plan) - 1:
|
||||
problems.append('accounted %d of %d specs, isolated run %d times' % (ran + once, len(plan), once))
|
||||
ok = not problems
|
||||
for p in problems:
|
||||
print(p, file=sys.stderr)
|
||||
print(json.dumps({'case': label, 'status': 'passed' if ok else 'failed', 'specs': ran + once, 'platformSkipped': platform, 'isolated': once}))
|
||||
PY
|
||||
}
|
||||
regression ./core/gallery 'regression: model and backend gallery specs (core/gallery)'
|
||||
regression ./core/services/galleryop 'regression: gallery operation specs (core/services/galleryop)'
|
||||
regression ./pkg/model 'regression: process adapter specs (pkg/model)' 'includes the exit code and final stderr line for an unexpected exit'
|
||||
|
||||
# 3. The browser against the real cluster.
|
||||
ready="$work/ready.json"
|
||||
"$work/foundation-cluster" -dir "$work/cluster" -server "$work/ui-test-server" -mock-backend "$root/tests/e2e/mock-backend/mock-backend" \
|
||||
-fixture "$work/fixture-addon" -ready "$ready" >"$cache/logs/foundation-cluster.log" 2>&1 &
|
||||
cluster_pid=$!
|
||||
i=0
|
||||
until [ -s "$ready" ]; do
|
||||
i=$((i + 1))
|
||||
kill -0 "$cluster_pid" 2>/dev/null || { echo 'foundation cluster exited' >&2; tail -20 "$cache/logs/foundation-cluster.log" >&2; exit 1; }
|
||||
[ "$i" -lt 180 ] || { echo 'foundation cluster readiness timed out' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
report="$cache/logs/foundation-report.json"
|
||||
rm -f "$report"
|
||||
status=0
|
||||
(cd "$ui" && ADDONS_FOUNDATION="$ready" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.foundation.config.js >"$cache/logs/foundation-playwright.log" 2>&1) || status=1
|
||||
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
|
||||
if [ "$status" -ne 0 ]; then
|
||||
# Keep the evidence in the runner output: the end of the browser log and
|
||||
# of the cluster log name the failing step and what the servers saw.
|
||||
echo '--- playwright log (tail) ---' >&2
|
||||
tail -60 "$cache/logs/foundation-playwright.log" >&2 || true
|
||||
echo '--- cluster log (tail) ---' >&2
|
||||
tail -30 "$cache/logs/foundation-cluster.log" >&2 || true
|
||||
fi
|
||||
exit "$status"
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs every foundation suite in order and prints the final inventory:
|
||||
# suite -> passed count, skipped count, status
|
||||
# It exits nonzero when any suite fails or reports a skipped case, and it never
|
||||
# stops at the first failure, so one run shows every broken suite.
|
||||
#
|
||||
# The set is explicit. A suite that is added to scripts/addons/suites.json and
|
||||
# not named here is reported as missing from the gate, so the gate cannot
|
||||
# silently shrink or fall behind the inventory.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
mkdir -p "$cache/logs" "$cache/tmp"
|
||||
. "$root/scripts/addons/leaks.sh"
|
||||
suites='prerequisites contracts catalog packages lifecycle runtime gateway policy proposals artifacts apps gallery-ui esm-ui host-slots recovery updates foundation-acceptance packaging'
|
||||
# Tables suites are not foundation suites: scripts/addons/tables_gate.sh runs
|
||||
# them. They are named here only so the inventory check does not report them
|
||||
# as missing from the foundation gate.
|
||||
tables_suites='tables-databases tables-schema tables-rows tables-tools tables-admin tables-ui tables-csv tables-acceptance'
|
||||
results="$cache/logs/foundation-gate-$$.jsonl"
|
||||
: >"$results"
|
||||
for suite in $suites; do
|
||||
out="$cache/logs/foundation-gate-$suite.out"
|
||||
# A suite that leaves a process that names the scratch directory behind
|
||||
# fails, whatever its own result was; the leftover is killed.
|
||||
leak=0
|
||||
ok=0
|
||||
sh scripts/addons/verify.sh "$suite" >"$out" 2>&1 || ok=1
|
||||
reap_leaks "$cache/tmp/" "$suite" paths-only >>"$out" 2>&1 || leak=1
|
||||
if [ "$leak" -eq 1 ]; then
|
||||
printf '{"suite":"%s","passed":0,"skipped":0,"status":"leaked-processes","log":"%s"}\n' "$suite" "$out" >>"$results"
|
||||
elif [ "$ok" -eq 0 ]; then
|
||||
line=$(tail -n 1 "$out")
|
||||
printf '%s\n' "$line" | python3 -I -c 'import json,sys; d=json.loads(sys.stdin.read()); assert d["suite"]=="'"$suite"'"; print(json.dumps({"suite":d["suite"],"passed":d["passed"],"skipped":d["skipped"],"status":"passed" if d["skipped"]==0 else "skipped"}))' >>"$results" 2>/dev/null \
|
||||
|| printf '{"suite":"%s","passed":0,"skipped":0,"status":"no-result"}\n' "$suite" >>"$results"
|
||||
else
|
||||
printf '{"suite":"%s","passed":0,"skipped":0,"status":"failed","log":"%s"}\n' "$suite" "$out" >>"$results"
|
||||
fi
|
||||
done
|
||||
python3 -I - "$results" "$root/scripts/addons/suites.json" "$suites" "$tables_suites" <<'PY'
|
||||
import json, sys
|
||||
rows = [json.loads(l) for l in open(sys.argv[1]) if l.strip()]
|
||||
inventory = json.load(open(sys.argv[2]))
|
||||
named = sys.argv[3].split()
|
||||
tables = sys.argv[4].split()
|
||||
missing = sorted(set(inventory) - set(named) - set(tables))
|
||||
absent = sorted(set(named) - set(inventory))
|
||||
width = max(len(r['suite']) for r in rows)
|
||||
print('foundation gate inventory')
|
||||
total = skipped = 0
|
||||
bad = False
|
||||
for r in rows:
|
||||
print(' %-*s passed=%-4d skipped=%-2d %s' % (width, r['suite'], r['passed'], r['skipped'], r['status']))
|
||||
total += r['passed']
|
||||
skipped += r['skipped']
|
||||
if r['status'] != 'passed':
|
||||
bad = True
|
||||
for name in missing:
|
||||
print(' %-*s not part of the gate: add it to scripts/addons/foundation_gate.sh' % (width, name))
|
||||
for name in absent:
|
||||
print(' %-*s named by the gate and absent from suites.json' % (width, name))
|
||||
print('total passed=%d skipped=%d suites=%d' % (total, skipped, len(rows)))
|
||||
sys.exit(1 if bad or missing or absent else 0)
|
||||
PY
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs the add-ons gallery UI checks and prints one JSON case line per test:
|
||||
# the node unit tests of the pure state, then the Playwright spec against the
|
||||
# real UI test server on an isolated port. verify.sh compares the lines with
|
||||
# the exact inventory of the gallery-ui suite and rejects a skipped, flaky or
|
||||
# missing case.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
ui="$root/core/http/react-ui"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
|
||||
# 1. Pure state and client: node's own runner, no dependencies.
|
||||
(cd "$ui" && node tests/unit/run.mjs addons.test.mjs)
|
||||
|
||||
# 2. Browser specs against the real server, which embeds the built UI.
|
||||
port=$(python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])')
|
||||
log="$cache/logs/gallery-ui-server.log"
|
||||
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$port" >"$log" 2>&1 &
|
||||
server=$!
|
||||
trap 'kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true' EXIT HUP INT TERM
|
||||
i=0
|
||||
until python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
|
||||
i=$((i + 1))
|
||||
kill -0 "$server" 2>/dev/null || { echo 'UI test server exited' >&2; exit 1; }
|
||||
[ "$i" -lt 90 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
report="$cache/logs/gallery-ui-report.json"
|
||||
rm -f "$report"
|
||||
status=0
|
||||
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$port" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.addons.config.js >"$cache/logs/gallery-ui-playwright.log" 2>&1) || status=$?
|
||||
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
|
||||
exit "$status"
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs the host UI slot checks and prints one JSON case line per test:
|
||||
# 1. the node unit tests of the slot registry, the loader's component
|
||||
# validation, the approval frame rules and the SDK approvals call;
|
||||
# 2. the Go specs of the manifest declarations (unknown slot, unknown
|
||||
# placement, remote icon, undeclared tool, duplicates, markup, bounds) and
|
||||
# of the apps list that carries them;
|
||||
# 3. the Playwright spec against the UI test server running with real
|
||||
# authentication and a real add-on owner, which installs the "slots"
|
||||
# fixture package AFTER the host build, from a catalog on loopback.
|
||||
# verify.sh compares the lines with the exact inventory of the host-slots suite
|
||||
# and rejects a skipped, flaky or missing case.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
ui="$root/core/http/react-ui"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
|
||||
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
# The server embeds the UI build: a build made before the last UI change would
|
||||
# test the old pages.
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
|
||||
# 1. Pure slot, loader, approval and SDK rules.
|
||||
(cd "$ui" && node tests/unit/run.mjs slots.test.mjs)
|
||||
|
||||
# 2. Go: ginkgo specs of the declarations, reported as case lines.
|
||||
go_report="$cache/logs/host-slots-go.json"
|
||||
rm -f "$go_report"
|
||||
go test -count=1 -p 2 -tags auth ./tests/addons/hostslots -ginkgo.json-report="$go_report" >"$cache/logs/host-slots-go.log" 2>&1 || { tail -40 "$cache/logs/host-slots-go.log" >&2; exit 1; }
|
||||
python3 -I - "$go_report" <<'PY'
|
||||
import json, sys
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It':
|
||||
continue
|
||||
status = {'passed': 'passed', 'skipped': 'skipped'}.get(spec['State'], 'failed')
|
||||
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
|
||||
PY
|
||||
|
||||
# 3. The real owner and the browser.
|
||||
fixture_exe="$cache/tmp/fixture-addon"
|
||||
go build -p 2 -tags auth -o "$fixture_exe" ./tests/addons/fixture/cmd
|
||||
port=$(python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])')
|
||||
creds="$cache/logs/host-slots-credentials.json"
|
||||
rm -f "$creds"
|
||||
log="$cache/logs/host-slots-server.log"
|
||||
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$port" \
|
||||
--addons-fixture="$root/tests/addons/fixture/ui" --addon-executable="$fixture_exe" --fixture-out="$creds" \
|
||||
--fixture-enable=slots@1.0.0 >"$log" 2>&1 &
|
||||
server=$!
|
||||
trap 'kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true' EXIT HUP INT TERM
|
||||
i=0
|
||||
until [ -s "$creds" ] && python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
|
||||
i=$((i + 1))
|
||||
kill -0 "$server" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$log" >&2; exit 1; }
|
||||
[ "$i" -lt 120 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
report="$cache/logs/host-slots-report.json"
|
||||
rm -f "$report"
|
||||
status=0
|
||||
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$port" ADDONS_FIXTURE="$creds" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.slots.config.js >"$cache/logs/host-slots-playwright.log" 2>&1) || status=$?
|
||||
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
|
||||
exit "$status"
|
||||
@@ -0,0 +1,48 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Sourced by the foundation scripts. leaked_pids DIR prints the id of every
|
||||
# live process, except this shell and its ancestors, whose executable, working
|
||||
# directory or command line names DIR. It reads /proc, so it finds a process
|
||||
# that was orphaned (parent 1) as well as a child.
|
||||
leaked_pids() {
|
||||
dir=$1
|
||||
# paths-only: a process is a leak only when its executable or working
|
||||
# directory is under DIR. The command line is not read, so an unrelated
|
||||
# shell that merely names the shared scratch directory (a log tail or a
|
||||
# wait loop of another session) is never killed.
|
||||
mode=${2:-}
|
||||
skip=" $$ "
|
||||
p=$$
|
||||
while [ "$p" -gt 1 ] 2>/dev/null; do
|
||||
p=$(awk '{ s = $0; sub(/^.*\) /, "", s); split(s, f, " "); print f[2] }' "/proc/$p/stat" 2>/dev/null) || break
|
||||
[ -n "$p" ] || break
|
||||
skip="$skip$p "
|
||||
done
|
||||
for d in /proc/[0-9]*; do
|
||||
pid=${d#/proc/}
|
||||
case "$skip" in *" $pid "*) continue ;; esac
|
||||
[ -r "$d/cmdline" ] || continue
|
||||
state=$(awk '{ s = $0; sub(/^.*\) /, "", s); print substr(s, 1, 1) }' "$d/stat" 2>/dev/null) || continue
|
||||
[ "$state" != Z ] || continue
|
||||
hit=
|
||||
case "$(readlink "$d/exe" 2>/dev/null)" in "$dir"*) hit=1 ;; esac
|
||||
case "$(readlink "$d/cwd" 2>/dev/null)" in "$dir"*) hit=1 ;; esac
|
||||
[ -n "$hit" ] || [ "$mode" = paths-only ] || case "$(tr '\0' ' ' <"$d/cmdline" 2>/dev/null)" in *"$dir"*) hit=1 ;; esac
|
||||
[ -z "$hit" ] || echo "$pid"
|
||||
done
|
||||
}
|
||||
|
||||
# reap_leaks DIR LABEL kills what leaked_pids finds, reports it on stderr and
|
||||
# returns 1 when anything was found, so a leak is never silent.
|
||||
reap_leaks() {
|
||||
found=$(leaked_pids "$1" "${3:-}")
|
||||
[ -n "$found" ] || return 0
|
||||
for pid in $found; do
|
||||
echo "LEAK ($2): process $pid survived: $(tr '\0' ' ' <"/proc/$pid/cmdline" 2>/dev/null | cut -c1-200)" >&2
|
||||
done
|
||||
# shellcheck disable=SC2086
|
||||
kill -TERM $found 2>/dev/null || true
|
||||
sleep 1
|
||||
# shellcheck disable=SC2086
|
||||
kill -KILL $found 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Builds the first-party add-on OCI package of one platform (the add-on is
|
||||
# named by the platform file, Tables by default) as a local OCI
|
||||
# layout. It never pushes, tags, signs or logs in to anything.
|
||||
#
|
||||
# sh scripts/addons/package.sh --layout <dir> --version <semver> --platform <os/arch>
|
||||
# [--executable <file>] [--runtime-dir <dir>] [--revision <sha>]
|
||||
# [--config <platforms.yaml>] [--local-check]
|
||||
#
|
||||
# The layout holds one platform image: an executable layer (the entrypoint and
|
||||
# the runtime files of the platform), a ui layer when the manifest declares a
|
||||
# UI, and the manifest layer, plus receipt.json with the digests. The layout is
|
||||
# verified before the script succeeds.
|
||||
#
|
||||
# --executable is the built binary of the platform. Without it the script
|
||||
# builds the executable of the add-on for the platform of this host and refuses any
|
||||
# other platform: a package is never made from a binary of another platform.
|
||||
# --local-check allows a platform that the platform matrix has not enabled yet;
|
||||
# the receipt then says publishable=false and the publish steps refuse it.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
layout= version= platform= executable= runtime_dir= revision= local_check=
|
||||
config=addons/tables/package/platforms.yaml
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--layout) layout=${2-}; shift 2 ;;
|
||||
--version) version=${2-}; shift 2 ;;
|
||||
--platform) platform=${2-}; shift 2 ;;
|
||||
--executable) executable=${2-}; shift 2 ;;
|
||||
--runtime-dir) runtime_dir=${2-}; shift 2 ;;
|
||||
--revision) revision=${2-}; shift 2 ;;
|
||||
--config) config=${2-}; shift 2 ;;
|
||||
--local-check) local_check=1; shift ;;
|
||||
*) echo "package.sh: unknown argument $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$layout" ] && [ -n "$version" ] && [ -n "$platform" ] || {
|
||||
echo 'usage: package.sh --layout <dir> --version <version> --platform <os/arch>' >&2
|
||||
exit 2
|
||||
}
|
||||
scratch=$(mktemp -d "${TMPDIR:-/tmp}/addon-package.XXXXXX")
|
||||
trap 'chmod -R u+rwx "$scratch" 2>/dev/null; rm -rf "$scratch"' EXIT HUP INT TERM
|
||||
tool=${ADDONPKG:-}
|
||||
if [ -z "$tool" ]; then
|
||||
go build -o "$scratch/addonpkg" ./scripts/addons/cmd/addonpkg
|
||||
tool="$scratch/addonpkg"
|
||||
fi
|
||||
# The add-on, its executable and its UI directory come from the platform file.
|
||||
[ -f "$config" ] || { echo "package.sh: platform file $config not found" >&2; exit 2; }
|
||||
addon=$(sed -n 's/^addon: //p' "$config")
|
||||
exe_name=$(sed -n 's/^executable: //p' "$config")
|
||||
ui_dir=$(sed -n 's/^ui: //p' "$config")
|
||||
for v in "$addon" "$exe_name"; do
|
||||
case "$v" in
|
||||
'' | *[!a-z0-9-]*) echo "package.sh: $config needs a plain addon and executable name" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
if [ -z "$executable" ]; then
|
||||
host="$(go env GOOS)/$(go env GOARCH)"
|
||||
[ "$platform" = "$host" ] || {
|
||||
echo "package.sh: platform $platform is not this host ($host); pass the native --executable" >&2
|
||||
exit 2
|
||||
}
|
||||
# Tables links a C library. The other add-ons are pure Go, so their
|
||||
# executable needs no C toolchain on any platform.
|
||||
cgo=0
|
||||
[ "$addon" != tables ] || cgo=1
|
||||
CGO_ENABLED=$cgo go build -trimpath -o "$scratch/$exe_name" "./addons/$addon/cmd/$exe_name"
|
||||
executable="$scratch/$exe_name"
|
||||
fi
|
||||
# The UI is a build output. Build it only when it is absent.
|
||||
if [ -n "$ui_dir" ] && [ ! -f "$ui_dir/ui/$addon.js" ]; then
|
||||
node "addons/$addon/ui/build.mjs"
|
||||
fi
|
||||
set -- --config "$config" --layout "$layout" --version "$version" --platform "$platform" --executable "$executable"
|
||||
[ -z "$runtime_dir" ] || set -- "$@" --runtime-dir "$runtime_dir"
|
||||
[ -z "$revision" ] || set -- "$@" --revision "$revision"
|
||||
[ -z "$local_check" ] || set -- "$@" --local-check
|
||||
"$tool" build "$@"
|
||||
"$tool" verify --config "$config" --layout "$layout" --platform "$platform" --version "$version"
|
||||
Executable
+172
@@ -0,0 +1,172 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Packaging checks of the first-party add-on OCI package. It prints one JSON
|
||||
# line per case; scripts/addons/verify.sh compares the lines with the exact
|
||||
# inventory of the "packaging" suite and rejects a skipped or missing case.
|
||||
#
|
||||
# 1. Go specs of pkg/addons/packaging: layout rules, rejected packages,
|
||||
# event and channel policy, merge completeness, publication and alias
|
||||
# promotion against an in-process registry, catalog template, and the
|
||||
# workflow policy with one failing mutant per rule.
|
||||
# 2. The installer contract: the package built by package.sh is installed
|
||||
# by the real installer from a real local OCI distribution and its
|
||||
# executable is started by the real supervisor.
|
||||
# 3. Shell entry points: package.sh, channel.sh, the workflow policy
|
||||
# command and static checks that nothing here pushes, tags or signs.
|
||||
#
|
||||
# Nothing here pushes, tags, signs or logs in: registries are loopback
|
||||
# distributions inside the test process and signatures come from a fixture
|
||||
# authority.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
|
||||
work=$(mktemp -d "$cache/tmp/packaging.XXXXXX")
|
||||
trap 'chmod -R u+rwx "$work" 2>/dev/null; rm -rf "$work"' EXIT HUP INT TERM
|
||||
|
||||
# The UI is a build output of this checkout; build it so the package holds
|
||||
# the current files.
|
||||
node addons/tables/ui/build.mjs >"$cache/logs/packaging-ui-build.log" 2>&1 || { cat "$cache/logs/packaging-ui-build.log" >&2; exit 1; }
|
||||
|
||||
go_cases() {
|
||||
python3 -I - "$1" <<'PY'
|
||||
import json, sys
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
|
||||
continue
|
||||
status = {'passed': 'passed'}.get(spec['State'], 'failed')
|
||||
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
|
||||
PY
|
||||
}
|
||||
run_go() {
|
||||
name=$1
|
||||
shift
|
||||
report="$cache/logs/packaging-$name.json"
|
||||
rm -f "$report"
|
||||
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/packaging-$name.log" 2>&1 || { tail -60 "$cache/logs/packaging-$name.log" >&2; exit 1; }
|
||||
go_cases "$report"
|
||||
}
|
||||
run_go specs ./pkg/addons/packaging
|
||||
run_go installer ./tests/addons/packaging
|
||||
|
||||
# Shell entry points.
|
||||
tool="$work/addonpkg"
|
||||
go build -o "$tool" ./scripts/addons/cmd/addonpkg
|
||||
export ADDONPKG="$tool"
|
||||
case_ok() { printf '{"case":"%s","status":"passed"}\n' "$1"; }
|
||||
expect_fail() { # name expected-text command...
|
||||
name=$1; expected=$2; shift 2
|
||||
if "$@" >"$work/out" 2>&1; then echo "FAIL $name: unexpectedly succeeded" >&2; cat "$work/out" >&2; exit 1; fi
|
||||
grep -F -- "$expected" "$work/out" >/dev/null || { echo "FAIL $name: wrong failure (expected $expected)" >&2; cat "$work/out" >&2; exit 1; }
|
||||
case_ok "$name"
|
||||
}
|
||||
sha=0123456789abcdef0123456789abcdef01234567
|
||||
host="$(go env GOOS)/$(go env GOARCH)"
|
||||
printf '#!/bin/sh\nexit 2\n' >"$work/stub"
|
||||
chmod 755 "$work/stub"
|
||||
|
||||
# package.sh: a local check layout for the host builds and verifies.
|
||||
sh scripts/addons/package.sh --layout "$work/leg" --version 1.2.3 --platform "$host" --executable "$work/stub" --local-check --revision "$sha" >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
|
||||
grep -F 'publishable=false' "$work/out" >/dev/null && grep -F 'verified=tables 1.2.3' "$work/out" >/dev/null
|
||||
test -f "$work/leg/receipt.json" && test -f "$work/leg/index.json" && test -f "$work/leg/oci-layout"
|
||||
case_ok 'package.sh builds and verifies a local check layout'
|
||||
expect_fail 'package.sh refuses a platform the matrix has not enabled' 'not enabled' sh scripts/addons/package.sh --layout "$work/leg2" --version 1.2.3 --platform "$host" --executable "$work/stub"
|
||||
expect_fail 'package.sh refuses an existing non-empty layout' 'not empty' sh scripts/addons/package.sh --layout "$work/leg" --version 1.2.3 --platform "$host" --executable "$work/stub" --local-check
|
||||
expect_fail 'package.sh requires its arguments' 'usage:' sh scripts/addons/package.sh --version 1.2.3
|
||||
expect_fail 'package.sh refuses an invalid version' 'semantic version' sh scripts/addons/package.sh --layout "$work/leg3" --version v1 --platform "$host" --executable "$work/stub" --local-check
|
||||
# package.sh reads the add-on from its platform file. The voice add-on is built
|
||||
# for the host as a local check, and its layout verifies.
|
||||
sh scripts/addons/package.sh --config addons/voice/package/platforms.yaml --layout "$work/voice" --version 1.2.3 --platform "$host" --local-check --revision "$sha" >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
|
||||
grep -F 'verified=voice 1.2.3' "$work/out" >/dev/null
|
||||
test -f "$work/voice/receipt.json"
|
||||
case_ok 'package.sh builds and verifies the voice add-on for the host'
|
||||
# A platform other than the host is refused for every add-on, whatever the config.
|
||||
if [ "$host" = linux/amd64 ]; then foreign=linux/arm64; else foreign=linux/amd64; fi
|
||||
for id in tables voice face fine-tuning quantization; do
|
||||
expect_fail "package.sh refuses a foreign platform without an executable: $id" 'is not this host' sh scripts/addons/package.sh --config "addons/$id/package/platforms.yaml" --layout "$work/foreign-$id" --version 1.2.3 --platform "$foreign" --local-check
|
||||
done
|
||||
case_ok 'package.sh refuses a foreign platform without an executable for every add-on'
|
||||
# The build line: cgo for Tables, none for the pure Go add-ons. A stub go
|
||||
# records the environment and the package it was asked to build.
|
||||
realgo=$(command -v go)
|
||||
mkdir "$work/stubbin"
|
||||
cat >"$work/stubbin/go" <<STUB
|
||||
#!/bin/sh
|
||||
if [ "\$1" = build ]; then
|
||||
out=
|
||||
for a in "\$@"; do
|
||||
if [ "\$prev" = -o ]; then out=\$a; fi
|
||||
prev=\$a
|
||||
last=\$a
|
||||
done
|
||||
printf '%s CGO_ENABLED=%s\n' "\$last" "\${CGO_ENABLED-unset}" >>"$work/gobuild.log"
|
||||
printf '#!/bin/sh\nexit 2\n' >"\$out"
|
||||
chmod 755 "\$out"
|
||||
exit 0
|
||||
fi
|
||||
exec "$realgo" "\$@"
|
||||
STUB
|
||||
chmod 755 "$work/stubbin/go"
|
||||
: >"$work/gobuild.log"
|
||||
for id in tables voice face fine-tuning quantization; do
|
||||
PATH="$work/stubbin:$PATH" sh scripts/addons/package.sh --config "addons/$id/package/platforms.yaml" --layout "$work/stub-$id" --version 1.2.3 --platform "$host" --local-check >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
|
||||
done
|
||||
grep -x './addons/tables/cmd/tables CGO_ENABLED=1' "$work/gobuild.log" >/dev/null
|
||||
for id in voice face fine-tuning quantization; do
|
||||
grep -x "./addons/$id/cmd/$id CGO_ENABLED=0" "$work/gobuild.log" >/dev/null || { echo "FAIL: $id is not built with CGO_ENABLED=0" >&2; cat "$work/gobuild.log" >&2; exit 1; }
|
||||
done
|
||||
case_ok 'package.sh builds Tables with cgo and the pure Go add-ons without'
|
||||
expect_fail 'matrix fails closed while no platform is enabled' 'no enabled platform' "$tool" matrix --config addons/tables/package/platforms.yaml
|
||||
expect_fail 'matrix fails closed without a file' 'platform matrix' "$tool" matrix --config "$work/absent.yaml"
|
||||
|
||||
# channel.sh.
|
||||
channel() { sh scripts/addons/channel.sh "$@"; }
|
||||
out=$(channel --event pull_request --ref refs/pull/7/merge --sha "$sha")
|
||||
printf '%s\n' "$out" | grep -x 'publish=false' >/dev/null && printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null
|
||||
case_ok 'channel.sh: a pull request never publishes'
|
||||
out=$(channel --event push --ref refs/heads/master --sha "$sha")
|
||||
printf '%s\n' "$out" | grep -x 'aliases=development' >/dev/null && printf '%s\n' "$out" | grep -x 'immutable=sha-0123456789ab' >/dev/null
|
||||
case_ok 'channel.sh: master publishes development'
|
||||
out=$(channel --event push --ref refs/heads/latest --sha "$sha")
|
||||
printf '%s\n' "$out" | grep -x 'publish=false' >/dev/null && printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null && printf '%s\n' "$out" | grep -x 'channel=' >/dev/null
|
||||
case_ok 'channel.sh: a branch push never publishes, even a branch named latest'
|
||||
out=$(channel --event push --ref refs/tags/v1.2.3 --sha "$sha")
|
||||
printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null && printf '%s\n' "$out" | grep -x 'immutable=1.2.3' >/dev/null
|
||||
case_ok 'channel.sh: a tag push prepares the version and moves no alias'
|
||||
out=$(channel --event release --ref refs/tags/v1.2.3 --sha "$sha")
|
||||
printf '%s\n' "$out" | grep -x 'aliases=latest' >/dev/null
|
||||
case_ok 'channel.sh: a published stable release promotes latest'
|
||||
out=$(channel --event release --ref refs/tags/v1.2.3 --sha "$sha" --prerelease)
|
||||
printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null
|
||||
case_ok 'channel.sh: a pre-release never promotes latest'
|
||||
expect_fail 'channel.sh: a draft release fails' 'draft' channel --event release --ref refs/tags/v1.2.3 --sha "$sha" --draft
|
||||
expect_fail 'channel.sh: an unknown event fails' 'not allowed to publish' channel --event workflow_dispatch --ref refs/heads/master --sha "$sha"
|
||||
expect_fail 'channel.sh: a tag that is not a version fails' 'release tag' channel --event push --ref refs/tags/latest --sha "$sha"
|
||||
|
||||
# Workflow policy.
|
||||
"$tool" policy --workflow .github/workflows/addons-package.yml >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
|
||||
case_ok 'workflow policy accepts the checked-in workflow'
|
||||
sed 's/^ release:$/ pull_request_target:\n types: [opened]\n release:/' .github/workflows/addons-package.yml >"$work/mutant.yml"
|
||||
expect_fail 'workflow policy rejects pull_request_target' 'P_PR_TARGET' "$tool" policy --workflow "$work/mutant.yml"
|
||||
sed 's/^ needs: \[plan, validate, package\]$/ needs: [plan, package]/' .github/workflows/addons-package.yml >"$work/mutant.yml"
|
||||
expect_fail 'workflow policy rejects a publish job that does not wait for every leg' 'P_NEEDS' "$tool" policy --workflow "$work/mutant.yml"
|
||||
sed 's/^ name: validate package rules$/ name: validate package rules\n continue-on-error: true/' .github/workflows/addons-package.yml >"$work/mutant.yml"
|
||||
expect_fail 'workflow policy rejects a validate job that cannot stop the release' 'P_VALIDATE_GATE' "$tool" policy --workflow "$work/mutant.yml"
|
||||
|
||||
# Nothing in the packaging scripts pushes, tags, signs or logs in.
|
||||
if grep -n -E 'docker push|docker login|git tag|git push|oras (push|copy)|cosign (sign|attest)|crane push|gh release' scripts/addons/package.sh scripts/addons/channel.sh scripts/addons/package_test.sh | grep -v '^[^:]*:[0-9]*:#' | grep -v 'grep -n -E' | grep -v 'static checks'; then
|
||||
echo 'FAIL: a packaging script pushes, tags or signs' >&2
|
||||
exit 1
|
||||
fi
|
||||
case_ok 'scripts never push, tag, sign or log in'
|
||||
if grep -n -i -E 'quay\.io|go-skynet|backend_merge|backend_build|backend\.yml|ci-cache|docker\.io' .github/workflows/addons-package.yml scripts/addons/package.sh scripts/addons/channel.sh addons/*/package/platforms.yaml gallery/addons/tables.yaml.tmpl; then
|
||||
echo 'FAIL: a packaging file references the backend pipeline' >&2
|
||||
exit 1
|
||||
fi
|
||||
case_ok 'packaging files never reference backend registries or workflows'
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
export GOTOOLCHAIN=local
|
||||
export GOPATH="$root/.cache/addons/go" GOMODCACHE="$root/.cache/addons/go/pkg/mod"
|
||||
export GOCACHE="$root/.cache/addons/go-build" GOBIN="$root/.cache/addons/bin"
|
||||
export npm_config_cache="$root/.cache/addons/npm" PLAYWRIGHT_BROWSERS_PATH="$root/.cache/addons/browsers"
|
||||
export PATH="$GOBIN:$PATH"
|
||||
mode=${1:-}
|
||||
case "$mode" in --check|--prepare|--integration) ;; *) echo 'usage: prepare.sh --check|--prepare|--integration' >&2; exit 2;; esac
|
||||
[ "$#" -eq 1 ] || exit 2
|
||||
fail() { echo "$*" >&2; exit 1; }
|
||||
go version 2>/dev/null | grep -Eq '^go version go1\.26\.[0-9]+ ' || fail 'Go 1.26.x required (GOTOOLCHAIN=local)'
|
||||
node --version 2>/dev/null | grep -Eq '^v22\.([1-9][2-9]|[2-9][0-9]|[1-9][0-9]{2,})\.[0-9]+$' || fail 'Node 22.12 or later within Node 22 required'
|
||||
[ "$(protoc --version 2>/dev/null)" = 'libprotoc 26.1' ] || fail 'protoc 26.1 required; provision explicitly (Makefile downloader uses a different version)'
|
||||
if [ "$mode" = --check ]; then
|
||||
# Fail before invoking compilers or package managers if generated inputs are absent.
|
||||
for file in pkg/grpc/proto/backend.pb.go pkg/grpc/proto/backend_grpc.pb.go core/http/react-ui/dist/index.html; do
|
||||
[ -s "$file" ] || fail "missing generated file: $file; run --integration explicitly"
|
||||
done
|
||||
fi
|
||||
command -v npm >/dev/null || fail 'npm required'
|
||||
command -v "${CC:-cc}" >/dev/null || fail 'CGO C compiler required'
|
||||
[ "$(go env CGO_ENABLED)" = 1 ] || fail 'CGO_ENABLED=1 required'
|
||||
space() {
|
||||
available=$(df -Pk "$root" | awk 'END {print $4}')
|
||||
[ "$available" -ge 20971520 ] || fail 'stop: at least 20 GiB free required before setup/build'
|
||||
}
|
||||
if [ "$mode" = --prepare ]; then
|
||||
space
|
||||
mkdir -p "$GOBIN" "$root/.cache/addons/tmp"
|
||||
export TMPDIR="$root/.cache/addons/tmp"
|
||||
# Exact existing Makefile pins; Go module checksums are authenticated by sum.golang.org.
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
|
||||
space
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f11e97d44e567e945af
|
||||
space
|
||||
go mod download
|
||||
space
|
||||
(cd core/http/react-ui && npm ci --no-audit --no-fund)
|
||||
# Explicit setup only: the lockfile-pinned local CLI selects the browser
|
||||
# revision. Never use npx or install host OS packages here.
|
||||
if [ -z "${PLAYWRIGHT_CHROMIUM_PATH:-}" ]; then
|
||||
space
|
||||
core/http/react-ui/node_modules/.bin/playwright install chromium
|
||||
else
|
||||
[ -x "$PLAYWRIGHT_CHROMIUM_PATH" ] || fail 'configured Chromium is not executable'
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
# Go resolves the Makefile commit to cmd/protoc-gen-go-grpc/v1.4.0.
|
||||
# Bind the tag to the checksum authenticated by sum.golang.org, not its label.
|
||||
check_plugin() {
|
||||
metadata=$(go version -m "$(command -v "$1")") || fail "$1 build metadata unavailable"
|
||||
printf '%s\n' "$metadata" | awk -v module="$2" -v version="$3" -v sum="$4" '
|
||||
$1 == "mod" && $2 == module && $3 == version && $4 == sum { found=1 }
|
||||
$1 == "=>" { replaced=1 }
|
||||
END { exit !(found && !replaced) }
|
||||
' || fail "$1 exact module version/checksum required"
|
||||
}
|
||||
check_plugin protoc-gen-go google.golang.org/protobuf v1.34.2 'h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg='
|
||||
check_plugin protoc-gen-go-grpc google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.4.0 'h1:9SxA29VM43MF5Z9dQu694wmY5t8E/Gxr7s+RSxiIDmc='
|
||||
export GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
if [ "$mode" = --integration ]; then
|
||||
space
|
||||
mkdir -p pkg/grpc/proto "$root/.cache/addons/tmp"
|
||||
export TMPDIR="$root/.cache/addons/tmp"
|
||||
protoc --experimental_allow_proto3_optional -Ibackend/ --go_out=pkg/grpc/proto/ --go_opt=paths=source_relative --go-grpc_out=pkg/grpc/proto/ --go-grpc_opt=paths=source_relative backend/backend.proto
|
||||
space
|
||||
(cd core/http/react-ui && npm run build)
|
||||
space
|
||||
go build -p 2 -tags auth -o tests/e2e/mock-backend/mock-backend ./tests/e2e/mock-backend
|
||||
space
|
||||
go build -p 2 -tags auth -o tests/e2e-ui/ui-test-server ./tests/e2e-ui
|
||||
exit 0
|
||||
fi
|
||||
for file in tests/e2e/mock-backend/mock-backend tests/e2e-ui/ui-test-server core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || fail "missing executable: $file"
|
||||
done
|
||||
(cd core/http/react-ui && npm ls --depth=0 --offline >/dev/null) || fail 'installed npm dependencies do not match package/lock requirements'
|
||||
# --check inspects only; verify performs native compile/runtime and browser probes.
|
||||
printf '{"case":"toolchain","status":"passed"}\n'
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Fixtures are isolated copies: never substitute fake tools in a real readiness run.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
export TMPDIR="$root/.cache/addons/tmp"
|
||||
mkdir -p "$TMPDIR"
|
||||
fixture=$(mktemp -d)
|
||||
trap 'rm -rf "$fixture"' EXIT HUP INT TERM
|
||||
mkdir -p "$fixture/scripts/addons" "$fixture/bin"
|
||||
for file in prepare.sh verify.sh suites.json ui_cases.py; do
|
||||
test -f "$root/scripts/addons/$file" || { echo "missing implementation: $file" >&2; exit 1; }
|
||||
cp "$root/scripts/addons/$file" "$fixture/scripts/addons/$file"
|
||||
done
|
||||
cat > "$fixture/bin/go" <<'TOOL'
|
||||
#!/bin/sh
|
||||
case "$*" in
|
||||
version) echo "go version ${FIXTURE_GO:-go1.26.1} linux/amd64";;
|
||||
*) exit 91;;
|
||||
esac
|
||||
TOOL
|
||||
cat > "$fixture/bin/node" <<'TOOL'
|
||||
#!/bin/sh
|
||||
echo "${FIXTURE_NODE:-v22.22.0}"
|
||||
TOOL
|
||||
cat > "$fixture/bin/protoc" <<'TOOL'
|
||||
#!/bin/sh
|
||||
echo "${FIXTURE_PROTOC:-libprotoc 26.1}"
|
||||
TOOL
|
||||
chmod +x "$fixture/bin/"*
|
||||
count=0
|
||||
reject() {
|
||||
name=$1; expected=$2; shift 2
|
||||
if "$@" > "$fixture/output" 2>&1; then
|
||||
echo "FAIL $name: unexpectedly succeeded" >&2; exit 1
|
||||
fi
|
||||
if ! grep -F "$expected" "$fixture/output" >/dev/null; then
|
||||
echo "FAIL $name: wrong failure (expected $expected)" >&2
|
||||
cat "$fixture/output" >&2; exit 1
|
||||
fi
|
||||
count=$((count + 1))
|
||||
printf '{"case":"%s","status":"passed"}\n' "$name"
|
||||
}
|
||||
prepare="$fixture/scripts/addons/prepare.sh"
|
||||
verify="$fixture/scripts/addons/verify.sh"
|
||||
reject missing-go 'Go 1.26' env PATH="$fixture/bin:/usr/bin:/bin" FIXTURE_GO=missing sh "$prepare" --check
|
||||
reject old-go 'Go 1.26' env PATH="$fixture/bin:$PATH" FIXTURE_GO=go1.25.9 sh "$prepare" --check
|
||||
reject future-go 'Go 1.26' env PATH="$fixture/bin:$PATH" FIXTURE_GO=go1.27.0 sh "$prepare" --check
|
||||
reject old-node 'Node 22.12' env PATH="$fixture/bin:$PATH" FIXTURE_NODE=v22.11.0 sh "$prepare" --check
|
||||
reject wrong-node 'Node 22.12' env PATH="$fixture/bin:$PATH" FIXTURE_NODE=v24.0.0 sh "$prepare" --check
|
||||
reject wrong-protoc 'protoc 26.1' env PATH="$fixture/bin:$PATH" FIXTURE_PROTOC='libprotoc 31.1' sh "$prepare" --check
|
||||
reject absent-generated 'generated file' env PATH="$fixture/bin:$PATH" sh "$prepare" --check
|
||||
reject empty-name 'suite' sh "$verify" ''
|
||||
reject future-suite 'suite' sh "$verify" unregistered
|
||||
# Replace the copied inventory and children, not production behavior.
|
||||
cat > "$fixture/scripts/addons/suites.json" <<'JSON'
|
||||
{"prerequisites":{"kind":"shell","commands":[],"cases":[]}}
|
||||
JSON
|
||||
reject empty-selection 'empty' sh "$verify" prerequisites
|
||||
cat > "$fixture/scripts/addons/suites.json" <<'JSON'
|
||||
{"prerequisites":{"kind":"shell","commands":[["sh","scripts/addons/prerequisites_test.sh"]],"cases":["required"]}}
|
||||
JSON
|
||||
printf '#!/bin/sh\nexit 42\n' > "$fixture/scripts/addons/prerequisites_test.sh"
|
||||
reject failed-child '42' sh "$verify" prerequisites
|
||||
# A failing child must leave its failing case and the end of its output in the
|
||||
# runner output, so a flake in a long gate run can be named afterwards.
|
||||
printf '#!/bin/sh\necho one; echo '\''{"case":"required","status":"failed"}'\''; echo tail-marker-line >&2; exit 7\n' > "$fixture/scripts/addons/prerequisites_test.sh"
|
||||
reject failed-case-named 'failed case: required' sh "$verify" prerequisites
|
||||
reject failed-tail-kept '| tail-marker-line' sh "$verify" prerequisites
|
||||
printf '#!/bin/sh\nexit 0\n' > "$fixture/scripts/addons/prerequisites_test.sh"
|
||||
reject zero-tests 'required' sh "$verify" prerequisites
|
||||
printf '#!/bin/sh\necho '\''{"case":"required","status":"skipped"}'\''\n' > "$fixture/scripts/addons/prerequisites_test.sh"
|
||||
reject skipped-tests 'required' sh "$verify" prerequisites
|
||||
# The Playwright report converter must never turn a failed, skipped, retried,
|
||||
# empty or missing report into a passing case.
|
||||
cat > "$fixture/scripts/addons/suites.json" <<'JSON'
|
||||
{"prerequisites":{"kind":"shell","commands":[["python3","-I","scripts/addons/ui_cases.py","playwright","report.json"]],"cases":["first","second"]}}
|
||||
JSON
|
||||
ui_report() {
|
||||
printf '{"suites":[{"title":"f.spec.js","specs":[{"title":"first","tests":[{"status":"expected","results":[{"status":"passed"}]}]},{"title":"second","tests":[{"status":"%s","results":%s}]}]}]}' "$1" "$2" > "$fixture/report.json"
|
||||
}
|
||||
ui_report unexpected '[{"status":"failed"}]'
|
||||
reject ui-report-failed 'required case not passed: second' sh "$verify" prerequisites
|
||||
ui_report skipped '[]'
|
||||
reject ui-report-skipped 'required case not passed: second' sh "$verify" prerequisites
|
||||
ui_report flaky '[{"status":"failed"},{"status":"passed"}]'
|
||||
reject ui-report-flaky 'required case not passed: second' sh "$verify" prerequisites
|
||||
printf '{"suites":[]}' > "$fixture/report.json"
|
||||
reject ui-report-empty 'playwright-report-empty' sh "$verify" prerequisites
|
||||
rm -f "$fixture/report.json"
|
||||
reject ui-report-missing 'playwright-report' sh "$verify" prerequisites
|
||||
# Machine-readable Ginkgo reports must reject empty and skipped selections.
|
||||
cat > "$fixture/scripts/addons/suites.json" <<'JSON'
|
||||
{"prerequisites":{"kind":"ginkgo","commands":[["go","test","-count=1","-tags","auth","./fixture","-ginkgo.json-report=.cache/report.json"]],"cases":["sqlite transaction"],"report":".cache/report.json"}}
|
||||
JSON
|
||||
cat > "$fixture/bin/go" <<'TOOL'
|
||||
#!/bin/sh
|
||||
mkdir -p .cache
|
||||
printf '%s' "$FIXTURE_REPORT" > .cache/report.json
|
||||
TOOL
|
||||
reject ginkgo-empty 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[]' sh "$verify" prerequisites
|
||||
reject ginkgo-skipped 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":true,"SpecReports":[{"State":"skipped","LeafNodeType":"It","LeafNodeText":"sqlite transaction"}]}]' sh "$verify" prerequisites
|
||||
reject ginkgo-missing 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='{}' sh "$verify" prerequisites
|
||||
# A failed Ginkgo run names its failed spec and the first line of its failure.
|
||||
cat > "$fixture/bin/go" <<'TOOL'
|
||||
#!/bin/sh
|
||||
mkdir -p .cache
|
||||
printf '%s' "$FIXTURE_REPORT" > .cache/report.json
|
||||
exit 1
|
||||
TOOL
|
||||
reject ginkgo-failed-spec-named 'failed spec: sqlite transaction' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":false,"SpecReports":[{"State":"failed","LeafNodeType":"It","LeafNodeText":"sqlite transaction","Failure":{"Message":"Expected 1 to equal 2\nmore"}}]}]' sh "$verify" prerequisites
|
||||
reject ginkgo-failure-message 'Expected 1 to equal 2' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":false,"SpecReports":[{"State":"failed","LeafNodeType":"It","LeafNodeText":"sqlite transaction","Failure":{"Message":"Expected 1 to equal 2\nmore"}}]}]' sh "$verify" prerequisites
|
||||
cat > "$fixture/bin/go" <<'TOOL'
|
||||
#!/bin/sh
|
||||
mkdir -p .cache
|
||||
printf '%s' "$FIXTURE_REPORT" > .cache/report.json
|
||||
TOOL
|
||||
# Registered contracts must reach the same report gate, never bypass it.
|
||||
python3 - "$fixture/scripts/addons/suites.json" <<'JSON'
|
||||
import json, sys
|
||||
p=sys.argv[1]
|
||||
v=json.load(open(p)); v['contracts']=v.pop('prerequisites')
|
||||
open(p,'w').write(json.dumps(v))
|
||||
JSON
|
||||
reject contracts-empty 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[]' sh "$verify" contracts
|
||||
reject contracts-skipped 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":true,"SpecReports":[{"State":"skipped","LeafNodeType":"It","LeafNodeText":"sqlite transaction"}]}]' sh "$verify" contracts
|
||||
# Every gateway report is mandatory, including a missing second report.
|
||||
cat > "$fixture/scripts/addons/suites.json" <<'JSON'
|
||||
{"gateway":{"kind":"ginkgo","commands":[["go","test","-count=1","-tags","auth","./fixture"]],"cases":["sqlite transaction","http"],"report":".cache/report.json","reports":[{"path":".cache/report.json","cases":["sqlite transaction"]},{"path":".cache/second.json","cases":["http"]}]}}
|
||||
JSON
|
||||
reject gateway-missing-report 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":true,"SpecReports":[{"State":"passed","LeafNodeType":"It","LeafNodeText":"sqlite transaction"}]}]' sh "$verify" gateway
|
||||
reject gateway-skipped-report 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":true,"SpecReports":[{"State":"skipped","LeafNodeType":"It","LeafNodeText":"sqlite transaction"}]}]' sh "$verify" gateway
|
||||
# A commit at a tagged module resolves to the tag, with an exact module sum.
|
||||
cat > "$fixture/bin/go" <<'TOOL'
|
||||
#!/bin/sh
|
||||
case "$*" in
|
||||
version) echo 'go version go1.26.1 linux/amd64';;
|
||||
'env CGO_ENABLED') echo 1;;
|
||||
'version -m '*protoc-gen-go-grpc) printf '\tmod\tgoogle.golang.org/grpc/cmd/protoc-gen-go-grpc\tv1.4.0\t%s\n' "${FIXTURE_SUM:-h1:9SxA29VM43MF5Z9dQu694wmY5t8E/Gxr7s+RSxiIDmc=}";;
|
||||
'version -m '*protoc-gen-go) printf '\tmod\tgoogle.golang.org/protobuf\tv1.34.2\th1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\n';;
|
||||
*) exit 91;;
|
||||
esac
|
||||
TOOL
|
||||
for tool in npm cc protoc-gen-go protoc-gen-go-grpc; do
|
||||
printf '#!/bin/sh\nexit 0\n' > "$fixture/bin/$tool"
|
||||
chmod +x "$fixture/bin/$tool"
|
||||
done
|
||||
cat > "$fixture/bin/protoc" <<'TOOL'
|
||||
#!/bin/sh
|
||||
if [ "$*" = --version ]; then echo 'libprotoc 26.1'; else echo 'pin accepted: generation sentinel' >&2; exit 92; fi
|
||||
TOOL
|
||||
reject exact-tag-pin 'generation sentinel' env PATH="$fixture/bin:$PATH" sh "$prepare" --integration
|
||||
reject wrong-tag-sum 'protoc-gen-go-grpc' env PATH="$fixture/bin:$PATH" FIXTURE_SUM=h1:wrong sh "$prepare" --integration
|
||||
# Explicit setup must use the local CLI and worktree browser cache, and
|
||||
# propagate an installer failure rather than reporting incomplete setup ready.
|
||||
cat > "$fixture/bin/go" <<'TOOL'
|
||||
#!/bin/sh
|
||||
case "$*" in
|
||||
version) echo 'go version go1.26.1 linux/amd64';;
|
||||
'env CGO_ENABLED') echo 1;;
|
||||
*) exit 0;;
|
||||
esac
|
||||
TOOL
|
||||
mkdir -p "$fixture/core/http/react-ui/node_modules/.bin"
|
||||
cat > "$fixture/core/http/react-ui/node_modules/.bin/playwright" <<'TOOL'
|
||||
#!/bin/sh
|
||||
[ "$*" = 'install chromium' ] || exit 93
|
||||
[ "$PLAYWRIGHT_BROWSERS_PATH" = "$EXPECTED_BROWSER_CACHE" ] || exit 94
|
||||
echo 'browser installation sentinel' >&2
|
||||
exit 95
|
||||
TOOL
|
||||
chmod +x "$fixture/core/http/react-ui/node_modules/.bin/playwright"
|
||||
# Select the download branch explicitly, regardless of the caller's browser.
|
||||
# This is scoped to the fixture child; verify must retain the real browser path.
|
||||
reject browser-setup-failure 'browser installation sentinel' env PATH="$fixture/bin:$PATH" PLAYWRIGHT_CHROMIUM_PATH= EXPECTED_BROWSER_CACHE="$fixture/.cache/addons/browsers" sh "$prepare" --prepare
|
||||
reject configured-browser-missing 'configured Chromium is not executable' env PATH="$fixture/bin:$PATH" PLAYWRIGHT_CHROMIUM_PATH="$fixture/missing-chromium" sh "$prepare" --prepare
|
||||
# Setup only checks executability and must bypass the failing installer above.
|
||||
# This stub is not browser readiness evidence: verify launches real Chromium.
|
||||
printf '#!/bin/sh\nexit 96\n' > "$fixture/bin/chromium"
|
||||
chmod +x "$fixture/bin/chromium"
|
||||
if ! env PATH="$fixture/bin:$PATH" PLAYWRIGHT_CHROMIUM_PATH="$fixture/bin/chromium" sh "$prepare" --prepare > "$fixture/output" 2>&1; then
|
||||
echo 'FAIL configured-browser-setup: unexpectedly failed' >&2
|
||||
cat "$fixture/output" >&2; exit 1
|
||||
fi
|
||||
count=$((count + 1))
|
||||
printf '{"case":"configured-browser-setup","status":"passed"}\n'
|
||||
# The leak scan of the shared scratch directory looks at executables and
|
||||
# working directories only. A shell of another session that merely names the
|
||||
# directory in its command line (a log tail, a wait loop) is not a leak and is
|
||||
# never killed; a process that runs in or from the directory still is.
|
||||
leaks="$root/scripts/addons/leaks.sh"
|
||||
[ -n "${LEAKS_SCRIPT:-}" ] && leaks=$LEAKS_SCRIPT
|
||||
scratch="$fixture/shared-scratch"
|
||||
mkdir -p "$scratch/run"
|
||||
sh -c ': '"$scratch"'/named-only; sleep 10' &
|
||||
named=$!
|
||||
(cd "$scratch/run" && exec sleep 10) &
|
||||
inside=$!
|
||||
sleep 0.3
|
||||
found=$(. "$leaks"; leaked_pids "$scratch/" paths-only)
|
||||
pkill -P "$named" 2>/dev/null || true
|
||||
kill "$named" "$inside" 2>/dev/null || true
|
||||
wait "$named" "$inside" 2>/dev/null || true
|
||||
case " $(echo $found) " in *" $named "*) echo 'FAIL leak-ignores-command-line: a shell that only names the directory was reported' >&2; exit 1 ;; esac
|
||||
count=$((count + 1))
|
||||
printf '{"case":"leak-ignores-command-line","status":"passed"}\n'
|
||||
case " $(echo $found) " in *" $inside "*) ;; *) echo 'FAIL leak-finds-working-directory: a process running in the directory was missed' >&2; exit 1 ;; esac
|
||||
count=$((count + 1))
|
||||
printf '{"case":"leak-finds-working-directory","status":"passed"}\n'
|
||||
printf '{"total":%s,"passed":%s,"skipped":0}\n' "$count" "$count"
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs the add-on data recovery checks and prints one JSON case line per test:
|
||||
# 1. the node unit tests of the recovery rules, the typed client and the
|
||||
# locale keys of the panel;
|
||||
# 2. the Go specs of the recovery package (WAL-consistent backup, corrupt and
|
||||
# oversized backups, cancellation, retention, every journal phase of an
|
||||
# upgrade and of a restore, grant resurrection, purge), of the policy
|
||||
# (hold, restore filter, revocation log, ownership migration), of the
|
||||
# Tables store (format migration) and of the real owner with the real
|
||||
# Tables executable over real HTTP (typed refusals, owner and frontend
|
||||
# parity, upgrade with data, reinstall identity, documentation);
|
||||
# 3. the Playwright spec against a UI test server that runs the real owner,
|
||||
# the real Tables executable and the real built UI package.
|
||||
# verify.sh compares the lines with the exact inventory of the recovery suite
|
||||
# and rejects a skipped, flaky or missing case.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
ui="$root/core/http/react-ui"
|
||||
pkg="$root/addons/tables/ui"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
|
||||
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
# The server embeds the UI build: a build made before the last change would
|
||||
# test the old host code.
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
for src in "$ui/src/components/addons/RecoveryPanel.jsx" "$ui/src/components/addons/recoveryState.js" "$ui/src/utils/addonsApi.js"; do
|
||||
[ "$ui/dist/index.html" -nt "$src" ] || { echo "the UI build is older than $src; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/core/services/addons/recovery/backup.go" ] || { echo 'ui-test-server is older than the recovery package; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/core/http/endpoints/addons/recovery.go" ] || { echo 'ui-test-server is older than the recovery routes; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
[ "$root/tests/e2e/mock-backend/mock-backend" -nt "$root/tests/e2e/mock-backend/main.go" ] || { echo 'mock-backend is older than its source; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
|
||||
# 1. Pure rules, the typed client and the locale keys.
|
||||
(cd "$ui" && node tests/unit/run.mjs recovery.test.mjs)
|
||||
|
||||
# 2. Go: ginkgo specs reported as case lines. Specs that a focus filtered out
|
||||
# are not part of the run and are not printed; a failed or skipped spec is.
|
||||
go_cases() {
|
||||
python3 -I - "$1" <<'PY'
|
||||
import json, sys
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
|
||||
continue
|
||||
status = {'passed': 'passed'}.get(spec['State'], 'failed')
|
||||
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
|
||||
PY
|
||||
}
|
||||
run_go() {
|
||||
name=$1
|
||||
shift
|
||||
report="$cache/logs/recovery-$name.json"
|
||||
rm -f "$report"
|
||||
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/recovery-$name.log" 2>&1 || { tail -40 "$cache/logs/recovery-$name.log" >&2; exit 1; }
|
||||
go_cases "$report"
|
||||
}
|
||||
run_go package ./core/services/addons/recovery
|
||||
run_go policy ./core/services/addons/policy -ginkgo.focus='Addon recovery state of the policy'
|
||||
run_go store ./addons/tables/internal/store -ginkgo.focus='Format migration'
|
||||
run_go owner ./tests/addons/recovery
|
||||
|
||||
# 3. The real owner, the real Tables executable, the built package.
|
||||
node "$pkg/build.mjs" >"$cache/logs/recovery-build.log" 2>&1 || { cat "$cache/logs/recovery-build.log" >&2; exit 1; }
|
||||
tables_exe="$cache/tmp/recovery-exe"
|
||||
CGO_ENABLED=1 go build -p 2 -o "$tables_exe" ./addons/tables/cmd/tables
|
||||
fixture_dir="$cache/tmp/recovery-fixture"
|
||||
rm -rf "$fixture_dir"
|
||||
mkdir -p "$fixture_dir/tables"
|
||||
cp "$root/addons/tables/manifest.json" "$fixture_dir/tables/manifest.json"
|
||||
cp -R "$pkg/dist/ui" "$fixture_dir/tables/ui"
|
||||
|
||||
free_port() { python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])'; }
|
||||
named_port=$(free_port)
|
||||
creds="$cache/logs/recovery-credentials.json"
|
||||
rm -f "$creds"
|
||||
shots=${RECOVERY_SHOTS:-}
|
||||
trap 'kill "$named" 2>/dev/null || true; wait "$named" 2>/dev/null || true; rm -rf "$fixture_dir" "$tables_exe"' EXIT HUP INT TERM
|
||||
named=0
|
||||
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$named_port" \
|
||||
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$creds" \
|
||||
--fixture-enable=tables@0.1.0 >"$cache/logs/recovery-server.log" 2>&1 &
|
||||
named=$!
|
||||
i=0
|
||||
until [ -s "$creds" ] && python3 -I -c "import urllib.request;urllib.request.urlopen('http://127.0.0.1:$named_port/readyz',timeout=1)" 2>/dev/null; do
|
||||
i=$((i + 1))
|
||||
kill -0 "$named" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$cache/logs/recovery-server.log" >&2; exit 1; }
|
||||
[ "$i" -lt 180 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
report="$cache/logs/recovery-report.json"
|
||||
rm -f "$report"
|
||||
status=0
|
||||
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$named_port" ADDONS_FIXTURE="$creds" ADDONS_SHOTS="$shots" \
|
||||
ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.recovery.config.js >"$cache/logs/recovery-playwright.log" 2>&1) || status=$?
|
||||
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
|
||||
exit "$status"
|
||||
File diff suppressed because it is too large.
Load diff
Executable
+170
@@ -0,0 +1,170 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs the Tables acceptance and prints one JSON case line per check:
|
||||
# 1. Go: the end-to-end specs of tests/addons/tablesacceptance. They start
|
||||
# the real owner and two forwarding frontends as separate server
|
||||
# processes over the real mTLS carrier with one shared account store, and
|
||||
# install the real packaged Tables (built by scripts/addons/package.sh)
|
||||
# from a local catalog. Every call uses a real named session or API key,
|
||||
# the public HTTP routes and the real SQLite databases of the add-on;
|
||||
# 2. the Playwright spec against the same kind of cluster, started by the
|
||||
# tables-cluster command;
|
||||
# 3. the evidence manifest, a JSON file the run writes under .cache/addons:
|
||||
# commands, case inventory, native platform, package digests and the
|
||||
# traces retained for a failure. A Go test checks it.
|
||||
# Nothing is signed, tagged, pushed or published. verify.sh compares the lines
|
||||
# with the exact inventory of the tables-acceptance suite and rejects a
|
||||
# skipped, flaky or missing case.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
ui="$root/core/http/react-ui"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
|
||||
for file in tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
[ -f "$ui/dist/index.html" ] || { echo 'missing UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
[ "$root/tests/e2e/mock-backend/mock-backend" -nt "$root/tests/e2e/mock-backend/main.go" ] || { echo 'mock-backend is older than its source; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
|
||||
work=$(mktemp -d "$cache/tmp/tables-acceptance.XXXXXX")
|
||||
cluster_pid=
|
||||
. "$root/scripts/addons/leaks.sh"
|
||||
leaked=0
|
||||
cleanup() {
|
||||
code=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
if [ -n "$cluster_pid" ]; then
|
||||
# The cluster tears its servers down on SIGTERM; wait, bounded.
|
||||
kill "$cluster_pid" 2>/dev/null || true
|
||||
n=0
|
||||
while kill -0 "$cluster_pid" 2>/dev/null && [ "$n" -lt 60 ]; do
|
||||
n=$((n + 1))
|
||||
sleep 0.5
|
||||
done
|
||||
kill -KILL "$cluster_pid" 2>/dev/null || true
|
||||
wait "$cluster_pid" 2>/dev/null || true
|
||||
fi
|
||||
# Nothing that names the work or scratch directories may survive. Whatever
|
||||
# does is killed, and the run fails.
|
||||
reap_leaks "$work" tables-acceptance || leaked=1
|
||||
reap_leaks "$cache/tmp/" tables-acceptance paths-only || leaked=1
|
||||
find "$work" -type d -exec chmod u+rwx {} + 2>/dev/null || true
|
||||
rm -rf "$work"
|
||||
if [ "$leaked" -ne 0 ] && [ "$code" -eq 0 ]; then
|
||||
code=1
|
||||
fi
|
||||
exit "$code"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 143' HUP INT TERM
|
||||
|
||||
# The binaries are built here, never taken from an earlier build: the server
|
||||
# embeds the UI build and every other part of the fixture.
|
||||
go build -p 2 -tags auth -o "$work/ui-test-server" ./tests/e2e-ui
|
||||
CGO_ENABLED=1 go build -p 2 -trimpath -o "$work/tables" ./addons/tables/cmd/tables
|
||||
go build -p 2 -tags auth -o "$work/tables-cluster" ./tests/addons/tablesacceptance/cmd/tables-cluster
|
||||
|
||||
evidence="$cache/tables-acceptance-evidence.json"
|
||||
rm -f "$evidence"
|
||||
rm -rf "$cache/logs/tables-acceptance-traces"
|
||||
|
||||
# 1. The end-to-end specs.
|
||||
go_report="$cache/logs/tables-acceptance-go.json"
|
||||
rm -f "$go_report"
|
||||
TABLES_ACCEPTANCE_SERVER="$work/ui-test-server" TABLES_ACCEPTANCE_TABLES="$work/tables" \
|
||||
TABLES_ACCEPTANCE_MOCK="$root/tests/e2e/mock-backend/mock-backend" TABLES_ACCEPTANCE_EVIDENCE="$evidence" \
|
||||
go test -count=1 -p 2 -tags auth -timeout 30m ./tests/addons/tablesacceptance -ginkgo.json-report="$go_report" >"$cache/logs/tables-acceptance-go.log" 2>&1 || {
|
||||
python3 -I - "$go_report" <<'PY' >&2 || true
|
||||
import json, sys
|
||||
try:
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] == 'It' and spec['State'] != 'passed':
|
||||
print('failed case: go: %s (%s)' % (spec['LeafNodeText'], spec['State']))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
PY
|
||||
tail -60 "$cache/logs/tables-acceptance-go.log" >&2
|
||||
exit 1
|
||||
}
|
||||
python3 -I - "$go_report" <<'PY'
|
||||
import json, sys
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It':
|
||||
continue
|
||||
status = {'passed': 'passed', 'skipped': 'skipped'}.get(spec['State'], 'failed')
|
||||
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
|
||||
PY
|
||||
|
||||
# 2. The browser against the real cluster.
|
||||
ready="$work/ready.json"
|
||||
"$work/tables-cluster" -dir "$work/state" -root "$root" -server "$work/ui-test-server" -mock-backend "$root/tests/e2e/mock-backend/mock-backend" \
|
||||
-tables "$work/tables" -ready "$ready" >"$cache/logs/tables-acceptance-cluster.log" 2>&1 &
|
||||
cluster_pid=$!
|
||||
i=0
|
||||
until [ -s "$ready" ]; do
|
||||
i=$((i + 1))
|
||||
kill -0 "$cluster_pid" 2>/dev/null || { echo 'acceptance cluster exited' >&2; tail -20 "$cache/logs/tables-acceptance-cluster.log" >&2; exit 1; }
|
||||
[ "$i" -lt 300 ] || { echo 'acceptance cluster readiness timed out' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
report="$cache/logs/tables-acceptance-report.json"
|
||||
rm -f "$report"
|
||||
status=0
|
||||
(cd "$ui" && ADDONS_TABLES_CLUSTER="$ready" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.tables-acceptance.config.js >"$cache/logs/tables-acceptance-playwright.log" 2>&1) || status=1
|
||||
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo '--- playwright log (tail) ---' >&2
|
||||
tail -60 "$cache/logs/tables-acceptance-playwright.log" >&2 || true
|
||||
echo '--- cluster log (tail) ---' >&2
|
||||
tail -30 "$cache/logs/tables-acceptance-cluster.log" >&2 || true
|
||||
mkdir -p "$cache/logs/tables-acceptance-traces"
|
||||
cp "$cache/logs/tables-acceptance-playwright.log" "$cache/logs/tables-acceptance-cluster.log" "$cache/logs/tables-acceptance-traces/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 3. The evidence manifest: add the browser cases and run the check.
|
||||
python3 -I - "$evidence" "$report" "$cache/logs/tables-acceptance-traces" "$cache/logs/tables-acceptance-playwright.log" <<'PY' || status=1
|
||||
import json, os, sys
|
||||
evidence, report, traces, log = sys.argv[1:5]
|
||||
ev = json.load(open(evidence))
|
||||
cases = []
|
||||
def walk(suite):
|
||||
for spec in suite.get('specs', []):
|
||||
for test in spec.get('tests', []):
|
||||
results = test.get('results', [])
|
||||
ok = test.get('status') == 'expected' and len(results) == 1 and results[0].get('status') == 'passed'
|
||||
case = {'name': spec['title'], 'state': 'passed' if ok else 'failed',
|
||||
'millis': int(sum(r.get('duration', 0) for r in results))}
|
||||
if not ok:
|
||||
case['trace'] = log
|
||||
case['error'] = ((results[-1].get('error') or {}).get('message') or '')[:2000] if results else ''
|
||||
cases.append(case)
|
||||
for child in suite.get('suites', []):
|
||||
walk(child)
|
||||
try:
|
||||
for suite in json.load(open(report)).get('suites', []):
|
||||
walk(suite)
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
ev['browser'] = cases
|
||||
ev['commands'].append(['playwright', 'test', '--config', 'playwright.tables-acceptance.config.js'])
|
||||
tmp = evidence + '.tmp'
|
||||
json.dump(ev, open(tmp, 'w'), indent=2)
|
||||
os.replace(tmp, evidence)
|
||||
PY
|
||||
if TABLES_ACCEPTANCE_CHECK_EVIDENCE=1 TABLES_ACCEPTANCE_EVIDENCE="$evidence" \
|
||||
go test -count=1 -tags auth -run 'TestEvidenceManifest$' -v ./tests/addons/tablesacceptance >"$cache/logs/tables-acceptance-evidence.log" 2>&1 \
|
||||
&& grep -q -- '--- PASS: TestEvidenceManifest' "$cache/logs/tables-acceptance-evidence.log"; then
|
||||
printf '{"case": "manifest: the evidence manifest names the platform, the commands, the package digests and every case", "status": "passed"}\n'
|
||||
else
|
||||
printf '{"case": "manifest: the evidence manifest names the platform, the commands, the package digests and every case", "status": "failed"}\n'
|
||||
tail -30 "$cache/logs/tables-acceptance-evidence.log" >&2
|
||||
status=1
|
||||
fi
|
||||
exit "$status"
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs the Tables CSV export checks and prints one JSON case line per test:
|
||||
# 1. the node unit tests of the file card rules and of the export call of
|
||||
# the Tables page;
|
||||
# 2. the Go specs of the CSV store (snapshot, quoting, formula guard,
|
||||
# bounds), of the runtime output handles, of the real owner with the real
|
||||
# Tables executable (private artifacts, failures, expiry, quota) and of
|
||||
# the owner with two real frontend processes (identical bytes and
|
||||
# headers from every replica);
|
||||
# 3. the Playwright spec against two UI test servers that run the real
|
||||
# owner, the real Tables executable and the real built UI package: one
|
||||
# with named accounts and one in local-owner mode.
|
||||
# verify.sh compares the lines with the exact inventory of the tables-csv
|
||||
# suite and rejects a skipped, flaky or missing case.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
ui="$root/core/http/react-ui"
|
||||
pkg="$root/addons/tables/ui"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
|
||||
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
# The server embeds the UI build: a build made before the last change would
|
||||
# test the old host code.
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/core/services/addons/resources/service.go" ] || { echo 'ui-test-server is older than the resource service; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
[ "$root/tests/e2e/mock-backend/mock-backend" -nt "$root/tests/e2e/mock-backend/main.go" ] || { echo 'mock-backend is older than its source; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/tests/e2e-ui/addons_fixture.go" ] || { echo 'ui-test-server is older than its fixture; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
|
||||
# 1. Pure rules: the file card and the export call.
|
||||
(cd "$ui" && node tests/unit/run.mjs tablesartifacts.test.mjs)
|
||||
|
||||
# 2. Go: ginkgo specs reported as case lines. Specs that a focus filtered out
|
||||
# are not part of the run and are not printed; a failed or skipped spec is.
|
||||
go_cases() {
|
||||
python3 -I - "$1" <<'PY'
|
||||
import json, sys
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
|
||||
continue
|
||||
status = {'passed': 'passed'}.get(spec['State'], 'failed')
|
||||
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
|
||||
PY
|
||||
}
|
||||
run_go() {
|
||||
name=$1
|
||||
shift
|
||||
report="$cache/logs/tables-csv-$name.json"
|
||||
rm -f "$report"
|
||||
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/tables-csv-$name.log" 2>&1 || { tail -40 "$cache/logs/tables-csv-$name.log" >&2; exit 1; }
|
||||
go_cases "$report"
|
||||
}
|
||||
run_go store ./addons/tables/internal/store -ginkgo.focus='CSV export'
|
||||
run_go server ./addons/tables/internal/server
|
||||
run_go owner ./tests/addons/tablesadmin -ginkgo.focus='CSV export'
|
||||
FOUNDATION_SERVER="$root/tests/e2e-ui/ui-test-server" run_go replicas ./tests/addons/tablesexport
|
||||
|
||||
# 3. The real owner, the real Tables executable, the built package.
|
||||
node "$pkg/build.mjs" >"$cache/logs/tables-csv-build.log" 2>&1 || { cat "$cache/logs/tables-csv-build.log" >&2; exit 1; }
|
||||
tables_exe="$cache/tmp/tables-csv-exe"
|
||||
CGO_ENABLED=1 go build -p 2 -o "$tables_exe" ./addons/tables/cmd/tables
|
||||
fixture_dir="$cache/tmp/tables-csv-fixture"
|
||||
rm -rf "$fixture_dir"
|
||||
mkdir -p "$fixture_dir/tables"
|
||||
cp "$root/addons/tables/manifest.json" "$fixture_dir/tables/manifest.json"
|
||||
cp -R "$pkg/dist/ui" "$fixture_dir/tables/ui"
|
||||
|
||||
free_port() { python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])'; }
|
||||
named_port=$(free_port)
|
||||
local_port=$(free_port)
|
||||
creds="$cache/logs/tables-csv-credentials.json"
|
||||
local_creds="$cache/logs/tables-csv-local-credentials.json"
|
||||
rm -f "$creds" "$local_creds"
|
||||
trap 'kill "$named" "$localsrv" 2>/dev/null || true; wait "$named" "$localsrv" 2>/dev/null || true; rm -rf "$fixture_dir"' EXIT HUP INT TERM
|
||||
named=0
|
||||
localsrv=0
|
||||
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$named_port" \
|
||||
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$creds" \
|
||||
--fixture-enable=tables@0.1.0 >"$cache/logs/tables-csv-server.log" 2>&1 &
|
||||
named=$!
|
||||
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$local_port" \
|
||||
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$local_creds" \
|
||||
--addons-local-owner --fixture-enable=tables@0.1.0 >"$cache/logs/tables-csv-local-server.log" 2>&1 &
|
||||
localsrv=$!
|
||||
wait_for() {
|
||||
file=$1
|
||||
port=$2
|
||||
pid=$3
|
||||
i=0
|
||||
until [ -s "$file" ] && python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
|
||||
i=$((i + 1))
|
||||
kill -0 "$pid" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$cache/logs/tables-csv-server.log" "$cache/logs/tables-csv-local-server.log" >&2; exit 1; }
|
||||
[ "$i" -lt 180 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
wait_for "$creds" "$named_port" "$named"
|
||||
wait_for "$local_creds" "$local_port" "$localsrv"
|
||||
report="$cache/logs/tables-csv-report.json"
|
||||
rm -f "$report"
|
||||
status=0
|
||||
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$named_port" ADDONS_LOCAL_URL="http://127.0.0.1:$local_port" ADDONS_FIXTURE="$creds" \
|
||||
ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.csv.config.js >"$cache/logs/tables-csv-playwright.log" 2>&1) || status=$?
|
||||
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
|
||||
exit "$status"
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs every Tables suite in order, the acceptance last, and prints the final
|
||||
# inventory:
|
||||
# suite -> passed count, skipped count, status
|
||||
# It exits nonzero when any suite fails, reports a skipped case, leaves a
|
||||
# process behind or is missing from the set, and it never stops at the first
|
||||
# failure, so one run shows every broken suite.
|
||||
#
|
||||
# The set is explicit. A suite named tables-* that is added to
|
||||
# scripts/addons/suites.json and not named here is reported as missing from the
|
||||
# gate, so the gate cannot silently fall behind the inventory. The foundation
|
||||
# suites are not part of this gate: scripts/addons/foundation_gate.sh runs them.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
mkdir -p "$cache/logs" "$cache/tmp"
|
||||
. "$root/scripts/addons/leaks.sh"
|
||||
suites='tables-databases tables-schema tables-rows tables-tools tables-admin tables-ui tables-csv tables-acceptance'
|
||||
results="$cache/logs/tables-gate-$$.jsonl"
|
||||
: >"$results"
|
||||
for suite in $suites; do
|
||||
out="$cache/logs/tables-gate-$suite.out"
|
||||
# A suite that leaves a process that names the scratch directory behind
|
||||
# fails, whatever its own result was; the leftover is killed.
|
||||
leak=0
|
||||
ok=0
|
||||
sh scripts/addons/verify.sh "$suite" >"$out" 2>&1 || ok=1
|
||||
reap_leaks "$cache/tmp/" "$suite" paths-only >>"$out" 2>&1 || leak=1
|
||||
if [ "$leak" -eq 1 ]; then
|
||||
printf '{"suite":"%s","passed":0,"skipped":0,"status":"leaked-processes","log":"%s"}\n' "$suite" "$out" >>"$results"
|
||||
elif [ "$ok" -eq 0 ]; then
|
||||
line=$(tail -n 1 "$out")
|
||||
printf '%s\n' "$line" | python3 -I -c 'import json,sys; d=json.loads(sys.stdin.read()); assert d["suite"]=="'"$suite"'"; print(json.dumps({"suite":d["suite"],"passed":d["passed"],"skipped":d["skipped"],"status":"passed" if d["skipped"]==0 else "skipped"}))' >>"$results" 2>/dev/null \
|
||||
|| printf '{"suite":"%s","passed":0,"skipped":0,"status":"no-result"}\n' "$suite" >>"$results"
|
||||
else
|
||||
printf '{"suite":"%s","passed":0,"skipped":0,"status":"failed","log":"%s"}\n' "$suite" "$out" >>"$results"
|
||||
fi
|
||||
done
|
||||
python3 -I - "$results" "$root/scripts/addons/suites.json" "$suites" <<'PY'
|
||||
import json, sys
|
||||
rows = [json.loads(l) for l in open(sys.argv[1]) if l.strip()]
|
||||
inventory = json.load(open(sys.argv[2]))
|
||||
named = sys.argv[3].split()
|
||||
tables = sorted(k for k in inventory if k.startswith('tables-'))
|
||||
missing = sorted(set(tables) - set(named))
|
||||
absent = sorted(set(named) - set(inventory))
|
||||
width = max(len(r['suite']) for r in rows)
|
||||
print('tables gate inventory')
|
||||
total = skipped = 0
|
||||
bad = False
|
||||
for r in rows:
|
||||
print(' %-*s passed=%-4d skipped=%-2d %s' % (width, r['suite'], r['passed'], r['skipped'], r['status']))
|
||||
total += r['passed']
|
||||
skipped += r['skipped']
|
||||
if r['status'] != 'passed':
|
||||
bad = True
|
||||
for name in missing:
|
||||
print(' %-*s not part of the gate: add it to scripts/addons/tables_gate.sh' % (width, name))
|
||||
for name in absent:
|
||||
print(' %-*s named by the gate and absent from suites.json' % (width, name))
|
||||
print('total passed=%d skipped=%d suites=%d' % (total, skipped, len(rows)))
|
||||
sys.exit(1 if bad or missing or absent else 0)
|
||||
PY
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs the Tables tool checks and prints one JSON case line per test:
|
||||
# 1. the node unit tests of the proposal card rules (reference parsing, the
|
||||
# stored history, the facts the card draws, the confirmation call);
|
||||
# 2. the Go specs of the tool adapter (discovery, forged fields, approval
|
||||
# aliases, the executors, the consumer matrix), of the pending proposal
|
||||
# list across the owner and forwarding frontends, and of the real owner
|
||||
# with the real Tables executable (tool calls to proposals to
|
||||
# confirmation);
|
||||
# 3. the Playwright spec against the UI test server running with real
|
||||
# authentication, the real owner, the real Tables executable and a
|
||||
# scripted model that answers with the tool call a spec encodes.
|
||||
# verify.sh compares the lines with the exact inventory of the tables-tools
|
||||
# suite and rejects a skipped, flaky or missing case.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
ui="$root/core/http/react-ui"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
|
||||
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
# The server embeds the UI build and the mock model embeds its script
|
||||
# markers: a build made before the last change would test the old code.
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
[ "$root/tests/e2e/mock-backend/mock-backend" -nt "$root/tests/e2e/mock-backend/main.go" ] || { echo 'mock-backend is older than its source; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/core/services/addons/tools/adapter.go" ] || { echo 'ui-test-server is older than the adapter; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
|
||||
# 1. Pure card, history and chat action rules.
|
||||
(cd "$ui" && node tests/unit/run.mjs tablestools.test.mjs)
|
||||
|
||||
# 2. Go: ginkgo specs reported as case lines. Specs that a focus filtered out
|
||||
# are not part of the run and are not printed; a failed or skipped spec is.
|
||||
go_cases() {
|
||||
python3 -I - "$1" <<'PY'
|
||||
import json, sys
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
|
||||
continue
|
||||
status = {'passed': 'passed'}.get(spec['State'], 'failed')
|
||||
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
|
||||
PY
|
||||
}
|
||||
run_go() {
|
||||
name=$1
|
||||
shift
|
||||
report="$cache/logs/tables-tools-$name.json"
|
||||
rm -f "$report"
|
||||
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/tables-tools-$name.log" 2>&1 || { tail -40 "$cache/logs/tables-tools-$name.log" >&2; exit 1; }
|
||||
go_cases "$report"
|
||||
}
|
||||
run_go adapter ./core/services/addons/tools
|
||||
run_go cli ./core/cli -ginkgo.focus='mcpJobSessionOption'
|
||||
run_go pending ./core/http/endpoints/addons -ginkgo.focus='lists the caller.s own pending proposals'
|
||||
run_go owner ./tests/addons/tables -ginkgo.focus='Tables tools under the real owner'
|
||||
|
||||
# 3. The real owner, the real Tables executable and the browser.
|
||||
tables_exe="$cache/tmp/tables-exe"
|
||||
CGO_ENABLED=1 go build -p 2 -o "$tables_exe" ./addons/tables/cmd/tables
|
||||
fixture_dir="$cache/tmp/tables-tools-fixture"
|
||||
rm -rf "$fixture_dir"
|
||||
mkdir -p "$fixture_dir/tables"
|
||||
cp "$root/addons/tables/manifest.json" "$fixture_dir/tables/manifest.json"
|
||||
# The manifest declares the pages of the package, so the package carries them.
|
||||
node "$root/addons/tables/ui/build.mjs" >"$cache/logs/tables-tools-ui-build.log" 2>&1 || { cat "$cache/logs/tables-tools-ui-build.log" >&2; exit 1; }
|
||||
cp -R "$root/addons/tables/ui/dist/ui" "$fixture_dir/tables/ui"
|
||||
port=$(python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])')
|
||||
creds="$cache/logs/tables-tools-credentials.json"
|
||||
rm -f "$creds"
|
||||
log="$cache/logs/tables-tools-server.log"
|
||||
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$port" \
|
||||
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$creds" \
|
||||
--fixture-enable=tables@0.1.0 >"$log" 2>&1 &
|
||||
server=$!
|
||||
trap 'kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true; rm -rf "$fixture_dir"' EXIT HUP INT TERM
|
||||
i=0
|
||||
until [ -s "$creds" ] && python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
|
||||
i=$((i + 1))
|
||||
kill -0 "$server" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$log" >&2; exit 1; }
|
||||
[ "$i" -lt 120 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
report="$cache/logs/tables-tools-report.json"
|
||||
rm -f "$report"
|
||||
status=0
|
||||
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$port" ADDONS_FIXTURE="$creds" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.tools.config.js >"$cache/logs/tables-tools-playwright.log" 2>&1) || status=$?
|
||||
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
|
||||
exit "$status"
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs the Tables UI checks and prints one JSON case line per test:
|
||||
# 1. the node unit tests of the host SDK resource calls and of the Tables UI
|
||||
# package (field codecs, filter builder, batch bounds, error mapping, the
|
||||
# build output and a render of the first states);
|
||||
# 2. the Go specs of the UI-intent change route against the real owner and
|
||||
# the real Tables executable;
|
||||
# 3. the Playwright spec against two UI test servers that run the real
|
||||
# owner, the real Tables executable and the real built UI package: one
|
||||
# with named accounts and one in local-owner mode.
|
||||
# verify.sh compares the lines with the exact inventory of the tables-ui suite
|
||||
# and rejects a skipped, flaky or missing case.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
ui="$root/core/http/react-ui"
|
||||
pkg="$root/addons/tables/ui"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
|
||||
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
# The server embeds the UI build: a build made before the last change would
|
||||
# test the old host code.
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/core/services/addons/resources/service.go" ] || { echo 'ui-test-server is older than the resource service; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/tests/e2e-ui/addons_fixture.go" ] || { echo 'ui-test-server is older than its fixture; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
|
||||
# 1. Pure rules: the SDK resource calls and the package.
|
||||
(cd "$ui" && node tests/unit/run.mjs resources.test.mjs)
|
||||
(cd "$pkg" && node test/run.mjs)
|
||||
|
||||
# 2. Go: the UI-intent change route.
|
||||
go_cases() {
|
||||
python3 -I - "$1" <<'PY'
|
||||
import json, sys
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
|
||||
continue
|
||||
status = {'passed': 'passed'}.get(spec['State'], 'failed')
|
||||
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
|
||||
PY
|
||||
}
|
||||
report="$cache/logs/tables-ui-apply.json"
|
||||
rm -f "$report"
|
||||
go test -count=1 -p 2 -tags auth ./tests/addons/tablesadmin -ginkgo.focus='UI changes of a database' -ginkgo.json-report="$report" >"$cache/logs/tables-ui-apply.log" 2>&1 || { tail -40 "$cache/logs/tables-ui-apply.log" >&2; exit 1; }
|
||||
go_cases "$report"
|
||||
|
||||
# 3. The real owner, the real Tables executable, the built package.
|
||||
node "$pkg/build.mjs" >"$cache/logs/tables-ui-build.log" 2>&1 || { cat "$cache/logs/tables-ui-build.log" >&2; exit 1; }
|
||||
tables_exe="$cache/tmp/tables-ui-exe"
|
||||
CGO_ENABLED=1 go build -p 2 -o "$tables_exe" ./addons/tables/cmd/tables
|
||||
fixture_dir="$cache/tmp/tables-ui-fixture"
|
||||
rm -rf "$fixture_dir"
|
||||
mkdir -p "$fixture_dir/tables" "$fixture_dir/tables-next"
|
||||
cp "$root/addons/tables/manifest.json" "$fixture_dir/tables/manifest.json"
|
||||
cp -R "$pkg/dist/ui" "$fixture_dir/tables/ui"
|
||||
# A second release, for the stale-tab checks: the same package, one patch up.
|
||||
python3 -I - "$root/addons/tables/manifest.json" "$fixture_dir/tables-next/manifest.json" <<'PY'
|
||||
import json, sys
|
||||
m = json.load(open(sys.argv[1]))
|
||||
m['version'] = '0.1.1'
|
||||
json.dump(m, open(sys.argv[2], 'w'))
|
||||
PY
|
||||
cp -R "$pkg/dist/ui" "$fixture_dir/tables-next/ui"
|
||||
|
||||
free_port() { python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])'; }
|
||||
named_port=$(free_port)
|
||||
local_port=$(free_port)
|
||||
creds="$cache/logs/tables-ui-credentials.json"
|
||||
local_creds="$cache/logs/tables-ui-local-credentials.json"
|
||||
rm -f "$creds" "$local_creds"
|
||||
trap 'kill "$named" "$localsrv" 2>/dev/null || true; wait "$named" "$localsrv" 2>/dev/null || true; rm -rf "$fixture_dir"' EXIT HUP INT TERM
|
||||
named=0
|
||||
localsrv=0
|
||||
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$named_port" \
|
||||
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$creds" \
|
||||
--fixture-enable=tables@0.1.0 >"$cache/logs/tables-ui-server.log" 2>&1 &
|
||||
named=$!
|
||||
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$local_port" \
|
||||
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$local_creds" \
|
||||
--addons-local-owner --fixture-enable=tables@0.1.0 >"$cache/logs/tables-ui-local-server.log" 2>&1 &
|
||||
localsrv=$!
|
||||
wait_for() {
|
||||
file=$1
|
||||
port=$2
|
||||
pid=$3
|
||||
i=0
|
||||
until [ -s "$file" ] && python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
|
||||
i=$((i + 1))
|
||||
kill -0 "$pid" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$cache/logs/tables-ui-server.log" "$cache/logs/tables-ui-local-server.log" >&2; exit 1; }
|
||||
[ "$i" -lt 180 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
wait_for "$creds" "$named_port" "$named"
|
||||
wait_for "$local_creds" "$local_port" "$localsrv"
|
||||
report="$cache/logs/tables-ui-report.json"
|
||||
rm -f "$report"
|
||||
status=0
|
||||
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$named_port" ADDONS_LOCAL_URL="http://127.0.0.1:$local_port" ADDONS_FIXTURE="$creds" \
|
||||
ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.tables.config.js >"$cache/logs/tables-ui-playwright.log" 2>&1) || status=$?
|
||||
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
|
||||
exit "$status"
|
||||
@@ -0,0 +1,58 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Turns a Playwright JSON report into the case lines the verify runner reads.
|
||||
|
||||
usage: ui_cases.py playwright REPORT.json
|
||||
|
||||
One line {"case": <spec title>, "status": passed|failed|skipped|flaky} per
|
||||
test. A test is passed only when it passed on its first attempt: a retry that
|
||||
passed is flaky, and a skipped, fixme or todo test is skipped. Any error the
|
||||
runner reports outside a test is a failed case of its own, so an empty or
|
||||
partial run can never look like success.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def walk(suite, out):
|
||||
for spec in suite.get('specs', []):
|
||||
for test in spec.get('tests', []):
|
||||
results = test.get('results', [])
|
||||
status = test.get('status')
|
||||
if status == 'expected' and len(results) == 1 and results[0].get('status') == 'passed':
|
||||
state = 'passed'
|
||||
elif status == 'skipped' or not results or all(r.get('status') == 'skipped' for r in results):
|
||||
state = 'skipped'
|
||||
elif status == 'flaky':
|
||||
state = 'flaky'
|
||||
else:
|
||||
state = 'failed'
|
||||
out.append({'case': spec['title'], 'status': state})
|
||||
for child in suite.get('suites', []):
|
||||
walk(child, out)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 3 or argv[1] != 'playwright':
|
||||
print('usage: ui_cases.py playwright REPORT.json', file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
with open(argv[2]) as f:
|
||||
report = json.load(f)
|
||||
assert isinstance(report, dict)
|
||||
except (OSError, ValueError, AssertionError):
|
||||
print(json.dumps({'case': 'playwright-report', 'status': 'failed'}))
|
||||
return 1
|
||||
cases = []
|
||||
for suite in report.get('suites', []):
|
||||
walk(suite, cases)
|
||||
for error in report.get('errors', []) or []:
|
||||
cases.append({'case': 'playwright-error: ' + str(error.get('message', ''))[:120], 'status': 'failed'})
|
||||
if not cases:
|
||||
print(json.dumps({'case': 'playwright-report-empty', 'status': 'failed'}))
|
||||
return 1
|
||||
for case in cases:
|
||||
print(json.dumps(case))
|
||||
return 0
|
||||
|
||||
|
||||
sys.exit(main(sys.argv))
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Runs the add-on update detection and upgrade checks and prints one JSON case
|
||||
# line per test:
|
||||
# 1. the node unit tests of the update rules, the typed client and the
|
||||
# locale keys;
|
||||
# 2. the Go specs of the checker (moving channels, immutable versions,
|
||||
# compatibility, failed checks, cache bounds, rate limit), of the
|
||||
# catalog reads it uses, of the real lifecycle manager (pinned upgrade,
|
||||
# duplicate and conflicting operations, last good generation) and of the
|
||||
# HTTP routes over the owner and two forwarding frontends;
|
||||
# 3. the Playwright spec against a UI test server that runs the real owner,
|
||||
# the real Tables executable and the real built UI package, with a
|
||||
# library whose channel the spec moves.
|
||||
# verify.sh compares the lines with the exact inventory of the updates suite
|
||||
# and rejects a skipped, flaky or missing case.
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
cache="$root/.cache/addons"
|
||||
ui="$root/core/http/react-ui"
|
||||
pkg="$root/addons/tables/ui"
|
||||
mkdir -p "$cache/tmp" "$cache/logs"
|
||||
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
|
||||
export PATH="$cache/bin:$PATH"
|
||||
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
|
||||
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
|
||||
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
|
||||
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
# The server embeds the UI build: a build made before the last change would
|
||||
# test the old host code.
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
for src in "$ui/src/components/addons/InstalledView.jsx" "$ui/src/components/addons/updateState.js" "$ui/src/components/addons/AppsView.jsx" "$ui/src/utils/addonsApi.js"; do
|
||||
[ "$ui/dist/index.html" -nt "$src" ] || { echo "the UI build is older than $src; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
for src in core/services/addons/updates/checker.go core/services/addons/updates.go core/http/endpoints/addons/updates.go tests/e2e-ui/addons_fixture.go; do
|
||||
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/$src" ] || { echo "ui-test-server is older than $src; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
|
||||
done
|
||||
[ "$root/tests/e2e/mock-backend/mock-backend" -nt "$root/tests/e2e/mock-backend/main.go" ] || { echo 'mock-backend is older than its source; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
|
||||
|
||||
# 1. Pure rules, the typed client and the locale keys.
|
||||
(cd "$ui" && node tests/unit/run.mjs updates.test.mjs)
|
||||
|
||||
# 2. Go: ginkgo specs reported as case lines. Specs that a focus filtered out
|
||||
# are not part of the run and are not printed; a failed or skipped spec is.
|
||||
go_cases() {
|
||||
python3 -I - "$1" <<'PY'
|
||||
import json, sys
|
||||
for report in json.load(open(sys.argv[1])):
|
||||
for spec in report['SpecReports']:
|
||||
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
|
||||
continue
|
||||
status = {'passed': 'passed'}.get(spec['State'], 'failed')
|
||||
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
|
||||
PY
|
||||
}
|
||||
run_go() {
|
||||
name=$1
|
||||
shift
|
||||
report="$cache/logs/updates-$name.json"
|
||||
rm -f "$report"
|
||||
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/updates-$name.log" 2>&1 || { tail -40 "$cache/logs/updates-$name.log" >&2; exit 1; }
|
||||
go_cases "$report"
|
||||
}
|
||||
run_go checker ./core/services/addons/updates
|
||||
run_go catalog ./core/services/addons/catalog -ginkgo.focus='what the update check reads'
|
||||
run_go manager ./core/services/addons -ginkgo.focus='explicit upgrade from an authoritative update check'
|
||||
run_go routes ./core/http/endpoints/addons -ginkgo.focus='update routes'
|
||||
|
||||
# 3. The real owner, the real Tables executable, the built package. The
|
||||
# fixture library has one moving channel: each directory is one generation of
|
||||
# it, with the same version label and other bytes. The last generation asks
|
||||
# for a host this one is not.
|
||||
node "$pkg/build.mjs" >"$cache/logs/updates-build.log" 2>&1 || { cat "$cache/logs/updates-build.log" >&2; exit 1; }
|
||||
tables_exe="$cache/tmp/updates-exe"
|
||||
CGO_ENABLED=1 go build -p 2 -o "$tables_exe" ./addons/tables/cmd/tables
|
||||
fixture_dir="$cache/tmp/updates-fixture"
|
||||
rm -rf "$fixture_dir"
|
||||
mkdir -p "$fixture_dir"
|
||||
for n in 1 2 3 4 5; do
|
||||
d="$fixture_dir/tables-dev$n"
|
||||
mkdir -p "$d"
|
||||
python3 -I - "$root/addons/tables/manifest.json" "$d/manifest.json" <<'PY'
|
||||
import json, sys
|
||||
m = json.load(open(sys.argv[1]))
|
||||
m['version'] = '0.1.0-dev'
|
||||
json.dump(m, open(sys.argv[2], 'w'))
|
||||
PY
|
||||
cp -R "$pkg/dist/ui" "$d/ui"
|
||||
printf 'build %s\n' "$n" >"$d/build.txt"
|
||||
printf 'development\n' >"$d/.fixture-channel"
|
||||
printf '%s\n' "$n" >"$d/.fixture-generation"
|
||||
done
|
||||
printf '{"host":{"min":9,"max":9}}\n' >"$fixture_dir/tables-dev5/.fixture-requires"
|
||||
|
||||
free_port() { python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])'; }
|
||||
named_port=$(free_port)
|
||||
creds="$cache/logs/updates-credentials.json"
|
||||
rm -f "$creds"
|
||||
shots=${UPDATES_SHOTS:-}
|
||||
trap 'kill "$named" 2>/dev/null || true; wait "$named" 2>/dev/null || true; rm -rf "$fixture_dir" "$tables_exe"' EXIT HUP INT TERM
|
||||
named=0
|
||||
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$named_port" \
|
||||
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$creds" \
|
||||
--fixture-enable=tables@development >"$cache/logs/updates-server.log" 2>&1 &
|
||||
named=$!
|
||||
i=0
|
||||
until [ -s "$creds" ] && python3 -I -c "import urllib.request;urllib.request.urlopen('http://127.0.0.1:$named_port/readyz',timeout=1)" 2>/dev/null; do
|
||||
i=$((i + 1))
|
||||
kill -0 "$named" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$cache/logs/updates-server.log" >&2; exit 1; }
|
||||
[ "$i" -lt 180 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
report="$cache/logs/updates-report.json"
|
||||
rm -f "$report"
|
||||
status=0
|
||||
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$named_port" ADDONS_FIXTURE="$creds" ADDONS_SHOTS="$shots" \
|
||||
ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.updates.config.js >"$cache/logs/updates-playwright.log" 2>&1) || status=$?
|
||||
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
|
||||
exit "$status"
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -eu
|
||||
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
cd "$root"
|
||||
[ "$#" -eq 1 ] && [ -n "$1" ] || { echo 'one nonempty suite required' >&2; exit 2; }
|
||||
exec python3 - "$1" <<'PY'
|
||||
import json, os, pathlib, subprocess, sys, tempfile, time, socket, urllib.request
|
||||
root = pathlib.Path.cwd()
|
||||
suite = sys.argv[1]
|
||||
inventory = json.loads((root / 'scripts/addons/suites.json').read_text())
|
||||
if suite not in inventory:
|
||||
sys.exit('unknown or unavailable suite: ' + suite)
|
||||
entry = inventory[suite]
|
||||
if not entry.get('commands') or not entry.get('cases'):
|
||||
sys.exit('empty command/test inventory')
|
||||
cache = root / '.cache/addons'
|
||||
(cache / 'logs').mkdir(parents=True, exist_ok=True)
|
||||
(cache / 'tmp').mkdir(exist_ok=True)
|
||||
env = dict(os.environ, GOTOOLCHAIN='local', GOPROXY='off', GOSUMDB='off', GOFLAGS='-mod=readonly',
|
||||
GOPATH=str(cache/'go'), GOMODCACHE=str(cache/'go/pkg/mod'), GOCACHE=str(cache/'go-build'),
|
||||
TMPDIR=str(cache/'tmp'), npm_config_cache=str(cache/'npm'), PLAYWRIGHT_BROWSERS_PATH=str(cache/'browsers'))
|
||||
env['PATH'] = str(cache/'bin') + os.pathsep + env['PATH']
|
||||
log = cache / 'logs' / ('verify-' + str(time.time_ns()) + '.jsonl')
|
||||
def run(command, cwd=root):
|
||||
result = subprocess.run(command, cwd=cwd, env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
with log.open('a') as f:
|
||||
f.write(json.dumps(dict(command=command, status=result.returncode, output=result.stdout))+'\n')
|
||||
print(result.stdout, end='', flush=True)
|
||||
if result.returncode:
|
||||
sys.exit(failure_summary(command, result.stdout, result.returncode))
|
||||
return result.stdout
|
||||
def failure_summary(command, output, status):
|
||||
"""Names what failed, so a flake in a long gate run can be diagnosed later.
|
||||
|
||||
It reads only: failed case lines of the child, failed specs of the Ginkgo
|
||||
reports the command names, and the last lines of the output. It never
|
||||
turns a failure into a pass."""
|
||||
names, lines = [], []
|
||||
for line in output.splitlines():
|
||||
try: item = json.loads(line)
|
||||
except ValueError: continue
|
||||
if isinstance(item, dict) and 'case' in item and item.get('status') not in (None, 'passed'):
|
||||
names.append(str(item['case']))
|
||||
lines.append('failed case: %s (%s)' % (item['case'], item['status']))
|
||||
for arg in command:
|
||||
if not arg.startswith('-ginkgo.json-report='): continue
|
||||
path = pathlib.Path(arg.split('=', 1)[1])
|
||||
path = path if path.is_absolute() else root / path
|
||||
try: reports = json.loads(path.read_text())
|
||||
except (OSError, ValueError): continue
|
||||
for report in reports if isinstance(reports, list) else []:
|
||||
for spec in report.get('SpecReports', []):
|
||||
if spec.get('State') in ('passed', 'skipped', 'pending'): continue
|
||||
message = ((spec.get('Failure') or {}).get('Message') or '').strip().splitlines()
|
||||
names.append(str(spec.get('LeafNodeText')))
|
||||
lines.append('failed spec: %s (%s)%s' % (spec.get('LeafNodeText'), spec.get('State'),
|
||||
': ' + message[0][:200] if message else ''))
|
||||
tail = [l.strip() for l in output.splitlines() if l.strip()][-5:]
|
||||
if tail: lines.append('last output: ' + ' | '.join(l[:200] for l in tail))
|
||||
lines.append('child status %d; failed: %s; log: %s' % (status, ', '.join(names[:10]) or 'unknown', log))
|
||||
return '\n'.join(lines)
|
||||
def ginkgo_report(path, expected):
|
||||
try:
|
||||
reports = json.loads(path.read_text())
|
||||
assert isinstance(reports, list) and reports
|
||||
cases = []
|
||||
for report in reports:
|
||||
assert report['SuiteSucceeded'] is True
|
||||
specs = report['SpecReports']
|
||||
assert specs
|
||||
for spec in specs:
|
||||
assert spec['State'] == 'passed'
|
||||
if spec['LeafNodeType'] == 'It': cases.append(spec['LeafNodeText'])
|
||||
assert cases and sorted(cases) == sorted(expected)
|
||||
except (OSError, ValueError, KeyError, TypeError, AssertionError):
|
||||
sys.exit('Ginkgo report missing, empty, skipped, failed or inventory mismatch: '+str(path))
|
||||
return cases
|
||||
|
||||
seen = []
|
||||
if entry['kind'] == 'ginkgo':
|
||||
reports = entry.get('reports', [dict(path=entry['report'], cases=entry['cases'])])
|
||||
if not reports: sys.exit('empty report inventory')
|
||||
for report in reports: (root/report['path']).unlink(missing_ok=True)
|
||||
for command in entry['commands']:
|
||||
if entry['kind'] == 'ginkgo' and (command[:3] != ['go', 'test', '-count=1'] or '-tags' not in command or 'auth' not in command):
|
||||
sys.exit('Ginkgo command requires go test -count=1 -tags auth')
|
||||
for line in run(command).splitlines():
|
||||
try: item = json.loads(line)
|
||||
except ValueError: continue
|
||||
if 'case' in item:
|
||||
if item.get('status') != 'passed': sys.exit('required case not passed: '+item['case'])
|
||||
seen.append(item['case'])
|
||||
if entry['kind'] == 'ginkgo':
|
||||
seen = []
|
||||
for report in reports: seen.extend(ginkgo_report(root/report['path'], report['cases']))
|
||||
if sorted(seen) != sorted(entry['cases']):
|
||||
sys.exit('required case inventory mismatch: '+repr(seen))
|
||||
# Run real readiness only after the shell test inventory has passed.
|
||||
run(['sh', 'scripts/addons/prepare.sh', '--check'])
|
||||
with tempfile.TemporaryDirectory(dir=cache/'tmp') as scratch:
|
||||
scratch = pathlib.Path(scratch)
|
||||
probe = scratch/'sqlite_test.go'
|
||||
probe.write_text('''package readiness_test
|
||||
import ("database/sql"; "testing"; _ "github.com/mattn/go-sqlite3"
|
||||
. "github.com/onsi/ginkgo/v2"; . "github.com/onsi/gomega")
|
||||
func TestReadiness(t *testing.T) { RegisterFailHandler(Fail); RunSpecs(t, "Native readiness") }
|
||||
var _ = Describe("SQLite", func() { It("sqlite transaction", func() {
|
||||
db,e:=sql.Open("sqlite3", ":memory:"); Expect(e).NotTo(HaveOccurred()); defer db.Close()
|
||||
tx,e:=db.Begin(); Expect(e).NotTo(HaveOccurred())
|
||||
_,e=tx.Exec("create table readiness(value integer); insert into readiness values (42)"); Expect(e).NotTo(HaveOccurred())
|
||||
Expect(tx.Commit()).To(Succeed()); var n int
|
||||
Expect(db.QueryRow("select value from readiness").Scan(&n)).To(Succeed()); Expect(n).To(Equal(42))
|
||||
}) })
|
||||
''')
|
||||
report = cache/'logs'/('ginkgo-'+str(time.time_ns())+'.json')
|
||||
run(['go', 'test', '-count=1', '-p', '2', '-tags', 'auth', str(probe), '-ginkgo.json-report='+str(report)])
|
||||
ginkgo_report(report, ['sqlite transaction'])
|
||||
# OS-selected isolated port, bounded health wait, private server temporary data.
|
||||
with socket.socket() as sock:
|
||||
sock.bind(('127.0.0.1', 0)); port = sock.getsockname()[1]
|
||||
env['TMPDIR'] = str(scratch)
|
||||
with (scratch/'server.log').open('w+') as output:
|
||||
command = [str(root/'tests/e2e-ui/ui-test-server'), '--mock-backend='+str(root/'tests/e2e/mock-backend/mock-backend'), '--port='+str(port)]
|
||||
server = subprocess.Popen(command, env=env, stdout=output, stderr=subprocess.STDOUT)
|
||||
try:
|
||||
url = 'http://127.0.0.1:'+str(port)
|
||||
for _ in range(120):
|
||||
if server.poll() is not None: raise RuntimeError('UI server exited '+str(server.returncode))
|
||||
try:
|
||||
with urllib.request.urlopen(url+'/readyz', timeout=1) as response:
|
||||
if response.status == 200: break
|
||||
except OSError: pass
|
||||
time.sleep(1)
|
||||
else: raise RuntimeError('UI server readiness timed out')
|
||||
# Local Playwright CLI and library, never npx or an implicit install.
|
||||
ui = root/'core/http/react-ui'
|
||||
run([str(ui/'node_modules/.bin/playwright'), '--version'])
|
||||
run(['node', '--input-type=module', '-e', '''
|
||||
import { chromium } from '@playwright/test';
|
||||
const browser = await chromium.launch({executablePath: process.env.PLAYWRIGHT_CHROMIUM_PATH || undefined});
|
||||
try { const page=await browser.newPage(); const response=await page.goto(process.argv[1]);
|
||||
if (!response.ok()) throw new Error('UI HTTP failure');
|
||||
await page.locator('#root').waitFor();
|
||||
await page.waitForFunction(()=>document.querySelector('#root')?.children.length > 0);
|
||||
console.log(JSON.stringify({case:'browser',status:'passed'}));
|
||||
} finally { await browser.close(); }
|
||||
''', url], cwd=ui)
|
||||
finally:
|
||||
server.terminate()
|
||||
try: server.wait(timeout=15)
|
||||
except subprocess.TimeoutExpired: server.kill(); server.wait()
|
||||
output.seek(0)
|
||||
with log.open('a') as f: f.write(json.dumps(dict(command=command,status=server.returncode,output=output.read()))+'\n')
|
||||
print(json.dumps(dict(suite=suite, passed=len(seen)+3, skipped=0, log=str(log))))
|
||||
PY
|
||||
Reference in new issue
Block a user