#![allow(unused_imports)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::datastore::FilesystemDataStore;
use crate::fs::Fs;
use crate::handlers::HandlerConfig;
use crate::packs::Pack;
use crate::paths::Pather;
use crate::preprocessing::divergence::DivergenceState;
use crate::preprocessing::pipeline::{
preprocess_pack, PreprocessMode, PreprocessResult, PreprocessorRegistry,
};
use crate::preprocessing::{ExpandedFile, Preprocessor, TransformType};
use crate::rules::PackEntry;
use crate::testing::TempEnvironment;
use crate::{DodotError, Result};
use super::{make_datastore, make_pack, make_registry, ScriptedPreprocessor};
fn run_template_preprocess(
env: &TempEnvironment,
pack_name: &str,
force: bool,
) -> PreprocessResult {
use std::collections::HashMap;
let template_pp = crate::preprocessing::template::TemplatePreprocessor::new(
vec!["tmpl".into()],
HashMap::new(),
env.paths.as_ref(),
)
.unwrap();
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(template_pp));
let datastore = make_datastore(env);
let pack = make_pack(pack_name, env.dotfiles_root.join(pack_name));
let entries = vec![PackEntry {
relative_path: "config.toml.tmpl".into(),
absolute_path: env.dotfiles_root.join(pack_name).join("config.toml.tmpl"),
is_dir: false,
gate_failure: None,
}];
preprocess_pack(
entries,
®istry,
&pack,
env.fs.as_ref(),
&datastore,
env.paths.as_ref(),
PreprocessMode::Active,
force,
)
.unwrap()
}
#[test]
fn divergence_guard_skips_when_deployed_was_edited() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", "name = original")
.done()
.build();
let first = run_template_preprocess(&env, "app", false);
assert!(first.skipped.is_empty(), "first deploy must not skip");
let deployed_path = &first.virtual_entries[0].absolute_path.clone();
env.fs
.write_file(deployed_path, b"name = USER EDITED")
.unwrap();
let second = run_template_preprocess(&env, "app", false);
assert_eq!(second.skipped.len(), 1, "deployed-edit must skip");
let skip = &second.skipped[0];
assert_eq!(skip.state, DivergenceState::OutputChanged);
assert_eq!(skip.pack, "app");
assert_eq!(skip.virtual_relative, std::path::Path::new("config.toml"));
let on_disk = env.fs.read_to_string(deployed_path).unwrap();
assert_eq!(on_disk, "name = USER EDITED");
assert_eq!(second.virtual_entries.len(), 1);
assert_eq!(&second.virtual_entries[0].absolute_path, deployed_path);
}
#[test]
fn divergence_guard_skips_when_both_changed() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", "name = original")
.done()
.build();
let first = run_template_preprocess(&env, "app", false);
let deployed_path = first.virtual_entries[0].absolute_path.clone();
env.fs
.write_file(
&env.dotfiles_root.join("app/config.toml.tmpl"),
b"name = SOURCE EDITED",
)
.unwrap();
env.fs
.write_file(&deployed_path, b"name = USER EDITED")
.unwrap();
let second = run_template_preprocess(&env, "app", false);
assert_eq!(second.skipped.len(), 1);
assert_eq!(second.skipped[0].state, DivergenceState::BothChanged);
let on_disk = env.fs.read_to_string(&deployed_path).unwrap();
assert_eq!(on_disk, "name = USER EDITED");
}
#[test]
fn divergence_guard_proceeds_when_source_changed_only() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", "name = original")
.done()
.build();
let first = run_template_preprocess(&env, "app", false);
let deployed_path = first.virtual_entries[0].absolute_path.clone();
env.fs
.write_file(
&env.dotfiles_root.join("app/config.toml.tmpl"),
b"name = NEW VALUE",
)
.unwrap();
let second = run_template_preprocess(&env, "app", false);
assert!(
second.skipped.is_empty(),
"source-only change must not trigger the guard"
);
let on_disk = env.fs.read_to_string(&deployed_path).unwrap();
assert_eq!(on_disk, "name = NEW VALUE");
}
#[test]
fn divergence_guard_no_op_when_nothing_changed() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", "name = original")
.done()
.build();
let _ = run_template_preprocess(&env, "app", false);
let second = run_template_preprocess(&env, "app", false);
assert!(second.skipped.is_empty());
}
#[test]
fn divergence_guard_overridden_by_force() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", "name = original")
.done()
.build();
let first = run_template_preprocess(&env, "app", false);
let deployed_path = first.virtual_entries[0].absolute_path.clone();
env.fs
.write_file(&deployed_path, b"name = USER EDITED")
.unwrap();
let second = run_template_preprocess(&env, "app", true);
assert!(
second.skipped.is_empty(),
"force=true must bypass the guard"
);
let on_disk = env.fs.read_to_string(&deployed_path).unwrap();
assert_eq!(
on_disk, "name = original",
"force must rewrite to the rendered content"
);
}
#[test]
fn divergence_guard_baseline_stays_pinned_to_last_successful_render() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", "name = original")
.done()
.build();
let first = run_template_preprocess(&env, "app", false);
let deployed_path = first.virtual_entries[0].absolute_path.clone();
let baseline_before = crate::preprocessing::baseline::Baseline::load(
env.fs.as_ref(),
env.paths.as_ref(),
"app",
"preprocessed",
"config.toml",
)
.unwrap()
.unwrap();
env.fs
.write_file(&deployed_path, b"name = USER EDITED")
.unwrap();
let _ = run_template_preprocess(&env, "app", false);
let baseline_after = crate::preprocessing::baseline::Baseline::load(
env.fs.as_ref(),
env.paths.as_ref(),
"app",
"preprocessed",
"config.toml",
)
.unwrap()
.unwrap();
assert_eq!(
baseline_before.rendered_hash, baseline_after.rendered_hash,
"baseline must not be rewritten when the guard skips"
);
assert_eq!(
baseline_before.rendered_content, baseline_after.rendered_content,
"baseline content must not change after a skipped write"
);
}
#[test]
fn divergence_guard_reproceeds_when_user_undoes_their_edit() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", "name = original")
.done()
.build();
let first = run_template_preprocess(&env, "app", false);
let deployed_path = first.virtual_entries[0].absolute_path.clone();
env.fs
.write_file(&deployed_path, b"name = USER EDITED")
.unwrap();
let blocked = run_template_preprocess(&env, "app", false);
assert_eq!(blocked.skipped.len(), 1);
env.fs
.write_file(&deployed_path, b"name = original")
.unwrap();
let cleared = run_template_preprocess(&env, "app", false);
assert!(
cleared.skipped.is_empty(),
"guard must clear once divergence is gone"
);
}
#[test]
fn divergence_guard_active_for_read_only_callers() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", "name = original")
.done()
.build();
let _ = run_template_preprocess(&env, "app", false);
let baseline_before = crate::preprocessing::baseline::Baseline::load(
env.fs.as_ref(),
env.paths.as_ref(),
"app",
"preprocessed",
"config.toml",
)
.unwrap()
.unwrap();
let deployed_path = env
.paths
.handler_data_dir("app", "preprocessed")
.join("config.toml");
env.fs
.write_file(&deployed_path, b"name = USER EDITED")
.unwrap();
use std::collections::HashMap;
let template_pp = crate::preprocessing::template::TemplatePreprocessor::new(
vec!["tmpl".into()],
HashMap::new(),
env.paths.as_ref(),
)
.unwrap();
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(template_pp));
let datastore = make_datastore(&env);
let pack = make_pack("app", env.dotfiles_root.join("app"));
let entries = vec![PackEntry {
relative_path: "config.toml.tmpl".into(),
absolute_path: env.dotfiles_root.join("app/config.toml.tmpl"),
is_dir: false,
gate_failure: None,
}];
let result = preprocess_pack(
entries,
®istry,
&pack,
env.fs.as_ref(),
&datastore,
env.paths.as_ref(),
crate::preprocessing::PreprocessMode::Passive,
false,
)
.unwrap();
assert_eq!(
result.skipped.len(),
1,
"guard must fire for read-only callers too"
);
assert_eq!(
env.fs.read_to_string(&deployed_path).unwrap(),
"name = USER EDITED",
"user's deployed-file edit must be preserved"
);
let baseline_after = crate::preprocessing::baseline::Baseline::load(
env.fs.as_ref(),
env.paths.as_ref(),
"app",
"preprocessed",
"config.toml",
)
.unwrap()
.unwrap();
assert_eq!(baseline_before, baseline_after);
}