Skip to main content

xtask/
test.rs

1//! `cargo xtask test`: nextest over the workspace, then the doctests.
2
3use crate::step;
4
5/// The environment variable that selects a test device's tier.
6///
7/// Defined by `unlit_wgpu::capabilities::DeviceTier`, and repeated here as a
8/// literal because this is a separate crate that does not depend on it.
9const TIER_ENV: &str = "UNLIT3D_DEVICE_TIER";
10
11/// The value that asks for the WebGL2 tier.
12const WEBGL2_TIER: &str = "webgl2";
13
14/// The environment variable that collects mismatched snapshot frames.
15///
16/// Defined by `unlit_wgpu_test_util`, and repeated here as a literal because
17/// this is a separate crate that does not depend on it.
18const MISMATCH_DIR_ENV: &str = "UNLIT3D_SNAPSHOT_MISMATCH_DIR";
19
20/// Where the mismatched frames of a tier are collected, relative to the
21/// workspace root.
22///
23/// An absolute path is passed to the tests because they run with their own
24/// package as the working directory, so a relative one would put each package's
25/// frames somewhere different. Collecting them in one directory is what lets
26/// CI upload every frame that failed from a single path.
27fn mismatch_dir(tier: Option<&str>) -> Result<std::path::PathBuf, String> {
28    let root = std::env::current_dir()
29        .map_err(|error| format!("reading the working directory: {error}"))?;
30    let name = match tier {
31        Some(tier) => format!("snapshot-mismatches-{tier}"),
32        None => "snapshot-mismatches".to_owned(),
33    };
34    Ok(root.join("target").join(name))
35}
36
37/// Run the workspace's tests.
38///
39/// `cargo nextest run` is the test runner: it gives every test its own
40/// process, so one test's device, logger or panic cannot reach another's.
41/// Nextest does not run doctests, so those get a `cargo test --doc` pass of
42/// their own — the two commands together are what CI runs.
43///
44/// The targets are named one by one rather than with `--all-targets`, which
45/// would also sweep in the benchmarks. A benchmark binary is not a test
46/// binary: it has no test harness and its `main` is the benchmark, so nextest
47/// cannot list it — `cargo bench` is what runs those.
48///
49/// The suite runs twice: once with the default tier — the WebGPU baseline's
50/// limits, which every implementation guarantees — and once with
51/// `UNLIT3D_DEVICE_TIER=webgl2`, which narrows every headless device to the
52/// limits and missing downlevel capabilities WebGL2 has. That second pass is
53/// what exercises the paths a browser takes — the shader's arrays read through
54/// textures instead of storage buffers, and a draw's vertex offset baked into
55/// its indices instead of carried in `base_vertex` — on a machine whose native
56/// backend is Vulkan, Metal or DX12. A real WebGL2 device is not required for
57/// it, and cannot be, since none is reachable from a headless test.
58pub fn run(release: bool) -> Result<(), String> {
59    let profile: &[&str] = if release { &["--release"] } else { &[] };
60
61    nextest(profile, None)?;
62
63    // The tier pass is the one that can fail on a path the plain pass never
64    // reaches, so it reports its own name rather than looking like a rerun.
65    nextest(profile, Some(WEBGL2_TIER))?;
66
67    let mut doctests = std::process::Command::new("cargo");
68    doctests
69        .args(["test", "--workspace", "--all-features", "--doc"])
70        .args(profile);
71    step::run(&mut doctests, "doctests")?;
72
73    Ok(())
74}
75
76/// Run the workspace's tests through nextest, optionally under a device tier.
77///
78/// Each pass collects its mismatched snapshot frames in a directory of its own,
79/// so a run that fails under both tiers keeps them apart, and CI can upload
80/// whatever was written however far the run got.
81fn nextest(profile: &[&str], tier: Option<&str>) -> Result<(), String> {
82    let mut command = std::process::Command::new("cargo");
83    command
84        .args([
85            "nextest",
86            "run",
87            "--workspace",
88            "--lib",
89            "--bins",
90            "--tests",
91            "--all-features",
92        ])
93        .args(profile)
94        .env(MISMATCH_DIR_ENV, mismatch_dir(tier)?);
95    let named = match tier {
96        Some(tier) => {
97            command.env(TIER_ENV, tier);
98            format!("nextest ({tier})")
99        }
100        None => "nextest".to_owned(),
101    };
102    step::run(&mut command, &named)
103}