use ifc_model::{Entity, EntityId, Model, Value};
use ifc_schema::Schema;
use ifc_validate::{validate, Finding, Report, Severity};
fn entity(schema: &Schema, type_name: &str, values: &[(&str, Value)]) -> Entity {
let names = schema.attribute_names(type_name);
let mut attributes = vec![Value::Null; names.len()];
for (name, value) in values {
let index = names
.iter()
.position(|candidate| candidate.eq_ignore_ascii_case(name))
.unwrap_or_else(|| panic!("{type_name} has no {name} in {}", schema.name()));
attributes[index] = value.clone();
}
Entity::new(type_name, attributes)
}
fn verdicts(report: &Report, rule: &str) -> Vec<Severity> {
report
.findings()
.iter()
.filter(|finding| finding.rule == rule)
.map(|finding| finding.severity)
.collect()
}
fn declared(token: &str) -> Model {
let mut model = Model::new();
model.header_mut().schema = vec![token.to_string()];
model.header_mut().implementation_level = "2;1".to_string();
model
}
const TABLES_WITHOUT_PRIORITY: &str = "
SCHEMA IFC4;
ENTITY IfcMaterialLayer;
Name : OPTIONAL STRING;
END_ENTITY;
END_SCHEMA;
";
#[test]
fn a_rule_that_cannot_resolve_its_operand_makes_the_model_non_conformant() {
let schema = Schema::from_express(TABLES_WITHOUT_PRIORITY);
assert_eq!(schema.version(), Some(ifc_schema::SchemaVersion::Ifc4));
let mut model = declared("IFC4");
model.push(Entity::new(
"IFCMATERIALLAYER",
vec![Value::Text("layer".into())],
));
let report = validate(&model, &schema);
let decisive: Vec<&Finding> = report
.findings()
.iter()
.filter(|finding| finding.severity != Severity::Unsupported)
.collect();
assert_eq!(
decisive.len(),
1,
"exactly the evaluation error, nothing else: {decisive:#?}"
);
let finding = decisive[0];
assert_eq!(finding.severity, Severity::EvaluationError);
assert_eq!(finding.rule, "IfcMaterialLayer.NormalizedPriority");
assert!(finding.message.contains("Priority"), "{}", finding.message);
assert_eq!(report.summary().evaluation_errors, 1);
assert_eq!(report.summary().errors, 0);
assert!(
!report.is_conformant(),
"a rule nobody could decide is not a rule that passed"
);
}
#[test]
fn the_same_record_under_complete_tables_is_conformant() {
let schema = ifc_schema::ifc4();
let mut model = declared("IFC4");
model.push(entity(
schema,
"IFCMATERIALLAYER",
&[("LayerThickness", Value::Real(1.0))],
));
let report = validate(&model, schema);
assert!(report.is_conformant(), "{:#?}", report.findings());
}
#[test]
fn every_unreadable_operand_is_an_evaluation_error() {
let schema = ifc_schema::ifc4();
type Build = fn(&Schema, Value) -> Model;
let cases: &[(&str, Build, Value, Value)] = &[
(
"IfcMaterialLayer.NormalizedPriority",
|schema, priority| {
let mut model = Model::new();
model.push(entity(
schema,
"IFCMATERIALLAYER",
&[("LayerThickness", Value::Real(1.0)), ("Priority", priority)],
));
model
},
Value::Text("high".into()),
Value::Integer(10),
),
(
"IfcRelConnectsPathElements.NormalizedRelatingPriorities",
|schema, priorities| {
let mut model = Model::new();
model.push(entity(
schema,
"IFCRELCONNECTSPATHELEMENTS",
&[("RelatingPriorities", priorities)],
));
model
},
Value::List(vec![Value::Text("first".into())]),
Value::List(vec![Value::Integer(1)]),
),
(
"IfcRelConnectsPathElements.NormalizedRelatedPriorities",
|schema, priorities| {
let mut model = Model::new();
model.push(entity(
schema,
"IFCRELCONNECTSPATHELEMENTS",
&[("RelatedPriorities", priorities)],
));
model
},
Value::Integer(5),
Value::List(vec![Value::Integer(5)]),
),
(
"IfcRelSequence.AvoidInconsistentSequence",
|schema, relating| {
let mut model = Model::new();
model.insert(EntityId(1), entity(schema, "IFCTASK", &[]));
model.push(entity(
schema,
"IFCRELSEQUENCE",
&[
("RelatingProcess", relating),
("RelatedProcess", Value::Ref(EntityId(1))),
],
));
model
},
Value::Text("task".into()),
Value::Ref(EntityId(2)),
),
(
"IfcRelAggregates.NoSelfReference",
|schema, relating| {
let mut model = Model::new();
model.insert(EntityId(1), entity(schema, "IFCPROJECT", &[]));
model.push(entity(
schema,
"IFCRELAGGREGATES",
&[
("RelatingObject", relating),
("RelatedObjects", Value::List(vec![Value::Ref(EntityId(1))])),
],
));
model
},
Value::Integer(1),
Value::Ref(EntityId(2)),
),
(
"IfcRelAssignsToGroup.NoSelfReference",
|schema, relating| {
let mut model = Model::new();
model.insert(EntityId(1), entity(schema, "IFCWALL", &[]));
model.push(entity(
schema,
"IFCRELASSIGNSTOGROUPBYFACTOR",
&[
("RelatingGroup", relating),
("RelatedObjects", Value::List(vec![Value::Ref(EntityId(1))])),
],
));
model
},
Value::Text("group".into()),
Value::Ref(EntityId(2)),
),
(
"IfcRelDefinesByProperties.NoRelatedTypeObject",
|schema, related| {
let mut model = Model::new();
model.insert(EntityId(1), entity(schema, "IFCWALL", &[]));
model.push(entity(
schema,
"IFCRELDEFINESBYPROPERTIES",
&[("RelatedObjects", related)],
));
model
},
Value::List(vec![Value::Ref(EntityId(99))]),
Value::List(vec![Value::Ref(EntityId(1))]),
),
(
"IfcRelDefinesByProperties.NoRelatedTypeObject",
|schema, related| {
let mut model = Model::new();
model.insert(EntityId(1), entity(schema, "IFCWALL", &[]));
model.push(entity(
schema,
"IFCRELDEFINESBYPROPERTIES",
&[("RelatedObjects", related)],
));
model
},
Value::List(vec![Value::Text("wall".into())]),
Value::List(vec![Value::Ref(EntityId(1))]),
),
(
"IfcRelSpaceBoundary.CorrectPhysOrVirt",
|schema, element| {
let mut model = Model::new();
model.insert(EntityId(1), entity(schema, "IFCWALL", &[]));
model.push(entity(
schema,
"IFCRELSPACEBOUNDARY2NDLEVEL",
&[
("RelatedBuildingElement", element),
("PhysicalOrVirtualBoundary", Value::Enum("PHYSICAL".into())),
],
));
model
},
Value::Ref(EntityId(99)),
Value::Ref(EntityId(1)),
),
(
"IfcRelSpaceBoundary.CorrectPhysOrVirt",
|schema, physicality| {
let mut model = Model::new();
model.insert(EntityId(1), entity(schema, "IFCWALL", &[]));
model.push(entity(
schema,
"IFCRELSPACEBOUNDARY",
&[
("RelatedBuildingElement", Value::Ref(EntityId(1))),
("PhysicalOrVirtualBoundary", physicality),
],
));
model
},
Value::Enum("SOMETIMES".into()),
Value::Enum("PHYSICAL".into()),
),
(
"global.UniqueGlobalId",
|schema, guid| {
let mut model = Model::new();
model.push(entity(schema, "IFCWALL", &[("GlobalId", guid)]));
model
},
Value::Integer(7),
Value::Text("0hMOPMBpTAoOL$IPqA1$xY".into()),
),
];
let mut failures = Vec::new();
for (rule, build, unreadable, readable) in cases {
let undecided = verdicts(&validate(&build(schema, unreadable.clone()), schema), rule);
if undecided != [Severity::EvaluationError] {
failures.push(format!("{rule} with {unreadable:?}: {undecided:?}"));
}
let decided = verdicts(&validate(&build(schema, readable.clone()), schema), rule);
if !decided.is_empty() {
failures.push(format!("{rule} with {readable:?}: {decided:?}"));
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn an_unset_operand_is_not_an_evaluation_error() {
let schema = ifc_schema::ifc4();
let mut model = Model::new();
model.push(entity(
schema,
"IFCMATERIALLAYER",
&[("LayerThickness", Value::Real(1.0))],
));
model.push(entity(schema, "IFCRELSEQUENCE", &[]));
let report = validate(&model, schema);
assert!(
!report
.findings()
.iter()
.any(|finding| finding.severity == Severity::EvaluationError),
"{:#?}",
report.findings()
);
}
#[test]
fn a_satisfied_disjunction_is_decided_despite_an_unresolvable_operand() {
let schema = Schema::from_express(
"
SCHEMA IFC4;
ENTITY IfcExternalReference;
Location : OPTIONAL STRING;
Name : OPTIONAL STRING;
END_ENTITY;
END_SCHEMA;
",
);
let record = |location: Value| {
let mut model = declared("IFC4");
model.push(Entity::new(
"IFCEXTERNALREFERENCE",
vec![location, Value::Null],
));
validate(&model, &schema)
};
let rule = "IfcExternalReference.WR1";
let empty = record(Value::Null);
assert_eq!(verdicts(&empty, rule), [Severity::EvaluationError]);
let located = record(Value::Text("urn:x".into()));
assert_eq!(verdicts(&located, rule), []);
assert!(located.is_conformant(), "{:#?}", located.findings());
}