use std::fmt::Write as _;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
const BINARY: &str = env!("CARGO_BIN_EXE_string-le");
static COUNTER: AtomicUsize = AtomicUsize::new(0);
fn skipped(case: &str, why: &str) {
eprintln!("SKIPPED {case}: {why}");
}
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!(
"string-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 path(&self) -> &Path {
&self.root
}
fn arg(&self) -> String {
self.root.to_string_lossy().into_owned()
}
fn write(&self, relative: &str, contents: &str) {
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");
}
}
impl Drop for Tree {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
struct Run {
code: i32,
stdout: String,
stderr: String,
}
fn run(args: &[&str]) -> Run {
let output = Command::new(BINARY).args(args).output().expect("it runs");
Run {
code: output.status.code().expect("an exit code, not a signal"),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
}
}
fn reported_files(run: &Run) -> Vec<String> {
run.stdout
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str::<serde_json::Value>(line).expect("JSON Lines"))
.filter_map(|report| report["file"].as_str().map(str::to_string))
.collect()
}
#[test]
fn every_path_in_the_report_uses_a_forward_slash() {
let tree = Tree::new("separators");
tree.write("src/ui/messages.ts", "const m = 'Delete this?';\n");
tree.write("config/app/settings.json", "{\"a\":\"value\"}\n");
let run = run(&[&tree.arg()]);
assert_eq!(run.code, 0, "{}", run.stderr);
let files = reported_files(&run);
assert_eq!(files.len(), 2, "{}", run.stdout);
for file in &files {
assert!(
!file.contains('\\'),
"the report spells a path with a backslash: {file}"
);
assert!(
file.contains("src/ui/") || file.contains("config/app/"),
"the nesting is not reported as nesting: {file}"
);
}
assert!(
!run.stderr.contains(".ts\\") && !run.stderr.contains('\\'),
"the human half spells a path differently: {}",
run.stderr
);
}
#[test]
fn the_answer_does_not_depend_on_the_timezone() {
let tree = Tree::new("timezone");
tree.write("messages.ts", "const m = 'Delete this?';\n");
let utc = Command::new(BINARY)
.arg(tree.arg())
.env("TZ", "UTC")
.output()
.expect("it runs");
let far = Command::new(BINARY)
.arg(tree.arg())
.env("TZ", "Pacific/Kiritimati")
.output()
.expect("it runs");
let none = Command::new(BINARY)
.arg(tree.arg())
.env_remove("TZ")
.output()
.expect("it runs");
assert_eq!(utc.stdout, none.stdout, "TZ=UTC and no TZ differ");
assert_eq!(utc.stdout, far.stdout, "two timezones differ");
assert_eq!(utc.stderr, none.stderr, "TZ=UTC and no TZ differ on stderr");
assert_eq!(utc.status.code(), none.status.code());
}
#[test]
fn a_case_insensitive_filesystem_does_not_produce_the_same_file_twice() {
let tree = Tree::new("case");
tree.write("README.md", "'upper'\n");
tree.write("readme.md", "'lower'\n");
let on_disk = std::fs::read_dir(tree.path())
.expect("the tree is readable")
.count();
assert!(
(1..=2).contains(&on_disk),
"the filesystem did something unexpected: {on_disk} entries"
);
if on_disk == 1 {
eprintln!("NOTE: this filesystem folds case — one file, not two");
}
let run = run(&[&tree.arg()]);
let mut files = reported_files(&run);
assert_eq!(files.len(), on_disk, "{}", run.stdout);
files.sort();
let mut unique = files.clone();
unique.dedup();
assert_eq!(files, unique, "a file is reported twice: {files:?}");
}
#[test]
fn reserved_windows_names_do_not_stop_the_walk() {
let tree = Tree::new("reserved");
tree.write("kept.json", "{\"a\":\"value\"}\n");
let mut created = Vec::new();
for name in ["CON", "PRN", "AUX", "NUL", "COM1"] {
match std::fs::write(tree.path().join(name), "'reserved'\n") {
Ok(()) => created.push(name),
Err(_) => skipped(
&format!("a file named {name}"),
"this platform reserves the name",
),
}
}
let run = run(&[&tree.arg()]);
assert!(run.code == 0 || run.code == 1, "{}", run.stderr);
let files = reported_files(&run);
assert!(
files.iter().any(|file| file.ends_with("kept.json")),
"the ordinary file was lost with the reserved ones: {files:?}"
);
for name in created {
assert!(
files.iter().any(|file| file.ends_with(name)),
"{name} was created and then not walked: {files:?}"
);
}
}
#[test]
fn writing_to_a_child_that_refuses_immediately_is_not_a_failure() {
let mut child = Command::new(BINARY)
.args(["--stdin", "a-file.json"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("the binary runs");
if let Some(stdin) = child.stdin.as_mut() {
let _ = stdin.write_all(&vec![b'x'; 1024 * 1024]);
let _ = stdin.flush();
}
drop(child.stdin.take());
let output = child.wait_with_output().expect("it finishes");
assert_eq!(
output.status.code(),
Some(2),
"the refusal is the assertion: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stdout.is_empty(), "a refusal writes no report");
}
#[test]
fn a_document_written_to_stdin_arrives_whole() {
let mut child = Command::new(BINARY)
.args(["--stdin", "--format", "json"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("the binary runs");
let mut document = String::from("{\n");
for index in 0..5_000 {
let _ = writeln!(document, " \"k{index}\": \"value {index}\",");
}
document.push_str(" \"last\": \"end\"\n}\n");
if let Some(stdin) = child.stdin.as_mut() {
let _ = stdin.write_all(document.as_bytes());
}
drop(child.stdin.take());
let output = child.wait_with_output().expect("it finishes");
assert_eq!(output.status.code(), Some(0));
let report: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout carries JSON");
assert_eq!(report["summary"]["strings"], 5_001);
}