use std::io::Write;
use std::process::{Command, Stdio};
fn run_repl(stdin_source: &str) -> (String, String, i32) {
let bin = env!("CARGO_BIN_EXE_bop");
let mut child = Command::new(bin)
.arg("repl")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn bop binary");
{
let mut stdin = child.stdin.take().expect("child stdin");
stdin
.write_all(stdin_source.as_bytes())
.expect("write stdin");
}
let output = child.wait_with_output().expect("wait child");
(
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
output.status.code().unwrap_or(-1),
)
}
#[test]
fn bare_expression_result_lands_on_stdout() {
let (stdout, stderr, code) = run_repl("1 + 2\n");
assert_eq!(code, 0, "stderr: {}", stderr);
assert_eq!(stdout.trim_end(), "3");
assert!(stderr.is_empty(), "unexpected stderr: {}", stderr);
}
#[test]
fn print_call_goes_to_stdout_and_no_trailing_none() {
let (stdout, _stderr, code) = run_repl("print(42)\n");
assert_eq!(code, 0);
assert_eq!(stdout.trim_end(), "42");
}
#[test]
fn multi_line_fn_decl_then_call_works() {
let src = "fn double(x) {\n return x + x\n}\nprint(double(21))\n";
let (stdout, _stderr, code) = run_repl(src);
assert_eq!(code, 0);
assert_eq!(stdout.trim_end(), "42");
}
#[test]
fn runtime_error_prints_to_stderr_with_carat() {
let (stdout, stderr, code) = run_repl("print(undefined)\n");
assert_ne!(code, 0, "expected non-zero exit on runtime error");
assert!(
stderr.contains("--> line 1:"),
"expected line+col header in stderr, got: {}",
stderr
);
assert!(
stderr.contains("^"),
"expected carat in stderr, got: {}",
stderr
);
assert!(
stdout.is_empty(),
"error path shouldn't have stdout output, got: {}",
stdout
);
}
#[test]
fn help_meta_command_lists_commands() {
let (stdout, _stderr, code) = run_repl(":help\n");
assert_eq!(code, 0);
assert!(stdout.contains(":help"));
assert!(stdout.contains(":vars"));
assert!(stdout.contains(":reset"));
assert!(stdout.contains(":quit"));
}
#[test]
fn empty_stdin_is_graceful() {
let (stdout, stderr, code) = run_repl("");
assert_eq!(code, 0);
assert!(stdout.is_empty());
assert!(stderr.is_empty());
}
#[test]
fn session_sees_bindings_across_statements_in_one_buffer() {
let src = "let x = 5\nprint(x)\n";
let (stdout, _stderr, code) = run_repl(src);
assert_eq!(code, 0);
assert_eq!(stdout.trim_end(), "5");
}
#[test]
fn bare_expressions_echo_per_line() {
let src = "let x = 10\nx\nx + 5\n";
let (stdout, _stderr, code) = run_repl(src);
assert_eq!(code, 0);
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["10", "15"]);
}
#[test]
fn error_in_one_line_does_not_abort_the_rest() {
let src = "let ok = 1\nundefined\nprint(ok)\n";
let (stdout, stderr, code) = run_repl(src);
assert_eq!(code, 1, "expected non-zero exit on any error");
assert_eq!(stdout.trim_end(), "1");
assert!(
stderr.contains("undefined"),
"expected error to name undefined, got: {}",
stderr
);
}
#[test]
fn reset_meta_command_clears_session() {
let src = "let x = 5\n:reset\nprint(x)\n";
let (_stdout, stderr, code) = run_repl(src);
assert_eq!(code, 1);
assert!(
stderr.contains("Variable `x` not found")
|| stderr.to_lowercase().contains("not found"),
"expected 'x not found' error after :reset, got: {}",
stderr
);
}
#[test]
fn vars_meta_lists_declared_names() {
let src = "let alpha = 1\nfn beta() { return 2 }\n:vars\n";
let (stdout, _stderr, code) = run_repl(src);
assert_eq!(code, 0);
assert!(stdout.contains("alpha"));
assert!(stdout.contains("beta"));
}
#[test]
fn quit_meta_exits_early_and_ignores_remaining_input() {
let src = "let x = 42\n:quit\nprint(does_not_exist)\n";
let (stdout, stderr, code) = run_repl(src);
assert_eq!(code, 0, "quit should succeed, stderr: {}", stderr);
assert!(
!stdout.contains("does_not_exist"),
"expected no output after :quit, got: {}",
stdout
);
assert!(
stderr.is_empty(),
"expected no errors after :quit, got: {}",
stderr
);
}
#[test]
fn struct_declaration_persists_across_lines() {
let src = r#"struct Point { x, y }
fn Point.sum(self) { return self.x + self.y }
print(Point { x: 3, y: 4 }.sum())
"#;
let (stdout, stderr, code) = run_repl(src);
assert_eq!(code, 0, "stderr: {}", stderr);
assert_eq!(stdout.trim_end(), "7");
}
#[test]
fn use_statement_imports_stay_live_across_lines() {
let src = "use std.math\nprint(9.sqrt())\n";
let (stdout, stderr, code) = run_repl(src);
assert_eq!(code, 0, "stderr: {}", stderr);
assert_eq!(stdout.trim_end(), "3");
}