use std::error::Error;
use super::commit::{BranchCommitError, CommitDurability, CommitRequest, commit_branch, merge};
use super::conflict::ConflictPolicy;
use super::fork::{create_branch_with_kind, fork_from_with_kind};
use super::handle::BranchHandle;
use super::lifecycle::{create_branch, open_branch};
use super::policy::{BranchKind, BranchPolicyError, check_fork_into};
use super::refstore::{BranchRefRecord, BranchRefStore, BranchShardRef};
use super::registry::BranchRegistry;
use crate::store::MemoryStore;
use crate::tree::{Hash, LeafNode, Node};
#[path = "kind_marker_matrix_cases.rs"]
mod cases;
use cases::{Disposition, FORK_CASES, MERGE_CASES, MatrixCase};
#[test]
fn branch_kind_fork_into_matrix_is_exhaustive() -> Result<(), Box<dyn Error>> {
assert_eq!(FORK_CASES.len(), 12);
for case in FORK_CASES {
let (source, destination) = identity_names(case)?;
let result = check_fork_into(
&source,
case.source_kind,
case.source_lineage,
&destination,
case.destination_kind,
case.destination_lineage,
);
assert_policy_result(case, &source, &destination, result)?;
let route_can_represent_cell = case.destination_kind == BranchKind::Namespace
|| case.source_lineage == case.destination_lineage;
assert_eq!(
case.synthetic_reason.is_none(),
route_can_represent_cell,
"{} fixture provenance is stale: {:?}",
case.cell,
case.synthetic_reason
);
if let Some(reason) = case.synthetic_reason {
exercise_synthetic_fork_relation(case, reason)?;
} else {
exercise_fork_route(case)?;
}
}
Ok(())
}
#[test]
fn branch_kind_merge_matrix_is_exhaustive() -> Result<(), Box<dyn Error>> {
assert_eq!(MERGE_CASES.len(), 12);
for case in MERGE_CASES {
assert!(
case.synthetic_reason.is_none(),
"{} unexpectedly uses a synthetic fixture: {:?}",
case.cell,
case.synthetic_reason
);
exercise_merge_route(case)?;
}
Ok(())
}
fn exercise_fork_route(case: MatrixCase) -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let registry = BranchRegistry::new();
let mut refs = BranchRefStore::open(dir.path())?;
let root = Hash::from_bytes([case.cell.as_bytes()[2]; 32]);
let (source_name, destination_name) = identity_names(case)?;
let source = create_identity(
&source_name,
case.source_kind,
case.source_lineage,
root,
&mut refs,
®istry,
1,
)?;
let source_before = refs.get(&source_name).cloned();
let destination_before = refs.get(&destination_name).cloned();
let result = fork_from_with_kind(
&source,
&destination_name,
case.destination_kind,
&mut refs,
®istry,
2,
);
if case.disposition == Disposition::Allow {
let child =
result.map_err(|error| format!("{} unexpectedly refused: {error}", case.cell))?;
let landed = refs
.get(&destination_name)
.ok_or("fork did not install child")?;
assert_eq!(child.current_root(), root, "{} handle root", case.cell);
assert_eq!(landed.shards[0].head, root, "{} durable head", case.cell);
assert_eq!(
landed.resolved_namespace_lineage(),
case.destination_lineage,
"{} lineage",
case.cell
);
} else {
let policy = policy_from_result(result, case.cell)?;
assert_policy_result(case, &source_name, &destination_name, Err(policy))?;
assert_eq!(
refs.get(&source_name).cloned(),
source_before,
"{} source changed",
case.cell
);
assert_eq!(
refs.get(&destination_name).cloned(),
destination_before,
"{} destination changed",
case.cell
);
}
Ok(())
}
fn exercise_synthetic_fork_relation(case: MatrixCase, reason: &str) -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let mut refs = BranchRefStore::open(dir.path())?;
let root = Hash::from_bytes([case.cell.as_bytes()[2]; 32]);
let (source_name, destination_name) = identity_names(case)?;
refs.create(record(
&source_name,
1,
case.source_kind,
stored_lineage(case.source_kind, case.source_lineage),
root,
root,
))
.map_err(|error| format!("{} synthetic fixture ({reason}) source: {error}", case.cell))?;
refs.create(record(
&destination_name,
2,
case.destination_kind,
stored_lineage(case.destination_kind, case.destination_lineage),
root,
root,
))
.map_err(|error| {
format!(
"{} synthetic fixture ({reason}) destination: {error}",
case.cell
)
})?;
let source_before = refs.get(&source_name).cloned();
let destination_before = refs.get(&destination_name).cloned();
let result = check_fork_into(
&source_name,
case.source_kind,
case.source_lineage,
&destination_name,
case.destination_kind,
case.destination_lineage,
);
assert_policy_result(case, &source_name, &destination_name, result)
.map_err(|error| format!("{} synthetic fixture ({reason}): {error}", case.cell))?;
assert_eq!(
refs.get(&source_name).cloned(),
source_before,
"{} synthetic fixture ({reason}) changed source",
case.cell
);
assert_eq!(
refs.get(&destination_name).cloned(),
destination_before,
"{} synthetic fixture ({reason}) changed destination",
case.cell
);
Ok(())
}
fn exercise_merge_route(case: MatrixCase) -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let registry = BranchRegistry::new();
let mut refs = BranchRefStore::open(dir.path())?;
let mut store = MemoryStore::new();
let anchor = store.put(&Node::Leaf(LeafNode::new(Vec::new())?));
let (source_name, destination_name) = identity_names(case)?;
let source = create_identity(
&source_name,
case.source_kind,
case.source_lineage,
anchor,
&mut refs,
®istry,
1,
)?;
let destination = create_identity(
&destination_name,
case.destination_kind,
case.destination_lineage,
anchor,
&mut refs,
®istry,
10,
)?;
if case.source_kind == BranchKind::Work {
source.put(0, b"source", case.cell.as_bytes())?;
commit_branch(
&source,
&mut store,
®istry,
CommitRequest {
durability: CommitDurability::Durable { refs: &mut refs },
extra_parents: &[],
timestamp: 20,
},
)?;
}
let source_head = source.current_root();
let destination_record_before = refs.get(&destination_name).cloned();
let destination_head_before = destination.current_root();
let result = merge(
&mut store,
&source,
&destination,
&refs,
&ConflictPolicy::Lww,
);
if case.disposition == Disposition::Allow {
let report =
result.map_err(|error| format!("{} unexpectedly refused: {error}", case.cell))?;
assert_eq!(
report.merged_root, source_head,
"{} did not land source root",
case.cell
);
} else {
let policy = policy_from_result(result, case.cell)?;
assert_policy_result(case, &source_name, &destination_name, Err(policy))?;
assert_eq!(
refs.get(&destination_name).cloned(),
destination_record_before,
"{} destination record changed",
case.cell
);
assert_eq!(
destination.current_root(),
destination_head_before,
"{} destination head changed",
case.cell
);
}
Ok(())
}
fn assert_policy_result(
case: MatrixCase,
source: &str,
destination: &str,
result: Result<(), BranchPolicyError>,
) -> Result<(), Box<dyn Error>> {
match (case.disposition, result) {
(Disposition::Allow, Ok(())) => {}
(
Disposition::ForkAcross,
Err(BranchPolicyError::ForkAcrossNamespaceBoundary {
source: actual_source,
source_kind,
source_lineage,
destination: actual_destination,
destination_kind,
destination_lineage,
}),
)
| (
Disposition::MergeAcross,
Err(BranchPolicyError::MergeAcrossNamespaceBoundary {
source: actual_source,
source_kind,
source_lineage,
destination: actual_destination,
destination_kind,
destination_lineage,
}),
) => {
assert_eq!(actual_source, source, "{} source", case.cell);
assert_eq!(source_kind, case.source_kind, "{} source kind", case.cell);
assert_eq!(
source_lineage.as_deref(),
case.source_lineage,
"{} source lineage",
case.cell
);
assert_eq!(actual_destination, destination, "{} destination", case.cell);
assert_eq!(
destination_kind, case.destination_kind,
"{} destination kind",
case.cell
);
assert_eq!(
destination_lineage.as_deref(),
case.destination_lineage,
"{} destination lineage",
case.cell
);
}
(
Disposition::NamespaceFork,
Err(BranchPolicyError::NamespaceFork {
source: actual_source,
source_kind,
destination: actual_destination,
destination_kind,
}),
)
| (
Disposition::NamespaceMerge,
Err(BranchPolicyError::NamespaceMerge {
source: actual_source,
source_kind,
destination: actual_destination,
destination_kind,
}),
) => {
assert_eq!(actual_source, source, "{} source", case.cell);
assert_eq!(source_kind, case.source_kind, "{} source kind", case.cell);
assert_eq!(actual_destination, destination, "{} destination", case.cell);
assert_eq!(
destination_kind, case.destination_kind,
"{} destination kind",
case.cell
);
}
(expected, actual) => {
return Err(format!("{} expected {expected:?}, got {actual:?}", case.cell).into());
}
}
Ok(())
}
fn policy_from_result<T>(
result: Result<T, BranchCommitError>,
cell: &str,
) -> Result<BranchPolicyError, Box<dyn Error>> {
match result {
Err(BranchCommitError::Policy(policy)) => Ok(policy),
Err(other) => Err(format!("{cell} returned non-policy error: {other}").into()),
Ok(_) => Err(format!("{cell} unexpectedly succeeded").into()),
}
}
fn identity_names(case: MatrixCase) -> Result<(String, String), Box<dyn Error>> {
let source = if case.source_kind == BranchKind::Namespace {
case.source_lineage
.ok_or_else(|| format!("{} Namespace source has no resolved name", case.cell))?
.to_owned()
} else {
format!("source-{}", case.cell)
};
let destination = if case.destination_kind == BranchKind::Namespace {
case.destination_lineage
.ok_or_else(|| format!("{} Namespace destination has no resolved name", case.cell))?
.to_owned()
} else {
format!("destination-{}", case.cell)
};
Ok((source, destination))
}
fn create_identity(
name: &str,
kind: BranchKind,
resolved_lineage: Option<&str>,
root: Hash,
refs: &mut BranchRefStore,
registry: &BranchRegistry,
timestamp: u64,
) -> Result<BranchHandle, Box<dyn Error>> {
if let Some(record) = refs.get(name) {
if record.kind != kind || record.resolved_namespace_lineage() != resolved_lineage {
return Err(format!(
"existing production fixture {name} is {:?}/{:?}, requested {kind:?}/{resolved_lineage:?}",
record.kind,
record.resolved_namespace_lineage()
)
.into());
}
return Ok(open_branch(name, refs, registry)?);
}
match (kind, resolved_lineage) {
(BranchKind::Namespace, Some(lineage)) if lineage == name => Ok(create_branch_with_kind(
name,
[(0, root)],
kind,
refs,
registry,
timestamp,
)?),
(BranchKind::Namespace, _) => {
Err(format!("Namespace production fixture {name} must resolve to its own name").into())
}
(BranchKind::Work, None) => {
Ok(create_branch(name, [(0, root)], refs, registry, timestamp)?)
}
(BranchKind::Work, Some(lineage)) => {
let namespace = create_identity(
lineage,
BranchKind::Namespace,
Some(lineage),
root,
refs,
registry,
timestamp,
)?;
Ok(fork_from_with_kind(
&namespace,
name,
BranchKind::Work,
refs,
registry,
timestamp + 1,
)?)
}
}
}
fn stored_lineage(kind: BranchKind, resolved: Option<&str>) -> Option<String> {
match kind {
BranchKind::Namespace => None,
BranchKind::Work => resolved.map(str::to_owned),
}
}
fn record(
name: &str,
created: u64,
kind: BranchKind,
namespace_lineage: Option<String>,
anchor: Hash,
head: Hash,
) -> BranchRefRecord {
BranchRefRecord {
name: name.to_owned(),
created,
kind,
namespace_lineage,
seq: 0,
timestamp: created,
shards: vec![BranchShardRef {
shard_id: 0,
fork_anchor: anchor,
head,
}],
parents: Vec::new(),
}
}