makepad/studio/hub/tests/gateway_dispatch_test.rs
Sabin Regmi 0cff3fd7f6
ft makepad_test (#974)
* ft makepad_test

* Improve run handling, manifest parsing, and stdout newline

Replace dynamic free-port lookup with an ephemeral localhost SocketAddr in test runtime and remove the unused find_free_listen_address helper. Ensure headless stdout messages end with a newline. Simplify send_to_app error handling and add a test that queued bootstrap messages are delivered once an app socket connects. Substantially enhance process_manager: unify cargo flag parsing, parse Cargo.toml to determine package/bin targets, resolve the correct binary name for direct stdio runs, and build the cargo/build+exec script from the resolved args. Add unit tests for manifest parsing and script generation and adjust related call sites.

* test harness

* Preserve test attrs; return Vec for gateway binds

In the test macro (libs/makepad_test/macros/src/lib.rs) preserve wrapper-only attributes (ignore and should_panic) on the generated wrapper test while removing them from the inner function. Added Attribute import, is_wrapper_only_test_attr helper, adjusted attribute filtering and emission, and added unit tests to verify attribute placement and expansion.

In the hub (studio/hub/src/hub.rs) change gateway_bind_candidates to return a Vec<SocketAddr> instead of an iterator and special-case ephemeral port 0 to preserve ephemeral binding; otherwise collect the range of candidate ports into a Vec. Added tests to validate candidate behavior. Also minor formatting/whitespace tweaks and a small IPv6 formatting adjustment.

* Add visible Studio mode and remote client

Enable running UI tests visibly through a running Makepad Studio. Adds a new makepad-network dependency and studio_remote client (libs/makepad_test/src/studio_remote.rs) and integrates it into the runtime via a TestConnection enum. Introduces visible-mode tooling: env vars (MAKEPAD_TEST_VISIBLE, MAKEPAD_TEST_STUDIO, MAKEPAD_TEST_STUDIO_MOUNT, MAKEPAD_TEST_STARTUP_DELAY_MS, MAKEPAD_TEST_ACTION_DELAY_MS, MAKEPAD_TEST_KEEP_OPEN_MS), pacing/delays after actions, and pause-before-shutdown. Splits startup into start_headless_app/start_visible_app, clears existing visible builds before launching, and updates tests, docs (GUIDE.md, README.md), and selector/runtime minor cleanups/formatting.
2026-03-25 16:09:03 +01:00

131 lines
4.2 KiB
Rust

use makepad_live_id::LiveId;
use makepad_micro_serde::{DeBin, SerBin};
use makepad_script_std::makepad_network::{
HttpMethod, HttpRequest, NetworkConfig, NetworkResponse, NetworkRuntime, WsMessage, WsSend,
};
use makepad_studio_hub::{HubConfig, MountConfig, StudioHub};
use makepad_studio_protocol::hub_protocol::{
ClientId, ClientToHub, ClientToHubEnvelope, HubToClient, QueryId,
};
use std::fs;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener};
use std::time::{Duration, Instant};
fn find_free_port() -> Option<u16> {
let listener = TcpListener::bind(("127.0.0.1", 0)).ok()?;
Some(listener.local_addr().ok()?.port())
}
fn wait_for_event<F>(
runtime: &NetworkRuntime,
timeout: Duration,
mut matcher: F,
) -> Option<NetworkResponse>
where
F: FnMut(&NetworkResponse) -> bool,
{
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if let Some(event) = runtime.recv_timeout(Duration::from_millis(50)) {
if matcher(&event) {
return Some(event);
}
}
}
None
}
fn wait_for_ws_binary(runtime: &NetworkRuntime, socket_id: LiveId, timeout: Duration) -> Vec<u8> {
let event = wait_for_event(runtime, timeout, |event| {
matches!(
event,
NetworkResponse::WsMessage {
socket_id: id,
message: WsMessage::Binary(_)
} if *id == socket_id
)
})
.expect("did not receive ws binary message");
match event {
NetworkResponse::WsMessage {
message: WsMessage::Binary(data),
..
} => data,
_ => unreachable!(),
}
}
#[test]
fn websocket_ui_hello_and_load_file_tree_roundtrip() {
let dir = makepad_studio_hub::test_support::tempdir().unwrap();
fs::create_dir_all(dir.path().join("src")).unwrap();
fs::write(dir.path().join("src/lib.rs"), "fn main() {}\n").unwrap();
let Some(port) = find_free_port() else {
return;
};
let config = HubConfig {
listen_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port),
post_max_size: 1024 * 1024,
mounts: vec![MountConfig {
name: "repo".to_string(),
path: dir.path().to_path_buf(),
}],
enable_in_process_gateway: false,
};
let _backend = match StudioHub::start_headless(config) {
Ok(v) => v,
Err(err) => {
if err.contains("failed to bind") {
return;
}
panic!("start backend failed: {}", err);
}
};
let runtime = NetworkRuntime::new(NetworkConfig::default());
let socket_id = LiveId::from_str("studio2.backend.gateway.test");
let request = HttpRequest::new(format!("ws://127.0.0.1:{port}/ui"), HttpMethod::GET);
if runtime.ws_open(socket_id, request).is_err() {
return;
}
let opened = wait_for_event(
&runtime,
Duration::from_secs(3),
|event| matches!(event, NetworkResponse::WsOpened { socket_id: id } if *id == socket_id),
);
assert!(opened.is_some(), "did not receive WsOpened");
let hello_bin = wait_for_ws_binary(&runtime, socket_id, Duration::from_secs(3));
let hello = HubToClient::deserialize_bin(&hello_bin).expect("decode hello");
let client_id = match hello {
HubToClient::Hello { client_id } => client_id,
other => panic!("expected hello, got {:?}", other),
};
assert_ne!(client_id, ClientId(u16::MAX));
let envelope = ClientToHubEnvelope {
query_id: QueryId::new(client_id, 0),
msg: ClientToHub::LoadFileTree {
mount: "repo".to_string(),
},
};
runtime
.ws_send(socket_id, WsSend::Binary(envelope.serialize_bin()))
.expect("ws_send");
let tree_bin = wait_for_ws_binary(&runtime, socket_id, Duration::from_secs(3));
let tree_msg = HubToClient::deserialize_bin(&tree_bin).expect("decode tree");
match tree_msg {
HubToClient::FileTree { mount, data } => {
assert_eq!(mount, "repo");
assert!(data.nodes.iter().any(|n| n.path == "repo/src/lib.rs"));
}
other => panic!("expected FileTree, got {:?}", other),
}
let _ = runtime.ws_close(socket_id);
}