Tests: the MCP remote-backend tests pass on a loaded machine

The vectorcraft-mcp tests of the remote backend in tests.rs gave a fake
app 2 or 5 s to accept a connection or to read a request, and waited
2 s for a connection to end or a line to arrive. When a busy machine
held the client's thread for longer, the fake app gave up and the test
failed. These waits now use WAIT (20 s).

remote_connect_fails_fast asserted that a refused connect returned
within 5 s, and remote_call_reports_nothing_sent_when_reconnect_is_refused
asserted that the call returned within 2 s. On Windows a refused local connect
ends at the backend's own 800 ms connect timeout. A connect without
that timeout takes about 2 s. The 5 s bound did not tell the two apart,
and the 2 s bound left 1.2 s for any pause of the thread. Both tests
now bound the connect by FAILED_CONNECT (1.5 s) and run it up to five
times, until one run is under the bound. The second test checks the
error of every run.
This commit is contained in:
dotCooCoo
2026-10-29 13:00:38 +07:00
committed by bflatastic
parent 6c781864d1
commit e02b43b6be
+49 -20
View File
@@ -822,13 +822,18 @@ fn answer_dialog_needs_the_desktop_app() {
assert!(text_of(&r).contains("needs the desktop app"), "{r}");
}
/// How long a fake app waits for [`Remote`] to connect or to send, and how long a test waits for
/// a connection to end or a line to arrive. The wait only ends a test whose client or app never
/// does. A machine busy with other builds can hold a thread for seconds.
const WAIT: std::time::Duration = std::time::Duration::from_secs(20);
#[test]
fn remote_does_not_replay_when_edit_reply_is_lost() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.set_nonblocking(true).unwrap();
let addr = listener.local_addr().unwrap().to_string();
let h = std::thread::spawn(move || {
let timeout = std::time::Duration::from_secs(5);
let timeout = WAIT;
let mut backend = Headless::with_document();
let base = backend.call("document.inspect", json!({})).unwrap()["objects"].as_u64().unwrap();
for connection in 0..2 {
@@ -988,7 +993,7 @@ fn remote_rejects_uncorrelated_replies_without_replaying() {
listener.set_nonblocking(true).unwrap();
let addr = listener.local_addr().unwrap().to_string();
let h = std::thread::spawn(move || {
let timeout = std::time::Duration::from_secs(5);
let timeout = WAIT;
let mut seen = vec![];
for connection in 0..2 {
let deadline = std::time::Instant::now() + timeout;
@@ -1043,7 +1048,7 @@ fn remote_rejects_a_reply_with_a_mismatched_id_and_reconnects() {
let h = std::thread::spawn(move || {
let mut seen = vec![];
for stream in listener.incoming().flatten().take(2) {
stream.set_read_timeout(Some(std::time::Duration::from_secs(5))).unwrap();
stream.set_read_timeout(Some(WAIT)).unwrap();
let mut out = stream.try_clone().unwrap();
let mut reader = BufReader::new(stream);
let mut line = String::new();
@@ -1079,12 +1084,12 @@ fn remote_reports_the_reason_of_a_refusal_without_an_id() {
let h = std::thread::spawn(move || {
let (mut refused, _) = listener.accept().unwrap();
// The request is read first, so the refusal answers it and the close is orderly.
refused.set_read_timeout(Some(std::time::Duration::from_secs(5))).unwrap();
refused.set_read_timeout(Some(WAIT)).unwrap();
let _ = BufReader::new(refused.try_clone().unwrap()).read_line(&mut String::new());
writeln!(refused, "{}", json!({"ok": false, "error": "too many control connections"})).unwrap();
drop(refused);
let (stream, _) = listener.accept().unwrap();
stream.set_read_timeout(Some(std::time::Duration::from_secs(5))).unwrap();
stream.set_read_timeout(Some(WAIT)).unwrap();
let mut out = stream.try_clone().unwrap();
let mut line = String::new();
BufReader::new(stream).read_line(&mut line).unwrap();
@@ -1099,6 +1104,26 @@ fn remote_reports_the_reason_of_a_refusal_without_an_id() {
h.join().unwrap();
}
/// How long a refused connect to the app may take: longer than the 800 ms connect timeout of
/// [`Remote`], and shorter than the 2 s a refused local connect takes on Windows without one.
const FAILED_CONNECT: std::time::Duration = std::time::Duration::from_millis(1500);
/// Runs `attempt` until one run takes less than `bound`, at most five times, and returns how long
/// each run took. A machine busy with other builds can hold the test's thread for seconds in one
/// run; a connect that waits longer than its own timeout is slow in every run.
fn runs_until_faster_than(bound: std::time::Duration, mut attempt: impl FnMut()) -> Vec<std::time::Duration> {
let mut took = Vec::new();
while took.len() < 5 {
let start = std::time::Instant::now();
attempt();
took.push(start.elapsed());
if took.last() < Some(&bound) {
break;
}
}
took
}
#[test]
fn remote_connect_fails_fast() {
// A port this test holds, bound but not listening, so connecting to it is refused. (A port
@@ -1106,9 +1131,8 @@ fn remote_connect_fails_fast() {
let held = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, None).unwrap();
held.bind(&std::net::SocketAddr::from(([127, 0, 0, 1], 0)).into()).unwrap();
let port = held.local_addr().unwrap().as_socket().unwrap().port();
let start = std::time::Instant::now();
assert!(Remote::connect(&format!("127.0.0.1:{port}")).is_err());
assert!(start.elapsed() < std::time::Duration::from_secs(5), "took {:?}", start.elapsed());
let took = runs_until_faster_than(FAILED_CONNECT, || assert!(Remote::connect(&format!("127.0.0.1:{port}")).is_err()));
assert!(took.last() < Some(&FAILED_CONNECT), "took {took:?}");
// Nothing else could listen there meanwhile.
assert!(std::net::TcpListener::bind(format!("127.0.0.1:{port}")).is_err());
drop(held);
@@ -1124,7 +1148,7 @@ fn reusable_listener(addr: std::net::SocketAddr) -> std::net::TcpListener {
fn wait_for_remote_eof(remote: &Remote) {
let probe = remote.connection_probe();
probe.set_read_timeout(Some(std::time::Duration::from_secs(2))).unwrap();
probe.set_read_timeout(Some(WAIT)).unwrap();
assert_eq!(probe.peek(&mut [0]).unwrap(), 0);
}
@@ -1135,7 +1159,7 @@ fn remote_edit_succeeds_once_when_app_restarts() {
let mut remote = Remote::connect(&addr.to_string()).unwrap();
let (old_connection, _) = listener.accept().unwrap();
let old_handle = std::thread::spawn(move || {
let timeout = std::time::Duration::from_secs(2);
let timeout = WAIT;
old_connection.set_read_timeout(Some(timeout)).unwrap();
old_connection.set_write_timeout(Some(timeout)).unwrap();
let mut reader = BufReader::new(old_connection.try_clone().unwrap());
@@ -1157,7 +1181,7 @@ fn remote_edit_succeeds_once_when_app_restarts() {
let listener = reusable_listener(addr);
listener.set_nonblocking(true).unwrap();
let handle = std::thread::spawn(move || {
let timeout = std::time::Duration::from_secs(2);
let timeout = WAIT;
let deadline = std::time::Instant::now() + timeout;
let stream = loop {
match listener.accept() {
@@ -1204,15 +1228,15 @@ fn remote_does_not_send_on_a_connection_with_a_stray_line() {
let addr = listener.local_addr().unwrap().to_string();
let mut remote = Remote::connect(&addr).unwrap();
let (mut stale, _) = listener.accept().unwrap();
stale.set_read_timeout(Some(std::time::Duration::from_secs(2))).unwrap();
stale.set_read_timeout(Some(WAIT)).unwrap();
writeln!(stale, "{}", json!({"ok": false, "error": "idle"})).unwrap();
let probe = remote.connection_probe();
probe.set_read_timeout(Some(std::time::Duration::from_secs(2))).unwrap();
probe.set_read_timeout(Some(WAIT)).unwrap();
assert_eq!(probe.peek(&mut [0]).unwrap(), 1, "the stray line arrived");
drop(probe);
let h = std::thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
stream.set_read_timeout(Some(std::time::Duration::from_secs(5))).unwrap();
stream.set_read_timeout(Some(WAIT)).unwrap();
let mut out = stream.try_clone().unwrap();
let mut line = String::new();
BufReader::new(stream).read_line(&mut line).unwrap();
@@ -1239,13 +1263,18 @@ fn remote_call_reports_nothing_sent_when_reconnect_is_refused() {
held.set_reuse_address(true).unwrap();
held.bind(&addr.into()).unwrap();
let start = std::time::Instant::now();
let error = remote.call("engine.execute", json!({"command": "shape.rectangle", "params": {"width": 20, "height": 20}})).unwrap_err();
// Every call connects again: the first after finding the old connection closed, a later one because the failed call left none.
let mut errors = Vec::new();
let took = runs_until_faster_than(FAILED_CONNECT, || {
errors.push(remote.call("engine.execute", json!({"command": "shape.rectangle", "params": {"width": 20, "height": 20}})).unwrap_err());
});
assert!(start.elapsed() < std::time::Duration::from_secs(2));
assert!(error.contains("not reachable"), "{error}");
assert!(error.contains("nothing was sent"), "{error}");
assert!(!error.contains("operation may have completed"), "{error}");
assert!(took.last() < Some(&FAILED_CONNECT), "took {took:?}");
for error in errors {
assert!(error.contains("not reachable"), "{error}");
assert!(error.contains("nothing was sent"), "{error}");
assert!(!error.contains("operation may have completed"), "{error}");
}
}
#[test]