Skip to content

Commit 6b816dc

Browse files
genednaclaude
andcommitted
v0.41.4: BB-02 shared smoke runner library
Add scripts/lib/smoke-runner.sh (plan-20261001 BB-02): run_case, optin_case, skip_case, require_tools, require_case_filter_hit, print_summary, finish and redact implement the ADR-BB-04 output and exit-code contract (summary always last; 0 pass, 1 fail, 2 unmatched / missing tool / named opt-in or skipped case). Case bodies run under `set -e` outside any condition, so `false; true` fails. redact replaces the token literally and strips URL queries. scripts/lib/ smoke-runner.selftest.sh covers every exit path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bJDjze83ToyPysYzmxxWS Signed-off-by: Eli Ma <eli@patch.sh>
1 parent be5c6b9 commit 6b816dc

5 files changed

Lines changed: 203 additions & 3 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mega2"
3-
version = "0.41.3"
3+
version = "0.41.4"
44
edition = "2024"
55
license = "MIT OR Apache-2.0"
66
description = "Monorepo engine and Git hosting server for AI agents: trunk-based storage, Git Smart HTTP, Git LFS, OCI registry, and Agent Session Capture"

‎docs/plan/plan-20261001.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@
706706
### Task BB-02: 共享 smoke runner 库
707707

708708
**Task type:** `implementation`
709-
**Lifecycle / Acceptance:** `pending` / (空)
709+
**Lifecycle / Acceptance:** `in-progress` / `remote-pending`(v0.41.4;Codex R2 PASS;C 组 2023 测试通过;待 Release 与 D 组 `docker` job)
710710
**Description:** 新建 `scripts/lib/smoke-runner.sh`(`run_case`、`optin_case`、`print_summary`、`redact`、`finish`)实现 ADR-BB-04 §3/§4 的输出与退出码契约,并附自测 `scripts/lib/smoke-runner.selftest.sh`(桩 case 覆盖各退出码路径)。唯一行为轴:smoke 输出 / 退出码契约。
711711
**Out of scope:** 迁移既有脚本(ADR-BB-01,永久非目标于本计划);libra 作用域文档(BB-05);域脚本(BB-10 / BB-30 / BB-50)。
712712
**Current evidence:**
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env bash
2+
# Self-test for scripts/lib/smoke-runner.sh: exercises every exit-code path
3+
# with stub cases and prints one `selftest ok:` line per assertion.
4+
set -euo pipefail
5+
LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/smoke-runner.sh"
6+
tmp=$(mktemp -d)
7+
trap 'rm -rf "$tmp"' EXIT
8+
9+
stub() {
10+
cat > "$tmp/stub.sh" <<STUB
11+
#!/usr/bin/env bash
12+
source "$LIB"
13+
smoke_init "selftest"
14+
ok() { true; }
15+
bad() { false; }
16+
$1
17+
finish
18+
STUB
19+
}
20+
21+
expect_rc() {
22+
local want="$1" label="$2" rc=0
23+
shift 2
24+
"$@" > "$tmp/out.txt" 2>&1 || rc=$?
25+
if [ "$rc" -ne "$want" ]; then
26+
echo "selftest FAIL: $label (exit $rc, want $want)" >&2
27+
cat "$tmp/out.txt" >&2
28+
exit 1
29+
fi
30+
}
31+
32+
last_line_is() {
33+
[ "$(tail -n 1 "$tmp/out.txt")" = "$1" ] || {
34+
echo "selftest FAIL: last line is '$(tail -n 1 "$tmp/out.txt")', want '$1'" >&2
35+
exit 1
36+
}
37+
}
38+
39+
stub 'run_case "a" ok; run_case "b" ok; skip_case "s" "not here"'
40+
expect_rc 0 "all pass" env -u MEGA2_SMOKE_CASE bash "$tmp/stub.sh"
41+
last_line_is 'selftest smoke storage_only summary: 2 passed, 0 failed (1 skipped)'
42+
grep -A1 -x '==> s' "$tmp/out.txt" | grep -qx 'SKIP: s (not here)'
43+
echo "selftest ok: all PASS exits 0"
44+
45+
stub 'failing_then_ok() { false; true; }; run_case "a" ok; run_case "b" failing_then_ok'
46+
expect_rc 1 "one fail" env -u MEGA2_SMOKE_CASE bash "$tmp/stub.sh"
47+
grep -qx 'FAIL: b' "$tmp/out.txt"
48+
echo "selftest ok: any FAIL exits 1"
49+
50+
stub 'run_case "a" ok'
51+
expect_rc 2 "unmatched" env MEGA2_SMOKE_CASE="nope" bash "$tmp/stub.sh"
52+
last_line_is 'selftest smoke storage_only summary: 0 passed, 0 failed (0 skipped)'
53+
echo "selftest ok: unmatched MEGA2_SMOKE_CASE exits 2"
54+
55+
stub 'optin_case "o" SELFTEST_SWITCH ok; skip_case "s" "not here"'
56+
expect_rc 2 "opt-in named without switch" env -u SELFTEST_SWITCH MEGA2_SMOKE_CASE="o" bash "$tmp/stub.sh"
57+
last_line_is 'selftest smoke storage_only summary: 0 passed, 0 failed (0 skipped)'
58+
expect_rc 2 "skip_case named" env MEGA2_SMOKE_CASE="s" bash "$tmp/stub.sh"
59+
last_line_is 'selftest smoke storage_only summary: 0 passed, 0 failed (0 skipped)'
60+
echo "selftest ok: named opt-in without switch exits 2"
61+
62+
stub 'run_case "a" ok; run_case "b" bad; optin_case "o" SELFTEST_SWITCH ok; skip_case "s" "not here"'
63+
expect_rc 0 "filtered summary" env -u SELFTEST_SWITCH MEGA2_SMOKE_CASE="a" bash "$tmp/stub.sh"
64+
last_line_is 'selftest smoke storage_only summary: 1 passed, 0 failed (0 skipped)'
65+
echo "selftest ok: summary counts only executed cases"
66+
67+
# shellcheck source=scripts/lib/smoke-runner.sh disable=SC1091
68+
out=$(printf 'url http://x:%s@h/r?sig=abc\n' 'a+b.c' | (source "$LIB"; redact 'a+b.c'))
69+
[ "$out" = 'url http://x:***@h/r?***' ]
70+
echo "selftest ok: redact replaces the token and strips URL queries"

