arf-console 0.7.0-beta.1

A cross-platform R console written in Rust
use super::*;
use std::ffi::OsStr;

const TEST_STARTUP_ENV_CARRIER: &str = r#"{"version":1,"variables":{}}"#;

#[test]
fn command_failed_rig_switch_error_preserves_reason_without_install_guidance() {
    let error = validate_rig_for_switch_with(|| {
        Err(RigError::CommandFailed("permission denied".to_string()))
    })
    .expect_err("a failed rig command should reject switching");

    assert!(error.contains("permission denied"));
    assert!(!error.contains("Install rig"));
}

#[test]
fn build_restart_command_restores_a_startup_value() {
    let changes = env_changes_for_switch(Some(r"4.0"), |var| {
        (var == "R_LIBS_USER").then(|| OsString::from(r"/user/r/library"))
    });
    let cmd = build_restart_command(
        std::path::Path::new(r"arf"),
        &[],
        &changes,
        TEST_STARTUP_ENV_CARRIER,
    );

    let restored = cmd
        .get_envs()
        .find(|(key, _)| *key == OsStr::new(r"R_LIBS_USER"))
        .map(|(_, value)| value);
    assert_eq!(restored, Some(Some(OsStr::new(r"/user/r/library"))));
}

#[test]
fn build_restart_command_restores_r_libs_from_startup_snapshot() {
    let changes = env_changes_for_switch(Some(r"4.0"), |var| {
        (var == "R_LIBS").then(|| OsString::from(r"/user/r/libs"))
    });
    let cmd = build_restart_command(
        std::path::Path::new(r"arf"),
        &[],
        &changes,
        TEST_STARTUP_ENV_CARRIER,
    );

    let restored = cmd
        .get_envs()
        .find(|(key, _)| *key == OsStr::new(r"R_LIBS"))
        .map(|(_, value)| value);
    assert_eq!(restored, Some(Some(OsStr::new(r"/user/r/libs"))));
}

#[test]
fn build_restart_command_removes_r_libs_absent_from_snapshot() {
    let changes = env_changes_for_switch(Some(r"4.0"), |_| None);
    let cmd = build_restart_command(
        std::path::Path::new(r"arf"),
        &[],
        &changes,
        TEST_STARTUP_ENV_CARRIER,
    );

    assert!(
        cmd.get_envs()
            .any(|(key, value)| key == OsStr::new(r"R_LIBS") && value.is_none())
    );
}

#[test]
fn build_restart_command_always_removes_ld_library_path() {
    let changes = env_changes_for_switch(Some(r"4.0"), |var| {
        (var == "LD_LIBRARY_PATH").then(|| OsString::from(r"/old/r/lib"))
    });
    let cmd = build_restart_command(
        std::path::Path::new(r"arf"),
        &[],
        &changes,
        TEST_STARTUP_ENV_CARRIER,
    );

    assert!(
        cmd.get_envs()
            .any(|(key, value)| key == OsStr::new(r"LD_LIBRARY_PATH") && value.is_none())
    );
}

#[cfg(target_os = "macos")]
#[test]
fn build_restart_command_restores_dyld_library_path_from_startup_snapshot() {
    let changes = env_changes_for_switch(Some(r"4.0"), |var| {
        (var == "DYLD_LIBRARY_PATH").then(|| OsString::from(r"/user/lib"))
    });
    let cmd = build_restart_command(
        std::path::Path::new(r"arf"),
        &[],
        &changes,
        TEST_STARTUP_ENV_CARRIER,
    );

    let restored = cmd
        .get_envs()
        .find(|(key, _)| *key == OsStr::new(r"DYLD_LIBRARY_PATH"))
        .map(|(_, value)| value);
    assert_eq!(restored, Some(Some(OsStr::new(r"/user/lib"))));
}

#[cfg(target_os = "macos")]
#[test]
fn build_restart_command_removes_dyld_library_path_absent_from_snapshot() {
    let changes = env_changes_for_switch(Some(r"4.0"), |_| None);
    let cmd = build_restart_command(
        std::path::Path::new(r"arf"),
        &[],
        &changes,
        TEST_STARTUP_ENV_CARRIER,
    );

    assert!(
        cmd.get_envs()
            .any(|(key, value)| key == OsStr::new(r"DYLD_LIBRARY_PATH") && value.is_none())
    );
}

#[test]
fn build_restart_command_always_removes_r_home_from_snapshot() {
    let changes = env_changes_for_switch(Some(r"4.0"), |var| {
        (var == "R_HOME").then(|| OsString::from(r"/old/r"))
    });
    let cmd = build_restart_command(
        std::path::Path::new(r"arf"),
        &[],
        &changes,
        TEST_STARTUP_ENV_CARRIER,
    );

    assert!(
        cmd.get_envs()
            .any(|(key, value)| key == OsStr::new(r"R_HOME") && value.is_none())
    );
    assert!(!changes.restored.iter().any(|(key, _)| *key == "R_HOME"));
}

