use std::path::Path;
use super::{dump, dump_json};
fn write(dir: &Path, name: &str, body: &str) {
let path = dir.join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("mkdir");
}
std::fs::write(path, body).expect("write");
}
fn manifest(root: &Path, body: &str) {
let dir = root.join(".mmz");
std::fs::create_dir_all(&dir).expect("mkdir .mmz");
std::fs::write(dir.join("config.yaml"), body).expect("write manifest");
}
#[test]
fn a_single_file_project_dumps_with_itself_as_the_only_source() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
write(base, "a.txt", "one");
manifest(
base,
"scopes:\n src: [\"*.txt\"]\ncommands:\n - name: sh\n inputs: [src]\n",
);
let text = dump(base).expect("dump");
assert!(
text.contains("sources:\n 1 .mmz/config.yaml\n"),
"the root manifest is the sole, first-numbered source: {text}"
);
assert!(
text.contains("src: # .mmz/config.yaml"),
"the scope is annotated with the root manifest: {text}"
);
assert!(
text.contains("sh: # .mmz/config.yaml"),
"the command is annotated with the root manifest: {text}"
);
let json: serde_json::Value =
serde_json::from_str(&dump_json(base).expect("json")).expect("valid json");
assert_eq!(
json.get("sources").and_then(serde_json::Value::as_array),
Some(&vec![serde_json::Value::String(
".mmz/config.yaml".to_owned()
)])
);
assert_eq!(
json.pointer("/scopes/0/source")
.and_then(serde_json::Value::as_str),
Some(".mmz/config.yaml")
);
assert_eq!(
json.pointer("/commands/0/source")
.and_then(serde_json::Value::as_str),
Some(".mmz/config.yaml")
);
}
fn composed_project(base: &Path) {
write(base, "a.txt", "one");
write(base, "a.rs", "one");
manifest(
base,
"imports: [conf.d/]\non_hit: \"notify {cache:command}\"\nscopes:\n src: [\"*.txt\"]\ncommands:\n - name: sh\n inputs: [src]\n",
);
write(
base,
".mmz/conf.d/10-rust.yaml",
"imports: [../nested.yaml]\nscopes:\n rust: [\"*.rs\"]\ncommands:\n - name: lint\n inputs: [rust]\n",
);
write(
base,
".mmz/nested.yaml",
"probes:\n fmt-recipe:\n run: echo hi\ncommands:\n - name: nested-cmd\n inputs: [fmt-recipe]\n",
);
}
#[test]
fn a_composed_project_attributes_every_entry_to_its_declaring_file_through_a_nested_import() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
composed_project(base);
let json: serde_json::Value =
serde_json::from_str(&dump_json(base).expect("json")).expect("valid json");
let source = |pointer: &str| {
json.pointer(pointer)
.and_then(serde_json::Value::as_str)
.unwrap_or_else(|| panic!("missing {pointer} in {json}"))
.to_owned()
};
assert_eq!(
source("/scopes/0/source"),
".mmz/conf.d/10-rust.yaml",
"rust"
);
assert_eq!(source("/scopes/1/source"), ".mmz/config.yaml", "src");
assert_eq!(source("/probes/0/source"), ".mmz/nested.yaml", "fmt-recipe");
assert_eq!(source("/commands/0/source"), ".mmz/config.yaml", "sh");
assert_eq!(
source("/commands/1/source"),
".mmz/conf.d/10-rust.yaml",
"lint"
);
assert_eq!(
source("/commands/2/source"),
".mmz/nested.yaml",
"nested-cmd"
);
let text = dump(base).expect("dump");
assert!(
text.contains("rust: # .mmz/conf.d/10-rust.yaml"),
"the human form annotates the same way: {text}"
);
assert!(
text.contains("nested-cmd: # .mmz/nested.yaml"),
"including the nested-only fragment: {text}"
);
}
#[test]
fn the_json_form_round_trips_and_carries_sources_in_load_order() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
composed_project(base);
let rendered = dump_json(base).expect("json");
let json: serde_json::Value = serde_json::from_str(&rendered).expect("valid json");
let sources: Vec<&str> = json
.get("sources")
.and_then(serde_json::Value::as_array)
.expect("sources array")
.iter()
.map(|value| value.as_str().expect("string source"))
.collect();
assert_eq!(
sources,
vec![
".mmz/config.yaml",
".mmz/conf.d/10-rust.yaml",
".mmz/nested.yaml",
],
"root first, then each import depth-first"
);
assert_eq!(
json.get("manifest").and_then(serde_json::Value::as_str),
Some(".mmz/config.yaml"),
"manifest names the same file sources[0] does"
);
let reparsed: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&json).expect("re-serialize"))
.expect("re-parse");
assert_eq!(json, reparsed);
}
#[test]
fn the_policy_section_reports_every_keys_effective_value_declared_and_defaulted() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
composed_project(base);
let json: serde_json::Value =
serde_json::from_str(&dump_json(base).expect("json")).expect("valid json");
let policy = json.get("policy").expect("policy object");
assert_eq!(
policy.get("source").and_then(serde_json::Value::as_str),
Some(".mmz/config.yaml"),
"policy can only ever come from the root manifest"
);
assert_eq!(
policy.get("gitignore").and_then(serde_json::Value::as_bool),
Some(true),
"defaulted gitignore shows its effective value"
);
assert_eq!(
policy.get("cache_dir").and_then(serde_json::Value::as_str),
Some(".mmz/cache"),
"defaulted cache_dir shows its effective value"
);
assert_eq!(
policy.get("strict").and_then(serde_json::Value::as_array),
Some(&vec![
serde_json::Value::String("no_match".to_owned()),
serde_json::Value::String("no_inputs".to_owned()),
]),
"defaulted strict enforces every case"
);
assert_eq!(
policy.get("on_hit").and_then(serde_json::Value::as_str),
Some("notify {cache:command}"),
"the declared on_hit overrides its (absent) default"
);
let text = dump(base).expect("dump");
assert!(
text.contains("policy: # .mmz/config.yaml"),
"the policy header names the root manifest once, not per key: {text}"
);
assert!(
text.contains("gitignore: true (default)"),
"a defaulted key is marked: {text}"
);
assert!(
text.contains("cache_dir: .mmz/cache (default)"),
"a defaulted key is marked: {text}"
);
assert!(
text.contains("strict: [no_match, no_inputs] (default)"),
"a defaulted key is marked: {text}"
);
assert!(
text.contains("on_hit: \"notify {cache:command}\"\n"),
"a declared key carries no (default) marker: {text}"
);
assert!(
!text.contains("on_hit: \"notify {cache:command}\" (default)"),
"declared on_hit is not marked defaulted: {text}"
);
}
#[test]
fn a_store_path_fragment_shows_its_absolute_path_and_an_in_root_one_shows_a_relative_path() {
let project = tempfile::tempdir().expect("project tempdir");
let base = project.path();
write(base, "a.txt", "one");
let store = tempfile::tempdir().expect("store tempdir");
let fragment = store.path().join("rules.yaml");
std::fs::write(
&fragment,
"scopes:\n ext: [\"*.txt\"]\ncommands:\n - name: ext-cmd\n inputs: [ext]\n",
)
.expect("write store fragment");
manifest(
base,
&format!(
"imports: [{}]\nscopes:\n src: [\"*.txt\"]\ncommands:\n - name: sh\n inputs: [src]\n",
fragment.display()
),
);
let expected_store_path = std::fs::canonicalize(&fragment)
.expect("canonicalize")
.display()
.to_string();
let json: serde_json::Value =
serde_json::from_str(&dump_json(base).expect("json")).expect("valid json");
assert_eq!(
json.pointer("/scopes/0/source")
.and_then(serde_json::Value::as_str),
Some(expected_store_path.as_str()),
"the store-path fragment's scope renders absolute"
);
assert_eq!(
json.pointer("/scopes/1/source")
.and_then(serde_json::Value::as_str),
Some(".mmz/config.yaml"),
"the in-root scope renders root-relative"
);
let sources: Vec<&str> = json
.get("sources")
.and_then(serde_json::Value::as_array)
.expect("sources array")
.iter()
.map(|value| value.as_str().expect("string source"))
.collect();
assert_eq!(
sources,
vec![".mmz/config.yaml", expected_store_path.as_str()]
);
}
#[test]
fn an_invalid_manifest_errors_from_both_forms_instead_of_dumping_partially() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
write(base, ".mmz/conf.d/a.yaml", "scopes:\n rust: [\"*.rs\"]\n");
manifest(base, "imports: [conf.d/]\nscopes:\n rust: [\"*.txt\"]\n");
let err = dump(base).expect_err("a scope declared in two files fails to merge");
let text = err.to_string();
assert!(
text.contains("rust") && text.contains("declared in both"),
"the merge error names the offending key: {text}"
);
let json_err = dump_json(base).expect_err("the json form fails identically");
assert_eq!(
err.to_string(),
json_err.to_string(),
"both renderings surface the same merge error"
);
}
#[test]
fn a_scope_s_and_a_rule_s_exclude_are_echoed_in_both_forms() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
write(base, "a.txt", "one");
manifest(
base,
concat!(
"scopes:\n",
" src:\n",
" globs: [\"*.txt\"]\n",
" exclude: [\"b.txt\"]\n",
"commands:\n",
" - name: sh\n",
" inputs: [src]\n",
" exclude: [\"**/*.log\"]\n",
),
);
let text = dump(base).expect("dump");
assert!(
text.contains(" globs: [*.txt]\n exclude: [b.txt]\n"),
"{text}"
);
assert!(
text.contains(" inputs: [src]\n exclude: [**/*.log]\n"),
"{text}"
);
let json: serde_json::Value =
serde_json::from_str(&dump_json(base).expect("json")).expect("valid json");
assert_eq!(
json.pointer("/scopes/0/exclude/0")
.and_then(serde_json::Value::as_str),
Some("b.txt")
);
assert_eq!(
json.pointer("/commands/0/exclude/0")
.and_then(serde_json::Value::as_str),
Some("**/*.log")
);
}