use ifc_model::{Model, Transaction, Value};
use ifc_properties::{
add_complex_property_template, add_door_lining_properties, add_door_panel_properties,
add_permeable_covering_properties, add_property_dependency_relationship,
add_property_enumeration, add_window_lining_properties, add_window_panel_properties,
DoorLiningDraft, PropertyError, WindowLiningDraft,
};
use ifc_schema::{ifc4, ifc4x3};
const GUID: &str = "0EI0MSHbX9gg8Fxwar7lb8";
fn invalid(err: &PropertyError) -> bool {
matches!(err, PropertyError::AuthoringInvalid { .. })
}
#[test]
fn written_arities_match_the_schema() {
const EXPECTED: &[(&str, usize)] = &[
("IfcDoorLiningProperties", 17),
("IfcWindowLiningProperties", 16),
("IfcDoorPanelProperties", 9),
("IfcWindowPanelProperties", 9),
("IfcPermeableCoveringProperties", 9),
("IfcComplexPropertyTemplate", 7),
("IfcPropertyDependencyRelationship", 5),
("IfcPropertyEnumeration", 3),
];
for schema in [ifc4(), ifc4x3()] {
for (entity, arity) in EXPECTED {
let declared = schema.attributes(entity);
if declared.is_empty() {
continue;
}
assert_eq!(declared.len(), *arity, "{entity} in {}", schema.name());
}
}
}
#[test]
fn a_door_depth_without_its_thickness_is_refused() {
let model = Model::new();
let mut tx = Transaction::new(&model);
let err = add_door_lining_properties(&mut tx, GUID, DoorLiningDraft::new().lining_depth(0.1))
.expect_err("WR31");
assert!(invalid(&err), "{err}");
let err =
add_door_lining_properties(&mut tx, GUID, DoorLiningDraft::new().threshold_depth(0.1))
.expect_err("WR32");
assert!(invalid(&err), "{err}");
add_door_lining_properties(
&mut tx,
GUID,
DoorLiningDraft::new()
.lining_depth(0.1)
.lining_thickness(0.02),
)
.expect("a depth with its thickness is legal");
}
#[test]
fn door_transom_and_casing_pairs_are_all_or_nothing() {
let model = Model::new();
let mut tx = Transaction::new(&model);
for draft in [
DoorLiningDraft::new().transom_offset(0.5),
DoorLiningDraft::new().transom_thickness(0.02),
DoorLiningDraft::new().casing_depth(0.1),
DoorLiningDraft::new().casing_thickness(0.02),
] {
let err = add_door_lining_properties(&mut tx, GUID, draft).expect_err("half a pair");
assert!(invalid(&err), "{err}");
}
add_door_lining_properties(
&mut tx,
GUID,
DoorLiningDraft::new()
.transom_offset(0.5)
.transom_thickness(0.02)
.casing_depth(0.1)
.casing_thickness(0.02),
)
.expect("complete pairs are legal");
}
#[test]
fn window_offsets_are_ordered_not_paired() {
let model = Model::new();
let mut tx = Transaction::new(&model);
add_window_lining_properties(
&mut tx,
GUID,
WindowLiningDraft::new().first_transom_offset(0.25),
)
.expect("a first offset alone is legal on a window");
for draft in [
WindowLiningDraft::new().second_transom_offset(0.75),
WindowLiningDraft::new().second_mullion_offset(0.75),
] {
let err = add_window_lining_properties(&mut tx, GUID, draft)
.expect_err("a second offset without the first");
assert!(invalid(&err), "{err}");
}
add_window_lining_properties(
&mut tx,
GUID,
WindowLiningDraft::new()
.first_transom_offset(0.25)
.second_transom_offset(0.75)
.first_mullion_offset(0.3)
.second_mullion_offset(0.6),
)
.expect("both offsets in order are legal");
}
#[test]
fn window_offsets_are_bounded_ratios() {
let model = Model::new();
let mut tx = Transaction::new(&model);
let err = add_window_lining_properties(
&mut tx,
GUID,
WindowLiningDraft::new().first_transom_offset(1.5),
)
.expect_err("IfcNormalisedRatioMeasure is bounded to [0, 1]");
assert!(invalid(&err), "{err}");
}
#[test]
fn length_measures_keep_their_own_sign_rules() {
let model = Model::new();
let mut tx = Transaction::new(&model);
add_door_lining_properties(
&mut tx,
GUID,
DoorLiningDraft::new()
.lining_offset(-0.01)
.threshold_offset(-0.02),
)
.expect("IfcLengthMeasure admits a negative offset");
for draft in [
DoorLiningDraft::new()
.lining_depth(0.0)
.lining_thickness(0.01),
DoorLiningDraft::new()
.lining_depth(0.1)
.lining_thickness(-0.01),
] {
let err = add_door_lining_properties(&mut tx, GUID, draft)
.expect_err("a non-positive depth or negative thickness");
assert!(invalid(&err), "{err}");
}
}
#[test]
fn panel_enumerations_are_per_entity() {
let mut model = Model::new();
let mut tx = Transaction::new(&model);
let id = add_door_panel_properties(
&mut tx,
GUID,
Some("Leaf"),
"SWINGING",
"LEFT",
(Some(0.04), Some(0.5)),
)
.expect("a door panel with legal tokens");
tx.commit(&mut model).expect("commits");
let entity = model.get(id).expect("staged");
assert_eq!(entity.attributes[5], Value::Enum("SWINGING".into()));
assert_eq!(entity.attributes[7], Value::Enum("LEFT".into()));
let mut tx = Transaction::new(&model);
let err = add_window_panel_properties(&mut tx, GUID, None, "USERDEFINED", "TOP", (None, None))
.expect_err("IfcWindowPanelOperationEnum has no USERDEFINED");
assert!(invalid(&err), "{err}");
add_permeable_covering_properties(&mut tx, GUID, None, "USERDEFINED", "TOP", (None, None))
.expect("the permeable covering enum does have USERDEFINED");
let err = add_door_panel_properties(&mut tx, GUID, None, "SWINGING", "TOP", (None, None))
.expect_err("TOP is a window position, not a door one");
assert!(invalid(&err), "{err}");
}
#[test]
fn panel_width_is_a_normalised_ratio() {
let model = Model::new();
let mut tx = Transaction::new(&model);
let err =
add_door_panel_properties(&mut tx, GUID, None, "SLIDING", "LEFT", (None, Some(900.0)))
.expect_err("900 is a millimetre reading, not a ratio");
assert!(invalid(&err), "{err}");
add_door_panel_properties(&mut tx, GUID, None, "SLIDING", "LEFT", (None, Some(0.9)))
.expect("a fraction is legal");
}
#[test]
fn a_malformed_guid_is_refused() {
let model = Model::new();
let mut tx = Transaction::new(&model);
let err = add_door_lining_properties(&mut tx, "nope", DoorLiningDraft::default())
.expect_err("four characters is not a GUID");
assert!(invalid(&err), "{err}");
assert!(tx.is_empty(), "nothing is staged when the GUID fails");
}
#[test]
fn enumeration_values_must_share_one_type() {
let model = Model::new();
let mut tx = Transaction::new(&model);
add_property_enumeration(&mut tx, "Widths", vec![length(0.9), length(1.2)], None)
.expect("one measure throughout is legal");
let err = add_property_enumeration(
&mut tx,
"Mixed",
vec![
length(0.9),
typed("IFCPOSITIVELENGTHMEASURE", Value::Real(1.2)),
],
None,
)
.expect_err("two measures are different types");
assert!(invalid(&err), "{err}");
let err = add_property_enumeration(
&mut tx,
"Mixed",
vec![label("a"), typed("IFCINTEGER", Value::Integer(1))],
None,
)
.expect_err("text among integers");
assert!(invalid(&err), "{err}");
}
#[test]
fn enumeration_values_are_typed_parameters() {
let model = Model::new();
let mut tx = Transaction::new(&model);
let err = add_property_enumeration(&mut tx, "Mixed", vec![length(0.9), Value::Real(1.2)], None)
.expect_err("a bare real names no measure");
assert!(
matches!(
err,
PropertyError::ValueForm {
attribute: "EnumerationValues",
typed_required: true,
..
}
),
"{err}"
);
assert!(tx.is_empty(), "nothing is staged when a value is bare");
}
#[test]
fn enumeration_values_are_unique_and_named() {
let model = Model::new();
let mut tx = Transaction::new(&model);
let integer = |v: i64| typed("IFCINTEGER", Value::Integer(v));
let err = add_property_enumeration(&mut tx, "Sizes", Vec::new(), None)
.expect_err("EnumerationValues is LIST [1:?]");
assert!(invalid(&err), "{err}");
let err = add_property_enumeration(&mut tx, " ", vec![integer(1)], None)
.expect_err("UR1 makes Name the key, so it cannot be blank");
assert!(invalid(&err), "{err}");
let err = add_property_enumeration(&mut tx, "Sizes", vec![integer(1), integer(1)], None)
.expect_err("the list is UNIQUE");
assert!(invalid(&err), "{err}");
add_property_enumeration(&mut tx, "Sizes", vec![integer(1), integer(2)], None)
.expect("distinct values of one type are legal");
}
fn typed(type_name: &str, value: Value) -> Value {
Value::Typed {
type_name: type_name.into(),
value: Box::new(value),
}
}
fn length(value: f64) -> Value {
typed("IFCLENGTHMEASURE", Value::Real(value))
}
fn label(text: &str) -> Value {
typed("IFCLABEL", Value::Text(text.into()))
}
#[test]
fn a_property_cannot_depend_on_itself() {
let mut model = Model::new();
let a = model.push(ifc_model::Entity::new("IFCPROPERTYSINGLEVALUE", vec![]));
let b = model.push(ifc_model::Entity::new("IFCPROPERTYSINGLEVALUE", vec![]));
let mut tx = Transaction::new(&model);
let err = add_property_dependency_relationship(&mut tx, None, None, (a, a), None)
.expect_err("a self-dependency loops any dependency walk");
assert!(invalid(&err), "{err}");
add_property_dependency_relationship(&mut tx, Some("derives"), None, (a, b), Some("x*2"))
.expect("two distinct properties are legal");
}
#[test]
fn complex_template_children_are_uniquely_named() {
let mut model = Model::new();
let a = model.push(ifc_model::Entity::new("IFCSIMPLEPROPERTYTEMPLATE", vec![]));
let b = model.push(ifc_model::Entity::new("IFCSIMPLEPROPERTYTEMPLATE", vec![]));
let mut tx = Transaction::new(&model);
let err = add_complex_property_template(
&mut tx,
GUID,
Some("Assembly"),
(None, None),
&[("Width", a), ("Width", b)],
)
.expect_err("UniquePropertyTemplateNames");
assert!(invalid(&err), "{err}");
let err = add_complex_property_template(
&mut tx,
GUID,
None,
(None, Some("X_COMPLEX")),
&[("Width", a)],
)
.expect_err("X_COMPLEX is not a member of the template type enum");
assert!(invalid(&err), "{err}");
add_complex_property_template(
&mut tx,
GUID,
Some("Assembly"),
(Some("Leaf"), Some("P_COMPLEX")),
&[("Width", a), ("Height", b)],
)
.expect("distinct child names are legal");
}
#[test]
fn a_window_lining_depth_needs_its_thickness() {
let model = Model::new();
let mut tx = Transaction::new(&model);
let err =
add_window_lining_properties(&mut tx, GUID, WindowLiningDraft::new().lining_depth(0.1))
.expect_err("WR31 on the window");
assert!(invalid(&err), "{err}");
add_window_lining_properties(
&mut tx,
GUID,
WindowLiningDraft::new()
.lining_depth(0.1)
.lining_thickness(0.02),
)
.expect("a depth with its thickness is legal");
}