arf-console 0.7.0-beta.1

A cross-platform R console written in Rust
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

#[test]
fn environment_providers_follow_configured_order_and_short_circuit() {
    for (providers, selected) in [
        (
            vec![RSourceOverride::RigProject, RSourceOverride::Pixi],
            "rig-project",
        ),
        (
            vec![RSourceOverride::Pixi, RSourceOverride::RigProject],
            "pixi",
        ),
    ] {
        let result = setup_r_via_overrides_with_environment_providers(
            &providers,
            None,
            || Ok(()),
            || Ok(Vec::new()),
            |_, _| panic!("environment providers do not resolve version files"),
            |_| {
                if selected == "pixi" {
                    Ok(pixi_target())
                } else {
                    Err(PixiFailure::NotActive {
                        workspace_manifest: None,
                    })
                }
            },
            |_| {
                if selected == "rig-project" {
                    Ok(rig_project_target())
                } else {
                    Err(RigProjectFailure::ProjectNotFound {
                        path: "/workspace/rproj.toml".into(),
                    })
                }
            },
        )
        .unwrap();
        match (selected, expect_environment(result).0) {
            ("pixi", RSourceStatus::Pixi { info }) => assert_eq!(info.environment, "analysis"),
            ("rig-project", RSourceStatus::RigProject { info }) => {
                assert_eq!(info.installation_name, "release");
            }
            (_, _) => panic!("provider order selected the wrong environment"),
        }
    }
}

#[test]
fn rig_failure_is_cached_and_does_not_block_later_pixi() {
    for unavailable in [true, false] {
        let temp = tempfile::tempdir().unwrap();
        std::fs::write(temp.path().join("first.r-version"), "4.4.2").unwrap();
        std::fs::write(temp.path().join("second.r-version"), "4.4.2").unwrap();
        let availability_calls = AtomicUsize::new(0);
        let list_calls = AtomicUsize::new(0);
        let result = setup_r_via_overrides_with_pixi(
            &[
                RSourceOverride::VersionFile {
                    file: "first.r-version".into(),
                },
                RSourceOverride::VersionFile {
                    file: "second.r-version".into(),
                },
                RSourceOverride::Pixi,
            ],
            Some(temp.path()),
            || {
                availability_calls.fetch_add(1, Ordering::SeqCst);
                if unavailable {
                    Err(external::rig::RigError::NotInstalled)
                } else {
                    Ok(())
                }
            },
            || {
                list_calls.fetch_add(1, Ordering::SeqCst);
                Err(external::rig::RigError::CommandFailed("list failed".into()))
            },
            |_, _| panic!("must not resolve rig after failure"),
            |_| Ok(pixi_target()),
        )
        .unwrap();
        let (status, diagnostics) = expect_environment(result);
        assert!(
            matches!(status, RSourceStatus::Pixi { ref info } if info.environment == "analysis")
        );
        assert_eq!(availability_calls.load(Ordering::SeqCst), 1);
        assert_eq!(list_calls.load(Ordering::SeqCst), usize::from(!unavailable));
        assert_eq!(diagnostics.len(), 2);
        assert!(
            diagnostics
                .iter()
                .all(|diagnostic| !diagnostic.message.to_lowercase().contains("falling back"))
        );
    }
}

