use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpStream;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use tungstenite::{
Message as WebSocketMessage, client::IntoClientRequest, connect, stream::MaybeTlsStream,
};
struct ChildGuard(Child);
impl Drop for ChildGuard {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
#[cfg(unix)]
struct WebTerminal {
_server: ChildGuard,
_stdout: BufReader<std::process::ChildStdout>,
socket: tungstenite::WebSocket<MaybeTlsStream<TcpStream>>,
transcript: Vec<u8>,
cursor_reports: usize,
}
#[cfg(unix)]
impl WebTerminal {
fn start(command: &mut Command) -> Self {
let mut child = command
.args(["--web", "--no-open", "--lang", "en", "--no-color"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut stdout = BufReader::new(child.stdout.take().unwrap());
let mut line = String::new();
stdout.read_line(&mut line).unwrap();
let url = line
.split_whitespace()
.find(|word| word.starts_with("http://127.0.0.1:"))
.expect("browser terminal URL");
let origin = url
.strip_prefix("http://")
.unwrap()
.split('/')
.next()
.unwrap();
let mut request = format!("{}ws", url.replacen("http://", "ws://", 1))
.into_client_request()
.unwrap();
request
.headers_mut()
.insert("Origin", format!("http://{origin}").parse().unwrap());
let (mut socket, _) = connect(request).unwrap();
if let MaybeTlsStream::Plain(stream) = socket.get_mut() {
stream
.set_read_timeout(Some(Duration::from_secs(20)))
.unwrap();
}
Self {
_server: ChildGuard(child),
_stdout: stdout,
socket,
transcript: Vec::new(),
cursor_reports: 0,
}
}
fn send(&mut self, bytes: &[u8]) {
self.socket
.send(WebSocketMessage::Binary(bytes.to_vec().into()))
.unwrap();
}
fn until(&mut self, from: usize, needle: &str) {
let deadline = std::time::Instant::now() + Duration::from_secs(25);
while !String::from_utf8_lossy(&self.transcript[from..])
.replace("\r\n", "\n")
.contains(needle)
{
assert!(
std::time::Instant::now() < deadline,
"missing {needle:?}: {}",
String::from_utf8_lossy(&self.transcript)
);
match self.socket.read().unwrap_or_else(|error| {
panic!(
"{error}; missing {needle:?}: {}",
String::from_utf8_lossy(&self.transcript)
)
}) {
WebSocketMessage::Binary(bytes) => self.transcript.extend(bytes),
WebSocketMessage::Text(text) => self.transcript.extend(text.as_bytes()),
WebSocketMessage::Ping(bytes) => {
self.socket.send(WebSocketMessage::Pong(bytes)).unwrap()
}
WebSocketMessage::Close(_) => panic!(
"terminal closed before {needle:?}: {}",
String::from_utf8_lossy(&self.transcript)
),
_ => {}
}
let queries = self
.transcript
.windows(4)
.filter(|bytes| *bytes == b"\x1b[6n")
.count();
while self.cursor_reports < queries {
self.send(b"\x1b[1;1R");
self.cursor_reports += 1;
}
}
}
fn after(&self, from: usize, text: &str) -> usize {
let crlf = text.replace('\n', "\r\n");
[text.as_bytes(), crlf.as_bytes()]
.into_iter()
.filter_map(|needle| {
self.transcript[from..]
.windows(needle.len())
.rposition(|bytes| bytes == needle)
.map(|index| from + index + needle.len())
})
.max()
.expect("observed terminal text")
}
fn until_painted(&mut self, from: usize, text: &str) {
self.until(from, text);
let text_end = self.after(from, text);
self.until(text_end, "\x1b[?25h");
}
fn prompt(&mut self, from: usize, number: usize) {
self.until_painted(from, &format!("In [{number}]:"));
}
fn evaluate(&mut self, code: &str, expected: &str) -> usize {
let start = self.transcript.len();
self.send(format!("{code}\r").as_bytes());
self.until(start, expected);
self.after(start, expected)
}
}
#[cfg(unix)]
impl Drop for WebTerminal {
fn drop(&mut self) {
let _ = self.socket.send(WebSocketMessage::Binary(vec![3].into()));
std::thread::sleep(Duration::from_millis(50));
let _ = self.socket.close(None);
}
}
#[cfg(unix)]
#[test]
fn interactive_terminal_recovers_from_interrupts_in_edit_compile_run_and_input() {
let dir = tempfile::tempdir().unwrap();
let wrapper = dir.path().join(if cfg!(windows) {
"cc.cmd"
} else {
"cc-interrupt"
});
let trigger = dir.path().join("slow");
let ready = dir.path().join("ready");
let survived = dir.path().join("survived");
if cfg!(windows) {
std::fs::write(&wrapper, "@echo off\r\nif exist \"%C_SHELL_SLOW_TRIGGER%\" (\r\n echo ready>\"%C_SHELL_SLOW_READY%\"\r\n powershell.exe -NoProfile -NonInteractive -Command \"Start-Sleep -Seconds 2; Set-Content -LiteralPath $env:C_SHELL_SLOW_SURVIVED -Value leaked; Start-Sleep -Seconds 30\"\r\n)\r\n\"%C_SHELL_TEST_CC%\" %*\r\n").unwrap();
} else {
std::fs::write(&wrapper, "#!/bin/sh\nif [ -f \"$C_SHELL_SLOW_TRIGGER\" ]; then\n printf ready > \"$C_SHELL_SLOW_READY\"\n sleep 2\n printf leaked > \"$C_SHELL_SLOW_SURVIVED\"\n sleep 30\nfi\nexec \"$C_SHELL_TEST_CC\" \"$@\"\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o700)).unwrap();
}
}
let compiler_name = std::env::var_os("CC").unwrap_or_else(|| {
if cfg!(windows) {
"gcc".into()
} else {
"cc".into()
}
});
let compiler =
which::which(&compiler_name).expect("resolve the real compiler before wrapping it");
let make_command = || {
let mut command = Command::new(env!("CARGO_BIN_EXE_c-shell"));
command
.arg("--cc")
.arg(&wrapper)
.env("C_SHELL_TEST_CC", &compiler)
.env("C_SHELL_SLOW_TRIGGER", &trigger)
.env("C_SHELL_SLOW_READY", &ready)
.env("C_SHELL_SLOW_SURVIVED", &survived);
command
};
let preflight = make_command()
.args(["--lang", "en", "-e", "1 + 1"])
.output()
.expect("run compiler wrapper preflight");
assert!(
preflight.status.success(),
"compiler wrapper preflight failed:\n{}\n{}",
String::from_utf8_lossy(&preflight.stdout),
String::from_utf8_lossy(&preflight.stderr)
);
assert_eq!(String::from_utf8_lossy(&preflight.stdout), "2\n");
let mut terminal = WebTerminal::start(&mut make_command());
terminal.prompt(0, 1);
let editing = terminal.transcript.len();
terminal.send(b"int discarded = 1");
terminal.until_painted(editing, "int discarded = 1");
let cancelled = terminal.transcript.len();
terminal.send(b"\x03");
terminal.prompt(cancelled, 1);
let start = terminal.evaluate("1 + 1", "Out[1]: 2");
terminal.prompt(start, 2);
std::fs::write(&trigger, "slow").unwrap();
terminal.send(b"2 + 2\r");
let deadline = std::time::Instant::now() + Duration::from_secs(20);
while !ready.exists() {
assert!(
std::time::Instant::now() < deadline,
"compiler wrapper never started"
);
std::thread::sleep(Duration::from_millis(10));
}
std::fs::remove_file(&trigger).unwrap();
let start = terminal.transcript.len();
terminal.send(b"\x03");
terminal.until(start, "interrupted");
terminal.prompt(start, 3);
let start = terminal.evaluate("3 + 3", "Out[3]: 6");
terminal.prompt(start, 4);
terminal.evaluate(
"{ puts(\"loop-ready\"); fflush(stdout); for (;;) {} }",
"loop-ready\n",
);
let start = terminal.transcript.len();
terminal.send(b"\x03");
terminal.prompt(start, 5);
let start = terminal.evaluate("40 + 2", "Out[5]: 42");
terminal.prompt(start, 6);
terminal.evaluate(
"{ int value; fputs(\"input-ready\\n\", stderr); fflush(stderr); scanf(\"%d\", &value); }",
"input-ready\n",
);
let start = terminal.transcript.len();
terminal.send(b"\x03");
terminal.prompt(start, 7);
let start = terminal.evaluate("6 * 7", "Out[7]: 42");
terminal.prompt(start, 8);
let start = terminal.transcript.len();
terminal.send(b"char text[16];\r");
terminal.prompt(start, 9);
let start = terminal.evaluate(
"fputs(\"line-ready\\n\", stderr); fflush(stderr); fgets(text, sizeof text, stdin);",
"line-ready\n",
);
terminal.send(b"hello\r");
terminal.prompt(start, 10);
let start = terminal.evaluate("text", "Out[10]: \"hello\\n\"");
terminal.prompt(start, 11);
let checks_available =
!runtime_check_unavailable(&runtime_check_output("en", &["%check 1 + 1"]));
if checks_available {
let start = terminal.evaluate(
"%check fprintf(stderr, \"runtime error: demo\\n\");",
"runtime error: demo\n",
);
terminal.prompt(start, 11);
let start = terminal.transcript.len();
let output_end = terminal.evaluate(
"%check fprintf(stderr, \"example.c:1:2: runtime error: demo\\n\");",
"example.c:1:2: runtime error: demo\n",
);
assert!(!String::from_utf8_lossy(&terminal.transcript[start..]).contains("Runtime check:"));
terminal.prompt(output_end, 11);
let start = terminal.transcript.len();
terminal.evaluate(
"%check { int n = 0; fprintf(stderr, \"example.c:1:2: runtime error: demo\\n\"); puts(\"checked-input-ready\"); fflush(stdout); scanf(\"%d\", &n); printf(\"read=%d\\n\", n); }",
"checked-input-ready\n",
);
terminal.send(b"42\r");
terminal.until(start, "read=42\n");
terminal.until(start, "example.c:1:2: runtime error: demo\n");
let output_end = terminal.after(start, "example.c:1:2: runtime error: demo\n");
terminal.prompt(output_end, 11);
let current = String::from_utf8_lossy(&terminal.transcript[start..]).replace("\r\n", "\n");
assert_eq!(
current
.matches("example.c:1:2: runtime error: demo\n")
.count(),
1,
"{current}"
);
assert!(!current.contains("Runtime check:"), "{current}");
}
std::thread::sleep(Duration::from_millis(2100));
assert!(
!survived.exists(),
"interrupted compiler descendants kept running"
);
}
#[cfg(unix)]
#[test]
fn expression_strategy_cache_saves_compilations_and_invalidates_on_context_changes() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let wrapper = dir.path().join("cc-plans");
let log = dir.path().join("compiles");
std::fs::write(&wrapper, "#!/bin/sh\nfor arg in \"$@\"; do\n case \"$arg\" in */input.c) printf 'compile\\n' >> \"$C_SHELL_PLAN_LOG\" ;; esac\ndone\nexec \"$C_SHELL_PLAN_CC\" \"$@\"\n").unwrap();
std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o700)).unwrap();
let mut command = Command::new(env!("CARGO_BIN_EXE_c-shell"));
command
.arg("--cc")
.arg(&wrapper)
.args(["--lang", "en"])
.env("C_SHELL_PLAN_LOG", &log)
.env(
"C_SHELL_PLAN_CC",
std::env::var_os("CC").unwrap_or_else(|| "cc".into()),
);
for input in [
"int matrix[2][2] = {{1,2},{3,4}};",
"matrix",
"matrix",
"int matrix[3] = {7,8,9};",
"matrix",
"matrix",
"%std c17",
"matrix",
] {
command.args(["-e", input]);
}
let output = command.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"{stdout}\n{}",
String::from_utf8_lossy(&output.stderr)
);
let rows = stdout
.lines()
.filter(|line| line.starts_with('{'))
.collect::<Vec<_>>();
assert_eq!(rows.len(), 5, "{stdout}");
assert_eq!(rows[0], rows[1]);
assert_eq!(rows[2], rows[3]);
assert_eq!(rows[3], rows[4]);
assert_ne!(
rows[0], rows[2],
"a stale array-dimension plan survived shadowing"
);
assert_eq!(
std::fs::read_to_string(&log).unwrap().lines().count(),
12,
"expected cold/warm matrix queries 3/1, shadowing 2 compiles, new array queries 2/1, and standard-change query 2"
);
}
fn run(lines: &[&str]) -> String {
run_with_timeout(15, lines)
}
fn run_with_timeout(secs: u32, lines: &[&str]) -> String {
run_with_language_and_timeout("en", secs, lines)
}
fn run_with_language(language: &str, lines: &[&str]) -> String {
run_with_language_and_timeout(language, 15, lines)
}
fn run_with_language_and_timeout(language: &str, secs: u32, lines: &[&str]) -> String {
let mut child = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.arg("--no-color")
.args(["--lang", language])
.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()
}
fn run_evals_with_stdin(codes: &[&str], input: &str) -> std::process::Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_c-shell"));
command.args(["--no-color", "--lang", "en"]);
for code in codes {
command.args(["-e", code]);
}
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("start c-shell -e with program stdin");
child
.stdin
.take()
.expect("program stdin")
.write_all(input.as_bytes())
.expect("write program stdin");
child.wait_with_output().expect("wait for c-shell -e")
}
#[test]
fn web_mode_serves_a_local_pty_repl_behind_a_random_path() {
let child = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["--web", "--no-open", "--lang", "en", "--no-color"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("start c-shell --web");
let mut child = ChildGuard(child);
let stdout = child.0.stdout.take().expect("web server stdout");
let mut stdout = BufReader::new(stdout);
let mut first_line = String::new();
stdout
.read_line(&mut first_line)
.expect("read browser terminal URL");
let url = first_line
.split_whitespace()
.find(|word| word.starts_with("http://127.0.0.1:"))
.unwrap_or_else(|| {
let deadline = std::time::Instant::now() + Duration::from_secs(1);
let mut status = child.0.try_wait().expect("web server status");
while status.is_none() && std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
status = child.0.try_wait().expect("web server status");
}
let mut errors = String::new();
if status.is_some() && let Some(mut stderr) = child.0.stderr.take() {
let _ = stderr.read_to_string(&mut errors);
}
panic!("missing local browser terminal URL: {first_line:?}; status: {status:?}; stderr: {errors}");
})
.to_string();
assert!(
url.ends_with('/')
&& url
.trim_end_matches('/')
.rsplit('/')
.next()
.is_some_and(|token| {
token.len() == 32
&& token
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
}),
"URL did not contain a random 128-bit path: {url}"
);
let origin = url
.strip_prefix("http://")
.and_then(|rest| rest.split_once('/'))
.map(|(authority, _)| format!("http://{authority}"))
.expect("URL authority");
let (authority, token_path) = url
.strip_prefix("http://")
.and_then(|rest| rest.split_once('/'))
.expect("HTTP URL parts");
let mut http = TcpStream::connect(authority).expect("connect browser page");
http.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set HTTP timeout");
write!(
http,
"GET /{token_path} HTTP/1.1\r\nHost: {authority}\r\nConnection: close\r\n\r\n"
)
.expect("request browser page");
let mut page = String::new();
http.read_to_string(&mut page).expect("read browser page");
assert!(
page.starts_with("HTTP/1.1 200 OK\r\n")
&& page.contains("content-security-policy:")
&& page.contains("<script defer src=\"xterm.js\"></script>")
&& page.contains("<main id=\"terminal\""),
"random-path page was incomplete:\n{page}"
);
let websocket_url = format!("{}ws", url.replacen("http://", "ws://", 1));
let mut forbidden = websocket_url
.clone()
.into_client_request()
.expect("invalid-origin request");
forbidden
.headers_mut()
.insert("Origin", "http://example.invalid".parse().unwrap());
let error = connect(forbidden).expect_err("foreign WebSocket origin was accepted");
assert!(
matches!(
error,
tungstenite::Error::Http(ref response)
if response.status() == tungstenite::http::StatusCode::FORBIDDEN
),
"unexpected invalid-origin result: {error}"
);
let mut request = websocket_url
.into_client_request()
.expect("local WebSocket request");
request
.headers_mut()
.insert("Origin", origin.parse().unwrap());
let (mut socket, response) = connect(request).expect("connect local browser terminal");
assert_eq!(
response.status(),
tungstenite::http::StatusCode::SWITCHING_PROTOCOLS
);
if let MaybeTlsStream::Plain(stream) = socket.get_mut() {
stream
.set_read_timeout(Some(Duration::from_secs(15)))
.expect("set WebSocket timeout");
}
socket
.send(WebSocketMessage::Text("resize:100:30".into()))
.expect("resize browser terminal");
let mut transcript = Vec::new();
let mut cursor_reports = 0;
let mut sent_expression = false;
while !String::from_utf8_lossy(&transcript).contains("Out[1]: 2") {
let message = socket.read().unwrap_or_else(|error| {
panic!(
"read browser terminal output: {error}\ntranscript:\n{}",
String::from_utf8_lossy(&transcript)
)
});
match message {
WebSocketMessage::Binary(bytes) => transcript.extend_from_slice(&bytes),
WebSocketMessage::Text(text) => transcript.extend_from_slice(text.as_bytes()),
WebSocketMessage::Close(_) => panic!(
"browser terminal closed early:\n{}",
String::from_utf8_lossy(&transcript)
),
WebSocketMessage::Ping(bytes) => {
socket
.send(WebSocketMessage::Pong(bytes))
.expect("reply to WebSocket ping");
}
WebSocketMessage::Pong(_) | WebSocketMessage::Frame(_) => {}
}
let cursor_queries = transcript
.windows(b"\x1b[6n".len())
.filter(|window| *window == b"\x1b[6n")
.count();
while cursor_reports < cursor_queries {
socket
.send(WebSocketMessage::Binary(b"\x1b[1;1R".to_vec().into()))
.expect("answer cursor-position query");
cursor_reports += 1;
}
if !sent_expression && String::from_utf8_lossy(&transcript).contains("In [1]:") {
socket
.send(WebSocketMessage::Binary(b"1 + 1\r".to_vec().into()))
.expect("send C expression");
sent_expression = true;
}
}
assert!(
String::from_utf8_lossy(&transcript)
.contains(&format!("c-shell {}", env!("CARGO_PKG_VERSION"))),
"PTY did not run the regular interactive REPL:\n{}",
String::from_utf8_lossy(&transcript)
);
assert!(
!String::from_utf8_lossy(&transcript).contains("c-shell --web"),
"browser PTY repeated the terminal-only web hint:\n{}",
String::from_utf8_lossy(&transcript)
);
let _ = socket.close(None);
}
#[test]
fn scanf_stdin_tape_replays_functions_and_loops_without_reading_again() {
let output = run_evals_with_stdin(
&[
"int read_number(void) { int value; scanf(\"%d\", &value); return value; }",
"int first = read_number();",
"int second = 0; for (int k = 0; k < 2; ++k) { second += read_number(); }",
"first + second",
],
"3\n4\n5\n",
);
assert!(
output.status.success(),
"stdin tape session failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "12");
assert!(
!String::from_utf8_lossy(&output.stderr).contains("external side-effect"),
"supported scanf still used the generic replay warning"
);
let divergent = run_evals_with_stdin(
&[
"int read_once(void) { int value; scanf(\"%d\", &value); return value; }",
"int kept = read_once();",
"int read_once(void) { return 99; }",
"kept",
],
"3\n",
);
assert!(!divergent.status.success(), "divergent tape was accepted");
assert!(
String::from_utf8_lossy(&divergent.stderr).contains("stdin tape diverged"),
"missing divergence diagnostic:\n{}",
String::from_utf8_lossy(&divergent.stderr)
);
assert_eq!(
String::from_utf8_lossy(&divergent.stdout).trim(),
"3",
"rejected replacement corrupted the retained tape/session"
);
}
#[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 explicit_u8_literals_get_a_safe_utf8_preview_only() {
let out = run(&[
r#"u8"\U0001F642""#,
"unsigned char bytes[] = { 240, 159, 153, 130, 0 };",
"bytes",
]);
assert!(
out.contains("Out[1]: u8\"🙂\"\ncode units: {0xf0, 0x9f, 0x99, 0x82, 0x00}"),
"valid explicit UTF-8 literal was not rendered as text:\n{out}"
);
assert!(
out.contains("Out[3]: {240, 159, 153, 130, 0}"),
"ordinary unsigned char array must stay numeric:\n{out}"
);
assert_eq!(
out.matches("u8\"🙂\"").count(),
1,
"numeric byte array was also treated as explicit UTF-8:\n{out}"
);
}
#[test]
fn unicode_magics_decode_explicit_code_units_without_changing_session() {
let out = run(&[
"int untouched = 0;",
r#"%utf8 ((++untouched == 1) ? u8"A\u597D\U0001F600" : u8"twice")"#,
r#"%utf16 u"A\u597D\U0001F600""#,
r#"%utf32 U"A\u597D\U0001F600""#,
"%utf8 -n 3 (const unsigned char[]){0x41, 0, 0x42}",
"%utf16 (const unsigned short[]){0xd800, 0}",
"%utf32 (const unsigned int[]){0x110000, 0}",
"%utf8 (const unsigned int[]){0x41, 0}",
"%utf8 42",
r#"%utf8 -n 4097 u8"x""#,
"untouched",
]);
assert!(
out.contains("text: u8\"A好😀\"")
&& out.contains("code units: {0x41, 0xe5, 0xa5, 0xbd, 0xf0, 0x9f, 0x98, 0x80, 0x00}"),
"UTF-8 magic failed:\n{out}"
);
assert!(
out.contains("text: u\"A好😀\"") && out.contains("0x0041, 0x597d, 0xd83d, 0xde00, 0x0000"),
"UTF-16 magic failed:\n{out}"
);
assert!(
out.contains("text: U\"A好😀\"")
&& out.contains("0x00000041, 0x0000597d, 0x0001f600, 0x00000000"),
"UTF-32 magic failed:\n{out}"
);
assert!(
out.contains("text: u8\"A\\0B\""),
"-n did not preserve an embedded NUL:\n{out}"
);
assert!(
out.contains("unpaired UTF-16 high surrogate at index 0")
&& out.contains("invalid UTF-32 scalar value at index 0"),
"invalid Unicode was silently replaced:\n{out}"
);
assert!(
out.contains("expected 1-byte code units")
&& out.contains("%utf8 supports pointers and arrays of integer code-unit types")
&& out.contains("-n is limited to 4096 code units"),
"Unicode magic error paths were not actionable:\n{out}"
);
assert!(
out.contains("Out[2]: 0"),
"Unicode magic evaluated more than once, retained its expression, or consumed an input number:\n{out}"
);
}
#[cfg(not(windows))]
#[test]
fn strict_iso_mode_keeps_the_runtime_timer_available() {
let out = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["--no-color", "--lang", "en", "--std", "c11", "-e", "1"])
.output()
.expect("run c-shell in strict C11 mode");
assert!(
out.status.success(),
"strict ISO mode broke generated runtime:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "1");
}
#[test]
fn complete_main_definition_gets_actionable_guidance() {
let out = run(&[
"int main(void) { printf(\"hello-main\\n\"); return 0; }",
"1 + 1",
]);
assert!(
out.contains("already provides main()")
&& out.contains("enter the statements from its body directly"),
"missing main guidance:\n{out}"
);
assert!(
!out.contains("warning"),
"-Wmain-style noise leaked:\n{out}"
);
assert!(
!out.lines().any(|line| line == "hello-main"),
"user main was unexpectedly executed:\n{out}"
);
assert!(
out.contains("Out[2]: 2"),
"session did not continue after guidance:\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 file_scope_redefinition_replaces_in_place_portably() {
let out = run(&[
"int f(int n) { return n * 2; }",
"f(3)",
"int f(int n) { return n * 3; }",
"f(3)",
]);
assert!(
out.contains("Out[2]: 6"),
"old function result missing:\n{out}"
);
assert!(
out.contains("replaced previous file-scope definition"),
"replacement was not reported:\n{out}"
);
assert!(out.contains("Out[4]: 9"), "new function not used:\n{out}");
assert!(
!out.contains("kept inside main"),
"function was silently demoted:\n{out}"
);
}
#[test]
fn tag_redefinition_replaces_the_old_type_when_retained_session_still_compiles() {
let out = run(&[
"struct Box { int old_value; };",
"struct Box box = { 3 };",
"struct Box { int new_value; };",
"box.new_value",
]);
assert!(
out.contains("replaced previous file-scope definition"),
"tag definition was not replaced:\n{out}"
);
assert!(
out.contains("Out[4]: 3"),
"retained declaration did not use the new type:\n{out}"
);
}
#[test]
fn file_scope_replacement_and_functions_never_demote() {
let out = run(&[
"int g(int n) { return n * 2; }",
"int g(int n) { return n * 3; }",
"g(3)",
"int local = 7;",
"int illegal(void) { return local; }",
"local",
]);
assert!(
out.contains("Out[3]: 9"),
"replacement did not update the function:\n{out}"
);
assert!(
out.contains("error") && out.contains("local"),
"file-scope failure was hidden by nested-function demotion:\n{out}"
);
assert!(
out.contains("Out[6]: 7"),
"session did not survive the rejected function:\n{out}"
);
}
#[test]
fn compound_literal_initializer_stays_at_block_scope() {
let out = run(&[
"typedef struct { int x, y; } Point;",
"(int){ 7 }",
"Point p = (Point){ 1, 2 };",
"p.x + p.y",
]);
assert_eq!(
out.matches("added at file scope").count(),
1,
"compound literal declaration was mistaken for a function:\n{out}"
);
assert!(
out.contains("Out[2]: 7"),
"scalar compound literal stopped being an expression:\n{out}"
);
assert!(out.contains("Out[4]: 3"), "unexpected result:\n{out}");
}
#[test]
fn struct_printing_never_implicitly_dereferences_pointer_members() {
let out = run(&[
"struct Person { char *name; int age; int scores[3]; struct Person *next; };",
"struct Person p = { (char *)(uintptr_t)0x1234, 30, { 0, 7, 0 }, NULL };",
"p",
"p.name = \"Alice\";",
"p.name",
"struct Person *person_ptr = &p;",
"person_ptr",
"*person_ptr",
"struct Pair { int x, y; };",
"struct Pair pair = { 1, 2 };",
"pair",
"1 + 1",
]);
assert!(
out.contains(".name = (void *)") && out.contains("1234"),
"pointer member was not rendered as an address:\n{out}"
);
assert!(
out.contains("Out[5]: \"Alice\""),
"an explicit char-pointer member did not use string formatting:\n{out}"
);
assert_eq!(
out.matches(".age = 30").count(),
2,
"a struct value and explicit *ptr should both expand:\n{out}"
);
assert!(
out.contains(".scores = {0, 7, 0}"),
"array member missing:\n{out}"
);
assert!(out.contains(".next = NULL"), "NULL member missing:\n{out}");
assert!(
out.contains("<struct Pair raw bytes:"),
"unsupported multi-declarator did not fall back safely:\n{out}"
);
assert!(
out.contains("Out[12]: 2"),
"printing the invalid pointer dereferenced or crashed:\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 diagnostics_hide_generated_wrappers_and_marker_tokens() {
let out = run(&["int number = 1;", "numbr + 1", "1 + 1"]);
assert!(out.contains("numbr"), "real diagnostic missing:\n{out}");
assert!(
!out.contains("CS_PRINT") && !out.contains("CS_MARK"),
"generated source leaked:\n{out}"
);
assert!(
!out.contains("'do'") && !out.contains("‘do’") && !out.contains("`do'"),
"generated marker expansion leaked:\n{out}"
);
assert!(
!out.contains("expected ';'") && !out.contains("expected ‘;’"),
"synthetic statement-fallback diagnostic leaked:\n{out}"
);
assert!(
out.contains("Out[3]: 2"),
"session did not survive cleaned diagnostic:\n{out}"
);
let incomplete = run(&["1 +", "1 + 1"]);
assert!(
incomplete.contains("expected expression at end of input"),
"incomplete expression lost its diagnostic:\n{incomplete}"
);
assert!(
!incomplete.contains("before ‘)’") && !incomplete.contains("before ')'"),
"closing expression wrapper leaked:\n{incomplete}"
);
}
#[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"),
"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(["--lang", "en", "-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(["--lang", "en", "-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 redirected_file_input_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 redirected input");
let out = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["--lang", "en"])
.stdin(std::fs::File::open(&path).expect("open redirected input"))
.output()
.expect("run c-shell with redirected input");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Out[2]: 42"),
"unexpected output:\n{stdout}"
);
}
#[test]
fn removed_script_and_quiet_options_are_not_advertised_or_accepted() {
for language in ["en", "zh"] {
let help = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["--lang", language, "--help"])
.output()
.unwrap();
assert!(help.status.success());
let help = String::from_utf8_lossy(&help.stdout);
for option in ["--script", "--quiet", "-q,"] {
assert!(!help.contains(option), "removed option in help: {help}");
}
for option in ["--script", "--quiet", "-q"] {
let output = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["--lang", language, option])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(2), "{option} was not rejected");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains(option), "{stderr}");
assert!(
stderr.contains(if language == "zh" {
"无法识别参数"
} else {
"unexpected argument"
}),
"{stderr}"
);
}
}
}
#[test]
fn piped_stdin_keeps_else_with_preceding_if() {
let out = run(&[
"int x = 0;",
"if (x) {",
" puts(\"wrong\");",
"}",
"else {",
" puts(\"else-ok\");",
"}",
"1 + 1",
]);
assert!(out.contains("else-ok"), "else was detached from if:\n{out}");
assert!(
out.contains("Out[3]: 2"),
"unexpected input numbering:\n{out}"
);
}
#[test]
fn piped_stdin_accumulates_conditional_preprocessor_group() {
let out = run(&[
"#if 1",
"#define CS_TEST_ANSWER 42",
"#else",
"#define CS_TEST_ANSWER 0",
"#endif",
"CS_TEST_ANSWER",
]);
assert!(out.contains("Out[2]: 42"), "unexpected output:\n{out}");
}
#[test]
fn piped_stdin_accumulates_control_body_and_do_while() {
let out = run(&[
"int n = 0;",
"if (1)",
" n++;",
"do {",
" n++;",
"} while (0);",
"n",
]);
assert!(out.contains("Out[4]: 2"), "unexpected output:\n{out}");
}
#[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}");
let tag = run(&[
"struct P { int x; int y; }",
";",
"struct P point = { 3, 4 };",
"point",
]);
assert!(
tag.contains("Out[3]: { .x = 3, .y = 4 }"),
"tag definition was submitted before its semicolon:\n{tag}"
);
}
#[test]
fn type_magic_reports_expression_types_without_evaluating_them() {
let out = run(&[
"int n = 0;",
"const char *message = \"hello\";",
"%type n",
"%type message",
"%type n + 0.5",
"%type n++",
"n",
]);
let lines: Vec<_> = out.lines().collect();
assert!(lines.contains(&"int"), "integer type missing:\n{out}");
assert!(
lines.contains(&"const char *"),
"qualified pointer type missing:\n{out}"
);
assert!(lines.contains(&"double"), "expression type missing:\n{out}");
assert!(
out.contains("Out[3]: 0"),
"%type evaluated its controlling expression or consumed an input number:\n{out}"
);
}
#[test]
fn type_magic_recognizes_named_and_typedef_aggregates() {
let out = run(&[
"struct Pair { int x, y; };",
"union Number { int i; double d; };",
"typedef struct { int code; } Result;",
"typedef union Number Numeric;",
"struct Pair pair = { 1, 2 };",
"Numeric number = { 3 };",
"Result result = { 0 };",
"%type pair",
"%type number",
"%type result",
]);
let lines: Vec<_> = out.lines().collect();
assert!(
lines.contains(&"Struct Pair"),
"named struct type missing:\n{out}"
);
assert!(
lines.contains(&"Union Number"),
"named union type missing:\n{out}"
);
assert!(
lines.contains(&"Struct Result"),
"anonymous typedef name missing:\n{out}"
);
}
#[test]
fn bits_magic_inspects_object_representations_and_evaluates_once() {
let out = run(&[
"int n = 0;",
"%bits ++n",
"%bits 0.1f",
"%bits (unsigned char)255",
"%Bits 0.1f",
"%Bits (unsigned char)255",
"%BITS 1",
"%bits (struct { int x; }){1}",
"n",
])
.replace("\r\n", "\n");
assert!(
out.contains("type: int\nsize: 4 bytes\nvalue: 1\nhex: 0x00000001\nbinary: 00000000"),
"integer representation missing:\n{out}"
);
assert!(
out.contains("type: float")
&& out.contains("hex: 0x3dcccccd")
&& out.contains("sign: 0")
&& out.contains("exponent: 123 (-4)")
&& out.contains("fraction: 0x4ccccd"),
"IEEE-754 fields missing:\n{out}"
);
assert!(
out.contains("type: unsigned char")
&& out.contains("binary: 11111111")
&& out.contains("memory: ff"),
"byte representation missing:\n{out}"
);
assert!(
out.contains("hex: 0X3DCCCCCD")
&& out.contains("memory: CD CC CC 3D")
&& out.contains("fraction: 0X4CCCCD")
&& out.contains("hex: 0XFF"),
"uppercase hexadecimal representation missing:\n{out}"
);
assert!(
out.contains("unknown command %BITS"),
"unexpected case-insensitive magic matching:\n{out}"
);
assert!(
out.contains("byte order: ") && out.contains("Out[2]: 0"),
"%bits evaluated its expression more than once, retained it, or consumed an input number:\n{out}"
);
assert!(
out.contains("%bits supports standard scalar values and pointers to scalar types"),
"unsupported type diagnostic missing:\n{out}"
);
}
#[test]
fn where_magic_uses_the_iso_index_without_consuming_input_number() {
let out = run(&[
"int marker = 7;",
"%where printf",
"%where sqrt",
"%where gets",
"%where ckd_add",
"%where getline",
"%where",
"marker",
])
.replace("\r\n", "\n");
assert!(
out.contains("name: printf")
&& out.contains("| header | documentation")
&& out.contains("| <stdio.h> | https://en.cppreference.com/c/header/stdio")
&& out.contains("signature: int printf("),
"stdio lookup missing:\n{out}"
);
assert!(
out.contains("name: sqrt")
&& out.contains("| <math.h> | https://en.cppreference.com/c/header/math")
&& out.contains("| <tgmath.h> | https://en.cppreference.com/c/header/tgmath")
&& out.contains("kind: function / type-generic macro"),
"multi-header lookup missing:\n{out}"
);
assert!(
out.contains("name: gets")
&& out.contains("removed in C11")
&& out.contains("note: deprecated;"),
"removed/deprecated metadata missing:\n{out}"
);
assert!(
out.contains("name: ckd_add")
&& out.contains("| <stdckdint.h> | https://en.cppreference.com/c/header/stdckdint")
&& out.contains("ISO C availability: C23 and later"),
"C23 lookup missing:\n{out}"
);
assert!(
out.contains("getline was not found in c-shell's ISO C standard library index"),
"POSIX identifier was not excluded:\n{out}"
);
assert!(
out.contains("usage: %where <identifier>"),
"bare %where did not show usage:\n{out}"
);
assert!(
out.contains("Out[2]: 7"),
"%where changed session state or consumed an input number:\n{out}"
);
}
#[test]
fn src_defaults_to_user_view_and_raw_keeps_scaffolding() {
let user = run(&["int source_value = 7;", "%src"]);
assert!(user.contains("int main(void)"), "main missing:\n{user}");
assert!(
user.contains("int source_value = 7;"),
"retained statement missing:\n{user}"
);
assert!(
!user.contains("CS_PRINT") && !user.contains("CS_MARK"),
"runtime scaffolding leaked into default source view:\n{user}"
);
let raw = run(&["int source_value = 7;", "%src --raw"]);
assert!(
raw.contains("CS_PRINT") && raw.contains("CS_MARK"),
"raw source omitted runtime scaffolding:\n{raw}"
);
}
#[test]
fn edit_without_previous_or_matching_c_input_is_safe() {
let out = run(&["%edit", "1 + 1"]);
assert!(
out.contains("nothing to edit"),
"unexpected edit result:\n{out}"
);
assert!(
out.contains("Out[1]: 2"),
"edit consumed an input number:\n{out}"
);
let missing = run(&["1 + 1", "%edit 7"]);
assert!(
missing.contains("no C input In[7]"),
"missing numbered input was not diagnosed:\n{missing}"
);
let batch = run(&["1 + 1", "%edit 1", "2 + 2"]);
assert!(
batch.contains("%edit is available only in interactive REPL mode"),
"batch edit did not explain that no prompt can be pre-filled:\n{batch}"
);
assert!(
batch.contains("Out[2]: 4"),
"batch edit consumed an input number or stopped the session:\n{batch}"
);
}
#[test]
fn clear_erases_screen_without_resetting_session() {
let out = run(&["int kept = 42;", "%clear", "kept"]);
assert!(
out.contains("\x1b[2J\x1b[H"),
"clear sequence missing:\n{out:?}"
);
assert!(
out.contains("Out[2]: 42"),
"clear changed session state or input numbering:\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 successful_early_exit_is_not_committed() {
let out = run(&["exit(0);", "1 + 1"]);
assert!(
out.contains("exited before the input completed"),
"missing protocol failure:\n{out}"
);
assert!(out.contains("Out[2]: 2"), "session stayed wedged:\n{out}");
}
#[test]
fn unsupported_value_category_is_explained_and_not_misclassified() {
let out = run(&["((struct { int x; }){ 1 })", "1 + 1"]);
assert!(
out.contains("value category has no printer"),
"missing printer explanation:\n{out}"
);
assert!(
out.contains("Out[2]: 2"),
"session did not continue:\n{out}"
);
}
#[test]
fn unsupported_explicit_standard_is_an_error() {
let out = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args([
"--lang",
"en",
"--std",
"definitely-not-a-c-standard",
"-e",
"1",
])
.output()
.expect("run c-shell with invalid standard");
assert!(!out.status.success(), "invalid --std was silently ignored");
assert!(
String::from_utf8_lossy(&out.stderr).contains("does not support requested standard"),
"unexpected diagnostic:\n{}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn external_side_effect_replay_warning_is_shown_once() {
let out = run(&[
"void write_risk(FILE *fp) { fprintf(fp, \"line\\n\"); fclose(fp); }",
"void process_risk(void) { system(\"echo not-called\"); }",
"1 + 1",
]);
assert!(
out.contains("external side-effect call detected (fprintf(), fclose())"),
"missing actionable replay warning:\n{out}"
);
assert_eq!(
out.matches("file or process effects may happen repeatedly")
.count(),
1,
"replay warning was not once-per-session:\n{out}"
);
assert!(
out.contains("Out[3]: 2"),
"session did not continue:\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}");
let partial = run(&["printf(\"partial\");", "1 + 1"]);
assert!(
partial.contains("partial\n"),
"partial output lost:\n{partial}"
);
assert!(
!partial.contains('↵'),
"TTY marker leaked into pipe:\n{partial}"
);
}
#[test]
fn time_and_timeit_magics_work() {
let out = run(&[
"int acc = 0;",
"%time ++acc",
"acc",
"%timeit acc += 5",
"acc",
"%time exit(0);",
]);
assert!(
out.contains("Wall time:"),
"missing %time output:
{out}"
);
assert!(
out.contains("Out[2]: 1"),
"%time expression was evaluated more than once:
{out}"
);
assert!(
out.contains("Out[3]: 1"),
"%time side effect was not retained:
{out}"
);
assert!(
out.contains("\"%timeit input\" may execute repeatedly and is not retained for replay"),
"missing %timeit non-replay warning:
{out}"
);
assert!(
out.contains("per loop"),
"missing %timeit report:
{out}"
);
assert!(
out.contains("Out[4]: 1"),
"%timeit modified session state:
{out}"
);
assert!(
!out.contains("c-shell/time"),
"internal timing marker leaked:
{out}"
);
assert!(
out.contains("Wall time: unavailable (input did not complete)"),
"incomplete %time input fell back to whole-process timing:
{out}"
);
}
#[test]
fn timeit_pure_function_does_not_warn() {
let out = run(&[
"long long fib(int n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); }",
"%timeit fib(8)",
]);
assert!(out.contains("per loop"), "missing %timeit report:\n{out}");
assert!(
!out.contains("\"%timeit input\" may execute repeatedly"),
"pure function benchmark emitted a warning:\n{out}"
);
}
#[test]
fn help_lists_commands_and_keeps_usage_notes_behind_verbose() {
let plain = run(&["%help"]);
assert!(
plain.contains("Commands:"),
"missing Commands list:
{plain}"
);
assert!(
plain.contains("%where <identifier>"),
"missing %where command:
{plain}"
);
assert!(
plain.contains("%utf8/%utf16/%utf32"),
"missing Unicode commands:
{plain}"
);
assert!(
!plain.contains("Notes:"),
"%help unexpectedly printed Notes:
{plain}"
);
let verbose = run(&["%help --verbose"]);
assert!(
verbose.contains("Commands:"),
"missing Commands list:
{verbose}"
);
assert!(
verbose.contains("Notes:"),
"missing Notes in verbose help:
{verbose}"
);
}
#[test]
fn language_override_localizes_c_shell_but_not_compiler_diagnostics() {
let chinese = run_with_language("zh", &["%help", "%where sqrt", "%bits 1"]);
assert!(
chinese.contains("命令:")
&& chinese.contains("名称: sqrt")
&& chinese.contains("头文件:")
&& chinese.contains("类型: int")
&& chinese.contains("大小: 4 字节"),
"Chinese UI was incomplete:\n{chinese}"
);
assert!(
!chinese.contains("Commands:") && !chinese.contains("Name: sqrt"),
"English c-shell labels leaked into the Chinese UI:\n{chinese}"
);
let help = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["--lang", "zh", "--help"])
.output()
.expect("run localized --help");
assert!(help.status.success());
let help = String::from_utf8_lossy(&help.stdout);
assert!(
help.contains("用法:") && help.contains("选项:") && help.contains("(默认:10)"),
"command-line help was not localized:\n{help}"
);
assert!(
!help.contains("[default:") && !help.contains("[possible values:"),
"clap's English annotations leaked into Chinese help:\n{help}"
);
let invalid_cli = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["--lang", "zh", "--timeout", "not-a-number"])
.output()
.expect("run invalid localized command line");
assert!(!invalid_cli.status.success());
let invalid_cli = String::from_utf8_lossy(&invalid_cli.stderr);
assert!(
invalid_cli.contains("错误:")
&& invalid_cli.contains("的值“not-a-number”无效")
&& invalid_cli.contains("用法:c-shell [OPTIONS]")
&& invalid_cli.contains("更多信息请尝试“--help”。"),
"command-line error was not localized:\n{invalid_cli}"
);
assert!(
!invalid_cli.contains("invalid value") && !invalid_cli.contains("For more information"),
"English clap text leaked into a Chinese command-line error:\n{invalid_cli}"
);
let compiler_error = |language| {
Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args([
"--no-color",
"--lang",
language,
"-e",
"c_shell_i18n_missing_identifier",
])
.output()
.expect("run failing C input")
};
let english_error = compiler_error("en");
let chinese_error = compiler_error("zh");
assert!(!english_error.status.success() && !chinese_error.status.success());
assert_eq!(
english_error.stderr, chinese_error.stderr,
"c-shell modified the compiler's diagnostic for the Chinese UI"
);
}
#[test]
fn header_lists_the_default_includes() {
let out = run(&["%header"]);
assert!(
out.contains("#include <stdio.h>")
&& out.contains("#include <string.h>")
&& out.contains("#if defined(__has_include)")
&& out.contains("# if __has_include(<uchar.h>)")
&& out.contains("#include <uchar.h>")
&& out.contains("#include <wchar.h>"),
"default headers were not listed:\n{out}"
);
}
#[test]
fn macro_expansions_retain_mutations_and_stdin_events() {
let output = run_evals_with_stdin(
&[
"#define READ scanf(\"%*d\")",
"READ",
"int n = 0;",
"#define INC (++n)",
"#define ALIAS INC",
"ALIAS",
"n",
],
"7\n",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8_lossy(&output.stdout), "0\n1\n1\n");
}
#[test]
fn char_array_previews_stop_at_the_array_boundary_and_evaluate_once() {
let output = run_evals_with_stdin(
&[
"struct S { char a[3]; char b[4]; };",
"struct S s = {{65,66,67}, {88,89,90,0}};",
"s.a",
"char rows[2][3] = {{65,66,67}, {88,89,90}};",
"int next = 0;",
"rows[next++]",
"next",
"char eight[8] = {65,66,67,68,69,70,71,72};",
"eight",
"const char constant[3] = {68,69,70};",
"constant",
"char *text = \"longer than a pointer\";",
"text",
"\"literal\"",
],
"",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"\"ABC\"\n\"ABC\"\n1\n\"ABCDEFGH\"\n\"DEF\"\n\"longer than a pointer\"\n\"literal\"\n"
);
}
#[test]
fn scanf_buffered_and_empty_conversions_replay_without_divergence() {
for (codes, input, expected) in [
(
vec![
"int a=0,b=0,c=0;",
"scanf(\"%d\",&a);",
"scanf(\"%d\",&b);",
"scanf(\"%d\",&c);",
"a+b+c",
],
"10 20\n99\n",
"129\n",
),
(
vec![
"char a=0,b=0;",
"scanf(\"%c\",&a);",
"scanf(\"%c\",&b);",
"(int)b",
],
"a\nb\n",
"10\n",
),
(vec!["scanf(\"\");", "42"], "unused\n", "42\n"),
(vec!["int n=0;", "scanf(\"%d\",&n);", "n"], "7", "7\n"),
] {
let output = run_evals_with_stdin(&codes, input);
assert!(
output.status.success(),
"{codes:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
expected,
"{codes:?}"
);
}
}
#[cfg(unix)]
#[test]
fn buffered_scanf_does_not_wait_for_another_parent_input_line() {
for codes in [
vec![
"char a=0,b=0;",
"scanf(\"%c\",&a);",
"scanf(\"%c\",&b);",
"(int)b",
],
vec!["scanf(\"\");", "10"],
] {
let mut command = Command::new(env!("CARGO_BIN_EXE_c-shell"));
command.args(["--lang", "en", "--no-color"]);
for code in &codes {
command.args(["-e", code]);
}
let mut child = ChildGuard(
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap(),
);
let mut input = child.0.stdin.take().unwrap();
input.write_all(b"a\n").unwrap();
input.flush().unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
if let Some(status) = child.0.try_wait().unwrap() {
assert!(status.success(), "buffered scanf failed: {codes:?}");
break;
}
assert!(
std::time::Instant::now() < deadline,
"buffered scanf stalled: {codes:?}"
);
std::thread::sleep(Duration::from_millis(10));
}
let mut stdout = String::new();
child
.0
.stdout
.take()
.unwrap()
.read_to_string(&mut stdout)
.unwrap();
assert_eq!(stdout, "10\n");
}
}
#[test]
fn leading_and_interleaved_comments_preserve_file_scope() {
let output = run_evals_with_stdin(
&[
"/* type */ struct /* tag */ S /* body */ { int n; };",
"/* function */ int f(void) { return 42; }",
"int g(void) { struct S s={f()}; return s.n; }",
"g()",
],
"",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8_lossy(&output.stdout), "42\n");
}
#[test]
fn c23_digit_separators_do_not_join_independent_batch_inputs() {
let probe = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["--std", "c23", "-e", "1'000"])
.output()
.unwrap();
if !probe.status.success() {
return;
}
let mut child = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.args(["--std", "c23", "--lang", "en", "--no-color"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b"1'000\n2+2\n")
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"Out[1]: 1000\nOut[2]: 4\n"
);
}
#[test]
fn magic_failures_set_exit_status_and_route_diagnostics_to_stderr() {
for code in [
"%type nonexistent_name",
"%bits",
"%utf8 -n 4097 text",
"%unknown",
"%where nonexistent_name",
"%std impossible-standard",
"%cc nonexistent-c-compiler",
"%edit 999",
"%time nonexistent_name",
"%timeit nonexistent_name",
"%help invalid",
"%src invalid",
] {
let output = run_evals_with_stdin(&[code], "");
assert!(!output.status.success(), "magic succeeded: {code}");
assert!(
output.stdout.is_empty(),
"magic error on stdout: {code}: {}",
String::from_utf8_lossy(&output.stdout)
);
assert!(!output.stderr.is_empty(), "missing diagnostic: {code}");
}
}
#[test]
fn timed_inputs_preserve_program_output_and_bare_value_formatting() {
let output = run_evals_with_stdin(&["%time puts(\"hello\");", "%time 40+2"], "");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let expected = if cfg!(windows) {
"hello\r\n42\n"
} else {
"hello\n42\n"
};
assert_eq!(String::from_utf8_lossy(&output.stdout), expected);
assert_eq!(
String::from_utf8_lossy(&output.stderr)
.matches("Wall time:")
.count(),
2
);
let transcript = run(&["%time puts(\"hello\");"]);
assert_eq!(transcript.matches("hello").count(), 1);
assert!(transcript.contains("Wall time:"));
}
#[test]
fn eval_and_redirected_input_clean_up_generated_files_even_after_errors() {
let dir = tempfile::tempdir().unwrap();
let scratch = dir.path().join("scratch");
std::fs::create_dir(&scratch).unwrap();
let input = dir.path().join("example.csh");
for code in ["42", "int broken = ;"] {
std::fs::write(&input, format!("{code}\n")).unwrap();
for redirected_input in [false, true] {
let mut command = Command::new(env!("CARGO_BIN_EXE_c-shell"));
command
.env("TMPDIR", &scratch)
.env("TMP", &scratch)
.env("TEMP", &scratch);
if redirected_input {
command.stdin(std::fs::File::open(&input).unwrap());
} else {
command.args(["-e", code]);
}
let output = command.output().unwrap();
assert_eq!(output.status.success(), redirected_input || code == "42");
if code != "42" {
let diagnostics = if redirected_input {
&output.stdout
} else {
&output.stderr
};
assert!(String::from_utf8_lossy(diagnostics).contains("error"));
}
let leftovers = std::fs::read_dir(&scratch).unwrap().collect::<Vec<_>>();
assert!(
leftovers.is_empty(),
"temporary files survived: {leftovers:?}"
);
}
}
}
#[cfg(unix)]
#[test]
fn timeit_compiles_once_for_calibration_and_all_rounds() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let wrapper = dir.path().join("cc-logger");
let log = dir.path().join("compilations");
std::fs::write(
&wrapper,
r#"#!/bin/sh
for arg in "$@"; do
case "$arg" in
*/input.c) printf 'compile\n' >> "$C_SHELL_TEST_LOG" ;;
esac
done
exec "${C_SHELL_TEST_CC:-cc}" "$@"
"#,
)
.unwrap();
std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o700)).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.arg("--cc")
.arg(&wrapper)
.args(["--lang", "en", "-e", "%timeit 1+1"])
.env("C_SHELL_TEST_LOG", &log)
.env(
"C_SHELL_TEST_CC",
std::env::var_os("CC").unwrap_or_else(|| "cc".into()),
)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("5 runs"));
assert_eq!(
std::fs::read_to_string(log).unwrap().lines().count(),
1,
"calibration and all measured rounds must share one executable"
);
}
#[test]
fn completed_scanf_calls_cannot_keep_renewing_the_execution_timeout() {
for input in [
"while (1) { scanf(\"\"); }",
"while (1) { scanf(\"%d\", &n); }",
] {
let output = run_with_timeout(1, &["int n=0;", input, "42"]);
assert!(
output.contains("killed after 1s"),
"missing deadline: {output}"
);
assert!(
output.contains("Out[3]: 42"),
"REPL failed to recover: {output}"
);
}
}
#[cfg(unix)]
#[test]
fn variable_length_character_array_expressions_are_evaluated_once() {
let output = run_evals_with_stdin(
&[
"int n=3;",
"char rows[2][n];",
"memset(rows,65,sizeof rows);",
"int next=0;",
"rows[next++]",
"next",
],
"",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8_lossy(&output.stdout), "\"AAA\"\n1\n");
assert!(!String::from_utf8_lossy(&output.stderr).contains("sequence-point"));
}
#[test]
fn timeit_calibrates_gradually_when_a_single_iteration_measures_zero() {
let output = run_evals_with_stdin(
&[
"struct TimerState { uint64_t iterations; } timer_state = {0};",
"uint64_t coarse_clock(void) { return timer_state.iterations <= 1 ? 0 : timer_state.iterations * 1000; }",
"#define cs_timeit_now_ns coarse_clock",
"%timeit if (++timer_state.iterations > 100000) exit(77);",
"timer_state.iterations",
],
"",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("per loop"), "missing benchmark: {stdout}");
assert!(
stdout.contains("5 runs, 100,000 loops each"),
"bad calibration: {stdout}"
);
assert!(
stdout.ends_with("\n0\n"),
"benchmark state leaked: {stdout}"
);
}
fn runtime_check_output(language: &str, inputs: &[&str]) -> std::process::Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_c-shell"));
command.args(["--no-color", "--lang", language, "--timeout", "15"]);
for input in inputs {
command.args(["-e", input]);
}
command.output().expect("run runtime-check experiment")
}
fn runtime_check_unavailable(output: &std::process::Output) -> bool {
let diagnostic = String::from_utf8_lossy(&output.stderr);
let unavailable = diagnostic
.contains("is unavailable with the current compiler, target, or runtime installation.");
if unavailable {
assert!(!output.status.success());
let required = std::env::var("C_SHELL_TEST_REQUIRE_SANITIZERS").unwrap_or_default();
let required_backend = required == "all"
|| (required == "address" && !diagnostic.contains("UndefinedBehaviorSanitizer"))
|| (required == "undefined" && !diagnostic.contains("AddressSanitizer"));
assert!(
!required_backend,
"CI required {required} checks but a backend was unavailable: {diagnostic}"
);
}
unavailable
}
#[test]
fn runtime_checks_are_temporary_and_success_is_quiet() {
let output = runtime_check_output(
"zh",
&["int x = 41;", "%check ++x", "x", "%check x = 99;", "x"],
);
if runtime_check_unavailable(&output) {
return;
}
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "{stderr}");
assert_eq!(String::from_utf8_lossy(&output.stdout), "42\n41\n41\n");
assert!(
stderr.is_empty(),
"successful checks emitted diagnostics: {stderr}"
);
}
#[test]
fn runtime_checks_report_undefined_arithmetic_and_leave_session_usable() {
let output = runtime_check_output(
"en",
&[
"int x = INT_MAX;",
"%check --undefined x + 1",
"%check --undefined { volatile int zero = 0; printf(\"%d\", 1 / zero); }",
"%check --undefined { volatile int shift = -1; printf(\"%u\", 1u << shift); }",
"x == INT_MAX",
],
);
if runtime_check_unavailable(&output) {
return;
}
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success());
for message in [
"signed integer overflow",
"division by zero",
"shift exponent -1",
] {
assert!(stderr.contains(message), "missing {message}: {stderr}");
}
assert_eq!(String::from_utf8_lossy(&output.stdout), "1\n");
assert_eq!(stderr.matches("Runtime check:").count(), 3);
}
#[test]
fn runtime_checks_detect_heap_bounds_and_freed_memory() {
let output = runtime_check_output(
"en",
&[
"int *p = malloc(2 * sizeof *p); if (!p) abort();",
"%check --address p[2] = 7;",
"%check --address { free(p); printf(\"%d\", *p); }",
"%check --address { free(p); free(p); }",
"6 * 7",
],
);
if runtime_check_unavailable(&output) {
return;
}
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success());
for message in [
"memory access out of bounds",
"use after free",
"double free",
] {
assert!(stderr.contains(message), "missing {message}: {stderr}");
}
assert_eq!(String::from_utf8_lossy(&output.stdout), "42\n");
assert!(!stderr.contains("LeakSanitizer"), "{stderr}");
}
#[test]
fn runtime_checks_keep_reports_from_replay_and_exit_handlers() {
let replay = runtime_check_output(
"en",
&[
"volatile int x = INT_MAX;",
"x += 1;",
"%check --undefined 42",
"42",
],
);
if !runtime_check_unavailable(&replay) {
let stderr = String::from_utf8_lossy(&replay.stderr);
assert!(!replay.status.success());
assert!(stderr.contains("signed integer overflow"), "{stderr}");
assert!(!stderr.contains("retained code, line"), "{stderr}");
assert_eq!(String::from_utf8_lossy(&replay.stdout), "42\n");
}
let exit = runtime_check_output(
"en",
&[
"void on_exit_check(void) { volatile int x = INT_MAX; x += 1; }",
"%check --undefined atexit(on_exit_check)",
"42",
],
);
if runtime_check_unavailable(&exit) {
return;
}
let stderr = String::from_utf8_lossy(&exit.stderr);
assert!(!exit.status.success());
assert!(stderr.contains("signed integer overflow"), "{stderr}");
assert!(stderr.contains("Runtime check:"), "{stderr}");
assert_eq!(String::from_utf8_lossy(&exit.stdout), "42\n");
}
#[test]
fn runtime_checks_reject_invalid_usage_in_english() {
for input in [
"%check",
"%check --address",
"%check --bogus 42",
"%check --verbose 42",
] {
let output = runtime_check_output("zh", &[input, "42"]);
assert!(!output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "42\n");
assert!(String::from_utf8_lossy(&output.stderr).contains("Usage: %check"));
}
}
#[cfg(unix)]
#[test]
fn runtime_checks_probe_on_demand_cache_results_and_reject_ignored_flags() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let wrapper = dir.path().join("cc-check-probe");
let log = dir.path().join("probes");
std::fs::write(
&wrapper,
r#"#!/bin/sh
for arg in "$@"; do
case "$arg" in
-fsanitize=*)
printf 'probe\n' >> "$C_SHELL_CHECK_LOG"
printf 'warning: ignoring unknown option %s\n' "$arg" >&2
exit 0
;;
esac
done
exec "$C_SHELL_CHECK_CC" "$@"
"#,
)
.unwrap();
std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o700)).unwrap();
let run = |inputs: &[&str]| {
let mut command = Command::new(env!("CARGO_BIN_EXE_c-shell"));
command
.arg("--cc")
.arg(&wrapper)
.args(["--lang", "zh"])
.env("C_SHELL_CHECK_LOG", &log)
.env(
"C_SHELL_CHECK_CC",
std::env::var_os("CC").unwrap_or_else(|| "cc".into()),
);
for input in inputs {
command.args(["-e", input]);
}
command.output().unwrap()
};
let plain = run(&["42"]);
assert!(
plain.status.success(),
"{}",
String::from_utf8_lossy(&plain.stderr)
);
assert!(
!log.exists(),
"ordinary evaluation must not probe sanitizers"
);
let checked = run(&[
"%check --undefined 42",
"%check --undefined 43",
"%std c17",
"%check --undefined 44",
"42",
]);
assert!(!checked.status.success());
let stderr = String::from_utf8_lossy(&checked.stderr);
assert_eq!(
stderr
.matches("UndefinedBehaviorSanitizer is unavailable")
.count(),
3
);
assert_eq!(
std::fs::read_to_string(log).unwrap().lines().count(),
2,
"failed probes are cached until the toolchain or standard changes"
);
assert!(String::from_utf8_lossy(&checked.stdout).ends_with("42\n"));
}
#[cfg(unix)]
#[test]
fn runtime_checks_automatically_use_the_available_backend() {
let supported = runtime_check_output("en", &["%check --undefined 1 + 1"]);
if runtime_check_unavailable(&supported) {
return;
}
assert!(
supported.status.success(),
"{}",
String::from_utf8_lossy(&supported.stderr)
);
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let wrapper = dir.path().join("cc-without-address-sanitizer");
std::fs::write(
&wrapper,
r#"#!/bin/sh
for arg in "$@"; do
case "$arg" in
-fsanitize=*address*) printf 'address runtime unavailable\n' >&2; exit 1 ;;
esac
done
exec "$C_SHELL_CHECK_CC" "$@"
"#,
)
.unwrap();
std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o700)).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_c-shell"))
.arg("--cc")
.arg(wrapper)
.args([
"--lang",
"en",
"-e",
"int x = INT_MAX;",
"-e",
"%check x + 1",
"-e",
"42",
])
.env(
"C_SHELL_CHECK_CC",
std::env::var_os("CC").unwrap_or_else(|| "cc".into()),
)
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success());
assert!(stderr.contains("signed integer overflow"), "{stderr}");
assert!(stderr.contains("Runtime check:"), "{stderr}");
assert!(!stderr.contains("is unavailable"), "{stderr}");
assert_eq!(String::from_utf8_lossy(&output.stdout), "42\n");
}
#[test]
fn runtime_check_reports_are_always_concise() {
let supported = runtime_check_output("en", &["%check --undefined 1 + 1"]);
if runtime_check_unavailable(&supported) {
return;
}
let concise = runtime_check_output("zh", &["int x = INT_MAX;", "%check x + 1"]);
if runtime_check_unavailable(&concise) {
return;
}
let stderr = String::from_utf8_lossy(&concise.stderr);
assert!(!concise.status.success());
assert!(
stderr.contains("Runtime check: signed integer overflow."),
"{stderr}"
);
assert!(
stderr.contains("2147483647 + 1 cannot be represented in type 'int'"),
"{stderr}"
);
assert!(!stderr.contains("--verbose"), "{stderr}");
assert_eq!(stderr.lines().count(), 2, "{stderr}");
assert!(!stderr.contains("not retained"), "{stderr}");
for internal in [
"#0",
"BuildId:",
"input.c:",
"libc.so",
"0x",
"(line ",
"<input>",
"<session>",
] {
assert!(
!stderr.contains(internal),
"unexpected {internal}: {stderr}"
);
}
}
#[test]
fn runtime_check_summary_preserves_program_stderr_separately() {
let output = runtime_check_output(
"en",
&[
"%check --address { fprintf(stderr, \"program diagnostic\\n\"); volatile int *p = malloc(sizeof *p); if (!p) abort(); p[2] = 7; }",
"42",
],
);
if runtime_check_unavailable(&output) {
return;
}
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success());
assert_eq!(stderr.matches("program diagnostic").count(), 1, "{stderr}");
assert!(stderr.contains("memory access out of bounds"), "{stderr}");
assert!(!stderr.contains("Shadow bytes"), "{stderr}");
assert!(!stderr.contains("AddressSanitizer:"), "{stderr}");
assert_eq!(String::from_utf8_lossy(&output.stdout), "42\n");
}
#[test]
fn runtime_check_does_not_mistake_program_messages_for_sanitizer_reports() {
let output = runtime_check_output(
"en",
&[
"%check fprintf(stderr, \"runtime error: demo\\n\");",
"%check fprintf(stderr, \"ERROR: AddressSanitizer is ordinary text\\n\");",
"%check fprintf(stderr, \"example.c:1:2: runtime error: demo\\n\");",
"42",
],
);
if runtime_check_unavailable(&output) {
return;
}
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "{stderr}");
let newline = if cfg!(windows) { "\r\n" } else { "\n" };
assert_eq!(
stderr,
format!(
"runtime error: demo{newline}ERROR: AddressSanitizer is ordinary text{newline}example.c:1:2: runtime error: demo{newline}"
)
);
assert_eq!(String::from_utf8_lossy(&output.stdout), "42\n");
}
#[test]
fn runtime_check_diagnostic_examples_do_not_block_stdin_requests() {
let output = run_evals_with_stdin(
&[
"%check { int n = 0; fprintf(stderr, \"example.c:1:2: runtime error: demo\\n\"); scanf(\"%d\", &n); printf(\"read=%d\\n\", n); }",
"42",
],
"7\n",
);
if runtime_check_unavailable(&output) {
return;
}
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "{stderr}");
let newline = if cfg!(windows) { "\r\n" } else { "\n" };
assert_eq!(
String::from_utf8_lossy(&output.stdout),
format!("read=7{newline}42\n")
);
assert!(
stderr.contains(&format!("example.c:1:2: runtime error: demo{newline}")),
"{stderr}"
);
assert!(!stderr.contains("Runtime check"), "{stderr}");
}
#[test]
fn string_previews_escape_controls_without_changing_program_output() {
let output = run_evals_with_stdin(
&[
r#""line\n\t\r\"\\\033[2J""#,
"(char)'\\n'",
r#"puts("real\noutput");"#,
r#""\xe4\xbd\xa0\xe5\xa5\xbd""#,
],
"",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let newline = if cfg!(windows) { "\r\n" } else { "\n" };
let expected = format!(
"\"line\\n\\t\\r\\\"\\\\\\033[2J\"\n'\\n' (10)\nreal{newline}output{newline}\"你好\"\n"
);
assert_eq!(String::from_utf8_lossy(&output.stdout), expected);
}
#[test]
fn piped_input_lines_survive_separate_evaluations_and_reset() {
let output = run_evals_with_stdin(
&[
"int first = 0; scanf(\"%d\", &first);",
"first",
"int second = 0; scanf(\"%d\", &second);",
"second",
"%reset",
"int third = 0; scanf(\"%d\", &third);",
"third",
],
"11\n22\n33\n",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"11\n22\nsession cleared\n33\n"
);
}
#[test]
fn character_and_line_input_replay_preserves_buffering_pushback_and_eof() {
let output = run_evals_with_stdin(
&[
"int first = getchar();",
"int second = getc(stdin);",
"int pushed = ungetc('X', stdin);",
"int third = fgetc(stdin);",
"char line[32];",
"fgets(line, sizeof line, stdin);",
"first",
"second",
"third",
"line",
"int number = 0; scanf(\"%d\", &number);",
"int separator = getchar();",
"char small[3]; fgets(small, sizeof small, stdin);",
"char rest[16]; fgets(rest, sizeof rest, stdin);",
"number",
"separator",
"small",
"rest",
"int end = getchar();",
"end == EOF",
"feof(stdin) != 0",
"clearerr(stdin);",
"getchar() == EOF",
],
"ab tail\n42\nlonger\n",
);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"stdout: {stdout}\nstderr: {stderr}"
);
assert_eq!(
stdout,
"97\n98\n88\n\" tail\\n\"\n42\n10\n\"lo\"\n\"nger\\n\"\n1\n1\n1\n"
);
assert!(!stderr.contains("diverged"), "{stderr}");
}
#[test]
fn taped_input_leaves_other_file_streams_with_native_behavior() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("input.txt");
let path = path.to_str().unwrap().replace('\\', "\\\\");
let open = format!("FILE *stream = fopen(\"{path}\", \"w+\"); if (!stream) abort();");
let output = run_evals_with_stdin(
&[
&open,
"fputs(\"ABCDE\\n\", stream); rewind(stream);",
"char line[3]; fgets(line, sizeof line, stream);",
"line",
"fgetc(stream)",
"getc(stream)",
"int from_stdin = getchar();",
"from_stdin",
"char rest[8]; fgets(rest, sizeof rest, stream);",
"rest",
],
"Z\n",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"\"AB\"\n67\n68\n90\n\"E\\n\"\n"
);
}
#[test]
fn buffered_character_reads_do_not_wait_for_another_input_line() {
let mut command = Command::new(env!("CARGO_BIN_EXE_c-shell"));
command.args(["--lang", "en", "--timeout", "2", "-e",
"int a = getchar(); int b = getchar(); int c = getchar(); printf(\"%d %d %d\\n\", a, b, c);",
"-e", "42"]);
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut input = child.stdin.take().unwrap();
input.write_all(b"ab\n").unwrap();
input.flush().unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
if child.try_wait().unwrap().is_some() {
break;
}
if std::time::Instant::now() > deadline {
let _ = child.kill();
panic!("buffered getchar requested another input line");
}
std::thread::sleep(Duration::from_millis(10));
}
drop(input);
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let newline = if cfg!(windows) { "\r\n" } else { "\n" };
assert_eq!(
String::from_utf8_lossy(&output.stdout),
format!("97 98 10{newline}42\n")
);
}