use std::io::Write;
use std::process::{Command, Output, Stdio};
fn rune(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_rune"))
.args(args)
.output()
.expect("failed to spawn rune binary")
}
fn rune_stdin(args: &[&str], input: &str) -> Output {
let mut child = Command::new(env!("CARGO_BIN_EXE_rune"))
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn rune binary");
child.stdin.take().unwrap().write_all(input.as_bytes()).unwrap();
child.wait_with_output().expect("failed to wait on rune")
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn stderr(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
#[test]
fn eval_arithmetic_prints_atom() {
let out = rune(&["eval", "6 * 7"]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert_eq!(stdout(&out).trim(), "42");
}
#[test]
fn eval_self_resolves_to_a_neuron() {
let out = rune(&["eval", "~self"]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let n: u64 = stdout(&out).trim().parse().expect("expected an atom");
assert_ne!(n, 0, "~self should resolve to a neuron");
}
#[test]
fn eval_prysm_value_renders_as_chunk() {
let out = rune(&["eval", r#"text("hi")"#]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert_eq!(stdout(&out).trim(), "hi");
}
#[test]
fn eval_error_decodes_message() {
let out = rune(&["eval", r#"error("boom")"#]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let s = stdout(&out);
assert!(s.contains("boom"), "got: {s}");
assert!(!s.contains("level"), "raw molecule leaked: {s}");
}
#[test]
fn eval_emit_act_renders_as_chunk() {
let out = rune(&["eval", r#"emit(text("live act"))"#]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert!(stdout(&out).contains("live act"), "got: {}", stdout(&out));
}
#[test]
fn eval_col_yields_multiple_chunks() {
let out = rune(&["eval", r#"col(text("a"), text("b"))"#]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
let s = stdout(&out);
let lines: Vec<_> = s.lines().filter(|l| !l.is_empty()).collect();
assert_eq!(lines.len(), 2, "expected one chunk per element, got: {lines:?}");
}
#[test]
fn eval_pure_register_runs() {
let out = rune(&["eval", "--pure", "(mul 6 7)"]);
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert_eq!(stdout(&out).trim(), "42");
}
#[test]
fn run_pure_register_via_pragma() {
let path = std::env::temp_dir().join("rune_cli_pragma.rune");
std::fs::write(&path, ":: register: rune\n(mul 8 8)").unwrap();
let out = rune(&["run", path.to_str().unwrap()]);
let _ = std::fs::remove_file(&path);
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert_eq!(stdout(&out).trim(), "64");
}
#[test]
fn fmt_converts_pure_input_to_classic() {
let path = std::env::temp_dir().join("rune_cli_fmt_pure.rune");
std::fs::write(&path, ":: register: rune\n(mul x 2)").unwrap();
let out = rune(&["fmt", "--to=rust", path.to_str().unwrap()]);
let _ = std::fs::remove_file(&path);
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert_eq!(stdout(&out).trim(), "x * 2");
}
#[test]
fn eval_parse_error_exits_nonzero() {
let out = rune(&["eval", "6 *"]);
assert!(!out.status.success());
assert!(stderr(&out).contains("parse error"), "got: {}", stderr(&out));
}
#[test]
fn fmt_converts_classic_to_pure_register() {
let path = std::env::temp_dir().join("rune_cli_fmt_to_rune.rune");
std::fs::write(&path, "6 * 7").unwrap();
let out = rune(&["fmt", "--to=rune", path.to_str().unwrap()]);
let _ = std::fs::remove_file(&path);
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert_eq!(stdout(&out).trim(), "(mul 6 7)");
}
#[test]
fn check_passes_on_well_typed_source() {
let path = std::env::temp_dir().join("rune_cli_check_ok.rune");
std::fs::write(&path, "6 * 7").unwrap();
let out = rune(&["check", path.to_str().unwrap()]);
let _ = std::fs::remove_file(&path);
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert_eq!(stdout(&out).trim(), "ok");
}
#[test]
fn check_catches_mold_mismatch() {
let path = std::env::temp_dir().join("rune_cli_check_bad.rune");
std::fs::write(&path, "let x: @ud = \"hi\"\nx").unwrap();
let out = rune(&["check", path.to_str().unwrap()]);
let _ = std::fs::remove_file(&path);
assert!(!out.status.success(), "expected nonzero exit on mold error");
assert!(stderr(&out).contains("mold error"), "got: {}", stderr(&out));
}
#[test]
fn help_lists_every_command() {
let out = rune(&["help"]);
assert!(out.status.success());
let s = stdout(&out);
for cmd in ["run", "eval", "fmt", "check"] {
assert!(s.contains(cmd), "help is missing `{cmd}`: {s}");
}
}
#[test]
fn unknown_command_exits_nonzero() {
let out = rune(&["frobnicate"]);
assert!(!out.status.success());
assert!(stderr(&out).contains("unknown command"), "got: {}", stderr(&out));
}
#[test]
fn version_prints_semver() {
let out = rune(&["--version"]);
assert!(out.status.success());
assert!(stdout(&out).starts_with("rune 0."), "got: {}", stdout(&out));
}
#[test]
fn repl_accumulates_bindings_across_lines() {
let out = rune_stdin(&["repl"], "let x = 5\nlet y = 7\nx + y\n:q\n");
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert!(stdout(&out).contains("= 12"), "got: {}", stdout(&out));
}
#[test]
fn repl_survives_a_bad_line() {
let out = rune_stdin(&["repl"], "6 *\n6 * 7\n:q\n");
assert!(out.status.success(), "stderr: {}", stderr(&out));
let s = stdout(&out);
assert!(s.contains('โ'), "expected an error marker, got: {s}");
assert!(s.contains("= 42"), "expected recovery, got: {s}");
}
#[test]
fn repl_nox_shows_lowered_formula() {
let out = rune_stdin(&["repl"], ":nox 6 * 7\n:q\n");
assert!(out.status.success(), "stderr: {}", stderr(&out));
assert!(stdout(&out).contains("[7 "), "got: {}", stdout(&out));
}
#[test]
fn repl_help_is_categorized() {
let out = rune_stdin(&["repl"], ":help\n:q\n");
assert!(out.status.success(), "stderr: {}", stderr(&out));
let s = stdout(&out);
assert!(s.contains("commands"), "got: {s}");
assert!(s.contains("builtins"), "got: {s}");
}