175 lines
6.8 KiB
Python
Executable File
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()
|