mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-14 03:14:23 +00:00
398 lines
15 KiB
Python
398 lines
15 KiB
Python
# IfcTester - IDS based model auditing
|
|
# Copyright (C) 2021-2022 Thomas Krijnen <thomas@aecgeeks.com>, Dion Moult <dion@thinkmoult.com>
|
|
#
|
|
# This file is part of IfcTester.
|
|
#
|
|
# IfcTester is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Lesser General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# IfcTester is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
from typing import Optional
|
|
|
|
import ifcopenshell
|
|
import ifcopenshell.api.material
|
|
import pytest
|
|
import xmlschema
|
|
|
|
from ifctester import ids
|
|
|
|
|
|
def run(
|
|
name: str,
|
|
ids: ids.Ids,
|
|
ifc: ifcopenshell.file,
|
|
expected: bool,
|
|
applicable_entities: Optional[list[ifcopenshell.entity_instance]] = None,
|
|
failed_entities: Optional[list[ifcopenshell.entity_instance]] = None,
|
|
):
|
|
ids.validate(ifc)
|
|
all_applicable = set()
|
|
all_failures = set()
|
|
if not applicable_entities:
|
|
applicable_entities = []
|
|
if not failed_entities:
|
|
failed_entities = []
|
|
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([f["element"] for f in requirement.failures])
|
|
assert set(all_applicable) == set(applicable_entities)
|
|
assert set(all_failures) == set(failed_entities)
|
|
|
|
|
|
class TestIds:
|
|
def test_failing_on_opening_invalid_ids_data(self):
|
|
with pytest.raises(ids.IdsXmlValidationError):
|
|
ids.open("""<?xml version="1.0" encoding="UTF-8"?><clearly_not_an_ids/>""")
|
|
|
|
def test_create_an_ids_with_minimal_information(self):
|
|
specs = ids.Ids()
|
|
assert specs.asdict() == {
|
|
"@xmlns": "http://standards.buildingsmart.org/IDS",
|
|
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
|
|
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/1.0/ids.xsd",
|
|
"info": {"title": "Untitled"},
|
|
"specifications": {"specification": []},
|
|
}
|
|
|
|
def test_reading_an_ids_from_an_xml_string(self):
|
|
specs = ids.Ids()
|
|
spec = ids.Specification(name="Name")
|
|
spec.applicability.append(ids.Entity(name="IFCWALL"))
|
|
specs.specifications.append(spec)
|
|
xml = specs.to_string()
|
|
specs2 = ids.from_string(xml)
|
|
assert len(specs2.specifications) == 1
|
|
|
|
def test_create_an_ids_with_all_possible_information(self):
|
|
specs = ids.Ids(
|
|
title="title",
|
|
copyright="copyright",
|
|
version="version",
|
|
description="description",
|
|
author="author@test.com",
|
|
date="2020-01-01",
|
|
purpose="purpose",
|
|
milestone="milestone",
|
|
)
|
|
assert specs.asdict() == {
|
|
"@xmlns": "http://standards.buildingsmart.org/IDS",
|
|
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
|
|
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/1.0/ids.xsd",
|
|
"info": {
|
|
"title": "title",
|
|
"copyright": "copyright",
|
|
"version": "version",
|
|
"description": "description",
|
|
"author": "author@test.com",
|
|
"date": "2020-01-01",
|
|
"purpose": "purpose",
|
|
"milestone": "milestone",
|
|
},
|
|
"specifications": {"specification": []},
|
|
}
|
|
|
|
def test_check_invalid_ids_information(self):
|
|
specs = ids.Ids(title=None, author="author", date="9999-99-99")
|
|
assert specs.asdict() == {
|
|
"@xmlns": "http://standards.buildingsmart.org/IDS",
|
|
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
|
|
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/1.0/ids.xsd",
|
|
"info": {"title": "Untitled"},
|
|
"specifications": {"specification": []},
|
|
}
|
|
|
|
def test_authoring_an_ids_with_no_specifications_is_invalid(self):
|
|
specs = ids.Ids()
|
|
with pytest.raises(xmlschema.validators.exceptions.XMLSchemaValidationError):
|
|
specs.to_string()
|
|
|
|
def test_saving_to_xml(self):
|
|
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"))
|
|
specs.specifications.append(spec)
|
|
fn = "tmp.xml"
|
|
specs.to_xml(fn)
|
|
os.remove(fn)
|
|
|
|
def test_creating_a_minimal_ids_and_validating(self):
|
|
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"))
|
|
specs.specifications.append(spec)
|
|
assert "http://standards.buildingsmart.org/IDS" in specs.to_string()
|
|
assert spec.status == None
|
|
|
|
model = ifcopenshell.file()
|
|
wall = model.createIfcWall()
|
|
waldo = model.createIfcWall(Name="Waldo")
|
|
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])
|
|
|
|
spec.ifcVersion = ["IFC2X3"]
|
|
run(
|
|
"Specification version is purely metadata and does not impact pass or fail result",
|
|
specs,
|
|
model,
|
|
True,
|
|
[wall, waldo],
|
|
)
|
|
|
|
spec.ifcVersion = []
|
|
spec.set_usage("required")
|
|
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.set_usage("optional")
|
|
model = ifcopenshell.file()
|
|
waldo = model.createIfcSlab(Name="Waldo")
|
|
run("Optional specifications may still pass if nothing is applicable", specs, model, True)
|
|
|
|
specs = ids.Ids(title="Title")
|
|
spec = ids.Specification(name="Name")
|
|
spec.applicability.append(ids.Entity(name="IFCWALL"))
|
|
spec.applicability.append(ids.Attribute(name="Name", value="Waldo"))
|
|
specs.specifications.append(spec)
|
|
spec.set_usage("prohibited")
|
|
model = ifcopenshell.file()
|
|
wall = model.createIfcSlab(Name="Waldo")
|
|
run("Prohibited specifications fail if at least one entity is applicable 1/3", specs, model, True)
|
|
model = ifcopenshell.file()
|
|
wall = model.createIfcWall(Name="Wally")
|
|
run(
|
|
"Prohibited specifications fail if at least one entity is applicable 2/3",
|
|
specs,
|
|
model,
|
|
True,
|
|
[],
|
|
[],
|
|
)
|
|
model = ifcopenshell.file()
|
|
wall = model.createIfcWall(Name="Waldo")
|
|
run(
|
|
"Prohibited specifications fail if at least one entity is applicable 3/3",
|
|
specs,
|
|
model,
|
|
False,
|
|
[wall],
|
|
[],
|
|
)
|
|
|
|
spec.set_usage("optional")
|
|
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])
|
|
|
|
# specs independency
|
|
specs = ids.Ids(title="Title")
|
|
spec = ids.Specification(name="Name")
|
|
spec.applicability.append(ids.Entity(name="IFCWALL"))
|
|
spec.requirements.append(ids.Attribute(name="Description", value="Foobar"))
|
|
specs.specifications.append(spec)
|
|
|
|
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)
|
|
|
|
wall.Name = "Waldo"
|
|
wall.Description = "Foobar"
|
|
wall2 = model.createIfcWall(Name="Waldo", Description="Foobar")
|
|
run("Multiple specifications are independent of one another", specs, model, True, [wall, wall2])
|
|
|
|
def test_creating_multiple_specifications(self):
|
|
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"))
|
|
specs.specifications.append(spec)
|
|
|
|
spec2 = ids.Specification(name="Name")
|
|
spec2.applicability.append(ids.Entity(name="IFCWALL"))
|
|
spec2.requirements.append(ids.Attribute(name="Name", value="Waldo"))
|
|
specs.specifications.append(spec2)
|
|
|
|
model = ifcopenshell.file()
|
|
wall = model.createIfcWall()
|
|
waldo = model.createIfcWall(Name="Waldo")
|
|
specs.validate(model)
|
|
|
|
assert spec.status == False
|
|
assert set(spec.applicable_entities) == {wall, waldo}
|
|
assert len(spec.requirements[0].failures) == 1
|
|
assert len(spec2.requirements[0].failures) == 1
|
|
assert spec.requirements[0].failures[0]["element"] == wall
|
|
assert spec2.requirements[0].failures[0]["element"] == wall
|
|
|
|
def test_parsing_entities_with_no_attributes(self):
|
|
model = ifcopenshell.file()
|
|
wall1 = model.createIfcWall(Name="Waldo")
|
|
wall2 = model.createIfcWall(Name="Wally")
|
|
model.createIfcWall(Name="Walter")
|
|
material = model.createIfcMaterial()
|
|
ifcopenshell.api.material.assign_material(model, products=[wall1, wall2], material=material)
|
|
specs = ids.Ids(title="Title")
|
|
spec = ids.Specification(name="Name")
|
|
spec.applicability.append(ids.Material())
|
|
spec.requirements.append(ids.Attribute(name="Name", value="Waldo"))
|
|
specs.specifications.append(spec)
|
|
xml = specs.to_string()
|
|
specs = ids.from_string(xml)
|
|
run(
|
|
"The material facet with no attributes selects elements with a material",
|
|
specs,
|
|
model,
|
|
False,
|
|
[wall1, wall2],
|
|
[wall2],
|
|
)
|
|
|
|
|
|
class TestSpecification:
|
|
def test_create_specification_with_minimal_information(self):
|
|
spec = ids.Specification()
|
|
assert spec.asdict() == {
|
|
"@name": "Unnamed",
|
|
"@ifcVersion": ["IFC2X3", "IFC4", "IFC4X3_ADD2"],
|
|
"applicability": {},
|
|
"requirements": {},
|
|
}
|
|
|
|
def test_create_specification_with_all_possible_information(self):
|
|
spec = ids.Specification(
|
|
name="name",
|
|
minOccurs=1,
|
|
maxOccurs=1,
|
|
ifcVersion="IFC4",
|
|
identifier="identifier",
|
|
description="description",
|
|
instructions="instructions",
|
|
)
|
|
assert spec.asdict() == {
|
|
"@name": "name",
|
|
"@ifcVersion": "IFC4",
|
|
"@identifier": "identifier",
|
|
"@description": "description",
|
|
"@instructions": "instructions",
|
|
"applicability": {},
|
|
"requirements": {},
|
|
}
|
|
|
|
def test_specification_has_no_requirements(self):
|
|
model = ifcopenshell.file()
|
|
wall = model.createIfcWall()
|
|
waldo = model.createIfcWall(Name="Waldo")
|
|
|
|
test_ids = ids.Ids(title="Title")
|
|
spec = ids.Specification(name="Name")
|
|
spec.applicability.append(ids.Entity(name="IFCWALL"))
|
|
test_ids.specifications.append(spec)
|
|
spec.set_usage("required")
|
|
run(
|
|
"A specification that is required and has at least one applicable entity but no requirements shall pass",
|
|
test_ids,
|
|
model,
|
|
True,
|
|
[wall, waldo],
|
|
None,
|
|
)
|
|
|
|
test_ids = ids.Ids(title="Title")
|
|
spec = ids.Specification(name="Name")
|
|
test_ids.specifications.append(spec)
|
|
spec.set_usage("required")
|
|
run(
|
|
"A specification that is required but has no applicable entities or requirements shall fail",
|
|
test_ids,
|
|
model,
|
|
False,
|
|
None,
|
|
None,
|
|
)
|
|
|
|
test_ids = ids.Ids(title="Title")
|
|
spec = ids.Specification(name="Name")
|
|
spec.applicability.append(ids.Entity(name="IFCWALL"))
|
|
test_ids.specifications.append(spec)
|
|
spec.set_usage("optional")
|
|
run(
|
|
"A specification that is optional and has at least one applicable entity but no requirements shall pass",
|
|
test_ids,
|
|
model,
|
|
True,
|
|
[wall, waldo],
|
|
None,
|
|
)
|
|
|
|
test_ids = ids.Ids(title="Title")
|
|
spec = ids.Specification(name="Name")
|
|
spec.applicability.append(ids.Entity(name="IFCWALL"))
|
|
test_ids.specifications.append(spec)
|
|
spec.set_usage("prohibited")
|
|
run(
|
|
"A specification that is prohibited and has at least one applicable entity but no requirements shall fail",
|
|
test_ids,
|
|
model,
|
|
False,
|
|
[wall, waldo],
|
|
None,
|
|
)
|
|
|
|
test_ids = ids.Ids(title="Title")
|
|
spec = ids.Specification(name="Name")
|
|
test_ids.specifications.append(spec)
|
|
spec.set_usage("prohibited")
|
|
run(
|
|
"A specification that is prohibited but has no applicable entities or requirements shall pass",
|
|
test_ids,
|
|
model,
|
|
True,
|
|
None,
|
|
None,
|
|
)
|
|
|
|
def test_prohibited_facet(self):
|
|
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="prohibited"))
|
|
specs.specifications.append(spec)
|
|
|
|
spec.set_usage("required")
|
|
model = ifcopenshell.file()
|
|
wall = model.createIfcWall(Name="Wally")
|
|
run(
|
|
"Prohibited facet not to fail if no entity passes it",
|
|
specs,
|
|
model,
|
|
True,
|
|
[wall],
|
|
[],
|
|
)
|