use lex_store::{Operation, OperationKind, StageTransition, Store, DEFAULT_BRANCH};
use std::collections::BTreeSet;
fn fresh() -> (Store, tempfile::TempDir) {
let tmp = tempfile::tempdir().unwrap();
let s = Store::open(tmp.path()).unwrap();
(s, tmp)
}
fn add(s: &Store, branch: &str, sig: &str, stg: &str) -> String {
let op = Operation::new(
OperationKind::AddFunction {
sig_id: sig.into(),
stage_id: stg.into(),
effects: BTreeSet::new(),
budget_cost: None,
},
s.get_branch(branch).unwrap().and_then(|b| b.head_op).into_iter().collect::<Vec<_>>(),
);
let t = StageTransition::Create { sig_id: sig.into(), stage_id: stg.into() };
s.apply_operation(branch, op, t).unwrap()
}
fn modify(s: &Store, branch: &str, sig: &str, from: &str, to: &str) -> String {
let parent = s.get_branch(branch).unwrap().and_then(|b| b.head_op).unwrap();
let op = Operation::new(
OperationKind::ModifyBody {
sig_id: sig.into(),
from_stage_id: from.into(),
to_stage_id: to.into(),
from_budget: None,
to_budget: None,
},
[parent],
);
let t = StageTransition::Replace {
sig_id: sig.into(), from: from.into(), to: to.into(),
};
s.apply_operation(branch, op, t).unwrap()
}
#[test]
fn fresh_store_lists_only_main() {
let (s, _tmp) = fresh();
assert_eq!(s.list_branches().unwrap(), vec![DEFAULT_BRANCH.to_string()]);
assert_eq!(s.current_branch(), DEFAULT_BRANCH);
}
#[test]
fn create_branch_inherits_head_op() {
let (s, _tmp) = fresh();
let _ = add(&s, DEFAULT_BRANCH, "sig1", "stageA");
s.create_branch("feature-x", DEFAULT_BRANCH).unwrap();
assert_eq!(
s.branch_head("feature-x").unwrap().get("sig1"),
Some(&"stageA".to_string()),
);
}
#[test]
fn merge_clean_when_only_one_side_modifies() {
let (s, _tmp) = fresh();
let _ = add(&s, DEFAULT_BRANCH, "sig1", "stageA");
s.create_branch("feature", DEFAULT_BRANCH).unwrap();
let _ = modify(&s, "feature", "sig1", "stageA", "stageB");
let report = s.merge("feature", DEFAULT_BRANCH).unwrap();
assert_eq!(report.conflicts.len(), 0, "report: {report:?}");
assert_eq!(report.merged.len(), 1);
assert_eq!(report.merged[0].stage_id, "stageB");
}
#[test]
fn merge_conflict_when_both_sides_modify_same_sig() {
let (s, _tmp) = fresh();
let _ = add(&s, DEFAULT_BRANCH, "sig1", "stageA");
s.create_branch("feature", DEFAULT_BRANCH).unwrap();
let _ = modify(&s, DEFAULT_BRANCH, "sig1", "stageA", "stageB");
let _ = modify(&s, "feature", "sig1", "stageA", "stageC");
let report = s.merge("feature", DEFAULT_BRANCH).unwrap();
assert_eq!(report.conflicts.len(), 1);
assert_eq!(report.conflicts[0].kind, "modify-modify");
}
fn real_stage(src: &str, name: &str) -> (lex_ast::Stage, String, String) {
let st = lex_ast::canonicalize_program(&lex_syntax::parse_source(src).unwrap())
.into_iter()
.find(|s| matches!(s, lex_ast::Stage::FnDecl(fd) if fd.name == name))
.expect("fn not found");
let sig = lex_ast::sig_id(&st).unwrap();
let stg = lex_ast::stage_id(&st).unwrap();
(st, sig, stg)
}
#[test]
fn commit_merge_advances_dst_head_op() {
let (s, _tmp) = fresh();
let (st_a, sig, stg_a) = real_stage("fn foo(n :: Int) -> Int { n }\n", "foo");
s.publish(&st_a).unwrap();
let _ = add(&s, DEFAULT_BRANCH, &sig, &stg_a);
s.create_branch("feature", DEFAULT_BRANCH).unwrap();
let (st_b, _, stg_b) = real_stage("fn foo(n :: Int) -> Int { n + 1 }\n", "foo");
s.publish(&st_b).unwrap();
let _ = modify(&s, "feature", &sig, &stg_a, &stg_b);
let report = s.merge("feature", DEFAULT_BRANCH).unwrap();
s.commit_merge(DEFAULT_BRANCH, &report).unwrap();
assert_eq!(s.branch_head(DEFAULT_BRANCH).unwrap().get(&sig), Some(&stg_b));
assert_eq!(s.branch_log(DEFAULT_BRANCH).unwrap().len(), 1);
}
#[test]
fn commit_merge_refuses_a_merge_naming_an_unpublished_stage() {
let (s, _tmp) = fresh();
let _ = add(&s, DEFAULT_BRANCH, "sig1", "stageA");
s.create_branch("feature", DEFAULT_BRANCH).unwrap();
let _ = modify(&s, "feature", "sig1", "stageA", "stageB");
let head_before = s.get_branch(DEFAULT_BRANCH).unwrap().unwrap().head_op;
let report = s.merge("feature", DEFAULT_BRANCH).unwrap();
assert!(s.commit_merge(DEFAULT_BRANCH, &report).is_err());
assert_eq!(s.get_branch(DEFAULT_BRANCH).unwrap().unwrap().head_op, head_before);
}
#[test]
fn delete_branch_refused_when_current_or_default() {
let (s, _tmp) = fresh();
s.create_branch("foo", DEFAULT_BRANCH).unwrap();
s.set_current_branch("foo").unwrap();
assert!(s.delete_branch("foo").is_err());
assert!(s.delete_branch(DEFAULT_BRANCH).is_err());
s.set_current_branch(DEFAULT_BRANCH).unwrap();
s.delete_branch("foo").unwrap();
assert_eq!(s.list_branches().unwrap(), vec![DEFAULT_BRANCH.to_string()]);
}
fn remove(s: &Store, branch: &str, sig: &str, last: &str) -> String {
let parent = s.get_branch(branch).unwrap().and_then(|b| b.head_op).unwrap();
let op = Operation::new(
OperationKind::RemoveFunction {
sig_id: sig.into(),
last_stage_id: last.into(),
},
[parent],
);
let t = StageTransition::Remove { sig_id: sig.into(), last: last.into() };
s.apply_operation(branch, op, t).unwrap()
}
#[test]
fn merge_into_empty_dst_fast_forwards() {
let (s, _tmp) = fresh();
s.create_branch("feature", DEFAULT_BRANCH).unwrap();
let _ = add(&s, "feature", "sig1", "stageA");
let report = s.merge("feature", DEFAULT_BRANCH).unwrap();
s.commit_merge(DEFAULT_BRANCH, &report).unwrap();
assert_eq!(
s.branch_head(DEFAULT_BRANCH).unwrap().get("sig1"),
Some(&"stageA".to_string()),
);
let head_op = s.get_branch(DEFAULT_BRANCH).unwrap().and_then(|b| b.head_op);
assert!(head_op.is_some(), "main should now have a head_op");
}
#[test]
fn merge_modify_delete_conflict() {
let (s, _tmp) = fresh();
let _ = add(&s, DEFAULT_BRANCH, "sig1", "stageA");
s.create_branch("feature", DEFAULT_BRANCH).unwrap();
let _ = modify(&s, "feature", "sig1", "stageA", "stageB"); let _ = remove(&s, DEFAULT_BRANCH, "sig1", "stageA"); let report = s.merge("feature", DEFAULT_BRANCH).unwrap();
assert_eq!(report.conflicts.len(), 1);
assert_eq!(report.conflicts[0].kind, "modify-delete");
}
#[test]
fn merge_delete_modify_conflict() {
let (s, _tmp) = fresh();
let _ = add(&s, DEFAULT_BRANCH, "sig1", "stageA");
s.create_branch("feature", DEFAULT_BRANCH).unwrap();
let _ = remove(&s, "feature", "sig1", "stageA"); let _ = modify(&s, DEFAULT_BRANCH, "sig1", "stageA", "stageB"); let report = s.merge("feature", DEFAULT_BRANCH).unwrap();
assert_eq!(report.conflicts.len(), 1);
assert_eq!(report.conflicts[0].kind, "delete-modify");
}
#[test]
fn merge_add_add_conflict() {
let (s, _tmp) = fresh();
s.create_branch("feature", DEFAULT_BRANCH).unwrap();
let _ = add(&s, "feature", "newsig", "stageA"); let _ = add(&s, DEFAULT_BRANCH, "newsig", "stageB"); let report = s.merge("feature", DEFAULT_BRANCH).unwrap();
assert_eq!(report.conflicts.len(), 1);
assert_eq!(report.conflicts[0].kind, "add-add");
}