Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/ifctester/ifctester/facet.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger]
if non_empty_values:
values = non_empty_values
else:
if self.cardinality == "optional":
return AttributeResult(True)
is_pass = False
reason = {"type": "FALSEY", "actual": values if len(values) > 1 else values[0]}

Expand Down Expand Up @@ -519,11 +521,15 @@ def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger]
break
parent = self.get_parent(parent)
if not is_pass:
if not ancestors and self.cardinality == "optional":
return PartOfResult(True)
reason = {"type": "ENTITY", "actual": ancestors}
elif self.relation == "IFCRELAGGREGATES":
aggregate = ifcopenshell.util.element.get_aggregate(inst)
is_pass = aggregate is not None
if not is_pass:
if self.cardinality == "optional":
return PartOfResult(True)
reason = {"type": "NOVALUE"}
if is_pass and self.name:
is_pass = False
Expand All @@ -550,6 +556,8 @@ def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger]
break
is_pass = group is not None
if not is_pass:
if self.cardinality == "optional":
return PartOfResult(True)
reason = {"type": "NOVALUE"}
if is_pass and self.name:
if group.is_a().upper() != self.name:
Expand All @@ -564,6 +572,8 @@ def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger]
container = ifcopenshell.util.element.get_container(inst)
is_pass = container is not None
if not is_pass:
if self.cardinality == "optional":
return PartOfResult(True)
reason = {"type": "NOVALUE"}
if is_pass and self.name:
if container.is_a().upper() != self.name:
Expand All @@ -578,6 +588,8 @@ def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger]
nest = ifcopenshell.util.element.get_nest(inst)
is_pass = nest is not None
if not is_pass:
if self.cardinality == "optional":
return PartOfResult(True)
reason = {"type": "NOVALUE"}
if is_pass and self.name:
is_pass = False
Expand Down Expand Up @@ -606,6 +618,8 @@ def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger]
building_element = ifcopenshell.util.element.get_voided_element(opening)
is_pass = building_element is not None
if not is_pass:
if self.cardinality == "optional":
return PartOfResult(True)
reason = {"type": "NOVALUE"}
if is_pass and self.name:
if building_element.is_a().upper() != self.name:
Expand Down
35 changes: 35 additions & 0 deletions src/ifctester/test/test_facet.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,31 @@ def test_filtering_using_an_attribute_facet(self):
expected=True,
)

ifc = ifcopenshell.file()
facet = Attribute(name="Description", value="Foobar", cardinality="optional")
run(
"An optional facet with a null attribute value should pass",
facet=facet,
inst=ifc.createIfcWall(Name="Waldo"),
expected=True,
)
ifc = ifcopenshell.file()
facet = Attribute(name="Name", cardinality="optional")
run(
"An optional facet with an empty string attribute value should pass",
facet=facet,
inst=ifc.createIfcWall(Name=""),
expected=True,
)
ifc = ifcopenshell.file()
facet = Attribute(name="Description", value="Foobar", cardinality="optional")
run(
"An optional facet with a present but non-matching value should fail",
facet=facet,
inst=ifc.createIfcWall(Name="Waldo", Description="NotFoobar"),
expected=False,
)

ifc = ifcopenshell.file()
facet = Attribute(name="Name")
run("Attributes with null values always fail", facet=facet, inst=ifc.createIfcWall(), expected=False)
Expand Down Expand Up @@ -1564,6 +1589,16 @@ def test_filtering_using_a_partof_facet(self):
facet = PartOf(name="IFCELEMENTASSEMBLY", relation="IFCRELAGGREGATES", cardinality="prohibited")
run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=subelement, expected=False)

ifc = ifcopenshell.file()
standalone = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
facet = PartOf(name="IFCELEMENTASSEMBLY", relation="IFCRELAGGREGATES", cardinality="optional")
run(
"An optional facet with no relationship should pass",
facet=facet,
inst=standalone,
expected=True,
)

ifc = ifcopenshell.file()
element = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcSlab")
subelement = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcBeam")
Expand Down
21 changes: 21 additions & 0 deletions src/ifctester/test/test_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,24 @@ def test_prohibited_facet(self):
[wall],
[],
)

def test_optional_facet_with_empty_value_passes(self):
"""Specification with required + optional facets should pass when optional attribute is empty."""
specs = ids.Ids(title="Title")
spec = ids.Specification(name="Name")
spec.applicability.append(ids.Entity(name="IFCWALL"))
spec.requirements.append(ids.Attribute(name="Name", value="Waldo", cardinality="required"))
spec.requirements.append(ids.Attribute(name="Description", value="Foobar", cardinality="optional"))
specs.specifications.append(spec)

spec.set_usage("required")
model = ifcopenshell.file()
wall = model.createIfcWall(Name="Waldo")
run(
"Optional facet with empty value should not cause specification failure",
specs,
model,
True,
[wall],
[],
)
Loading