use std::path::PathBuf;
use std::process::{Command, Output};
fn blue() -> Command {
let mut c = Command::new(env!("CARGO_BIN_EXE_blue"));
c.env_remove("BLUE_PATH")
.env_remove("BLUE_CONFIG")
.env_remove("BLUE_TIER");
c
}
fn write(name: &str, src: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("blue-cli-test-{name}.b"));
std::fs::write(&p, src).expect("write fixture");
p
}
fn run(args: &[&str]) -> Output {
blue().args(args).output().expect("spawn blue")
}
fn run_env(args: &[&str], env: &[(&str, &str)]) -> Output {
let mut c = blue();
for (k, v) in env {
c.env(k, v);
}
c.args(args).output().expect("spawn blue")
}
fn write_at(rel: &str, contents: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("blue-cli-test-{rel}"));
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).expect("create fixture dir");
}
std::fs::write(&p, contents).expect("write fixture");
p
}
fn stdout(o: &Output) -> String {
String::from_utf8_lossy(&o.stdout).to_string()
}
fn stderr(o: &Output) -> String {
String::from_utf8_lossy(&o.stderr).to_string()
}
const PROGRAM: &str =
"def fact(n: Int) -> Int\n if n < 2\n 1\n else\n n * fact(n - 1)\n end\nend\nfact(6)";
#[test]
fn run_passes_the_programs_arguments_to_argv() {
let f = write("argv", "join(argv(), \"|\")");
let o = run(&["run", f.to_str().unwrap(), "rust", "--dry-run", "--min-age-hours", "6", "/x"]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
assert_eq!(stdout(&o).trim(), "rust|--dry-run|--min-age-hours|6|/x");
let o = run(&["run", f.to_str().unwrap()]);
assert_eq!(stdout(&o).trim(), "");
let o = run(&["run", f.to_str().unwrap(), "--", "--input"]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
assert_eq!(stdout(&o).trim(), "--input");
}
#[test]
fn run_executes_a_program_and_prints_its_value() {
let f = write("run", PROGRAM);
let o = run(&["run", f.to_str().unwrap()]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
assert_eq!(stdout(&o).trim(), "720");
}
#[test]
fn run_rejects_a_type_error_with_a_failing_exit_code() {
let f = write("run-bad-type", "def bad(a: Int) -> Str\n a\nend\nbad(1)");
let o = run(&["run", f.to_str().unwrap()]);
assert!(!o.status.success(), "a type error must fail the process");
assert!(
stderr(&o).contains("Str") && stderr(&o).contains("Int"),
"the error must name both types: {}",
stderr(&o)
);
assert!(
stdout(&o).trim().is_empty(),
"a rejected program must not print a value: {:?}",
stdout(&o)
);
}
#[test]
fn run_accepts_the_same_program_without_annotations() {
let f = write("run-untyped", "def ok(a)\n a\nend\nok(1)");
let o = run(&["run", f.to_str().unwrap()]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
assert_eq!(stdout(&o).trim(), "1");
}
#[test]
fn a_missing_file_fails_with_the_path_named() {
let o = run(&["run", "/nonexistent/definitely-not-here.b"]);
assert!(!o.status.success());
assert!(
stderr(&o).contains("definitely-not-here.b"),
"the error must name the path: {}",
stderr(&o)
);
}
#[test]
fn fmt_output_is_already_formatted() {
let f = write("fmt-idem", PROGRAM);
let once = run(&["fmt", f.to_str().unwrap()]);
assert!(once.status.success(), "stderr: {}", stderr(&once));
let g = write("fmt-idem-2", &stdout(&once));
let check = run(&["fmt", "--check", g.to_str().unwrap()]);
assert!(
check.status.success(),
"formatted output must satisfy --check; got: {}",
stderr(&check)
);
}
#[test]
fn fmt_check_fails_on_unformatted_input() {
let f = write("fmt-drift", "1 + 2");
let o = run(&["fmt", "--check", f.to_str().unwrap()]);
assert!(!o.status.success(), "unformatted input must fail --check");
assert!(
stderr(&o).contains("not formatted"),
"and say why: {}",
stderr(&o)
);
}
#[test]
fn fmt_check_does_not_rewrite_the_file() {
let original = "1 + 2";
let f = write("fmt-check-pure", original);
let _ = run(&["fmt", "--check", f.to_str().unwrap()]);
assert_eq!(
std::fs::read_to_string(&f).expect("read back"),
original,
"--check must leave the file alone"
);
}
#[test]
fn fmt_write_rewrites_in_place() {
let f = write("fmt-write", "1 + 2");
let o = run(&["fmt", "--write", f.to_str().unwrap()]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
assert_eq!(
std::fs::read_to_string(&f).expect("read back").trim(),
"1 + 2"
);
}
#[test]
fn fmt_does_not_turn_a_call_into_a_method_send() {
let f = write("fmt-call", PROGRAM);
let out = stdout(&run(&["fmt", f.to_str().unwrap()]));
assert!(
out.contains("fact(n - 1)"),
"a recursive call must render as a call: {out}"
);
assert!(
!out.contains(").fact"),
"and must not render as a method send: {out}"
);
}
#[test]
fn ast_keeps_annotations_and_erase_drops_them() {
let f = write("ast-erase", PROGRAM);
let ast = stdout(&run(&["ast", f.to_str().unwrap()]));
let erased = stdout(&run(&["erase", f.to_str().unwrap()]));
assert!(
ast.contains("define-typed") && ast.contains("Int"),
"ast must keep the annotation: {ast}"
);
assert!(
!erased.contains("define-typed") && !erased.contains("Int"),
"erase must drop every trace of it: {erased}"
);
assert!(
erased.contains("(define (fact n)"),
"and leave a plain define: {erased}"
);
}
#[test]
fn erase_is_the_identity_on_untyped_source() {
let f = write("erase-untyped", "def f(x)\n x + 1\nend");
assert_eq!(
stdout(&run(&["erase", f.to_str().unwrap()])).trim(),
stdout(&run(&["ast", f.to_str().unwrap()])).trim()
);
}
#[test]
fn check_reports_zero_analysis_for_an_untyped_program() {
let f = write("check-untyped", "def f(x)\n x + 1\nend\nf(1)");
let o = run(&["check", f.to_str().unwrap()]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
let out = stdout(&o);
assert!(
out.contains("typed declarations: 0"),
"expected no typed decls: {out}"
);
assert!(
out.contains("nodes analysed: 0"),
"an untyped program must cost zero analysis: {out}"
);
}
#[test]
fn check_reports_analysis_for_an_annotated_program() {
let f = write("check-typed", PROGRAM);
let out = stdout(&run(&["check", f.to_str().unwrap()]));
assert!(
out.contains("typed declarations: 1"),
"expected one typed decl: {out}"
);
assert!(
!out.contains("nodes analysed: 0"),
"an annotation must buy real analysis: {out}"
);
}
#[test]
fn check_exits_non_zero_on_a_type_error() {
let f = write("check-bad", "def bad(a: Int) -> Str\n a\nend");
let o = run(&["check", f.to_str().unwrap()]);
assert!(!o.status.success(), "check must fail on a type error");
}
#[test]
fn help_lists_every_subcommand() {
let o = run(&["--help"]);
assert!(o.status.success());
let help = stdout(&o);
for cmd in [
"run", "fmt", "ast", "erase", "check", "test", "deps", "posture", "lsp", "banner", "shift",
"morph", "bluefile", "lock",
] {
assert!(help.contains(cmd), "--help must list `{cmd}`: {help}");
}
}
#[test]
fn no_arguments_is_an_error_not_a_silent_success() {
let o = blue().output().expect("spawn");
assert!(!o.status.success(), "bare `blue` must not exit 0");
}
const SUITE: &str = "def add(a, b)\n a + b\nend\n\ntest \"adds\"\n assert add(1, 2) == 3\nend";
#[test]
fn test_runs_a_passing_suite_and_exits_zero() {
let f = write("test-pass", SUITE);
let o = run(&["test", f.to_str().unwrap()]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
assert!(
stdout(&o).contains("1 passed, 0 failed"),
"expected a tally: {}",
stdout(&o)
);
}
#[test]
fn test_exits_non_zero_when_a_test_fails() {
let f = write("test-fail", "test \"wrong\"\n assert 1 + 1 == 3\nend");
let o = run(&["test", f.to_str().unwrap()]);
assert!(!o.status.success(), "a failing suite must fail the process");
assert!(
stdout(&o).contains("0 passed, 1 failed"),
"tally: {}",
stdout(&o)
);
}
#[test]
fn test_failure_reports_the_expression_as_written() {
let f = write(
"test-msg",
"def add(a, b)\n a + b\nend\n\ntest \"wrong\"\n assert add(1, 2) == 4\nend",
);
let o = run(&["test", f.to_str().unwrap()]);
assert!(
stderr(&o).contains("assert add(1, 2) == 4"),
"the message must be the expression in canonical blue source: {}",
stderr(&o)
);
}
#[test]
fn test_on_a_file_with_no_tests_reports_zero_and_fails() {
let f = write("test-none", "def f(x)\n x\nend");
let o = run(&["test", f.to_str().unwrap()]);
assert!(
stdout(&o).contains("0 test(s)"),
"an empty run must be visibly empty: {}",
stdout(&o)
);
assert!(
!o.status.success(),
"a file with no tests must fail, or a gate over it is vacuous: {}",
stderr(&o)
);
}
const BLUEFILE: &str = "def app_version()\n \"0.2.0\"\nend\n\npackage(\"myapp\", app_version())\nneeds(\"gaming\", \"^1.2\")\nposture(:preceding)";
#[test]
fn deps_reports_the_manifest_including_a_computed_version() {
let f = write("deps", BLUEFILE);
let o = run(&["deps", f.to_str().unwrap()]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
let out = stdout(&o);
assert!(
out.contains("myapp 0.2.0"),
"the version came from a function call: {out}"
);
assert!(out.contains("needs gaming ^1.2.0"), "{out}");
}
#[test]
fn deps_says_it_cannot_resolve_rather_than_printing_a_hollow_resolution() {
let f = write("deps-honest", BLUEFILE);
let out = stdout(&run(&["deps", f.to_str().unwrap()]));
assert!(
out.contains("nothing to resolve against"),
"it must say resolution has no distribution: {out}"
);
assert!(
!out.contains("resolved against"),
"it must not claim a resolution it did not perform: {out}"
);
}
fn distribution(name: &str) -> PathBuf {
let manifest = write_at(
&format!("dist-{name}/gaming/Bluefile"),
"package(\"gaming\", \"1.2.0\")",
);
manifest
.parent()
.and_then(std::path::Path::parent)
.expect("<root>/gaming/Bluefile")
.to_path_buf()
}
#[test]
fn config_show_prints_the_prescribed_bounds() {
let out = stdout(&run(&["config", "default"]));
assert!(out.contains("solver_max_steps: 100000"), "{out}");
assert!(out.contains("max_expr_depth: 256"), "{out}");
}
#[test]
fn config_show_bare_is_the_zero_opinion_floor() {
let out = stdout(&run(&["config", "bare"]));
assert!(out.contains("solver_max_steps: 0"), "{out}");
assert!(out.contains("max_expr_depth: 0"), "{out}");
}
#[test]
fn deps_resolves_against_a_distribution_on_the_load_path() {
let f = write("deps-resolve", BLUEFILE);
let root = distribution("resolve");
let out = stdout(&run_env(
&["deps", f.to_str().unwrap()],
&[("BLUE_PATH", root.to_str().unwrap())],
));
assert!(out.contains("resolved against 1 package(s)"), "{out}");
assert!(out.contains("gaming 1.2.0"), "{out}");
}
#[test]
fn solver_max_steps_is_read_from_the_deployed_yaml() {
let f = write("deps-bounded", BLUEFILE);
let root = distribution("bounded");
let cfg = write_at(
"cfg-solver.yaml",
"solver_max_steps: 0\nmax_expr_depth: 256\n",
);
let o = run_env(
&["deps", f.to_str().unwrap()],
&[
("BLUE_PATH", root.to_str().unwrap()),
("BLUE_CONFIG", cfg.to_str().unwrap()),
],
);
assert!(
!o.status.success(),
"a 0-step budget must not resolve anything; stdout: {} stderr: {}",
stdout(&o),
stderr(&o)
);
assert!(
stderr(&o).contains("gave up after"),
"the failure must name the exhausted budget: {}",
stderr(&o)
);
}
#[test]
fn max_expr_depth_is_read_from_the_deployed_yaml() {
let src = format!("{}1{}", "(".repeat(12), ")".repeat(12));
let f = write("ast-bounded", &src);
let cfg = write_at(
"cfg-depth.yaml",
"solver_max_steps: 100000\nmax_expr_depth: 4\n",
);
assert!(
run(&["ast", f.to_str().unwrap()]).status.success(),
"precondition: the DEFAULT bound accepts this file"
);
let o = run_env(
&["ast", f.to_str().unwrap()],
&[("BLUE_CONFIG", cfg.to_str().unwrap())],
);
assert!(
!o.status.success(),
"a configured bound of 4 must refuse depth 12: {}",
stdout(&o)
);
assert!(
stderr(&o).contains("nests deeper than 4"),
"the error must name the CONFIGURED bound, not the constant: {}",
stderr(&o)
);
}
#[test]
fn an_explicit_tier_beats_the_config_file() {
let src = format!("{}1{}", "(".repeat(12), ")".repeat(12));
let f = write("ast-tier", &src);
let cfg = write_at(
"cfg-tier.yaml",
"solver_max_steps: 100000\nmax_expr_depth: 4\n",
);
let o = run_env(
&["ast", f.to_str().unwrap()],
&[
("BLUE_CONFIG", cfg.to_str().unwrap()),
("BLUE_TIER", "default"),
],
);
assert!(
o.status.success(),
"BLUE_TIER=default must ignore the file's lower bound: {}",
stderr(&o)
);
}
#[test]
fn posture_reports_the_declared_floor() {
let f = write("posture", BLUEFILE);
let o = run(&["posture", f.to_str().unwrap()]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
let out = stdout(&o);
assert!(out.contains("Preceding"), "the declared `when`: {out}");
assert!(
out.contains("unrestricted"),
"undeclared reach is the top: {out}"
);
}
#[test]
fn a_bluefile_without_a_package_call_fails() {
let f = write("bluefile-nopkg", "needs(\"a\", \"*\")");
let o = run(&["deps", f.to_str().unwrap()]);
assert!(!o.status.success(), "must fail");
assert!(
stderr(&o).contains("package"),
"and say what is missing: {}",
stderr(&o)
);
}
#[test]
fn fmt_write_preserves_comments() {
let f = write("fmt-comments", "# keep me\n1 + 2\n");
let o = run(&["fmt", "--write", f.to_str().unwrap()]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
let after = std::fs::read_to_string(&f).expect("read back");
assert!(
after.contains("# keep me"),
"the comment must survive: {after:?}"
);
assert!(
after.contains("1 + 2"),
"and the code must be formatted: {after:?}"
);
}
#[test]
fn fmt_check_settles_on_a_commented_file() {
let f = write("fmt-check-comments", "# note\n1 + 2\n");
let o = run(&["fmt", "--check", f.to_str().unwrap()]);
assert!(
o.status.success(),
"an already-canonical commented file must pass --check: {}",
stderr(&o)
);
}
#[test]
fn fmt_write_refuses_a_comment_it_cannot_place() {
let original = "def f(x)\n # inside\n x\nend\n";
let f = write("fmt-inner-comment", original);
let o = run(&["fmt", "--write", f.to_str().unwrap()]);
assert!(!o.status.success(), "it must refuse");
assert!(
stderr(&o).contains("comment"),
"and say why: {}",
stderr(&o)
);
assert_eq!(
std::fs::read_to_string(&f).expect("read back"),
original,
"the file must be untouched"
);
}
#[test]
fn fmt_write_still_works_without_comments() {
let f = write("fmt-nocomments", "1 + 2");
let o = run(&["fmt", "--write", f.to_str().unwrap()]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
assert_eq!(
std::fs::read_to_string(&f).expect("read back").trim(),
"1 + 2"
);
}
fn write_bytes(name: &str, bytes: &[u8]) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("blue-cli-input-{name}"));
std::fs::write(&p, bytes).expect("write input");
p
}
const SCHEMA: &[u8] = b"id,name,email\n";
const SCHEMA_HASH: &str = "b3:1e8c349d3151d7c4c19836c6ba0e0d82270a2c174d98af445e102055040ac92c";
fn schema_program() -> String {
let mut s = String::from("definput(\"schema\", \"");
s.push_str(SCHEMA_HASH);
s.push_str("\")\n");
s.push_str(
"defmacro column_count()\n quote\n length(split(trim(input(\"schema\")), \",\"))\n end\nend\n\
column_count()",
);
s
}
#[test]
fn a_macro_reads_a_content_addressed_input() {
let f = write("input-ok", &schema_program());
let schema = write_bytes("schema-ok", SCHEMA);
let o = run(&[
"run",
f.to_str().unwrap(),
"--input",
&format!("schema={}", schema.display()),
]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
assert_eq!(stdout(&o).trim(), "3", "three columns in the schema");
}
#[test]
fn input_bytes_must_match_the_declared_hash() {
let f = write("input-tampered", &schema_program());
let schema = write_bytes("schema-tampered", b"id,name,email,extra\n");
let o = run(&[
"run",
f.to_str().unwrap(),
"--input",
&format!("schema={}", schema.display()),
]);
assert!(!o.status.success(), "a hash mismatch must fail the run");
let err = stderr(&o);
assert!(err.contains(SCHEMA_HASH), "must name the expected: {err}");
assert!(err.contains("hash to"), "and the actual: {err}");
}
#[test]
fn a_declared_input_with_no_flag_names_the_missing_flag() {
let f = write("input-missing", &schema_program());
let o = run(&["run", f.to_str().unwrap()]);
assert!(!o.status.success());
let err = stderr(&o);
assert!(err.contains("--input schema="), "must name the flag: {err}");
}
#[test]
fn an_undeclared_input_name_is_its_own_error() {
let f = write("input-undeclared", &schema_program());
let schema = write_bytes("schema-undeclared", SCHEMA);
let o = run(&[
"run",
f.to_str().unwrap(),
"--input",
&format!("nosuch={}", schema.display()),
]);
assert!(!o.status.success());
assert!(
stderr(&o).contains("never declares it"),
"got: {}",
stderr(&o)
);
}
#[test]
fn a_program_without_declarations_still_runs() {
let f = write("input-none", PROGRAM);
let o = run(&["run", f.to_str().unwrap()]);
assert!(o.status.success(), "stderr: {}", stderr(&o));
assert_eq!(stdout(&o).trim(), "720");
}
fn manifest_json(name: &str, body: &str) -> serde_json::Value {
let f = write_at(
&format!("json-{name}/Bluefile"),
&(String::from("package(\"proj\", \"0.3.0\")\n") + body),
);
let o = run(&["bluefile", "--json", f.to_str().unwrap()]);
assert!(o.status.success(), "{body}: {}", stderr(&o));
serde_json::from_slice(&o.stdout).expect("--json prints JSON")
}
#[test]
fn bluefile_json_reads_back_identity_needs_and_posture() {
let v = manifest_json("identity", "needs(\"kazu\", \"^0.1\")\nposture(:preceding)");
assert_eq!(v["schema"], 1);
assert_eq!(v["name"], "proj");
assert_eq!(v["version"], "0.3.0");
assert_eq!(v["needs"], serde_json::json!({ "kazu": "^0.1.0" }));
assert_eq!(v["when"], "preceding");
}
#[test]
fn bluefile_json_reads_back_source() {
let v = manifest_json(
"source",
"source(\"blue\", \"github:pleme-io/blue\", \"bidamas\")",
);
assert_eq!(
v["sources"],
serde_json::json!({ "blue": { "url": "github:pleme-io/blue", "dir": "bidamas" } })
);
}
#[test]
fn bluefile_json_reads_back_packages() {
let v = manifest_json("packages", "packages(\"bidamas\")");
assert_eq!(v["packages"], serde_json::json!(["bidamas"]));
}
#[test]
fn bluefile_json_reads_back_run() {
let v = manifest_json(
"run",
"run(\"games\", \"src/games.b\")\nrun(\"report\", \"src/report.b\", [\"games\"])",
);
assert_eq!(
v["runs"],
serde_json::json!({
"games": { "file": "src/games.b", "reads": [] },
"report": { "file": "src/report.b", "reads": ["games"] }
})
);
}
#[test]
fn bluefile_json_reads_back_tool() {
let v = manifest_json("tool", "tool(\"duckdb\")");
assert_eq!(v["tools"], serde_json::json!(["duckdb"]));
}
#[test]
fn bluefile_json_reads_back_check() {
let v = manifest_json("check", "check(\"unit\", \"tests/unit.b\")");
assert_eq!(
v["checks"],
serde_json::json!({ "unit": { "file": "tests/unit.b" } })
);
}
#[test]
fn bluefile_json_reads_back_app() {
let v = manifest_json("app", "app(\"report\", \"bin/report.b\")");
assert_eq!(
v["apps"],
serde_json::json!({ "report": { "file": "bin/report.b" } })
);
}
#[test]
fn bluefile_json_reads_back_catalog() {
let v = manifest_json(
"catalog",
"packages(\"bidamas\")\ncatalog(\"bidamas/CATALOG.md\")",
);
assert_eq!(v["catalog"], "bidamas/CATALOG.md");
}
#[test]
fn bluefile_json_reads_back_generate() {
let v = manifest_json(
"generate",
"generate(\"tables\", \"src/tables.rs\", \"gen/tables.b\")",
);
assert_eq!(
v["generated"],
serde_json::json!({ "tables": { "output": "src/tables.rs", "program": "gen/tables.b" } })
);
}
#[test]
fn bluefile_json_refuses_a_misspelled_word_naming_it() {
let f = write_at(
"json-typo/Bluefile",
"package(\"p\", \"0.1.0\")\ntoool(\"jq\")",
);
let o = run(&["bluefile", "--json", f.to_str().unwrap()]);
assert!(!o.status.success(), "a misspelled word must not exit 0");
assert!(stderr(&o).contains("toool"), "{}", stderr(&o));
assert!(
o.stdout.is_empty(),
"nothing may be printed for a refused manifest"
);
}
#[test]
fn bluefile_json_refuses_a_malformed_call() {
let f = write_at(
"json-malformed/Bluefile",
"package(\"p\", \"0.1.0\")\nneeds(\"a\", 1)",
);
let o = run(&["bluefile", "--json", f.to_str().unwrap()]);
assert!(!o.status.success());
assert!(
stderr(&o).contains("`needs`: `range` must be a string"),
"{}",
stderr(&o)
);
}
#[test]
fn bluefile_needs_exactly_one_mode_and_json_one_file() {
let f = write_at("json-mode/Bluefile", "package(\"p\", \"0.1.0\")");
let path = f.to_str().unwrap();
assert!(!run(&["bluefile", path]).status.success(), "no mode");
assert!(
!run(&["bluefile", "--json", "--confirm", path])
.status
.success(),
"both modes"
);
let o = run(&["bluefile", "--json", path, path]);
assert!(!o.status.success(), "two files for --json");
assert!(stderr(&o).contains("one manifest"), "{}", stderr(&o));
}
fn lockable(name: &str, src: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("blue-cli-test-lock-{name}"));
let _ = std::fs::remove_file(dir.join("Bluefile.lock"));
std::fs::create_dir_all(&dir).expect("create package dir");
std::fs::write(dir.join("Bluefile"), src).expect("write Bluefile");
dir
}
fn confirm(dir: &std::path::Path) -> Output {
run(&[
"bluefile",
"--confirm",
dir.join("Bluefile").to_str().unwrap(),
])
}
const LOCKABLE: &str = "package(\"lockme\", \"1.0.0\")\nneeds(\"kazu\", \"^0.1\")\n";
#[test]
fn lock_writes_a_lock_that_confirm_accepts() {
let dir = lockable("fresh", LOCKABLE);
let o = run(&["lock", dir.to_str().unwrap()]);
assert!(o.status.success(), "{}", stderr(&o));
let lock: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(dir.join("Bluefile.lock")).expect("the lock was written"),
)
.expect("the lock is JSON");
assert_eq!(lock["schema"], 1);
assert_eq!(lock["sources"], serde_json::json!({}));
assert!(lock["bluefile_b3"].as_str().unwrap().starts_with("b3:"));
let json = run(&["bluefile", "--json", dir.join("Bluefile").to_str().unwrap()]);
let manifest: serde_json::Value = serde_json::from_slice(&json.stdout).expect("json");
assert_eq!(
lock["manifest"], manifest,
"the lock's manifest IS --json's"
);
let c = confirm(&dir);
assert!(c.status.success(), "{}", stderr(&c));
assert!(
stdout(&c).contains("\"status\":\"fresh\""),
"{}",
stdout(&c)
);
}
#[test]
fn confirm_fails_on_a_zeroed_hash() {
let dir = lockable("zeroed", LOCKABLE);
assert!(run(&["lock", dir.to_str().unwrap()]).status.success());
let path = dir.join("Bluefile.lock");
let mut lock: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
lock["bluefile_b3"] = serde_json::Value::from(String::from("b3:") + &"0".repeat(64));
std::fs::write(&path, lock.to_string()).unwrap();
let c = confirm(&dir);
assert!(!c.status.success(), "a zeroed hash must not confirm");
let line: serde_json::Value = serde_json::from_slice(&c.stdout).expect("json line");
assert_eq!(line["status"], "stale");
assert_eq!(line["reason"]["kind"], "hash");
assert!(stderr(&c).contains("blue lock"), "{}", stderr(&c));
}
#[test]
fn confirm_fails_until_an_edited_bluefile_is_relocked() {
let dir = lockable("edited", LOCKABLE);
assert!(run(&["lock", dir.to_str().unwrap()]).status.success());
std::fs::write(
dir.join("Bluefile"),
String::from(LOCKABLE) + "tool(\"jq\")\n",
)
.unwrap();
assert!(
!confirm(&dir).status.success(),
"an edited Bluefile is stale"
);
assert!(run(&["lock", dir.to_str().unwrap()]).status.success());
assert!(confirm(&dir).status.success(), "relocked is fresh");
}
#[test]
fn confirm_fails_when_there_is_no_lock() {
let dir = lockable("missing", LOCKABLE);
let c = confirm(&dir);
assert!(!c.status.success());
assert!(
stdout(&c).contains("\"kind\":\"missing\""),
"{}",
stdout(&c)
);
}
#[test]
fn every_bidama_lock_is_confirmed_fresh() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../bidamas");
let mut manifests: Vec<String> = std::fs::read_dir(&root)
.expect("bidamas/")
.filter_map(Result::ok)
.map(|e| e.path().join("Bluefile"))
.filter(|p| p.is_file())
.map(|p| p.to_string_lossy().into_owned())
.collect();
manifests.sort();
let mut args = vec!["bluefile", "--confirm"];
args.extend(manifests.iter().map(String::as_str));
let o = run(&args);
assert!(o.status.success(), "stale bidama locks:\n{}", stderr(&o));
let fresh = stdout(&o).matches("\"status\":\"fresh\"").count();
assert_eq!(fresh, manifests.len());
assert!(fresh >= 21, "only {fresh} bidama locks were confirmed");
}