use super::*;
use crate::task::{Blocker, On};
use std::fs;
use tempfile::tempdir;
fn task(blockers: Vec<Blocker>) -> Task {
Task { blockers, ..Task::default() }
}
fn touch(dir: &Path, id: &str) {
let tasks = dir.join("tasks");
fs::create_dir_all(&tasks).unwrap();
fs::write(tasks.join(format!("{id}.md")), "").unwrap();
}
fn claim_blocker(id: &str) -> Blocker {
Blocker { id: id.into(), on: On::Claim }
}
fn close_blocker(id: &str) -> Blocker {
Blocker { id: id.into(), on: On::Close }
}
#[test]
fn claim_allows_a_task_with_no_blockers() {
let d = tempdir().unwrap();
claim(&task(vec![]), "bl-1", d.path()).unwrap();
}
#[test]
fn claim_is_blocked_by_an_open_dependency() {
let d = tempdir().unwrap();
touch(d.path(), "bl-dep"); let err = claim(&task(vec![claim_blocker("bl-dep")]), "bl-1", d.path()).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
assert_eq!(err.to_string(), "claim: bl-1 blocked by unresolved bl-dep");
}
#[test]
fn claim_allows_once_the_dependency_resolves() {
let d = tempdir().unwrap(); claim(&task(vec![claim_blocker("bl-dep")]), "bl-1", d.path()).unwrap();
}
#[test]
fn a_close_blocker_does_not_gate_claim() {
let d = tempdir().unwrap();
touch(d.path(), "bl-gate"); claim(&task(vec![close_blocker("bl-gate")]), "bl-1", d.path()).unwrap();
}
#[test]
fn close_allows_a_task_with_no_gates() {
let d = tempdir().unwrap();
close(&task(vec![]), "bl-1", d.path()).unwrap();
}
#[test]
fn close_is_blocked_by_an_open_gate() {
let d = tempdir().unwrap();
touch(d.path(), "bl-gate"); let err = close(&task(vec![close_blocker("bl-gate")]), "bl-1", d.path()).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
assert_eq!(err.to_string(), "close: bl-1 blocked by unresolved bl-gate");
}
#[test]
fn close_allows_once_the_gate_resolves() {
let d = tempdir().unwrap(); close(&task(vec![close_blocker("bl-gate")]), "bl-1", d.path()).unwrap();
}
#[test]
fn a_claim_blocker_does_not_gate_close() {
let d = tempdir().unwrap();
touch(d.path(), "bl-dep"); close(&task(vec![claim_blocker("bl-dep")]), "bl-1", d.path()).unwrap();
}
#[test]
fn the_refusal_names_every_open_blocker() {
let d = tempdir().unwrap();
touch(d.path(), "bl-a");
touch(d.path(), "bl-b");
let blockers = vec![claim_blocker("bl-a"), claim_blocker("bl-b")];
let err = claim(&task(blockers), "bl-1", d.path()).unwrap_err();
assert_eq!(err.to_string(), "claim: bl-1 blocked by unresolved bl-a, bl-b");
}
#[test]
fn the_refusal_names_only_blockers_open_on_this_op() {
let d = tempdir().unwrap();
touch(d.path(), "bl-open");
touch(d.path(), "bl-gate"); let blockers = vec![claim_blocker("bl-open"), claim_blocker("bl-done"), close_blocker("bl-gate")];
let err = claim(&task(blockers), "bl-1", d.path()).unwrap_err();
assert_eq!(err.to_string(), "claim: bl-1 blocked by unresolved bl-open");
}
fn op_blocker(id: &str, on: Verb) -> Blocker {
Blocker { id: id.into(), on }
}
#[test]
fn gate_refuses_the_op_its_blocker_names() {
let d = tempdir().unwrap();
touch(d.path(), "bl-x");
let err = gate(&task(vec![op_blocker("bl-x", Verb::Update)]), Verb::Update, "bl-1", d.path()).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
assert_eq!(err.to_string(), "update: bl-1 blocked by unresolved bl-x");
}
#[test]
fn gate_ignores_a_blocker_naming_a_different_op() {
let d = tempdir().unwrap();
touch(d.path(), "bl-dep"); gate(&task(vec![claim_blocker("bl-dep")]), Verb::Unclaim, "bl-1", d.path()).unwrap();
}
#[test]
fn gate_allows_once_the_blocker_resolves() {
let d = tempdir().unwrap(); gate(&task(vec![op_blocker("bl-x", Verb::Unclaim)]), Verb::Unclaim, "bl-1", d.path()).unwrap();
}
fn ball(dir: &Path, id: &str, blockers: Vec<Blocker>) {
crate::taskfile::write_task(dir, id, &task(blockers)).unwrap();
}
#[test]
fn acyclic_allows_the_one_edge_gate() {
let d = tempdir().unwrap();
ball(d.path(), "bl-gate", vec![]);
acyclic(d.path(), Verb::Create, "bl-work", &close_blocker("bl-gate")).unwrap();
}
#[test]
fn acyclic_refuses_the_lernie_two_edge_deadlock() {
let d = tempdir().unwrap();
ball(d.path(), "bl-work", vec![close_blocker("bl-gate")]);
let err = acyclic(d.path(), Verb::Update, "bl-gate", &claim_blocker("bl-work")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
let msg = err.to_string();
assert!(msg.contains("bl-gate -claim-> bl-work -close-> bl-gate"), "{msg}");
assert!(msg.contains("bl-54fe"), "{msg}");
}
#[test]
fn acyclic_refuses_a_self_edge() {
let d = tempdir().unwrap();
ball(d.path(), "bl-a", vec![close_blocker("bl-a")]);
let err = acyclic(d.path(), Verb::Update, "bl-a", &close_blocker("bl-a")).unwrap_err();
assert!(err.to_string().contains("bl-a -close-> bl-a —"), "{err}");
}
#[test]
fn acyclic_names_every_hop_of_a_longer_loop() {
let d = tempdir().unwrap();
ball(d.path(), "bl-b", vec![claim_blocker("bl-c")]);
ball(d.path(), "bl-c", vec![close_blocker("bl-a")]);
let err = acyclic(d.path(), Verb::Create, "bl-a", &close_blocker("bl-b")).unwrap_err();
assert!(
err.to_string().contains("bl-a -close-> bl-b -claim-> bl-c -close-> bl-a"),
"{err}"
);
}
#[test]
fn acyclic_ignores_edges_off_the_lifecycle() {
let d = tempdir().unwrap();
ball(d.path(), "bl-b", vec![claim_blocker("bl-a")]);
acyclic(d.path(), Verb::Update, "bl-a", &op_blocker("bl-b", Verb::Update)).unwrap();
ball(d.path(), "bl-b", vec![op_blocker("bl-c", Verb::Update)]);
ball(d.path(), "bl-c", vec![close_blocker("bl-a")]);
acyclic(d.path(), Verb::Update, "bl-a", &close_blocker("bl-b")).unwrap();
}
#[test]
fn acyclic_treats_an_absent_ball_as_resolved() {
let d = tempdir().unwrap();
acyclic(d.path(), Verb::Update, "bl-a", &claim_blocker("bl-gone")).unwrap();
}
#[test]
fn a_preexisting_side_loop_never_refuses_an_unrelated_edge() {
let d = tempdir().unwrap();
ball(d.path(), "bl-b", vec![claim_blocker("bl-c")]);
ball(d.path(), "bl-c", vec![claim_blocker("bl-b")]);
acyclic(d.path(), Verb::Update, "bl-a", &claim_blocker("bl-b")).unwrap();
}