use std::io::Write;
use std::process::{Command, Stdio};
fn run(lines: &[&str]) -> String {
run_with_timeout(15, lines)
}
fn run_with_timeout(secs: u32, lines: &[&str]) -> String {
let mut child = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.arg("--no-color")
.arg("--timeout")
.arg(secs.to_string())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to start c-shell");
let mut stdin = child.stdin.take().expect("stdin piped");
for l in lines {
writeln!(stdin, "{l}").expect("write to c-shell stdin");
}
drop(stdin);
let out = child.wait_with_output().expect("wait for c-shell");
assert!(
out.status.success(),
"c-shell exited abnormally: {:?}\nstderr:\n{}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
}
#[test]
fn evaluates_expressions_against_session_state() {
let out = run(&["int x = 41;", "x + 1"]);
assert!(out.contains("Out[2]: 42"), "unexpected output:\n{out}");
}
#[test]
fn prints_floating_point_and_bool() {
let out = run(&["3.0 / 2", "1 == 2"]);
assert!(out.contains("Out[1]: 1.5"), "unexpected output:\n{out}");
assert!(out.contains("Out[2]: 0"), "unexpected output:\n{out}");
}
#[test]
fn defines_and_calls_functions() {
let out = run(&["int add(int a, int b) { return a + b; }", "add(3, 4)"]);
assert!(out.contains("Out[2]: 7"), "unexpected output:\n{out}");
}
#[test]
fn statements_execute_and_mutate_state() {
let out = run(&["int n = 1;", "n = n * 10;", "n"]);
assert!(out.contains("Out[3]: 10"), "unexpected output:\n{out}");
}
#[test]
fn survives_compile_errors() {
let out = run(&["int x = ;", "1 + 1"]);
assert!(out.contains("error"), "no diagnostic shown:\n{out}");
assert!(out.contains("Out[2]: 2"), "session did not survive:\n{out}");
}
#[test]
fn survives_a_crashing_input() {
let out = run(&["int x = 7;", "char *p = 0; *p = 1;", "x"]);
assert!(out.contains("Out[3]: 7"), "session did not survive:\n{out}");
}
#[test]
fn defining_things_without_using_them_warns_nothing() {
let out = run(&[
"int foo(int x) { return x; }",
"int unused_yet = 1;",
"unused_yet + 1;",
]);
assert!(
!out.contains("warning"),
"unused-style warning leaked:\n{out}"
);
}
#[test]
fn real_warnings_still_show() {
let out = run(&["int neg = -1;", "unsigned pos = 0;", "neg > pos"]);
assert!(
out.contains("warning") || out.contains("C4018"),
"sign-compare warning disappeared:\n{out}"
);
}
#[test]
fn disguised_calls_commit_their_side_effects() {
let out = run(&[
"int f(void) { static int n; return ++n; }",
"int (*fp)(void) = f;",
"f/**/()",
"f /* comment */ ()",
"(f)()",
"(*fp)()",
"f()",
]);
assert!(out.contains("Out[3]: 1"), "first call:\n{out}");
assert!(
out.contains("Out[7]: 5"),
"a disguised call was dropped:\n{out}"
);
}
#[test]
fn eval_flag_prints_bare_value_and_exits_clean() {
let out = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["-e", "1 + 1"])
.output()
.expect("run c-shell -e");
assert!(out.status.success());
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "2");
}
#[test]
fn eval_flag_failure_sets_exit_code_and_uses_stderr() {
let out = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["-e", "no_such_variable"])
.output()
.expect("run c-shell -e");
assert!(!out.status.success(), "failure must be visible to scripts");
assert!(
String::from_utf8_lossy(&out.stderr).contains("error"),
"diagnostic missing from stderr"
);
}
#[test]
fn script_mode_accumulates_multi_line_definitions() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("t.csh");
std::fs::write(
&path,
"// a comment-only line is skipped
int mul(int a, int b)
{
return a * b;
}
mul(6, 7)
",
)
.expect("write script");
let out = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.arg("--script")
.arg(&path)
.output()
.expect("run c-shell --script");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Out[2]: 42"),
"unexpected output:\n{stdout}"
);
}
#[test]
fn piped_stdin_accumulates_multi_line_definitions() {
let out = run(&["int sq(int a)", "{", " return a * a;", "}", "sq(9)"]);
assert!(out.contains("Out[2]: 81"), "unexpected output:\n{out}");
}
#[test]
fn history_separates_input_numbers_from_magic_commands() {
let out = run(&["int h1 = 41;", "%src", "h1 + 1", "%history"]);
assert!(out.contains("In[ 1] int h1 = 41;"), "numbered row:\n{out}");
assert!(out.contains("In[ 2] h1 + 1"), "numbered row:\n{out}");
assert!(out.contains("-- %src"), "magic row:\n{out}");
}
#[test]
fn piped_stdin_shows_no_banner_or_prompt_noise() {
let out = run(&["1 + 1"]);
assert!(
!out.contains("____"),
"ASCII banner leaked into pipe:\n{out}"
);
assert!(!out.contains("bye"), "interactive farewell leaked:\n{out}");
assert!(out.contains("Out[1]: 2"), "unexpected output:\n{out}");
}
#[cfg(unix)]
#[test]
fn timeout_kills_forked_descendants_too() {
let out = run_with_timeout(
2,
&[
"#include <unistd.h>",
"if (fork() == 0) { for (;;) pause(); } for (;;) pause();",
"1 + 1",
],
);
assert!(out.contains("killed after 2s"), "no timeout report:\n{out}");
assert!(out.contains("Out[3]: 2"), "REPL did not survive:\n{out}");
}
#[test]
fn program_output_is_shown_once() {
let out = run(&["puts(\"marker-once\");", "1 + 1"]);
let hits = out.matches("marker-once").count();
assert_eq!(hits, 1, "output replayed {hits} times:\n{out}");
}