use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicUsize, Ordering};
const ORIENT3D: &str = include_str!("fixtures/orient3d.pred");
const INSPHERE: &str = include_str!("fixtures/insphere.pred");
struct Workspace {
root: PathBuf,
}
impl Workspace {
fn new(definitions: &[(&str, &str)]) -> Self {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let root = std::env::temp_dir().join(format!(
"implicit-predicates-gen-cli-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(root.join("predicates")).unwrap();
for (name, source) in definitions {
fs::write(root.join("predicates").join(name), source).unwrap();
}
Workspace { root }
}
fn path(&self, relative: &str) -> PathBuf {
self.root.join(relative)
}
fn run(&self, check: bool) -> Output {
self.command(check, None)
}
fn command(&self, check: bool, path_variable: Option<&str>) -> Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_implicit-predicates-gen"));
command.current_dir(&self.root).args([
"predicates",
"--stages",
"stages",
"--checks",
"checks",
]);
if check {
command.arg("--check");
}
if let Some(value) = path_variable {
command.env("PATH", value);
}
command.output().unwrap()
}
}
impl Drop for Workspace {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn files(dir: &Path) -> Vec<String> {
let mut names: Vec<String> = fs::read_dir(dir)
.map(|entries| {
entries
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect()
})
.unwrap_or_default();
names.sort();
names
}
#[test]
fn writes_one_file_per_predicate() {
let w = Workspace::new(&[("orient3d.pred", ORIENT3D), ("insphere.pred", INSPHERE)]);
let output = w.run(false);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(files(&w.path("stages")), ["insphere.rs", "orient3d.rs"]);
assert_eq!(files(&w.path("checks")), ["insphere.rs", "orient3d.rs"]);
let notice =
"// Generated by implicit-predicates-gen from predicates/orient3d.pred. Do not edit.\n";
let code = fs::read_to_string(w.path("stages/orient3d.rs")).unwrap();
assert!(code.starts_with(notice));
assert!(code.contains("pub fn staged(p: [&[f64; 3]; 4]) -> Option<Sign> {"));
let checks = fs::read_to_string(w.path("checks/orient3d.rs")).unwrap();
assert!(checks.starts_with(notice));
assert!(checks.contains("pub fn exact(p: &[[f64; 3]; 4]) -> Sign {"));
}
#[test]
fn check_passes_right_after_writing() {
let w = Workspace::new(&[("orient3d.pred", ORIENT3D)]);
let output = w.run(false);
assert!(output.status.success(), "{}", stderr(&output));
let output = w.run(true);
assert!(output.status.success(), "{}", stderr(&output));
}
#[test]
fn check_fails_and_names_a_modified_file() {
let w = Workspace::new(&[("orient3d.pred", ORIENT3D)]);
assert!(w.run(false).status.success());
let file = w.path("stages/orient3d.rs");
let mut code = fs::read_to_string(&file).unwrap();
code.push_str("// edited\n");
fs::write(&file, &code).unwrap();
let output = w.run(true);
assert!(!output.status.success());
let message = stderr(&output);
assert!(message.contains("orient3d.rs"), "{message}");
assert!(message.contains("differs"), "{message}");
assert_eq!(fs::read_to_string(&file).unwrap(), code);
}
#[test]
fn check_fails_and_names_a_modified_checks_file() {
let w = Workspace::new(&[("orient3d.pred", ORIENT3D)]);
assert!(w.run(false).status.success());
let file = w.path("checks/orient3d.rs");
let mut code = fs::read_to_string(&file).unwrap();
code.push_str("// edited\n");
fs::write(&file, &code).unwrap();
let output = w.run(true);
assert!(!output.status.success());
let message = stderr(&output);
assert!(message.contains("checks"), "{message}");
assert!(message.contains("differs"), "{message}");
assert!(!message.contains("stages"), "{message}");
}
#[test]
fn check_fails_and_names_a_missing_file() {
let w = Workspace::new(&[("orient3d.pred", ORIENT3D), ("insphere.pred", INSPHERE)]);
assert!(w.run(false).status.success());
fs::remove_file(w.path("stages/insphere.rs")).unwrap();
let output = w.run(true);
assert!(!output.status.success());
let message = stderr(&output);
assert!(message.contains("insphere.rs"), "{message}");
assert!(message.contains("missing"), "{message}");
assert!(!message.contains("orient3d.rs"), "{message}");
assert!(!w.path("stages/insphere.rs").exists());
}
#[test]
fn a_definition_error_is_printed_with_its_position_and_fails() {
let w = Workspace::new(&[(
"bad.pred",
"fn bad(a: Point3, b: Point3) -> Sign {\n a.x * b.y\n}\n",
)]);
let output = w.run(false);
assert!(!output.status.success());
let message = stderr(&output);
let expected = Path::new("predicates").join("bad.pred");
assert!(
message.contains(&format!("{}:1:4: ", expected.display())),
"{message}"
);
assert!(!w.path("stages").exists());
}
#[test]
fn an_unreadable_directory_is_reported() {
let w = Workspace::new(&[]);
let mut command = Command::new(env!("CARGO_BIN_EXE_implicit-predicates-gen"));
let output = command
.current_dir(&w.root)
.args(["missing", "--stages", "stages", "--checks", "checks"])
.output()
.unwrap();
assert!(!output.status.success());
assert!(stderr(&output).contains("missing"), "{}", stderr(&output));
}
#[test]
fn a_missing_rustfmt_is_reported() {
let w = Workspace::new(&[("orient3d.pred", ORIENT3D)]);
let output = w.command(false, Some(""));
assert!(!output.status.success());
let message = stderr(&output);
assert!(message.contains("rustfmt"), "{message}");
assert!(!w.path("stages/orient3d.rs").exists());
}
#[test]
fn the_header_gives_the_command_that_wrote_the_file() {
let workspace = Workspace::new(&[("orient3d.pred", ORIENT3D)]);
assert!(workspace.run(false).status.success());
let expected = "// Generated by implicit-predicates-gen from predicates/orient3d.pred. \
Do not edit.\n\
// To regenerate, run from the directory that the paths are relative to:\n\
// implicit-predicates-gen predicates --stages stages --checks checks\n\n";
for file in ["stages/orient3d.rs", "checks/orient3d.rs"] {
let code = fs::read_to_string(workspace.path(file)).unwrap();
assert!(code.starts_with(expected), "{file}:\n{}", &code[..300]);
}
}
#[test]
fn absolute_paths_are_refused() {
let workspace = Workspace::new(&[("orient3d.pred", ORIENT3D)]);
let absolute = workspace.path("predicates");
let output = Command::new(env!("CARGO_BIN_EXE_implicit-predicates-gen"))
.current_dir(&workspace.root)
.arg(&absolute)
.args(["--stages", "stages", "--checks", "checks"])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(2));
assert!(
stderr(&output).contains("pass paths relative to the directory the command is run from"),
"{}",
stderr(&output)
);
assert!(!workspace.path("stages").exists());
}