#[path = "common/harness.rs"]
mod harness;
use std::time::Duration;
use harness::TestHarness;
use macrame::branch::Ancestor;
use macrame::error::DbError;
use macrame::graph::EdgeAssertion;
use macrame::temporal::{resolve_beliefs, EdgeBelief};
use macrame::util::Clock;
use macrame::{BranchId, ConceptUpsert, Database, ReadPlan};
const EPOCH: &str = "1970-01-01T00:00:00.000000Z";
const OPEN: &str = "9999-12-31T23:59:59.999999Z";
const SHADOWED: &str = "1970-01-01T00:30:00.000000Z";
const STEP: Duration = Duration::from_secs(3_600);
fn id(name: &str) -> BranchId {
BranchId::new(name).unwrap()
}
async fn seed(db: &Database, names: &[&str]) {
db.write_concepts(
names
.iter()
.map(|n| ConceptUpsert::new(*n, "n").valid_from(EPOCH))
.collect(),
)
.await
.unwrap();
}
async fn edge(db: &Database, s: &str, t: &str, branch: Option<&str>) {
let mut e = EdgeAssertion::new(s, t, "LEADSTO")
.valid_from(EPOCH)
.valid_to(OPEN);
if let Some(b) = branch {
e = e.on_branch(id(b));
}
db.assert_edge(e).await.unwrap();
}
fn keys(mut beliefs: Vec<EdgeBelief>) -> Vec<String> {
beliefs.sort();
beliefs
.into_iter()
.map(|e| format!("{}|{}", e.entity_id(), e.branch_id))
.collect()
}
fn live_at(beliefs: Vec<EdgeBelief>, valid: &str) -> Vec<EdgeBelief> {
beliefs
.into_iter()
.filter(|e| e.valid_from.as_str() <= valid && valid < e.valid_to.as_str())
.collect()
}
async fn agrees_with_the_plan(db: &Database, branch: &str, recorded: &str, at: &str) {
let folded = db.reconstruct_on(recorded, branch).await.unwrap();
let planned = db
.edges(
ReadPlan::new()
.on(id(branch))
.recorded_at(recorded)
.valid_at(at),
)
.await
.unwrap();
let (got, want) = (keys(live_at(folded.edges, at)), keys(planned));
assert_eq!(got, want, "`{branch}` at recorded {recorded}, valid {at}");
assert!(
!got.is_empty(),
"vacuous: `{branch}` sees nothing at {recorded}"
);
}
async fn forked_and_diverged(h: &TestHarness) -> (Database, Vec<String>) {
let db = h.db_with_fake_clock().await;
let mut marks = Vec::new();
seed(&db, &["a", "b", "c", "d"]).await;
edge(&db, "a", "b", None).await;
h.advance(STEP);
marks.push(h.clock.now());
db.fork(id("exp"), BranchId::main()).await.unwrap();
h.advance(STEP);
marks.push(h.clock.now());
edge(&db, "c", "d", None).await;
edge(&db, "b", "c", Some("exp")).await;
db.assert_edge(
EdgeAssertion::new("a", "b", "LEADSTO")
.valid_from(EPOCH)
.valid_to(SHADOWED)
.on_branch(id("exp")),
)
.await
.unwrap();
h.advance(STEP);
marks.push(h.clock.now()); marks.push("2999-01-01T00:00:00.000000Z".to_string());
(db, marks)
}
#[tokio::test]
async fn on_the_trunk_of_an_unforked_ledger_nothing_changes() {
let h = TestHarness::new();
let db = h.db_with_fake_clock().await;
seed(&db, &["a", "b", "c"]).await;
edge(&db, "a", "b", None).await;
edge(&db, "b", "c", None).await;
h.advance(STEP);
let now = h.clock.now();
let whole = db.reconstruct(&now).await.unwrap();
let one = db.reconstruct_on(&now, "main").await.unwrap();
assert_eq!(whole.edges, one.edges, "one lineage, so nothing to narrow");
assert_eq!(whole.concepts.len(), one.concepts.len());
agrees_with_the_plan(&db, "main", &now, EPOCH).await;
db.close().await.unwrap();
}
#[tokio::test]
async fn the_resolved_fold_agrees_with_the_lowering_at_every_instant() {
let h = TestHarness::starting_at(std::time::UNIX_EPOCH + Duration::from_secs(1_000_000));
let (db, marks) = forked_and_diverged(&h).await;
assert_eq!(marks.len(), 4);
for branch in ["main", "exp"] {
for recorded in &marks {
agrees_with_the_plan(&db, branch, recorded, EPOCH).await;
}
}
db.close().await.unwrap();
}
#[tokio::test]
async fn a_branch_does_not_see_what_the_trunk_wrote_after_the_fork() {
let h = TestHarness::starting_at(std::time::UNIX_EPOCH + Duration::from_secs(1_000_000));
let (db, _) = forked_and_diverged(&h).await;
let now = h.clock.now();
let whole = db.reconstruct(&now).await.unwrap();
let on_exp = db.reconstruct_on(&now, "exp").await.unwrap();
let has = |s: &[EdgeBelief], src: &str| s.iter().any(|e| e.source_id == src);
assert!(has(&whole.edges, "c"), "the ledger holds the trunk's `c→d`");
assert!(
!has(&on_exp.edges, "c"),
"`exp` forked before it and must not inherit it: {:?}",
on_exp.edges
);
assert!(has(&on_exp.edges, "a"), "`a→b` is pre-fork and inherited");
assert!(has(&on_exp.edges, "b"), "`b→c` is the branch's own");
db.close().await.unwrap();
}
#[tokio::test]
async fn the_nearest_lineage_wins_at_a_shared_key() {
let h = TestHarness::starting_at(std::time::UNIX_EPOCH + Duration::from_secs(1_000_000));
let (db, marks) = forked_and_diverged(&h).await;
let now = marks[2].clone();
let on_exp = db.reconstruct_on(&now, "exp").await.unwrap();
let shadowed: Vec<_> = on_exp
.edges
.iter()
.filter(|e| e.source_id == "a" && e.target_id == "b")
.collect();
assert_eq!(shadowed.len(), 1, "one belief per key: {shadowed:?}");
assert_eq!(shadowed[0].branch_id, "exp", "the nearer lineage");
assert_eq!(shadowed[0].valid_to, SHADOWED);
let on_main = db.reconstruct_on(&now, "main").await.unwrap();
let trunk: Vec<_> = on_main
.edges
.iter()
.filter(|e| e.source_id == "a" && e.target_id == "b")
.collect();
assert_eq!(trunk.len(), 1);
assert_eq!(trunk[0].valid_to, OPEN, "the ancestor's row is not closed");
agrees_with_the_plan(&db, "exp", &now, EPOCH).await;
db.close().await.unwrap();
}
#[tokio::test]
async fn an_unknown_lineage_is_refused_by_name() {
let h = TestHarness::new();
let (db, _) = forked_and_diverged(&h).await;
let now = h.clock.now();
match db.reconstruct_on(&now, "ghost").await {
Err(DbError::UnknownBranch(name)) => assert_eq!(name, "ghost"),
other => panic!("expected UnknownBranch, got {other:?}"),
}
match db.ancestry("ghost").await {
Err(DbError::UnknownBranch(name)) => assert_eq!(name, "ghost"),
other => panic!("expected UnknownBranch, got {other:?}"),
}
db.close().await.unwrap();
}
#[tokio::test]
async fn the_published_ancestry_is_the_one_the_reader_uses() {
let h = TestHarness::starting_at(std::time::UNIX_EPOCH + Duration::from_secs(1_000_000));
let (db, _) = forked_and_diverged(&h).await;
let anc = db.ancestry("exp").await.unwrap();
assert_eq!(anc.len(), 2);
assert_eq!(anc[0].branch_id, "exp");
assert_eq!(anc[0].dist, 0);
assert_eq!(anc[0].cutoff, None, "the reader has no cutoff");
assert_eq!(anc[1].branch_id, "main");
assert_eq!(anc[1].dist, 1);
assert!(anc[1].cutoff.is_some(), "`main` is cut at the fork point");
let anc = db.ancestry("main").await.unwrap();
assert_eq!(anc.len(), 1);
assert_eq!(anc[0].branch_id, "main");
db.close().await.unwrap();
}
fn belief(src: &str, branch: &str) -> EdgeBelief {
EdgeBelief::new(src, "t", "LEADSTO", EPOCH, OPEN).on_branch(branch)
}
fn ancestor(name: &str, dist: i64) -> Ancestor {
Ancestor::new(name, dist)
}
#[test]
fn the_pure_resolution_picks_by_distance_and_ignores_strangers() {
let anc = [ancestor("exp", 0), ancestor("main", 1)];
let held = vec![
belief("a", "main"),
belief("a", "exp"),
belief("a", "sibling"),
];
let out = resolve_beliefs(&held, &anc);
assert_eq!(out.len(), 1, "one row per key: {out:?}");
assert_eq!(out[0].branch_id, "exp", "dist 0 beats dist 1");
let reversed: Vec<_> = held.iter().rev().cloned().collect();
assert_eq!(resolve_beliefs(&reversed, &anc), out);
let out = resolve_beliefs(&[belief("b", "main"), belief("c", "sibling")], &anc);
assert_eq!(out.len(), 1);
assert_eq!(out[0].source_id, "b");
assert!(resolve_beliefs(&held, &[]).is_empty());
}
#[tokio::test]
async fn a_lineages_view_survives_the_rows_moving_to_cold_storage() {
let h = TestHarness::starting_at(std::time::UNIX_EPOCH + Duration::from_secs(1_000_000));
let (db, marks) = forked_and_diverged(&h).await;
let early = marks[0].clone();
let before = keys(db.reconstruct_on(&early, "exp").await.unwrap().edges);
assert!(!before.is_empty(), "the fixture has something to lose");
h.advance(STEP);
let report = db.archive(&h.clock.now()).await.unwrap();
assert!(
db.reconstruct(&early).await.is_ok(),
"the whole-ledger read crosses the boundary too, and is the control"
);
let after = keys(db.reconstruct_on(&early, "exp").await.unwrap().edges);
assert_eq!(
before, after,
"an archive is a move, not a retirement (report: {report:?})"
);
let view = db.reconstruct_on(&early, "exp").await.unwrap();
assert!(
!view.edges.iter().any(|e| e.source_id == "c"),
"the trunk's post-fork edge is still not inherited: {:?}",
view.edges
);
db.close().await.unwrap();
}