use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
fn snapshot(dir: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
fn walk(dir: &Path, base: &Path, out: &mut BTreeMap<PathBuf, Vec<u8>>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk(&path, base, out);
} else if let Ok(bytes) = fs::read(&path) {
let relative = path.strip_prefix(base).unwrap_or(&path).to_path_buf();
out.insert(relative, bytes);
}
}
}
let mut out = BTreeMap::new();
walk(dir, dir, &mut out);
out
}
fn write_fixture(src: &Path, assets: &Path) {
fs::create_dir_all(src.join("nested")).unwrap();
fs::write(
src.join("a.css"),
b"body {\n color: red;\n}\n/* a comment */\n",
)
.unwrap();
fs::write(src.join("nested/b.css"), b".nested {\n margin: 0;\n}\n").unwrap();
fs::write(src.join("already.min.css"), b".m{padding:0}").unwrap();
fs::write(src.join("script.js"), b"export const x = 1;\n").unwrap();
fs::write(src.join("already.min.js"), b"const y=2;").unwrap();
fs::create_dir_all(assets.join("img")).unwrap();
fs::write(assets.join("robots.txt"), b"User-agent: *\n").unwrap();
fs::write(assets.join("img/pixel.bin"), [0u8, 1, 2, 3, 255]).unwrap();
}
async fn build_both() -> (BTreeMap<PathBuf, Vec<u8>>, BTreeMap<PathBuf, Vec<u8>>) {
let src = TempDir::new().unwrap();
let assets = TempDir::new().unwrap();
write_fixture(src.path(), assets.path());
let reference_out = TempDir::new().unwrap();
let subject_out = TempDir::new().unwrap();
mini_static::Server::new(reference_out.path())
.unwrap()
.with_source_folder(src.path())
.unwrap()
.with_asset_folder(assets.path())
.unwrap()
.with_css_tool(
mini_static::CssTool::LightningCss,
mini_static::CssOptions::default(),
)
.with_js_tool(
mini_static::JsTool::Esbuild,
mini_static::JsOptions::default(),
)
.unwrap()
.build()
.await
.expect("reference build");
mini_build::Builder::new(subject_out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.asset_folder(assets.path())
.unwrap()
.css_tool(
mini_build::CssTool::LightningCss,
mini_build::CssOptions::default(),
)
.js_tool(
mini_build::JsTool::Esbuild,
mini_build::JsOptions::default(),
)
.unwrap()
.build()
.expect("subject build");
(snapshot(reference_out.path()), snapshot(subject_out.path()))
}
#[tokio::test]
async fn mini_build_reproduces_mini_statics_output_byte_for_byte() {
let (reference, subject) = build_both().await;
assert!(
!reference.is_empty(),
"the reference build produced nothing, so this test would pass vacuously"
);
let reference_paths: Vec<_> = reference.keys().collect();
let subject_paths: Vec<_> = subject.keys().collect();
assert_eq!(
reference_paths, subject_paths,
"the two builds produced different sets of files"
);
for (path, reference_bytes) in &reference {
assert_eq!(
subject.get(path),
Some(reference_bytes),
"{} differs between the two implementations",
path.display()
);
}
}
#[tokio::test]
async fn already_minified_sources_are_copied_unchanged_by_both() {
let (reference, subject) = build_both().await;
for name in ["already.min.css", "already.min.js"] {
let path = PathBuf::from(name);
assert_eq!(
reference.get(&path).map(Vec::as_slice),
subject.get(&path).map(Vec::as_slice),
"{name} disagrees between implementations"
);
}
assert_eq!(
subject.get(&PathBuf::from("already.min.css")).unwrap(),
b".m{padding:0}",
"an already-minified source must reach the output untouched"
);
}
#[tokio::test]
async fn assets_are_mirrored_identically_by_both() {
let (reference, subject) = build_both().await;
let pixel = PathBuf::from("img/pixel.bin");
assert_eq!(
reference.get(&pixel),
subject.get(&pixel),
"binary asset differs between implementations"
);
assert_eq!(
subject.get(&pixel).map(Vec::as_slice),
Some([0u8, 1, 2, 3, 255].as_slice()),
"binary bytes must survive the copy exactly"
);
}