#[test]
fn successful_provider_short_circuits_and_rejected_pixi_continues() {
    let temp = tempfile::tempdir().unwrap();
    std::fs::write(temp.path().join(".r-version"), "4.4.2").unwrap();
    let file = RSourceOverride::VersionFile {
        file: ".r-version".into(),
    };
    let version = rig_version("4.4.2");
    let resolve = |_: &semver::Version, _: &[external::rig::RigVersion]| {
        Ok(ResolvedRSource {
            status: RSourceStatus::Rig {
                version: "4.4.2".into(),
                override_info: None,
            },
            r_home: Some(temp.path().to_path_buf()),
        })
    };
    let result = setup_r_via_overrides_with_pixi(
        &[file.clone(), RSourceOverride::Pixi],
        Some(temp.path()),
        || Ok(()),
        || Ok(vec![version.clone()]),
        resolve,
        |_| panic!("successful file provider must skip Pixi"),
    );
    assert!(matches!(result, Some(OverrideResolution::Applied { .. })));
    let failures = [
        PixiFailure::EnvironmentInvalid {
            reason: "invalid marker",
            path: Some("/prefix/conda-meta/pixi".into()),
        },
        PixiFailure::RUnavailable {
            reason: "R probe timed out",
            path: Some("/prefix/bin/R".into()),
        },
    ];
    let mut summaries = Vec::new();
    for failure in failures {
        let result = setup_r_via_overrides_with_pixi(
            &[RSourceOverride::Pixi, file.clone()],
            Some(temp.path()),
            || Ok(()),
            || Ok(vec![version.clone()]),
            resolve,
            |_| Err(failure.clone()),
        );
        let Some(OverrideResolution::Applied { diagnostics, .. }) = result else {
            panic!("expected file provider")
        };
        assert_eq!(diagnostics.len(), 1);
        summaries.push(serde_json::json!({
            "code": diagnostics[0].code,
            "message": diagnostics[0].message,
            "path": diagnostics[0].path,
        }));
    }
    insta::assert_snapshot!(serde_json::to_string_pretty(&summaries).unwrap(), @r###"
    [
      {
        "code": "r_source_override.pixi_environment_invalid",
        "message": "Warning: The active Pixi environment is invalid: invalid marker; trying the next R source override.",
        "path": null
      },
      {
        "code": "r_source_override.pixi_r_unavailable",
        "message": "Warning: R from the active Pixi environment is unavailable: R probe timed out; trying the next R source override.",
        "path": null
      }
    ]
    "###);
    let result = setup_r_via_overrides_with_pixi(
        &[RSourceOverride::Pixi],
        Some(temp.path()),
        || panic!("must not query rig"),
        || panic!("must not query rig"),
        resolve,
        |_| Ok(pixi_target()),
    );
    assert!(matches!(
        result,
        Some(OverrideResolution::EnvironmentApplied { .. })
    ));
}

#[test]
fn pixi_only_override_resolution_does_not_need_a_base_dir() {
    let result = setup_r_via_overrides_with_pixi(
        &[RSourceOverride::Pixi],
        None,
        || panic!("must not query rig"),
        || panic!("must not query rig"),
        |_, _| panic!("must not query rig"),
        |cwd| {
            assert!(cwd.is_none());
            Err(PixiFailure::NotActive {
                workspace_manifest: None,
            })
        },
    );
    assert!(result.is_none());
}

#[test]
fn command_failed_rig_warning_preserves_reason_without_install_guidance() {
    let temp = tempfile::tempdir().unwrap();
    std::fs::write(temp.path().join("project.r-version"), "4.4.2\n").unwrap();

    let result = setup_r_via_overrides_with(
        &[RSourceOverride::VersionFile {
            file: "project.r-version".into(),
        }],
        Some(temp.path()),
        || {
            Err(external::rig::RigError::CommandFailed(
                "permission denied".to_string(),
            ))
        },
        || panic!("rig versions should not be listed after availability fails"),
        |_, _| panic!("R version should not be resolved after availability fails"),
    )
    .expect("an existing override should produce a fallback");

    let OverrideResolution::Fallback { diagnostics } = result else {
        panic!("a failed rig command should fall back to startup.r_source");
    };
    let message = diagnostic_text(&diagnostics);
    assert!(message.contains("permission denied"));
    assert!(!message.contains("Install rig"));
}

#[test]
fn not_installed_override_falls_through_to_installed_provider() {
    let temp = tempfile::tempdir().unwrap();
    std::fs::write(temp.path().join("first.r-version"), "4.3.0\n").unwrap();
    std::fs::write(temp.path().join("second.r-version"), "4.4.2\n").unwrap();

    let result = setup_with_rig_4_4_2(
        &[
            RSourceOverride::VersionFile {
                file: "first.r-version".into(),
            },
            RSourceOverride::VersionFile {
                file: "second.r-version".into(),
            },
        ],
        temp.path(),
    )
    .unwrap();

    let OverrideResolution::Applied {
        status,
        info,
        diagnostics,
        ..
    } = result
    else {
        panic!("the installed provider should be applied");
    };
    assert!(matches!(*status, RSourceStatus::Rig { .. }));
    assert_eq!(info.file, Path::new("second.r-version"));
    assert_eq!(info.resolved_version, "4.4.2");
    assert_eq!(diagnostics.len(), 1);
    assert_eq!(
        diagnostics[0].code,
        "r_source_override.version_not_installed"
    );
    assert!(diagnostics[0].message.contains("4.3.0"));
    assert!(
        diagnostics[0]
            .message
            .contains("Trying the next R source override")
    );
    assert!(
        !diagnostics[0]
            .message
            .contains("Falling back to startup.r_source")
    );
}

#[test]
fn rig_data_is_fetched_once_across_override_providers() {
    let temp = tempfile::tempdir().unwrap();
    std::fs::write(temp.path().join("first.r-version"), "4.3.0\n").unwrap();
    std::fs::write(temp.path().join("second.r-version"), "4.4.2\n").unwrap();

    let rig_available_calls = Arc::new(AtomicUsize::new(0));
    let list_versions_calls = Arc::new(AtomicUsize::new(0));
    let rig_available_calls_for_closure = Arc::clone(&rig_available_calls);
    let list_versions_calls_for_closure = Arc::clone(&list_versions_calls);

    let result = setup_r_via_overrides_with(
        &[
            RSourceOverride::VersionFile {
                file: "first.r-version".into(),
            },
            RSourceOverride::VersionFile {
                file: "second.r-version".into(),
            },
        ],
        Some(temp.path()),
        move || {
            rig_available_calls_for_closure.fetch_add(1, Ordering::Relaxed);
            Ok(())
        },
        move || {
            list_versions_calls_for_closure.fetch_add(1, Ordering::Relaxed);
            Ok(vec![rig_version("4.4.2")])
        },
        resolve_installed_4_4_2,
    )
    .unwrap();

    assert!(matches!(result, OverrideResolution::Applied { .. }));
    assert_eq!(rig_available_calls.load(Ordering::Relaxed), 1);
    assert_eq!(list_versions_calls.load(Ordering::Relaxed), 1);
}

#[test]
fn missing_override_files_do_not_fetch_rig_data() {
    let temp = tempfile::tempdir().unwrap();
    let rig_available_calls = Arc::new(AtomicUsize::new(0));
    let list_versions_calls = Arc::new(AtomicUsize::new(0));
    let rig_available_calls_for_closure = Arc::clone(&rig_available_calls);
    let list_versions_calls_for_closure = Arc::clone(&list_versions_calls);

    let result = setup_r_via_overrides_with(
        &[
            RSourceOverride::VersionFile {
                file: "missing-first.r-version".into(),
            },
            RSourceOverride::VersionFile {
                file: "missing-second.r-version".into(),
            },
        ],
        Some(temp.path()),
        move || {
            rig_available_calls_for_closure.fetch_add(1, Ordering::Relaxed);
            Ok(())
        },
        move || {
            list_versions_calls_for_closure.fetch_add(1, Ordering::Relaxed);
            Ok(Vec::new())
        },
        |_, _| panic!("R version resolution should not be attempted"),
    );

    assert!(result.is_none());
    assert_eq!(rig_available_calls.load(Ordering::Relaxed), 0);
    assert_eq!(list_versions_calls.load(Ordering::Relaxed), 0);
}