ci-speed
Squashed from ci-speed into main.
6 files changed,
+345
−14
.amadan/ci.d/10-gate+14 −9.amadan/test-gate.sh+254 −0.amadan/test-weights.sh+37 −0.amadan/test-weights.tsv+28 −0AGENTS.md+7 −1Makefile+5 −4
CI passed — run details
CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok amadan.net/carlos/platform.git/launchctx 0.016s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
=== vet (1 s)
=== gofmt (1 s)
gofmt -l .: clean
=== rest (252 s)
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.663s
ok github.com/carlosframework/platform/cmd/moveprobe 93.733s
ok github.com/carlosframework/platform/cmd/zddprobe 0.707s
ok github.com/carlosframework/platform/internal/activator 249.688s
ok github.com/carlosframework/platform/internal/agentsock 0.029s
ok github.com/carlosframework/platform/internal/alerting 0.005s
ok github.com/carlosframework/platform/internal/analytics 0.032s
ok github.com/carlosframework/platform/internal/appconfig 2.862s
ok github.com/carlosframework/platform/internal/apperr 0.143s
ok github.com/carlosframework/platform/internal/applog 0.235s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.008s
ok github.com/carlosframework/platform/internal/boxid 0.681s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.010s
ok github.com/carlosframework/platform/internal/caps 0.032s
ok github.com/carlosframework/platform/internal/certwarm 0.012s
ok github.com/carlosframework/platform/internal/cgroup 0.745s
ok github.com/carlosframework/platform/internal/changelog 0.478s
ok github.com/carlosframework/platform/internal/claimactivation 1.172s
ok github.com/carlosframework/platform/internal/clamd 0.066s
? github.com/carlosframework/platform/internal/cliflag [no test files]
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 5.775s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.014s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.004s
ok github.com/carlosframework/platform/internal/docsite 0.006s
ok github.com/carlosframework/platform/internal/domains 2.833s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 45.245s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.012s
ok github.com/carlosframework/platform/internal/fleetapi 0.193s
ok github.com/carlosframework/platform/internal/gate 1.932s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.038s
ok github.com/carlosframework/platform/internal/geoip 0.054s
ok github.com/carlosframework/platform/internal/historyreadback 0.277s
ok github.com/carlosframework/platform/internal/host 193.347s
ok github.com/carlosframework/platform/internal/hostactors 6.059s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.010s
ok github.com/carlosframework/platform/internal/hostbox 3.070s
ok github.com/carlosframework/platform/internal/hostclaim 0.788s
ok github.com/carlosframework/platform/internal/hostenv 0.007s
ok github.com/carlosframework/platform/internal/hostevidence 0.010s
ok github.com/carlosframework/platform/internal/hostexec 5.724s
ok github.com/carlosframework/platform/internal/hostfence 0.049s
ok github.com/carlosframework/platform/internal/hostfiles 10.324s
ok github.com/carlosframework/platform/internal/hostfloor 0.021s
ok github.com/carlosframework/platform/internal/hostguard 1.088s
ok github.com/carlosframework/platform/internal/hostops 2.103s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.008s
ok github.com/carlosframework/platform/internal/hostquarantine 0.008s
ok github.com/carlosframework/platform/internal/hostrelocate 0.653s
ok github.com/carlosframework/platform/internal/hostresolve 0.304s
ok github.com/carlosframework/platform/internal/hostwrite 0.004s
ok github.com/carlosframework/platform/internal/infraview 0.004s
ok github.com/carlosframework/platform/internal/instancectl 5.252s
ok github.com/carlosframework/platform/internal/instanceexec 2.857s
ok github.com/carlosframework/platform/internal/instancefs 0.011s
ok github.com/carlosframework/platform/internal/instancekey 0.004s
ok github.com/carlosframework/platform/internal/instanceupdates 0.017s
ok github.com/carlosframework/platform/internal/layout 1.954s
ok github.com/carlosframework/platform/internal/lbhealth 0.004s
ok github.com/carlosframework/platform/internal/lease 10.108s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.633s
ok github.com/carlosframework/platform/internal/ledger 0.035s
ok github.com/carlosframework/platform/internal/mail 0.238s
ok github.com/carlosframework/platform/internal/maintenance 0.089s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.607s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.095s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 21.423s
ok github.com/carlosframework/platform/internal/moves/archive 1.097s
ok github.com/carlosframework/platform/internal/objstore 0.329s
ok github.com/carlosframework/platform/internal/ops 0.237s
ok github.com/carlosframework/platform/internal/park 0.303s
ok github.com/carlosframework/platform/internal/plans 0.006s
ok github.com/carlosframework/platform/internal/privatefile 0.006s
ok github.com/carlosframework/platform/internal/provision 19.362s
ok github.com/carlosframework/platform/internal/publicrelease 0.013s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.666s
ok github.com/carlosframework/platform/internal/relay 7.245s
ok github.com/carlosframework/platform/internal/release 25.369s
ok github.com/carlosframework/platform/internal/rollout 3.872s
ok github.com/carlosframework/platform/internal/rolloutwords 0.006s
ok github.com/carlosframework/platform/internal/rootaws 0.008s
ok github.com/carlosframework/platform/internal/rootparticipation 0.060s
ok github.com/carlosframework/platform/internal/rootstate 0.027s
ok github.com/carlosframework/platform/internal/runs 0.569s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.011s
ok github.com/carlosframework/platform/internal/schedule 2.724s
ok github.com/carlosframework/platform/internal/seeds 0.013s
ok github.com/carlosframework/platform/internal/sim/hostlife 11.116s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.771s
ok github.com/carlosframework/platform/internal/status 0.098s
ok github.com/carlosframework/platform/internal/steering 0.010s
ok github.com/carlosframework/platform/internal/stopevidence 0.166s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.008s
ok github.com/carlosframework/platform/internal/termfence 0.006s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.003s
ok github.com/carlosframework/platform/internal/updatewords 0.005s
ok github.com/carlosframework/platform/internal/usage 0.006s
ok github.com/carlosframework/platform/internal/version 0.003s
ok github.com/carlosframework/platform/internal/vet 1.622s
=== internal_console@0 (209 s)
ok github.com/carlosframework/platform/internal/console 206.813s
=== internal_console@1 (34 s)
ok github.com/carlosframework/platform/internal/console 32.469s
=== internal_console@2 (34 s)
ok github.com/carlosframework/platform/internal/console 31.959s
=== internal_console@3 (36 s)
ok github.com/carlosframework/platform/internal/console 33.924s
=== internal_console@4 (36 s)
ok github.com/carlosframework/platform/internal/console 34.264s
=== internal_console@5 (37 s)
ok github.com/carlosframework/platform/internal/console 34.460s
=== internal_console@6 (38 s)
ok github.com/carlosframework/platform/internal/console 35.924s
=== internal_console@7 (39 s)
ok github.com/carlosframework/platform/internal/console 36.602s
=== root@0 (33 s)
ok github.com/carlosframework/platform 30.527s
=== root@1 (44 s)
ok github.com/carlosframework/platform 42.333s
=== root@2 (44 s)
ok github.com/carlosframework/platform 42.242s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 49873058 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.024s
=== RUN TestT4StorageProbeMinio
2026/10/21 14:01:57 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0904E278458D4, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.023s
minio contract: PASS
diff --git a/.amadan/ci.d/10-gate b/.amadan/ci.d/10-gate| index 434170661..daf132329 100755 |
| --- a/.amadan/ci.d/10-gate |
| +++ b/.amadan/ci.d/10-gate |
| @@ -36,16 +36,21 @@ |
| # same target a maintainer runs on a laptop — so CI and the local gate |
| # cannot drift apart, and a check added to one is a check added to both. |
| # That is how the gofmt check gets here at all: `make test` runs vet, a |
| -# repo-wide `gofmt -l .`, then the tests, and this file no longer restates |
| -# any of it. |
| +# repo-wide `gofmt -l .` and the tests (side by side, through |
| +# .amadan/test-gate.sh), and this file no longer restates any of it. |
| # |
| -# `go build ./...` stays spelled out because `make test` does not do it — |
| -# the Makefile's test target starts at vet. |
| +# `go build ./...` stays spelled out because `make test` does not do it. |
| # |
| -# -count=1 and -timeout 30m moved to the Makefile's go test line along with |
| -# the reasoning for them; they are still in force here, just no longer |
| -# written down twice. |
| +# -count=1 and -timeout 30m live in .amadan/test-gate.sh, on each go test |
| +# line, with the Makefile's note on why; they are still in force here, |
| +# just not written down twice. |
| set -eu |
| -go build ./... |
| -exec make test |
| +# The build runs beside the tests rather than before them: neither needs |
| +# the other, and the gate's wall clock is whichever finishes last. |
| +go build ./... & |
| +build=$! |
| +status=0 |
| +make test || status=$? |
| +wait "$build" || status=1 |
| +exit "$status" |
diff --git a/.amadan/test-gate.sh b/.amadan/test-gate.sh| new file mode 100755 |
| index 000000000..f1f02c9cd |
| --- /dev/null |
| +++ b/.amadan/test-gate.sh |
| @@ -0,0 +1,254 @@ |
| +#!/bin/sh |
| +# The test half of `make test`, and so of CI: `.amadan/ci.d/10-gate` runs |
| +# `make test`, which runs this. Still one definition. |
| +# |
| +# What it checks is what `make test` always checked: launchctx's vet and |
| +# tests, `go vet ./...`, a repo-wide `gofmt -l .`, and every test in every |
| +# package with -count=1. What changed is the shape of the run. |
| +# |
| +# The gate's wall clock was the slowest single package, because Go runs |
| +# packages side by side but a package's own tests one after another. |
| +# internal/console took about 440 s on CI's 32 cores while most of them |
| +# sat idle, and the root package was not far behind. So: |
| +# |
| +# - vet, gofmt, launchctx and the tests all start at once instead of |
| +# vet finishing before the first test starts; |
| +# - the packages named in TEST_SPLIT have their top-level tests dealt |
| +# across several `go test -run` processes running concurrently. |
| +# |
| +# The deal is by measured weight (.amadan/test-weights.tsv, longest first |
| +# onto the least-loaded shard), so one slow test gets a shard to itself |
| +# rather than dragging its neighbours along. A test missing from the |
| +# weights file still runs; it is only dealt as if it were short. Stale |
| +# weights cost balance, never coverage. |
| +# |
| +# THE FENCE. Each split package's test list comes from `go test -list`, |
| +# every name on it goes to exactly one shard, and a shard whose -run |
| +# pattern matches nothing fails the gate. A test can therefore be added, |
| +# renamed or removed without touching this file, and no edit to the |
| +# deal can drop one silently. |
| +# |
| +# Shards are separate processes, so tests that leaned on running in the |
| +# same process as some other test (package state one test leaves for the |
| +# next) can surface here. That is a bug in the test, not in the split. |
| +set -eu |
| + |
| +: "${TEST_SPLIT=./internal/console=8 .=3}" |
| +weights=$(pwd)/.amadan/test-weights.tsv |
| + |
| +# A shard count that is not a whole number of at least 1 would take a |
| +# package out of the ordinary run and then launch none of its shards: a |
| +# green gate that ran none of that package's tests. Refuse it up front. |
| +for spec in $TEST_SPLIT; do |
| + count=${spec#*=} |
| + case $spec:$count in |
| + *=*:[0-9]*) ;; |
| + *) |
| + echo "TEST_SPLIT entry '$spec' is not dir=count" >&2 |
| + exit 2 |
| + ;; |
| + esac |
| + case $count in |
| + *[!0-9]*) |
| + echo "TEST_SPLIT entry '$spec' needs a whole number of shards" >&2 |
| + exit 2 |
| + ;; |
| + esac |
| + if [ "$count" -lt 1 ]; then |
| + echo "TEST_SPLIT entry '$spec' needs at least one shard" >&2 |
| + exit 2 |
| + fi |
| +done |
| + |
| +# Stopping the gate stops its jobs: the go test processes and the test |
| +# binaries under them, not just the shells that started them. That covers |
| +# an interrupt, a TERM from the runner, and an early exit while jobs are |
| +# already running. Process groups (set -m) would be simpler, but dash |
| +# turns job control off without a terminal, which is how CI runs it, so |
| +# each job's tree is walked with pgrep -P instead, and the gate refuses |
| +# to start without it rather than leave orphans when it is stopped. |
| +if ! command -v pgrep >/dev/null 2>&1; then |
| + echo "pgrep is required: without it a stopped gate cannot stop its jobs" >&2 |
| + exit 2 |
| +fi |
| +out=$(mktemp -d "${TMPDIR:-/tmp}/carlos-gate-XXXXXX") |
| +# started holds name:pid for every job started; discovery is the one |
| +# foreground command in flight, if any. A job is only signalled while its |
| +# .secs file (the last thing it writes) is absent, so a finished job's |
| +# pid, which the system may since have handed to someone else's process |
| +# on this shared machine, is never touched. |
| +started="" |
| +discovery="" |
| +kill_tree() { |
| + for child in $(pgrep -P "$1" 2>/dev/null); do |
| + kill_tree "$child" |
| + done |
| + kill -TERM "$1" 2>/dev/null || true |
| +} |
| +stop_jobs() { |
| + if [ -n "$discovery" ]; then |
| + kill_tree "$discovery" |
| + fi |
| + for job in $started; do |
| + if [ ! -e "$out/${job%:*}.secs" ]; then |
| + kill_tree "${job##*:}" |
| + fi |
| + done |
| + wait 2>/dev/null || true |
| + rm -rf "$out" |
| +} |
| +trap stop_jobs EXIT |
| +trap 'trap - EXIT; stop_jobs; exit 130' INT TERM |
| + |
| +names="" |
| +# A failure found before any test runs (a package that will not list, a |
| +# deal that lost a test) is reported as a job of its own, so the jobs |
| +# already running still finish and print rather than being cut off. |
| +fail_job() { |
| + names="$names $1" |
| + printf '%s\n' "$2" >>"$out/$1.log" |
| + echo 1 >"$out/$1.rc" |
| + echo 0 >"$out/$1.secs" |
| +} |
| +start() { |
| + start_name=$1 |
| + shift |
| + names="$names $start_name" |
| + ( |
| + set +e |
| + t0=$(date +%s) |
| + "$@" >"$out/$start_name.log" 2>&1 |
| + echo $? >"$out/$start_name.rc" |
| + echo $(($(date +%s) - t0)) >"$out/$start_name.secs" |
| + ) & |
| + started="$started $start_name:$!" |
| +} |
| + |
| +# Runs one discovery command in the background and waits for it, so an |
| +# interrupt reaches the trap at once instead of after a compile. |
| +discover() { |
| + "$@" & |
| + discovery=$! |
| + discover_rc=0 |
| + wait "$discovery" || discover_rc=$? |
| + discovery="" |
| + return "$discover_rc" |
| +} |
| + |
| +start launchctx sh -c 'cd launchctx && go vet ./... && go test ./... -count=1' |
| +start vet go vet ./... |
| +start gofmt sh -c 'fmt_out=$(gofmt -l .); if [ -n "$fmt_out" ]; then echo "gofmt needed:"; echo "$fmt_out"; exit 1; fi; echo "gofmt -l .: clean"' |
| + |
| +# Everything not split runs as one ordinary `go test`. Packages are named |
| +# by import path, so match TEST_SPLIT's directories through `go list`. |
| +split_paths="" |
| +for spec in $TEST_SPLIT; do |
| + discover go list "${spec%=*}" >"$out/go-list" |
| + split_paths="$split_paths $(cat "$out/go-list")" |
| +done |
| +# go list runs on its own, not in a pipeline: a pipeline's status is its |
| +# last command's, and a go list that failed halfway would hand on a short |
| +# package list with a zero status. Each go list goes through discover, and |
| +# set -e stops the gate on its status. |
| +discover go list ./... >"$out/go-list" |
| +all=$(cat "$out/go-list") |
| +if [ -z "$all" ]; then |
| + echo "go list ./... found no packages" >&2 |
| + exit 1 |
| +fi |
| +rest="" |
| +for p in $all; do |
| + case " $split_paths " in |
| + *" $p "*) ;; |
| + *) rest="$rest $p" ;; |
| + esac |
| +done |
| +# With no packages left, go test would test the current directory instead. |
| +if [ -n "$rest" ]; then |
| + # shellcheck disable=SC2086 # one word per package, on purpose |
| + start rest go test -count=1 -timeout 30m $rest |
| +fi |
| + |
| +for spec in $TEST_SPLIT; do |
| + dir=${spec%=*} |
| + shards=${spec#*=} |
| + discover go list "$dir" >"$out/go-list" |
| + pkg=$(cat "$out/go-list") |
| + tag=$(echo "$dir" | sed 's|^\./||; s|^\.$|root|; s|/|_|g') |
| + |
| + # -list compiles the test binary, which the shards below then reuse |
| + # from the build cache. Benchmarks are listed but never run by a plain |
| + # `go test`, so they are left out of the deal. |
| + if ! discover go test -count=1 -list '.*' "$dir" >"$out/list-$tag.raw" 2>&1; then |
| + cp "$out/list-$tag.raw" "$out/$tag.log" |
| + fail_job "$tag" "could not list the tests in $pkg" |
| + continue |
| + fi |
| + grep -E '^(Test|Example|Fuzz)' "$out/list-$tag.raw" | sort -u >"$out/list-$tag" || true |
| + listed=$(wc -l <"$out/list-$tag") |
| + if [ "$listed" -eq 0 ]; then |
| + fail_job "$tag" "go test -list found no tests in $pkg, which is not believable" |
| + continue |
| + fi |
| + |
| + # Longest-processing-time first: heaviest test onto the least-loaded |
| + # shard. Unweighted tests count as 0.2 s. |
| + awk -v pkg="$pkg" -v n="$shards" -F '\t' ' |
| + FILENAME == ARGV[1] { if ($1 == pkg) w[$2] = $3; next } |
| + { print (($0 in w) ? w[$0] : 0.2) "\t" $0 } |
| + ' "$weights" "$out/list-$tag" | sort -t "$(printf '\t')" -k1,1gr -k2,2 | |
| + awk -v n="$shards" -F '\t' ' |
| + { |
| + best = 0 |
| + for (i = 1; i < n; i++) if (load[i] < load[best]) best = i |
| + load[best] += $1 |
| + print best "\t" $2 |
| + } |
| + ' >"$out/deal-$tag" |
| + |
| + dealt=$(wc -l <"$out/deal-$tag") |
| + if [ "$dealt" -ne "$listed" ]; then |
| + fail_job "$tag" "dealt $dealt of $listed tests in $pkg" |
| + continue |
| + fi |
| + |
| + i=0 |
| + launched=0 |
| + while [ "$i" -lt "$shards" ]; do |
| + pattern=$(awk -v i="$i" -F '\t' '$1 == i { printf "%s%s", sep, $2; sep = "|" }' "$out/deal-$tag") |
| + if [ -n "$pattern" ]; then |
| + start "$tag@$i" go test -count=1 -timeout 30m -run "^($pattern)\$" "$dir" |
| + launched=$((launched + $(awk -v i="$i" -F '\t' '$1 == i' "$out/deal-$tag" | wc -l))) |
| + fi |
| + i=$((i + 1)) |
| + done |
| + if [ "$launched" -ne "$listed" ]; then |
| + fail_job "$tag-deal" "launched $launched of $listed tests in $pkg" |
| + fi |
| +done |
| + |
| +wait |
| +started="" |
| + |
| +failed="" |
| +for name in $names; do |
| + # A job with no status file was killed before it could write one. |
| + rc=$(cat "$out/$name.rc" 2>/dev/null || echo 1) |
| + echo "=== $name ($(cat "$out/$name.secs" 2>/dev/null || echo '?') s)" |
| + cat "$out/$name.log" 2>/dev/null || true |
| + case $name in |
| + *@*) |
| + if grep -q 'no tests to run' "$out/$name.log"; then |
| + echo "--- $name matched no tests; its -run pattern has drifted from the list" |
| + rc=1 |
| + fi |
| + ;; |
| + esac |
| + [ "$rc" -eq 0 ] || failed="$failed $name" |
| +done |
| + |
| +if [ -n "$failed" ]; then |
| + echo |
| + echo "FAILED:$failed" |
| + exit 1 |
| +fi |
diff --git a/.amadan/test-weights.sh b/.amadan/test-weights.sh| new file mode 100755 |
| index 000000000..651e4a271 |
| --- /dev/null |
| +++ b/.amadan/test-weights.sh |
| @@ -0,0 +1,37 @@ |
| +#!/bin/sh |
| +# Rewrites .amadan/test-weights.tsv, the measured weights .amadan/test-gate.sh |
| +# deals the split packages' tests by. |
| +# |
| +# go test -count=1 -json ./internal/console/ >console.json |
| +# go test -count=1 -json . >root.json |
| +# .amadan/test-weights.sh console.json root.json |
| +# |
| +# Measure on a quiet machine with the temp root on tmpfs, as CI has it, or |
| +# the fsync-heavy tests weigh several times what they do there. Only |
| +# top-level tests of a second or more are kept; everything else is dealt |
| +# as short, which is what it is. |
| +set -eu |
| + |
| +[ $# -gt 0 ] || { |
| + echo "usage: .amadan/test-weights.sh go-test-json-file..." >&2 |
| + exit 2 |
| +} |
| + |
| +out=.amadan/test-weights.tsv |
| +{ |
| + printf '# package\ttest\tseconds (.amadan/test-weights.sh, %s)\n' "$(date -u +%Y-%m-%d)" |
| + cat "$@" | awk ' |
| + /"Action":"(pass|fail)"/ && /"Test":"/ { |
| + if (!match($0, /"Test":"[^"]*"/)) next |
| + test = substr($0, RSTART + 8, RLENGTH - 9) |
| + if (index(test, "/")) next |
| + if (!match($0, /"Package":"[^"]*"/)) next |
| + pkg = substr($0, RSTART + 11, RLENGTH - 12) |
| + if (!match($0, /"Elapsed":[0-9.]+/)) next |
| + secs = substr($0, RSTART + 10, RLENGTH - 10) + 0 |
| + if (secs >= 1) printf "%s\t%s\t%.1f\n", pkg, test, secs |
| + } |
| + ' | sort -t "$(printf '\t')" -k1,1 -k3,3gr |
| +} >"$out.tmp" |
| +mv "$out.tmp" "$out" |
| +echo "wrote $out: $(($(wc -l <"$out") - 1)) weighted tests" |
diff --git a/.amadan/test-weights.tsv b/.amadan/test-weights.tsv| new file mode 100644 |
| index 000000000..d994e6ff6 |
| --- /dev/null |
| +++ b/.amadan/test-weights.tsv |
| @@ -0,0 +1,28 @@ |
| +# package test seconds (.amadan/test-weights.sh, 2026-10-21) |
| +github.com/carlosframework/platform/internal/console TestClaimConsoleMutationInvariant 199.7 |
| +github.com/carlosframework/platform/internal/console TestRetiringAnInstancePurgesItsMoveLogRows 9.3 |
| +github.com/carlosframework/platform/internal/console TestDeclaredLostIsLoggedByTheConsole 7.2 |
| +github.com/carlosframework/platform/internal/console TestClaimConsoleMutationInvariantWitnesses 6.6 |
| +github.com/carlosframework/platform/internal/console TestClaimMailPushCountsOutboundEffects 6.4 |
| +github.com/carlosframework/platform/internal/console TestEconomicsPassWritesCensusOnce 5.0 |
| +github.com/carlosframework/platform/internal/console TestCallbackPostsToSelectedHost 3.8 |
| +github.com/carlosframework/platform/internal/console TestClaimDeleteRetryWhileParkingIsNotDone 3.2 |
| +github.com/carlosframework/platform/internal/console TestClaimProbeRunnerDetectsAHollowRequest 3.2 |
| +github.com/carlosframework/platform/internal/console TestClaimPurgeMailRecordRefusesBeforeEffects 2.5 |
| +github.com/carlosframework/platform/internal/console TestDestroyedLocalOnlyLogsNoMoveEvent 2.4 |
| +github.com/carlosframework/platform/internal/console TestSystemOperatorRollsSignedReleases 2.3 |
| +github.com/carlosframework/platform/internal/console TestSystemDoorsRefuseAPreLayoutMoveWhileABoxIsNested 2.2 |
| +github.com/carlosframework/platform/internal/console TestSystemStatusAndRollback 2.2 |
| +github.com/carlosframework/platform/internal/console TestTodaysFleetWithNoLayoutFieldKeepsRollbackAndUpdate 2.2 |
| +github.com/carlosframework/platform/internal/console TestPublishSurvivesDatabasesThatHangOrPanicTheParser 2.1 |
| +github.com/carlosframework/platform/internal/console TestSelectMoveBoxesComposesTheCreateDoorsInputs 1.7 |
| +github.com/carlosframework/platform/internal/console TestClaimPurgeStoreInventoryRefusesBeforeEffects 1.6 |
| +github.com/carlosframework/platform/internal/console TestJ4HostDoorRoutesOnlyMoveKinds 1.2 |
| +github.com/carlosframework/platform/internal/console TestJ4MoveDoorInspectsThenDeclares 1.2 |
| +github.com/carlosframework/platform/internal/console TestJ4MoveDoorStaleSubmission 1.2 |
| +github.com/carlosframework/platform/internal/console TestAPIPromoteRetryResignsThePointer 1.1 |
| +github.com/carlosframework/platform/internal/console TestMoveDoorRefusesAnIneligibleSource 1.1 |
| +github.com/carlosframework/platform/internal/console TestPlan9RollbackCASConflictIsAConflictAndNotRetried 1.1 |
| +github.com/carlosframework/platform/internal/console TestAnIncompletePlacementReadIsNotNoRoom 1.0 |
| +github.com/carlosframework/platform/internal/console TestClaimLossPagesScanProgressSurvivesRestartAndWraps 1.0 |
| +github.com/carlosframework/platform/internal/console TestCreateDoorsRefuseWithNoRoomBeforeRegistering 1.0 |
diff --git a/AGENTS.md b/AGENTS.md| index 8b81caa89..f3637c3d5 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -190,9 +190,15 @@ anything. |
| that used to run this gate was deleted on 2026-08-24, once the amadan |
| steps covered everything it did and more; `.amadan/README.md` records |
| what each step descends from, and `.amadan/ci.d/10-gate`'s header |
| - records why the switch was safe. `10-gate` is `go build ./...` then |
| + records why the switch was safe. `10-gate` is `go build ./...` beside |
| `make test`, so CI and what you run locally are one definition rather |
| than two that can drift. |
| +- **`make test` splits the slowest packages' tests across processes** |
| + (`.amadan/test-gate.sh`, `TEST_SPLIT`). Each shard is its own process, |
| + so a test must not depend on another top-level test having run first |
| + in the same process. Shards are dealt by `.amadan/test-weights.tsv`; |
| + refresh it when the gate's shard times in the CI log drift far apart |
| + (its header says how). A test missing from it still runs. |
| - **CI is triggered by a push to amadan, not by GitHub.** Nothing on |
| GitHub runs a build. A branch that exists only on GitHub has been gated |
| by exactly nothing. Read a branch's verdict with |
diff --git a/Makefile b/Makefile| index 8228319ee..fd9a7f63f 100644 |
| --- a/Makefile |
| +++ b/Makefile |
| @@ -11,11 +11,12 @@ GOARCH ?= |
| # -count=1 is CLAUDE.md's rule: never satisfy the gate from a cached pass. |
| # The 30m timeout is above Go's 10m default because the root package's e2e |
| # tests compile the carlos binary many times over. |
| +# |
| +# .amadan/test-gate.sh runs launchctx, vet, gofmt and the tests at once, |
| +# and splits the slowest packages' tests across processes. TEST_SPLIT |
| +# overrides which packages and how many ways (TEST_SPLIT= runs none split). |
| test: |
| - cd launchctx && go vet ./... && go test ./... -count=1 |
| - go vet ./... |
| - @fmt_out=$$(gofmt -l .); if [ -n "$$fmt_out" ]; then echo "gofmt needed:"; echo "$$fmt_out"; exit 1; fi; echo "gofmt -l .: clean" |
| - go test ./... -count=1 -timeout 30m |
| + .amadan/test-gate.sh |
| build: |
| CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags "$(LDFLAGS)" -o carlos . |