use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
fn bynk() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_bynk"))
}
fn bynkc_sibling() -> Option<PathBuf> {
let p = bynk().parent().unwrap().join("bynkc");
p.exists().then_some(p)
}
fn scratch(name: &str) -> PathBuf {
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(name);
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write(path: &Path, contents: &str) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, contents).unwrap();
}
fn run_bynk_in(cwd: &Path, args: &[&str]) -> (i32, String, String) {
run_in(&bynk(), cwd, args, None)
}
fn run_in(bin: &Path, cwd: &Path, args: &[&str], stdin: Option<&str>) -> (i32, String, String) {
let mut cmd = Command::new(bin);
cmd.args(args)
.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if stdin.is_some() {
cmd.stdin(Stdio::piped());
}
let mut child = cmd.spawn().expect("spawn");
if let Some(text) = stdin {
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(text.as_bytes())
.unwrap();
}
let out = child.wait_with_output().expect("wait");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
const CLEAN_COMMONS: &str = "commons calc {\n fn dbl(n: Int) -> Int { n + n }\n}\n";
const BAD_SOURCE: &str = "commons demo\n\nlet x: Int = \"nope\"\n";
#[test]
fn check_clean_project_exits_zero() {
let proj = scratch("check-clean");
write(
&proj.join("bynk.toml"),
"[project]\nname = \"calc\"\nversion = \"0.1.0\"\n",
);
write(&proj.join("src/calc.bynk"), CLEAN_COMMONS);
let (code, _out, err) = run_bynk_in(&proj, &["check", "."]);
assert_eq!(code, 0, "clean project should check clean; stderr:\n{err}");
}
#[test]
fn check_single_file_error_short_is_deterministic() {
let dir = scratch("check-short");
write(&dir.join("bad.bynk"), BAD_SOURCE);
let (code, out, err) = run_bynk_in(&dir, &["check", "bad.bynk", "--format", "short"]);
assert_eq!(code, 1, "an error source must exit non-zero");
assert!(out.is_empty(), "diagnostics go to stderr, not stdout");
let first = err.lines().next().unwrap_or_default();
assert!(
first.starts_with("bad.bynk:") && first.contains("error["),
"expected a `bad.bynk:line:col: error[...]` short line, got:\n{err}"
);
}
#[test]
fn check_matches_bynkc_when_present() {
let Some(bynkc) = bynkc_sibling() else {
eprintln!("skipping: sibling bynkc not built (run the workspace test suite for parity)");
return;
};
let dir = scratch("check-parity");
write(&dir.join("bad.bynk"), BAD_SOURCE);
write(&dir.join("ok.bynk"), CLEAN_COMMONS);
for args in [
vec!["check", "bad.bynk"],
vec!["check", "bad.bynk", "--format", "short"],
vec!["check", "ok.bynk"],
] {
let driven = run_bynk_in(&dir, &args);
let direct = run_in(&bynkc, &dir, &args, None);
assert_eq!(
driven,
direct,
"`bynk {0}` must match `bynkc {0}` exactly",
args.join(" ")
);
}
}
const MESSY: &str = "commons calc {\n\n\n fn dbl(n: Int) -> Int { n + n }\n}\n";
fn canonical(source: &str) -> String {
bynk_fmt::format_source(source, &bynk_fmt::FormatOptions::default()).expect("MESSY parses")
}
#[test]
fn fmt_stdin_writes_canonical_to_stdout() {
let dir = scratch("fmt-stdin");
let want = canonical(MESSY);
assert_ne!(
MESSY, want,
"MESSY must be non-canonical for this test to bite"
);
let (code, out, err) = run_in(&bynk(), &dir, &["fmt", "-"], Some(MESSY));
assert_eq!(code, 0, "stdin fmt should succeed; stderr:\n{err}");
assert_eq!(out, want, "`bynk fmt -` must emit the canonical form");
}
#[test]
fn fmt_check_flags_noncanonical_without_writing() {
let dir = scratch("fmt-check");
let file = dir.join("calc.bynk");
write(&file, MESSY);
let (code, _out, err) = run_bynk_in(&dir, &["fmt", "calc.bynk", "--check"]);
assert_eq!(
code, 1,
"--check must exit non-zero on a non-canonical file"
);
assert!(
err.contains("not canonically formatted"),
"expected a non-canonical notice, got:\n{err}"
);
assert_eq!(
std::fs::read_to_string(&file).unwrap(),
MESSY,
"--check must not rewrite the file"
);
}
#[test]
fn fmt_rewrites_in_place() {
let dir = scratch("fmt-write");
let file = dir.join("calc.bynk");
write(&file, MESSY);
let (code, _out, err) = run_bynk_in(&dir, &["fmt", "calc.bynk"]);
assert_eq!(code, 0, "fmt should succeed; stderr:\n{err}");
assert_eq!(
std::fs::read_to_string(&file).unwrap(),
canonical(MESSY),
"fmt must rewrite the file to its canonical form"
);
}
#[test]
fn test_discovery_delegates_and_matches_bynkc() {
let Some(bynkc) = bynkc_sibling() else {
eprintln!("skipping: sibling bynkc not built (delegation parity needs it)");
return;
};
let proj = scratch("test-discovery");
write(
&proj.join("bynk.toml"),
"[project]\nname = \"calc\"\nversion = \"0.1.0\"\n",
);
write(
&proj.join("src/calc.bynk"),
"commons calc {\n fn dbl(n: Int) -> Int { n + n }\n}\n\nsuite calc {\n case \"doubles\" {\n expect dbl(3) == 6\n }\n}\n",
);
let args = ["test", ".", "--no-run", "--format", "json"];
let driven = run_bynk_in(&proj, &args);
let direct = run_in(&bynkc, &proj, &args, None);
assert_eq!(driven.0, direct.0, "delegated exit code must match bynkc's");
assert_eq!(
driven.1, direct.1,
"delegated discovery document must match bynkc's byte-for-byte"
);
}