blue-lang-cli 0.0.151

The blue command line: run, fmt, ast, erase, check.
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

const CHECK: &str = "test \"the declared input is there\"
  assert path_exists(path_join(getenv(\"GEN_ROOT\", \".\"), \"docs/a.md\"))
end

test \"a path the check does not declare is not\"
  assert not path_exists(path_join(getenv(\"GEN_ROOT\", \".\"), \"unread/b.md\"))
end

test \"a path that climbs out of a declared input reaches nothing\"
  root = getenv(\"GEN_ROOT\", \".\")
  assert not path_exists(path_join(root, \"docs/../unread/b.md\"))
  assert not path_exists(path_join(root, \"docs/../t.b\"))
end
";

const GENERATOR: &str = "def main()
  write_file(
    getenv(\"GEN_OUT\", \"out.txt\"),
    to_s(path_exists(path_join(getenv(\"GEN_ROOT\", \".\"), \"unread/b.md\")))
  )
end
";

fn project(name: &str, inputs: &str) -> PathBuf {
    let dir = fixture(Path::new(env!("CARGO_TARGET_TMPDIR")), name, inputs);
    for args in [&["init", "-q"][..], &["add", "-A"][..]] {
        let g = git(&dir, args);
        assert!(g.status.success(), "{}", text(&g));
    }
    dir
}

fn loose(name: &str, inputs: &str) -> PathBuf {
    let dir = fixture(&std::env::temp_dir(), name, inputs);
    assert!(
        !git(&dir, &["rev-parse", "--git-dir"]).status.success(),
        "{} is inside a git repository, and this case needs one outside",
        dir.display()
    );
    dir
}

fn git(dir: &Path, args: &[&str]) -> Output {
    Command::new("git")
        .arg("-C")
        .arg(dir)
        .args(args)
        .env_remove("GIT_DIR")
        .env_remove("GIT_WORK_TREE")
        .env_remove("GIT_INDEX_FILE")
        .output()
        .expect("git runs")
}

fn fixture(base: &Path, name: &str, inputs: &str) -> PathBuf {
    let dir = base.join(format!("declared-{name}-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(dir.join("docs")).expect("docs dir");
    std::fs::create_dir_all(dir.join("unread")).expect("unread dir");
    std::fs::write(dir.join("docs/a.md"), "# A\n").expect("declared file");
    std::fs::write(dir.join("unread/b.md"), "# B\n").expect("undeclared file");
    std::fs::write(dir.join("t.b"), CHECK).expect("check file");
    std::fs::write(dir.join("g.b"), GENERATOR).expect("generator");
    std::fs::write(
        dir.join("Bluefile"),
        format!(
            "package(\"fixture\", \"0.1.0\")\n\ncheck(\"reads\", \"t.b\", {inputs})\ngenerate(\"page\", \"out.txt\", \"g.b\", {inputs})\n"
        ),
    )
    .expect("Bluefile");
    let locked = blue(&dir, &["lock", "."], &[]);
    assert!(locked.status.success(), "{}", text(&locked));
    dir.canonicalize().expect("canonical project")
}

fn blue(dir: &Path, args: &[&str], env: &[(&str, &str)]) -> Output {
    let mut c = Command::new(env!("CARGO_BIN_EXE_blue"));
    c.current_dir(dir)
        .args(args)
        .env_remove("GEN_ROOT")
        .env_remove("BLUE_INPUTS")
        .env_remove("BLUE_PATH")
        .env("TMPDIR", Path::new(env!("CARGO_TARGET_TMPDIR")));
    for (k, v) in env {
        c.env(k, v);
    }
    c.output().expect("blue runs")
}

fn text(o: &Output) -> String {
    format!(
        "{}{}",
        String::from_utf8_lossy(&o.stdout),
        String::from_utf8_lossy(&o.stderr)
    )
}

#[test]
fn a_check_reads_only_its_declared_inputs_as_nix_runs_it_and_the_off_switch_reads_the_checkout() {
    let dir = project("check", "[\"docs\"]");
    let held = blue(&dir, &["test", "t.b"], &[]);
    assert!(held.status.success(), "{}", text(&held));
    let open = blue(&dir, &["test", "t.b"], &[("BLUE_INPUTS", "checkout")]);
    assert!(!open.status.success(), "{}", text(&open));
    let named = blue(&dir, &["test", "t.b"], &[("GEN_ROOT", ".")]);
    assert!(
        !named.status.success(),
        "an explicit GEN_ROOT wins: {}",
        text(&named)
    );
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn a_generator_run_reads_only_its_declared_inputs_and_the_off_switch_reads_the_checkout() {
    let dir = project("generate", "[\"docs\"]");
    let held = blue(&dir, &["run", "--quiet", "g.b"], &[]);
    assert!(held.status.success(), "{}", text(&held));
    assert_eq!(
        std::fs::read_to_string(dir.join("out.txt")).unwrap(),
        "false"
    );
    let open = blue(
        &dir,
        &["run", "--quiet", "g.b"],
        &[("BLUE_INPUTS", "checkout")],
    );
    assert!(open.status.success(), "{}", text(&open));
    assert_eq!(
        std::fs::read_to_string(dir.join("out.txt")).unwrap(),
        "true"
    );
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn a_failing_check_says_it_read_only_its_declared_inputs_and_names_the_off_switch() {
    let dir = project("note", "[\"unread\"]");
    let o = blue(&dir, &["test", "t.b"], &[]);
    assert!(!o.status.success(), "{}", text(&o));
    let said = text(&o);
    assert!(said.contains("ran as check(\"reads\")"), "{said}");
    assert!(said.contains("BLUE_INPUTS=checkout"), "{said}");
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn a_declared_input_that_does_not_exist_is_refused_by_name() {
    let dir = project("gone", "[\"docs\"]");
    std::fs::remove_dir_all(dir.join("docs")).unwrap();
    let o = blue(&dir, &["test", "t.b"], &[]);
    assert!(!o.status.success());
    assert!(
        text(&o).contains("declares the input `docs`"),
        "{}",
        text(&o)
    );
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn a_declared_input_git_does_not_track_is_refused_as_the_sandbox_would_not_see_it() {
    let dir = project("untracked", "[\"docs\"]");
    std::fs::create_dir_all(dir.join("more")).unwrap();
    std::fs::write(dir.join("more/c.md"), "# C\n").unwrap();
    std::fs::write(
        dir.join("Bluefile"),
        "package(\"fixture\", \"0.1.0\")\n\ncheck(\"reads\", \"t.b\", [\"more\"])\n",
    )
    .unwrap();
    assert!(blue(&dir, &["lock", "."], &[]).status.success());
    let o = blue(&dir, &["test", "t.b"], &[]);
    assert!(!o.status.success());
    assert!(
        text(&o).contains("git tracks no file there"),
        "{}",
        text(&o)
    );
    std::fs::write(
        dir.join("Bluefile"),
        "package(\"fixture\", \"0.1.0\")\n\ncheck(\"reads\", \"t.b\", [\"more/c.md\"])\n",
    )
    .unwrap();
    assert!(blue(&dir, &["lock", "."], &[]).status.success());
    let one = blue(&dir, &["test", "t.b"], &[]);
    assert!(!one.status.success());
    assert!(
        text(&one).contains("git tracks no file there"),
        "{}",
        text(&one)
    );
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn a_declared_file_is_there_alone_and_nothing_beside_it() {
    let dir = project("file", "[\"docs/a.md\"]");
    let o = blue(&dir, &["test", "t.b"], &[]);
    assert!(o.status.success(), "{}", text(&o));
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn outside_git_every_file_of_a_declared_directory_is_read_and_a_declared_file_too() {
    let dir = loose("loose", "[\"docs\", \"Bluefile\"]");
    let o = blue(&dir, &["test", "t.b"], &[]);
    assert!(o.status.success(), "{}", text(&o));
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn a_switch_outside_its_set_is_refused() {
    let dir = project("bad", "[\"docs\"]");
    let o = blue(&dir, &["test", "t.b"], &[("BLUE_INPUTS", "sometimes")]);
    assert!(!o.status.success());
    assert!(
        text(&o).contains("BLUE_INPUTS is \"sometimes\""),
        "{}",
        text(&o)
    );
    let _ = std::fs::remove_dir_all(&dir);
}