From d0eb7167381f14819075deea9f694c1fcb374e37 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 8 Sep 2021 20:16:39 +1000 Subject: [PATCH] Fix #1727. BIMTester now runs within the BlenderBIM Add-on, implement attribute checking in IDS, and fix some minor IDS bugs. --- src/blenderbim/Makefile | 13 +++++ .../bim/module/bimtester/operator.py | 20 +------ src/blenderbim/test/bim/bootstrap.py | 26 +++++++-- .../test/bim/module/bimtester/__init__.py | 0 .../bim/module/bimtester/test_operator.py | 31 +++++++++++ .../test/bim/module/root/test_operator.py | 18 +++---- src/blenderbim/test/files/sample-ids.xml | 18 +++++++ src/ifcbimtester/bimtester/reports.py | 9 ++-- src/ifcbimtester/bimtester/run.py | 2 +- src/ifcopenshell-python/ifcopenshell/ids.py | 53 ++++++++++--------- 10 files changed, 129 insertions(+), 61 deletions(-) create mode 100644 src/blenderbim/test/bim/module/bimtester/__init__.py create mode 100644 src/blenderbim/test/bim/module/bimtester/test_operator.py create mode 100644 src/blenderbim/test/files/sample-ids.xml diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 71ddad9744..3447aa25da 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -33,11 +33,13 @@ ifeq ($(PYVERSION), py37) HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp-fcl-1.7.5-py37h5f1835d_0.tar.bz2 EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py37h95e2c48_0.tar.bz2 BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py37h0379df6_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/30/c0/d0526314971fc661b083ab135747dc68446a3022686da8c16d25fcf6ef07/lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl endif ifeq ($(PYVERSION), py39) HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp-fcl-1.7.5-py39hbcdfc36_0.tar.bz2 EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py39h5aed9d1_0.tar.bz2 BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py39h5472131_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/19/d9/a69c6aff5673554df48120565a14a50eaa41d29ae03b02faa0b023666318/lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl endif ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/linux-64/assimp-5.0.1-hedfc422_6.tar.bz2 OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/linux-64/octomap-1.9.7-h4bd325d_0.tar.bz2 @@ -49,11 +51,13 @@ ifeq ($(PYVERSION), py37) HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py37h2d7f23a_0.tar.bz2 EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2 BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/1e/3e/f0abc15d5dac50939bccc589aae336d5ead4c72e7ad1039a2e0f3630ea92/lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl endif ifeq ($(PYVERSION), py39) HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py39h1e32b98_0.tar.bz2 EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py39h5405915_0.tar.bz2 BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py39ha641261_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/b8/74/a71f7ad72e8db54ce899efab84507b801660750cbbfa6a39e6717557d36a/lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl endif ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-64/assimp-5.0.1-h1224e73_6.tar.bz2 OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/osx-64/octomap-1.9.7-h940c156_0.tar.bz2 @@ -65,11 +69,13 @@ ifeq ($(PYVERSION), py37) HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-fcl-1.7.5-py37h839d6b1_0.tar.bz2 EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py37h2c32e34_0.tar.bz2 BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py37h3b38789_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/9e/5e/171ee9d40a600f565fe691ec5bf7596247ec62cfb2edc00c91afe8ea837b/lxml-4.6.3-cp37-cp37m-win_amd64.whl endif ifeq ($(PYVERSION), py39) HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-fcl-1.7.5-py39h2e7c763_0.tar.bz2 EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py39h3ce40e6_0.tar.bz2 BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py39hefe7e4c_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/72/d4/426ecb8849c47c3e370c87aa0ac05d85768df917ffea27fcd6686a5e6495/lxml-4.6.3-cp39-cp39-win_amd64.whl endif ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/win-64/assimp-5.0.1-hc2aa0de_6.tar.bz2 OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/win-64/octomap-1.9.7-h5362a0b_0.tar.bz2 @@ -350,6 +356,13 @@ endif cd dist/working/ && patch ../blenderbim/libs/site/packages/behave/runner_util.py < runner_util.patch rm -rf dist/working + # Required by ids + mkdir dist/working + cd dist/working && wget $(LXML_URL) + cd dist/working && cp *.whl lxml.zip && unzip lxml.zip + cp -r dist/working/lxml dist/blenderbim/libs/site/packages/ + rm -rf dist/working + # Required by behave mkdir dist/working cd dist/working && wget https://files.pythonhosted.org/packages/f4/65/220bb4075fddb09d5b3ea2c1c1fa66c1c72be9361ec187aab50fa161e576/parse-1.15.0.tar.gz diff --git a/src/blenderbim/blenderbim/bim/module/bimtester/operator.py b/src/blenderbim/blenderbim/bim/module/bimtester/operator.py index 7bf9905313..d7c229f13d 100644 --- a/src/blenderbim/blenderbim/bim/module/bimtester/operator.py +++ b/src/blenderbim/blenderbim/bim/module/bimtester/operator.py @@ -21,7 +21,6 @@ import os import bpy import tempfile import webbrowser -import ifcopenshell try: import bimtester @@ -31,7 +30,6 @@ except: print("Failed to load BIMTester. Try disabling other add-ons, in particular Blender-OSM. See bug #1318.") from pathlib import Path -from itertools import cycle from blenderbim.bim.ifc import IfcStore @@ -42,7 +40,7 @@ class ExecuteBIMTester(bpy.types.Operator): @classmethod def poll(cls, context): props = context.scene.BimTesterProperties - return props.ifc_file and props.feature + return (props.ifc_file or props.should_load_from_memory) and props.feature def execute(self, context): props = context.scene.BimTesterProperties @@ -243,19 +241,3 @@ class QAHelper: is_in_scenario = False destination.write(source_line) os.remove(filename + "~") - - -colour_list = [ - (0.651, 0.81, 0.892, 1), - (0.121, 0.471, 0.706, 1), - (0.699, 0.876, 0.54, 1), - (0.199, 0.629, 0.174, 1), - (0.983, 0.605, 0.602, 1), - (0.89, 0.101, 0.112, 1), - (0.989, 0.751, 0.427, 1), - (0.986, 0.497, 0.1, 1), - (0.792, 0.699, 0.839, 1), - (0.414, 0.239, 0.603, 1), - (0.993, 0.999, 0.6, 1), - (0.693, 0.349, 0.157, 1), -] diff --git a/src/blenderbim/test/bim/bootstrap.py b/src/blenderbim/test/bim/bootstrap.py index ae18d2ff31..f4230acde2 100644 --- a/src/blenderbim/test/bim/bootstrap.py +++ b/src/blenderbim/test/bim/bootstrap.py @@ -17,14 +17,19 @@ # along with BlenderBIM Add-on. If not, see . +import os import re import bpy import pytest +import webbrowser import blenderbim import ifcopenshell import ifcopenshell.util.representation from blenderbim.bim.ifc import IfcStore +# Monkey-patch webbrowser opening since we want to test headlessly +webbrowser.open = lambda x: True + class NewFile: @pytest.fixture(autouse=True) @@ -64,8 +69,15 @@ def the_object_name_is_selected(name): obj.select_set(True) -def i_select_value_in_prop(value, prop): - exec(f'bpy.context.{prop} = "{value}"') +def i_set_prop_to_value(prop, value): + try: + exec(f'bpy.context.{prop} = "{value}"') + except: + exec(f"bpy.context.{prop} = {value}") + + +def i_enable_prop(prop): + exec(f"bpy.context.{prop} = True") def i_press_operator(operator): @@ -136,11 +148,17 @@ def the_object_name1_and_name2_are_different_elements(name1, name2): assert element1 != element2, f"Objects {name1} and {name2} have same elements {element1} and {element2}" +def the_file_name_should_contain_value(name, value): + with open(name, "r") as f: + assert value in f.read() + + definitions = { "an empty IFC project": an_empty_ifc_project, "I add a cube": i_add_a_cube, 'the object "(.*)" is selected': the_object_name_is_selected, - 'I select "(.*)" in "(.*)"': i_select_value_in_prop, + 'I set "(.*)" to "(.*)"': i_set_prop_to_value, + 'I enable "(.*)"': i_enable_prop, 'I press "(.*)"': i_press_operator, 'the object "(.*)" is an "(.*)"': the_object_name_is_an_ifc_class, 'the object "(.*)" is in the collection "(.*)"': the_object_name_is_in_the_collection_collection, @@ -151,6 +169,7 @@ definitions = { 'the object "(.*)" is contained in "(.*)"': the_object_name_is_contained_in_container_name, "I duplicate the selected objects": i_duplicate_the_selected_objects, 'the object "(.*)" and "(.*)" are different elements': the_object_name1_and_name2_are_different_elements, + 'the file "(.*)" should contain "(.*)"': the_file_name_should_contain_value, } @@ -158,6 +177,7 @@ definitions = { def run(scenario): for line in scenario.split("\n"): line = line.strip() + line = line.replace("{cwd}", os.getcwd()) if not line: continue match = None diff --git a/src/blenderbim/test/bim/module/bimtester/__init__.py b/src/blenderbim/test/bim/module/bimtester/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/blenderbim/test/bim/module/bimtester/test_operator.py b/src/blenderbim/test/bim/module/bimtester/test_operator.py new file mode 100644 index 0000000000..0834a8fbb0 --- /dev/null +++ b/src/blenderbim/test/bim/module/bimtester/test_operator.py @@ -0,0 +1,31 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import test.bim.bootstrap + + +class TestExecuteBIMTester(test.bim.bootstrap.NewFile): + @test.bim.bootstrap.scenario + def test_executing_bimtester(self): + return """ + Given an empty IFC project + When I enable "scene.BimTesterProperties.should_load_from_memory" + And I set "scene.BimTesterProperties.feature" to "{cwd}/test/files/sample-ids.xml" + And I press "bim.execute_bim_tester" + Then the file "{cwd}/test/files/sample-ids.xml.html" should contain "Tests passed: 1 / 1 (100%)" + """ diff --git a/src/blenderbim/test/bim/module/root/test_operator.py b/src/blenderbim/test/bim/module/root/test_operator.py index 335d45d9cb..e49310d3e2 100644 --- a/src/blenderbim/test/bim/module/root/test_operator.py +++ b/src/blenderbim/test/bim/module/root/test_operator.py @@ -26,7 +26,7 @@ class TestAssignClass(test.bim.bootstrap.NewFile): Given an empty IFC project When I add a cube And the object "Cube" is selected - And I select "IfcWall" in "scene.BIMRootProperties.ifc_class" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" is an "IfcWall" And the object "IfcWall/Cube" is in the collection "Collection" @@ -39,8 +39,8 @@ class TestAssignClass(test.bim.bootstrap.NewFile): Given an empty IFC project When I add a cube And the object "Cube" is selected - And I select "IfcElementType" in "scene.BIMRootProperties.ifc_product" - And I select "IfcWallType" in "scene.BIMRootProperties.ifc_class" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" Then the object "IfcWallType/Cube" is an "IfcWallType" And the object "IfcWallType/Cube" is in the collection "Types" @@ -53,8 +53,8 @@ class TestAssignClass(test.bim.bootstrap.NewFile): Given an empty IFC project When I add a cube And the object "Cube" is selected - And I select "IfcSpatialElement" in "scene.BIMRootProperties.ifc_product" - And I select "IfcBuilding" in "scene.BIMRootProperties.ifc_class" + And I set "scene.BIMRootProperties.ifc_product" to "IfcSpatialElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcBuilding" And I press "bim.assign_class" Then the object "IfcBuilding/Cube" is an "IfcBuilding" And the object "IfcBuilding/Cube" is in the collection "IfcBuilding/Cube" @@ -67,8 +67,8 @@ class TestAssignClass(test.bim.bootstrap.NewFile): Given an empty IFC project When I add a cube And the object "Cube" is selected - And I select "IfcElement" in "scene.BIMRootProperties.ifc_product" - And I select "IfcOpeningElement" in "scene.BIMRootProperties.ifc_class" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" And I press "bim.assign_class" Then the object "IfcOpeningElement/Cube" is an "IfcOpeningElement" And the object "IfcOpeningElement/Cube" is in the collection "IfcOpeningElements" @@ -82,7 +82,7 @@ class TestAssignClass(test.bim.bootstrap.NewFile): When I add a cube And the object "Cube" is selected And the object "Cube" is placed in the collection "IfcBuildingStorey/My Storey" - And I select "IfcWall" in "scene.BIMRootProperties.ifc_class" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" is contained in "My Storey" """ @@ -95,7 +95,7 @@ class TestCopyClass(test.bim.bootstrap.NewFile): Given an empty IFC project When I add a cube And the object "Cube" is selected - And I select "IfcWall" in "scene.BIMRootProperties.ifc_class" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I duplicate the selected objects Then the object "IfcWall/Cube" and "IfcWall/Cube.001" are different elements diff --git a/src/blenderbim/test/files/sample-ids.xml b/src/blenderbim/test/files/sample-ids.xml new file mode 100644 index 0000000000..95592edaa9 --- /dev/null +++ b/src/blenderbim/test/files/sample-ids.xml @@ -0,0 +1,18 @@ + + + + + + IfcBuilding + + + + + attribute + name + My Building + + + + + diff --git a/src/ifcbimtester/bimtester/reports.py b/src/ifcbimtester/bimtester/reports.py index c76cb9b26c..708705eb55 100644 --- a/src/ifcbimtester/bimtester/reports.py +++ b/src/ifcbimtester/bimtester/reports.py @@ -41,13 +41,16 @@ class ReportGenerator: return for scenario in feature["elements"]: - scenario_data = self.process_scenario(scenario) + scenario_data = self.process_scenario(scenario, feature) if scenario_data: data["scenarios"].append(scenario_data) data["total_passes"] = sum([s["total_passes"] for s in data["scenarios"]]) data["total_steps"] = sum([s["total_steps"] for s in data["scenarios"]]) - data["pass_rate"] = round((data["total_passes"] / data["total_steps"]) * 100) + try: + data["pass_rate"] = round((data["total_passes"] / data["total_steps"]) * 100) + except ZeroDivisionError: + data["pass_rate"] = 0 data.update(self.get_template_strings()) @@ -57,7 +60,7 @@ class ReportGenerator: ) as template: out.write(pystache.render(template.read(), data)) - def process_scenario(self, scenario): + def process_scenario(self, scenario, feature): if len(scenario["steps"]) == 0: print("Scenario '{}' in feature '{}' has no steps.".format(scenario["name"], feature["name"])) return diff --git a/src/ifcbimtester/bimtester/run.py b/src/ifcbimtester/bimtester/run.py index a80867ffeb..60d76d4074 100644 --- a/src/ifcbimtester/bimtester/run.py +++ b/src/ifcbimtester/bimtester/run.py @@ -88,7 +88,7 @@ class TestRunner: logging.basicConfig(level=logging.INFO, format="%(message)s") ids_handler = IDSHandler() logger.addHandler(ids_handler) - ids_file = ifcopenshell.ids.ids(args["feature"]) + ids_file = ifcopenshell.ids.ids.open(args["feature"]) ids_file.validate(IfcStore.file, logger) tmpdir = tempfile.mkdtemp() diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index adb2bf3ee6..a45f43c642 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -16,11 +16,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import os import re import logging -import operator -import os -import csv import numpy as np from datetime import date @@ -30,13 +28,8 @@ import ifcopenshell.util.placement from bcf.v2.bcfxml import BcfXml from bcf.v2 import data as bcf -from bcf import bcfxml - from xmlschema import XMLSchema -from xmlschema import XMLSchemaConverter from xmlschema import etree_tostring -from lxml import etree as ElementTree -from xmlschema.validators import facets from xmlschema.validators import identities @@ -233,13 +226,18 @@ class ids: else: logger.debug("No applicable elements found. None required.") + try: + percentage = self.ifc_passed / self.ifc_applicable * 100 + except ZeroDivisionError: + percentage = 0 + logger.debug( "Out of %s IFC elements, %s were applicable and %s of them passed (%s)." % ( len(ifc_file.by_type("IfcProduct")), self.ifc_applicable, self.ifc_passed, - str(self.ifc_passed / self.ifc_applicable * 100) + "%", + str(percentage) + "%", ) ) for h in logger.handlers: @@ -686,25 +684,28 @@ class property(facet): self.location = self.node["@location"] - # TODO sometimes AttributeError: 'str' object has no attribute 'wrappedValue' - instance_props = ifcopenshell.util.element.get_psets(inst) - - if ifcopenshell.util.element.get_type(inst): - type_props = ifcopenshell.util.element.get_psets(ifcopenshell.util.element.get_type(inst)) + if self.propertyset == "attribute": + val = {k.lower(): v for k, v in inst.get_info().items()}.get(self.name, None) else: - type_props = {} + # TODO sometimes AttributeError: 'str' object has no attribute 'wrappedValue' + instance_props = ifcopenshell.util.element.get_psets(inst) - if self.location == "instance": - props = instance_props - elif self.location == "type" and type_props: - props = type_props - elif self.location == "any" and (instance_props or type_props): - props = {**instance_props, **type_props} - else: - props = {} + if ifcopenshell.util.element.get_type(inst): + type_props = ifcopenshell.util.element.get_psets(ifcopenshell.util.element.get_type(inst)) + else: + type_props = {} - pset = props.get(self.propertyset) - val = pset.get(self.name) if pset else None + if self.location == "instance": + props = instance_props + elif self.location == "type" and type_props: + props = type_props + elif self.location == "any" and (instance_props or type_props): + props = {**instance_props, **type_props} + else: + props = {} + + pset = props.get(self.propertyset) + val = pset.get(self.name) if pset else None self.location_msg = location[self.location] di = {"name": self.name, "propertyset": self.propertyset, "value": "'%s'" % val, "location": self.location_msg} @@ -913,7 +914,7 @@ class restriction: elif n[3:6] == "max": r.options.append("<=") else: - self.options.append("==") + r.options.append("==") r.options[-1] += str(ids_dict[n]["@value"]) elif n == "pattern": r.type = "pattern"