Skip to main content

unlit_wgpu_test_util/
proxy.rs

1//! The native proxy that runs each test in a browser instead of in-process.
2//!
3//! When [`WASM_TEST_ENV`] is set, the test binary stops being a runner and
4//! becomes a *client*: `cargo nextest` still lists and drives it, but each trial
5//! makes an HTTP request to the local runner, which opens the page and runs that
6//! one test in a real browser. The verdict comes back as the response's status,
7//! so nextest reports a browser failure exactly as it reports an in-process one
8//! — same listing, same filters, same exit code.
9//!
10//! The mode is chosen from the environment rather than by a `cfg`.
11//!
12//! This is why the trial names carry a `[wasm]` marker: the `wasm` nextest
13//! profile selects them with `default-filter = "test([wasm])"`, so a single
14//! `cargo nextest run` drives only the browser side and never the in-process
15//! one.
16
17use crate::TestEntry;
18
19/// The environment variable that puts a test binary in proxy mode.
20///
21/// Any value selects it. The module the page loads is a compile-time constant
22/// of each test binary rather than part of this variable, since one value shared
23/// by a whole `cargo nextest run` could not say which binary is asking.
24pub const WASM_TEST_ENV: &str = "UNLIT3D_WASM_TEST";
25
26/// The port the browser runner listens on.
27///
28/// Fixed so the two halves agree without configuration, and because the runner
29/// is started and stopped within one `cargo xtask test-wasm`.
30const PORT: u16 = 3000;
31
32/// The marker every trial name carries, and what the `wasm` nextest profile
33/// filters on.
34const WASM_MARKER: &str = "[wasm]";
35
36/// Run every registered test in the browser, reporting through `libtest-mimic`.
37///
38/// `wasm_module` names the wasm binary the page should load.
39///
40/// Never returns: it exits the process with libtest's status.
41pub fn run_wasm(tests: Vec<TestEntry>, wasm_module: &'static str) -> ! {
42    let args = libtest_mimic::Arguments::from_args();
43    // A host-only test is not in the wasm build, so a browser could not run
44    // it. The host build is the one nextest lists trials from, so dropping it
45    // here is what keeps the two sides' listings equal.
46    let trials = tests
47        .into_iter()
48        .filter(|entry| !entry.host_only)
49        .map(|entry| {
50            // The marker leads, so the profile's filter matches on it without
51            // having to know any test's name.
52            let trial_name = format!("{WASM_MARKER} {}", entry.name);
53            libtest_mimic::Trial::test(trial_name, move || run_in_browser(wasm_module, entry.name))
54        })
55        .collect();
56
57    libtest_mimic::run(&args, trials).exit()
58}
59
60/// Ask the runner to run one test in a browser, and report what it said.
61fn run_in_browser(wasm_module: &str, name: &str) -> Result<(), libtest_mimic::Failed> {
62    // A failing test answers with a 5xx and its message in the body, so the
63    // status is the verdict and the body is the explanation. Left as a response
64    // rather than turned into an error, which would discard the body.
65    let agent: ureq::Agent = ureq::Agent::config_builder()
66        .http_status_as_error(false)
67        .build()
68        .into();
69
70    let mut response = agent
71        .get(&format!("http://127.0.0.1:{PORT}/run_test"))
72        .query("wasm", wasm_module)
73        .query("name", name)
74        .call()?;
75
76    if response.status().is_success() {
77        return Ok(());
78    }
79    let message = response
80        .body_mut()
81        .read_to_string()
82        .unwrap_or_else(|error| format!("the runner's failure message could not be read: {error}"));
83    Err(libtest_mimic::Failed::from(message))
84}