Implemented better testing and fixed skipepd frames

This commit is contained in:
Martino Ferrari
2026-07-01 16:39:34 +02:00
parent 7a326c5d78
commit 0bea41f866
46 changed files with 4358 additions and 1739 deletions
Binary file not shown.
Binary file not shown.
+90 -17
View File
@@ -21,6 +21,7 @@ import os
import re
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
@@ -60,18 +61,88 @@ def gtest_suite(gtest_bin, work):
return s
# ── Go ────────────────────────────────────────────────────────────────────────
# ── C++ Integration (DebugService runtime, non-GTest) ─────────────────────────
def go_suite(client_dir, work):
s = {"name": "Go (chain-client)", "lang": "go", "total": 0, "passed": 0,
def integration_suite(int_bin, work, timeout=220):
"""Run the printf-narrated IntegrationTests.ex binary and heuristically
derive per-test pass/fail from its stdout.
This binary predates GTest adoption and always ``return 0`` from main()
(only an internal 180s SIGALRM timeout or an OS-level crash produce a
non-zero exit), so exit code alone is not a reliable signal. Each of its
7 "--- Test N: ..." blocks prints "SUCCESS:"/"VALIDATION SUCCESSFUL:" on
success or "ERROR:"/"FAILURE:" on failure, so split stdout by those
headers and flag a block failed if it contains an ERROR/FAILURE marker.
Exercises DebugServiceBase.cpp/DebugService.cpp runtime logic that the
header-only DebugServiceGTest suite deliberately does not touch, so it is
the only source of real coverage for those files.
"""
s = {"name": "C++ Integration", "lang": "cpp", "total": 0, "passed": 0,
"failed": 0, "skipped": 0, "time_s": 0.0, "ok": False, "avail": False}
cov_p = os.path.join(work, "go_cover.out")
rc, out, err = _run(["go", "test", "-json", f"-coverprofile={cov_p}", "./..."],
cwd=client_dir)
if rc == 127:
s["detail"] = "go toolchain not found"
if not int_bin or not os.path.exists(int_bin):
s["detail"] = "IntegrationTests binary not found"
return s
s["avail"] = True
t0 = time.time()
rc, out, err = _run([int_bin], timeout=timeout)
s["time_s"] = round(time.time() - t0, 1)
text = out + "\n" + err
blocks = re.split(r"\n(?=--- Test \d+:)", text)
test_blocks = [b for b in blocks if b.lstrip().startswith("--- Test")]
finished = "All Integration Tests Finished." in text
s["total"] = len(test_blocks)
s["failed"] = sum(1 for b in test_blocks if re.search(r"\b(ERROR|FAILURE):", b))
if not finished and s["total"] == 0:
# Crashed/timed out before printing anything useful.
s["total"] = 1
s["failed"] = 1
s["detail"] = f"binary did not complete (rc={rc}): {(err or out)[-200:]}"
elif not finished:
s["detail"] = f"binary exited rc={rc} before finishing all tests"
s["passed"] = s["total"] - s["failed"]
s["ok"] = finished and s["failed"] == 0 and s["total"] > 0
return s
# ── Go ────────────────────────────────────────────────────────────────────────
def go_all_suites(repo, work):
"""Run Go test suites across all project modules and aggregate results."""
modules = [
(os.path.join(repo, "Test/E2E/chain/client"),
"Go (chain-client)"),
(os.path.join(repo, "Common/Client/go"),
"Go (common udpsprotocol + wshub)"),
(os.path.join(repo, "Client/debugger"),
"Go (debugger)"),
]
total_pct = 0.0
pct_count = 0
suites = []
for mod_dir, name in modules:
s = {"name": name, "lang": "go", "total": 0, "passed": 0,
"failed": 0, "skipped": 0, "time_s": 0.0, "ok": False, "avail": False}
cov_p = os.path.join(work, f"go_cover_{name.replace(' ', '_')}.out")
rc, out, err = _run(
["go", "test", "-json", f"-coverprofile={cov_p}", "./..."],
cwd=mod_dir)
if rc == 127:
s["detail"] = "go toolchain not found"
suites.append(s)
continue
s["avail"] = True
cov_pct = _parse_go_json(out, s)
if cov_pct is not None:
s["cov_pct"] = cov_pct
total_pct += cov_pct
pct_count += 1
suites.append(s)
return suites, (round(total_pct / pct_count, 1) if pct_count else None)
def _parse_go_json(out, s):
"""Parse Go test -json output into passed/failed/skipped counts.
Returns coverage percentage (float or None)."""
cov_pct = None
for line in out.splitlines():
try:
@@ -93,8 +164,7 @@ def go_suite(client_dir, work):
if m:
cov_pct = float(m.group(1))
s["ok"] = s["failed"] == 0 and s["total"] > 0
s["cov_pct"] = cov_pct
return s
return cov_pct
# ── Python ──────────────────────────────────────────────────────────────────
@@ -250,12 +320,16 @@ def main():
work = args.work or args.out
os.makedirs(work, exist_ok=True)
chain_dir = os.path.dirname(os.path.abspath(__file__))
client_dir = os.path.join(chain_dir, "client")
gtest_bin = os.path.join(args.repo, "Build", args.target, "GTest", "MainGTest.ex")
# BUILD_DIR for Test/Integration doubles the last path component
# ($(PACKAGE)/$(lastword of CURDIR)) — see MakeStdLibDefs.gcc.
integration_bin = os.path.join(args.repo, "Build", args.target,
"Test", "Integration", "Integration",
"IntegrationTests.ex")
suites = [gtest_suite(gtest_bin, work),
go_suite(client_dir, work),
py_suite(chain_dir, work)]
go_suites, go_avg_cov = go_all_suites(args.repo, work)
suites = ([gtest_suite(gtest_bin, work), integration_suite(integration_bin, work)]
+ go_suites + [py_suite(chain_dir, work)])
totals = {k: sum(s.get(k, 0) for s in suites)
for k in ("total", "passed", "failed", "skipped")}
ut = {"suites": suites, "totals": totals,
@@ -265,11 +339,10 @@ def main():
langs = []
py = next(s for s in suites if s["lang"] == "python")
go = next(s for s in suites if s["lang"] == "go")
langs.append({"name": "Python", "avail": py.get("cov_pct") is not None,
"pct": py.get("cov_pct"), "note": "coverage.py"})
langs.append({"name": "Go", "avail": go.get("cov_pct") is not None,
"pct": go.get("cov_pct"), "note": "go test -cover"})
langs.append({"name": "Go", "avail": go_avg_cov is not None,
"pct": go_avg_cov, "note": "go test -cover (avg across modules)"})
cpp = cpp_coverage(args.repo, args.target) if args.cpp_coverage else \
{"name": "C++", "avail": False, "pct": None, "note": "skipped (use --cpp-coverage)"}
langs.append(cpp)
+2 -2
View File
@@ -24,7 +24,7 @@ mkdir -p "${OUT_DIR}" "${WORK}"
SKIP_BUILD=0
ONLY=""
PDF_ONLY=0
CPP_COV=0
CPP_COV=1
while [ $# -gt 0 ]; do
case "$1" in
--skip-build) SKIP_BUILD=1 ;;
@@ -254,7 +254,7 @@ if [ "${CPP_COV}" -eq 1 ]; then
make -C "${REPO_ROOT}" -f Makefile.gcc clean >/dev/null 2>&1 || true
make -C "${REPO_ROOT}" -f Makefile.gcc core TARGET="${TARGET}" \
OPTIM="${COV_O}" LFLAGS="${COV_L}" 2>&1 | tail -1
for d in Test/Components/DataSources/UDPStreamer Test/Applications/StreamHub Test/GTest; do
for d in Test/Components/DataSources/UDPStreamer Test/Components/DataSources/UDPStreamerClient Test/Applications/StreamHub Test/GTest Test/Integration; do
make -C "${REPO_ROOT}/${d}" -f Makefile.gcc TARGET="${TARGET}" \
OPTIM="${COV_O}" LFLAGS="${COV_L}" 2>&1 | tail -1
done
+39 -1
View File
@@ -21,6 +21,14 @@ Oracle (per signal)
phase offset automatically). nRMSE tolerance is relaxed by the quant step.
* **Fed reference** (when ``--tap`` given): each received value must be within
``tol`` of some tap value too.
* **Continuity** (always, ≥10 points): a stream that stalls (client falling
behind, hub failing to flush a window, ...) can still pass fidelity —
whatever few samples *did* arrive still match the ground truth — while the
plot shows gaping holes. Flags any inter-sample gaps that are >10x the
median spacing and fails when their *summed* duration exceeds 5% of the
capture span. Calibrated against the full scenario matrix: healthy streams
(bursty per-tick live pushes, decimation, fragmentation, multicast, ...) top
out at 0.7% outlier-gap time; a stalled stream showed 55-91%.
"""
import argparse
import json
@@ -122,6 +130,29 @@ def nearest_err(recv_v, truth_v):
return float(np.max(d)) if d.size else 0.0
def gap_check(t_recv, outlier_mult=10.0, max_outlier_frac=0.05):
"""Detect large discontiguous holes in a received time series.
A handful of gaps a few times the median spacing are normal (bursty
per-tick live pushes, decimation, LTTB). Returns (ok, gap_frac, n_gaps,
max_gap): ``gap_frac`` is the fraction of the total capture span consumed
by gaps larger than ``outlier_mult`` times the median inter-sample gap;
when that adds up to more than ``max_outlier_frac`` of the whole capture,
the stream stalled/dropped a chunk rather than merely being decimated.
"""
if t_recv.size < 10:
return True, 0.0, 0, 0.0
t = np.sort(t_recv.astype(np.float64))
dt = np.diff(t)
span = float(t[-1] - t[0])
med = float(np.median(dt))
if span <= 0.0 or med <= 0.0:
return True, 0.0, 0, 0.0
outliers = dt[dt > outlier_mult * med]
gap_frac = float(outliers.sum() / span)
return gap_frac <= max_outlier_frac, gap_frac, int(outliers.size), float(dt.max())
def sine_shape(t, v, freq):
"""Return (corr, nrmse, amp_fit) for a sinusoid fit at ``freq``."""
w = 2.0 * np.pi * freq
@@ -153,6 +184,13 @@ def compare_signal(gt, t_recv, v_recv, tap_v=None):
fidelity_ok = max_err <= tol
m["fidelity_ok"] = bool(fidelity_ok)
gap_ok, gap_frac, n_gaps, max_gap = gap_check(t_recv)
m.update(gap_ok=bool(gap_ok), gap_frac=round(gap_frac, 4),
n_gaps=n_gaps, max_gap=max_gap)
if not gap_ok:
m["reason"] = (f"data hole: {gap_frac:.1%} of capture span in "
f"{n_gaps} gaps >10x median spacing (max={max_gap:.4g}s)")
shape_ok = True
if gt["formula"] == "sine" and v_recv.size >= 8 and gt["freq"]:
corr, nrmse, amp = sine_shape(t_recv, v_recv, gt["freq"])
@@ -180,7 +218,7 @@ def compare_signal(gt, t_recv, v_recv, tap_v=None):
fed_ok = fed_err <= tol
m.update(fed_err=fed_err, fed_ok=bool(fed_ok))
m["pass"] = bool(fidelity_ok and shape_ok and fed_ok)
m["pass"] = bool(fidelity_ok and shape_ok and fed_ok and gap_ok)
return m