#![forbid(unsafe_code)]
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{self, Command, Stdio};
const TARGET: &str = "wasm32-unknown-unknown";
const RUNNER_HTML: &[u8] = include_bytes!("../../rust/browser/runner.html");
const BROWSER_JS: &[u8] = include_bytes!("../../rust/browser/browser.js");
const USAGE: &str = "cargo vanilla-test --browser [--out-dir PATH] [--manifest-path PATH] [--page PATH]\n\
\n\
Compile the selected package's existing library tests to browser WebAssembly.\n\
Use --page to replace the built-in runner with a compatible browser test page.\n\
Default output: dist/vanilla-test";
#[derive(Debug)]
struct Options {
out_dir: PathBuf,
manifest_path: PathBuf,
page: Option<PathBuf>,
}
fn main() {
if let Err(message) = run(env::args().skip(1)) {
eprintln!("cargo vanilla-test: {message}\n\n{USAGE}");
process::exit(2);
}
}
fn run(arguments: impl Iterator<Item = String>) -> Result<(), String> {
let mut arguments: Vec<_> = arguments.collect();
if arguments
.first()
.is_some_and(|argument| argument == "vanilla-test")
{
arguments.remove(0);
}
if arguments
.iter()
.any(|argument| matches!(argument.as_str(), "--help" | "-h"))
{
println!("{USAGE}");
return Ok(());
}
if arguments
.iter()
.any(|argument| matches!(argument.as_str(), "--version" | "-V"))
{
println!("{}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
let options = parse(arguments.into_iter())?;
let project = options
.manifest_path
.parent()
.ok_or("--manifest-path must name a Cargo.toml file")?;
let page = options
.page
.as_deref()
.map(|path| {
let path = absolute(project, path);
fs::read(&path).map_err(|error| format!("could not read {}: {error}", path.display()))
})
.transpose()?;
let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
let mut command = Command::new(cargo);
command
.current_dir(project)
.args([
"test",
"--release",
"--lib",
"--target",
TARGET,
"--no-run",
"--message-format=json-render-diagnostics",
])
.arg("--manifest-path")
.arg(&options.manifest_path)
.stderr(Stdio::inherit());
let output = command
.output()
.map_err(|error| format!("could not start Cargo: {error}"))?;
if !output.status.success() {
return Err(format!("Cargo exited with {}", output.status));
}
let artifact = wasm_artifact(&output.stdout)?;
let out_dir = absolute(project, &options.out_dir);
fs::create_dir_all(&out_dir)
.map_err(|error| format!("could not create {}: {error}", out_dir.display()))?;
copy(&artifact, &out_dir.join("vanilla-test-tests.wasm"))?;
if let Some(page) = page {
write(&out_dir.join("index.html"), &page)?;
} else {
write(&out_dir.join("index.html"), RUNNER_HTML)?;
}
write(&out_dir.join("browser.js"), BROWSER_JS)?;
println!("Rust browser test ready at {}", out_dir.display());
Ok(())
}
fn parse(mut arguments: impl Iterator<Item = String>) -> Result<Options, String> {
let mut browser = false;
let mut out_dir = PathBuf::from("dist/vanilla-test");
let mut manifest_path = PathBuf::from("Cargo.toml");
let mut page = None;
while let Some(argument) = arguments.next() {
match argument.as_str() {
"--browser" => browser = true,
"--out-dir" => out_dir = value(&mut arguments, "--out-dir")?.into(),
"--manifest-path" => manifest_path = value(&mut arguments, "--manifest-path")?.into(),
"--page" => page = Some(value(&mut arguments, "--page")?.into()),
_ => return Err(format!("unknown option {argument}")),
}
}
if !browser {
return Err("--browser is required".into());
}
if !manifest_path.is_absolute() {
manifest_path = env::current_dir()
.map_err(|error| format!("could not read the current directory: {error}"))?
.join(manifest_path);
}
Ok(Options {
out_dir,
manifest_path,
page,
})
}
fn value(arguments: &mut impl Iterator<Item = String>, option: &str) -> Result<String, String> {
arguments
.next()
.filter(|value| !value.is_empty())
.ok_or_else(|| format!("{option} requires a value"))
}
fn wasm_artifact(output: &[u8]) -> Result<PathBuf, String> {
let output =
std::str::from_utf8(output).map_err(|error| format!("invalid Cargo output: {error}"))?;
output
.lines()
.filter(|line| line.contains("\"reason\":\"compiler-artifact\""))
.filter_map(|line| json_executable(line).map(PathBuf::from))
.find(|path| {
path.extension()
.is_some_and(|extension| extension == "wasm")
})
.ok_or_else(|| "Cargo did not report a WebAssembly test harness".into())
}
fn json_executable(line: &str) -> Option<String> {
const FIELD: &str = "\"executable\":\"";
let mut characters = line[line.find(FIELD)? + FIELD.len()..].chars();
let mut value = String::new();
while let Some(character) = characters.next() {
match character {
'"' => return Some(value),
'\\' => match characters.next()? {
'"' => value.push('"'),
'\\' => value.push('\\'),
'/' => value.push('/'),
'b' => value.push('\u{0008}'),
'f' => value.push('\u{000c}'),
'n' => value.push('\n'),
'r' => value.push('\r'),
't' => value.push('\t'),
_ => return None,
},
_ => value.push(character),
}
}
None
}
fn absolute(project: &Path, path: &Path) -> PathBuf {
if path.is_absolute() {
path.into()
} else {
project.join(path)
}
}
fn copy(source: &Path, destination: &Path) -> Result<(), String> {
fs::copy(source, destination).map(|_| ()).map_err(|error| {
format!(
"could not copy {} to {}: {error}",
source.display(),
destination.display()
)
})
}
fn write(path: &Path, content: &[u8]) -> Result<(), String> {
fs::write(path, content).map_err(|error| format!("could not write {}: {error}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn browser_defaults_need_no_project_configuration() {
let options = parse(["--browser".into()].into_iter()).unwrap();
assert_eq!(options.out_dir, PathBuf::from("dist/vanilla-test"));
assert!(options.manifest_path.ends_with("Cargo.toml"));
assert_eq!(options.page, None);
let options = parse(
["--browser", "--page", "tests/browser.html"]
.map(String::from)
.into_iter(),
)
.unwrap();
assert_eq!(options.page, Some(PathBuf::from("tests/browser.html")));
}
#[test]
fn cargo_output_selects_the_exact_test_artifact() {
let output = br#"{"reason":"compiler-artifact","executable":"C:\\work\\old.exe"}
{"reason":"compiler-artifact","executable":"C:\\work\\suite-a1b2.wasm"}"#;
assert_eq!(
wasm_artifact(output).unwrap(),
PathBuf::from(r"C:\work\suite-a1b2.wasm")
);
}
#[test]
fn browser_flag_is_explicit() {
assert_eq!(
parse(std::iter::empty()).unwrap_err(),
"--browser is required"
);
}
}