Files
2026-06-25 00:45:45 +02:00

175 lines
6.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""
validate_binary.py — Compare MARTe2 binary input and output files.
Usage: python3 validate_binary.py <input.bin> <output.bin> [--label NAME]
Reads MARTe2 binary format (header: 4B numSigs, per-sig: 2B type + 32B name + 4B elems),
then compares signal count, element counts, and data rows between input and output.
Output rows are matched against input rows via set membership to handle
async FileWriter flush ordering.
"""
import struct, sys, os
def read_binary(path):
"""Read MARTe2 binary file. Returns (signals, raw_data_bytes)."""
if not os.path.exists(path):
return None, None
with open(path, 'rb') as f:
data = f.read()
if len(data) < 4:
return None, None
ns = struct.unpack_from('<I', data, 0)[0]
sigs = []
off = 4
for _ in range(ns):
if off + 38 > len(data):
break
tc = struct.unpack_from('<H', data, off)[0]
nm = data[off+2:off+34].rstrip(b'\x00').decode(errors='replace')
ne = struct.unpack_from('<I', data, off+34)[0]
sigs.append((nm, tc, ne))
off += 38
raw = data[off:]
return sigs, raw
def validate(input_path, output_path, label="test"):
"""Validate output against input. Returns (passed, message, details)."""
in_sigs, in_raw = read_binary(input_path)
out_sigs, out_raw = read_binary(output_path)
if in_sigs is None:
return False, f"[{label}] Cannot read input file: {input_path}", {}
if out_sigs is None or out_raw is None:
return False, f"[{label}] Cannot read output file: {output_path}", {}
# Signal validation
if len(in_sigs) != len(out_sigs):
return False, f"[{label}] Signal count mismatch: {len(in_sigs)} in vs {len(out_sigs)} out", {}
for i, (si, so) in enumerate(zip(in_sigs, out_sigs)):
if si[2] != so[2]:
return False, f"[{label}] Signal '{si[0]}' size mismatch: {si[2]} in vs {so[2]} out", {}
total_elems = sum(ne for _, _, ne in in_sigs)
in_row_bytes = total_elems * 4
out_row_bytes = sum(ne for _, _, ne in out_sigs) * 4
if in_row_bytes != out_row_bytes:
return False, f"[{label}] Row size mismatch: {in_row_bytes} in vs {out_row_bytes} out", {}
n_rows_in = len(in_raw) // in_row_bytes if in_row_bytes > 0 else 0
n_rows_out = len(out_raw) // out_row_bytes if out_row_bytes > 0 else 0
if n_rows_out == 0:
return False, f"[{label}] Output file has no data rows", {}
# Build input row set
input_rows = set()
for r in range(n_rows_in):
input_rows.add(in_raw[r * in_row_bytes:(r + 1) * in_row_bytes])
# Classify each output row into exactly one of three buckets:
# zero — all-zero row (benign startup transient before first DATA)
# matching — non-zero row that equals some input row (correct transport)
# mismatching — non-zero row that matches no input row (data corruption)
zero_rows = 0
matching = 0
mismatching = 0
first_zero = -1
first_match = -1
first_mismatch = -1
for r in range(n_rows_out):
row = out_raw[r * out_row_bytes:(r + 1) * out_row_bytes]
if not any(row):
zero_rows += 1
if first_zero < 0:
first_zero = r
elif row in input_rows:
matching += 1
if first_match < 0:
first_match = r
else:
mismatching += 1
if first_mismatch < 0:
first_mismatch = r
details = {
"n_signals": len(in_sigs),
"signal_names": [s[0] for s in in_sigs],
"signal_sizes": [s[2] for s in in_sigs],
"row_bytes": in_row_bytes,
"n_rows_in": n_rows_in,
"n_rows_out": n_rows_out,
"zero_rows": zero_rows,
"matching_rows": matching,
"mismatching_rows": mismatching,
"first_zero_row": first_zero,
"first_matching_row": first_match,
"first_mismatch_row": first_mismatch,
"input_bytes": len(in_raw),
"output_bytes": len(out_raw),
}
# Fail threshold:
# - any non-zero output row that matches no input row → data corruption → FAIL
# - no matching rows at all (incl. all-zero output) → FAIL
if matching == 0:
return False, (f"[{label}] FAIL: no output row matches any input row "
f"({n_rows_out} rows: {zero_rows} zero, {mismatching} corrupted)"), details
if mismatching > 0:
return False, (f"[{label}] FAIL: {mismatching} non-zero output rows match no input row "
f"(first at row {first_mismatch})"), details
if zero_rows > 0:
return True, (f"[{label}] WARN: {matching}/{n_rows_out} rows match "
f"({zero_rows} startup zero rows; first match at row {first_match})"), details
return True, f"[{label}] PASS: all {n_rows_out} rows match input", details
def main():
import argparse
p = argparse.ArgumentParser(description="Validate MARTe2 binary output against input")
p.add_argument("input", help="Input binary file")
p.add_argument("output", help="Output binary file")
p.add_argument("--label", default="e2e", help="Test label for output messages")
p.add_argument("--json", default=None, help="Write the metrics (incl. pass/fail) to this JSON file")
args = p.parse_args()
passed, msg, details = validate(args.input, args.output, args.label)
if args.json is not None:
import json
record = dict(details)
record["label"] = args.label
record["passed"] = passed
record["status"] = "PASS" if passed else "FAIL"
record["message"] = msg.strip()
with open(args.json, "w") as jf:
json.dump(record, jf, indent=2)
# Always print details
if details:
print(f" Signals: {details['n_signals']} ({', '.join(f'{n}({s})' for n,s in zip(details['signal_names'], details['signal_sizes']))})")
print(f" Row size: {details['row_bytes']} B")
print(f" Input: {details['input_bytes']} B ({details['n_rows_in']} rows)")
print(f" Output: {details['output_bytes']} B ({details['n_rows_out']} rows)")
print(f" Matching: {details['matching_rows']}/{details['n_rows_out']}")
print(f" Zero rows: {details['zero_rows']}/{details['n_rows_out']}")
print(f" Mismatching: {details['mismatching_rows']}/{details['n_rows_out']} (non-zero, unmatched)")
if details['first_matching_row'] >= 0:
print(f" First matching row: {details['first_matching_row']}")
if details['first_zero_row'] >= 0:
print(f" First zero row: {details['first_zero_row']}")
if details['first_mismatch_row'] >= 0:
print(f" First mismatching row: {details['first_mismatch_row']}")
print(f" {'✓' if passed else '✗'} {msg}")
sys.exit(0 if passed else 1)
if __name__ == "__main__":
main()