use std::error::Error;
use std::path::Path;
use haematite::db::DatabaseConfig;
use haematite::store::NodeCacheBudget;
use super::{MergePin, MergePlan, MergeState, Storage, holder_name};
use crate::serve::protocol::{Change, Fork, Mutation, Selection, Version};
type TestResult<T = ()> = Result<T, Box<dyn Error>>;
fn create(path: &Path) -> TestResult<Storage> {
Ok(Storage::create(
DatabaseConfig {
data_dir: path.to_path_buf(),
shard_count: 1,
distributed: None,
executor_threads: Some(1),
node_cache_budget: Some(NodeCacheBudget::Unlimited),
retention: haematite::Retention::KeepAll,
},
NodeCacheBudget::Unlimited,
fixture_long_wait_interval(),
)?)
}
fn live(storage: &Storage, branch: &str) -> TestResult<Version> {
Ok(storage
.capture(&Selection {
branch: branch.to_owned(),
version: None,
})?
.version)
}
fn holders(storage: &Storage, id: &str) -> usize {
["origin", "source", "target"]
.iter()
.filter(|slot| storage.refs.get(&holder_name(id, slot)).is_some())
.count()
}
fn refusal(result: Result<MergePlan, super::Failure>, accepted: &str) -> TestResult<String> {
Ok(result.err().ok_or(accepted)?.code)
}
fn state(storage: &Storage, id: &str) -> TestResult<MergeState> {
Ok(storage.merge_plan(id)?.ok_or("lost merge row")?.state)
}
#[test]
fn merge_holders_survive_reopen_and_release_after_abandon() -> TestResult {
let directory = tempfile::tempdir()?;
let path = directory.path().join("database");
let mut storage = create(&path)?;
let target = MergePin {
branch: "main".to_owned(),
version: live(&storage, "main")?,
};
let fork = storage.mutate(&Mutation {
request_id: "fork".to_owned(),
branch: target.branch.clone(),
expected: target.version,
change: Change::Fork(Fork {
name: "feature".to_owned(),
from: target.version,
}),
})?;
let source = MergePin {
branch: fork.branch,
version: fork.version,
};
let plan = storage.merge_open("review", &source, &target)?;
assert_eq!(plan.state, MergeState::Open);
assert_eq!(plan.origin, target);
assert_eq!(storage.merge_open("review", &source, &target)?, plan);
assert_eq!(holders(&storage, "review"), 3);
assert_eq!(live(&storage, "main")?, target.version);
let mut different = source.clone();
different.version.sequence += 1;
assert_eq!(
refusal(
storage.merge_open("review", &different, &target),
"identity change accepted"
)?,
"merge_identity_conflict"
);
assert_eq!(
refusal(
storage.merge_open("moved", &different, &target),
"moved pin accepted"
)?,
"merge_pins_moved"
);
let mut interrupted = storage.merge_open("interrupted", &source, &target)?;
interrupted.state = MergeState::Abandoned;
storage.write_merge_row(&interrupted)?;
drop(storage);
let mut storage = Storage::open(
&path,
NodeCacheBudget::Unlimited,
fixture_long_wait_interval(),
)?;
assert_eq!(storage.merge_plan("review")?.ok_or("lost plan")?, plan);
assert_eq!(state(&storage, "interrupted")?, MergeState::Abandoned);
assert_eq!(holders(&storage, "interrupted"), 3);
storage.merge_abandon("interrupted")?;
assert_eq!(holders(&storage, "interrupted"), 0);
assert_eq!(
storage.merge_abandon("review")?.state,
MergeState::Abandoned
);
assert_eq!(holders(&storage, "review"), 0);
assert_eq!(
storage.merge_abandon("review")?.state,
MergeState::Abandoned
);
assert_eq!(
refusal(
storage.merge_open("review", &source, &target),
"abandoned plan reopened"
)?,
"merge_abandoned"
);
drop(storage);
let storage = Storage::open(
&path,
NodeCacheBudget::Unlimited,
fixture_long_wait_interval(),
)?;
assert_eq!(state(&storage, "review")?, MergeState::Abandoned);
Ok(())
}
const fn fixture_long_wait_interval() -> std::time::Duration {
const LONG_WAIT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
LONG_WAIT_INTERVAL
}