implicit-predicates-gen 0.0.1

Code generator for exact geometric predicates over explicit and implicit points
Documentation
//! Tests for the command line tool, run as a separate process in a
//! temporary directory.

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");

/// A temporary directory with a `predicates` directory of definitions,
/// removed when dropped.
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)
    }

    /// Runs the tool on `predicates`, writing to `stages` and `checks`.
    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}");
    // Nothing is written in check mode.
    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());
}