mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
Merge pull request #4478 from dotCooCoo/fix/mcp-remote-tests-load-waits
Tests: the MCP remote-backend tests pass on a loaded machine
This commit is contained in:
+70
-30
@@ -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();
|
||||
@@ -1228,24 +1252,40 @@ fn remote_does_not_send_on_a_connection_with_a_stray_line() {
|
||||
|
||||
#[test]
|
||||
fn remote_call_reports_nothing_sent_when_reconnect_is_refused() {
|
||||
let listener = reusable_listener(std::net::SocketAddr::from(([127, 0, 0, 1], 0)));
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let mut remote = Remote::connect(&addr.to_string()).unwrap();
|
||||
let (old_connection, _) = listener.accept().unwrap();
|
||||
drop(old_connection);
|
||||
drop(listener);
|
||||
wait_for_remote_eof(&remote);
|
||||
let held = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, None).unwrap();
|
||||
held.set_reuse_address(true).unwrap();
|
||||
held.bind(&addr.into()).unwrap();
|
||||
// A remote whose connection the app closed, and the app's port bound but not listening.
|
||||
let closed_remote = || {
|
||||
let listener = reusable_listener(std::net::SocketAddr::from(([127, 0, 0, 1], 0)));
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let remote = Remote::connect(&addr.to_string()).unwrap();
|
||||
let (old_connection, _) = listener.accept().unwrap();
|
||||
drop(old_connection);
|
||||
drop(listener);
|
||||
wait_for_remote_eof(&remote);
|
||||
let held = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, None).unwrap();
|
||||
held.set_reuse_address(true).unwrap();
|
||||
held.bind(&addr.into()).unwrap();
|
||||
(remote, held)
|
||||
};
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let error = remote.call("engine.execute", json!({"command": "shape.rectangle", "params": {"width": 20, "height": 20}})).unwrap_err();
|
||||
// Each run finds the old connection closed and connects again; only the call is timed.
|
||||
let mut errors = Vec::new();
|
||||
let mut took = Vec::new();
|
||||
while took.len() < 5 {
|
||||
let (mut remote, _held) = closed_remote();
|
||||
let start = std::time::Instant::now();
|
||||
errors.push(remote.call("engine.execute", json!({"command": "shape.rectangle", "params": {"width": 20, "height": 20}})).unwrap_err());
|
||||
took.push(start.elapsed());
|
||||
if took.last() < Some(&FAILED_CONNECT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user