use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const BINARY: &str = env!("CARGO_BIN_EXE_paths-le");
const FILES: usize = 500;
const CEILING: Duration = Duration::from_secs(3);
const LINEARITY: f64 = 6.0;
fn enabled(name: &str) -> bool {
if std::env::var_os("PATHS_LE_BUDGET").is_some() {
return true;
}
eprintln!("SKIPPED {name}: set PATHS_LE_BUDGET to run it");
false
}
struct Tree {
root: PathBuf,
}
impl Tree {
fn new(name: &str) -> Self {
let root =
std::env::temp_dir().join(format!("paths-le-budget-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("a temporary directory");
Self {
root: std::fs::canonicalize(&root).expect("a canonical directory"),
}
}
fn path(&self) -> &Path {
&self.root
}
}
impl Drop for Tree {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
fn populate(root: &Path, copies: usize) {
for copy in 0..copies {
let package = root.join(format!("package-{copy}"));
std::fs::create_dir_all(package.join("src")).expect("a directory");
std::fs::create_dir_all(package.join("assets")).expect("a directory");
for index in 0..FILES / 5 {
std::fs::write(package.join(format!("assets/asset-{index}.txt")), "x")
.expect("an asset");
let neighbour = (index + 1) % (FILES / 5);
std::fs::write(
package.join(format!("src/module-{index}.ts")),
format!(
"import './module-{neighbour}.ts';\n\
import '../assets/asset-{index}.txt';\n\
import './missing-{index}.ts';\n\
export const asset = '../assets/asset-{neighbour}.txt';\n"
),
)
.expect("a source file");
std::fs::write(
package.join(format!("src/config-{index}.json")),
format!(
"{{\"main\":\"./module-{index}.ts\",\
\"asset\":\"../assets/asset-{index}.txt\",\
\"missing\":\"./gone-{index}.ts\"}}"
),
)
.expect("a config file");
std::fs::write(
package.join(format!("src/deploy-{index}.yml")),
format!(
"steps:\n - uses: ./module-{index}.ts\n - path: ../assets/asset-{index}.txt\n"
),
)
.expect("a manifest");
std::fs::write(
package.join(format!("src/notes-{index}.md")),
format!("See ./module-{index}.ts and ../assets/asset-{index}.txt.\n"),
)
.expect("a note");
}
let mut lockfile = String::from("{\n \"packages\": {\n");
for index in 0..2_000 {
writeln!(
lockfile,
" \"node_modules/pkg-{index}\": {{ \"resolved\": \"./vendor/pkg-{index}.tgz\" }},"
)
.expect("a string grows");
}
lockfile.push_str(" \"\": {}\n }\n}\n");
std::fs::write(package.join("lock.json"), lockfile).expect("a lockfile");
}
}
fn measure(root: &Path) -> (Duration, u64) {
let started = Instant::now();
let output = Command::new(BINARY)
.args(["--strict", "--root"])
.arg(root)
.arg(root)
.stdin(Stdio::null())
.output()
.expect("the binary runs");
let elapsed = started.elapsed();
let code = output.status.code().expect("an exit code, not a signal");
assert!(
(0..=1).contains(&code),
"the tree must be examinable: exit {code}\n{}",
String::from_utf8_lossy(&output.stderr)
);
let paths = String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
let report: serde_json::Value =
serde_json::from_str(line).expect("stdout carries only JSON");
report["summary"]["paths"].as_u64().unwrap_or(0)
})
.sum();
(elapsed, paths)
}
#[test]
fn a_five_hundred_file_tree_scans_inside_its_budget() {
if !enabled("a_five_hundred_file_tree_scans_inside_its_budget") {
return;
}
let tree = Tree::new("ceiling");
populate(tree.path(), 1);
let (elapsed, paths) = measure(tree.path());
eprintln!("budget: {paths} paths in {elapsed:?} (ceiling {CEILING:?})");
assert!(
elapsed < CEILING,
"the scan took {elapsed:?}, over the {CEILING:?} ceiling — \
ten times the recorded local measurement. Something got an order \
of magnitude slower, or the ceiling needs re-measuring with the \
machine named in this file."
);
assert!(paths > 2_500, "only {paths} paths were examined");
}
#[test]
fn four_times_the_tree_does_not_cost_six_times_the_time() {
if !enabled("four_times_the_tree_does_not_cost_six_times_the_time") {
return;
}
let one = Tree::new("linear-one");
populate(one.path(), 1);
let four = Tree::new("linear-four");
populate(four.path(), 4);
let _ = measure(one.path());
let _ = measure(four.path());
let (small, small_paths) = measure(one.path());
let (large, large_paths) = measure(four.path());
let ratio = large.as_secs_f64() / small.as_secs_f64().max(0.000_001);
eprintln!(
"linearity: {small_paths} paths in {small:?}, {large_paths} paths in {large:?} — {ratio:.2}×"
);
assert!(
large_paths > small_paths * 3,
"the larger tree must actually be larger: {small_paths} then {large_paths}"
);
assert!(
ratio < LINEARITY,
"four times the tree cost {ratio:.2}× the time, over the {LINEARITY}× \
bound — that is the shape of an algorithm that is not linear in the \
size of what it reads"
);
}