diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 865e0fe435..3c399e1d10 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -162,6 +162,9 @@ class Attribute(Facet): super().__init__(name, value, minOccurs, maxOccurs, instructions) def __call__(self, inst, logger=None): + if self.minOccurs == 0 and self.maxOccurs != 0: + return AttributeResult(True) + def get_values(element, name): if isinstance(name, str): return [getattr(element, name, None)] @@ -245,6 +248,8 @@ class Attribute(Facet): reason = {"type": "VALUE", "actual": value} break + if self.maxOccurs == 0: + return AttributeResult(not is_pass, {"type": "PROHIBITED"}) return AttributeResult(is_pass, reason) @@ -267,6 +272,9 @@ class Classification(Facet): pass def __call__(self, inst, logger=None): + if self.minOccurs == 0 and self.maxOccurs != 0: + return ClassificationResult(True) + leaf_references = ifcopenshell.util.classification.get_references(inst) references = leaf_references.copy() @@ -291,12 +299,14 @@ class Classification(Facet): if not is_pass: reason = {"type": "SYSTEM", "actual": systems} + if self.maxOccurs == 0: + return ClassificationResult(not is_pass, {"type": "PROHIBITED"}) return ClassificationResult(is_pass, reason) class PartOf(Facet): - def __init__(self, entity=None, relation="IfcRelAggregates", instructions=None): - self.parameters = ["entity", "@relation", "@instructions"] + def __init__(self, entity=None, relation="IfcRelAggregates", minOccurs=None, maxOccurs=None, instructions=None): + self.parameters = ["entity", "@relation", "@minOccurs", "@maxOccurs", "@instructions"] self.applicability_templates = [ "An element with an {relation} relationship with an {entity}", "An element with an {relation} relationship", @@ -305,9 +315,12 @@ class PartOf(Facet): "An element must have an {relation} relationship with an {entity}", "An element must have an {relation} relationship", ] - super().__init__(entity, relation, instructions) + super().__init__(entity, relation, minOccurs, maxOccurs, instructions) def __call__(self, inst, logger=None): + if self.minOccurs == 0 and self.maxOccurs != 0: + return PartOfResult(True) + reason = None if self.relation == "IfcRelAggregates": aggregate = ifcopenshell.util.element.get_aggregate(inst) @@ -363,6 +376,9 @@ class PartOf(Facet): nest = self.get_nested_whole(nest) if not is_pass: reason = {"type": "ENTITY", "actual": ancestors} + + if self.maxOccurs == 0: + return PartOfResult(not is_pass, {"type": "PROHIBITED"}) return PartOfResult(is_pass, reason) def get_nested_whole(self, element): @@ -403,6 +419,9 @@ class Property(Facet): super().__init__(propertySet, name, value, measure, uri, minOccurs, maxOccurs, instructions) def __call__(self, inst, logger=None): + if self.minOccurs == 0 and self.maxOccurs != 0: + return PropertyResult(True) + all_psets = ifcopenshell.util.element.get_psets(inst) if isinstance(self.propertySet, str): @@ -488,6 +507,9 @@ class Property(Facet): is_pass = False reason = {"type": "VALUE", "actual": value} break + + if self.maxOccurs == 0: + return PropertyResult(not is_pass, {"type": "PROHIBITED"}) return PropertyResult(is_pass, reason) @@ -505,6 +527,9 @@ class Material(Facet): super().__init__(value, uri, minOccurs, maxOccurs, instructions) def __call__(self, inst, logger=None): + if self.minOccurs == 0 and self.maxOccurs != 0: + return MaterialResult(True) + material = ifcopenshell.util.element.get_material(inst, should_skip_usage=True) is_pass = material is not None @@ -542,6 +567,8 @@ class Material(Facet): if not is_pass: reason = {"type": "VALUE", "actual": values} + if self.maxOccurs == 0: + return MaterialResult(not is_pass, {"type": "PROHIBITED"}) return MaterialResult(is_pass, reason) diff --git a/src/ifctester/test/ids_doc_generator.py b/src/ifctester/test/ids_doc_generator.py index c3ccba5a67..c278fb81aa 100644 --- a/src/ifctester/test/ids_doc_generator.py +++ b/src/ifctester/test/ids_doc_generator.py @@ -63,7 +63,7 @@ class DocGenerator: basename = f"{result}-" + re.sub("[^0-9a-zA-Z]", "_", name.lower()) # Write IFC to disk - f.write(os.path.join(outdir, "testcases", f"{basename}.ifc")) + f.write(os.path.join(outdir, "testcases", self.facet, f"{basename}.ifc")) # Create an IDS with the applicability selecting exactly # the entity type passed to us in `inst`. @@ -74,7 +74,7 @@ class DocGenerator: specs.specifications.append(spec) # Write IDS to disk - with open(os.path.join(outdir, "testcases", f"{basename}.ids"), "w", encoding="utf-8") as ids_file: + with open(os.path.join(outdir, "testcases", self.facet, f"{basename}.ids"), "w", encoding="utf-8") as ids_file: ids_file.write(specs.to_string()) xml_text = "\n".join( @@ -123,7 +123,7 @@ for facet, testcases in test_facet.run.testcases.items(): write("~~~") write() write( - f"[Sample IDS](testcases/{testcase['basename']}.ids) - [Sample IFC: {testcase['id']}](testcases/{testcase['basename']}.ifc)" + f"[Sample IDS](testcases/{facet}/{testcase['basename']}.ids) - [Sample IFC: {testcase['id']}](testcases/{facet}/{testcase['basename']}.ifc)" ) write() diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py index e7aa454356..e0a0ad021f 100644 --- a/src/ifctester/test/test_facet.py +++ b/src/ifctester/test/test_facet.py @@ -238,7 +238,7 @@ class TestAttribute: set_facet("attribute") ifc = ifcopenshell.file() - facet = Attribute(name="Foobar") + facet = Attribute(name="Rabbit") run("Invalid attribute names always fail", facet=facet, inst=ifc.createIfcWall(), expected=False) ifc = ifcopenshell.file() @@ -249,7 +249,20 @@ class TestAttribute: inst=ifc.createIfcWall(Name="Foobar"), expected=True, ) + ifc = ifcopenshell.file() + facet = Attribute(name="Name") + element = ifc.createIfcWall(Name="Foobar") + run("A required facet checks all parameters as normal", facet=facet, inst=element, expected=True) + facet = Attribute(name="Name", minOccurs=0, maxOccurs=0) + run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False) + facet = Attribute(name="Name", minOccurs=0) + run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) + facet = Attribute(name="Rabbit", minOccurs=0) + run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element, expected=True) + + ifc = ifcopenshell.file() + facet = Attribute(name="Name") run("Attributes with null values always fail", facet=facet, inst=ifc.createIfcWall(), expected=False) # The logic is that unfortunately most BIM users cannot differentiate between the two. ifc = ifcopenshell.file() @@ -724,6 +737,14 @@ class TestClassification: expected=True, ) + run("A required facet checks all parameters as normal", facet=facet, inst=element1, expected=True) + facet = Classification(minOccurs=0, maxOccurs=0) + run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element1, expected=False) + facet = Attribute(name="Name", minOccurs=0) + run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element0, expected=True) + facet = Attribute(name="Rabbit", minOccurs=0) + run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element1, expected=True) + facet = Classification(value="1") run( "Values should match exactly if lightweight classifications are used", @@ -859,6 +880,15 @@ class TestProperty: run("Properties with a null value fail", facet=facet, inst=element, expected=False) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) run("A name check will match any property with any string value", facet=facet, inst=element, expected=True) + + run("A required facet checks all parameters as normal", facet=facet, inst=element, expected=True) + facet = Property(propertySet="Foo_Bar", name="Foo", measure="IfcLabel", minOccurs=0, maxOccurs=0) + run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False) + facet = Property(propertySet="Foo_Bar", name="Foo", measure="IfcLabel", minOccurs=0) + run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) + facet = Property(propertySet="Foo_Bar", name="Bar", measure="IfcLabel", minOccurs=0) + run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element, expected=True) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ""}) facet = Property(propertySet="Foo_Bar", name="Foo", measure="IfcLogical") run("An empty string is considered falsey and will not pass", facet=facet, inst=element, expected=False) @@ -1091,6 +1121,14 @@ class TestMaterial: ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) run("Elements with any material will pass an empty material facet", facet=facet, inst=element, expected=True) + run("A required facet checks all parameters as normal", facet=facet, inst=element, expected=True) + facet = Material(minOccurs=0, maxOccurs=0) + run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False) + facet = Material(minOccurs=0) + run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) + facet = Material(value="Foo", minOccurs=0) + run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) + ifc = ifcopenshell.file() facet = Material(value="Foo") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") @@ -1221,10 +1259,18 @@ class TestPartOf: def test_creating_a_partof_facet(self): facet = PartOf() assert facet.asdict() == {"@relation": "IfcRelAggregates"} - facet = PartOf(entity="IfcGroup", relation="IfcRelAssignsToGroup", instructions="instructions") + facet = PartOf( + entity="IfcGroup", + relation="IfcRelAssignsToGroup", + minOccurs="0", + maxOccurs="unbounded", + instructions="instructions", + ) assert facet.asdict() == { "entity": {"simpleValue": "IfcGroup"}, "@relation": "IfcRelAssignsToGroup", + "@minOccurs": "0", + "@maxOccurs": "unbounded", "@instructions": "instructions", } @@ -1241,6 +1287,11 @@ class TestPartOf: run("The aggregated whole fails an aggregate relationship", facet=facet, inst=element, expected=False) run("The aggregated part passes an aggregate relationship", facet=facet, inst=subelement, expected=True) + run("A required facet checks all parameters as normal", facet=facet, inst=subelement, expected=True) + run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=subelement, expected=False) + run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) + run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=subelement, expected=True) + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam") ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element)