* 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>
193 lines
6.6 KiB
Python
Executable file
193 lines
6.6 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Copyright (c) 2026 YAMS Contributors
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Print bounded diagnostics for failed tests in a Meson build directory."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import TextIO
|
|
|
|
NONFAIL_RESULTS = {"OK", "SKIP", "EXPECTEDFAIL"}
|
|
MAX_TEST_OUTPUT_CHARS = 16_000
|
|
MAX_TEXT_OUTPUT_CHARS = 16_000
|
|
MAX_TEXT_READ_BYTES = 64_000
|
|
_SENSITIVE_TEXT_LINE = re.compile(
|
|
r"^\s*(?:>>>|command\s*[:=]|(?:inherited\s+)?env(?:ironment)?\s*[:=])", re.IGNORECASE
|
|
)
|
|
|
|
|
|
def _display_scalar(value: object, default: str) -> str:
|
|
if isinstance(value, (str, int, float, bool)):
|
|
return str(value)
|
|
return default
|
|
|
|
|
|
def _bounded_tail(text: str, limit: int, label: str) -> str:
|
|
if len(text) <= limit:
|
|
return text
|
|
omitted = len(text) - limit
|
|
return f"[... {label} truncated: {omitted} leading characters omitted ...]\n{text[-limit:]}"
|
|
|
|
|
|
def _print_failure(record: dict[str, object], output: TextIO) -> None:
|
|
name = _display_scalar(record.get("name"), "<unnamed>")
|
|
status = _display_scalar(record.get("result"), "UNKNOWN").strip().upper() or "UNKNOWN"
|
|
returncode = _display_scalar(record.get("returncode"), "<unknown>")
|
|
print(f"Meson test failure: name={name} status={status} returncode={returncode}", file=output)
|
|
|
|
sections: list[tuple[str, str]] = []
|
|
for key in ("stdout", "stderr"):
|
|
value = record.get(key)
|
|
if isinstance(value, str) and value:
|
|
sections.append((key, value))
|
|
|
|
if not sections:
|
|
print(" (no stdout/stderr captured)", file=output)
|
|
return
|
|
|
|
section_limit = MAX_TEST_OUTPUT_CHARS // len(sections)
|
|
for label, text in sections:
|
|
print(f"--- {label} (bounded) ---", file=output)
|
|
print(_bounded_tail(text, section_limit, f"{label} for {name}"), file=output)
|
|
|
|
|
|
def _read_json_log(path: Path, output: TextIO) -> tuple[list[dict[str, object]], bool]:
|
|
records: list[dict[str, object]] = []
|
|
malformed = 0
|
|
try:
|
|
with path.open("r", encoding="utf-8", errors="replace") as log:
|
|
for line_number, line in enumerate(log, start=1):
|
|
if not line.strip():
|
|
continue
|
|
try:
|
|
value = json.loads(line)
|
|
except (json.JSONDecodeError, ValueError) as error:
|
|
malformed += 1
|
|
print(
|
|
f"Meson JSONL diagnostic: malformed record at line {line_number}: "
|
|
f"{error.msg if isinstance(error, json.JSONDecodeError) else error}",
|
|
file=output,
|
|
)
|
|
continue
|
|
if not isinstance(value, dict):
|
|
malformed += 1
|
|
print(
|
|
f"Meson JSONL diagnostic: non-object record at line {line_number}; ignored.",
|
|
file=output,
|
|
)
|
|
continue
|
|
records.append(value)
|
|
except OSError as error:
|
|
print(f"Meson JSONL diagnostic: cannot read {path}: {error}", file=output)
|
|
return [], True
|
|
|
|
if malformed:
|
|
print(
|
|
f"Meson JSONL diagnostic: retained {len(records)} valid record(s); "
|
|
f"ignored {malformed} malformed record(s).",
|
|
file=output,
|
|
)
|
|
if not records:
|
|
print(f"Meson JSONL diagnostic: {path} contains no usable records.", file=output)
|
|
return records, malformed > 0 or not records
|
|
|
|
|
|
def _sanitized_text_tail(path: Path) -> tuple[str, bool]:
|
|
with path.open("rb") as log:
|
|
log.seek(0, 2)
|
|
size = log.tell()
|
|
start = max(0, size - MAX_TEXT_READ_BYTES)
|
|
log.seek(start)
|
|
raw = log.read(MAX_TEXT_READ_BYTES)
|
|
|
|
text = raw.decode("utf-8", errors="replace")
|
|
if start:
|
|
# Never emit a partial leading line: it may be the tail of an environment header.
|
|
text = text.partition("\n")[2]
|
|
|
|
lines: list[str] = []
|
|
redacted = False
|
|
for line in text.splitlines():
|
|
if _SENSITIVE_TEXT_LINE.match(line):
|
|
if not redacted or not lines or lines[-1] != "[command/environment line omitted]":
|
|
lines.append("[command/environment line omitted]")
|
|
redacted = True
|
|
else:
|
|
lines.append(line)
|
|
|
|
sanitized = "\n".join(lines)
|
|
if start:
|
|
sanitized = (
|
|
f"[... testlog.txt prefix omitted; showing final {len(raw)} bytes ...]\n"
|
|
f"{sanitized}"
|
|
)
|
|
return _bounded_tail(sanitized, MAX_TEXT_OUTPUT_CHARS, "testlog.txt fallback"), redacted
|
|
|
|
|
|
def _print_text_fallback(path: Path, output: TextIO) -> None:
|
|
if not path.is_file():
|
|
print(f"Meson text fallback unavailable: {path} is missing.", file=output)
|
|
return
|
|
try:
|
|
tail, redacted = _sanitized_text_tail(path)
|
|
except OSError as error:
|
|
print(f"Meson text fallback unreadable: {path}: {error}", file=output)
|
|
return
|
|
|
|
print(f"--- bounded fallback tail from {path} ---", file=output)
|
|
if tail:
|
|
print(tail, file=output)
|
|
else:
|
|
print("(testlog.txt is empty after filtering)", file=output)
|
|
if redacted:
|
|
print("Meson text fallback omitted command/environment lines.", file=output)
|
|
|
|
|
|
def report(build_dir: Path, output: TextIO = sys.stdout) -> None:
|
|
log_dir = build_dir / "meson-logs"
|
|
json_path = log_dir / "testlog.json"
|
|
text_path = log_dir / "testlog.txt"
|
|
|
|
if not json_path.is_file():
|
|
print(f"Meson JSONL diagnostic: {json_path} is missing.", file=output)
|
|
_print_text_fallback(text_path, output)
|
|
return
|
|
|
|
records, needs_fallback = _read_json_log(json_path, output)
|
|
failures = []
|
|
for record in records:
|
|
status = _display_scalar(record.get("result"), "UNKNOWN").strip().upper()
|
|
if status not in NONFAIL_RESULTS:
|
|
failures.append(record)
|
|
|
|
if failures:
|
|
for record in failures:
|
|
_print_failure(record, output)
|
|
elif records:
|
|
print("Meson JSONL diagnostic: no failed test records found.", file=output)
|
|
|
|
if needs_fallback:
|
|
print(
|
|
"Meson JSONL diagnostic: using text fallback because JSONL was incomplete or unusable.",
|
|
file=output,
|
|
)
|
|
_print_text_fallback(text_path, output)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Report Meson test failures without deciding the CI gate result."
|
|
)
|
|
parser.add_argument("build_dir", type=Path, help="Meson build directory")
|
|
args = parser.parse_args(argv)
|
|
report(args.build_dir)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|