* test(search,daemon,storage): assert behaviour instead of frozen literals Several suites encoded exact internal values that legitimate tuning and ranking changes are allowed to move: SCIENTIFIC-era profile weights and rrfK values, corpus-statistic figures, and hardcoded timeout/cancellation numbers. They failed on every platform after those changes landed, so the real behaviour regressions they should catch were hidden behind noise. Replace the frozen literals with contract assertions: weights are finite, non-negative and normalize, state selection still holds, capability-gated sources stay zeroed, and hybrid results keep matching canonical hash/path sets. Deadlines are not widened and the daemon paths they cover are unchanged. Also add independent corpus and adapter oracles, deterministic collector cancellation and late-result gates, and synchronize ServiceManager shutdown observation, so the tests fail for the behaviour they name. The portability allowlist keeps its two reviewed raw test-env reads; only their line numbers moved. Signed-off-by: trvon <git@trevon.dev> * ci(tests): summarise Meson failures without flooding the log A failing test run printed the whole Meson suite output, so the actual failing test names and their assertion messages were buried and the uploaded logs were dominated by passing-test chatter. Add a bounded reporter that extracts the failing test names plus their surrounding diagnostics, keeps an overall cap on emitted lines, and is unit-tested against representative Meson output. Wire it into the Tests workflow so failures stay readable. Signed-off-by: trvon <git@trevon.dev> * build(deb): validate candidate identity, mapping and upgrade ordering The Debian packaging scripts took filenames and versions on trust, so a misnamed candidate or a wrong package ordering could pass validation while the real install path was never exercised. Fail closed on candidate identity, version and architecture, verify the package-to-source mapping, seed the legacy-only layout, wait on the actual services, and assert install/upgrade/remove ordering for both Debian and RPM in an offline container. Cases that cannot be proven on a host are reported as skipped rather than silently passing. Signed-off-by: trvon <git@trevon.dev> --------- Signed-off-by: trvon <git@trevon.dev>
363 lines
17 KiB
Python
363 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise package script functions without installing packages or running services."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
CHECKS = ROOT / "scripts/local-ci/package-validate-checks.sh"
|
|
BUILDER = ROOT / "scripts/build-deb.sh"
|
|
|
|
|
|
def shell(source: str, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(["bash", "-c", source], text=True, capture_output=True,
|
|
env={**os.environ, **(env or {})}, timeout=10)
|
|
|
|
|
|
class PackageVersions(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
if int(shell("printf '%s' \"${BASH_VERSINFO[0]}\"").stdout) < 4:
|
|
raise unittest.SkipTest("builder requires Bash 4+; run this suite on Linux")
|
|
source = BUILDER.read_text()
|
|
cls.functions = source[source.index("get_base_version() {"):source.index("prepare_stage_docs() {")]
|
|
|
|
def version(self, surface: str, value: str) -> list[str]:
|
|
result = shell(self.functions + f'\ncompute_{surface} "{value}"', {"BASE_VERSION": "0.20.5"})
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
return result.stdout.splitlines()
|
|
|
|
def test_stable_versions_unchanged(self) -> None:
|
|
self.assertEqual(self.version("deb_version", "0.20.5"), ["0.20.5"])
|
|
self.assertEqual(self.version("rpm_nv", "0.20.5"), ["0.20.5", "1"])
|
|
|
|
def test_nightly_deb_is_after_base_stable(self) -> None:
|
|
version = self.version("deb_version", "nightly-20261005-5406C399")[0]
|
|
self.assertEqual(version, "0.20.5+nightly.20261005+g5406c399")
|
|
if shutil.which("dpkg"):
|
|
self.assertEqual(shell(f'dpkg --compare-versions 0.20.5 lt "{version}"').returncode, 0)
|
|
self.assertEqual(shell(f'dpkg --compare-versions "{version}" lt 0.20.6').returncode, 0)
|
|
|
|
def test_weekly_deb_is_after_base_stable(self) -> None:
|
|
self.assertEqual(self.version("deb_version", "weekly-2026w41-5406c399"),
|
|
["0.20.5+weekly.2026w41+g5406c399"])
|
|
|
|
def test_nightly_rpm_is_after_base_release(self) -> None:
|
|
self.assertEqual(self.version("rpm_nv", "nightly-20261005-5406c399"),
|
|
["0.20.5", "1.1.nightly.20261005.g5406c399"])
|
|
|
|
def test_weekly_rpm_is_after_base_release(self) -> None:
|
|
self.assertEqual(self.version("rpm_nv", "weekly-2026w41-5406c399"),
|
|
["0.20.5", "1.1.weekly.2026w41.g5406c399"])
|
|
|
|
def test_later_nightly_and_next_stable_order(self) -> None:
|
|
a = self.version("deb_version", "nightly-20261005-5406c399")[0]
|
|
b = self.version("deb_version", "nightly-20261006-5406c399")[0]
|
|
if not shutil.which("dpkg"):
|
|
self.skipTest("Debian comparator unavailable")
|
|
self.assertEqual(shell(f'dpkg --compare-versions "{a}" lt "{b}"').returncode, 0)
|
|
self.assertEqual(shell(f'dpkg --compare-versions "{b}" lt 0.20.6').returncode, 0)
|
|
|
|
|
|
class UpgradeOracle(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name)
|
|
(self.root / "user-units").mkdir()
|
|
(self.root / "system-units").mkdir()
|
|
(self.root / "user-units/yams-daemon.service").write_text("unit\n")
|
|
self.source = CHECKS.read_text().rsplit('case "${PHASE}" in', 1)[0]
|
|
# Relocate fixed in-container scratch paths; never write host /var/tmp or /usr.
|
|
self.source = self.source.replace("/var/tmp/", str(self.root) + "/")
|
|
self.mocks = r'''
|
|
TOKEN_FILE="$TEST_ROOT/tokens"
|
|
USER_UNIT_DIR="$TEST_ROOT/user-units"
|
|
SYSTEM_UNIT_DIR="$TEST_ROOT/system-units"
|
|
LEGACY_DATA_DIR="$TEST_ROOT/legacy"
|
|
UPGRADE_NOTE="$TEST_ROOT/upgrade-note.txt"
|
|
LEGACY_MANIFEST="$TEST_ROOT/legacy.sha256"
|
|
ensure_user() { :; }
|
|
wait_user_manager() { return 0; }
|
|
wait_user_active() { return 0; }
|
|
user_journal() { :; }
|
|
sleep() { :; }
|
|
token() { printf 'fixedseedtoken'; }
|
|
check_no_dangling_links() { pass "$1" clean; }
|
|
uctl() { case "$*" in *is-enabled*) printf enabled;; esac; }
|
|
systemctl() {
|
|
case "$*" in
|
|
is-active*)
|
|
if [ -e "$SYSTEM_UNIT_DIR/yams-daemon.service" ] && [ "${SYSTEM_STOPPED:-0}" != 1 ]; then
|
|
local polls
|
|
polls="$(cat "$TEST_ROOT/system-polls" 2>/dev/null || printf 0)"
|
|
polls=$((polls + 1))
|
|
printf '%s' "$polls" > "$TEST_ROOT/system-polls"
|
|
if [ "$polls" -ge "${SYSTEM_READY_AFTER:-1}" ]; then printf active; return 0; fi
|
|
printf activating
|
|
return 3
|
|
fi
|
|
printf inactive
|
|
return 3
|
|
;;
|
|
*is-enabled*) printf enabled;;
|
|
stop*) SYSTEM_STOPPED=1; return 0;;
|
|
show*) printf 123;;
|
|
esac
|
|
}
|
|
as_user() {
|
|
shift
|
|
case "$*" in
|
|
*"yams cat"*) if [ "${BODY_CHANGED:-0}" = 1 ]; then printf corrupted; else command cat "$UPGRADE_NOTE"; fi ;;
|
|
*"yams add"*) return "${ADD_RC:-0}";;
|
|
*) return 0;;
|
|
esac
|
|
}
|
|
search_finds() { return "${SEARCH_RC:-0}"; }
|
|
installed_package_identity() { printf '%s' "${ACTUAL_ID:-yams 0.20.5+nightly.20261005+g5406c399 amd64}"; }
|
|
package_identity_from_file() { printf '%s' 'yams 0.20.5+nightly.20261005+g5406c399 amd64'; }
|
|
rm() { :; }
|
|
apt-get() { return "${INSTALL_RC:-0}"; }
|
|
sha256sum() { python3 "$TEST_ROOT/hash.py" "$@"; }
|
|
'''
|
|
(self.root / "hash.py").write_text('''import hashlib, pathlib, sys
|
|
args = [a for a in sys.argv[1:] if a != "--quiet"]
|
|
if args and args[0] == "-c":
|
|
for line in pathlib.Path(args[1]).read_text().splitlines():
|
|
digest, name = line.split(" ", 1)
|
|
p = pathlib.Path(name)
|
|
if not p.is_file() or hashlib.sha256(p.read_bytes()).hexdigest() != digest:
|
|
sys.exit(1)
|
|
else:
|
|
for name in args:
|
|
print(hashlib.sha256(pathlib.Path(name).read_bytes()).hexdigest() + " " + name)
|
|
''')
|
|
|
|
def run_shell(self, commands: str, **env: str) -> str:
|
|
result = shell('set -- seed deb dummy.deb\n' + self.source + self.mocks + commands,
|
|
{"TEST_ROOT": str(self.root), **env})
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
return result.stdout
|
|
|
|
def seed(self, **env: str) -> str:
|
|
return self.run_shell('phase_seed\nprintf "MODE=%s\\n" "$(recall UPGRADE_MODE)"\n', **env)
|
|
|
|
def seeded_state(self, verified: str = "yes") -> str:
|
|
note = b"package upgrade note fixedseedtoken\n"
|
|
(self.root / "upgrade-note.txt").write_bytes(note)
|
|
digest = hashlib.sha256(note).hexdigest()
|
|
return (f'remember UPGRADE_MODE per-user\nremember LEGACY_PRESENT no\nremember UPGRADE_TOKEN fixedseedtoken\n'
|
|
f'remember UPGRADE_HASH {digest}\nremember INSTALL_VERIFIED {verified}\n'
|
|
'remember INSTALL_EXPECTED "yams 0.20.5+nightly.20261005+g5406c399 amd64"\n')
|
|
|
|
def test_per_user_seed_records_and_hydrates_pre_transition_note(self) -> None:
|
|
out = self.seed()
|
|
self.assertIn("MODE=per-user", out)
|
|
self.assertIn("PASS upgrade-seed-body", out)
|
|
self.assertNotIn("FAIL ", out)
|
|
|
|
def test_unrecognized_service_fixture_fails(self) -> None:
|
|
(self.root / "user-units/yams-daemon.service").unlink()
|
|
self.assertIn("FAIL upgrade-seed-mode", self.seed())
|
|
|
|
def test_legacy_seed_snapshots_existing_bytes(self) -> None:
|
|
(self.root / "system-units/yams-daemon.service").write_text("legacy unit\n")
|
|
(self.root / "legacy").mkdir()
|
|
(self.root / "legacy/blob").write_bytes(b"old corpus bytes")
|
|
out = self.seed()
|
|
self.assertIn("MODE=legacy", out)
|
|
self.assertIn("PASS upgrade-seed-legacy", out)
|
|
self.assertIn("PASS upgrade-seed-body", out)
|
|
self.assertNotIn("FAIL ", out)
|
|
self.assertIn(hashlib.sha256(b"old corpus bytes").hexdigest(),
|
|
(self.root / "legacy.sha256").read_text())
|
|
|
|
def test_legacy_seed_waits_for_actual_active_state(self) -> None:
|
|
(self.root / "system-units/yams-daemon.service").write_text("legacy unit\n")
|
|
(self.root / "legacy").mkdir()
|
|
(self.root / "legacy/blob").write_bytes(b"existing legacy bytes")
|
|
out = self.seed(SYSTEM_READY_AFTER="3")
|
|
self.assertIn("MODE=legacy", out)
|
|
self.assertIn("PASS upgrade-seed-body", out)
|
|
self.assertNotIn("FAIL ", out)
|
|
self.assertEqual((self.root / "system-polls").read_text(), "3")
|
|
|
|
def test_legacy_seed_does_not_manufacture_absent_corpus(self) -> None:
|
|
(self.root / "system-units/yams-daemon.service").write_text("legacy unit\n")
|
|
out = self.seed()
|
|
self.assertIn("FAIL upgrade-seed-legacy", out)
|
|
self.assertFalse((self.root / "legacy").exists())
|
|
self.assertIn("MODE=\n", out)
|
|
|
|
def test_empty_legacy_corpus_not_accepted_as_hydration(self) -> None:
|
|
(self.root / "system-units/yams-daemon.service").write_text("legacy unit\n")
|
|
(self.root / "legacy").mkdir()
|
|
self.assertIn("FAIL upgrade-seed-legacy", self.seed())
|
|
|
|
def test_seed_add_failure_does_not_record_valid_mode(self) -> None:
|
|
out = self.seed(ADD_RC="1")
|
|
self.assertIn("FAIL upgrade-seed-add", out)
|
|
self.assertIn("MODE=\n", out)
|
|
|
|
def test_seed_body_mismatch_rejected(self) -> None:
|
|
self.assertIn("FAIL upgrade-seed-body", self.seed(BODY_CHANGED="1"))
|
|
|
|
def test_unseeded_upgrade_rejected(self) -> None:
|
|
out = self.run_shell(self.seeded_state() + ': > "$TOKEN_FILE"\nphase_upgraded\n')
|
|
self.assertIn("FAIL upgrade-prerequisites", out)
|
|
self.assertNotIn("PASS upgrade-cli", out)
|
|
|
|
def test_per_user_upgrade_has_no_unseeded_legacy_directory_requirement(self) -> None:
|
|
out = self.run_shell(self.seeded_state() + 'phase_upgraded\n')
|
|
self.assertIn("PASS upgrade-seeded-body", out)
|
|
self.assertNotIn("FAIL ", out)
|
|
self.assertIn("INFO upgrade-old-corpus-kept", out)
|
|
|
|
def test_missing_legacy_precondition_rejected(self) -> None:
|
|
for value in ("", "unknown"):
|
|
with self.subTest(value=value):
|
|
out = self.run_shell(self.seeded_state() + f'remember LEGACY_PRESENT "{value}"\nphase_upgraded\n')
|
|
self.assertIn("FAIL upgrade-prerequisites", out)
|
|
self.assertNotIn("PASS upgrade-cli", out)
|
|
|
|
def test_changed_seeded_body_rejected(self) -> None:
|
|
out = self.run_shell(self.seeded_state() + 'phase_upgraded\n', BODY_CHANGED="1")
|
|
self.assertIn("FAIL upgrade-seeded-body", out)
|
|
|
|
def test_failed_install_blocks_upgrade(self) -> None:
|
|
out = self.run_shell(self.seeded_state("no") + 'phase_upgraded\n')
|
|
self.assertIn("FAIL upgrade-package-identity", out)
|
|
self.assertNotIn("PASS upgrade-cli", out)
|
|
|
|
def test_wrong_installed_candidate_blocks_upgrade(self) -> None:
|
|
out = self.run_shell(self.seeded_state() + 'phase_upgraded\n', ACTUAL_ID="yams 0.20.5 amd64")
|
|
self.assertIn("FAIL upgrade-package-identity", out)
|
|
self.assertNotIn("PASS upgrade-cli", out)
|
|
|
|
def test_failed_install_invalidates_previous_success_record(self) -> None:
|
|
out = self.run_shell(self.seeded_state() + 'ARG=dummy.deb\nphase_install\nphase_upgraded\n', INSTALL_RC="100")
|
|
self.assertIn("FAIL install", out)
|
|
self.assertIn("FAIL upgrade-package-identity", out)
|
|
|
|
def test_real_debian_metadata_helpers_check_configured_status(self) -> None:
|
|
functions = self.source[self.source.index("package_identity_from_file() {"):
|
|
self.source.index("phase_install() {")]
|
|
commands = functions + r'''
|
|
dpkg-deb() { case "$3" in Package) printf yams;; Version) printf candidate;; Architecture) printf arm64;; esac; }
|
|
dpkg-query() { case "$*" in *Status*) printf '%s' "$PACKAGE_STATUS";; *) printf 'yams candidate arm64';; esac; }
|
|
package_identity_from_file dummy.deb
|
|
if installed_package_identity; then printf '\nVERIFIED\n'; else printf '\nREJECTED\n'; fi
|
|
'''
|
|
out = self.run_shell(commands, PACKAGE_STATUS="install ok installed")
|
|
self.assertIn("yams candidate arm64", out)
|
|
self.assertIn("VERIFIED", out)
|
|
out = self.run_shell(commands, PACKAGE_STATUS="install ok unpacked")
|
|
self.assertIn("REJECTED", out)
|
|
|
|
def test_real_metadata_helper_rejects_foreign_package(self) -> None:
|
|
functions = self.source[self.source.index("package_identity_from_file() {"):
|
|
self.source.index("phase_install() {")]
|
|
out = self.run_shell(functions + r'''
|
|
dpkg-deb() { case "$3" in Package) printf other;; Version) printf candidate;; Architecture) printf arm64;; esac; }
|
|
if package_identity_from_file dummy.deb; then printf ACCEPTED; else printf REJECTED; fi
|
|
''')
|
|
self.assertIn("REJECTED", out)
|
|
|
|
def test_install_checks_candidate_identity_even_on_success_exit(self) -> None:
|
|
out = self.run_shell('ARG=dummy.deb\nphase_install\n', ACTUAL_ID="yams 0.20.5 amd64")
|
|
self.assertIn("FAIL install-identity", out)
|
|
self.assertNotIn("PASS install ", out)
|
|
|
|
def test_legacy_manifest_rejects_changed_bytes(self) -> None:
|
|
(self.root / "legacy").mkdir()
|
|
data = self.root / "legacy/blob"
|
|
data.write_bytes(b"original legacy content")
|
|
manifest = self.root / "legacy.sha256"
|
|
manifest.write_text(hashlib.sha256(data.read_bytes()).hexdigest() + " " + str(data) + "\n")
|
|
commands = self.seeded_state() + 'remember UPGRADE_MODE legacy\nremember LEGACY_PRESENT yes\n'
|
|
out = self.run_shell(commands + 'phase_upgraded\n')
|
|
self.assertIn("PASS upgrade-old-corpus-kept", out)
|
|
data.write_bytes(b"changed legacy content")
|
|
out = self.run_shell(commands + 'phase_upgraded\n')
|
|
self.assertIn("FAIL upgrade-old-corpus-kept", out)
|
|
|
|
def test_missing_legacy_manifest_rejected(self) -> None:
|
|
out = self.run_shell(self.seeded_state() + 'remember UPGRADE_MODE legacy\nremember LEGACY_PRESENT yes\nphase_upgraded\n')
|
|
self.assertIn("FAIL upgrade-old-corpus-kept", out)
|
|
|
|
|
|
class HarnessPhaseGates(unittest.TestCase):
|
|
def test_transport_failure_is_recorded_without_child_fail_lines(self) -> None:
|
|
text = (ROOT / "scripts/local-ci/package-validate.sh").read_text()
|
|
function = text[text.index("run_phase() {"):text.index("note_fail() {")]
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
root = Path(temp)
|
|
mocks = r'''
|
|
WORK="$TEST_ROOT"
|
|
log() { :; }
|
|
lane_fmt() { printf deb; }
|
|
docker() { return "$TRANSPORT_RC"; }
|
|
note_fail() { printf '%s FAIL %s %s\n' "$1" "$2" "$3" >> "$WORK/$1.results"; }
|
|
'''
|
|
for code in (0, 42):
|
|
with self.subTest(code=code):
|
|
result_file = root / "debian.results"
|
|
result_file.write_text("")
|
|
result = shell('set -o pipefail\n' + function + mocks +
|
|
'\nrun_phase debian container install candidate.deb\n'
|
|
'printf "RESULT=%s\\n" "$?"\n',
|
|
{"TEST_ROOT": temp, "TRANSPORT_RC": str(code)})
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertIn(f"RESULT={code}", result.stdout)
|
|
if code:
|
|
self.assertIn("FAIL install", result_file.read_text())
|
|
else:
|
|
self.assertNotIn("FAIL ", result_file.read_text())
|
|
|
|
def test_failed_prerequisites_do_not_reach_upgraded_phase(self) -> None:
|
|
text = (ROOT / "scripts/local-ci/package-validate.sh").read_text()
|
|
functions = text[text.index("validate_lane() {"):text.index('for lane in "${LANES[@]}"; do')]
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
root = Path(temp)
|
|
(root / "old.deb").touch()
|
|
(root / "candidate.deb").touch()
|
|
mocks = r'''
|
|
WORK="$TEST_ROOT"
|
|
RUN_TAG=unit
|
|
abs() { printf '%s' "$1"; }
|
|
lane_pkg() { printf '%s/candidate.deb' "$TEST_ROOT"; }
|
|
lane_upgrade_from() { printf '%s/old.deb' "$TEST_ROOT"; }
|
|
build_substrate() { printf fake-image; }
|
|
boot() { :; }
|
|
stage_files() { :; }
|
|
log() { :; }
|
|
note_fail() { printf 'HARNESS FAILURE'; }
|
|
reboot() { :; }
|
|
docker() { :; }
|
|
lane_base_image() { printf fake-base; }
|
|
lane_platform() { :; }
|
|
CONTAINERS=()
|
|
run_phase() {
|
|
printf '%s %s\\n' "$3" "${4:-}" >> "$TEST_ROOT/phases"
|
|
if [ "$3" = "$FAIL_PHASE" ] && [ "${4:-}" = "$FAIL_PACKAGE" ]; then return 1; fi
|
|
return 0
|
|
}
|
|
'''
|
|
for phase, package in [("seed", ""), ("install", str(root / "old.deb")),
|
|
("install", str(root / "candidate.deb"))]:
|
|
with self.subTest(phase=phase, package=package):
|
|
(root / "phases").write_text("")
|
|
result = shell(functions + mocks + '\nvalidate_lane debian\n',
|
|
{"TEST_ROOT": temp, "FAIL_PHASE": phase, "FAIL_PACKAGE": package})
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertNotIn("upgraded", (root / "phases").read_text())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|