use std::io::Write;
use std::process::{Command, Stdio};
fn run_repl(input: &str) -> (String, String) {
let mut child = Command::new(env!("CARGO_BIN_EXE_keleusma"))
.arg("repl")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn keleusma repl");
child
.stdin
.take()
.expect("child stdin")
.write_all(input.as_bytes())
.expect("write to child stdin");
let out = child.wait_with_output().expect("wait for repl");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
#[test]
fn run_steps_a_loop_main_and_evolves_shared_state() {
let input = concat!(
"shared data state { count: Word }\n",
"loop main(tick: Word) -> Word { state.count = state.count + tick; yield state.count }\n",
":run\n",
":resume\n",
":resume\n",
":shared\n",
":stop\n",
":quit\n",
);
let (out, err) = run_repl(input);
assert!(out.contains("stepping loop main"), "out: {out}\nerr: {err}");
assert!(out.contains("yield => 1"), "out: {out}");
assert!(out.contains("yield => 3"), "out: {out}");
assert!(out.contains("yield => 7"), "out: {out}");
assert!(out.contains("state.count = 1"), "out: {out}");
assert!(out.contains("state.count = 7"), "out: {out}");
assert!(out.contains("stopped stepping"), "out: {out}");
}
#[test]
fn resume_with_explicit_tick_overrides_the_convention() {
let input = concat!(
"shared data state { count: Word }\n",
"loop main(tick: Word) -> Word { state.count = state.count + tick; yield state.count }\n",
":run\n",
":resume 10\n",
":stop\n",
":quit\n",
);
let (out, _err) = run_repl(input);
assert!(out.contains("yield => 1"), "out: {out}");
assert!(out.contains("yield => 11"), "out: {out}");
assert!(out.contains("state.count = 11"), "out: {out}");
}
#[test]
fn run_an_atomic_fn_main_prints_its_result_without_stepping() {
let input = concat!("fn main() -> Word { 6 * 7 }\n", ":run\n", ":quit\n");
let (out, _err) = run_repl(input);
assert!(out.contains("=> 42"), "out: {out}");
assert!(!out.contains("stepping"), "out: {out}");
}
#[test]
fn run_without_a_main_reports_a_helpful_error() {
let input = concat!(
"fn double(x: Word) -> Word { x * 2 }\n",
":run\n",
":quit\n",
);
let (_out, err) = run_repl(input);
assert!(err.contains(":run needs a `main`"), "err: {err}");
}
#[test]
fn save_then_load_round_trips_the_session_program() {
let path =
std::env::temp_dir().join(format!("keleusma_repl_saveload_{}.kel", std::process::id()));
let path_str = path.to_str().expect("utf8 temp path").to_string();
let _ = std::fs::remove_file(&path);
let input = format!(
concat!(
"fn double(x: Word) -> Word {{ x * 2 }}\n",
":save {p}\n",
":reset\n",
":load {p}\n",
"double(21)\n",
":quit\n",
),
p = path_str
);
let (out, err) = run_repl(&input);
assert!(out.contains("saved session to"), "out: {out}\nerr: {err}");
assert!(out.contains("loaded"), "out: {out}");
assert!(out.contains("42"), "out: {out}");
let saved = std::fs::read_to_string(&path).expect("read saved .kel file");
assert!(
saved.contains("fn double(x: Word) -> Word { x * 2 }"),
"saved: {saved}"
);
let _ = std::fs::remove_file(&path);
}