use std::fmt::Write as _;
use std::fs::File;
use std::io::Write as _;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
const BINARY: &str = env!("CARGO_BIN_EXE_numbers-le");
static COUNTER: AtomicUsize = AtomicUsize::new(0);
const LIMIT: Duration = Duration::from_secs(60);
struct Tree {
root: PathBuf,
}
impl Tree {
fn new(name: &str) -> Self {
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"numbers-le-platform-{name}-{}-{unique}",
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 text(&self) -> String {
self.root.to_string_lossy().into_owned()
}
fn write(&self, relative: &str, contents: &str) -> PathBuf {
let target = self.root.join(relative);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).expect("a parent directory");
}
std::fs::write(&target, contents).expect("a file");
target
}
}
impl Drop for Tree {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
struct Run {
code: Option<i32>,
stdout: String,
}
fn execute(args: &[&str], timezone: Option<&str>) -> Run {
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let capture = std::env::temp_dir().join(format!(
"numbers-le-platform-capture-{}-{unique}",
std::process::id()
));
std::fs::create_dir_all(&capture).expect("a capture directory");
let out = capture.join("stdout");
let mut command = Command::new(BINARY);
command
.args(args)
.stdin(Stdio::null())
.stdout(File::create(&out).expect("a stdout file"))
.stderr(Stdio::null());
match timezone {
Some(zone) => command.env("TZ", zone),
None => command.env_remove("TZ"),
};
let mut child = command.spawn().expect("the binary runs");
let started = Instant::now();
let status = loop {
match child.try_wait().expect("the child can be waited on") {
Some(status) => break status,
None if started.elapsed() >= LIMIT => {
let _ = child.kill();
let _ = child.wait();
panic!("the run hung past {LIMIT:?}: {args:?}");
}
None => std::thread::sleep(Duration::from_millis(5)),
}
};
let stdout = String::from_utf8_lossy(&std::fs::read(&out).unwrap_or_default()).into_owned();
let _ = std::fs::remove_dir_all(&capture);
Run {
code: status.code(),
stdout,
}
}
fn run(args: &[&str]) -> Run {
execute(args, Some("UTC"))
}
fn reports(run: &Run) -> Vec<serde_json::Value> {
run.stdout
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str(line).expect("stdout carries only JSON"))
.collect()
}
fn skipped(case: &str, why: &str) {
eprintln!("SKIPPED {case}: {why}");
}
fn nested(name: &str) -> Tree {
let tree = Tree::new(name);
tree.write("rates.env", "VAT=0.2\n");
tree.write("src/deep/pricing.ts", "const MARKUP = 1.15;\n");
tree.write("config/app.toml", "port = 8080\n");
tree
}
#[test]
fn every_path_in_the_report_is_separated_by_forward_slashes() {
let tree = nested("separators");
let outcome = run(&[&tree.text()]);
assert_eq!(outcome.code, Some(0));
let scanned = reports(&outcome);
assert_eq!(scanned.len(), 3, "the whole tree was walked");
for report in &scanned {
let file = report["file"].as_str().expect("a file name");
assert!(
!file.contains('\\'),
"a backslash in a reported path: {file}"
);
assert!(
file.contains('/'),
"a nested path lost its separators: {file}"
);
}
}
#[test]
fn the_answer_does_not_depend_on_the_timezone() {
let tree = nested("timezone");
let with = execute(&[&tree.text()], Some("UTC"));
let without = execute(&[&tree.text()], None);
let elsewhere = execute(&[&tree.text()], Some("Pacific/Kiritimati"));
assert_eq!(with.stdout, without.stdout, "TZ=UTC against no TZ");
assert_eq!(with.stdout, elsewhere.stdout, "TZ=UTC against TZ=+14");
assert_eq!(with.code, without.code);
}
#[test]
fn a_file_is_never_reported_twice_on_a_case_insensitive_filesystem() {
let tree = Tree::new("case");
tree.write("README.md", "rate 0.2\n");
tree.write("readme.md", "rate 0.3\n");
let outcome = run(&[&tree.text()]);
let named: Vec<String> = reports(&outcome)
.iter()
.filter_map(|report| report["file"].as_str())
.map(str::to_string)
.collect();
let insensitive = named.len() == 1;
if insensitive {
eprintln!("case-insensitive filesystem: the two names are one file");
}
let mut unique = named.clone();
unique.sort_unstable();
unique.dedup();
assert_eq!(
unique.len(),
named.len(),
"a file was reported twice: {named:?}"
);
assert!(
named.len() <= 2,
"more report lines than files written: {named:?}"
);
}
#[test]
fn the_walk_survives_the_reserved_windows_filenames() {
let tree = Tree::new("reserved");
tree.write("ordinary.env", "VAT=0.2\n");
let mut made = Vec::new();
for reserved in ["CON", "PRN", "AUX", "NUL", "COM1"] {
let target = tree.root.join(reserved);
match std::fs::write(&target, "A=1\n") {
Ok(()) => made.push(reserved),
Err(_) => skipped(
&format!("a file named {reserved}"),
"this filesystem reserves the name",
),
}
}
let outcome = run(&[&tree.text()]);
let code = outcome.code.expect("an exit code, not a signal");
assert!((0..=2).contains(&code), "exit {code}");
let named: Vec<String> = reports(&outcome)
.iter()
.filter_map(|report| report["file"].as_str())
.map(|file| file.rsplit('/').next().unwrap_or(file).to_string())
.collect();
assert!(
named.iter().any(|file| file == "ordinary.env"),
"the reserved names took the rest of the tree with them: {named:?}\n\
created: {made:?}"
);
}
#[test]
fn a_child_that_refuses_before_reading_stdin_still_exits_two() {
let mut child = Command::new(BINARY)
.args(["--stdin", "unexpected.json"])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("the binary runs");
let _ = child
.stdin
.as_mut()
.expect("stdin")
.write_all(&vec![b'1'; 1 << 20]);
drop(child.stdin.take());
let status = child.wait().expect("the child finishes");
assert_eq!(status.code(), Some(2));
}
#[test]
fn a_document_on_stdin_is_read_whole() {
let mut child = Command::new(BINARY)
.args(["--stdin", "--format", "csv", "--values"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("the binary runs");
let mut document = String::new();
for row in 0..10_000 {
let _ = writeln!(document, "row{row},{row}");
}
child
.stdin
.as_mut()
.expect("stdin")
.write_all(document.as_bytes())
.expect("the child is still reading");
drop(child.stdin.take());
let output = child.wait_with_output().expect("the child finishes");
assert_eq!(output.status.code(), Some(0));
let written = String::from_utf8_lossy(&output.stdout);
assert_eq!(
written.lines().count(),
10_000,
"a document arriving in pieces was read short"
);
}