#![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::pipeline::{preprocess_pack, PreprocessMode, 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};
#[test]
fn conflict_marker_in_template_source_blocks_expansion() {
use std::collections::HashMap;
let template_with_conflict = format!(
"name = Alice\n{}\nhost = \"{{{{ env.DB_HOST }}}}\"\n{}\nhost = \"prod\"\n{}\nport = 5432\n",
crate::preprocessing::conflict::MARKER_START,
crate::preprocessing::conflict::MARKER_MID,
crate::preprocessing::conflict::MARKER_END,
);
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", &template_with_conflict)
.done()
.build();
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 err = preprocess_pack(
entries,
®istry,
&pack,
env.fs.as_ref(),
&datastore,
env.paths.as_ref(),
PreprocessMode::Active,
false,
)
.unwrap_err();
match err {
DodotError::UnresolvedConflictMarker {
source_file,
line_numbers,
} => {
assert!(source_file.ends_with("config.toml.tmpl"));
assert_eq!(line_numbers.len(), 3, "got: {line_numbers:?}");
}
other => panic!("expected UnresolvedConflictMarker, got: {other}"),
}
let datastore_path = env
.paths
.data_dir()
.join("packs")
.join("app")
.join("preprocessed")
.join("config.toml");
assert!(
!env.fs.exists(&datastore_path),
"no rendered output should land in the datastore when the gate fires"
);
let baseline_path = env
.paths
.preprocessor_baseline_path("app", "preprocessed", "config.toml");
assert!(
!env.fs.exists(&baseline_path),
"no baseline should be written when the gate fires"
);
}
#[test]
fn conflict_marker_gate_skipped_for_preprocessors_without_reverse_merge() {
let env = TempEnvironment::builder()
.pack("app")
.file(
"data.scripted",
&format!(
"header\n{}\nbody\n",
crate::preprocessing::conflict::MARKER_START
),
)
.done()
.build();
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(ScriptedPreprocessor {
name: "bytes-only",
extension: ".scripted",
outputs: vec![crate::preprocessing::ExpandedFile {
relative_path: PathBuf::from("data"),
content: b"emitted".to_vec(),
is_dir: false,
..Default::default()
}],
supports_reverse_merge: false,
}));
let datastore = make_datastore(&env);
let pack = make_pack("app", env.dotfiles_root.join("app"));
let entries = vec![PackEntry {
relative_path: "data.scripted".into(),
absolute_path: env.dotfiles_root.join("app/data.scripted"),
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::Active,
false,
)
.expect("non-tracking preprocessor must not be gated by markers in its source");
assert_eq!(result.virtual_entries.len(), 1);
}
#[test]
fn conflict_marker_gate_runs_on_tracking_scripted_preprocessor() {
let env = TempEnvironment::builder()
.pack("app")
.file(
"config.toml.tracked",
&format!(
"ok\n{}\nbody\n{}\n",
crate::preprocessing::conflict::MARKER_START,
crate::preprocessing::conflict::MARKER_END
),
)
.done()
.build();
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(ScriptedPreprocessor {
name: "tracking-bytes",
extension: ".tracked",
outputs: vec![crate::preprocessing::ExpandedFile {
relative_path: PathBuf::from("config.toml"),
content: b"x".to_vec(),
is_dir: false,
tracked_render: Some("x".into()),
context_hash: Some([0; 32]),
secret_line_ranges: Vec::new(),
deploy_mode: None,
}],
supports_reverse_merge: true,
}));
let datastore = make_datastore(&env);
let pack = make_pack("app", env.dotfiles_root.join("app"));
let entries = vec![PackEntry {
relative_path: "config.toml.tracked".into(),
absolute_path: env.dotfiles_root.join("app/config.toml.tracked"),
is_dir: false,
gate_failure: None,
}];
let err = preprocess_pack(
entries,
®istry,
&pack,
env.fs.as_ref(),
&datastore,
env.paths.as_ref(),
crate::preprocessing::PreprocessMode::Active,
false,
)
.unwrap_err();
assert!(
matches!(err, DodotError::UnresolvedConflictMarker { .. }),
"expected UnresolvedConflictMarker, got: {err}"
);
}
#[test]
fn gate_handles_non_utf8_source_via_lossy_decode() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tracked", "placeholder")
.done()
.build();
let bytes: Vec<u8> = vec![
b'h', b'e', b'l', b'l', b'o', b'\n', 0xff, 0xfe, b'\n', b'w', b'o', b'r', b'l', b'd', b'\n',
];
env.fs
.write_file(&env.dotfiles_root.join("app/config.toml.tracked"), &bytes)
.unwrap();
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(ScriptedPreprocessor {
name: "tracking-bytes",
extension: ".tracked",
outputs: vec![crate::preprocessing::ExpandedFile {
relative_path: PathBuf::from("config.toml"),
content: b"x".to_vec(),
is_dir: false,
tracked_render: Some("x".into()),
context_hash: Some([0; 32]),
secret_line_ranges: Vec::new(),
deploy_mode: None,
}],
supports_reverse_merge: true,
}));
let datastore = make_datastore(&env);
let pack = make_pack("app", env.dotfiles_root.join("app"));
let entries = vec![PackEntry {
relative_path: "config.toml.tracked".into(),
absolute_path: env.dotfiles_root.join("app/config.toml.tracked"),
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::Active,
false,
)
.expect("non-UTF-8 source without markers must not crash the gate");
assert_eq!(result.virtual_entries.len(), 1);
}
#[test]
fn gate_detects_markers_in_non_utf8_source() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tracked", "placeholder")
.done()
.build();
let mut bytes: Vec<u8> = Vec::new();
bytes.extend_from_slice(b"prefix\n");
bytes.push(0xff);
bytes.push(0xfe);
bytes.push(b'\n');
bytes.extend_from_slice(crate::preprocessing::conflict::MARKER_START.as_bytes());
bytes.push(b'\n');
bytes.extend_from_slice(b"body\n");
env.fs
.write_file(&env.dotfiles_root.join("app/config.toml.tracked"), &bytes)
.unwrap();
let mut registry = PreprocessorRegistry::new();
registry.register(Box::new(ScriptedPreprocessor {
name: "tracking-bytes",
extension: ".tracked",
outputs: vec![crate::preprocessing::ExpandedFile {
relative_path: PathBuf::from("config.toml"),
content: b"x".to_vec(),
is_dir: false,
tracked_render: Some("x".into()),
context_hash: Some([0; 32]),
secret_line_ranges: Vec::new(),
deploy_mode: None,
}],
supports_reverse_merge: true,
}));
let datastore = make_datastore(&env);
let pack = make_pack("app", env.dotfiles_root.join("app"));
let entries = vec![PackEntry {
relative_path: "config.toml.tracked".into(),
absolute_path: env.dotfiles_root.join("app/config.toml.tracked"),
is_dir: false,
gate_failure: None,
}];
let err = preprocess_pack(
entries,
®istry,
&pack,
env.fs.as_ref(),
&datastore,
env.paths.as_ref(),
crate::preprocessing::PreprocessMode::Active,
false,
)
.unwrap_err();
assert!(
matches!(err, DodotError::UnresolvedConflictMarker { .. }),
"expected UnresolvedConflictMarker even on non-UTF-8 source, got: {err}"
);
}
#[test]
fn template_renders_normally_after_markers_are_resolved() {
use std::collections::HashMap;
let env = TempEnvironment::builder()
.pack("app")
.file("greet.tmpl", "hello {{ name }}")
.done()
.build();
let mut vars = HashMap::new();
vars.insert("name".into(), "Alice".into());
let template_pp = crate::preprocessing::template::TemplatePreprocessor::new(
vec!["tmpl".into()],
vars,
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: "greet.tmpl".into(),
absolute_path: env.dotfiles_root.join("app/greet.tmpl"),
is_dir: false,
gate_failure: None,
}];
let result = preprocess_pack(
entries.clone(),
®istry,
&pack,
env.fs.as_ref(),
&datastore,
env.paths.as_ref(),
PreprocessMode::Active,
false,
)
.expect("clean source should expand successfully");
assert_eq!(result.virtual_entries.len(), 1);
let dirty = format!(
"hello\n{}\n{{{{ name }}}}\n{}\n",
crate::preprocessing::conflict::MARKER_START,
crate::preprocessing::conflict::MARKER_END,
);
env.fs
.write_file(&env.dotfiles_root.join("app/greet.tmpl"), dirty.as_bytes())
.unwrap();
let err = preprocess_pack(
entries.clone(),
®istry,
&pack,
env.fs.as_ref(),
&datastore,
env.paths.as_ref(),
PreprocessMode::Active,
false,
)
.unwrap_err();
assert!(matches!(err, DodotError::UnresolvedConflictMarker { .. }));
env.fs
.write_file(
&env.dotfiles_root.join("app/greet.tmpl"),
b"hello {{ name }}",
)
.unwrap();
let result = preprocess_pack(
entries,
®istry,
&pack,
env.fs.as_ref(),
&datastore,
env.paths.as_ref(),
PreprocessMode::Active,
false,
)
.expect("resolved source should expand again");
assert_eq!(result.virtual_entries.len(), 1);
}