CI: one test binary per crate, line tables only, one WASM check (#2710)

* CI: dev profile keeps line tables for workspace crates and no debug info for dependencies

* Control server tests: persistent_requests_and_slow_replies_outlive_the_idle_read_timeout no longer races the idle timeout

Each request (and the closing half-shutdown) reaches the socket before the
worker reads, so on a slow machine only the reply waits outlast the 100 ms
idle timeout, as the test intends. The idle test sends its input the same
way, and the no-frames test allows 5 s instead of 2 s to give up.

* CI: one integration-test binary per crate

Each crate's tests/*.rs become modules of tests/it/main.rs (51 test files
into 13 binaries, plus 6 that stay their own). Inner #![cfg] attributes
move to the mod lines. Files that change state for the whole process keep
a binary of their own: codecs jxl_tail_heap and raw sr2_heap (dhat global
allocator), gpu shutdown, shutdown_first and memory, ui-egui shutdown.
Docs, comments and the GPU probe name the new paths and targets
(--test it <module>::); the CI report names an it binary's skips after
the crate.

* CI: tests under nextest, doctests apart, one wasm check

xtask ci runs the tests with cargo nextest run --lib --bins --tests when
cargo-nextest is installed, else cargo test with the same targets and a
one-line notice; doctests run after them with cargo test --workspace --doc.
The heif and hardware GPU steps use the same runner. Examples stay out of
the test build (clippy --all-targets still checks them). Concurrent tests
under nextest follow the RUST_TEST_THREADS cap (NEXTEST_TEST_THREADS).
The wasm check is one cargo check --keep-going over the same crates, and
clippy runs with --keep-going.

* CI: tests run under cargo test by default, nextest with LIGHTCRAFT_NEXTEST=1 (per-process scene cache)
This commit is contained in:
aiongg
2026-10-23 13:34:30 -04:00
committed by GitHub
parent 0e6b0b150f
commit f3bb7a9f30
89 changed files with 1002 additions and 820 deletions
+4
View File
@@ -0,0 +1,4 @@
//! The integration tests, one module per file, linked into one test binary.
mod cli;
mod mcp_shutdown;
+20 -11
View File
@@ -240,11 +240,14 @@ mod tests {
}
impl Connection {
fn new(open: &Arc<AtomicUsize>, idle: Duration) -> Self {
/// `before` acts on the client before the worker starts reading, so what it sends is
/// already there however late this thread gets scheduled.
fn new(open: &Arc<AtomicUsize>, idle: Duration, before: impl FnOnce(&mut TcpStream)) -> Self {
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
client.set_read_timeout(Some(Duration::from_secs(3))).unwrap();
let (stream, _) = listener.accept().unwrap();
before(&mut client);
let (tx, requests) = channel();
let worker = spawn_connection(stream, tx, egui::Context::default(), Arc::clone(open), idle).unwrap();
Self { client: BufReader::new(client), requests, worker: Some(worker) }
@@ -271,9 +274,8 @@ mod tests {
fn idle_connections_release_their_slots_without_dispatching() {
for input in ["", "{\"method\":\"engine.execute\""] {
let open = Arc::new(AtomicUsize::new(0));
let mut connection = Connection::new(&open, Duration::from_millis(100));
let mut connection = Connection::new(&open, Duration::from_millis(100), |c| c.write_all(input.as_bytes()).unwrap());
assert_eq!(open.load(Ordering::SeqCst), 1);
connection.client.get_mut().write_all(input.as_bytes()).unwrap();
let mut reply = String::new();
let closed = connection.client.read_to_string(&mut reply);
assert!(closed.is_ok(), "idle connection stayed open: {closed:?}");
@@ -285,8 +287,7 @@ mod tests {
assert!(reply["error"].as_str().unwrap().contains("idle read timed out"));
// A subsequent connection is admitted through the same accounting path.
let mut next = Connection::new(&open, Duration::from_secs(1));
next.client.get_mut().shutdown(std::net::Shutdown::Write).unwrap();
let mut next = Connection::new(&open, Duration::from_secs(1), |c| c.shutdown(std::net::Shutdown::Write).unwrap());
let mut eof = String::new();
next.client.read_to_string(&mut eof).unwrap();
next.join();
@@ -298,12 +299,20 @@ mod tests {
#[test]
fn persistent_requests_and_slow_replies_outlive_the_idle_read_timeout() {
let open = Arc::new(AtomicUsize::new(0));
let mut connection = Connection::new(&open, Duration::from_millis(100));
let request = |id: u64| format!("{}\n", json!({"id": id, "method": "engine.commands"}));
// Each request reaches the socket before the worker reads it (the next one while the worker
// waits for the reply), so only the reply waits outlast the idle timeout, however slowly
// this thread runs.
let mut connection = Connection::new(&open, Duration::from_millis(100), |c| c.write_all(request(1).as_bytes()).unwrap());
for id in [1, 2] {
writeln!(connection.client.get_mut(), "{}", json!({"id": id, "method": "engine.commands"})).unwrap();
let req = connection.requests.recv_timeout(Duration::from_secs(3)).unwrap();
let req = connection.requests.recv_timeout(Duration::from_secs(10)).unwrap();
assert_eq!(req.method, "engine.commands");
assert!(req.take());
if id == 1 {
connection.client.get_mut().write_all(request(2).as_bytes()).unwrap();
} else {
connection.client.get_mut().shutdown(std::net::Shutdown::Write).unwrap();
}
// A dispatched command waits for its reply, not for socket input.
std::thread::sleep(Duration::from_millis(250));
req.reply.send(json!({"ok": true, "result": id})).unwrap();
@@ -312,7 +321,6 @@ mod tests {
let reply: Value = serde_json::from_str(&reply).unwrap();
assert_eq!(reply, json!({"id": id, "ok": true, "result": id}));
}
connection.client.get_mut().shutdown(std::net::Shutdown::Write).unwrap();
let mut eof = String::new();
connection.client.read_to_string(&mut eof).unwrap();
connection.join();
@@ -327,7 +335,8 @@ mod tests {
let (req, rrx) = ControlRequest::new("ui.inspect", json!({}));
let t0 = std::time::Instant::now();
let r = await_reply(&rrx, &req.state, || 7, || {}, Duration::from_millis(50), Duration::from_secs(10));
assert!(t0.elapsed() < Duration::from_secs(2));
// the frame window (50 ms), not the 10 s timeout; the margin is for a slow machine
assert!(t0.elapsed() < Duration::from_secs(5), "{:?}", t0.elapsed());
assert_eq!(r["ok"], false);
assert!(r["error"].as_str().unwrap().contains("not visible"), "{r}");
assert!(!req.take(), "the app does not run it once it shows up");