use ifc_model::{Conflict, Edit, Entity, EntityId, Model, Transaction, Value};
fn model() -> (Model, EntityId, EntityId) {
let mut model = Model::new();
let storey = model.push(Entity::new(
"IFCBUILDINGSTOREY",
vec![Value::Text("L1".into())],
));
let wall = model.push(Entity::new(
"IFCWALL",
vec![Value::Text("W1".into()), Value::Ref(storey)],
));
(model, storey, wall)
}
#[test]
fn a_valid_transaction_commits() {
let (mut model, storey, _wall) = model();
let before = model.len();
let mut tx = Transaction::new(&model);
let slab = tx.create(Entity::new("IFCSLAB", vec![Value::Ref(storey)]));
tx.set_attribute(storey, 0, Value::Text("Level 1".into()));
let applied = tx.commit(&mut model).expect("no conflicts");
assert_eq!(applied.created, vec![slab], "the new id is reported");
assert_eq!(model.len(), before + 1);
assert_eq!(
model.get(storey).unwrap().attribute(0).unwrap().as_text(),
Some("Level 1"),
"the attribute write landed"
);
}
#[test]
fn a_created_entity_is_referenceable_within_the_same_transaction() {
let (mut model, _storey, _wall) = model();
let mut tx = Transaction::new(&model);
let point = tx.create(Entity::new("IFCCARTESIANPOINT", vec![]));
let placement = tx.create(Entity::new("IFCAXIS2PLACEMENT3D", vec![Value::Ref(point)]));
let applied = tx.commit(&mut model).expect("forward reference resolves");
assert_eq!(applied.created, vec![point, placement]);
assert_eq!(
model
.get(placement)
.unwrap()
.attribute(0)
.unwrap()
.as_ref_id(),
Some(point)
);
}
#[test]
fn a_removal_that_would_dangle_is_refused() {
let (mut model, storey, wall) = model();
let before = model.len();
let mut tx = Transaction::new(&model);
tx.remove(storey);
let conflicts = tx.commit(&mut model).expect_err("the wall still refers");
assert_eq!(
conflicts,
vec![Conflict::RemovalWouldDangle {
edit: 0,
removed: storey,
referrer: wall,
slot: 1,
}],
"the surviving referrer and its slot are both named"
);
assert_eq!(model.len(), before, "a refused commit changes nothing");
}
#[test]
fn removing_referrer_and_target_together_is_allowed() {
let (mut model, storey, wall) = model();
let mut tx = Transaction::new(&model);
tx.remove(storey);
tx.remove(wall);
let applied = tx.commit(&mut model).expect("nothing survives to dangle");
assert_eq!(applied.removed.len(), 2);
assert!(model.is_empty());
}
#[test]
fn repointing_a_reference_permits_the_removal() {
let (mut model, storey, wall) = model();
let other = model.push(Entity::new(
"IFCBUILDINGSTOREY",
vec![Value::Text("L2".into())],
));
let mut tx = Transaction::new(&model);
tx.set_attribute(wall, 1, Value::Ref(other));
tx.remove(storey);
tx.commit(&mut model)
.expect("the wall now points elsewhere");
assert_eq!(
model.get(wall).unwrap().attribute(1).unwrap().as_ref_id(),
Some(other)
);
assert!(model.get(storey).is_none());
}
#[test]
fn a_dangling_reference_is_refused() {
let (mut model, _storey, wall) = model();
let mut tx = Transaction::new(&model);
tx.set_attribute(wall, 1, Value::Ref(EntityId(9999)));
let conflicts = tx.commit(&mut model).expect_err("target does not exist");
assert_eq!(
conflicts,
vec![Conflict::DanglingReference {
edit: 0,
from: wall,
slot: 1,
target: EntityId(9999),
}]
);
}
#[test]
fn references_nested_in_a_list_are_checked() {
let (mut model, storey, _wall) = model();
let mut tx = Transaction::new(&model);
tx.create(Entity::new(
"IFCRELAGGREGATES",
vec![Value::List(vec![
Value::Ref(storey),
Value::Ref(EntityId(4242)),
])],
));
let conflicts = tx.commit(&mut model).expect_err("one member is missing");
assert_eq!(conflicts.len(), 1, "only the bad member is reported");
assert!(matches!(
conflicts[0],
Conflict::DanglingReference {
target: EntityId(4242),
..
}
));
}
#[test]
fn editing_a_missing_entity_is_refused() {
let (mut model, _storey, _wall) = model();
let mut tx = Transaction::new(&model);
tx.set_attribute(EntityId(777), 0, Value::Integer(1));
let conflicts = tx.commit(&mut model).expect_err("no such entity");
assert_eq!(
conflicts,
vec![Conflict::MissingTarget {
edit: 0,
id: EntityId(777)
}]
);
}
#[test]
fn a_stale_transaction_is_refused() {
let (mut model, storey, _wall) = model();
let mut tx = Transaction::new(&model);
tx.set_attribute(storey, 0, Value::Text("Renamed".into()));
model.push(Entity::new("IFCSLAB", vec![]));
let conflicts = tx.commit(&mut model).expect_err("the model moved");
assert!(
matches!(conflicts[0], Conflict::StaleRevision { .. }),
"got {:?}",
conflicts[0]
);
assert_eq!(
model.get(storey).unwrap().attribute(0).unwrap().as_text(),
Some("L1"),
"the stale edit did not land"
);
}
#[test]
fn every_mutation_moves_the_revision() {
let (mut model, storey, wall) = model();
let start = model.revision();
model.set_attribute(storey, 0, Value::Text("a".into()));
let after_set = model.revision();
assert!(after_set > start, "set_attribute must bump");
model.retype(storey, "IFCSPACE");
let after_retype = model.revision();
assert!(after_retype > after_set, "retype must bump");
model.set_attributes(wall, [(0, Value::Text("b".into()))]);
let after_batch = model.revision();
assert!(after_batch > after_retype, "set_attributes must bump");
model.remove(wall);
assert!(model.revision() > after_batch, "remove must bump");
}
#[test]
fn preflight_reports_all_conflicts() {
let (model, _storey, wall) = model();
let mut tx = Transaction::new(&model);
tx.set_attribute(EntityId(500), 0, Value::Integer(1));
tx.set_attribute(wall, 1, Value::Ref(EntityId(501)));
tx.retype(EntityId(502), "IFCWALL");
let conflicts = tx.preflight(&model);
assert_eq!(conflicts.len(), 3, "got {conflicts:?}");
assert!(matches!(
conflicts[0],
Conflict::MissingTarget { edit: 0, .. }
));
assert!(matches!(
conflicts[1],
Conflict::DanglingReference { edit: 1, .. }
));
assert!(matches!(
conflicts[2],
Conflict::MissingTarget { edit: 2, .. }
));
}
#[test]
fn preflight_leaves_the_model_untouched() {
let (model, storey, _wall) = model();
let mut tx = Transaction::new(&model);
tx.set_attribute(storey, 0, Value::Text("changed".into()));
let revision = model.revision();
assert!(tx.preflight(&model).is_empty());
assert_eq!(model.revision(), revision, "preflight is read-only");
assert_eq!(
model.get(storey).unwrap().attribute(0).unwrap().as_text(),
Some("L1")
);
}
#[test]
fn removing_the_same_entity_twice_is_idempotent() {
let (mut model, storey, wall) = model();
let mut tx = Transaction::new(&model);
tx.remove(wall);
tx.remove(wall);
tx.remove(storey);
let applied = tx.commit(&mut model).expect("got {applied:?}");
assert_eq!(
applied.removed.len(),
2,
"the second removal is a no-op, not a second entry"
);
assert!(model.is_empty());
}
#[test]
fn a_repeated_conflict_is_reported_once() {
let (model, storey, wall) = model();
let mut tx = Transaction::new(&model);
tx.remove(storey);
tx.remove(storey);
let conflicts = tx.preflight(&model);
assert_eq!(
conflicts,
vec![Conflict::RemovalWouldDangle {
edit: 0,
removed: storey,
referrer: wall,
slot: 1,
}],
"one problem, one conflict"
);
}
#[test]
fn a_removal_returns_the_entity() {
let (mut model, storey, wall) = model();
let mut tx = Transaction::new(&model);
tx.remove(wall);
tx.remove(storey);
let applied = tx.commit(&mut model).expect("both go");
let names: Vec<_> = applied
.removed
.iter()
.map(|(_, e)| e.type_name.to_string())
.collect();
assert_eq!(names, vec!["IFCWALL", "IFCBUILDINGSTOREY"]);
}
#[test]
fn referencing_a_removed_entity_is_refused() {
let (mut model, storey, wall) = model();
let other = model.push(Entity::new(
"IFCBUILDINGSTOREY",
vec![Value::Text("L2".into())],
));
let mut tx = Transaction::new(&model);
tx.set_attribute(wall, 1, Value::Ref(other));
tx.remove(other);
let conflicts = tx.commit(&mut model).expect_err("the target is going away");
assert!(
conflicts.iter().any(|c| matches!(
c,
Conflict::DanglingReference {
target,
..
} if *target == other
)),
"got {conflicts:?}"
);
assert!(model.get(other).is_some(), "nothing was applied");
assert_eq!(
model.get(wall).unwrap().attribute(1).unwrap().as_ref_id(),
Some(storey),
"the wall still points at its original storey"
);
}
#[test]
fn a_create_over_an_existing_id_is_refused() {
let (mut model, storey, _wall) = model();
let before = model.get(storey).unwrap().clone();
let mut tx = Transaction::new(&model);
tx.stage(Edit::Create {
id: storey,
entity: Entity::new("IFCSLAB", vec![]),
});
let conflicts = tx.commit(&mut model).expect_err("that id is taken");
assert_eq!(
conflicts,
vec![Conflict::IdAlreadyExists {
edit: 0,
id: storey
}]
);
assert_eq!(
model.get(storey).unwrap(),
&before,
"the existing entity is untouched"
);
}