Files
lamp-testbench/crates/brother-ql/tests/network.rs
T
grabowski 2a3f28001c Lamp testbench in Rust around the HPCS 6500
lampbench station / export / label / tm30 / calibrate replace the Python
lamp_station.py, lamp_export.py, label_printer.py, tm30.py and
calibration.py that lived in HPCS6500-py. The driver is a submodule
(HPCS6500/, like mppt-testbench). Workspace crates: tm30 (TM-30-18 port of
colour-science 0.4.7), brother-ql (raster protocol, byte-identical to
brother_ql) and lamp-label (QR + ID labels for the QL-820NWB).

Bundles are byte-identical to lamp_export.py's (golden tests); the station
runs end to end in a test against the simulated instrument.
2026-10-22 17:26:14 +07:00

188 lines
6.0 KiB
Rust

//! `send_network` against a fake printer on 127.0.0.1.
//!
//! The Python network backend never reads from the socket (helpers.send
//! returns right after the write, even with `blocking=True`), so the fake
//! printers here check: the exact bytes arrive, the client closes the
//! connection on its own (EOF) right after writing, and it does not wait for
//! the status replies a real QL printer sends.
use std::io::{Read, Write};
use std::net::{Shutdown, TcpListener};
use std::path::Path;
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use brother_ql::{Error, Outcome, SendStatus, send_network};
fn job() -> Vec<u8> {
// a real two-label job for L0006 as produced by Python brother_ql
let p = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/golden/label_L0006_x2.bin");
std::fs::read(p).expect("golden data")
}
const SENT: SendStatus = SendStatus {
instructions_sent: true,
outcome: Outcome::Sent,
did_print: false,
ready_for_next_job: false,
};
/// 32-byte QL status reply (80 20 42 ...): model QL-820NWB (0x41), 23 mm
/// die-cut media, given status/phase type.
fn status_reply(status_type: u8, phase_type: u8) -> [u8; 32] {
let mut r = [0u8; 32];
r[0] = 0x80;
r[1] = 0x20;
r[2] = 0x42;
r[3] = 0x34;
r[4] = 0x41;
r[5] = 0x30;
r[10] = 23; // media width
r[11] = 0x0B; // die-cut
r[17] = 23; // media length
r[18] = status_type;
r[19] = phase_type;
r
}
#[test]
fn sends_exact_bytes_and_closes() {
let data = job();
for prefix in ["tcp://", ""] {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = thread::spawn(move || {
let (mut s, _) = listener.accept().unwrap();
let mut got = Vec::new();
s.read_to_end(&mut got).unwrap(); // returns only once the client closed
got
});
let status = send_network(
&format!("{prefix}127.0.0.1:{port}"),
&data,
true,
Duration::from_secs(5),
)
.unwrap();
assert_eq!(status, SENT);
let got = server.join().unwrap();
assert_eq!(got.len(), data.len());
assert!(got == data, "received bytes differ");
}
}
#[test]
fn blocking_does_not_wait_for_status_replies() {
let data = job();
let n = data.len();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let (tx, rx) = mpsc::channel();
let server = thread::spawn(move || {
let (mut s, _) = listener.accept().unwrap();
let mut got = vec![0u8; n];
s.read_exact(&mut got).unwrap();
// A real printer answers ESC i S and later reports "printing
// completed" (0x01) and "phase change: waiting to receive" (0x06/0x00).
// Hold them back for a while: the client must not wait for them.
let eof = {
let mut buf = [0u8; 1];
s.set_read_timeout(Some(Duration::from_secs(3))).unwrap();
matches!(s.read(&mut buf), Ok(0))
};
tx.send(()).unwrap();
let _ = s.write_all(&status_reply(0x01, 0x00));
let _ = s.write_all(&status_reply(0x06, 0x00));
let _ = s.shutdown(Shutdown::Both);
(got, eof)
});
let t0 = Instant::now();
let status = send_network(
&format!("tcp://127.0.0.1:{port}"),
&data,
true,
Duration::from_secs(5),
)
.unwrap();
let elapsed = t0.elapsed();
assert_eq!(status, SENT, "network backend never reports printed/ready");
let (got, eof) = server.join().unwrap();
rx.recv().unwrap();
assert!(got == data);
assert!(eof, "client did not close the connection after writing");
assert!(
elapsed < Duration::from_secs(2),
"send_network waited {elapsed:?}"
);
}
#[test]
fn non_blocking_is_identical() {
let data = b"\x1B\x40hello".to_vec();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = thread::spawn(move || {
let (mut s, _) = listener.accept().unwrap();
let mut got = Vec::new();
s.read_to_end(&mut got).unwrap();
got
});
let status = send_network(
&format!("127.0.0.1:{port}"),
&data,
false,
Duration::from_secs(5),
)
.unwrap();
assert_eq!(status, SENT);
assert_eq!(server.join().unwrap(), data);
}
#[test]
fn connection_refused_is_an_error() {
let port = {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
l.local_addr().unwrap().port()
};
let err = send_network(
&format!("tcp://127.0.0.1:{port}"),
b"x",
true,
Duration::from_secs(2),
)
.unwrap_err();
assert!(matches!(err, Error::Connect { .. }), "{err}");
let err = send_network("tcp://", b"x", true, Duration::from_secs(2)).unwrap_err();
assert!(matches!(err, Error::InvalidAddress(_)), "{err}");
}
#[test]
fn write_times_out_when_printer_stalls() {
// The fake printer accepts but never reads, so the socket buffers fill up.
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let (done_tx, done_rx) = mpsc::channel::<()>();
let server = thread::spawn(move || {
let (s, _) = listener.accept().unwrap();
let _ = done_rx.recv_timeout(Duration::from_secs(30));
drop(s);
});
let data = vec![0u8; 64 << 20];
let t0 = Instant::now();
let res = send_network(
&format!("127.0.0.1:{port}"),
&data,
true,
Duration::from_millis(500),
);
let elapsed = t0.elapsed();
done_tx.send(()).unwrap();
server.join().unwrap();
match res {
Err(Error::Write(e)) => assert_eq!(e.kind(), std::io::ErrorKind::TimedOut, "{e}"),
other => panic!("expected a write timeout, got {other:?}"),
}
assert!(elapsed < Duration::from_secs(5), "timeout took {elapsed:?}");
}