mod common;
#[cfg(unix)]
use common::Terminal;
#[test]
#[cfg(unix)]
fn test_pty_startup() {
let mut terminal = Terminal::spawn().expect("Failed to spawn arf");
terminal
.expect("# arf console v")
.expect("Should show version banner");
terminal
.expect("is ready")
.expect("R should be initialized");
terminal.wait_for_prompt().expect("Should show prompt");
terminal.quit().expect("Should quit cleanly");
}
#[test]
#[cfg(unix)]
fn test_pty_ctrl_c() {
let mut terminal = Terminal::spawn().expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal.send("some_incomplete").expect("Should write");
std::thread::sleep(std::time::Duration::from_millis(100));
terminal.send_interrupt().expect("Should send interrupt");
terminal
.wait_for_prompt()
.expect("Should show prompt after Ctrl+C");
terminal.quit().expect("Should quit cleanly");
}
#[test]
#[cfg(unix)]
fn test_pty_startup_screen() {
let mut terminal = Terminal::spawn().expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal
.current_line()
.assert_contains("> ")
.expect("Current line should show prompt");
terminal.send_line("").expect("Should send empty line");
std::thread::sleep(std::time::Duration::from_millis(100));
terminal
.current_line()
.assert_contains("> ")
.expect("Should show prompt after empty input");
terminal.quit().expect("Should quit cleanly");
}
#[test]
#[cfg(unix)]
fn test_pty_basic_expression() {
let mut terminal = Terminal::spawn().expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal.send_line("1 + 1").expect("Should send expression");
terminal
.previous_line(1)
.assert_contains("[1] 2")
.expect("Should show result [1] 2");
terminal
.current_line()
.assert_contains("> ")
.expect("Should show new prompt");
terminal.quit().expect("Should quit cleanly");
}
#[test]
#[cfg(unix)]
fn test_pty_variable_assignment() {
let mut terminal =
Terminal::spawn_with_args(&["--no-auto-match"]).expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal
.send_line("x <- 'hello'")
.expect("Should send assignment");
terminal
.clear_and_expect("> ")
.expect("Should show prompt after assignment");
terminal
.send_line("nchar(x)")
.expect("Should send nchar(x)");
terminal
.clear_and_expect("[1] 5")
.expect("nchar(x) should return 5");
terminal.quit().expect("Should quit cleanly");
}
#[test]
#[cfg(unix)]
fn test_pty_cat_output() {
let mut terminal = Terminal::spawn().expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal
.send_line("cat('hello world\\n')")
.expect("Should send cat expression");
terminal
.expect("hello world")
.expect("Should see cat output");
terminal
.wait_for_prompt()
.expect("Should show prompt after cat");
terminal.quit().expect("Should quit cleanly");
}
#[test]
#[cfg(unix)]
fn test_pty_multiline_input() {
let mut terminal =
Terminal::spawn_with_args(&["--no-auto-match"]).expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal
.send_line("f <- function(x) {")
.expect("Should send first line");
terminal
.clear_and_expect("+")
.expect("Should show continuation prompt");
terminal
.send_line(" x + 1")
.expect("Should send second line");
terminal
.clear_and_expect("+")
.expect("Should show continuation prompt again");
terminal.send_line("}").expect("Should send closing brace");
terminal
.clear_and_expect("> ")
.expect("Should show normal prompt");
terminal.send_line("f(10)").expect("Should call function");
terminal
.clear_and_expect("[1] 11")
.expect("f(10) should return 11");
terminal.quit().expect("Should quit cleanly");
}
#[test]
#[cfg(unix)]
fn test_pty_multiline_string_input() {
let mut terminal =
Terminal::spawn_with_args(&["--no-auto-match"]).expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal
.send("\"test")
.expect("Should send opening quote and text");
terminal.send("\r").expect("Should send Enter");
terminal
.clear_and_expect("+")
.expect("Should show continuation prompt for incomplete string");
terminal
.send("end\"")
.expect("Should send closing text and quote");
terminal.send("\r").expect("Should send Enter");
terminal
.clear_and_expect("[1]")
.expect("Should see R output");
terminal.quit().expect("Should quit cleanly");
}
#[test]
#[cfg(unix)]
fn test_pty_error_handling() {
let mut terminal =
Terminal::spawn_with_args(&["--no-auto-match"]).expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal
.send_line("stop('Test error')")
.expect("Should send stop()");
terminal.expect("Error").expect("Should see error output");
terminal
.current_line()
.assert_contains("> ")
.expect("Should show prompt after error");
terminal.quit().expect("Should quit cleanly");
}
#[test]
#[cfg(unix)]
fn test_pty_history_exit_status() {
use reedline::{History, SearchDirection, SearchQuery, SqliteBackedHistory};
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let history_dir = temp_dir.path().to_string_lossy().to_string();
let mut terminal =
Terminal::spawn_with_args(&["--no-auto-match", "--history-dir", &history_dir])
.expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal.send_line("42").expect("Should send expression");
terminal.expect("[1] 42").expect("Should see result");
terminal
.send_line("stop('test error')")
.expect("Should send stop()");
terminal.expect("Error").expect("Should see error");
terminal.send_line("1").expect("Should send expression");
terminal.expect("[1] 1").expect("Should see result");
terminal.quit().expect("Should quit cleanly");
std::thread::sleep(std::time::Duration::from_millis(500));
let history_path = temp_dir.path().join("r.db");
assert!(
history_path.exists(),
"History database should exist at {:?}",
history_path
);
let history =
SqliteBackedHistory::with_file(history_path, None, None).expect("Failed to open history");
let entries = history
.search(SearchQuery::everything(SearchDirection::Forward, None))
.expect("Failed to search history");
assert!(
entries.len() >= 3,
"Should have at least 3 history entries, got {}",
entries.len()
);
let error_entry = entries
.iter()
.find(|e| e.command_line.contains("stop("))
.expect("Should find stop() command in history");
assert_eq!(
error_entry.exit_status,
Some(1),
"Failed command should have exit_status = 1"
);
let success_entry = entries
.iter()
.find(|e| e.command_line == "42")
.expect("Should find '42' command in history");
assert_eq!(
success_entry.exit_status,
Some(0),
"Successful command should have exit_status = 0"
);
}
#[test]
#[cfg(unix)]
fn test_pty_rlang_error_detection() {
if !common::has_dplyr() {
eprintln!("Skipping test: dplyr not available");
return;
}
use reedline::{History, SearchDirection, SearchQuery, SqliteBackedHistory};
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let history_dir = temp_dir.path().to_string_lossy().to_string();
let mut terminal =
Terminal::spawn_with_args(&["--no-auto-match", "--history-dir", &history_dir])
.expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal.send_line("42").expect("Should send expression");
terminal.expect("[1] 42").expect("Should see result");
terminal
.send_line("mtcars |> dplyr::select(nonexistent_column)")
.expect("Should send dplyr command");
terminal
.expect("doesn't exist")
.expect("Should see dplyr error message");
terminal
.wait_for_prompt()
.expect("Should show prompt after error");
terminal.send_line("1").expect("Should send expression");
terminal.expect("[1] 1").expect("Should see result");
terminal.quit().expect("Should quit cleanly");
std::thread::sleep(std::time::Duration::from_millis(500));
let history_path = temp_dir.path().join("r.db");
assert!(
history_path.exists(),
"History database should exist at {:?}",
history_path
);
let history =
SqliteBackedHistory::with_file(history_path, None, None).expect("Failed to open history");
let entries = history
.search(SearchQuery::everything(SearchDirection::Forward, None))
.expect("Failed to search history");
let error_entry = entries
.iter()
.find(|e| e.command_line.contains("dplyr::select"))
.expect("Should find dplyr command in history");
assert_eq!(
error_entry.exit_status,
Some(1),
"dplyr error should have exit_status = 1, indicating error was detected. \
This tests that rlang/cli errors that output to stdout are correctly caught \
via globalCallingHandlers."
);
let success_entry = entries
.iter()
.find(|e| e.command_line == "42")
.expect("Should find '42' command in history");
assert_eq!(
success_entry.exit_status,
Some(0),
"Successful command should have exit_status = 0"
);
}
#[test]
#[cfg(unix)]
fn test_pty_history_sponge_forget() {
use reedline::{History, SearchDirection, SearchQuery, SqliteBackedHistory};
use std::io::Write;
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let history_dir = temp_dir.path().to_string_lossy().to_string();
let config_path = temp_dir.path().join("arf.toml");
let mut config_file =
std::fs::File::create(&config_path).expect("Failed to create config file");
writeln!(
config_file,
r#"
[experimental.history_forget]
enabled = true
delay = 1
on_exit_only = false
[experimental.prompt_spinner]
frames = ""
"#
)
.expect("Failed to write config");
let config_path_str = config_path.to_string_lossy().to_string();
let mut terminal = Terminal::spawn_with_args(&[
"--no-auto-match",
"--config",
&config_path_str,
"--history-dir",
&history_dir,
])
.expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal.send_line("1").expect("Should send expression");
terminal.expect("[1] 1").expect("Should see result");
terminal
.send_line("stop('error1')")
.expect("Should send first error");
terminal.expect("error1").expect("Should see error1");
terminal
.send_line("stop('error2')")
.expect("Should send second error");
terminal.expect("error2").expect("Should see error2");
terminal
.send_line("stop('error3')")
.expect("Should send third error");
terminal.expect("error3").expect("Should see error3");
terminal.quit().expect("Should quit cleanly");
std::thread::sleep(std::time::Duration::from_millis(500));
let history_path = temp_dir.path().join("r.db");
assert!(
history_path.exists(),
"History database should exist at {:?}",
history_path
);
let history =
SqliteBackedHistory::with_file(history_path, None, None).expect("Failed to open history");
let entries = history
.search(SearchQuery::everything(SearchDirection::Forward, None))
.expect("Failed to search history");
let error_commands: Vec<_> = entries
.iter()
.filter(|e| e.command_line.contains("stop("))
.collect();
assert!(
error_commands.len() <= 1,
"At most 1 failed command (the most recent) should remain. Found: {:?}",
error_commands
.iter()
.map(|e| &e.command_line)
.collect::<Vec<_>>()
);
if !error_commands.is_empty() {
assert!(
error_commands[0].command_line.contains("error3"),
"The remaining error should be error3, got: {}",
error_commands[0].command_line
);
}
let success_commands: Vec<_> = entries.iter().filter(|e| e.command_line == "1").collect();
assert_eq!(
success_commands.len(),
1,
"Successful command should be present"
);
}
#[test]
#[cfg(unix)]
fn test_pty_width_synced_at_startup() {
let mut terminal =
Terminal::spawn_with_size(&["--no-auto-match"], 24, 120).expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal
.send_line("getOption('width')")
.expect("Should send getOption");
terminal
.clear_and_expect("[1] 120")
.expect("R width should match terminal columns (120)");
terminal.quit().expect("Should quit cleanly");
}
#[test]
#[cfg(unix)]
fn test_pty_width_not_synced_when_disabled() {
use std::io::Write;
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("arf.toml");
let mut config_file =
std::fs::File::create(&config_path).expect("Failed to create config file");
writeln!(
config_file,
r#"
[r]
auto_width = false
"#
)
.expect("Failed to write config");
let config_path_str = config_path.to_string_lossy().to_string();
let mut terminal =
Terminal::spawn_with_args(&["--no-auto-match", "--config", &config_path_str])
.expect("Failed to spawn arf");
terminal.wait_for_prompt().expect("Should show prompt");
terminal
.send_line("options(width = 42)")
.expect("Should set custom width");
terminal
.clear_and_expect("> ")
.expect("Should return to prompt");
std::thread::sleep(std::time::Duration::from_millis(200));
terminal
.send_line("getOption('width')")
.expect("Should send getOption");
terminal
.clear_and_expect("[1] 42")
.expect("Custom width should be preserved when auto_width is disabled");
terminal.quit().expect("Should quit cleanly");
}