dotm-rs 2.0.0

Dotfile manager with composable roles, templates, and host-specific overrides
Documentation
use dotm::config::{DotmSettings, PackageConfig, RootConfig};
use dotm::resolver::resolve_packages;
use std::collections::HashMap;

fn make_root(packages: Vec<(&str, Vec<&str>, Vec<&str>)>) -> RootConfig {
    let mut pkg_map = HashMap::new();
    for (name, deps, suggests) in packages {
        pkg_map.insert(
            name.to_string(),
            PackageConfig {
                depends: deps.into_iter().map(String::from).collect(),
                suggests: suggests.into_iter().map(String::from).collect(),
                ..Default::default()
            },
        );
    }
    RootConfig {
        dotm: DotmSettings {
            target: "~".to_string(),
            packages_dir: "packages".to_string(),
            auto_prune: false,
        },
        packages: pkg_map,
    }
}

#[test]
fn resolve_single_package_no_deps() {
    let root = make_root(vec![("zsh", vec![], vec![])]);
    let result = resolve_packages(&root, &["zsh"]).unwrap();
    assert_eq!(result, vec!["zsh"]);
}

#[test]
fn resolve_package_with_dep() {
    let root = make_root(vec![
        ("kde", vec!["util"], vec![]),
        ("util", vec![], vec![]),
    ]);
    let result = resolve_packages(&root, &["kde"]).unwrap();
    assert_eq!(result, vec!["util", "kde"]);
}

#[test]
fn resolve_transitive_deps() {
    let root = make_root(vec![
        ("a", vec!["b"], vec![]),
        ("b", vec!["c"], vec![]),
        ("c", vec![], vec![]),
    ]);
    let result = resolve_packages(&root, &["a"]).unwrap();
    assert_eq!(result, vec!["c", "b", "a"]);
}

#[test]
fn resolve_deduplicates() {
    let root = make_root(vec![
        ("kde", vec!["util"], vec![]),
        ("dev", vec!["util"], vec![]),
        ("util", vec![], vec![]),
    ]);
    let result = resolve_packages(&root, &["kde", "dev"]).unwrap();
    assert!(result.iter().filter(|p| *p == "util").count() == 1);
    let util_pos = result.iter().position(|p| p == "util").unwrap();
    let kde_pos = result.iter().position(|p| p == "kde").unwrap();
    let dev_pos = result.iter().position(|p| p == "dev").unwrap();
    assert!(util_pos < kde_pos);
    assert!(util_pos < dev_pos);
}

#[test]
fn resolve_circular_dep_errors() {
    let root = make_root(vec![
        ("a", vec!["b"], vec![]),
        ("b", vec!["a"], vec![]),
    ]);
    let result = resolve_packages(&root, &["a"]);
    assert!(result.is_err());
    let err = result.unwrap_err().to_string();
    assert!(
        err.contains("circular"),
        "expected circular dep error, got: {err}"
    );
}

#[test]
fn resolve_unknown_package_errors() {
    let root = make_root(vec![("a", vec!["nonexistent"], vec![])]);
    let result = resolve_packages(&root, &["a"]);
    assert!(result.is_err());
}

#[test]
fn resolve_suggests_not_included() {
    let root = make_root(vec![
        ("kde", vec![], vec!["gaming"]),
        ("gaming", vec![], vec![]),
    ]);
    let result = resolve_packages(&root, &["kde"]).unwrap();
    assert_eq!(result, vec!["kde"]);
}