blue-lang-cli 0.0.211

The blue command line: run, fmt, ast, erase, check.
#[macro_use]
mod moduledoc;

use std::path::{Path, PathBuf};
use std::process::{Command, Output};

const GENERATOR: &str = moduledoc!(
    "page: the generator copies its declared note through zed and logs each run.",
    "to_s(1) #=> \"1\"",
    "use(\"zed\", [:shout])

def main()
  append_file(getenv(\"RUNS_LOG\", \"runs.log\"), \"ran\\n\")
  write_file(
    getenv(\"GEN_OUT\", \"out.txt\"),
    shout(read_file(path_join(getenv(\"GEN_ROOT\", \".\"), \"docs/a.md\")))
  )
end
"
);

const ZED: &str = moduledoc!(
    "zed: a fixture package.",
    "shout(\"a\") #=> \"a!\"",
    "@doc \"\"\"
shout: The text with a mark.

    shout(\"a\") #=> \"a!\"
\"\"\"
def shout(s)
  \"#{s}!\"
end
"
);

struct Project {
    dir: PathBuf,
    cache: PathBuf,
    log: PathBuf,
}

impl Project {
    fn new(name: &str) -> Self {
        let base = Path::new(env!("CARGO_TARGET_TMPDIR"))
            .join(format!("gen-cache-{name}-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&base);
        let dir = base.join("project");
        std::fs::create_dir_all(dir.join("docs")).expect("docs dir");
        std::fs::create_dir_all(dir.join("unread")).expect("unread dir");
        std::fs::create_dir_all(dir.join("bidamas/zed")).expect("package dir");
        std::fs::write(dir.join("docs/a.md"), "one").expect("declared file");
        std::fs::write(dir.join("unread/b.md"), "two").expect("undeclared file");
        std::fs::write(dir.join("g.b"), GENERATOR).expect("generator");
        std::fs::write(
            dir.join("bidamas/zed/Bluefile"),
            "package(\"zed\", \"0.1.0\")\n",
        )
        .expect("package Bluefile");
        std::fs::write(dir.join("bidamas/zed/zed.b"), ZED).expect("package");
        std::fs::write(
            dir.join("Bluefile"),
            "package(\"fixture\", \"0.1.0\")\n\npackages(\"bidamas\")\ngenerate(\"page\", \"out.txt\", \"g.b\", [\"docs\"])\n",
        )
        .expect("Bluefile");
        let p = Self {
            dir: dir.canonicalize().expect("canonical project"),
            cache: base.join("cache"),
            log: base.join("runs.log"),
        };
        for args in [&["lock", "bidamas/zed"][..], &["lock", "."][..]] {
            let locked = p.blue(args, &[]);
            assert!(locked.status.success(), "{}", text(&locked));
        }
        for args in [&["init", "-q"][..], &["add", "-A"][..]] {
            let g = Command::new("git")
                .arg("-C")
                .arg(&p.dir)
                .args(args)
                .env_remove("GIT_DIR")
                .env_remove("GIT_WORK_TREE")
                .env_remove("GIT_INDEX_FILE")
                .output()
                .expect("git runs");
            assert!(g.status.success(), "{}", text(&g));
        }
        p
    }

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

    fn generate(&self, env: &[(&str, &str)]) -> usize {
        let o = self.blue(&["run", "g.b"], env);
        assert!(o.status.success(), "{}", text(&o));
        std::fs::read_to_string(&self.log)
            .unwrap_or_default()
            .lines()
            .count()
    }

    fn write(&self, rel: &str, content: &str) {
        std::fs::write(self.dir.join(rel), content).expect("write");
    }

    fn out(&self) -> String {
        std::fs::read_to_string(self.dir.join("out.txt")).expect("the output is written")
    }
}

impl Drop for Project {
    fn drop(&mut self) {
        if let Some(base) = self.dir.parent() {
            let _ = std::fs::remove_dir_all(base);
        }
    }
}

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

#[test]
fn a_generator_whose_inputs_hash_the_same_does_not_run_again() {
    let p = Project::new("same");
    assert_eq!(p.generate(&[]), 1);
    assert_eq!(p.out(), "one!");
    assert_eq!(p.generate(&[]), 1);
    assert_eq!(p.out(), "one!");
}

#[test]
fn a_changed_declared_input_runs_it_again_and_an_undeclared_file_does_not() {
    let p = Project::new("input");
    assert_eq!(p.generate(&[]), 1);
    p.write("unread/b.md", "three");
    assert_eq!(p.generate(&[]), 1);
    p.write("docs/a.md", "four");
    assert_eq!(p.generate(&[]), 2);
    assert_eq!(p.out(), "four!");
    assert_eq!(p.generate(&[]), 2);
}

#[test]
fn a_changed_program_or_package_runs_it_again() {
    let p = Project::new("code");
    assert_eq!(p.generate(&[]), 1);
    p.write(
        "g.b",
        &GENERATOR
            .replace("shout(read_file", "shout(shout(read_file")
            .replace("docs/a.md\")))", "docs/a.md\"))))"),
    );
    assert_eq!(p.generate(&[]), 2);
    assert_eq!(p.out(), "one!!");
    p.write(
        "bidamas/zed/zed.b",
        &ZED.replace("\"#{s}!\"", "\"#{s}?\"")
            .replace("#=> \"a!\"", "#=> \"a?\""),
    );
    assert_eq!(p.generate(&[]), 3);
    assert_eq!(p.out(), "one??");
}

#[test]
fn an_output_edited_by_hand_is_generated_again() {
    let p = Project::new("output");
    assert_eq!(p.generate(&[]), 1);
    p.write("out.txt", "edited");
    assert_eq!(p.generate(&[]), 2);
    assert_eq!(p.out(), "one!");
}

#[test]
fn the_off_switch_runs_it_every_time() {
    let p = Project::new("off");
    assert_eq!(p.generate(&[]), 1);
    assert_eq!(p.generate(&[("BLUE_GEN_CACHE", "off")]), 2);
    assert_eq!(p.generate(&[]), 2);
    let refused = p.blue(&["run", "g.b"], &[("BLUE_GEN_CACHE", "sometimes")]);
    assert!(!refused.status.success(), "{}", text(&refused));
    assert!(
        text(&refused).contains("BLUE_GEN_CACHE"),
        "{}",
        text(&refused)
    );
}