use std::{
env, fs,
path::{Path, PathBuf},
process::{Command, Output},
time::SystemTime,
};
const CRATE_ROOT: &str = env!("CARGO_MANIFEST_DIR");
fn toolchain_available(toolchain: &str) -> bool {
Command::new("cargo")
.arg(format!("+{toolchain}"))
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
struct Fixture {
dir: PathBuf,
pkg: String,
toolchain: String,
}
impl Fixture {
fn scaffold(tag: &str, with_build_rs: bool, toolchain: &str) -> Self {
let dir = env::temp_dir().join(format!("tf-track-{}-{tag}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
let client = dir.join("client");
fs::create_dir_all(client.join("src")).unwrap();
fs::create_dir_all(client.join("node_modules")).unwrap();
fs::write(client.join("package.json"), "{}\n").unwrap();
let pkg = format!("tf_fixture_{tag}");
fs::write(dir.join("Cargo.toml"), cargo_toml(&pkg, with_build_rs)).unwrap();
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(dir.join("src/main.rs"), MAIN_RS).unwrap();
if with_build_rs {
fs::write(dir.join("build.rs"), BUILD_RS).unwrap();
}
fs::write(
dir.join(".gitignore"),
"/dist\n/client/dist\n/client/node_modules\n",
)
.unwrap();
Fixture {
dir,
pkg,
toolchain: toolchain.to_string(),
}
}
fn client(&self) -> PathBuf {
self.dir.join("client")
}
fn set_content(&self, token: &str) {
fs::write(self.client().join("src/content.txt"), format!("{token}\n")).unwrap();
}
fn target_dir(&self) -> PathBuf {
self.dir.join("target")
}
fn build(&self) -> Output {
self.cargo_build(&[])
}
fn build_all_targets(&self) -> Output {
self.cargo_build(&["--all-targets"])
}
fn cargo_build(&self, extra: &[&str]) -> Output {
Command::new("cargo")
.arg(format!("+{}", self.toolchain))
.arg("build")
.args(extra)
.current_dir(&self.dir)
.env("CARGO_TARGET_DIR", self.target_dir())
.output()
.expect("failed to spawn cargo")
}
fn build_count(&self) -> usize {
fs::read_to_string(self.client().join("node_modules/build_count"))
.map(|counter| counter.lines().count())
.unwrap_or(0)
}
fn binary(&self) -> PathBuf {
let name = if cfg!(windows) {
format!("{}.exe", self.pkg)
} else {
self.pkg.clone()
};
self.target_dir().join("debug").join(name)
}
fn binary_contains(&self, needle: &[u8]) -> bool {
let bytes = fs::read(self.binary()).expect("read fixture binary");
bytes.windows(needle.len()).any(|w| w == needle)
}
fn dist_index_mtime(&self) -> SystemTime {
fs::metadata(self.client().join("dist/index.html"))
.expect("dist/index.html should exist after a build")
.modified()
.unwrap()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
if env::var_os("TF_TRACK_KEEP").is_none() {
let _ = fs::remove_dir_all(&self.dir);
}
}
}
fn cargo_toml(pkg: &str, with_build_rs: bool) -> String {
let root = Path::new(CRATE_ROOT).display();
let build_deps = if with_build_rs {
format!("\n[build-dependencies]\ntrillium-frontend = {{ path = \"{root}\" }}\n")
} else {
String::new()
};
format!(
"[package]\nname = \"{pkg}\"\nversion = \"0.0.0\"\nedition = \"2024\"\npublish = \
false\n\n# Own workspace so cargo doesn't walk up the temp dir looking for \
one.\n[workspace]\n\n[dependencies]\ntrillium-frontend = {{ path = \"{root}\" \
}}\n{build_deps}"
)
}
const MAIN_RS: &str = r#"use trillium_frontend::frontend;
fn main() {
std::hint::black_box(frontend!(
path = "./client",
build = "mkdir -p dist && cp src/content.txt dist/index.html && echo ran >> node_modules/build_count",
dist = "dist"
));
}
"#;
const BUILD_RS: &str = "fn main() {\n trillium_frontend::build::track_frontend_sources();\n}\n";
fn assert_build_ok(out: &Output, context: &str) {
assert!(
out.status.success(),
"cargo build failed ({context}):\n{}",
String::from_utf8_lossy(&out.stderr)
);
}
fn run_scenario(toolchain: &str, with_build_rs: bool) {
if !toolchain_available(toolchain) {
eprintln!("SKIP: `{toolchain}` toolchain not installed");
return;
}
let tag = format!(
"{toolchain}_{}",
if with_build_rs { "brs" } else { "nobrs" }
);
let fx = Fixture::scaffold(&tag, with_build_rs, toolchain);
let alpha = format!("TF_TOKEN_{tag}_ALPHA");
fx.set_content(&alpha);
assert_build_ok(&fx.build(), "initial build");
assert_build_ok(&fx.build(), "arming rebuild");
assert!(
fx.binary_contains(alpha.as_bytes()),
"initial build did not embed the expected assets ({tag})"
);
let before = fx.dist_index_mtime();
assert_build_ok(&fx.build(), "no-op rebuild");
assert_eq!(
before,
fx.dist_index_mtime(),
"frontend build re-ran on a no-op `cargo build` — over-build ({tag})"
);
let bravo = format!("TF_TOKEN_{tag}_BRAVO");
fx.set_content(&bravo);
assert_build_ok(&fx.build(), "rebuild after source edit");
assert!(
fx.binary_contains(bravo.as_bytes()),
"STALE: a frontend source edit was not reflected in the embedded assets ({tag})"
);
}
#[test]
#[ignore = "runs a real cargo build; needs the nightly toolchain"]
fn nightly_native_tracking() {
run_scenario("nightly", false);
}
#[test]
#[ignore = "runs a real cargo build; needs the stable toolchain"]
fn stable_shim_tracking() {
run_scenario("stable", true);
}
#[test]
#[ignore = "runs a real cargo build; needs the nightly toolchain"]
fn nightly_with_shim_present() {
run_scenario("nightly", true);
}
#[test]
#[ignore = "runs a real cargo build; needs the nightly toolchain"]
fn concurrent_expansion_builds_once() {
if !toolchain_available("nightly") {
eprintln!("SKIP: `nightly` toolchain not installed");
return;
}
let fx = Fixture::scaffold("concurrent", false, "nightly");
fx.set_content("TF_TOKEN_concurrent");
assert_build_ok(&fx.build_all_targets(), "concurrent --all-targets build");
assert_eq!(
fx.build_count(),
1,
"one `frontend!` expanded concurrently ran the frontend build {} times; concurrent \
expansions must be serialized and redundant builds skipped, or they clobber each other's \
dist",
fx.build_count()
);
}