implicit-predicates-gen 0.0.2

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");
const POINTS: &str = include_str!("fixtures/points.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());
}

#[test]
fn a_file_of_only_points_writes_the_point_module_and_its_checks() {
    let w = Workspace::new(&[("points.pred", POINTS)]);
    let output = w.run(false);
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(files(&w.path("stages")), ["points.rs"]);
    assert_eq!(files(&w.path("checks")), ["points.rs"]);
    let notice =
        "// Generated by implicit-predicates-gen from predicates/points.pred. Do not edit.\n";
    let code = fs::read_to_string(w.path("stages/points.rs")).unwrap();
    assert!(code.starts_with(notice));
    assert!(code.contains("pub struct Lnc {"));
    assert!(code.contains("pub enum AnyPoint {"));
    let checks = fs::read_to_string(w.path("checks/points.rs")).unwrap();
    assert!(checks.starts_with(notice));
    assert!(checks.contains("pub fn exact_lnc(c: [[f64; 3]; 2], s: [f64; 1]) -> [Dyadic; 3] {"));
}

#[test]
fn points_and_predicates_are_written_side_by_side_and_checked() {
    let w = Workspace::new(&[("orient3d.pred", ORIENT3D), ("points.pred", POINTS)]);
    let output = w.run(false);
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(files(&w.path("stages")), ["orient3d.rs", "points.rs"]);
    assert_eq!(files(&w.path("checks")), ["orient3d.rs", "points.rs"]);
    let output = w.run(true);
    assert!(output.status.success(), "{}", stderr(&output));
    for dir in ["stages", "checks"] {
        let file = w.path(&format!("{dir}/points.rs"));
        let original = fs::read_to_string(&file).unwrap();
        fs::write(&file, original.replace("Lnc", "Lmc")).unwrap();
        let output = w.run(true);
        assert!(!output.status.success());
        let message = stderr(&output);
        assert!(
            message.contains(&format!("{dir}/points.rs"))
                || message.contains(&format!("{dir}\\points.rs")),
            "{message}"
        );
        assert!(message.contains("differs"), "{message}");
        assert!(!message.contains("orient3d"), "{message}");
        fs::write(&file, original).unwrap();
    }
    fs::remove_file(w.path("checks/points.rs")).unwrap();
    let message = stderr(&w.run(true));
    assert!(message.contains("points.rs is missing"), "{message}");
}

#[test]
fn point_types_in_two_files_are_an_error_at_the_second_declaration() {
    let w = Workspace::new(&[
        ("a.pred", "point A(p: F64x3) = p;\n"),
        ("b.pred", "\npoint B(p: F64x3) = p;\n"),
    ]);
    let output = w.run(false);
    assert!(!output.status.success());
    let message = stderr(&output);
    let at = format!("{}:2:7: ", Path::new("predicates").join("b.pred").display());
    assert!(message.starts_with(&at), "{message}");
    assert!(
        message.contains("point types are also declared in"),
        "{message}"
    );
    assert!(message.contains("a.pred"), "{message}");
    assert!(!w.path("stages").exists());
}

/// The source of `orient3d` with the predicate renamed `points`.
fn predicate_named_points() -> String {
    ORIENT3D.replace("fn orient3d(", "fn points(")
}

/// The `line:column: ` of the predicate `points` in `source`.
fn predicate_position(source: &str) -> String {
    let p = implicit_predicates_gen::parse(source, "t.pred").unwrap();
    let p = p.iter().find(|p| p.name == "points").unwrap();
    format!("{}:{}: ", p.position.line, p.position.column)
}

#[test]
fn a_predicate_named_points_clashes_with_the_point_types() {
    let named = predicate_named_points();
    // The predicate is read first.
    let w = Workspace::new(&[("a.pred", &named), ("points.pred", POINTS)]);
    let output = w.run(false);
    assert!(!output.status.success());
    let message = stderr(&output);
    let file = Path::new("predicates").join("points.pred");
    let first = POINTS
        .lines()
        .position(|l| l.starts_with("point "))
        .unwrap()
        + 1;
    assert!(
        message.starts_with(&format!("{}:{first}:7: ", file.display())),
        "{message}"
    );
    assert!(message.contains("the predicate `points`"), "{message}");
    assert!(message.contains("a.pred"), "{message}");
    assert!(!w.path("stages").exists());
    // The point types are read first.
    let w = Workspace::new(&[("points.pred", POINTS), ("z.pred", &named)]);
    let output = w.run(false);
    assert!(!output.status.success());
    let message = stderr(&output);
    let file = Path::new("predicates").join("z.pred");
    let at = format!("{}:{}", file.display(), predicate_position(&named));
    assert!(message.starts_with(&at), "{message}");
    assert!(
        message.contains("the module of the point types"),
        "{message}"
    );
    assert!(!w.path("stages").exists());
}

#[test]
fn a_point_declaration_error_is_printed_with_its_position_and_fails() {
    let w = Workspace::new(&[("points.pred", "point A(new: F64x3) = new;\n")]);
    let output = w.run(false);
    assert!(!output.status.success());
    let message = stderr(&output);
    let expected = Path::new("predicates").join("points.pred");
    assert!(
        message.contains(&format!("{}:1:9: ", expected.display())),
        "{message}"
    );
    assert!(!w.path("stages").exists());
}