use super::*;
use std::time::Duration;
use tempfile::TempDir;
#[test]
fn pipeline_does_not_rebroadcast_its_own_output_echo() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
std::fs::write(src.path().join("style.css"), "body { margin: 0; }").unwrap();
let broadcaster = Broadcaster::new();
let pipeline = SourcePipeline::new(
vec![src.path().to_path_buf()],
Vec::new(),
Vec::new(),
out.path().to_path_buf(),
Some((CssTool::TestEcho, CssOptions::new().bundle(true))),
None,
false,
broadcaster.clone(),
);
let pipeline_rx = broadcaster.subscribe();
let observer = broadcaster.subscribe();
broadcaster.broadcast(ChangeEvent {
path: src.path().join("style.css"),
change_type: ChangeType::Css,
});
const MAX_ITERATIONS: usize = 10;
let mut iterations = 0usize;
while let Ok(event) = pipeline_rx.recv_timeout(Duration::from_millis(200)) {
iterations += 1;
assert!(
iterations <= MAX_ITERATIONS,
"the pipeline is feeding on its own output: still producing events after \
{MAX_ITERATIONS} rounds"
);
if let Err(e) = pipeline.process_change(&event.path, &event.change_type) {
eprintln!("source pipeline error: {e}");
}
}
let mut count = 0usize;
while observer.try_recv().is_ok() {
count += 1;
}
assert_eq!(
count, 2,
"a single source css change must emit exactly the source event plus the bundle \
broadcast, and then stop — not feed back forever (got {count})"
);
}
#[test]
fn css_bundle_mode_concatenates_into_the_configured_output_name() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
std::fs::write(src.path().join("a.css"), "A").unwrap();
let pipeline = SourcePipeline::new(
vec![src.path().to_path_buf()],
Vec::new(),
Vec::new(),
out.path().to_path_buf(),
Some((
CssTool::TestEcho,
CssOptions::new()
.bundle(true)
.bundle_output_name("main.css"),
)),
None,
false,
Broadcaster::new(),
);
pipeline.full_build().unwrap();
assert_eq!(fs_read(out.path().join("main.css")), "A");
}
#[test]
fn css_per_file_mode_mirrors_every_source_file() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
std::fs::write(src.path().join("a.css"), "A").unwrap();
std::fs::write(src.path().join("b.css"), "B").unwrap();
let pipeline = SourcePipeline::new(
vec![src.path().to_path_buf()],
Vec::new(),
Vec::new(),
out.path().to_path_buf(),
Some((CssTool::TestEcho, CssOptions::new())),
None,
false,
Broadcaster::new(),
);
pipeline.full_build().unwrap();
assert_eq!(fs_read(out.path().join("a.css")), "A");
assert_eq!(fs_read(out.path().join("b.css")), "B");
}
#[test]
fn js_bundle_mode_writes_the_entry_through_the_tool() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
let entry = src.path().join("main.js");
std::fs::write(&entry, "const x = 1;").unwrap();
let pipeline = SourcePipeline::new(
vec![src.path().to_path_buf()],
Vec::new(),
Vec::new(),
out.path().to_path_buf(),
None,
Some((
JsTool::TestEcho,
JsOptions::new().bundle_entry(&entry, "bundle.js"),
)),
false,
Broadcaster::new(),
);
pipeline.full_build().unwrap();
assert_eq!(fs_read(out.path().join("bundle.js")), "const x = 1;");
}
#[test]
fn no_tool_configured_processes_nothing() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
std::fs::write(src.path().join("app.css"), "body{}").unwrap();
std::fs::write(src.path().join("app.js"), "x=1;").unwrap();
let pipeline = SourcePipeline::new(
vec![src.path().to_path_buf()],
Vec::new(),
Vec::new(),
out.path().to_path_buf(),
None,
None,
false,
Broadcaster::new(),
);
pipeline.full_build().unwrap();
assert!(
!out.path().join("app.css").exists() && !out.path().join("app.js").exists(),
"with no css/js tool configured, nothing should be written to the output dir"
);
}
#[test]
fn prune_output_removes_stale_css_bundle_when_no_css_sources_remain() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
std::fs::write(out.path().join("styles.css"), "/* stale */").unwrap();
let pipeline = SourcePipeline::new(
vec![src.path().to_path_buf()],
Vec::new(),
Vec::new(),
out.path().to_path_buf(),
Some((CssTool::TestEcho, CssOptions::new().bundle(true))),
None,
true,
Broadcaster::new(),
);
pipeline.full_build().unwrap();
assert!(
!out.path().join("styles.css").exists(),
"prune is opt-in: with_prune_output should delete the stale bundle produced by no css sources"
);
}
#[test]
fn without_prune_stale_css_bundle_is_left_in_place() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
std::fs::write(out.path().join("styles.css"), "/* stale */").unwrap();
let pipeline = SourcePipeline::new(
vec![src.path().to_path_buf()],
Vec::new(),
Vec::new(),
out.path().to_path_buf(),
Some((CssTool::TestEcho, CssOptions::new().bundle(true))),
None,
false,
Broadcaster::new(),
);
pipeline.full_build().unwrap();
assert!(
out.path().join("styles.css").exists(),
"without with_prune_output the stale bundle must be left in place"
);
}
#[test]
fn asset_folder_mirrors_every_file_byte_identical_on_full_build() {
let assets = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
std::fs::write(assets.path().join("index.html"), "<html></html>").unwrap();
std::fs::create_dir(assets.path().join("images")).unwrap();
std::fs::write(assets.path().join("images/logo.svg"), "<svg></svg>").unwrap();
let pipeline = SourcePipeline::new(
Vec::new(),
Vec::new(),
vec![assets.path().to_path_buf()],
out.path().to_path_buf(),
None,
None,
false,
Broadcaster::new(),
);
pipeline.full_build().unwrap();
assert_eq!(fs_read(out.path().join("index.html")), "<html></html>");
assert_eq!(
fs_read(out.path().join("images/logo.svg")),
"<svg></svg>",
"nested paths under the asset folder must be preserved in the output"
);
}
#[test]
fn asset_folder_change_recopies_just_that_file_and_broadcasts() {
let assets = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
let source = assets.path().join("index.html");
std::fs::write(&source, "v1").unwrap();
let broadcaster = Broadcaster::new();
let observer = broadcaster.subscribe();
let pipeline = SourcePipeline::new(
Vec::new(),
Vec::new(),
vec![assets.path().to_path_buf()],
out.path().to_path_buf(),
None,
None,
false,
broadcaster,
);
std::fs::write(&source, "v2").unwrap();
pipeline.process_change(&source, &ChangeType::Html).unwrap();
assert_eq!(fs_read(out.path().join("index.html")), "v2");
let event = observer.recv().expect("must broadcast the rebuild");
assert_eq!(event.path, out.path().join("index.html"));
}
fn fs_read(path: PathBuf) -> String {
std::fs::read_to_string(path).unwrap()
}