‎scripts/lib/smoke-runner.sh‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env bash
2+
# Shared case runner for the plan-20261001 storage-only smoke scripts
3+
# (oci_client / artifacts / libra). Contract: ADR-BB-04 §3/§4.
4+
#
5+
# source scripts/lib/smoke-runner.sh
6+
# smoke_init "oci client" # summary label
7+
# require_tools curl jq # exit 2 when a tool is missing
8+
# run_case "OCI ping" case_ping # default-stack case
9+
# optin_case "OCI x (none)" MEGA2_SMOKE_AUTH_NONE case_x
10+
# skip_case "OCI y" "reason" # registered but not runnable here
11+
# finish # summary + exit code
12+
#
13+
# Case functions run in a subshell with `set -e` in effect, so any failing
14+
# command fails the case (no `|| return 1` bookkeeping needed).
15+
#
16+
# Output: `==> <case>`, then `PASS: <case>` / `FAIL: <case>` /
17+
# `SKIP: <case> (<reason>)`; last line
18+
# `<label> smoke storage_only summary: <N> passed, <M> failed (<K> skipped)`.
19+
# Exit: 0 = no failure (and, when MEGA2_SMOKE_CASE is set, that case passed);
20+
# 1 = any failure; 2 = unmatched MEGA2_SMOKE_CASE, missing tool, or a named
21+
# opt-in case whose switch is not set. Cases filtered out by MEGA2_SMOKE_CASE
22+
# are not counted.
23+
24+
SMOKE_LABEL=""
25+
SMOKE_CASE_FILTER="${MEGA2_SMOKE_CASE:-}"
26+
SMOKE_CASE_HIT=0
27+
SMOKE_PASS=0
28+
SMOKE_FAIL=0
29+
SMOKE_SKIP=0
30+
31+
smoke_init() {
32+
SMOKE_LABEL="$1"
33+
}
34+
35+
# smoke_die <message>: exit 2; the summary stays the last line of output.
36+
smoke_die() {
37+
echo "ERROR: $*" >&2
38+
print_summary
39+
exit 2
40+
}
41+
42+
require_tools() {
43+
local tool
44+
for tool in "$@"; do
45+
command -v "$tool" >/dev/null 2>&1 || smoke_die "required tool not found: $tool"
46+
done
47+
}
48+
49+
# smoke_selected <name>: true when the case should be considered at all.
50+
smoke_selected() {
51+
[ -z "$SMOKE_CASE_FILTER" ] || [ "$SMOKE_CASE_FILTER" = "$1" ]
52+
}
53+
54+
run_case() {
55+
local name="$1" fn="$2" rc=0 had_errexit=0
56+
smoke_selected "$name" || return 0
57+
SMOKE_CASE_HIT=1
58+
echo "==> $name"
59+
# Not inside an `if`/`||` condition: that would disable `set -e` in the
60+
# case body and let `false; true` pass.
61+
case $- in *e*) had_errexit=1 ;; esac
62+
set +e
63+
( set -e; "$fn" )
64+
rc=$?
65+
if [ "$had_errexit" -eq 1 ]; then set -e; fi
66+
if [ "$rc" -eq 0 ]; then
67+
SMOKE_PASS=$((SMOKE_PASS + 1))
68+
echo "PASS: $name"
69+
else
70+
SMOKE_FAIL=$((SMOKE_FAIL + 1))
71+
echo "FAIL: $name" >&2
72+
fi
73+
}
74+
75+
# skip_case <name> <reason>: a registered case that cannot run in this
76+
# environment. Counted as skipped; naming it in MEGA2_SMOKE_CASE exits 2.
77+
skip_case() {
78+
local name="$1" reason="$2"
79+
smoke_selected "$name" || return 0
80+
SMOKE_CASE_HIT=1
81+
if [ -n "$SMOKE_CASE_FILTER" ]; then
82+
smoke_die "case '$name' requested but cannot run: $reason"
83+
fi
84+
SMOKE_SKIP=$((SMOKE_SKIP + 1))
85+
echo "==> $name"
86+
echo "SKIP: $name ($reason)"
87+
}
88+
89+
# optin_case <name> <switch-var> <fn>: runs only when ${switch-var}=1.
90+
optin_case() {
91+
local name="$1" switch="$2" fn="$3"
92+
smoke_selected "$name" || return 0
93+
SMOKE_CASE_HIT=1
94+
if [ "${!switch:-0}" != "1" ]; then
95+
if [ -n "$SMOKE_CASE_FILTER" ]; then
96+
smoke_die "opt-in case '$name' requested but $switch=1 is not set"
97+
fi
98+
SMOKE_SKIP=$((SMOKE_SKIP + 1))
99+
echo "==> $name"
100+
echo "SKIP: $name ($switch not set)"
101+
return 0
102+
fi
103+
run_case "$name" "$fn"
104+
}
105+
106+
print_summary() {
107+
echo "$SMOKE_LABEL smoke storage_only summary: $SMOKE_PASS passed, $SMOKE_FAIL failed ($SMOKE_SKIP skipped)"
108+
}
109+
110+
# require_case_filter_hit: exit 2 when MEGA2_SMOKE_CASE matched no case.
111+
require_case_filter_hit() {
112+
if [ -n "$SMOKE_CASE_FILTER" ] && [ "$SMOKE_CASE_HIT" -eq 0 ]; then
113+
smoke_die "MEGA2_SMOKE_CASE did not match any case: $SMOKE_CASE_FILTER"
114+
fi
115+
}
116+
117+
finish() {
118+
require_case_filter_hit
119+
print_summary
120+
if [ "$SMOKE_FAIL" -gt 0 ]; then
121+
exit 1
122+
fi
123+
exit 0
124+
}
125+
126+
# redact <token>: stdin -> stdout with the literal token replaced by *** and
127+
# every URL query removed (signed URLs carry credentials in the query).
128+
redact() {
129+
TOK="${1:-}" perl -pe 'BEGIN{$t=$ENV{TOK}} s/\Q$t\E/***/g if length $t; s{(https?://[^\s"?]+)\?[^\s"]*}{$1?***}g'
130+
}

0 commit comments

Comments
 (0)