use super::directory::{dir_command_hint, meta_cd, meta_popd, meta_pushd};
use super::history::dedup_history_stores;
use super::*;
use crate::config::ReprexMode;
use crate::editor::prompt::PromptFormatter;
use crate::history::HistoryFailureDetail;
use crate::repl::reprex::ReprexRuntime;
fn create_test_prompt_config() -> PromptRuntimeConfig {
PromptRuntimeConfig::builder(PromptFormatter::default(), "r> ", "+ ", "[bash] $ ").build()
}
fn default_r_source_status() -> RSourceStatus {
RSourceStatus::Path
}
fn call_meta(
input: &str,
config: &mut PromptRuntimeConfig,
_r_history_path: &Option<PathBuf>,
_shell_history_path: &Option<PathBuf>,
status: &RSourceStatus,
) -> Option<MetaCommandResult> {
let mut reprex =
ReprexRuntime::new(ReprexMode::Off, "#> ", crate::config::FormatterBackend::Air);
call_meta_with_runtime(input, config, &mut reprex, status)
}
fn call_meta_with_runtime(
input: &str,
config: &mut PromptRuntimeConfig,
reprex: &mut ReprexRuntime,
status: &RSourceStatus,
) -> Option<MetaCommandResult> {
let mut dir_stack = Vec::new();
call_meta_with_dir_stack(input, config, reprex, status, &mut dir_stack)
}
fn call_meta_with_dir_stack(
input: &str,
config: &mut PromptRuntimeConfig,
reprex: &mut ReprexRuntime,
status: &RSourceStatus,
dir_stack: &mut Vec<PathBuf>,
) -> Option<MetaCommandResult> {
let unavailable_history = HistoryRuntime::Unavailable {
failure: HistoryFailureDetail::test_memory(),
previous_failure: None,
};
let mut ctx = MetaCommandContext {
prompt_config: config,
reprex,
r_history: &unavailable_history,
shell_history: &unavailable_history,
r_source_status: status,
dir_stack,
history_session_id: None,
r_home: None,
};
process_meta_command(input, &mut ctx)
}
#[test]
fn test_process_meta_command_not_meta() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
let result = call_meta("print(x)", &mut config, &None, &None, &status);
assert!(result.is_none());
}
#[test]
fn test_process_meta_command_reprex_explicit_mode() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let mut reprex =
ReprexRuntime::new(ReprexMode::Off, "#> ", crate::config::FormatterBackend::Air);
let status = default_r_source_status();
assert!(!reprex.is_enabled());
let result = call_meta_with_runtime(":reprex on", &mut config, &mut reprex, &status);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert!(reprex.is_enabled());
let result = call_meta_with_runtime(":reprex off", &mut config, &mut reprex, &status);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert!(!reprex.is_enabled());
let result = call_meta_with_runtime(":reprex", &mut config, &mut reprex, &status);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert!(!reprex.is_enabled());
reprex.set_mode(ReprexMode::Format);
let result = call_meta_with_runtime(":reprex format", &mut config, &mut reprex, &status);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert_eq!(reprex.mode, ReprexMode::Format);
let result = call_meta_with_runtime(":reprex on extra", &mut config, &mut reprex, &status);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert!(reprex.is_enabled());
}
#[test]
fn test_process_meta_command_dispatch() {
type Expect = fn(&Option<MetaCommandResult>) -> bool;
let cases: [(&str, Expect); 9] = [
(":commands", |r| {
matches!(r, Some(MetaCommandResult::Handled))
}),
(":cmds", |r| matches!(r, Some(MetaCommandResult::Handled))),
(":info", |r| {
matches!(r, Some(MetaCommandResult::ShowSessionInfo))
}),
(":session", |r| {
matches!(r, Some(MetaCommandResult::ShowSessionInfo))
}),
(":quit", |r| matches!(r, Some(MetaCommandResult::Exit))),
(":exit", |r| matches!(r, Some(MetaCommandResult::Exit))),
(":unknown", |r| {
matches!(r, Some(MetaCommandResult::Unknown(_)))
}),
(":", |r| matches!(r, Some(MetaCommandResult::Handled))),
(" :reprex ", |r| {
matches!(r, Some(MetaCommandResult::Handled))
}),
];
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
for (input, expected) in cases {
let result = call_meta(input, &mut config, &None, &None, &status);
assert!(expected(&result), "unexpected result for {input:?}");
}
}
#[test]
fn test_process_meta_command_shell_enter() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
assert!(!config.is_shell_enabled());
let result = call_meta(":shell", &mut config, &None, &None, &status);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert!(config.is_shell_enabled());
}
#[test]
fn test_process_meta_command_shell_exit_with_r() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
config.set_shell(true);
assert!(config.is_shell_enabled());
let result = call_meta(":r", &mut config, &None, &None, &status);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert!(!config.is_shell_enabled());
}
#[test]
fn test_process_meta_command_shell_exit_with_uppercase_r() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
config.set_shell(true);
assert!(config.is_shell_enabled());
let result = call_meta(":R", &mut config, &None, &None, &status);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert!(!config.is_shell_enabled());
}
#[test]
fn test_process_meta_command_r_when_not_in_shell() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
assert!(!config.is_shell_enabled());
let result = call_meta(":r", &mut config, &None, &None, &status);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert!(!config.is_shell_enabled()); }
#[test]
fn test_process_meta_command_system() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
let result = call_meta(":system echo hello", &mut config, &None, &None, &status);
assert!(matches!(result, Some(MetaCommandResult::ShellExecuted)));
}
#[test]
fn test_process_meta_command_system_empty() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
let result = call_meta(":system", &mut config, &None, &None, &status);
assert!(matches!(result, Some(MetaCommandResult::ShellExecuted)));
}
#[test]
fn test_process_meta_command_switch_requires_rig() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status_path = RSourceStatus::Path;
let result = call_meta(":switch 4.4", &mut config, &None, &None, &status_path);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
let status_rig = RSourceStatus::Rig {
version: "4.4.0".to_string(),
override_info: None,
};
let result = call_meta(":switch", &mut config, &None, &None, &status_rig);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
}
#[test]
fn test_meta_cd_relative_path() {
let _guard = crate::test_utils::lock_cwd();
let tmp = tempfile::tempdir().unwrap();
let subdir = tmp.path().join("sub");
std::fs::create_dir(&subdir).unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
let result = meta_cd("sub");
assert!(result.is_ok());
assert!(result.unwrap().ends_with("sub"));
}
#[test]
fn test_meta_cd_absolute_path() {
let _guard = crate::test_utils::lock_cwd();
let tmp = tempfile::tempdir().unwrap();
let result = meta_cd(&tmp.path().to_string_lossy());
assert!(result.is_ok());
}
#[test]
fn test_meta_cd_tilde() {
let _guard = crate::test_utils::lock_env_and_cwd();
let result = meta_cd("~");
assert!(result.is_ok());
if let Some(home) = dirs::home_dir() {
assert_eq!(
result.unwrap().canonicalize().ok(),
home.canonicalize().ok()
);
}
}
#[test]
fn test_meta_cd_no_args() {
let _guard = crate::test_utils::lock_env_and_cwd();
let result = meta_cd("");
assert!(result.is_ok());
if let Some(home) = dirs::home_dir() {
assert_eq!(
result.unwrap().canonicalize().ok(),
home.canonicalize().ok()
);
}
}
#[test]
fn test_meta_cd_nonexistent() {
let result = meta_cd("/nonexistent_path_12345");
assert!(result.is_err());
}
#[test]
fn test_meta_pushd_popd() {
let _guard = crate::test_utils::lock_cwd();
let tmp = tempfile::tempdir().unwrap();
let mut dir_stack = Vec::new();
std::env::set_current_dir(tmp.path()).unwrap();
let subdir = tmp.path().join("sub");
std::fs::create_dir(&subdir).unwrap();
let result = meta_pushd(&mut dir_stack, "sub");
assert!(result.is_ok());
assert_eq!(dir_stack.len(), 1);
assert!(std::env::current_dir().unwrap().ends_with("sub"));
let result = meta_popd(&mut dir_stack);
assert!(result.is_ok());
assert!(dir_stack.is_empty());
assert_eq!(
std::env::current_dir().unwrap().canonicalize().ok(),
tmp.path().canonicalize().ok()
);
}
#[test]
fn test_meta_popd_empty_stack() {
let mut dir_stack: Vec<PathBuf> = Vec::new();
let result = meta_popd(&mut dir_stack);
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty"));
}
#[test]
fn test_meta_pushd_no_args_returns_error() {
let mut dir_stack: Vec<PathBuf> = Vec::new();
let result = meta_pushd(&mut dir_stack, "");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Usage"));
assert!(dir_stack.is_empty());
}
#[test]
fn test_meta_pushd_saves_previous() {
let _guard = crate::test_utils::lock_cwd();
let tmp = tempfile::tempdir().unwrap();
let subdir = tmp.path().join("sub");
std::fs::create_dir(&subdir).unwrap();
let mut dir_stack = Vec::new();
std::env::set_current_dir(tmp.path()).unwrap();
let before_pushd = std::env::current_dir().unwrap();
let _ = meta_pushd(&mut dir_stack, "sub");
assert_eq!(dir_stack.len(), 1);
assert_eq!(
dir_stack[0].canonicalize().ok(),
before_pushd.canonicalize().ok()
);
}
#[test]
fn test_process_meta_command_cd() {
let _guard = crate::test_utils::lock_env_and_cwd();
let tmp = tempfile::tempdir().unwrap();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
let mut dir_stack = Vec::new();
let result = call_meta_with_dir_stack(
&format!(":cd {}", tmp.path().display()),
&mut config,
&mut ReprexRuntime::new(ReprexMode::Off, "#> ", crate::config::FormatterBackend::Air),
&status,
&mut dir_stack,
);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
}
#[test]
fn test_process_meta_command_pushd_popd() {
let _guard = crate::test_utils::lock_env_and_cwd();
let tmp = tempfile::tempdir().unwrap();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
let mut dir_stack = Vec::new();
let result = call_meta_with_dir_stack(
&format!(":pushd {}", tmp.path().display()),
&mut config,
&mut ReprexRuntime::new(ReprexMode::Off, "#> ", crate::config::FormatterBackend::Air),
&status,
&mut dir_stack,
);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert_eq!(dir_stack.len(), 1);
let result = call_meta_with_dir_stack(
":popd",
&mut config,
&mut ReprexRuntime::new(ReprexMode::Off, "#> ", crate::config::FormatterBackend::Air),
&status,
&mut dir_stack,
);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
assert!(dir_stack.is_empty());
}
#[test]
fn test_dir_command_hint() {
for (input, expected) in [
("cd /tmp", Some(":cd")),
("pushd /tmp", Some(":pushd")),
("popd", Some(":popd")),
("ls -la", None),
("echo cd", None),
("", None),
] {
match (dir_command_hint(input), expected) {
(Some(hint), Some(command)) => {
assert!(hint.contains(command), "hint for {input:?}: {hint}")
}
(None, None) => {}
(hint, expected) => panic!("hint for {input:?}: {hint:?}, expected {expected:?}"),
}
}
}
#[test]
fn test_process_meta_command_restart_force() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
let result = call_meta(":restart!", &mut config, &None, &None, &status);
assert!(matches!(result, Some(MetaCommandResult::Restart(None))));
}
#[test]
fn test_process_meta_command_restart_force_with_whitespace() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
let result = call_meta(" :restart! ", &mut config, &None, &None, &status);
assert!(matches!(result, Some(MetaCommandResult::Restart(None))));
}
#[test]
fn test_process_meta_command_switch_force() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status_rig = RSourceStatus::Rig {
version: "4.4.0".to_string(),
override_info: None,
};
let result = call_meta(":switch! 4.4", &mut config, &None, &None, &status_rig);
assert!(matches!(result, Some(MetaCommandResult::Restart(Some(ref v))) if v == "4.4"));
}
#[test]
fn test_process_meta_command_switch_force_accepts_space_after_colon() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status_rig = RSourceStatus::Rig {
version: "4.4.0".to_string(),
override_info: None,
};
let result = call_meta(": switch! 4.4", &mut config, &None, &None, &status_rig);
assert!(matches!(
result,
Some(MetaCommandResult::Restart(Some(ref version))) if version == "4.4"
));
}
#[test]
fn test_process_meta_command_switch_force_accepts_spaced_version_range() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status_rig = RSourceStatus::Rig {
version: "4.4.0".to_string(),
override_info: None,
};
let result = call_meta(
":switch! >=4.3, <5.0",
&mut config,
&None,
&None,
&status_rig,
);
assert!(matches!(
result,
Some(MetaCommandResult::Restart(Some(ref version)))
if version == ">=4.3, <5.0"
));
}
#[test]
fn test_process_meta_command_switch_force_preserves_named_version() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status_rig = RSourceStatus::Rig {
version: "4.4.0".to_string(),
override_info: None,
};
let result = call_meta(":switch! release", &mut config, &None, &None, &status_rig);
assert!(matches!(
result,
Some(MetaCommandResult::Restart(Some(ref version))) if version == "release"
));
}
#[test]
fn test_process_meta_command_switch_force_no_version() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status_rig = RSourceStatus::Rig {
version: "4.4.0".to_string(),
override_info: None,
};
let result = call_meta(":switch!", &mut config, &None, &None, &status_rig);
assert!(matches!(result, Some(MetaCommandResult::Handled)));
}
#[test]
fn test_process_meta_command_unknown_with_bang() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_prompt_config();
let status = default_r_source_status();
let result = call_meta(":shell!", &mut config, &None, &None, &status);
assert!(matches!(result, Some(MetaCommandResult::Unknown(_))));
}
#[test]
fn history_clear_deduplicates_shared_store_owners() {
let dir = tempfile::tempdir().unwrap();
let store = HistoryStore::open(
dir.path().join("history.db"),
crate::history::artifact::HistoryKind::R,
None,
None,
)
.unwrap();
let unique = HistoryStore::open(
dir.path().join("other.db"),
crate::history::artifact::HistoryKind::R,
None,
None,
)
.unwrap();
let stores = dedup_history_stores(vec![("R", store.clone()), ("Shell", store), ("R", unique)]);
assert_eq!(stores.len(), 2);
assert_eq!(stores[0].0, "R");
}