#[test]
fn build_restart_command_removes_rig_activation_markers() {
    let changes = env_changes_for_switch(Some(r"4.0"), |var| match var {
        "RVENV" => Some(OsString::from(r"/project/.rvenv")),
        "RVENV_UNSYNCED" => Some(OsString::from("1")),
        _ => None,
    });
    let cmd = build_restart_command(
        std::path::Path::new(r"arf"),
        &[],
        &changes,
        TEST_STARTUP_ENV_CARRIER,
    );

    for marker in ["RVENV", "RVENV_UNSYNCED"] {
        assert!(
            cmd.get_envs()
                .any(|(key, value)| key == OsStr::new(marker) && value.is_none())
        );
        assert!(!changes.restored.iter().any(|(key, _)| *key == marker));
    }
}

#[test]
fn version_none_registers_no_environment_changes() {
    let changes = env_changes_for_switch(None, |_| {
        panic!("restart without a version must not inspect the startup snapshot")
    });
    let cmd = build_restart_command(
        std::path::Path::new(r"arf"),
        &[],
        &changes,
        TEST_STARTUP_ENV_CARRIER,
    );

    assert!(changes.restored.is_empty());
    assert!(changes.removed.is_empty());
    assert!(
        cmd.get_envs()
            .all(|(key, _)| { key != OsStr::new("RVENV") && key != OsStr::new("RVENV_UNSYNCED") })
    );
    assert_eq!(
        cmd.get_envs()
            .find(|(key, _)| *key == OsStr::new(crate::STARTUP_ENV_CARRIER))
            .map(|(_, value)| value),
        Some(Some(OsStr::new(TEST_STARTUP_ENV_CARRIER)))
    );
}

#[test]
fn restart_only_removes_unchanged_arf_supplied_wrapper_paths() {
    let wrapper_values = [
        ("R_DOC_DIR", OsString::from(r"/old/R/doc")),
        ("R_SHARE_DIR", OsString::from(r"/old/R/share")),
        ("R_INCLUDE_DIR", OsString::from(r"/old/R/include")),
    ];
    let removed = arf_supplied_wrapper_paths_to_remove(
        wrapper_values,
        |name| (name == "R_DOC_DIR").then(|| OsString::from(r"/user/selected/doc")),
        |name| match name {
            "R_DOC_DIR" => Some(OsString::from(r"/old/R/doc")),
            "R_SHARE_DIR" => Some(OsString::from(r"/old/R/share")),
            "R_INCLUDE_DIR" => Some(OsString::from(r"/session/selected/include")),
            _ => None,
        },
    );

    assert_eq!(removed, ["R_SHARE_DIR"]);
}

#[test]
fn build_restart_command_sets_carrier_for_switch() {
    let changes = env_changes_for_switch(Some(r"4.0"), |_| None);
    let cmd = build_restart_command(
        std::path::Path::new(r"arf"),
        &[],
        &changes,
        TEST_STARTUP_ENV_CARRIER,
    );

    assert_eq!(
        cmd.get_envs()
            .find(|(key, _)| *key == OsStr::new(crate::STARTUP_ENV_CARRIER))
            .map(|(_, value)| value),
        Some(Some(OsStr::new(TEST_STARTUP_ENV_CARRIER)))
    );
}

#[test]
fn switching_replaces_explicit_r_sources_before_user_arguments() {
    let args = vec![
        OsString::from("--quiet"),
        OsString::from("--with-r-version"),
        OsString::from("4.3"),
        OsString::from("--r-provider=rig-project"),
        OsString::from("--r-provider"),
        OsString::from("pixi"),
        OsString::from("--with-r-version=4.4"),
        OsString::from("--"),
        OsString::from("--r-provider"),
        OsString::from("script-argument"),
    ];

    assert_eq!(
        args_with_r_version(args, "4.5"),
        vec![
            OsString::from("--quiet"),
            OsString::from("--with-r-version"),
            OsString::from("4.5"),
            OsString::from("--"),
            OsString::from("--r-provider"),
            OsString::from("script-argument"),
        ]
    );
}

#[cfg(unix)]
#[test]
fn switching_preserves_non_utf8_arguments_after_separator() {
    use std::os::unix::ffi::OsStringExt;

    let user_arg = OsString::from_vec(vec![b'R', b'-', 0xff]);
    let args = vec![OsString::from("--"), user_arg.clone()];

    let switched = args_with_r_version(args, "4.5");

    assert_eq!(
        switched,
        vec![
            OsString::from("--with-r-version"),
            OsString::from("4.5"),
            OsString::from("--"),
            user_arg,
        ]
    );
}