use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use grex_core::execute::{ExecCtx, ExecError, MetaVisitedSet, StepKind};
use grex_core::pack::{self, PackManifest};
use grex_core::plugin::pack_type::MetaPlugin;
use grex_core::plugin::{PackTypePlugin, PackTypeRegistry};
use grex_core::{Registry, VarEnv};
use tempfile::TempDir;
use tokio::runtime::Builder;
fn write_pack(dir: &Path, yaml: &str) {
fs::create_dir_all(dir.join(".grex")).unwrap();
fs::write(dir.join(".grex").join("pack.yaml"), yaml).unwrap();
}
fn declarative_mkdir_pack(name: &str, target: &Path) -> String {
format!(
"schema_version: \"1\"\nname: {}\ntype: declarative\nactions:\n - mkdir:\n path: {}\n",
name,
target.to_string_lossy().replace('\\', "/"),
)
}
fn meta_pack_with_children(name: &str, child_paths: &[&str]) -> String {
let mut yaml = format!("schema_version: \"1\"\nname: {name}\ntype: meta\n");
if !child_paths.is_empty() {
yaml.push_str("children:\n");
for path in child_paths {
yaml.push_str(&format!(" - url: https://example.invalid/{path}\n path: {path}\n"));
}
}
yaml
}
fn parse(s: &str) -> PackManifest {
pack::parse(s).expect("fixture parses")
}
fn new_visited() -> MetaVisitedSet {
Arc::new(Mutex::new(HashSet::new()))
}
fn ctx<'a>(
vars: &'a VarEnv,
pack_root: &'a Path,
workspace: &'a Path,
action_reg: &'a Arc<Registry>,
pack_type_reg: &'a Arc<PackTypeRegistry>,
visited: &'a MetaVisitedSet,
) -> ExecCtx<'a> {
ExecCtx::new(vars, pack_root, workspace)
.with_registry(action_reg)
.with_pack_type_registry(pack_type_reg)
.with_visited_meta(visited)
}
fn rt() -> tokio::runtime::Runtime {
Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap()
}
#[test]
fn meta_recurses_one_declarative_child() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
let sink = tmp.path().join("sink-a");
write_pack(&root, &meta_pack_with_children("parent", &["child-a"]));
write_pack(&root.join("child-a"), &declarative_mkdir_pack("child-a", &sink));
let pack = parse(&meta_pack_with_children("parent", &["child-a"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
let step = rt().block_on(MetaPlugin.install(&ctx, &pack)).expect("install ok");
assert_eq!(step.action_name.as_ref(), "meta");
assert!(sink.is_dir(), "child mkdir must have run");
match step.details {
StepKind::When { nested_steps, .. } => assert_eq!(nested_steps.len(), 1),
other => panic!("expected When envelope, got {other:?}"),
}
}
#[test]
fn meta_recurses_two_children_in_order() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
let sink_a = tmp.path().join("a");
let sink_b = tmp.path().join("b");
write_pack(&root, &meta_pack_with_children("parent", &["a", "b"]));
write_pack(&root.join("a"), &declarative_mkdir_pack("a", &sink_a));
write_pack(&root.join("b"), &declarative_mkdir_pack("b", &sink_b));
let pack = parse(&meta_pack_with_children("parent", &["a", "b"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
let step = rt().block_on(MetaPlugin.install(&ctx, &pack)).expect("install ok");
assert!(sink_a.is_dir() && sink_b.is_dir());
match step.details {
StepKind::When { nested_steps, .. } => assert_eq!(nested_steps.len(), 2),
other => panic!("expected When, got {other:?}"),
}
}
#[test]
fn meta_recurses_three_levels_deep() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root"); let mid = root.join("mid"); let leaf = mid.join("leaf"); let sink = tmp.path().join("leaf-sink");
write_pack(&root, &meta_pack_with_children("root", &["mid"]));
write_pack(&mid, &meta_pack_with_children("mid", &["leaf"]));
write_pack(&leaf, &declarative_mkdir_pack("leaf", &sink));
let pack = parse(&meta_pack_with_children("root", &["mid"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
rt().block_on(MetaPlugin.install(&ctx, &pack)).expect("3-level install ok");
assert!(sink.is_dir(), "3-level-deep leaf mkdir must have run");
}
#[test]
fn meta_cycle_a_to_b_to_a_errors() {
let tmp = TempDir::new().unwrap();
let dir_a = tmp.path().join("pack-a");
let dir_b = tmp.path().join("pack-b");
fs::create_dir_all(&dir_a).unwrap();
fs::create_dir_all(&dir_b).unwrap();
let abs_a = fs::canonicalize(&dir_a).unwrap();
let abs_b = fs::canonicalize(&dir_b).unwrap();
let abs_a_s = abs_a.to_string_lossy().replace('\\', "/");
let abs_b_s = abs_b.to_string_lossy().replace('\\', "/");
fs::create_dir_all(dir_a.join(".grex")).unwrap();
fs::write(
dir_a.join(".grex").join("pack.yaml"),
format!(
"schema_version: \"1\"\nname: pack-a\ntype: meta\nchildren:\n - url: https://example.invalid/b\n path: {abs_b_s}\n"
),
)
.unwrap();
fs::create_dir_all(dir_b.join(".grex")).unwrap();
fs::write(
dir_b.join(".grex").join("pack.yaml"),
format!(
"schema_version: \"1\"\nname: pack-b\ntype: meta\nchildren:\n - url: https://example.invalid/a\n path: {abs_a_s}\n"
),
)
.unwrap();
let pack = pack::parse(&fs::read_to_string(dir_a.join(".grex").join("pack.yaml")).unwrap())
.expect("pack A parses");
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &dir_a, tmp.path(), &action_reg, &pack_type_reg, &visited);
let err = rt().block_on(MetaPlugin.install(&ctx, &pack)).expect_err("cycle must error");
assert!(matches!(err, ExecError::MetaCycle { .. }), "expected MetaCycle, got: {err:?}");
}
#[test]
fn meta_self_cycle_errors() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("self");
write_pack(&root, &meta_pack_with_children("self", &["me"]));
write_pack(&root.join("me"), &meta_pack_with_children("me", &[".."]));
let pack = parse(&meta_pack_with_children("self", &["me"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
let err = rt().block_on(MetaPlugin.install(&ctx, &pack)).expect_err("self-cycle errors");
assert!(matches!(err, ExecError::MetaCycle { .. }), "expected MetaCycle, got: {err:?}");
}
#[test]
fn meta_missing_child_path_errors() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
write_pack(&root, &meta_pack_with_children("p", &["missing"]));
let pack = parse(&meta_pack_with_children("p", &["missing"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
let err = rt().block_on(MetaPlugin.install(&ctx, &pack)).expect_err("missing child errors");
match err {
ExecError::ExecInvalid(msg) => {
assert!(msg.contains("child manifest load failed"), "msg: {msg}");
}
other => panic!("expected ExecInvalid, got: {other:?}"),
}
}
#[test]
fn meta_malformed_child_manifest_errors() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
write_pack(&root, &meta_pack_with_children("p", &["bad"]));
fs::create_dir_all(root.join("bad").join(".grex")).unwrap();
fs::write(root.join("bad").join(".grex").join("pack.yaml"), "this is not yaml: : :").unwrap();
let pack = parse(&meta_pack_with_children("p", &["bad"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
let err = rt().block_on(MetaPlugin.install(&ctx, &pack)).expect_err("malformed errors");
assert!(matches!(err, ExecError::ExecInvalid(_)), "got: {err:?}");
}
#[test]
fn meta_unknown_child_pack_type_errors() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
write_pack(&root, &meta_pack_with_children("p", &["kid"]));
write_pack(&root.join("kid"), &declarative_mkdir_pack("kid", &tmp.path().join("k")));
let pack = parse(&meta_pack_with_children("p", &["kid"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::new());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
let err = rt().block_on(MetaPlugin.install(&ctx, &pack)).expect_err("unknown type errors");
match err {
ExecError::UnknownPackType { requested } => assert_eq!(requested, "declarative"),
other => panic!("expected UnknownPackType, got: {other:?}"),
}
}
#[test]
fn meta_halts_on_second_child_error_does_not_run_third() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
let sink_a = tmp.path().join("ok-a");
let sink_c = tmp.path().join("never-c");
write_pack(&root, &meta_pack_with_children("p", &["a", "b", "c"]));
write_pack(&root.join("a"), &declarative_mkdir_pack("a", &sink_a));
fs::create_dir_all(root.join("b").join(".grex")).unwrap();
fs::write(root.join("b").join(".grex").join("pack.yaml"), "garbage: : :").unwrap();
write_pack(&root.join("c"), &declarative_mkdir_pack("c", &sink_c));
let pack = parse(&meta_pack_with_children("p", &["a", "b", "c"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
let err = rt().block_on(MetaPlugin.install(&ctx, &pack)).expect_err("b halts");
assert!(matches!(err, ExecError::ExecInvalid(_)), "got: {err:?}");
assert!(sink_a.is_dir(), "first child must have completed");
assert!(!sink_c.exists(), "third child must NOT have been dispatched");
}
#[test]
#[allow(clippy::too_many_lines)] fn meta_teardown_visits_children_in_reverse() {
use grex_core::plugin::pack_type::DeclarativePlugin;
use std::sync::Mutex as StdMutex;
struct Probe {
order: Arc<StdMutex<Vec<String>>>,
}
#[async_trait::async_trait]
impl PackTypePlugin for Probe {
fn name(&self) -> &str {
"declarative"
}
async fn install(
&self,
ctx: &ExecCtx<'_>,
pack: &PackManifest,
) -> Result<grex_core::ExecStep, ExecError> {
self.order
.lock()
.unwrap()
.push(format!("install:{}", ctx.pack_root.file_name().unwrap().to_string_lossy()));
DeclarativePlugin.install(ctx, pack).await
}
async fn update(
&self,
ctx: &ExecCtx<'_>,
pack: &PackManifest,
) -> Result<grex_core::ExecStep, ExecError> {
self.install(ctx, pack).await
}
async fn teardown(
&self,
ctx: &ExecCtx<'_>,
pack: &PackManifest,
) -> Result<grex_core::ExecStep, ExecError> {
self.order
.lock()
.unwrap()
.push(format!("teardown:{}", ctx.pack_root.file_name().unwrap().to_string_lossy()));
DeclarativePlugin.teardown(ctx, pack).await
}
async fn sync(
&self,
ctx: &ExecCtx<'_>,
pack: &PackManifest,
) -> Result<grex_core::ExecStep, ExecError> {
self.install(ctx, pack).await
}
}
let order = Arc::new(StdMutex::new(Vec::<String>::new()));
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
write_pack(&root, &meta_pack_with_children("p", &["a", "b", "c"]));
for name in ["a", "b", "c"] {
write_pack(
&root.join(name),
&format!("schema_version: \"1\"\nname: {name}\ntype: declarative\n"),
);
}
let pack = parse(&meta_pack_with_children("p", &["a", "b", "c"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let mut reg = PackTypeRegistry::new();
reg.register(MetaPlugin);
reg.register(Probe { order: order.clone() });
let pack_type_reg = Arc::new(reg);
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
rt().block_on(MetaPlugin.teardown(&ctx, &pack)).expect("teardown ok");
let seen = order.lock().unwrap().clone();
assert_eq!(seen, vec!["teardown:c", "teardown:b", "teardown:a"], "got: {seen:?}");
}
#[test]
fn meta_update_equals_install_semantics() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
let sink = tmp.path().join("upd-sink");
write_pack(&root, &meta_pack_with_children("p", &["k"]));
write_pack(&root.join("k"), &declarative_mkdir_pack("k", &sink));
let pack = parse(&meta_pack_with_children("p", &["k"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
rt().block_on(MetaPlugin.update(&ctx, &pack)).expect("update ok");
assert!(sink.is_dir(), "update must dispatch child action");
}
#[test]
fn meta_sync_equals_install_semantics() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
let sink = tmp.path().join("sync-sink");
write_pack(&root, &meta_pack_with_children("p", &["k"]));
write_pack(&root.join("k"), &declarative_mkdir_pack("k", &sink));
let pack = parse(&meta_pack_with_children("p", &["k"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
rt().block_on(MetaPlugin.sync(&ctx, &pack)).expect("sync ok");
assert!(sink.is_dir(), "sync must dispatch child action");
}
#[test]
fn multi_thread_runtime_allows_nested_dispatch() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
let mid = root.join("mid");
let leaf = mid.join("leaf");
let sink = tmp.path().join("nested-sink");
write_pack(&root, &meta_pack_with_children("root", &["mid"]));
write_pack(&mid, &meta_pack_with_children("mid", &["leaf"]));
write_pack(&leaf, &declarative_mkdir_pack("leaf", &sink));
let pack = parse(&meta_pack_with_children("root", &["mid"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
rt().block_on(MetaPlugin.install(&ctx, &pack)).expect("nested install ok");
assert!(sink.is_dir());
}
#[test]
fn meta_without_pack_type_registry_errors_cleanly() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
write_pack(&root, &meta_pack_with_children("p", &["k"]));
write_pack(&root.join("k"), &declarative_mkdir_pack("k", &tmp.path().join("k-sink")));
let pack = parse(&meta_pack_with_children("p", &["k"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let visited = new_visited();
let ctx = ExecCtx::new(&vars, root.as_path(), tmp.path())
.with_registry(&action_reg)
.with_visited_meta(&visited);
let err = rt().block_on(MetaPlugin.install(&ctx, &pack)).expect_err("missing registry errors");
match err {
ExecError::ExecInvalid(msg) => {
assert!(msg.contains("pack_type_registry"), "msg: {msg}");
}
other => panic!("expected ExecInvalid, got: {other:?}"),
}
}
#[test]
#[cfg(unix)]
fn cycle_detection_canonicalises_symlinks() {
use std::os::unix::fs::symlink;
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
let b = root.join("b");
write_pack(&root, &meta_pack_with_children("root", &["b"]));
write_pack(&b, &meta_pack_with_children("b", &["b-link"]));
symlink(&b, b.join("b-link")).unwrap();
let pack = parse(&meta_pack_with_children("root", &["b"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let ctx = ctx(&vars, &root, tmp.path(), &action_reg, &pack_type_reg, &visited);
let err = rt().block_on(MetaPlugin.install(&ctx, &pack)).expect_err("symlink cycle errors");
assert!(
matches!(err, ExecError::MetaCycle { .. }),
"symlinked cycle should be MetaCycle, got: {err:?}"
);
}
#[test]
fn meta_teardown_child_failure_preserves_parent_gitignore_block() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("root");
let workspace = tmp.path().join("ws");
fs::create_dir_all(&workspace).unwrap();
let parent_yaml = "schema_version: \"1\"\nname: parent\ntype: meta\nx-gitignore:\n - parent-ignored/\nchildren:\n - url: https://example.invalid/a\n path: a\n - url: https://example.invalid/b\n path: b\n";
write_pack(&root, parent_yaml);
write_pack(&root.join("a"), "schema_version: \"1\"\nname: child-a\ntype: declarative\n");
fs::create_dir_all(root.join("b").join(".grex")).unwrap();
fs::write(root.join("b").join(".grex").join("pack.yaml"), "garbage: : :").unwrap();
let pack = parse(parent_yaml);
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let v1 = new_visited();
let ctx1 = ExecCtx::new(&vars, &root, &workspace)
.with_registry(&action_reg)
.with_pack_type_registry(&pack_type_reg)
.with_visited_meta(&v1);
fs::write(
root.join("b").join(".grex").join("pack.yaml"),
"schema_version: \"1\"\nname: child-b\ntype: declarative\n",
)
.unwrap();
rt().block_on(MetaPlugin.install(&ctx1, &pack)).expect("install ok");
let gi_path = workspace.join(".gitignore");
let pre = fs::read_to_string(&gi_path).expect("gitignore present post-install");
assert!(pre.contains("# >>> grex:parent >>>"), "install must write block: {pre}");
fs::write(root.join("b").join(".grex").join("pack.yaml"), "garbage: : :").unwrap();
let v2 = new_visited();
let ctx2 = ExecCtx::new(&vars, &root, &workspace)
.with_registry(&action_reg)
.with_pack_type_registry(&pack_type_reg)
.with_visited_meta(&v2);
let err = rt().block_on(MetaPlugin.teardown(&ctx2, &pack)).expect_err("child b halts");
assert!(matches!(err, ExecError::ExecInvalid(_)), "got: {err:?}");
let post = fs::read_to_string(&gi_path).expect("gitignore still present");
assert!(
post.contains("# >>> grex:parent >>>"),
"gitignore block must survive partial teardown: {post}"
);
}
#[test]
fn cycle_detection_under_multi_thread_runtime() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("self");
write_pack(&root, &meta_pack_with_children("self", &["me"]));
write_pack(&root.join("me"), &meta_pack_with_children("me", &[".."]));
let pack = parse(&meta_pack_with_children("self", &["me"]));
let vars = VarEnv::default();
let action_reg = Arc::new(Registry::bootstrap());
let pack_type_reg = Arc::new(PackTypeRegistry::bootstrap());
let visited = new_visited();
let runtime = rt();
let (r1, r2) = runtime.block_on(async {
let pack1 = pack.clone();
let pack2 = pack.clone();
let root1 = root.clone();
let root2 = root.clone();
let ws = tmp.path().to_path_buf();
let ws2 = ws.clone();
let vars1 = vars.clone();
let vars2 = vars.clone();
let ar = action_reg.clone();
let pr = pack_type_reg.clone();
let ar2 = action_reg.clone();
let pr2 = pack_type_reg.clone();
let v1 = visited.clone();
let v2 = visited.clone();
let t1 = tokio::spawn(async move {
let ctx = ExecCtx::new(&vars1, &root1, &ws)
.with_registry(&ar)
.with_pack_type_registry(&pr)
.with_visited_meta(&v1);
MetaPlugin.install(&ctx, &pack1).await
});
let t2 = tokio::spawn(async move {
tokio::task::yield_now().await;
let ctx = ExecCtx::new(&vars2, &root2, &ws2)
.with_registry(&ar2)
.with_pack_type_registry(&pr2)
.with_visited_meta(&v2);
MetaPlugin.install(&ctx, &pack2).await
});
(t1.await.unwrap(), t2.await.unwrap())
});
let is_cycle =
|r: &Result<grex_core::ExecStep, ExecError>| matches!(r, Err(ExecError::MetaCycle { .. }));
assert!(
is_cycle(&r1) || is_cycle(&r2),
"at least one dispatch must halt with MetaCycle — r1={r1:?} r2={r2:?}"
);
}
#[test]
fn _imports_live() {
let _: PathBuf = PathBuf::from("/");
let _: &Path = Path::new("/");
}