use std::io::Write;
use std::process::{Command, Stdio};
use tempfile::NamedTempFile;
#[test]
fn test_version_flag() {
let output = sanitized_arf_command()
.arg("--version")
.output()
.expect("Failed to run arf");
assert!(output.status.success(), "arf --version should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("arf") || stdout.contains("0.1.0"),
"Version output should contain version info: {}",
stdout
);
}
#[test]
fn test_help_flag() {
let output = sanitized_arf_command()
.arg("--help")
.output()
.expect("Failed to run arf");
assert!(output.status.success(), "arf --help should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("--reprex") && stdout.contains("--no-banner") && stdout.contains("--eval"),
"Help should show CLI options: {}",
stdout
);
}
#[test]
fn test_completions_subcommand() {
let output = sanitized_arf_command()
.args(["completions", "bash"])
.output()
.expect("Failed to run arf completions");
assert!(
output.status.success(),
"arf completions bash should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("complete") || stdout.contains("arf"),
"Completion output should contain bash completion code: {}",
stdout
);
}
#[test]
fn test_history_schema_subcommand() {
let output = sanitized_arf_command()
.args(["history", "schema"])
.output()
.expect("Failed to run arf history schema");
assert!(output.status.success(), "arf history schema should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("# History Database"),
"Should contain title: {}",
stdout
);
assert!(
stdout.contains("## Location"),
"Should contain location section: {}",
stdout
);
assert!(
stdout.contains("## SQLite Schema"),
"Should contain schema section: {}",
stdout
);
assert!(
stdout.contains("## Indexes"),
"Should contain indexes section: {}",
stdout
);
assert!(
stdout.contains("## Analyze or Export"),
"Should contain export section: {}",
stdout
);
assert!(
stdout.contains("CREATE TABLE history"),
"Should contain CREATE TABLE: {}",
stdout
);
assert!(
stdout.contains("command_line"),
"Should contain command_line column: {}",
stdout
);
assert!(
stdout.contains("library(DBI)"),
"Should contain R DBI example: {}",
stdout
);
assert!(
stdout.contains("dbConnect"),
"Should contain dbConnect: {}",
stdout
);
}
#[test]
fn test_history_schema_piped_no_colors() {
let output = sanitized_arf_command()
.args(["history", "schema"])
.output()
.expect("Failed to run arf history schema");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("\x1b["),
"Piped output should not contain ANSI escape codes: {:?}",
&stdout[..stdout.len().min(200)]
);
}
#[test]
fn test_history_import_rejects_self_import_r_db() {
use reedline::SqliteBackedHistory;
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let history_dir = temp_dir.path();
let r_db_path = history_dir.join("r.db");
let _db = SqliteBackedHistory::with_file(r_db_path.clone(), None, None)
.expect("Failed to create r.db");
drop(_db);
let output = sanitized_arf_command()
.args([
"--history-dir",
history_dir.to_str().unwrap(),
"history",
"import",
"--from",
"arf",
"--file",
r_db_path.to_str().unwrap(),
])
.output()
.expect("Failed to run arf history import");
assert!(
!output.status.success(),
"Self-import should fail, but succeeded"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Refusing to import") && stderr.contains("into itself"),
"Error should mention refusing self-import, got: {}",
stderr
);
}
#[test]
fn test_history_import_rejects_self_import_shell_db() {
use reedline::SqliteBackedHistory;
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let history_dir = temp_dir.path();
let shell_db_path = history_dir.join("shell.db");
let _db = SqliteBackedHistory::with_file(shell_db_path.clone(), None, None)
.expect("Failed to create shell.db");
drop(_db);
let output = sanitized_arf_command()
.args([
"--history-dir",
history_dir.to_str().unwrap(),
"history",
"import",
"--from",
"arf",
"--file",
shell_db_path.to_str().unwrap(),
])
.output()
.expect("Failed to run arf history import");
assert!(
!output.status.success(),
"Self-import of shell.db should fail, but succeeded"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Refusing to import") && stderr.contains("into itself"),
"Error should mention refusing self-import for shell.db, got: {}",
stderr
);
}
#[test]
fn test_history_import_arf_requires_file() {
let output = sanitized_arf_command()
.args(["history", "import", "--from", "arf"])
.output()
.expect("Failed to run arf history import");
assert!(
!output.status.success(),
"Import from arf without --file should fail"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--file") && stderr.contains("required"),
"Error should mention --file is required, got: {}",
stderr
);
}
fn sanitized_arf_command() -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_arf"));
for variable in ["ARF_R_HOME", "ARF_R_VERSION", "ARF_HISTORY_DIR"] {
command.env_remove(variable);
}
command
}
fn assert_top_level_scope_error(args: &[&str], expected: &[&str]) {
let output = sanitized_arf_command()
.args(args)
.output()
.expect("Failed to run arf");
assert_eq!(
output.status.code(),
Some(2),
"scope errors should use clap's exit code: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
for text in expected {
assert!(
stderr.contains(text),
"scope error should contain {text:?}: {stderr}"
);
}
}
#[test]
fn test_r_source_flag_before_headless_rejected_with_corrected_form() {
assert_top_level_scope_error(
&["--r-home", "/tmp", "headless"],
&[
"--r-home",
"before the 'headless' subcommand",
"arf headless --r-home <R_HOME>",
],
);
}
#[test]
fn test_no_banner_before_completions_rejected() {
assert_top_level_scope_error(
&["--no-banner", "completions", "bash"],
&[
"--no-banner",
"not used by the 'completions' subcommand",
"arf --no-banner",
],
);
}
#[test]
fn test_top_level_config_before_history_schema_rejected() {
assert_top_level_scope_error(
&["--config", "x", "history", "schema"],
&[
"--config",
"not used by the 'history schema' subcommand",
"arf --config",
],
);
}
#[test]
fn test_top_level_history_dir_before_history_schema_rejected() {
assert_top_level_scope_error(
&["--history-dir", "/tmp", "history", "schema"],
&[
"--history-dir",
"not used by the 'history schema' subcommand",
"arf --history-dir",
],
);
}
#[test]
fn test_top_level_config_before_config_check_rejected_with_nested_corrected_form() {
let output = sanitized_arf_command()
.args(["--config", "x", "config", "check"])
.output()
.expect("Failed to run arf config check");
assert_eq!(output.status.code(), Some(2));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Usage: arf config check")
&& stderr.contains("before the 'config check' subcommand"),
"{stderr}"
);
assert!(
stderr.contains("arf config check --config <CONFIG>"),
"{stderr}"
);
assert!(!stderr.contains("arf --config"), "{stderr}");
}
#[test]
fn test_config_check_reports_all_removed_reprex_keys() {
let mut config_file = NamedTempFile::new().expect("Failed to create temp config file");
write!(
config_file,
r##"[startup.mode]
reprex = true
[mode.reprex]
comment = "#> "
[reprex]
enabled = true
autoformat = true
[prompt.indicators]
autoformat = true
"##
)
.expect("Failed to write config file");
let output = sanitized_arf_command()
.args([
"config",
"check",
"--config",
config_file.path().to_str().unwrap(),
])
.output()
.expect("Failed to run arf config check");
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
insta::assert_snapshot!(stderr, @r###"
Error: Config file has errors:
[startup.mode] was removed; use [startup] reprex = "off"|"on"|"format"
[mode.reprex] was removed; use [reprex]
reprex.enabled was removed; use startup.reprex
reprex.autoformat was removed; use startup.reprex
prompt.indicators.autoformat was removed; use prompt.indicators.reprex_format
"###);
}
#[test]
fn test_top_level_no_banner_before_nested_ipc_rejected() {
assert_top_level_scope_error(
&["--no-banner", "ipc", "list"],
&[
"--no-banner",
"Usage: arf ipc list",
"not used by the 'ipc list' subcommand",
"arf --no-banner",
],
);
}
#[test]
fn test_vanilla_before_headless_rejected() {
assert_top_level_scope_error(
&["--vanilla", "headless"],
&[
"--vanilla",
"before the 'headless' subcommand",
"arf headless --vanilla",
],
);
}
#[test]
fn test_quiet_before_headless_rejected() {
assert_top_level_scope_error(
&["--quiet", "headless"],
&[
"--quiet",
"before the 'headless' subcommand",
"arf headless --quiet",
],
);
}
#[test]
fn test_ipc_bind_before_headless_rejected() {
assert_top_level_scope_error(
&["--ipc-bind", "/tmp/x.sock", "headless"],
&[
"--ipc-bind",
"before the 'headless' subcommand",
"arf headless --ipc-bind <BIND>",
],
);
}
#[test]
fn test_top_level_config_is_consumed_by_history_export() {
use reedline::SqliteBackedHistory;
use tempfile::TempDir;
let history_dir = TempDir::new().expect("Failed to create history directory");
let r_db = history_dir.path().join("r.db");
drop(
SqliteBackedHistory::with_file(r_db, None, None)
.expect("Failed to create history database"),
);
let export_file = history_dir.path().join("export.db");
let output = sanitized_arf_command()
.env("ARF_HISTORY_DIR", history_dir.path())
.args([
"--config",
"x",
"history",
"export",
"--file",
export_file.to_str().unwrap(),
])
.output()
.expect("Failed to run arf history export");
assert!(
output.status.success(),
"top-level --config should be accepted by history: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_top_level_history_dir_is_consumed_by_history_import() {
use reedline::SqliteBackedHistory;
use tempfile::TempDir;
let temp = TempDir::new().expect("Failed to create temporary directory");
let source_file = temp.path().join("r.db");
drop(
SqliteBackedHistory::with_file(source_file.clone(), None, None)
.expect("Failed to create source history database"),
);
let target_dir = temp.path().join("target");
let output = sanitized_arf_command()
.args([
"--history-dir",
target_dir.to_str().unwrap(),
"history",
"import",
"--from",
"arf",
"--file",
source_file.to_str().unwrap(),
"--dry-run",
])
.output()
.expect("Failed to run arf history import");
assert!(
output.status.success(),
"top-level --history-dir should be accepted by history: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_vanilla_without_subcommand_still_works() {
let output = sanitized_arf_command()
.args(["--vanilla", "-e", "1 + 1"])
.output()
.expect("Failed to run arf");
assert!(
output.status.success(),
"top-level --vanilla without a subcommand should work: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_eval_basic() {
let output = sanitized_arf_command()
.args(["-e", "1 + 1"])
.output()
.expect("Failed to run arf -e");
assert!(output.status.success(), "arf -e should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("[1] 2"), "Should output [1] 2: {}", stdout);
}
#[test]
#[cfg(unix)]
fn test_eval_with_mismatched_r_home() {
let output = sanitized_arf_command()
.env("R_HOME", r"/tmp/arf-test-nonexistent-r-4.0.5")
.args(["-e", r#"1 + 1"#])
.output()
.expect("Failed to run arf -e with mismatched R_HOME");
assert!(
output.status.success(),
"arf -e should succeed with a mismatched R_HOME: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("[1] 2"),
"R should evaluate an expression with a mismatched R_HOME: {}",
stdout
);
}
#[test]
fn test_eval_multiple_expressions() {
let output = sanitized_arf_command()
.args(["-e", "x <- 5\nx * 2"])
.output()
.expect("Failed to run arf -e");
assert!(output.status.success(), "arf -e should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("[1] 10"),
"Should output [1] 10: {}",
stdout
);
}
#[test]
fn test_eval_function() {
let output = sanitized_arf_command()
.args(["-e", "f <- function(x) { x + 1 }\nf(10)"])
.output()
.expect("Failed to run arf -e");
assert!(output.status.success(), "arf -e should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("[1] 11"),
"Function should return 11: {}",
stdout
);
}
#[test]
fn test_eval_error_handling() {
let output = sanitized_arf_command()
.args(["-e", "stop('Test error')"])
.output()
.expect("Failed to run arf -e");
assert!(
output.status.success(),
"arf -e should succeed even with R errors"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Test error") || stderr.contains("Error"),
"Should show error message: {}",
stderr
);
}
#[test]
fn test_eval_pipe_error() {
let output = sanitized_arf_command()
.args(["-e", "1 |> 1"])
.output()
.expect("Failed to run arf -e");
assert!(
output.status.success(),
"arf -e should succeed even with R errors"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Error") || stderr.contains("error"),
"Should show pipe error: {}",
stderr
);
}
#[test]
fn test_eval_reprex_mode() {
let output = sanitized_arf_command()
.args(["--reprex", "on", "-e", "1 + 1"])
.output()
.expect("Failed to run arf --reprex -e");
assert!(output.status.success(), "arf --reprex -e should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("1 + 1"),
"Output should echo source code: {}",
stdout
);
assert!(
stdout.contains("#> [1] 2"),
"Output should be prefixed with #>: {}",
stdout
);
}
#[test]
fn test_eval_reprex_custom_comment() {
let mut config_file = NamedTempFile::new().expect("Failed to create temp config file");
writeln!(
config_file,
r###"[reprex]
comment = "## "
"###
)
.expect("Failed to write config file");
let output = sanitized_arf_command()
.args([
"--config",
config_file.path().to_str().unwrap(),
"--reprex",
"on",
"-e",
"1 + 1",
])
.output()
.expect("Failed to run arf --reprex with config file");
assert!(
output.status.success(),
"arf with custom reprex comment should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("1 + 1"),
"Output should echo source code: {}",
stdout
);
assert!(
stdout.contains("## [1] 2"),
"Output should be prefixed with custom comment: {}",
stdout
);
}
#[test]
fn test_eval_reprex_cat_output() {
let output = sanitized_arf_command()
.args(["--reprex", "on", "-e", r#"cat("hello")"#])
.output()
.expect("Failed to run arf --reprex -e cat()");
assert!(
output.status.success(),
"arf --reprex -e cat() should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains(r#"cat("hello")"#),
"Output should echo source code: {}",
stdout
);
assert!(
stdout.contains("#> hello"),
"cat() output should be prefixed with #>: {}",
stdout
);
}
#[test]
fn test_eval_reprex_cat_with_newline() {
let output = sanitized_arf_command()
.args(["--reprex", "on", "-e", r#"cat("hello\n")"#])
.output()
.expect("Failed to run arf --reprex -e cat() with newline");
assert!(
output.status.success(),
"arf --reprex -e cat() with newline should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("#> hello"),
"cat() output with newline should be prefixed with #>: {}",
stdout
);
}
#[test]
fn test_script_file() {
let mut file = NamedTempFile::new().expect("Failed to create temp file");
writeln!(file, "x <- 5").expect("Failed to write");
writeln!(file, "y <- 10").expect("Failed to write");
writeln!(file, "x + y").expect("Failed to write");
let output = sanitized_arf_command()
.arg("-f")
.arg(file.path())
.output()
.expect("Failed to run arf with script file");
assert!(output.status.success(), "arf -f script.R should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("[1] 15"),
"Script should output [1] 15: {}",
stdout
);
}
#[test]
fn test_script_file_function() {
let mut file = NamedTempFile::new().expect("Failed to create temp file");
writeln!(file, "f <- function(x) {{").expect("Failed to write");
writeln!(file, " x + 1").expect("Failed to write");
writeln!(file, "}}").expect("Failed to write");
writeln!(file, "f(10)").expect("Failed to write");
let output = sanitized_arf_command()
.arg("-f")
.arg(file.path())
.output()
.expect("Failed to run arf with script file");
assert!(output.status.success(), "arf -f script.R should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("[1] 11"),
"Function should return 11: {}",
stdout
);
}
#[test]
fn test_script_file_reprex() {
let mut file = NamedTempFile::new().expect("Failed to create temp file");
writeln!(file, "1 + 1").expect("Failed to write");
let output = sanitized_arf_command()
.args(["--reprex", "on"])
.arg("-f")
.arg(file.path())
.output()
.expect("Failed to run arf --reprex -f script.R");
assert!(
output.status.success(),
"arf --reprex -f script.R should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("1 + 1"),
"Output should echo source code: {}",
stdout
);
assert!(
stdout.contains("#> [1] 2"),
"Output should be prefixed with #>: {}",
stdout
);
}
#[test]
fn test_script_file_not_found() {
let output = sanitized_arf_command()
.arg("-f")
.arg("/nonexistent/path/to/script.R")
.output()
.expect("Failed to run arf");
assert!(
!output.status.success(),
"arf should fail for non-existent file"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Failed to read") || stderr.contains("No such file"),
"Should show file not found error: {}",
stderr
);
}
#[test]
fn test_script_file_stdin() {
let mut child = sanitized_arf_command()
.args(["-f", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn arf");
child
.stdin
.take()
.unwrap()
.write_all(b"x <- 7\ny <- 8\nx + y\n")
.expect("Failed to write to stdin");
let output = child.wait_with_output().expect("Failed to wait for arf");
assert!(
output.status.success(),
"arf -f - should succeed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("[1] 15"),
"Script via stdin should output [1] 15: {}",
stdout
);
}
#[test]
fn test_script_file_stdin_reprex() {
let mut child = sanitized_arf_command()
.args(["--reprex", "on", "-f", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn arf");
child
.stdin
.take()
.unwrap()
.write_all(b"1 + 1\n")
.expect("Failed to write to stdin");
let output = child.wait_with_output().expect("Failed to wait for arf");
assert!(
output.status.success(),
"arf --reprex -f - should succeed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("1 + 1"),
"Reprex via stdin should echo source code: {}",
stdout
);
assert!(
stdout.contains("#> [1] 2"),
"Reprex via stdin should prefix output with #>: {}",
stdout
);
}
#[test]
fn test_ipc_eval_stdin_fallback() {
let sessions_dir = tempfile::tempdir().expect("Failed to create temp sessions dir");
let output = sanitized_arf_command()
.args(["ipc", "eval"])
.env("ARF_IPC_SESSIONS_DIR", sessions_dir.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("Failed to spawn arf");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stderr.contains("NO_CODE_PROVIDED"),
"Should not complain about missing code when stdin is provided: {}",
stderr
);
assert!(
stdout.contains("SESSION_NOT_FOUND") || stderr.contains("SESSION_NOT_FOUND"),
"Should fail at IPC level with SESSION_NOT_FOUND: stdout={} stderr={}",
stdout,
stderr
);
}
#[test]
fn test_ipc_send_stdin_fallback() {
let sessions_dir = tempfile::tempdir().expect("Failed to create temp sessions dir");
let output = sanitized_arf_command()
.args(["ipc", "send"])
.env("ARF_IPC_SESSIONS_DIR", sessions_dir.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("Failed to spawn arf");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stderr.contains("NO_CODE_PROVIDED"),
"Should not complain about missing code when stdin is provided: {}",
stderr
);
assert!(
stdout.contains("SESSION_NOT_FOUND") || stderr.contains("SESSION_NOT_FOUND"),
"Should fail at IPC level with SESSION_NOT_FOUND: stdout={} stderr={}",
stdout,
stderr
);
}
#[test]
fn test_eval_sources_user_rprofile() {
let mut rprofile = NamedTempFile::new().expect("Failed to create temp .Rprofile");
writeln!(rprofile, "cat('ARF_RPROFILE_MARKER\\n')").expect("Failed to write .Rprofile");
let rprofile_path = rprofile.path().to_path_buf();
let output = sanitized_arf_command()
.env("R_PROFILE_USER", &rprofile_path)
.args(["-e", "1"])
.output()
.expect("Failed to run arf -e");
assert!(output.status.success(), "arf -e should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.contains("ARF_RPROFILE_MARKER") || stderr.contains("ARF_RPROFILE_MARKER"),
".Rprofile should be sourced by arf -e. stdout={stdout}, stderr={stderr}"
);
}
#[cfg(unix)]
#[test]
fn test_eval_empty_r_profile_user_sources_default_rprofile() {
let working_dir = tempfile::tempdir().expect("Failed to create working directory");
let rprofile_path = working_dir.path().join(".Rprofile");
std::fs::write(&rprofile_path, "cat('ARF_RPROFILE_MARKER\\n')\n")
.expect("Failed to write .Rprofile");
let output = sanitized_arf_command()
.current_dir(working_dir.path())
.env("R_PROFILE_USER", "")
.args(["-e", "1"])
.output()
.expect("Failed to run arf -e");
assert!(output.status.success(), "arf -e should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.contains("ARF_RPROFILE_MARKER") || stderr.contains("ARF_RPROFILE_MARKER"),
"default .Rprofile should be sourced when R_PROFILE_USER is empty. \
stdout={stdout}, stderr={stderr}"
);
}
#[test]
fn test_eval_vanilla_skips_user_rprofile() {
let mut rprofile = NamedTempFile::new().expect("Failed to create temp .Rprofile");
writeln!(rprofile, "cat('ARF_RPROFILE_MARKER\\n')").expect("Failed to write .Rprofile");
let rprofile_path = rprofile.path().to_path_buf();
let output = sanitized_arf_command()
.env("R_PROFILE_USER", &rprofile_path)
.args(["--vanilla", "-e", "1"])
.output()
.expect("Failed to run arf --vanilla -e");
assert!(output.status.success(), "arf --vanilla -e should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stdout.contains("ARF_RPROFILE_MARKER") && !stderr.contains("ARF_RPROFILE_MARKER"),
".Rprofile must not be sourced under --vanilla. stdout={stdout}, stderr={stderr}"
);
}
#[test]
fn test_positional_script_rejected() {
let output = sanitized_arf_command()
.arg("some_script.R")
.output()
.expect("Failed to run arf");
assert!(
!output.status.success(),
"positional script arg should be rejected"
);
assert_eq!(
output.status.code(),
Some(2),
"positional script rejection should use clap's parse error exit code"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("some_script.R"),
"Error should mention the offending token: {}",
stderr
);
}
#[test]
fn test_eval_with_subcommand_rejected() {
let output = sanitized_arf_command()
.args(["-e", "1+1", "completions", "bash"])
.output()
.expect("Failed to run arf");
assert!(
!output.status.success(),
"--eval with subcommand should be rejected"
);
assert_eq!(
output.status.code(),
Some(2),
"--eval with subcommand should exit with code 2"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--eval")
&& stderr.contains("cannot be used")
&& stderr.contains("completions"),
"Should show conflict error mentioning --eval and the subcommand: {}",
stderr
);
}
#[test]
fn test_file_with_subcommand_rejected() {
let output = sanitized_arf_command()
.args(["-f", "some.R", "completions", "bash"])
.output()
.expect("Failed to run arf");
assert!(
!output.status.success(),
"--file with subcommand should be rejected"
);
assert_eq!(
output.status.code(),
Some(2),
"--file with subcommand should exit with code 2"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--file")
&& stderr.contains("cannot be used")
&& stderr.contains("completions"),
"Should show conflict error mentioning --file and the subcommand: {}",
stderr
);
}
#[test]
fn test_r_completion_functions() {
let output = sanitized_arf_command()
.args([
"-e",
r#"
utils:::.assignLinebuffer("pri")
utils:::.assignEnd(3)
token <- utils:::.guessTokenFromLine()
print(token)
"#,
])
.output()
.expect("Failed to run arf -e");
assert!(output.status.success(), "arf -e should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("pri"), "Token should be 'pri': {}", stdout);
}
#[test]
fn test_r_complete_token() {
let output = sanitized_arf_command()
.args([
"-e",
r#"
utils:::.assignLinebuffer("prin")
utils:::.assignEnd(4L)
utils:::.guessTokenFromLine()
utils:::.completeToken()
comps <- utils:::.retrieveCompletions()
print(comps)
"#,
])
.output()
.expect("Failed to run arf -e");
assert!(output.status.success(), "arf -e should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("print"),
"Completions should include 'print': {}",
stdout
);
}