Files
HPCS6500/tests/golden.rs
T
grabowski 54242da04b Rewrite the driver in Rust; move the testbench tools out
The repository is now only the HPCS 6500 driver: a Rust library (serial
transport, every protocol command, block parsing, USBPcap captures, a
simulated instrument) and the hpcs6500 CLI with the flags of the former
hpcs6500.py, plus dump/capture for reverse engineering.

The lamp station, lamp export, TM-30, label printer and LED calibration
move to B4L/lamp-testbench. Golden tests pin the Rust output to the Python
driver's: parsed values bit-exact, report text, CSV and number formatting.
The Python driver is at tag python-final.
2026-10-22 17:01:47 +07:00

190 lines
6.3 KiB
Rust

//! Golden tests against the output of the former Python driver
//! (`hpcs6500.py`), generated from real USB captures.
use hpcs6500::format::{csv_header, csv_row, format_quick, format_reading, pcap_report};
use hpcs6500::{Reading, pcap, pyfmt};
use serde_json::Value;
use std::path::{Path, PathBuf};
fn dir(sub: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join(sub)
}
fn fixture(name: &str) -> Reading {
let meas = std::fs::read(dir("fixtures").join(format!("{name}_meas.bin"))).unwrap();
let elec = std::fs::read(dir("fixtures").join(format!("{name}_elec.bin"))).ok();
Reading::parse(&meas, elec.as_deref()).unwrap()
}
fn golden() -> Vec<Value> {
serde_json::from_str(&std::fs::read_to_string(dir("golden").join("readings.json")).unwrap())
.unwrap()
}
fn text(name: &str) -> String {
std::fs::read_to_string(dir("golden").join(name))
.unwrap()
.replace("\r\n", "\n")
}
/// Python values in the golden file: floats as {"f": repr}, lists, ints, strings.
fn same(rust: &Value, py: &Value, path: &str) {
match py {
Value::Object(o) if o.contains_key("f") => {
let want = o["f"].as_str().unwrap();
let got = pyfmt::repr(
rust.as_f64()
.unwrap_or_else(|| panic!("{path}: not a number: {rust}")),
);
assert_eq!(got, want, "{path}");
}
Value::Array(a) => {
let r = rust
.as_array()
.unwrap_or_else(|| panic!("{path}: not a list"));
assert_eq!(r.len(), a.len(), "{path}: length");
for (i, (x, y)) in r.iter().zip(a).enumerate() {
same(x, y, &format!("{path}[{i}]"));
}
}
other => assert_eq!(rust, other, "{path}"),
}
}
#[test]
fn parsed_blocks_match_python_exactly() {
for g in golden() {
let name = g["name"].as_str().unwrap();
let r = fixture(name);
let v = serde_json::to_value(&r).unwrap();
let keys: Vec<&str> = v.as_object().unwrap().keys().map(|k| k.as_str()).collect();
let want: Vec<&str> = g["keys"]
.as_array()
.unwrap()
.iter()
.map(|k| k.as_str().unwrap())
.collect();
assert_eq!(keys, want, "{name}: key order");
for k in want {
same(&v[k], &g["values"][k], &format!("{name}.{k}"));
}
assert_eq!(
r.electrical.as_ref().unwrap().harmonics.is_some(),
g["harmonics"].as_bool().unwrap(),
"{name}"
);
}
}
#[test]
fn report_text_matches_python() {
for g in golden() {
let name = g["name"].as_str().unwrap();
let r = fixture(name);
assert_eq!(
format_reading(&r, false),
text(&format!("format_{name}.txt")),
"{name}"
);
assert_eq!(
format_reading(&r, true),
text(&format!("format_{name}_spectrum.txt")),
"{name} spectrum"
);
let n: usize = name[1..].parse().unwrap();
assert_eq!(
format!("{}\n{}", format_quick(&r, Some(n)), format_quick(&r, None)),
text(&format!("quick_{name}.txt"))
);
}
}
#[test]
fn csv_matches_python() {
for g in golden() {
let name = g["name"].as_str().unwrap();
let want = std::fs::read_to_string(dir("golden").join(format!("csv_{name}.txt"))).unwrap();
assert_eq!(csv_header() + &csv_row("T", &fixture(name)), want, "{name}");
}
}
#[test]
fn float_formatting_matches_python() {
let cases: Vec<Value> =
serde_json::from_str(&std::fs::read_to_string(dir("golden").join("pyfmt.json")).unwrap())
.unwrap();
assert!(cases.len() > 1000);
for c in &cases {
let x = f64::from_bits(c["bits"].as_u64().unwrap());
assert_eq!(pyfmt::repr(x), c["repr"].as_str().unwrap(), "repr {x:e}");
assert_eq!(pyfmt::g(x, 6), c["g6"].as_str().unwrap(), "g6 {x:e}");
for (k, p) in [("f2", 2), ("f1", 1), ("f0", 0)] {
if let Some(w) = c[k].as_str() {
assert_eq!(pyfmt::fixed(x, p), w, "{k} {x:e}");
}
}
if let Some(w) = c["round1"].as_str() {
assert_eq!(pyfmt::repr(pyfmt::round(x, 1)), w, "round1 {x:e}");
}
}
}
#[test]
fn pcap_round_trip_and_report() {
// a capture with two readings, written in memory
let (a, b) = (fixture("r01"), fixture("r02"));
let block =
|n: &str, k: &str| std::fs::read(dir("fixtures").join(format!("{n}_{k}.bin"))).unwrap();
let file = pcap::write(&[
(pcap::Dir::Tx, vec![0x8C, 0x13]),
(pcap::Dir::Rx, vec![0x8C, 0x13, 0x0F, 0x40]),
(pcap::Dir::Rx, block("r01", "meas")),
(pcap::Dir::Rx, block("r01", "elec")),
(pcap::Dir::Rx, block("r02", "meas")),
(pcap::Dir::Rx, block("r02", "elec")),
]);
let msgs = pcap::messages(&file).unwrap();
assert_eq!(msgs.len(), 6);
assert_eq!(msgs[0].dir, pcap::Dir::Tx);
assert_eq!(pcap::readings(&msgs), vec![a, b]);
let report = pcap_report(&msgs, true, false);
assert_eq!(report.lines().count(), 2);
assert!(pcap::messages(&[]).is_err());
assert!(pcap::messages(&[0u8; 30]).is_err());
}
/// The `--parse` reports of real captures. The captures are not in git
/// (`captures/` is ignored); the test runs where they are.
#[test]
fn parse_reports_of_real_captures() {
let caps = Path::new(env!("CARGO_MANIFEST_DIR")).join("captures");
let mut checked = 0;
for cap in [
"usb_capture_20260204_123655_hub1",
"usb_capture_20260204_134301_hub1",
] {
let path = caps.join(format!("{cap}.pcap"));
if !path.exists() {
eprintln!("skipped: {} not here", path.display());
continue;
}
let msgs = pcap::read_messages(&path).unwrap();
assert_eq!(
pcap_report(&msgs, false, false),
text(&format!("parse_{cap}.txt"))
);
assert_eq!(
pcap_report(&msgs, true, false),
text(&format!("parse_{cap}_quick.txt"))
);
assert_eq!(
pcap_report(&msgs, false, true),
text(&format!("parse_{cap}_harmonics.txt"))
);
checked += 1;
}
eprintln!("{checked} capture(s) checked");
}