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}