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.
188 lines
6.0 KiB
Rust
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:?}");
|
|
}
|