#![allow(dead_code)]
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::PathBuf;
use macrame::graph::EdgeAssertion;
use macrame::integrity::audit_current;
use macrame::{BranchId, ConceptUpsert, Database, ReadPlan};
pub const NODES: [&str; 4] = ["a", "b", "c", "d"];
pub const EPOCH: &str = "1970-01-01T00:00:00.000000Z";
pub const T1: &str = "1970-01-02T00:00:00.000000Z";
pub const T2: &str = "1970-01-03T00:00:00.000000Z";
pub const T3: &str = "1970-01-04T00:00:00.000000Z";
pub const OPEN: &str = "9999-12-31T23:59:59.999999Z";
pub const LATE: &str = "2999-01-01T00:00:00.000000Z";
pub const BRANCHES: [&str; 3] = ["main", "x", "y"];
pub const KEYS: [(usize, usize, usize, &str); 4] = [
(0, 1, 0, EPOCH), (1, 2, 0, EPOCH), (2, 3, 0, EPOCH), (0, 2, 1, T1), ];
pub const TYPES: [&str; 2] = ["LEADSTO", "PARTOF"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Op {
Assert {
key: usize,
branch: usize,
closed: bool,
},
Retire { key: usize, branch: usize },
Fork { parent: usize, child: usize },
Archive,
ArchiveBranch { branch: usize },
}
pub fn id(name: &str) -> BranchId {
BranchId::new(name).unwrap()
}
fn key_parts(k: usize) -> (&'static str, &'static str, &'static str, &'static str) {
let (s, t, ty, vf) = KEYS[k % KEYS.len()];
(NODES[s], NODES[t], TYPES[ty], vf)
}
pub async fn seed(db: &Database) {
db.write_concepts(
NODES
.iter()
.map(|n| ConceptUpsert::new(*n, "n").valid_from(EPOCH))
.collect(),
)
.await
.unwrap();
for k in [0usize, 1] {
let (s, t, ty, vf) = key_parts(k);
db.assert_edge(
EdgeAssertion::new(s, t, ty)
.valid_from(vf)
.valid_to(OPEN),
)
.await
.unwrap();
}
}
pub async fn step(db: &Database, op: Op, tree: &mut Lineages) {
match op {
Op::Assert { key, branch, closed } => {
let (s, t, ty, vf) = key_parts(key);
let mut e = EdgeAssertion::new(s, t, ty)
.valid_from(vf)
.valid_to(if closed { T2 } else { OPEN });
let name = BRANCHES[branch % BRANCHES.len()];
if name != "main" {
e = e.on_branch(id(name));
}
let _ = db.assert_edge(e).await;
}
Op::Retire { key, branch } => {
let (s, t, ty, vf) = key_parts(key);
let name = BRANCHES[branch % BRANCHES.len()];
let _ = if name == "main" {
db.retire_edge(s, t, ty, vf, T2).await
} else {
db.retire_edge_on(s, t, ty, vf, T2, id(name)).await
};
}
Op::Fork { parent, child } => {
let p = BRANCHES[parent % BRANCHES.len()];
let c = BRANCHES[child % BRANCHES.len()];
if db.fork(id(c), id(p)).await.is_ok() {
tree.record(p, c);
}
}
Op::Archive => {
let _ = db.archive(LATE).await;
}
Op::ArchiveBranch { branch } => {
let name = BRANCHES[branch % BRANCHES.len()];
if db.archive_branch(id(name)).await.is_ok() {
tree.forget(name);
}
}
}
}
#[derive(Debug, Default, Clone)]
pub struct Lineages {
parent: BTreeMap<String, String>,
live: Vec<String>,
}
impl Lineages {
pub fn new() -> Self {
Self {
parent: BTreeMap::new(),
live: vec!["main".to_string()],
}
}
fn record(&mut self, parent: &str, child: &str) {
self.parent.insert(child.to_string(), parent.to_string());
if !self.live.iter().any(|b| b == child) {
self.live.push(child.to_string());
}
}
fn forget(&mut self, name: &str) {
self.live.retain(|b| b != name);
}
pub fn live(&self) -> &[String] {
&self.live
}
pub fn subtree(&self, name: &str) -> Vec<String> {
let mut out = vec![name.to_string()];
let mut grew = true;
while grew {
grew = false;
for (child, parent) in &self.parent {
if out.iter().any(|b| b == parent) && !out.iter().any(|b| b == child) {
out.push(child.clone());
grew = true;
}
}
}
out
}
}
pub async fn reach(db: &Database, branch: &str) -> Vec<String> {
let mut v: Vec<String> = db
.edges(ReadPlan::new().on(id(branch)).valid_at(T3))
.await
.map(|es| {
es.into_iter()
.map(|e| format!("{}|{}", e.entity_id(), e.branch_id))
.collect()
})
.unwrap_or_default();
v.sort();
v
}
pub async fn reach_all(db: &Database, tree: &Lineages) -> BTreeMap<String, Vec<String>> {
let mut out = BTreeMap::new();
for b in tree.live() {
out.insert(b.clone(), reach(db, b).await);
}
out
}
pub async fn fold_agrees(db: &Database, branch: &str, recorded: &str) -> Result<(), String> {
let folded = match db.reconstruct_on(recorded, branch).await {
Ok(s) => s,
Err(_) => return Ok(()),
};
let mut got: Vec<String> = folded
.edges
.into_iter()
.filter(|e| e.valid_from.as_str() <= T3 && T3 < e.valid_to.as_str())
.map(|e| format!("{}|{}", e.entity_id(), e.branch_id))
.collect();
got.sort();
let mut want: Vec<String> = match db
.edges(
ReadPlan::new()
.on(id(branch))
.recorded_at(recorded)
.valid_at(T3),
)
.await
{
Ok(es) => es
.into_iter()
.map(|e| format!("{}|{}", e.entity_id(), e.branch_id))
.collect(),
Err(_) => return Ok(()),
};
want.sort();
if got == want {
Ok(())
} else {
Err(format!(
"`{branch}` at recorded {recorded}: the fold says {got:?}, the lowering says {want:?}"
))
}
}
pub async fn audit_is_silent(db: &Database) -> Result<(), String> {
match audit_current(db.read_conn()).await {
Ok(0) => Ok(()),
Ok(n) => Err(format!("audit_current reported {n} rows of drift")),
Err(e) => Err(format!("audit_current failed: {e}")),
}
}
pub fn cases_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("lineage_cases")
}
pub fn render(history: &[Op]) -> String {
let mut s = String::new();
for op in history {
match *op {
Op::Assert { key, branch, closed } => {
let _ = writeln!(
s,
"assert key={} branch={} closed={}",
key % KEYS.len(),
BRANCHES[branch % BRANCHES.len()],
u8::from(closed)
);
}
Op::Retire { key, branch } => {
let _ = writeln!(
s,
"retire key={} branch={}",
key % KEYS.len(),
BRANCHES[branch % BRANCHES.len()]
);
}
Op::Fork { parent, child } => {
let _ = writeln!(
s,
"fork parent={} child={}",
BRANCHES[parent % BRANCHES.len()],
BRANCHES[child % BRANCHES.len()]
);
}
Op::Archive => {
let _ = writeln!(s, "archive");
}
Op::ArchiveBranch { branch } => {
let _ = writeln!(s, "archive_branch branch={}", BRANCHES[branch % BRANCHES.len()]);
}
}
}
s
}
fn branch_index(name: &str) -> Result<usize, String> {
BRANCHES
.iter()
.position(|b| *b == name)
.ok_or_else(|| format!("unknown branch `{name}`"))
}
fn field<'a>(tok: &'a str, want: &str) -> Result<&'a str, String> {
tok.strip_prefix(want)
.and_then(|r| r.strip_prefix('='))
.ok_or_else(|| format!("expected `{want}=…`, found `{tok}`"))
}
pub fn parse(text: &str) -> Result<Vec<Op>, String> {
let mut out = Vec::new();
for (n, raw) in text.lines().enumerate() {
let line = raw.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let t: Vec<&str> = line.split_whitespace().collect();
let at = |e: String| format!("line {}: {e}", n + 1);
let op = match t[0] {
"assert" if t.len() == 4 => Op::Assert {
key: field(t[1], "key").map_err(at)?.parse().map_err(|_| at("bad key".into()))?,
branch: branch_index(field(t[2], "branch").map_err(at)?).map_err(at)?,
closed: field(t[3], "closed").map_err(at)? != "0",
},
"retire" if t.len() == 3 => Op::Retire {
key: field(t[1], "key").map_err(at)?.parse().map_err(|_| at("bad key".into()))?,
branch: branch_index(field(t[2], "branch").map_err(at)?).map_err(at)?,
},
"fork" if t.len() == 3 => Op::Fork {
parent: branch_index(field(t[1], "parent").map_err(at)?).map_err(at)?,
child: branch_index(field(t[2], "child").map_err(at)?).map_err(at)?,
},
"archive" if t.len() == 1 => Op::Archive,
"archive_branch" if t.len() == 2 => Op::ArchiveBranch {
branch: branch_index(field(t[1], "branch").map_err(at)?).map_err(at)?,
},
other => return Err(at(format!("unknown op `{other}`"))),
};
out.push(op);
}
Ok(out)
}
pub async fn run_history(
db: &Database,
history: &[Op],
advance: &dyn Fn(),
now: &dyn Fn() -> String,
) -> Result<(), String> {
let mut tree = Lineages::new();
seed(db).await;
advance();
for (i, op) in history.iter().enumerate() {
let where_ = |e: String| format!("after op {i} ({op:?}): {e}");
let before = match op {
Op::Archive | Op::ArchiveBranch { .. } => Some(reach_all(db, &tree).await),
_ => None,
};
let doomed = match op {
Op::ArchiveBranch { branch } => tree.subtree(BRANCHES[branch % BRANCHES.len()]),
_ => Vec::new(),
};
step(db, *op, &mut tree).await;
advance();
let stamp = now();
audit_is_silent(db).await.map_err(where_)?;
for b in tree.live() {
fold_agrees(db, b, &stamp).await.map_err(where_)?;
}
if let Some(before) = before {
let after = reach_all(db, &tree).await;
for (name, was) in &before {
if doomed.iter().any(|d| d == name) {
continue;
}
let Some(is) = after.get(name) else { continue };
if was != is {
return Err(where_(format!(
"`{name}` believed {was:?} and now believes {is:?} — \
an archive is a move, not a retirement"
)));
}
}
}
}
Ok(())
}
use proptest::prelude::*;
fn key_strategy() -> impl Strategy<Value = usize> {
prop_oneof![5 => Just(0usize), 5 => Just(1usize), 1 => Just(2usize), 1 => Just(3usize)]
}
fn branch_strategy() -> impl Strategy<Value = usize> {
prop_oneof![3 => Just(0usize), 4 => Just(1usize), 2 => Just(2usize)]
}
pub fn op_strategy() -> impl Strategy<Value = Op> {
prop_oneof![
6 => (key_strategy(), branch_strategy(), any::<bool>())
.prop_map(|(key, branch, closed)| Op::Assert { key, branch, closed }),
3 => (key_strategy(), branch_strategy()).prop_map(|(key, branch)| Op::Retire { key, branch }),
3 => (prop_oneof![4 => Just(0usize), 1 => Just(1usize), 1 => Just(2usize)], 1..BRANCHES.len())
.prop_map(|(parent, child)| Op::Fork { parent, child }),
2 => Just(Op::Archive),
1 => (0..BRANCHES.len()).prop_map(|branch| Op::ArchiveBranch { branch }),
]
}
pub fn history_strategy() -> impl Strategy<Value = Vec<Op>> {
prop::collection::vec(op_strategy(), 6..20)
}