2f119a3fb7
write_cumulative() now strips any "-- \n<MAJOR>.<MINOR>(.<PATCH>)?\n" sentinel
from each input patch and emits a single canonical separator between, but not
after, concatenated patches. Source patches in patches/<scope>/ can therefore
keep their original git format-patch shape regardless of their position in
fleet/<host>.yaml — the brittle "trailer flip-flop on include reorder" mode
from PR #28 (commits 84734ba ↔ ceec602) is gone.
Tests:
- new unit covers strip_trailer + write_cumulative shape with mixed
trailer states + asserts no orphan trailer leaks at EOF
- fresnel parity b2sum re-recorded after the shape change
(4d9d93c6... -> 9c21751c...) — the cumulative is byte-identical
modulo per-patch trailer normalisation; git apply --check on the
v7.0 baseline still passes
- existing series-dir, bad-include, missing-patch, duplicate-include
rejections unchanged
182 lines
6.7 KiB
Bash
Executable File
182 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ka-promote test suite.
|
|
#
|
|
# Each test runs ka-promote against a fixture from fixtures/ in a temporary
|
|
# sandboxed kernel-agent tree (bin/, fleet/, patches/ — patches/ symlinked
|
|
# from the real repo so we exercise the real scope-tagged patch files).
|
|
#
|
|
# Exit 0 iff every test passes. Non-zero on first failure.
|
|
|
|
set -euo pipefail
|
|
|
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
fixtures="${repo_root}/tests/ka-promote/fixtures"
|
|
|
|
# Phase-3 ground truth — re-recorded 2026-05-19 after issue #31 fix
|
|
# (write_cumulative now strips per-input trailers + emits canonical
|
|
# separators between, but not after, concatenated patches).
|
|
FRESNEL_EXPECTED_B2SUM=9c21751cc48ab57cdf48058cc4309752de169c567bbb898c342ff3e4a5cc79add53e3fd4217c2ae2ae7c16b0f19518cf1791907367e1ea9ef16458e1e90c05e0
|
|
|
|
pass=0
|
|
fail=0
|
|
results=()
|
|
|
|
note() { printf ' %s\n' "$*"; }
|
|
ok() { results+=("PASS $1"); pass=$((pass+1)); note "PASS"; }
|
|
ko() { results+=("FAIL $1: $2"); fail=$((fail+1)); note "FAIL: $2"; }
|
|
|
|
make_sandbox() {
|
|
# $1 = fixture yaml path. Builds a scratch tree with bin/+fleet/+patches/
|
|
# and copies the fixture into fleet/<host>.yaml (extracting host from fixture).
|
|
local fixture="$1"
|
|
local scratch
|
|
scratch=$(mktemp -d -t ka-promote-test.XXXXXX)
|
|
mkdir -p "$scratch/bin" "$scratch/fleet"
|
|
cp "$repo_root/bin/ka-promote" "$scratch/bin/ka-promote"
|
|
ln -s "$repo_root/patches" "$scratch/patches"
|
|
local host
|
|
host=$(python3 -c "import yaml,sys; print(yaml.safe_load(open(sys.argv[1]))['host'])" "$fixture")
|
|
cp "$fixture" "$scratch/fleet/${host}.yaml"
|
|
echo "$scratch|$host"
|
|
}
|
|
|
|
run_test() {
|
|
local name="$1"
|
|
local fixture="$2"
|
|
local expected_exit="$3"
|
|
local check_fn="${4:-}"
|
|
|
|
echo "::: $name"
|
|
local pair scratch host
|
|
pair=$(make_sandbox "$fixture")
|
|
scratch="${pair%|*}"
|
|
host="${pair#*|}"
|
|
set +e
|
|
out=$("$scratch/bin/ka-promote" "$host" --output-dir "$scratch/build" 2>&1)
|
|
actual_exit=$?
|
|
set -e
|
|
|
|
if [ "$actual_exit" -ne "$expected_exit" ]; then
|
|
ko "$name" "expected exit $expected_exit, got $actual_exit. Output: $out"
|
|
rm -rf "$scratch"
|
|
return
|
|
fi
|
|
if [ -n "$check_fn" ]; then
|
|
if ! "$check_fn" "$scratch" "$host" "$out"; then
|
|
ko "$name" "check function reported failure (see notes above)"
|
|
rm -rf "$scratch"
|
|
return
|
|
fi
|
|
fi
|
|
ok "$name"
|
|
rm -rf "$scratch"
|
|
}
|
|
|
|
check_series_dir() {
|
|
# 7 patches resolved, all from_series:true, in filename order.
|
|
local scratch="$1" host="$2"
|
|
local lock="$scratch/build/$host/v7.0/manifest.lock"
|
|
[ -f "$lock" ] || { note "manifest.lock missing"; return 1; }
|
|
local n
|
|
n=$(python3 -c "import yaml; print(len(yaml.safe_load(open('$lock'))['resolved_patches']))")
|
|
if [ "$n" -ne 7 ]; then
|
|
note "expected 7 resolved patches, got $n"
|
|
return 1
|
|
fi
|
|
python3 - "$lock" <<'PY' || { note "from_series check failed"; exit 1; }
|
|
import yaml, sys
|
|
lk = yaml.safe_load(open(sys.argv[1]))
|
|
for r in lk['resolved_patches']:
|
|
assert r['from_series'] is True, f"{r['include']} not flagged from_series"
|
|
expected_basenames = [f'0{i:03d}'[1:] for i in range(1,8)] # 0001..0007
|
|
got = [r['include'].split('/')[-1][:4] for r in lk['resolved_patches']]
|
|
assert got == ['000'+str(i) for i in range(1,8)], f'apply order mismatch: {got}'
|
|
PY
|
|
}
|
|
|
|
check_fresnel_parity() {
|
|
local scratch="$1" host="$2"
|
|
local lock="$scratch/build/$host/v7.0/manifest.lock"
|
|
[ -f "$lock" ] || { note "manifest.lock missing"; return 1; }
|
|
local b2
|
|
b2=$(python3 -c "import yaml; print(yaml.safe_load(open('$lock'))['cumulative']['b2sum'])")
|
|
if [ "$b2" != "$FRESNEL_EXPECTED_B2SUM" ]; then
|
|
note "b2sum mismatch"
|
|
note " expected: $FRESNEL_EXPECTED_B2SUM"
|
|
note " got: $b2"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
echo
|
|
echo "Running ka-promote test suite from $repo_root"
|
|
echo
|
|
|
|
# ----- unit: strip_trailer + write_cumulative shape (issue #31) -----
|
|
echo "::: strip_trailer + cumulative shape (issue #31)"
|
|
python3 - "$repo_root" <<'PY'
|
|
import importlib.util, pathlib, sys, tempfile, os
|
|
root = pathlib.Path(sys.argv[1])
|
|
from importlib.machinery import SourceFileLoader
|
|
mod = SourceFileLoader("ka_promote", str(root/"bin"/"ka-promote")).load_module()
|
|
|
|
# strip_trailer accepts both shapes and yields newline-terminated body
|
|
assert mod.strip_trailer(b"...body...\n-- \n2.54.0\n\n") == b"...body...\n"
|
|
assert mod.strip_trailer(b"...body...\n-- \n2.53.0\n\n") == b"...body...\n"
|
|
assert mod.strip_trailer(b"...body...\n-- \n2.20\n\n") == b"...body...\n"
|
|
assert mod.strip_trailer(b"...body...\n") == b"...body...\n"
|
|
assert mod.strip_trailer(b"...body...") == b"...body...\n"
|
|
# Multiple trailing blanks after the version still strip
|
|
assert mod.strip_trailer(b"x\n-- \n2.54.0\n\n\n") == b"x\n"
|
|
|
|
# write_cumulative: 3 inputs (mix of with-/without-trailer), check ordering
|
|
with tempfile.TemporaryDirectory() as d:
|
|
p1 = os.path.join(d, "a.patch"); open(p1,"wb").write(b"PA\n-- \n2.54.0\n\n")
|
|
p2 = os.path.join(d, "b.patch"); open(p2,"wb").write(b"PB\n") # already bare
|
|
p3 = os.path.join(d, "c.patch"); open(p3,"wb").write(b"PC\n-- \n2.40.1\n\n")
|
|
out = os.path.join(d, "out.patch")
|
|
resolved = [{"src": p1}, {"src": p2}, {"src": p3}]
|
|
mod.write_cumulative(resolved, out)
|
|
body = open(out,"rb").read()
|
|
assert body == b"PA\n-- \n2.54.0\n\nPB\n-- \n2.54.0\n\nPC\n", repr(body)
|
|
# Last patch (PC) must NOT carry an orphan trailer at EOF
|
|
assert not body.rstrip(b"\n").endswith(b"2.40.1"), \
|
|
f"last patch's trailer leaked into cumulative: {body[-40:]!r}"
|
|
print("PASS")
|
|
PY
|
|
if [ $? -eq 0 ]; then
|
|
results+=("PASS strip_trailer + cumulative shape (issue #31)")
|
|
pass=$((pass+1))
|
|
else
|
|
results+=("FAIL strip_trailer + cumulative shape (issue #31)")
|
|
fail=$((fail+1))
|
|
fi
|
|
echo
|
|
|
|
# Use the real fleet/fresnel.yaml — copy into a sandbox so the test is hermetic.
|
|
mkdir -p /tmp/ka-promote-parity-fixture
|
|
cp "$repo_root/fleet/fresnel.yaml" /tmp/ka-promote-parity-fixture/fresnel.yaml
|
|
run_test "fresnel parity (= Phase-3 ground truth b2sum)" \
|
|
/tmp/ka-promote-parity-fixture/fresnel.yaml 0 check_fresnel_parity
|
|
rm -rf /tmp/ka-promote-parity-fixture
|
|
|
|
run_test "series-dir resolver (bes2600/staging-prep-series-danctnix → 7 patches)" \
|
|
"$fixtures/series-dir.yaml" 0 check_series_dir
|
|
|
|
run_test "bad-include rejection (exit 4)" \
|
|
"$fixtures/bad-include.yaml" 4
|
|
|
|
run_test "missing-patch rejection (exit 2)" \
|
|
"$fixtures/missing-patch.yaml" 2
|
|
|
|
run_test "duplicate-include rejection (exit 4)" \
|
|
"$fixtures/duplicate-include.yaml" 4
|
|
|
|
echo
|
|
echo "===================="
|
|
printf '%s\n' "${results[@]}"
|
|
echo "===================="
|
|
echo "passed: $pass"
|
|
echo "failed: $fail"
|
|
[ "$fail" -eq 0 ] || exit 1
|