cockup 0.2.1

Yet another backup tool for various configurations
use super::*;

#[derive(serde::Deserialize)]
struct Ingredient {
    rules: Vec<IngredientRule>,
}

#[derive(serde::Deserialize)]
struct IngredientRule {
    #[serde(rename = "from")]
    source: String,
    targets: Vec<String>,
    to: String,
}

#[test]
fn every_library_ingredient_round_trips_through_the_cli() {
    let library = Path::new(env!("CARGO_MANIFEST_DIR")).join("ingredients/library");
    let mut ingredients: Vec<_> = fs::read_dir(&library)
        .unwrap()
        .map(|entry| entry.unwrap().path())
        .filter(|path| {
            path.extension()
                .is_some_and(|extension| extension == "yaml")
        })
        .collect();
    ingredients.sort();
    assert!(!ingredients.is_empty(), "ingredient library is empty");

    for ingredient in ingredients {
        let name = ingredient.file_stem().unwrap().to_str().unwrap();
        let yaml = fs::read_to_string(&ingredient).unwrap();
        let rules: Ingredient = serde_yaml_ng::from_str(&yaml).unwrap();
        assert!(!rules.rules.is_empty(), "{name} has no rules");

        let dir = TempDir::new().unwrap();
        let home = dir.path();
        let mut expected = Vec::new();
        for rule in &rules.rules {
            let parent = rule
                .source
                .strip_prefix("~/")
                .or_else(|| (rule.source == "~").then_some(""))
                .expect("ingredient sources must be home-relative");
            assert!(
                !rule.targets.is_empty(),
                "{name} has a rule without targets"
            );
            for target in &rule.targets {
                assert!(
                    !target.contains(['*', '?', '[']),
                    "{name}: add a fixture strategy for glob target {target}"
                );
                let source = home.join(parent).join(target);
                let backup = home.join("backup").join(name).join(&rule.to).join(target);
                let contents = format!("{name}:{}", source.display());
                fs::create_dir_all(source.parent().unwrap()).unwrap();
                fs::write(&source, &contents).unwrap();
                expected.push((source, backup, contents));
            }
        }
        write(
            home,
            "config.yaml",
            &format!(
                "symlinks: reference\ndestination: backup\nrules: []\ninclude: [{{file: {}, wrap: {name}}}]\n",
                ingredient.display()
            ),
        );
        let run = |action| {
            Command::new(env!("CARGO_BIN_EXE_cockup"))
                .current_dir(home)
                .env("HOME", home)
                .env("NO_COLOR", "1")
                .args([action, "config.yaml", "-a"])
                .output()
                .unwrap()
        };
        let backup = run("backup");
        assert!(backup.status.success(), "{name}: {}", text(&backup));
        for (source, backup, contents) in &expected {
            assert_eq!(fs::read_to_string(backup).unwrap(), *contents, "{name}");
            fs::remove_file(source).unwrap();
        }
        let restore = run("restore");
        assert!(restore.status.success(), "{name}: {}", text(&restore));
        for (source, _, contents) in &expected {
            assert_eq!(fs::read_to_string(source).unwrap(), *contents, "{name}");
        }
    }
}