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.
This commit is contained in:
1 parent
3ac7cd243b
commit
54242da04b
79 files changed
+23340
-3246
No files matched your search
+189
@@ -0,0 +1,189 @@
|
||||
//! 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");
|
||||
}
|
||||
Reference in new issue
Block a user