use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::mpsc;
use std::time::Duration;
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")
.env_remove("BLUE_LANG");
c
}
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("blue-scripts-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("scratch dir");
dir.canonicalize().expect("canonical scratch dir")
}
fn put(path: &Path, text: &str) {
std::fs::create_dir_all(path.parent().unwrap()).expect("mkdir");
std::fs::write(path, text).expect("write fixture");
}
fn out(o: &Output) -> String {
String::from_utf8_lossy(&o.stdout).into_owned()
}
fn err(o: &Output) -> String {
String::from_utf8_lossy(&o.stderr).into_owned()
}
const ARGS: &str = "write_stdout(\"#{join(argv, \",\")}\\n\")\n42\n";
#[test]
fn a_bare_file_runs_quietly_with_its_arguments_verbatim() {
let dir = scratch("bare");
let file = dir.join("args.b");
put(&file, ARGS);
let o = blue()
.arg(&file)
.args(["a", "-q", "--", "b"])
.output()
.unwrap();
assert!(o.status.success(), "{}", err(&o));
assert_eq!(out(&o), "a,-q,--,b\n");
let o = blue()
.arg("run")
.arg(&file)
.args(["--", "a"])
.output()
.unwrap();
assert_eq!(out(&o), "a\n42\n");
}
#[test]
fn a_failing_script_exits_non_zero() {
let dir = scratch("failing");
let file = dir.join("boom.b");
put(&file, "raise(:boom, \"no\")\n");
let o = blue().arg(&file).output().unwrap();
assert!(!o.status.success(), "{}", out(&o));
assert!(
err(&o).contains("boom: no"),
"the program did not run: {}",
err(&o)
);
}
#[test]
fn a_missing_b_file_is_reported_as_a_missing_file() {
let o = blue()
.arg("/nonexistent/blue-scripts/x.b")
.output()
.unwrap();
assert!(!o.status.success());
assert!(
err(&o).contains("x.b") && !err(&o).contains("subcommand"),
"{}",
err(&o)
);
}
#[cfg(unix)]
#[test]
fn a_shebang_script_runs_directly() {
let dir = scratch("shebang");
let script = dir.join("greet");
put(
&script,
"#!/usr/bin/env blue\nuse(\"kazu\", [:clamp])\nuse(\"retsu\", [:size])\n\nwrite_stdout(\"#{size(argv)} #{clamp(99, 1, 10)}\\n\")\n",
);
executable(&script);
let bin = Path::new(env!("CARGO_BIN_EXE_blue")).parent().unwrap();
let path = std::env::join_paths(std::iter::once(bin.to_path_buf()).chain(
std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default()),
))
.unwrap();
let o = Command::new(&script)
.args(["x", "y"])
.env("PATH", path)
.env_remove("BLUE_PATH")
.env_remove("BLUE_CONFIG")
.env_remove("BLUE_TIER")
.output()
.expect("the OS runs the script");
assert!(o.status.success(), "{}", err(&o));
assert_eq!(out(&o), "2 10\n");
}
#[test]
fn a_script_with_no_structure_uses_the_standard_distribution() {
let dir = scratch("standard");
let file = dir.join("s.b");
put(
&file,
"use(\"retsu\", [:first])\nuse(\"shuugou\", [:frequencies])\n\nwrite_stdout(\"#{first(first(frequencies([:a, :b, :a])))}\\n\")\n",
);
let o = blue().arg(&file).output().unwrap();
assert!(o.status.success(), "{}", err(&o));
assert_eq!(out(&o), "a\n");
for door in ["check", "test"] {
let o = blue().arg(door).arg(&file).output().unwrap();
assert!(!err(&o).contains("no bidama"), "{door}: {}", err(&o));
}
}
#[test]
fn a_script_in_a_project_uses_the_projects_packages() {
let dir = scratch("project");
let proj = dir.join("proj");
put(
&proj.join("Bluefile"),
"package(\"my-proj\", \"0.1.0\")\n\npackages(\"pkgs\")\n",
);
put(
&proj.join("pkgs/hoshi/Bluefile"),
"package(\"hoshi\", \"0.1.0\")\n",
);
put(
&proj.join("pkgs/hoshi/hoshi.b"),
"def twinkle()\n \"*\"\nend\n",
);
let script = "use(\"hoshi\", [:twinkle])\n\nwrite_stdout(\"#{twinkle()}\\n\")\n";
put(&proj.join("tools/s.b"), script);
let o = blue().arg(proj.join("tools/s.b")).output().unwrap();
assert!(o.status.success(), "{}", err(&o));
assert_eq!(out(&o), "*\n");
put(&dir.join("s.b"), script);
let o = blue().arg(dir.join("s.b")).output().unwrap();
assert!(!o.status.success());
assert!(err(&o).contains("no bidama named \"hoshi\""), "{}", err(&o));
}
#[test]
fn blue_path_overrides_the_compiled_in_distribution() {
let dir = scratch("override");
put(
&dir.join("root/retsu/Bluefile"),
"package(\"retsu\", \"0.1.0\")\n",
);
put(
&dir.join("root/retsu/retsu.b"),
"def precedence_marker()\n 42\nend\n",
);
let file = dir.join("s.b");
put(
&file,
"use(\"retsu\", [:precedence_marker])\n\nwrite_stdout(\"#{precedence_marker()}\\n\")\n",
);
let o = blue().arg(&file).output().unwrap();
assert!(!o.status.success(), "the compiled-in retsu has no marker");
let o = blue()
.env("BLUE_PATH", dir.join("root"))
.arg(&file)
.output()
.unwrap();
assert!(o.status.success(), "{}", err(&o));
assert_eq!(out(&o), "42\n");
}
#[test]
fn a_subcommand_beats_a_file_of_its_name() {
let dir = scratch("precedence");
put(
&dir.join("test"),
"#!/usr/bin/env blue\nwrite_stdout(\"script\\n\")\n",
);
let o = blue().current_dir(&dir).arg("test").output().unwrap();
assert!(
!o.status.success(),
"`blue test` with no FILE is a usage error"
);
assert!(!out(&o).contains("script"), "{}", out(&o));
let o = blue().current_dir(&dir).arg("./test").output().unwrap();
assert!(o.status.success(), "{}", err(&o));
assert_eq!(out(&o), "script\n");
}
#[test]
fn a_file_without_a_blue_shebang_is_not_run() {
let dir = scratch("not-a-script");
put(&dir.join("notes"), "#!/bin/sh\nwrite_stdout(\"ran\\n\")\n");
let o = blue().arg(dir.join("notes")).output().unwrap();
assert!(!o.status.success());
assert!(!out(&o).contains("ran"));
assert!(err(&o).contains("subcommand"), "{}", err(&o));
}
#[test]
fn the_formatter_keeps_the_shebang() {
let dir = scratch("fmt");
let file = dir.join("s.b");
put(
&file,
"#!/usr/bin/env blue\nx = 1\n\n\n\nwrite_stdout(\"#{x}\\n\")\n",
);
let o = blue().arg(&file).output().unwrap();
assert!(o.status.success(), "{}", err(&o));
let text = std::fs::read_to_string(&file).unwrap();
assert!(text.starts_with("#!/usr/bin/env blue\n"), "{text}");
let o = blue().args(["fmt", "--check"]).arg(&file).output().unwrap();
assert!(o.status.success(), "{}", err(&o));
}
#[test]
fn blue_new_writes_what_passes_its_own_gates() {
let dir = scratch("new");
let ok = |o: Output| {
assert!(o.status.success(), "{}{}", out(&o), err(&o));
out(&o)
};
ok(blue()
.current_dir(&dir)
.args(["new", "kumo"])
.output()
.unwrap());
ok(blue()
.current_dir(&dir)
.args(["new", "--bidama", "hoshi"])
.output()
.unwrap());
ok(blue()
.current_dir(&dir)
.args(["new", "--script", "hi.b"])
.output()
.unwrap());
for test in ["kumo/bidamas/kumo/kumo.b", "hoshi/hoshi.b"] {
let o = ok(blue()
.current_dir(&dir)
.args(["test", test])
.output()
.unwrap());
assert!(o.contains("passed, 0 failed"), "{test}: {o}");
}
ok(blue()
.current_dir(&dir)
.args(["fmt", "--check", "kumo/main.b", "kumo/Bluefile"])
.args([
"kumo/bidamas/kumo/kumo.b",
"hoshi/hoshi.b",
"hoshi/Bluefile",
"hi.b",
])
.output()
.unwrap());
ok(blue()
.current_dir(&dir)
.args(["bluefile", "--confirm", "kumo/Bluefile"])
.args(["kumo/bidamas/kumo/Bluefile", "hoshi/Bluefile"])
.output()
.unwrap());
assert!(dir.join("kumo/flake.nix").is_file());
let o = ok(blue()
.current_dir(&dir)
.args(["kumo/main.b", "blue"])
.output()
.unwrap());
assert_eq!(o, "hello, blue\n");
let o = ok(blue()
.current_dir(&dir)
.args(["hi.b", "x"])
.output()
.unwrap());
assert_eq!(o, "hello, x (1 argument(s))\n");
for refused in [
&["new", "kumo"][..],
&["new", "my-tool"],
&["new", "--bidama", "retsu"],
] {
let o = blue().current_dir(&dir).args(refused).output().unwrap();
assert!(!o.status.success(), "{refused:?} was accepted");
}
}
#[test]
fn blue_watch_reruns_on_save() {
let dir = scratch("watch");
let file = dir.join("w.b");
let passing = "def f()\n 1\nend\n\ntest \"f is one\"\n assert f == 1\nend\n";
put(&file, passing);
let mut child = blue()
.args(["watch", "test"])
.arg(&file)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn blue watch");
let (tx, rx) = mpsc::channel();
let stdout = child.stdout.take().unwrap();
std::thread::spawn(move || {
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
if tx.send(line).is_err() {
break;
}
}
});
let wait_for = |want: &str| -> Vec<String> {
let mut seen = Vec::new();
while let Ok(line) = rx.recv_timeout(Duration::from_secs(20)) {
let done = line.contains(want);
seen.push(line);
if done {
return seen;
}
}
panic!("no line containing {want:?}; saw {seen:?}");
};
let first = wait_for("── watching");
assert!(
first
.iter()
.any(|l| l == "── ok" || l.starts_with("── ok in")),
"{first:?}"
);
std::thread::sleep(Duration::from_millis(1100));
put(&file, &passing.replace(" 1\n", " 2\n"));
let second = wait_for("── watching");
let _ = child.kill();
let _ = child.wait();
assert!(
second.iter().any(|l| l.starts_with("── failed")),
"the save did not rerun the tests: {second:?}"
);
}
#[cfg(unix)]
fn executable(path: &Path) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
}