supercov_engine/
rust_wasm_pack.rs1use std::{
15 fs,
16 path::{Path, PathBuf},
17 process::Command,
18};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct WasmPackTest {
23 pub command: Vec<String>,
25 pub runner_environment: Vec<(String, String)>,
28}
29
30const RUNNER_VARIABLES: &[&str] = &[
32 "CHROMEDRIVER",
33 "CHROMEDRIVER_ARGS",
34 "GECKODRIVER",
35 "GECKODRIVER_ARGS",
36 "SAFARIDRIVER",
37 "SAFARIDRIVER_ARGS",
38 "MSEDGEDRIVER",
39 "MSEDGEDRIVER_ARGS",
40 "NO_HEADLESS",
41 "WASM_BINDGEN_TEST_ONLY_WEB",
42 "WASM_BINDGEN_TEST_ONLY_NODE",
43];
44
45pub fn is_wasm_pack_test(command: &[String]) -> bool {
47 command.first().is_some_and(|program| {
48 Path::new(program)
49 .file_stem()
50 .is_some_and(|name| name == "wasm-pack")
51 }) && command.get(1).is_some_and(|word| word == "test")
52}
53
54pub fn learn(root: &Path, command: &[String], scratch: &Path) -> Result<WasmPackTest, String> {
57 if cfg!(windows) {
58 return Err("`wasm-pack test` is measured on Linux and macOS; on Windows, run the `cargo test --target wasm32-unknown-unknown` it runs".into());
59 }
60 let cargo = which("cargo").ok_or("no `cargo` on PATH for `wasm-pack test`")?;
61 let bin = scratch.join("wasm-pack-cargo");
62 fs::create_dir_all(&bin).map_err(|error| format!("{}: {error}", bin.display()))?;
63 let record = scratch.join("wasm-pack-test.txt");
64 let _ = fs::remove_file(&record);
65 write_cargo(&bin, &cargo, &record)?;
66 let mut path = vec![bin.clone()];
67 path.extend(std::env::split_paths(
68 &std::env::var_os("PATH").unwrap_or_default(),
69 ));
70 let output = Command::new(&command[0])
71 .args(&command[1..])
72 .current_dir(root)
73 .env(
74 "PATH",
75 std::env::join_paths(path).map_err(|error| error.to_string())?,
76 )
77 .output()
78 .map_err(|error| format!("could not run {}: {error}", command[0]))?;
79 let text = fs::read_to_string(&record).map_err(|_| {
80 let said = String::from_utf8_lossy(&output.stderr);
81 let tail = said.lines().rev().take(8).collect::<Vec<_>>();
82 format!(
83 "`wasm-pack test` stopped before it ran `cargo test`:\n{}",
84 tail.into_iter().rev().collect::<Vec<_>>().join("\n")
85 )
86 })?;
87 parse_record(root, &text)
88}
89
90fn parse_record(root: &Path, text: &str) -> Result<WasmPackTest, String> {
93 let mut lines = text.lines();
94 let directory = PathBuf::from(lines.next().ok_or("an empty wasm-pack record")?);
95 let mut arguments = Vec::new();
96 let mut environment = Vec::new();
97 let mut in_environment = false;
98 for line in lines {
99 if line == "--env" {
100 in_environment = true;
101 } else if in_environment {
102 if let Some((name, value)) = line.split_once('=') {
103 environment.push((name.to_owned(), value.to_owned()));
104 }
105 } else {
106 arguments.push(line.to_owned());
107 }
108 }
109 let mut command = vec!["cargo".to_owned()];
110 command.extend(arguments);
111 let mut runner_environment = Vec::new();
113 for (name, value) in environment {
114 if let Some(target) = name
115 .strip_prefix("CARGO_TARGET_")
116 .and_then(|rest| rest.strip_suffix("_RUNNER"))
117 {
118 let triple = target.to_ascii_lowercase().replace('_', "-");
119 command.push("--config".into());
120 command.push(format!(
123 "target.{triple}.runner={}",
124 serde_json::to_string(&value).map_err(|error| error.to_string())?
125 ));
126 } else if RUNNER_VARIABLES.contains(&name.as_str()) {
127 runner_environment.push((name, value));
128 }
129 }
130 let canonical_root = fs::canonicalize(root).unwrap_or_else(|_| root.to_owned());
132 let canonical_directory = fs::canonicalize(&directory).unwrap_or(directory);
133 if canonical_directory != canonical_root
134 && let Ok(relative) = canonical_directory.strip_prefix(&canonical_root)
135 {
136 command.push("--manifest-path".into());
137 command.push(relative.join("Cargo.toml").to_string_lossy().into_owned());
138 }
139 Ok(WasmPackTest {
140 command,
141 runner_environment,
142 })
143}
144
145fn write_cargo(bin: &Path, cargo: &Path, record: &Path) -> Result<(), String> {
146 #[cfg(unix)]
147 {
148 use std::os::unix::fs::PermissionsExt;
149 let quote = |path: &Path| format!("'{}'", path.to_string_lossy().replace('\'', r"'\''"));
150 let variables = RUNNER_VARIABLES.join("|");
151 let script = format!(
152 r#"#!/bin/sh
153# Written by Supercov: records the `cargo test` wasm-pack runs, instead of
154# running it, and skips the `cargo build --tests` that only warms a cache.
155case "$1" in
156 test)
157 {{
158 pwd
159 for argument in "$@"; do printf '%s\n' "$argument"; done
160 echo --env
161 env | grep -E '^(CARGO_TARGET_[A-Z0-9_]+_RUNNER|{variables})='
162 }} > {record}
163 exit 0
164 ;;
165 build) exit 0 ;;
166esac
167exec {cargo} "$@"
168"#,
169 record = quote(record),
170 cargo = quote(cargo),
171 );
172 let path = bin.join("cargo");
173 fs::write(&path, script).map_err(|error| format!("{}: {error}", path.display()))?;
174 fs::set_permissions(&path, fs::Permissions::from_mode(0o755))
175 .map_err(|error| format!("{}: {error}", path.display()))?;
176 }
177 #[cfg(not(unix))]
178 {
179 let _ = (bin, cargo, record);
180 }
181 Ok(())
182}
183
184fn which(name: &str) -> Option<PathBuf> {
185 std::env::split_paths(&std::env::var_os("PATH")?)
186 .map(|directory| directory.join(name))
187 .find(|candidate| fs::metadata(candidate).is_ok_and(|metadata| metadata.is_file()))
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn only_wasm_pack_test_is_learned() {
196 let words = |text: &str| text.split(' ').map(str::to_owned).collect::<Vec<_>>();
197 assert!(is_wasm_pack_test(&words(
198 "wasm-pack test --headless --chrome"
199 )));
200 assert!(is_wasm_pack_test(&words("/usr/bin/wasm-pack test --node")));
201 assert!(!is_wasm_pack_test(&words("wasm-pack build")));
202 assert!(!is_wasm_pack_test(&words("cargo test")));
203 }
204
205 #[test]
208 fn a_record_becomes_the_cargo_test_it_stands_for() {
209 let root = std::env::temp_dir().join(format!("supercov-wasm-pack-{}", std::process::id()));
210 fs::create_dir_all(root.join("crates/web")).unwrap();
211 let record = format!(
212 "{}\ntest\n--target\nwasm32-unknown-unknown\n--env\nCARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=/c/wasm-bindgen-test-runner\nCHROMEDRIVER=/c/chromedriver\nWASM_BINDGEN_TEST_ONLY_WEB=1\n",
213 root.join("crates/web").display()
214 );
215 let learned = parse_record(&root, &record).unwrap();
216 let manifest = Path::new("crates").join("web").join("Cargo.toml");
218 assert_eq!(
219 learned.command,
220 [
221 "cargo",
222 "test",
223 "--target",
224 "wasm32-unknown-unknown",
225 "--config",
226 "target.wasm32-unknown-unknown.runner=\"/c/wasm-bindgen-test-runner\"",
227 "--manifest-path",
228 &manifest.to_string_lossy(),
229 ]
230 );
231 assert_eq!(
232 learned.runner_environment,
233 [
234 ("CHROMEDRIVER".to_owned(), "/c/chromedriver".to_owned()),
235 ("WASM_BINDGEN_TEST_ONLY_WEB".to_owned(), "1".to_owned()),
236 ]
237 );
238 fs::remove_dir_all(&root).unwrap();
239 }
240}