Implement optionality in IfcTester, and generate integration testcases

This commit is contained in:
Dion Moult
2022-09-13 19:50:51 +10:00
parent fc0b0ca188
commit 422cc4802c
5 changed files with 405 additions and 111 deletions
+40 -30
View File
@@ -106,6 +106,14 @@ class Facet:
raise Exception(str(parameter) + " was not able to be converted into 'Parameter_dict'")
return parameter_dict
def get_usage(self):
if self.minOccurs != 0:
return "required"
elif self.minOccurs == 0 and self.maxOccurs != 0:
return "optional"
elif self.maxOccurs == 0:
return "prohibited"
class Entity(Facet):
def __init__(self, name="IFCWALL", predefinedType=None, instructions=None):
@@ -165,24 +173,11 @@ class Attribute(Facet):
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)]
return [v for k, v in element.get_info().items() if k == name]
element_type = ifcopenshell.util.element.get_type(inst)
if isinstance(self.name, str):
type_value = getattr(element_type, self.name, None) if element_type else None
occurrence_value = getattr(inst, self.name, None)
names = [self.name]
values = [occurrence_value if occurrence_value is not None else type_value]
values = [getattr(inst, self.name, None)]
else:
if element_type:
info = element_type.get_info()
info.update({k: v for k, v in inst.get_info().items() if v is not None})
else:
info = inst.get_info()
info = inst.get_info()
names = []
values = []
for k, v in info.items():
@@ -197,29 +192,31 @@ class Attribute(Facet):
reason = {"type": "NOVALUE"}
if is_pass:
non_empty_values = []
for i, value in enumerate(values):
is_empty = False
if value is None:
is_pass = False
reason = {"type": "FALSEY", "actual": value}
is_empty = True
elif value == "":
is_pass = False
reason = {"type": "FALSEY", "actual": value}
is_empty = True
elif value == tuple():
is_pass = False
reason = {"type": "FALSEY", "actual": value}
is_empty = True
else:
argument_index = inst.wrapped_data.get_argument_index(names[i])
try:
attribute_type = inst.attribute_type(argument_index)
if attribute_type == "LOGICAL" and value == "UNKNOWN":
is_pass = False
reason = {"type": "FALSEY", "actual": value}
is_empty = True
except:
if names[i] in inst.wrapped_data.get_inverse_attribute_names():
is_pass = False
reason = {"type": "INVALID"}
if not is_pass:
break
is_empty = True
if not is_empty:
non_empty_values.append(value)
if non_empty_values:
values = non_empty_values
else:
is_pass = False
reason = {"type": "FALSEY", "actual": values if len(values) > 1 else values[0]}
if is_pass and self.value:
for value in values:
@@ -326,7 +323,7 @@ class PartOf(Facet):
aggregate = ifcopenshell.util.element.get_aggregate(inst)
is_pass = aggregate is not None
if not is_pass:
reason = {"type": "RELATION"}
reason = {"type": "NOVALUE"}
if is_pass and self.entity:
is_pass = False
ancestors = []
@@ -355,7 +352,7 @@ class PartOf(Facet):
container = ifcopenshell.util.element.get_container(inst)
is_pass = container is not None
if not is_pass:
reason = {"type": "RELATION"}
reason = {"type": "NOVALUE"}
if is_pass and self.entity:
if container.is_a().upper() != self.entity:
is_pass = False
@@ -871,6 +868,8 @@ class AttributeResult(Result):
return f"An invalid attribute name was specified in the IDS"
elif self.reason["type"] == "VALUE":
return f"The attribute value \"{str(self.reason['actual'])}\" does not match the requirement"
elif self.reason["type"] == "PROHIBITED":
return f"The attribute value should not have met the requirement"
class ClassificationResult(Result):
@@ -881,11 +880,18 @@ class ClassificationResult(Result):
return f"The references \"{str(self.reason['actual'])}\" do not match the requirements"
elif self.reason["type"] == "system":
return f"The systems \"{str(self.reason['actual'])}\" do not match the requirements"
elif self.reason["type"] == "PROHIBITED":
return f"The classification should not have met the requirement"
class PartOfResult(Result):
def to_string(self):
return "TODO"
if self.reason["type"] == "NOVALUE":
return "The entity has no relationship"
elif self.reason["type"] == "ENTITY":
return f"The entity has a relationship with incorrect entities: \"{str(self.reason['actual'])}\""
elif self.reason["type"] == "PROHIBITED":
return f"The relationship should not have met the requirement"
class PropertyResult(Result):
@@ -900,6 +906,8 @@ class PropertyResult(Result):
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"
elif self.reason["type"] == "PROHIBITED":
return f"The property should not have met the requirement"
class MaterialResult(Result):
@@ -910,3 +918,5 @@ class MaterialResult(Result):
return (
f"The material names and categories of \"{str(self.reason['actual'])}\" does not match the requirement"
)
elif self.reason["type"] == "PROHIBITED":
return f"The material should not have met the requirement"
+39 -20
View File
@@ -32,9 +32,7 @@ def open(filepath, validate=False):
if validate:
get_schema().validate(filepath)
return Ids().parse(
get_schema().decode(
filepath, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"}
)
get_schema().decode(filepath, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"})
)
@@ -114,18 +112,18 @@ class Ids:
ET.ElementTree(get_schema().encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True)
return get_schema().is_valid(filepath)
def validate(self, ifc_file):
def validate(self, ifc_file, filter_version=False):
for specification in self.specifications:
specification.reset_status()
specification.validate(ifc_file)
specification.validate(ifc_file, filter_version=filter_version)
class Specification:
def __init__(
self,
name="Unnamed",
minOccurs=None,
maxOccurs=None,
minOccurs=0,
maxOccurs="unbounded",
ifcVersion=["IFC2X3", "IFC4"],
identifier=None,
description=None,
@@ -153,7 +151,7 @@ class Specification:
}
for attribute in ["identifier", "description", "instructions", "minOccurs", "maxOccurs"]:
value = getattr(self, attribute)
if value:
if value is not None:
results[f"@{attribute}"] = value
for clause_type in ["applicability", "requirements"]:
clause = getattr(self, clause_type)
@@ -194,11 +192,12 @@ class Specification:
self.applicable_entities.clear()
self.failed_entities = set()
for facet in self.requirements:
facet.status = None
facet.failed_entities.clear()
self.status = None
def validate(self, ifc_file):
if ifc_file.schema not in self.ifcVersion:
def validate(self, ifc_file, filter_version=False):
if filter_version and ifc_file.schema not in self.ifcVersion:
return
elements = []
@@ -218,18 +217,38 @@ class Specification:
self.applicable_entities.append(element)
for facet in self.requirements:
result = facet(element)
facet.status = bool(result)
if not facet.status:
if not bool(result):
self.failed_entities.add(element)
facet.failed_entities.append(element)
facet.failed_reasons.append(str(result))
for facet in self.requirements:
if facet.minOccurs != 0:
facet.status = not bool(facet.failed_entities)
elif facet.minOccurs == 0 and facet.maxOccurs != 0:
facet.status = True
elif facet.maxOccurs == 0:
facet.status = bool(facet.failed_entities)
self.status = True
if self.failed_entities:
self.status = False
elif self.minOccurs != 0 and not self.applicable_entities:
self.status = False
for facet in self.requirements:
facet.status = False
elif len(self.applicable_entities) > (self.maxOccurs or 1):
self.status = False
if self.minOccurs != 0:
if not self.applicable_entities:
self.status = False
for facet in self.requirements:
facet.status = False
elif self.failed_entities:
self.status = False
elif self.minOccurs == 0 and self.maxOccurs != 0:
if self.failed_entities:
self.status = False
elif self.maxOccurs == 0:
if (len(self.applicable_entities) - len(self.failed_entities)) > 0:
self.status = False
def get_usage(self):
if self.minOccurs != 0:
return "required"
elif self.minOccurs == 0 and self.maxOccurs != 0:
return "optional"
elif self.maxOccurs == 0:
return "prohibited"
+142 -23
View File
@@ -24,27 +24,21 @@ import functools
import ifcopenshell
import ifctester
import test_facet
import test_ids
from xml.dom.minidom import parseString
from ifctester import ids
from ifcopenshell import validate
outdir = "build"
class StableUUIDGenerator:
file = None
@classmethod
def generate(cls):
ns = uuid.UUID('b59aa156-82a4-4b4c-a6e5-3d04a0236af9')
if cls.file:
return ifcopenshell.guid.compress(uuid.uuid5(ns, str(cls.file.wrapped_data.getMaxId())).hex)
return ifcopenshell.guid.compress(uuid.uuid4().hex)
# Just for aesthetics so we don't keep on getting brand new GlobalIds on each generation
ifcopenshell.guid.new = StableUUIDGenerator.generate
def regenerate_guids(ifc):
ns = uuid.UUID("b59aa156-82a4-4b4c-a6e5-3d04a0236af9")
for element in ifc.by_type("IfcRoot"):
element.GlobalId = ifcopenshell.guid.compress(uuid.uuid5(ns, str(element.id())).hex)
class DocGenerator:
class FacetDocGenerator:
def __init__(self):
self.facet = None
self.testcases = {}
@@ -55,12 +49,13 @@ class DocGenerator:
result = "pass" if expected is True else "fail"
f = inst.wrapped_data.file
StableUUIDGenerator.file = f
ifc = inst.wrapped_data.file
if "GlobalId" not in name:
regenerate_guids(ifc)
# Validate the file created and loop over the issues, fixing them one by one.
l = validate.json_logger()
validate.validate(f, l)
validate.validate(ifc, l)
for issue in l.statements:
if "GlobalId" in issue["message"]:
issue["instance"].GlobalId = ifcopenshell.guid.new()
@@ -68,17 +63,17 @@ class DocGenerator:
ty = re.findall("\\(.+?\\)", issue["message"])[0][1:-1].split(", ")[0]
issue["instance"].PredefinedType = ty
elif "IfcMaterialList" in issue["message"]:
issue["instance"].Materials = [f.createIfcMaterial("Concrete", None, "CONCRETE")]
issue["instance"].Materials = [ifc.createIfcMaterial("Concrete", None, "CONCRETE")]
else:
raise Exception("About to emit invalid example data:", issue)
# ifc_text = "\n".join([f"{e} /* Testcase */" if e == inst else str(e) for e in f])
lines = f.wrapped_data.to_string().split("\n")[7:-3]
lines = ifc.wrapped_data.to_string().split("\n")[7:-3]
ifc_text = "\n".join([f"{l} /* Testcase */" if f"#{inst.id()}=" in l else l for l in lines])
basename = f"{result}-" + re.sub("[^0-9a-zA-Z]", "_", name.lower())
# Write IFC to disk
f.write(os.path.join(outdir, "testcases", self.facet, f"{basename}.ifc"))
ifc.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`.
@@ -112,8 +107,73 @@ class DocGenerator:
self.facet = facet
test_facet.run = DocGenerator()
class IdsDocGenerator:
def __init__(self):
self.testcases = []
def __call__(self, name, ids, ifc, expected, applicable_entities=[], failed_entities=[]):
ids.validate(ifc)
all_applicable = set()
all_failures = set()
for spec in ids.specifications:
assert spec.status is expected
all_applicable.update(spec.applicable_entities)
for requirement in spec.requirements:
if requirement.status is False:
all_failures.update(requirement.failed_entities)
assert set(all_applicable) == set(applicable_entities)
assert set(all_failures) == set(failed_entities)
result = "pass" if expected is True else "fail"
regenerate_guids(ifc)
l = validate.json_logger()
validate.validate(ifc, l)
for issue in l.statements:
raise Exception("About to emit invalid example data:", issue)
lines = ifc.wrapped_data.to_string().split("\n")[7:-3]
ifc_text = ""
for i, line in enumerate(lines):
step_id = int(line[1 : line.index("=")])
element = ifc.by_id(step_id)
newline = "" if i == 0 else "\n"
if element in applicable_entities:
pass_or_fail = "FAIL" if element in failed_entities else "PASS"
ifc_text += f"{newline}[{pass_or_fail}] {line}"
else:
ifc_text += f"{newline} {line}"
basename = f"{result}-" + re.sub("[^0-9a-zA-Z]", "_", name.lower())
# Write IFC to disk
ifc.write(os.path.join(outdir, "testcases", "ids", f"{basename}.ifc"))
# Write IDS to disk
with open(os.path.join(outdir, "testcases", "ids", f"{basename}.ids"), "w", encoding="utf-8") as ids_file:
ids_file.write(ids.to_string())
reports = []
for spec in ids.specifications:
report = {"applicability": [], "requirements": [], "usage": spec.get_usage(), "status": spec.status}
for facet in spec.applicability:
report["applicability"].append(facet.to_string("applicability"))
for facet in spec.requirements:
report["requirements"].append(
{"status": facet.status, "text": facet.to_string("requirement"), "usage": facet.get_usage()}
)
reports.append(report)
xml_text = "\n".join([l[4:] for l in ids.to_string().split("\n")[4:-1]]).replace("\t", " ")
self.testcases.append(
{"name": name, "ids": xml_text, "ifc": ifc_text, "basename": basename, "result": result, "reports": reports}
)
test_facet.run = FacetDocGenerator()
test_facet.set_facet = test_facet.run.set_facet
test_ids.run = IdsDocGenerator()
pytest.main(["-p", "no:pytest-blender"])
@@ -142,6 +202,42 @@ for facet, testcases in test_facet.run.testcases.items():
)
write()
with open(os.path.join(outdir, f"testcases-ids.md"), "w") as f:
write = functools.partial(print, file=f)
write(f"# IDS integration testcases")
write()
write(
"These testcases are designed to help describe behaviour in edge cases and ambiguities. All valid IDS implementations must demonstrate identical behaviour to these test cases."
)
write()
for testcase in test_ids.run.testcases:
write(f"## [{testcase['result'].upper()}] {testcase['name']}")
write()
write("~~~xml")
write(testcase["ids"])
write("~~~")
write()
write("~~~lua")
write(testcase["ifc"])
write("~~~")
write()
for report in testcase["reports"]:
write("```")
icon = "✔️" if report["status"] else ""
write(f"# {icon} Specification ({report['usage']})")
write("Applies to:")
for facet in report["applicability"]:
write(f" - {facet}")
write("Requirements:")
for facet in report["requirements"]:
icon = "✔️" if facet["status"] else ""
write(f" - {icon} {facet['text']} ({facet['usage']})")
write("```")
write()
write(
f"[Sample IDS](testcases/ids/{testcase['basename']}.ids) - [Sample IFC](testcases/ids/{testcase['basename']}.ifc)"
)
write()
specs = ifctester.ids.Ids(
title="buildingSMART Sample IDS",
@@ -153,15 +249,38 @@ specs = ifctester.ids.Ids(
purpose="Contractual requirements",
)
spec = ifctester.ids.Specification(name="Project naming", ifcVersion=["IFC4"], description="Projects shall be named correctly for the purposes of identification, project archival, and model federation.", instructions="Each discipline is responsible for naming their own project.")
spec = ifctester.ids.Specification(
name="Project naming",
ifcVersion=["IFC4"],
description="Projects shall be named correctly for the purposes of identification, project archival, and model federation.",
instructions="Each discipline is responsible for naming their own project.",
)
specs.specifications.append(spec)
spec.applicability.append(ifctester.ids.Entity(name="IFCPROJECT"))
spec.requirements.append(ifctester.ids.Attribute(name="Name", value="TEST", instructions="The project manager shall confirm the short project code with the client based on their real estate portfolio naming scheme."))
spec.requirements.append(
ifctester.ids.Attribute(
name="Name",
value="TEST",
instructions="The project manager shall confirm the short project code with the client based on their real estate portfolio naming scheme.",
)
)
spec = ifctester.ids.Specification(name="Fire rating", ifcVersion=["IFC4"], description="All objects must have a fire rating for building compliance checks and to know the protection strategies needed for any penetrations.", instructions="The architect is responsible for including this data.")
spec = ifctester.ids.Specification(
name="Fire rating",
ifcVersion=["IFC4"],
description="All objects must have a fire rating for building compliance checks and to know the protection strategies needed for any penetrations.",
instructions="The architect is responsible for including this data.",
)
specs.specifications.append(spec)
spec.applicability.append(ifctester.ids.Entity(name="IFCWALLTYPE"))
restriction = ifctester.ids.Restriction(options="(-|[0-9]{2,3})\/(-|[0-9]{2,3})\/(-|[0-9]{2,3})", type="pattern")
spec.requirements.append(ifctester.ids.Property(propertySet="Pset_WallCommon", name="FireRating", value=restriction, instructions="Fire rating is specified using the Fire Resistance Level as defined in the Australian National Construction Code (NCC) 2019. Valid examples include -/-/-, -/120/120, and 60/60/60"))
spec.requirements.append(
ifctester.ids.Property(
propertySet="Pset_WallCommon",
name="FireRating",
value=restriction,
instructions="Fire rating is specified using the Fire Resistance Level as defined in the Australian National Construction Code (NCC) 2019. Valid examples include -/-/-, -/120/120, and 60/60/60",
)
)
specs.to_xml(os.path.join(outdir, "library", "sample.ids"))
+103 -30
View File
@@ -583,14 +583,14 @@ class TestAttribute:
run(
"Durations are treated as strings 1/2",
facet=facet,
inst=ifc.createIfcClassification(Name="Name", EditionDate="PT16H"),
expected=False,
inst=ifc.createIfcTaskTime(Name="Name", ScheduleDuration="PT16H"),
expected=True,
)
ifc = ifcopenshell.file()
run(
"Durations are treated as strings 2/2",
facet=facet,
inst=ifc.createIfcClassification(Name="Name", EditionDate="P2D"),
inst=ifc.createIfcTaskTime(Name="Name", ScheduleDuration="P2D"),
expected=False,
)
@@ -598,30 +598,27 @@ class TestAttribute:
facet = Attribute(name=restriction)
ifc = ifcopenshell.file()
run(
"Name restrictions may be used 1/4",
"Name restrictions will match any result 1/3",
facet=facet,
inst=ifc.createIfcMaterialLayerSet(
MaterialLayers=[ifc.createIfcMaterialLayer(LayerThickness=1)], LayerSetName="Foo"
),
expected=True,
)
ifc = ifcopenshell.file()
run(
"Name restrictions may be used 2/4",
facet=facet,
inst=ifc.createIfcMaterialConstituentSet(Name="Foo"),
expected=True,
)
restriction = Restriction(options=["Name", "Description"], type="enumeration")
facet = Attribute(name=restriction)
ifc = ifcopenshell.file()
run("Name restrictions may be used 3/4", facet=facet, inst=ifc.createIfcWall(Name="Foo"), expected=False)
run(
"Name restrictions will match any result 2/3",
facet=facet,
inst=ifc.createIfcWall(Name="Foo"),
expected=True,
)
ifc = ifcopenshell.file()
run(
"Name restrictions may be used 4/4",
"Name restrictions will match any result 3/3",
facet=facet,
inst=ifc.createIfcWall(Name="Foo", Description="Bar"),
inst=ifc.createIfcWall(Description="Bar"),
expected=True,
)
@@ -639,6 +636,7 @@ class TestAttribute:
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)
wall_type.Description = "Foobar"
facet = Attribute(name="Description", value="Foobar")
run("Attributes are not inherited by the occurrence", facet=facet, inst=wall, expected=False)
restriction = Restriction(options=["42"], type="enumeration", base="string")
@@ -857,18 +855,7 @@ class TestProperty:
def test_filtering_using_a_property_facet(self):
set_facet("property")
ifc = ifcopenshell.file()
ifc.createIfcProject()
# Milli prefix used to check measurement conversions
lengthunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
areaunit = ifcopenshell.api.run(
"unit.add_si_unit", ifc, unit_type="AREAUNIT", name="SQUARE_METRE", prefix="MILLI"
)
volumeunit = ifcopenshell.api.run(
"unit.add_si_unit", ifc, unit_type="VOLUMEUNIT", name="CUBIC_METRE", prefix="MILLI"
)
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])
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", measure="IfcLabel")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
@@ -876,11 +863,12 @@ class TestProperty:
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
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})
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"AnotherProperty": None})
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)
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", measure="IfcLabel")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
@@ -912,6 +900,7 @@ class TestProperty:
expected=True,
)
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar", measure="IfcLabel")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
@@ -933,6 +922,7 @@ class TestProperty:
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)
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", value="1", measure="IfcLabel")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
@@ -1009,6 +999,7 @@ class TestProperty:
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)
ifc = self.setup_ifc()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Pset_WallCommon")
pset_template = ifcopenshell.util.pset.get_template("IFC4").get_by_name("Pset_WallCommon")
@@ -1026,6 +1017,7 @@ class TestProperty:
facet = Property(propertySet="Pset_WallCommon", name="Status", value="NEW", measure="IfcLabel")
run("Any matching value in an enumerated property will pass 3/3", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
list_property = ifc.createIfcPropertyListValue(
@@ -1039,6 +1031,7 @@ class TestProperty:
facet = Property(propertySet="Foo_Bar", name="Foo", value="Z", measure="IfcLabel")
run("Any matching value in a list property will pass 3/3", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
bounded_property = ifc.createIfcPropertyBoundedValue(
@@ -1057,6 +1050,7 @@ class TestProperty:
facet = Property(propertySet="Foo_Bar", name="Foo", value="2", measure="IfcLengthMeasure")
run("Any matching value in a bounded property will pass 4/4", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
table_property = ifc.createIfcPropertyTableValue(
@@ -1070,12 +1064,14 @@ class TestProperty:
facet = Property(propertySet="Foo_Bar", name="Foo", value="Y", measure="IfcLabel")
run("Any matching value in a table property will pass 3/3", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
pset.HasProperties = [ifc.createIfcPropertyReferenceValue(Name="Foo")]
facet = Property(propertySet="Foo_Bar", name="Foo", measure="IfcLabel")
run("Reference properties are treated as objects and not supported", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDoor")
pset = ifc.create_entity(
"IfcDoorPanelProperties",
@@ -1099,6 +1095,7 @@ class TestProperty:
)
run("Predefined properties are supported but discouraged 2/2", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", measure="IfcLengthMeasure")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
qto = ifcopenshell.api.run("pset.add_qto", ifc, product=element, name="Foo_Bar")
@@ -1107,6 +1104,7 @@ class TestProperty:
facet = Property(propertySet="Foo_Bar", name="Foo", measure="IfcAreaMeasure")
run("Quantities must also match the appropriate measure", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
complex_property = ifc.createIfcComplexProperty(Name="Foo", UsageName="RabbitAgilityTraining")
@@ -1117,6 +1115,7 @@ class TestProperty:
facet = Property(propertySet="Foo", name="Rabbits", measure="IfcLabel")
run("Complex properties are not supported 2/2", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
qto = ifcopenshell.api.run("pset.add_qto", ifc, product=element, name="Foo_Bar")
complex_quantity = ifc.createIfcPhysicalComplexQuantity(Name="Foo", Discrimination="FurThickness")
@@ -1129,6 +1128,7 @@ class TestProperty:
facet = Property(propertySet="Foo", name="MyLength", measure="IfcLengthMeasure")
run("Complex properties are not supported 2/2", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
restriction = Restriction(options="Foo_.*", type="pattern")
facet = Property(propertySet=restriction, name="Foo", measure="IfcLabel")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
@@ -1141,6 +1141,7 @@ class TestProperty:
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
run("All matching property sets must satisfy requirements 3/3", facet=facet, inst=element, expected=True)
ifc = self.setup_ifc()
restriction = Restriction(options="Foo.*", type="pattern")
facet = Property(propertySet="Foo_Bar", name=restriction, value="x", measure="IfcLabel")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
@@ -1152,6 +1153,7 @@ class TestProperty:
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "y"})
run("All matching properties must satisfy requirements 3/3", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
restriction1 = Restriction(options="Foo.*", type="pattern")
restriction2 = Restriction(options=["x", "y"], type="enumeration")
facet = Property(propertySet="Foo_Bar", name=restriction1, value=restriction2, measure="IfcLabel")
@@ -1172,6 +1174,7 @@ class TestProperty:
expected=False,
)
ifc = self.setup_ifc()
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")
@@ -1180,6 +1183,7 @@ class TestProperty:
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcTimeMeasure(2)})
run("Measures are used to specify an IFC data type 2/2", facet=facet, inst=element, expected=True)
ifc = self.setup_ifc()
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")
@@ -1198,6 +1202,7 @@ class TestProperty:
expected=True,
)
ifc = self.setup_ifc()
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)
@@ -1207,6 +1212,7 @@ class TestProperty:
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)
ifc = self.setup_ifc()
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)
@@ -1218,6 +1224,21 @@ class TestProperty:
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)
def setup_ifc(self):
ifc = ifcopenshell.file()
ifc.createIfcProject()
# Milli prefix used to check measurement conversions
lengthunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
areaunit = ifcopenshell.api.run(
"unit.add_si_unit", ifc, unit_type="AREAUNIT", name="SQUARE_METRE", prefix="MILLI"
)
volumeunit = ifcopenshell.api.run(
"unit.add_si_unit", ifc, unit_type="VOLUMEUNIT", name="CUBIC_METRE", prefix="MILLI"
)
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])
return ifc
class TestMaterial:
def test_creating_a_material_facet(self):
@@ -1258,7 +1279,6 @@ class TestMaterial:
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
material = ifcopenshell.api.run("material.add_material", ifc)
ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material)
run("Material with no data will fail a value check", facet=facet, inst=element, expected=False)
material.Name = "Foo"
run("A material name may pass the value check", facet=facet, inst=element, expected=True)
material.Name = "Bar"
@@ -1270,7 +1290,6 @@ class TestMaterial:
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialList")
ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set)
run("A material list with no data will fail a value check", facet=facet, inst=element, expected=False)
material = ifcopenshell.api.run("material.add_material", ifc)
ifcopenshell.api.run("material.add_list_item", ifc, material_list=material_set, material=material)
material.Name = "Foo"
@@ -1418,6 +1437,7 @@ class TestPartOf:
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)
ifc = ifcopenshell.file()
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)
@@ -1426,6 +1446,7 @@ class TestPartOf:
facet = PartOf(entity="IFCWALL", relation="IfcRelAggregates")
run("An aggregate may specify the entity of the whole 2/2", facet=facet, inst=subelement, expected=False)
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab")
subsubelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam")
@@ -1434,6 +1455,7 @@ class TestPartOf:
facet = PartOf(entity="IFCELEMENTASSEMBLY", relation="IfcRelAggregates")
run("An aggregate entity may pass any ancestral whole passes", facet=facet, inst=subsubelement, expected=True)
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
group = ifcopenshell.api.run("group.add_group", ifc)
facet = PartOf(relation="IfcRelAssignsToGroup")
@@ -1441,6 +1463,7 @@ class TestPartOf:
ifcopenshell.api.run("group.assign_group", ifc, products=[element], group=group)
run("A grouped element passes a group relationship", facet=facet, inst=element, expected=True)
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
group = ifc.createIfcInventory()
facet = PartOf(entity="IFCGROUP", relation="IfcRelAssignsToGroup")
@@ -1449,6 +1472,7 @@ class TestPartOf:
facet = PartOf(entity="IFCINVENTORY", relation="IfcRelAssignsToGroup")
run("A group entity must match exactly 2/2", facet=facet, inst=element, expected=True)
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace")
facet = PartOf(relation="IfcRelContainedInSpatialStructure")
@@ -1457,6 +1481,7 @@ class TestPartOf:
run("Any contained element passes a containment relationship 2/2", facet=facet, inst=element, expected=True)
run("The container itself always fails", facet=facet, inst=container, expected=False)
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace")
ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container)
@@ -1465,6 +1490,7 @@ class TestPartOf:
facet = PartOf(relation="IfcRelContainedInSpatialStructure", entity="IFCSPACE")
run("The container entity must match exactly 2/2", facet=facet, inst=element, expected=True)
ifc = ifcopenshell.file()
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)
@@ -1473,6 +1499,7 @@ class TestPartOf:
facet = PartOf(relation="IfcRelContainedInSpatialStructure", entity="IFCSPACE")
run("The container may be indirect", facet=facet, inst=subelement, expected=True)
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory")
ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element)
@@ -1480,6 +1507,7 @@ class TestPartOf:
run("Any nested part passes a nest relationship", facet=facet, inst=subelement, expected=True)
run("Any nested whole fails a nest relationship", facet=facet, inst=element, expected=False)
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory")
ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element)
@@ -1488,6 +1516,7 @@ class TestPartOf:
facet = PartOf(relation="IfcRelNests", entity="IFCFURNITURE")
run("The nest entity must match exactly 2/2", facet=facet, inst=subelement, expected=True)
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory")
subsubelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcMechanicalFastener")
@@ -1516,3 +1545,47 @@ class TestRestriction:
assert restriction == "AB01"
assert restriction != "AB"
assert restriction != "01"
def test_filtering_using_an_enumeration(self):
set_facet("restriction")
ifc = ifcopenshell.file()
restriction = Restriction(options=["Foo", "Bar"], type="enumeration")
facet = Attribute(name="Name", value=restriction)
element = ifc.createIfcWall(Name="Foo")
run("An enumeration matches case sensitively 1/3", facet=facet, inst=element, expected=True)
element.Name = "Bar"
run("An enumeration matches case sensitively 1/3", facet=facet, inst=element, expected=True)
element.Name = "Baz"
run("An enumeration matches case sensitively 1/3", facet=facet, inst=element, expected=False)
ifc = ifcopenshell.file()
restriction = Restriction(options={"minInclusive": 0, "maxInclusive": 10}, type="bounds", base="integer")
element = ifc.createIfcSurfaceStyleRefraction(RefractionIndex=0)
facet = Attribute(name="RefractionIndex", value=restriction)
run("A bound can be inclusive 1/4", facet=facet, inst=element, expected=True)
element.RefractionIndex = 5
run("A bound can be inclusive 2/4", facet=facet, inst=element, expected=True)
element.RefractionIndex = 10
run("A bound can be inclusive 3/4", facet=facet, inst=element, expected=True)
element.RefractionIndex = 100
run("A bound can be inclusive 4/4", facet=facet, inst=element, expected=False)
restriction = Restriction(options={"minExclusive": 0, "maxExclusive": 10}, type="bounds", base="integer")
facet = Attribute(name="RefractionIndex", value=restriction)
element.RefractionIndex = 0
run("A bound can be inclusive 1/3", facet=facet, inst=element, expected=False)
element.RefractionIndex = 5
run("A bound can be inclusive 2/3", facet=facet, inst=element, expected=True)
element.RefractionIndex = 10
run("A bound can be inclusive 3/3", facet=facet, inst=element, expected=False)
ifc = ifcopenshell.file()
restriction = Restriction(options="[A-Z]{2}[0-9]{2}", type="pattern")
facet = Attribute(name="Name", value=restriction)
element = ifc.createIfcWall(Name="WT01")
run("Regex patterns can be used 1/3", facet=facet, inst=element, expected=True)
element.Name = "XY99"
run("Regex patterns can be used 2/3", facet=facet, inst=element, expected=True)
element.Name = "A5"
run("Regex patterns can be used 3/3", facet=facet, inst=element, expected=False)
+81 -8
View File
@@ -23,6 +23,14 @@ import ifcopenshell
from ifctester import ids
def run(name, specs, model, expected, applicable_entities, failed_entities):
specs.validate(model)
spec = specs.specifications[0]
assert spec.status is expected
assert set(spec.applicable_entities) == set(applicable_entities)
assert set(spec.requirements[0].failed_entities) == set(failed_entities)
class TestIds:
def test_failing_on_opening_invalid_ids_data(self):
with pytest.raises(xmlschema.validators.exceptions.XMLSchemaValidationError):
@@ -106,11 +114,73 @@ class TestIds:
model = ifcopenshell.file()
wall = model.createIfcWall()
waldo = model.createIfcWall(Name="Waldo")
specs.validate(model)
run("A minimal IDS can check a minimal IFC 1/2", specs, model, False, [wall, waldo], [wall])
wall.Name = "Waldo"
run("A minimal IDS can check a minimal IFC 2/2", specs, model, True, [wall, waldo], [])
assert spec.status == False
assert set(spec.applicable_entities) == {wall, waldo}
assert spec.requirements[0].failed_entities == [wall]
spec.ifcVersion = []
run(
"Specification version is purely metadata and does not impact pass or fail result",
specs,
model,
True,
[wall, waldo],
)
spec.minOccurs = 1
model = ifcopenshell.file()
waldo = model.createIfcWall(Name="Waldo")
run("Required specifications need at least one applicable entity 1/2", specs, model, True, [waldo])
model = ifcopenshell.file()
waldo = model.createIfcSlab(Name="Waldo")
run("Required specifications need at least one applicable entity 2/2", specs, model, False)
spec.minOccurs = 0
model = ifcopenshell.file()
waldo = model.createIfcSlab(Name="Waldo")
run("Optional specifications may still pass if nothing is applicable", specs, model, True)
spec.minOccurs = 0
spec.maxOccurs = 0
model = ifcopenshell.file()
wall = model.createIfcSlab(Name="Waldo")
run("Prohibited specifications fail if at least one entity passes all requirements 1/3", specs, model, True)
model = ifcopenshell.file()
wall = model.createIfcWall(Name="Wally")
run("Prohibited specifications fail if at least one entity passes all requirements 2/3", specs, model, True, [wall], [wall])
model = ifcopenshell.file()
wall = model.createIfcWall(Name="Waldo")
run("Prohibited specifications fail if at least one entity passes all requirements 3/3", specs, model, False, [wall])
spec.minOccurs = 0
spec.maxOccurs = "unbounded"
model = ifcopenshell.file()
wall = model.createIfcWall(Name="Waldo")
spec.requirements.append(ids.Attribute(name="Description", value="Foobar"))
run("A specification passes only if all requirements pass 1/2", specs, model, False, [wall], [wall])
wall.Description = "Foobar"
run("A specification passes only if all requirements pass 2/2", specs, model, True, [wall])
spec.requirements[1].minOccurs = 0
wall.Description = None
run("Specification optionality and facet optionality can be combined", specs, model, True, [wall])
spec.minOccurs = 0
spec.maxOccurs = 0
spec.requirements[0].minOccurs = 0
spec.requirements[0].maxOccurs = 0
spec.requirements[1].minOccurs = 0
spec.requirements[1].maxOccurs = 0
wall.Name = "Waldo"
wall.Description = "Foobar"
run("A prohibited specification and a prohibited facet results in a double negative", specs, model, True, [wall])
spec = ids.Specification(name="Name")
spec.applicability.append(ids.Entity(name="IFCWALL"))
spec.requirements.append(ids.Attribute(name="Name", value="Waldo"))
specs.specifications.append(spec)
wall2 = model.createIfcWall(Name="Waldo")
run("Multiple specifications are independent of one another", specs, model, True, [wall, wall2])
def test_creating_multiple_specifications(self):
specs = ids.Ids(title="Title")
@@ -138,9 +208,12 @@ class TestIds:
class TestSpecification:
def test_create_specification_with_minimal_information(self):
spec = ids.Specification()
print(spec.asdict())
assert spec.asdict() == {
"@name": "Unnamed",
"@ifcVersion": ["IFC2X3", "IFC4"],
"@minOccurs": 0,
"@maxOccurs": "unbounded",
"applicability": {},
"requirements": {},
}
@@ -148,8 +221,8 @@ class TestSpecification:
def test_create_specification_with_all_possible_information(self):
spec = ids.Specification(
name="name",
minOccurs="0",
maxOccurs="unbounded",
minOccurs=1,
maxOccurs=1,
ifcVersion="IFC4",
identifier="identifier",
description="description",
@@ -157,8 +230,8 @@ class TestSpecification:
)
assert spec.asdict() == {
"@name": "name",
"@minOccurs": "0",
"@maxOccurs": "unbounded",
"@minOccurs": 1,
"@maxOccurs": 1,
"@ifcVersion": "IFC4",
"@identifier": "identifier",
"@description": "description",