#[path = "common/harness.rs"]
mod harness;
use std::time::Duration;
use harness::TestHarness;
use macrame::error::DbError;
use macrame::graph::{EdgeAssertion, TraversalBuilder};
use macrame::{BranchId, ConceptUpsert, Database};
const EPOCH: &str = "1970-01-01T00:00:00.000000Z";
const OPEN: &str = "9999-12-31T23:59:59.999999Z";
const CLOSED: &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 seeded_with_two_siblings(h: &TestHarness) -> Database {
let db = h.db_with_fake_clock().await;
db.write_concepts(
["a", "b", "c"]
.iter()
.map(|n| ConceptUpsert::new(*n, "n").valid_from(EPOCH))
.collect(),
)
.await
.unwrap();
db.bulk_import(vec![
EdgeAssertion::new("a", "b", "LEADSTO")
.valid_from(EPOCH)
.valid_to(OPEN),
EdgeAssertion::new("b", "c", "LEADSTO")
.valid_from(EPOCH)
.valid_to(OPEN),
])
.await
.unwrap();
h.advance(STEP);
db.fork(id("alpha"), BranchId::main()).await.unwrap();
db.fork(id("beta"), BranchId::main()).await.unwrap();
h.advance(STEP);
db
}
async fn reached(db: &Database, branch: Option<&str>) -> Vec<String> {
let mut b = TraversalBuilder::new("a");
if let Some(n) = branch {
b = b.on_branch(id(n));
}
let mut v = b.execute_ids(db.read_conn(), EPOCH).await.unwrap();
v.sort();
v
}
async fn scalar(db: &Database, sql: &str) -> i64 {
db.read_conn()
.query(sql, ())
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap()
}
async fn one_lineage_reaching_into_anothers_vocabulary(db: &Database) {
db.write_concepts(vec![ConceptUpsert::new("d", "n")
.valid_from(EPOCH)
.on_branch(id("alpha"))])
.await
.unwrap();
db.assert_edge(
EdgeAssertion::new("c", "d", "LEADSTO")
.valid_from(EPOCH)
.valid_to(OPEN)
.on_branch(id("beta")),
)
.await
.unwrap();
}
#[tokio::test]
async fn a_batch_naming_two_lineages_lands_one_row_on_each() {
let h = TestHarness::new();
let db = seeded_with_two_siblings(&h).await;
db.bulk_import(vec![
EdgeAssertion::new("a", "c", "LEADSTO")
.valid_from(EPOCH)
.valid_to(OPEN)
.on_branch(id("alpha")),
EdgeAssertion::new("b", "a", "LEADSTO")
.valid_from(EPOCH)
.valid_to(OPEN)
.on_branch(id("beta")),
])
.await
.unwrap();
assert_eq!(
scalar(&db, "SELECT COUNT(*) FROM links WHERE branch_id = 'alpha'").await,
1
);
assert_eq!(
scalar(&db, "SELECT COUNT(*) FROM links WHERE branch_id = 'beta'").await,
1
);
assert_eq!(
scalar(
&db,
"SELECT COUNT(*) FROM links WHERE branch_id NOT IN ('main','alpha','beta')"
)
.await,
0,
"every row belongs to exactly one registered lineage"
);
assert_eq!(reached(&db, Some("alpha")).await, ["a", "b", "c"]);
assert_eq!(reached(&db, Some("beta")).await, ["a", "b", "c"]);
}
#[tokio::test]
async fn an_edge_may_name_a_concept_a_sibling_minted() {
let h = TestHarness::new();
let db = seeded_with_two_siblings(&h).await;
one_lineage_reaching_into_anothers_vocabulary(&db).await;
assert_eq!(
scalar(
&db,
"SELECT COUNT(*) FROM links WHERE branch_id = 'beta' AND target_id = 'd'"
)
.await,
1,
"the edge lands on the lineage that asserted it"
);
assert_eq!(
reached(&db, Some("beta")).await,
["a", "b", "c", "d"],
"and beta reads through it to a concept it did not mint"
);
assert_eq!(
reached(&db, Some("alpha")).await,
["a", "b", "c"],
"while alpha, which minted the concept, has no edge to reach it by: \
vocabulary is shared and belief is not"
);
}
#[tokio::test]
async fn a_lineage_whose_concept_a_sibling_names_is_refused_by_name() {
let h = TestHarness::new();
let db = seeded_with_two_siblings(&h).await;
one_lineage_reaching_into_anothers_vocabulary(&db).await;
let err = db.archive_branch(id("alpha")).await.unwrap_err();
assert!(
matches!(&err, DbError::BranchNotArchivable { branch, reason }
if branch == "alpha" && reason.contains('d')),
"the refusal has to name the concept holding the lineage, because that \
is what the caller must retire: {err:?}"
);
assert_eq!(
reached(&db, Some("beta")).await,
["a", "b", "c", "d"],
"a refusal must change nothing"
);
assert_eq!(
scalar(
&db,
"SELECT COUNT(*) FROM concepts WHERE branch_id = 'alpha'"
)
.await,
1
);
}
#[tokio::test]
async fn retiring_the_sibling_edge_and_archiving_it_releases_the_lineage() {
let h = TestHarness::new();
let db = seeded_with_two_siblings(&h).await;
one_lineage_reaching_into_anothers_vocabulary(&db).await;
db.retire_edge_on("c", "d", "LEADSTO", EPOCH, CLOSED, id("beta"))
.await
.unwrap();
let err = db.archive_branch(id("alpha")).await.unwrap_err();
assert!(
matches!(&err, DbError::BranchNotArchivable { .. }),
"retirement alone leaves the rows hot, so the guard still holds: {err:?}"
);
h.advance(STEP);
db.archive(&h.clock.peek()).await.unwrap();
assert_eq!(
scalar(
&db,
"SELECT COUNT(*) FROM links WHERE branch_id = 'beta' AND target_id = 'd'"
)
.await,
0,
"the sibling's edge is cold, which is what the guard reads"
);
db.archive_branch(id("alpha")).await.unwrap();
assert_eq!(
scalar(
&db,
"SELECT COUNT(*) FROM branches WHERE branch_id = 'alpha'"
)
.await,
0,
"and the lineage the reach was holding open is now forgettable"
);
}