diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index f310cd9eeb..b4df337dcd 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -28,8 +28,9 @@ def cast_to_value(from_value, to_value): try: target_type = type(to_value).__name__ if target_type == "int": - # Casting str -> float -> int means that notation like '1e3' is preserved - return int(float(from_value)) + # Casting str -> float means that notation like '1e3' is preserved + # We do not cast to int because 42.0 == 42 and 42.3 != 42 + return float(from_value) elif target_type == "bool": if from_value == "TRUE": return True @@ -366,7 +367,11 @@ class Property(Facet): props[pset_name] = {} if isinstance(self.name, str): prop = pset_props.get(self.name) - if prop is not None and prop != "": + if prop == "UNKNOWN" and [ + p for p in inst.wrapped_data.file.by_id(pset_props["id"]).HasProperties if p.Name == self.name + ][0].NominalValue.is_a("IfcLogical"): + pass + elif prop is not None and prop != "": props[pset_name][self.name] = prop else: props[pset_name] = {k: v for k, v in pset_props.items() if k == self.name} @@ -386,7 +391,7 @@ class Property(Facet): ): continue - data_type = prop_entity.NominalValue.is_a().replace("Ifc", "").replace("Measure", "") + data_type = prop_entity.NominalValue.is_a() if data_type != self.measure: is_pass = False @@ -407,10 +412,22 @@ class Property(Facet): break if self.value: - if any([v != self.value for v in props[pset_name].values()]): - is_pass = False - reason = {"type": "VALUE", "actual": list(props[pset_name].values())} - break + for value in props[pset_name].values(): + if isinstance(self.value, str) and isinstance(value, str): + if value != self.value: + is_pass = False + reason = {"type": "VALUE", "actual": value} + break + elif isinstance(self.value, str): + cast_value = cast_to_value(self.value, value) + if value != cast_value: + is_pass = False + reason = {"type": "VALUE", "actual": value} + break + elif value != self.value: + is_pass = False + reason = {"type": "VALUE", "actual": value} + break return PropertyResult(is_pass, reason) @@ -654,7 +671,7 @@ class PropertyResult(Result): return "The property set does not contain the required property" elif self.reason["type"] == "MEASURE": return f"The data type \"{str(self.reason['actual'])}\" does not match the requirements" - elif self.reason["type"] == "VALUE" and len(self.reason['actual']) == 1: + elif self.reason["type"] == "VALUE" and len(self.reason["actual"]) == 1: return f"The property value \"{str(self.reason['actual'][0])}\" does not match the requirements" elif self.reason["type"] == "VALUE": return f"The property values \"{str(self.reason['actual'])}\" do not match the requirements" diff --git a/src/ifctester/ifctester/ids.xsd b/src/ifctester/ifctester/ids.xsd index 98e754d218..884cc780ad 100644 --- a/src/ifctester/ifctester/ids.xsd +++ b/src/ifctester/ifctester/ids.xsd @@ -1,6 +1,6 @@ - - + + @@ -83,67 +83,11 @@ - + - See the documentation and default units of these measures on https://github.com/buildingSMART/IDS/blob/master/Documentation/Physical_Quantities_and_Units.md + This is the name of an IFC Defined Types. See the documentation and default units on https://github.com/buildingSMART/IDS/blob/master/Documentation/Physical_Quantities_and_Units.md - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -169,41 +113,19 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + @@ -270,7 +192,15 @@ - + + + + + + + + + @@ -304,4 +234,12 @@ - + + + + + + + + + \ No newline at end of file diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py index d7d63387f4..dee1a98986 100644 --- a/src/ifctester/test/test_facet.py +++ b/src/ifctester/test/test_facet.py @@ -401,8 +401,6 @@ class TestAttribute: expected=False, ) - # TODO continue from here - global_id = ifcopenshell.guid.new() facet = Attribute(name="GlobalId", value=global_id) ifc = ifcopenshell.file() @@ -509,27 +507,10 @@ class TestAttribute: facet = Attribute(name="NumberOfRisers", value="42.3") ifc = ifcopenshell.file() run( - "Integers are always floored when cast 1/2", + "Specifying a float when the value is an integer will fail", facet=facet, inst=ifc.createIfcStairFlight(NumberOfRisers=42), - expected=True, - ) - facet = Attribute(name="NumberOfRisers", value="42.7") - ifc = ifcopenshell.file() - run( - "Integers are always floored when cast 2/2", - facet=facet, - inst=ifc.createIfcStairFlight(NumberOfRisers=42), - expected=True, - ) - - facet = Attribute(name="NumberOfRisers", value="42.7") - ifc = ifcopenshell.file() - run( - "Integers are always floored when cast 2/2", - facet=facet, - inst=ifc.createIfcStairFlight(NumberOfRisers=42), - expected=True, + expected=False, ) facet = Attribute(name="IsMilestone", value="TRUE") @@ -841,134 +822,177 @@ class TestProperty: timeunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="TIMEUNIT", name="SECOND") ifcopenshell.api.run("unit.assign_unit", ifc, units=[lengthunit, areaunit, volumeunit, timeunit]) - # A name check by itself only checks that a property is non-null and non empty string - # The logic is that unfortunately most BIM users cannot differentiate between the two. facet = Property(propertySet="Foo_Bar", name="Foo") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - run("", facet=facet, inst=element, expected=False) + run("Elements with no properties always fail", facet=facet, inst=element, expected=False) pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") - run("", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"AnotherProperty": "AnotherValue"}) + run("Elements with a matching pset but no property also fail", facet=facet, inst=element, expected=False) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": None}) - run("", facet=facet, inst=element, expected=False) + 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("", facet=facet, inst=element, expected=True) + run("A name check will match any property with any string value", facet=facet, inst=element, expected=True) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ""}) + run("An empty string is considered falsey and will not pass", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLogical("UNKNOWN")}) + run("A logical unknown is considered falsey and will not pass", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("P0D")}) + run("A zero duration will pass", facet=facet, inst=element, expected=True) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcBoolean(True)}) - run("", facet=facet, inst=element, expected=True) + run("A property set to true will pass a name check", facet=facet, inst=element, expected=True) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": False}) - run("", facet=facet, inst=element, expected=True) + run( + "A property set to false is still considered a value and will pass a name check", + facet=facet, + inst=element, + expected=True, + ) - # A simple value checks an exact case-sensitive match facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) - run("", facet=facet, inst=element, expected=True) + run("Specifying a value performs a case-sensitive match 1/2", facet=facet, inst=element, expected=True) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "bar"}) + run("Specifying a value performs a case-sensitive match 2/2", facet=facet, inst=element, expected=False) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"}) - run("", facet=facet, inst=element, expected=False) + run("Specifying a value fails against different values", facet=facet, inst=element, expected=False) + + facet = Property(propertySet="Foo_Bar", name="Foo", value="♫") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "♫"}) + run("Non-ascii characters are treated without encoding", facet=facet, inst=element, expected=True) + + identifier = "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345" + facet = Property(propertySet="Foo_Bar", name="Foo", value=identifier + "_extra_characters") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcIdentifier(identifier)}) + run("IDS does not handle string truncation such as for identifiers", facet=facet, inst=element, expected=False) - # Simple values only check string matches facet = Property(propertySet="Foo_Bar", name="Foo", value="1") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "1"}) - run("", facet=facet, inst=element, expected=True) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcInteger(1)}) - run("", facet=facet, inst=element, expected=False) + run("A number specified as a string is treated as a string", facet=facet, inst=element, expected=True) + + facet = Property(propertySet="Foo_Bar", name="Foo", value="42") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcInteger(42)}) + run("Integer values are checked using type casting 1/4", facet=facet, inst=element, expected=True) + facet = Property(propertySet="Foo_Bar", name="Foo", value="42.") + run("Integer values are checked using type casting 2/4", facet=facet, inst=element, expected=True) + facet = Property(propertySet="Foo_Bar", name="Foo", value="42.0") + run("Integer values are checked using type casting 3/4", facet=facet, inst=element, expected=True) + facet = Property(propertySet="Foo_Bar", name="Foo", value="42.3") + run("Integer values are checked using type casting 4/4", facet=facet, inst=element, expected=False) + + facet = Property(propertySet="Foo_Bar", name="Foo", value="42") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0)}) + run("Real values are checked using type casting 1/3", facet=facet, inst=element, expected=True) + facet = Property(propertySet="Foo_Bar", name="Foo", value="42.0") + run("Real values are checked using type casting 2/3", facet=facet, inst=element, expected=True) + facet = Property(propertySet="Foo_Bar", name="Foo", value="42.3") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.3)}) + run("Real values are checked using type casting 3/3", facet=facet, inst=element, expected=True) + + facet = Property(propertySet="Foo_Bar", name="Foo", value="42,3") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.3)}) + run("Only specifically formatted numbers are allowed 1/4", facet=facet, inst=element, expected=False) + facet = Property(propertySet="Foo_Bar", name="Foo", value="123,4.5") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(1234.5)}) + run("Only specifically formatted numbers are allowed 2/4", facet=facet, inst=element, expected=False) + facet = Property(propertySet="Foo_Bar", name="Foo", value="1.2345e3") + run("Only specifically formatted numbers are allowed 3/4", facet=facet, inst=element, expected=True) + facet = Property(propertySet="Foo_Bar", name="Foo", value="1.2345E3") + run("Only specifically formatted numbers are allowed 4/4", facet=facet, inst=element, expected=True) + + facet = Property(propertySet="Foo_Bar", name="Foo", value="TRUE") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcBoolean(False)}) + run("Booleans must be specified as uppercase strings 1/3", facet=facet, inst=element, expected=False) + facet = Property(propertySet="Foo_Bar", name="Foo", value="FALSE") + run("Booleans must be specified as uppercase strings 2/3", facet=facet, inst=element, expected=True) + facet = Property(propertySet="Foo_Bar", name="Foo", value="False") + run("Booleans must be specified as uppercase strings 3/3", facet=facet, inst=element, expected=False) + + facet = Property(propertySet="Foo_Bar", name="Foo", value="2022-01-01") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDate("2022-01-01")}) + run("Dates are treated as strings 1/2", facet=facet, inst=element, expected=True) + ifcopenshell.api.run( + "pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDate("2022-01-01+00:00")} + ) + run("Dates are treated as strings 2/2", facet=facet, inst=element, expected=False) + + facet = Property(propertySet="Foo_Bar", name="Foo", value="PT16H") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("PT16H")}) + run("Durations are treated as strings 1/2", facet=facet, inst=element, expected=True) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("P2D")}) + run("Durations are treated as strings 1/2", facet=facet, inst=element, expected=False) - # Restrictions are supported for property sets. If multiple are matched, all must satisfy requirements. restriction = Restriction(options="Foo_.*", type="pattern") facet = Property(propertySet=restriction, name="Foo") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) - run("", facet=facet, inst=element, expected=True) + run("All matching property sets must satisfy requirements 1/3", facet=facet, inst=element, expected=True) pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Baz") - run("", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"AnotherProperty": "AnotherValue"}) + run("All matching property sets must satisfy requirements 2/3", facet=facet, inst=element, expected=False) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) - run("", facet=facet, inst=element, expected=True) + run("All matching property sets must satisfy requirements 3/3", facet=facet, inst=element, expected=True) - # Restrictions are supported for names. If multiple are matched, all must satisfy requirements. restriction = Restriction(options="Foo.*", type="pattern") facet = Property(propertySet="Foo_Bar", name=restriction, value="x") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x"}) - run("", facet=facet, inst=element, expected=True) + run("All matching properties must satisfy requirements 1/3", facet=facet, inst=element, expected=True) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "x"}) - run("", facet=facet, inst=element, expected=True) + run("All matching properties must satisfy requirements 2/3", facet=facet, inst=element, expected=True) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "y"}) - run("", facet=facet, inst=element, expected=False) + run("All matching properties must satisfy requirements 3/3", facet=facet, inst=element, expected=False) - # Restrictions are supported for values. If multiple are matched, all must satisfy requirements. restriction1 = Restriction(options="Foo.*", type="pattern") restriction2 = Restriction(options=["x", "y"], type="enumeration") facet = Property(propertySet="Foo_Bar", name=restriction1, value=restriction2) element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "y"}) - run("", facet=facet, inst=element, expected=True) + run( + "If multiple properties are matched, all values must satisfy requirements 1/2", + facet=facet, + inst=element, + expected=True, + ) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "z"}) - run("", facet=facet, inst=element, expected=False) + run( + "If multiple properties are matched, all values must satisfy requirements 2/2", + facet=facet, + inst=element, + expected=False, + ) - # Restrictions may be used to check basic data primitives - restriction = Restriction(options=[42.12], type="enumeration", base="decimal") - facet = Property(propertySet="Foo_Bar", name="Foobar", value=restriction) - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": 42.12}) - run("", facet=facet, inst=element, expected=True) - restriction = Restriction(options=[42], type="enumeration", base="integer") - facet = Property(propertySet="Foo_Bar", name="Foobar", value=restriction) - run("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": 42}) - run("", facet=facet, inst=element, expected=True) - restriction = Restriction(options=[True], type="enumeration", base="boolean") - facet = Property(propertySet="Foo_Bar", name="Foobar", value=restriction) - run("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": True}) - run("", facet=facet, inst=element, expected=True) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": False}) - run("", facet=facet, inst=element, expected=False) - - # When measure is not specified, no unit conversion is done and only primitives are checked - restriction = Restriction(options=[42.12], type="enumeration", base="decimal") - facet = Property(propertySet="Foo_Bar", name="Foobar", value=restriction) - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": 42.12}) - run("", facet=facet, inst=element, expected=True) - - # Measure may be used to specify an IFC data type - restriction = Restriction(options=[2], type="enumeration", base="decimal") - facet = Property(propertySet="Foo_Bar", name="Foo", value=restriction, measure="Time") + facet = Property(propertySet="Foo_Bar", name="Foo", value="2", measure="IfcTimeMeasure") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcMassMeasure(2)}) - run("", facet=facet, inst=element, expected=False) + run("Measure may be used to specify an IFC data type 1/2", facet=facet, inst=element, expected=False) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcTimeMeasure(2)}) - run("", facet=facet, inst=element, expected=True) + run("Measure may be used to specify an IFC data type 2/2", facet=facet, inst=element, expected=True) - # Measure also implies that a unit matters, and so a conversion shall take place to SI units - restriction = Restriction(options=[2], type="enumeration", base="decimal") - facet = Property(propertySet="Foo_Bar", name="Foo", value=restriction, measure="Length") + facet = Property(propertySet="Foo_Bar", name="Foo", value="2", measure="IfcLengthMeasure") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2)}) - run("", facet=facet, inst=element, expected=False) + run("Unit conversions shall take place to IDS-nominated standard units 1/2", facet=facet, inst=element, expected=False) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2000)}) - run("", facet=facet, inst=element, expected=True) + run("Unit conversions shall take place to IDS-nominated standard units 2/2", facet=facet, inst=element, expected=True) - # The facet checks inherited properties from the type wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall_type, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) facet = Property(propertySet="Foo_Bar", name="Foo") - run("", facet=facet, inst=wall, expected=True) - run("", facet=facet, inst=wall_type, expected=True) + run("Properties can be inherited from the type 1/2", facet=facet, inst=wall, expected=True) + run("Properties can be inherited from the type 2/2", facet=facet, inst=wall_type, expected=True) - # The facet checks overriden properties from the occurrence wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) @@ -977,8 +1001,8 @@ class TestProperty: pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar") - run("", facet=facet, inst=wall, expected=True) - run("", facet=facet, inst=wall_type, expected=False) + run("Properties can be overriden by an occurrence 1/2", facet=facet, inst=wall, expected=True) + run("Properties can be overriden by an occurrence 2/2", facet=facet, inst=wall_type, expected=False) class TestMaterial: