use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
fn run(args: &[&str]) -> (bool, String, String) {
let out = ilo()
.args(args)
.output()
.unwrap_or_else(|e| panic!("failed to spawn ilo: {e}"));
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
)
}
fn write_temp(content: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("prog.ilo");
std::fs::write(&path, content).expect("write temp ilo");
(dir, path)
}
#[test]
fn single_fn_file_runs_with_no_func_arg() {
let (_dir, path) = write_temp("f>n;42\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap()]);
assert!(ok, "expected success; stderr: {stderr}");
assert_eq!(stdout.trim(), "42");
assert!(
!stdout.contains("\"declarations\""),
"no AST dump expected; got: {stdout}"
);
}
#[test]
fn single_fn_file_runs_with_positional_args() {
let (_dir, path) = write_temp("double x:n>n;*x 2\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap(), "7"]);
assert!(ok, "stderr: {stderr}");
assert_eq!(stdout.trim(), "14");
}
#[test]
fn multi_fn_file_with_no_func_arg_lists_and_exits_nonzero() {
let (_dir, path) = write_temp("foo>n;1\nbar>n;2\nbaz>n;3\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap()]);
assert!(!ok, "expected non-zero exit; stdout: {stdout}");
assert!(
stderr.contains("defines multiple functions"),
"expected listing header; stderr: {stderr}"
);
assert!(
stderr.contains("foo"),
"expected foo listed; stderr: {stderr}"
);
assert!(
stderr.contains("bar"),
"expected bar listed; stderr: {stderr}"
);
assert!(
stderr.contains("baz"),
"expected baz listed; stderr: {stderr}"
);
assert!(
stderr.contains("--ast"),
"expected --ast hint; stderr: {stderr}"
);
assert!(
!stdout.contains("\"declarations\""),
"no AST dump expected; got: {stdout}"
);
}
#[test]
fn multi_fn_file_with_func_name_runs_unchanged() {
let (_dir, path) = write_temp("foo>n;1\nbar>n;2\nbaz>n;3\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap(), "bar"]);
assert!(ok, "expected success; stderr: {stderr}");
assert_eq!(stdout.trim(), "2");
}
#[test]
fn multi_fn_file_with_func_name_and_args() {
let (_dir, path) = write_temp("add a:n b:n>n;+a b\nmul a:n b:n>n;*a b\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap(), "mul", "6", "7"]);
assert!(ok, "stderr: {stderr}");
assert_eq!(stdout.trim(), "42");
}
#[test]
fn ast_flag_dumps_single_fn_file() {
let (_dir, path) = write_temp("f>n;42\n");
let (ok, stdout, _stderr) = run(&["--ast", path.to_str().unwrap()]);
assert!(ok);
assert!(
stdout.contains("\"declarations\""),
"expected JSON AST; got: {stdout}"
);
assert!(
!stdout.trim().starts_with("42"),
"AST dump must not execute; got: {stdout}"
);
}
#[test]
fn ast_flag_dumps_multi_fn_file() {
let (_dir, path) = write_temp("foo>n;1\nbar>n;2\n");
let (ok, stdout, stderr) = run(&["--ast", path.to_str().unwrap()]);
assert!(ok, "stderr: {stderr}");
assert!(
stdout.contains("\"declarations\""),
"expected JSON AST; got: {stdout}"
);
assert!(
!stderr.contains("defines multiple functions"),
"no listing should fire when --ast is explicit; stderr: {stderr}"
);
}
#[test]
fn ast_flag_trailing_position() {
let (_dir, path) = write_temp("foo>n;1\nbar>n;2\n");
let (ok, stdout, _stderr) = run(&[path.to_str().unwrap(), "--ast"]);
assert!(ok);
assert!(stdout.contains("\"declarations\""));
}
#[test]
fn ast_flag_on_inline_code() {
let (ok, stdout, _stderr) = run(&["--ast", "f>n;5"]);
assert!(ok);
assert!(stdout.contains("\"declarations\""));
}
#[test]
fn multi_fn_file_with_main_auto_runs_main() {
let (_dir, path) = write_temp("helper a:n>n;+a 1\nmain>n;helper 41\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap()]);
assert!(ok, "expected success; stderr: {stderr}");
assert_eq!(stdout.trim(), "42");
assert!(
!stderr.contains("defines multiple functions"),
"no listing should fire when main is defined; stderr: {stderr}"
);
}
#[test]
fn multi_fn_file_explicit_func_arg_overrides_main() {
let (_dir, path) = write_temp("helper>n;7\nmain>n;42\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap(), "helper"]);
assert!(ok, "stderr: {stderr}");
assert_eq!(stdout.trim(), "7");
}
#[test]
fn inline_single_fn_auto_runs() {
let (ok, stdout, stderr) = run(&["f>n;42"]);
assert!(ok, "expected success; stderr: {stderr}");
assert_eq!(stdout.trim(), "42");
assert!(
!stdout.contains("\"declarations\""),
"no AST dump expected for runnable inline snippet; got: {stdout}"
);
}
#[test]
fn inline_multi_fn_with_main_auto_runs_main() {
let (ok, stdout, stderr) = run(&["helper a:n>n;+a 1\nmain>n;helper 41"]);
assert!(ok, "stderr: {stderr}");
assert_eq!(stdout.trim(), "42");
}
#[test]
fn inline_multi_fn_without_main_still_dumps_ast() {
let (ok, stdout, _stderr) = run(&["foo>n;1\nbar>n;2"]);
assert!(ok);
assert!(
stdout.contains("\"declarations\""),
"non-runnable inline snippet should AST-dump; got: {stdout}"
);
}
#[test]
fn inline_zero_fn_still_dumps_ast() {
let (ok, stdout, _stderr) = run(&["-- comment only"]);
assert!(ok);
assert!(
stdout.contains("\"declarations\""),
"zero-fn inline snippet should AST-dump; got: {stdout}"
);
}
#[test]
fn empty_file_dumps_ast() {
let (_dir, path) = write_temp("");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap()]);
assert!(ok, "stderr: {stderr}");
assert!(stdout.contains("\"declarations\""));
}
#[test]
fn synthetic_lambda_decls_hidden_from_multi_fn_listing() {
let (_dir, path) =
write_temp("sq xs:L n>L n;map (x:n>n;*x x) xs\nother xs:L n>L n;flt (x:n>b;>x 0) xs\n");
let (ok, _stdout, stderr) = run(&[path.to_str().unwrap()]);
assert!(!ok, "expected non-zero exit");
assert!(
!stderr.contains("__lit"),
"synthetic __lit_N names must not leak to user listing; stderr: {stderr}"
);
assert!(
stderr.contains("sq") && stderr.contains("other"),
"real fn names should still be listed; stderr: {stderr}"
);
}
#[test]
fn unknown_subcommand_errors_with_available_listing() {
let (_dir, path) = write_temp("helper a:n>n;+a 1\nmain>n;helper 41\n");
let (ok, _stdout, stderr) = run(&[path.to_str().unwrap(), "wibble", "x"]);
assert!(!ok, "unknown subcommand should exit non-zero");
assert!(
stderr.contains("no such function 'wibble'"),
"expected 'no such function' error; stderr: {stderr}"
);
assert!(
stderr.contains("available functions"),
"expected available-functions listing; stderr: {stderr}"
);
assert!(
stderr.contains("helper") && stderr.contains("main"),
"expected both fn names listed; stderr: {stderr}"
);
assert!(
!stderr.contains("expected 1 args, got 2"),
"must not leak first-fn arity error; stderr: {stderr}"
);
}
#[test]
fn known_subcommand_still_routes_correctly_in_multi_fn_file() {
let (_dir, path) = write_temp("helper a:n>n;+a 1\nmain>n;helper 41\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap(), "helper", "5"]);
assert!(ok, "known subcommand should succeed; stderr: {stderr}");
assert_eq!(stdout.trim(), "6");
}
#[test]
fn unknown_subcommand_does_not_list_synthetic_lit_decls() {
let (_dir, path) =
write_temp("sq xs:L n>L n;map (x:n>n;*x x) xs\nother xs:L n>L n;flt (x:n>b;>x 0) xs\n");
let (ok, _stdout, stderr) = run(&[path.to_str().unwrap(), "wibble"]);
assert!(!ok, "unknown subcommand should exit non-zero");
assert!(
stderr.contains("no such function 'wibble'"),
"expected no-such-function error; stderr: {stderr}"
);
assert!(
!stderr.contains("__lit"),
"synthetic __lit_N names must not leak to user listing; stderr: {stderr}"
);
assert!(
stderr.contains("sq") && stderr.contains("other"),
"real fn names should still be listed; stderr: {stderr}"
);
}
#[test]
fn single_fn_file_treats_unknown_leading_token_as_arg() {
let (_dir, path) = write_temp("dbl x:n>n;*x 2\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap(), "21"]);
assert!(ok, "single-fn auto-run should succeed; stderr: {stderr}");
assert_eq!(stdout.trim(), "42");
}
#[test]
fn inline_program_treats_unknown_leading_token_as_arg() {
let (ok, stdout, stderr) = run(&["dbl x:n>n;*x 2", "21"]);
assert!(ok, "inline auto-run should succeed; stderr: {stderr}");
assert_eq!(stdout.trim(), "42");
}
#[test]
fn multi_fn_file_numeric_leading_arg_passes_through_to_entry_fn() {
let (_dir, path) = write_temp("outer x:n>n;+x 1\ninner x:n>n;*x 2\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap(), "41"]);
assert!(
ok,
"numeric leading arg should pass through to entry fn; stderr: {stderr}"
);
assert_eq!(stdout.trim(), "42");
}
#[test]
fn multi_fn_file_quoted_string_leading_arg_passes_through() {
let (_dir, path) = write_temp("greet s:t>t;s\nother s:t>t;s\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap(), "\"hi\""]);
assert!(
ok,
"quoted-string leading arg should pass through; stderr: {stderr}"
);
assert_eq!(stdout.trim().trim_matches('"'), "hi");
}
#[test]
fn multi_fn_file_bracketed_list_leading_arg_passes_through() {
let (_dir, path) = write_temp("first xs:L n>n;hd xs\nother xs:L n>n;hd xs\n");
let (ok, stdout, stderr) = run(&[path.to_str().unwrap(), "[7,8,9]"]);
assert!(
ok,
"bracketed-list leading arg should pass through; stderr: {stderr}"
);
assert_eq!(stdout.trim(), "7");
}