From 860d795ba1af396f8a97eaf2f6ee8a5cec493242 Mon Sep 17 00:00:00 2001 From: Roch Bertucat Date: Thu, 10 Dec 2020 10:35:13 +0100 Subject: [PATCH 01/13] Add a way to generate a report after the tests in the same command --- src/ifcbimtester/startbimtester.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ifcbimtester/startbimtester.py b/src/ifcbimtester/startbimtester.py index 436a3c6703..88b755ae6a 100644 --- a/src/ifcbimtester/startbimtester.py +++ b/src/ifcbimtester/startbimtester.py @@ -42,6 +42,12 @@ if __name__ == "__main__": action="store_true", help="Generate a HTML report" ) + parser.add_argument( + "-rr", + "--report_after_run", + action="store_true", + help="Generate a HTML report after running the tests" + ) parser.add_argument( "-c", "--console", @@ -103,4 +109,6 @@ if __name__ == "__main__": show_widget(args["featuresdir"], args["ifcfile"]) else: run.run_tests(args) + if args["report_after_run"]: + reports.generate_report() print("# All tasks are complete :-)") From cada9c9e3b6568db1d8cb0320444032c8536e27a Mon Sep 17 00:00:00 2001 From: Roch Bertucat Date: Thu, 10 Dec 2020 10:35:47 +0100 Subject: [PATCH 02/13] Specify encoding for HTML report for special characters --- src/ifcbimtester/bimtester/reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcbimtester/bimtester/reports.py b/src/ifcbimtester/bimtester/reports.py index cc6a09373f..bb82ea3b71 100644 --- a/src/ifcbimtester/bimtester/reports.py +++ b/src/ifcbimtester/bimtester/reports.py @@ -94,6 +94,6 @@ def generate_report(adir="."): data["pass_rate"] = round((data["total_passes"] / data["total_steps"]) * 100) html_report_file = os.path.join(report_dir, "{}.html".format(file_name)) - with open(html_report_file, "w") as out: + with open(html_report_file, "w", encoding="utf-8") as out: with open(html_template_file) as template: out.write(pystache.render(template.read(), data)) From 479b937f81fb354ad4c3d206892cf1d41e002c62 Mon Sep 17 00:00:00 2001 From: Roch Bertucat Date: Thu, 10 Dec 2020 10:37:17 +0100 Subject: [PATCH 03/13] Add a way to load a custom IFC Schema and add util methods --- .../bimtester/features/steps/ifcdata.py | 7 +++++++ .../bimtester/features/steps/utils.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/ifcbimtester/bimtester/features/steps/ifcdata.py b/src/ifcbimtester/bimtester/features/steps/ifcdata.py index 11c4a02053..a6e69e2bf8 100644 --- a/src/ifcbimtester/bimtester/features/steps/ifcdata.py +++ b/src/ifcbimtester/bimtester/features/steps/ifcdata.py @@ -2,6 +2,13 @@ from behave import step from utils import IfcFile +@step('The IFC schema "{schema}" must be provided') +def step_impl(context, schema): + try: + IfcFile.load_schema(schema) + except: + assert False + @step('The IFC file "{file}" must be provided') def step_impl(context, file): diff --git a/src/ifcbimtester/bimtester/features/steps/utils.py b/src/ifcbimtester/bimtester/features/steps/utils.py index 038b0c4149..3d348ce95f 100644 --- a/src/ifcbimtester/bimtester/features/steps/utils.py +++ b/src/ifcbimtester/bimtester/features/steps/utils.py @@ -1,4 +1,5 @@ import ifcopenshell +import ifcopenshell.express import ifcopenshell.util import ifcopenshell.util.element @@ -10,6 +11,13 @@ class IfcFile(object): @classmethod def load(cls, path=None): cls.file = ifcopenshell.open(path) + if not cls.file: + assert False + + @classmethod + def load_schema(cls, path=None): + schema = ifcopenshell.express.parse(path) + ifcopenshell.register_schema(schema) @classmethod def get(cls): @@ -23,6 +31,17 @@ class IfcFile(object): return cls.get().by_guid(guid) except: assert False, "An element with the ID {} could not be found.".format(guid) + + @classmethod + def by_type(cls, ifc_type): + return cls.get().by_type(ifc_type.strip()) + + @classmethod + def by_types(cls, ifc_types): + elements = [] + for ifc_type in ifc_types.split(","): + elements += cls.by_type(ifc_type.strip()) + return elements def assert_number(number): From 34174dd3f31e0091bf0667f66a89f32bec770fa4 Mon Sep 17 00:00:00 2001 From: Roch Bertucat Date: Thu, 10 Dec 2020 10:42:49 +0100 Subject: [PATCH 04/13] Add a set of steps to test aggregations in IFC files --- .../bimtester/features/steps/aggregation.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/ifcbimtester/bimtester/features/steps/aggregation.py diff --git a/src/ifcbimtester/bimtester/features/steps/aggregation.py b/src/ifcbimtester/bimtester/features/steps/aggregation.py new file mode 100644 index 0000000000..f3bbec37b8 --- /dev/null +++ b/src/ifcbimtester/bimtester/features/steps/aggregation.py @@ -0,0 +1,114 @@ +from behave import step, given, when, then, use_step_matcher + +from utils import IfcFile + +use_step_matcher("parse") +@step(u"There must be exactly {number} {ifc_class} element") +@step(u"There must be exactly {number} {ifc_class} elements") +def step_impl(context, number, ifc_class): + num = len(IfcFile.get().by_type(ifc_class)) + assert num == int(number), "Could not find {} elements of {}. Found {} element(s).".format(number, ifc_class, num) + +@given(u'a set of specific related elements') +def step_impl(context): + model = getattr(context, "model", None) + if not model: + context.model = TableModel() + for row in context.table: + context.model.add_row(row["RelatedObjects"], row["RelatingGroup"]) + +@given(u'a set of specific related elements taken from the file "{path_file}"') +def step_impl(context, path_file): + import csv + model = getattr(context, "model", None) + if not model: + context.model = TableModel() + with open(path_file, encoding="utf-8") as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + context.model.add_row(row["RelatedObjects"], row["RelatingGroup"]) + +@then(u'there must be exactly a number of {ifc_class} equals to the number of distinct row value') +def step_impl(context, ifc_class): + try: + context.execute_steps(u""" + then There must be exactly {number} {ifc_class} elements + """.format(ifc_class=ifc_class, number=context.model.get_count_distinct_values())) + except AssertionError as error: + str_error = str(error) + assert False, str_error[:str_error.find("Traceback")] + assert True + +@then(u'there is a relationship {ifc_class} with {left_attribute} and {right_attribute} between the two elements of each row') +def step_impl(context, ifc_class, left_attribute, right_attribute): + rows = context.model.rows + elements = IfcFile.by_type(ifc_class) + errors = [] + for key, value in rows.items(): + found = False + for element in elements: + if any(x.Name == key for x in getattr(element, left_attribute))\ + and getattr(element, right_attribute).Name == value: + found = True + if not found: + errors.append(f'The row ({key}, {value}) does not have the relationship.') + assert not errors, "Errors occured:\n{}".format("\n".join(errors)) + +use_step_matcher("re") +@step("all IfcGroup must be linked to a type in the list (?P.*)") +def step_impl(context, linked_ifc_classes): + groups = IfcFile.by_type("IfcGroup") + errors = [] + for group in groups: + if not hasattr(group, "IsGroupedBy"): + errors.append(f'The element "{group.Name}" has no "IsGroupedBy" attribute.') + else: + for grouped_by in getattr(group, "IsGroupedBy"): + if not hasattr(grouped_by, "RelatedObjects"): + errors.append(f'The element "{grouped_by.Name}" has no "RelatedObjects" attribute.') + else: + for related_object in getattr(grouped_by, "RelatedObjects"): + found = False + for linked_ifc_class in linked_ifc_classes.split(","): + if(related_object.is_a(linked_ifc_class)): + found = True + if not found: + errors.append(f'The element "{related_object.Name}" does not have the right associated type.') + assert not errors, "Errors occured:\n{}".format("\n".join(errors)) + +@then(u'there is an element of type (?P.*) with a (?P.*) attribute for each row key') +def step_impl(context, ifc_types, attribute_name): + check_if_element_exists_by_types_with_attribute_name(ifc_types, attribute_name, context.model.rows.keys()) + +@then(u'there is an element of type (?P.*) with a (?P.*) attribute for each row value') +def step_impl(context, ifc_types, attribute_name): + values = set(context.model.rows.values()) + check_if_element_exists_by_types_with_attribute_name(ifc_types, attribute_name, values) + +def check_if_element_exists_by_types_with_attribute_name(ifc_types, attribute_name, attribute_values): + errors = [] + # retrieve all elements of that type + elements = IfcFile.by_types(ifc_types) + # loop + for attribute_value in attribute_values: + found = False + for element in elements: + if hasattr(element, attribute_name) and getattr(element, attribute_name) == attribute_value: + found = True + if not found: + errors.append(f'An element with {attribute_name} attribute "{attribute_value}" was not found.') + assert not errors, "Errors occured:\n{}".format("\n".join(errors)) + +class TableModel(object): + """This class represents a table of data.""" + def __init__(self): + self.rows = dict() + + def add_row(self, related, relating): + self.rows[related] = relating + + def get_count(self): + return len(self.rows) + + def get_count_distinct_values(self): + return len(set(self.rows.values())) \ No newline at end of file From e01b677845fba9678d62bbbf01e1e851d0284f87 Mon Sep 17 00:00:00 2001 From: Roch Bertucat Date: Fri, 18 Dec 2020 11:20:42 +0100 Subject: [PATCH 05/13] Add build/dist folders to gitignore --- src/ifcbimtester/.gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/ifcbimtester/.gitignore diff --git a/src/ifcbimtester/.gitignore b/src/ifcbimtester/.gitignore new file mode 100644 index 0000000000..db03d2ec9b --- /dev/null +++ b/src/ifcbimtester/.gitignore @@ -0,0 +1,3 @@ +# Dependency and build folders created by the build scripts +/build/ +/dist/ \ No newline at end of file From 9ffc2874daaf8dee56ddb4ccd90e6f3b7e099a8b Mon Sep 17 00:00:00 2001 From: Roch Bertucat Date: Fri, 18 Dec 2020 11:22:14 +0100 Subject: [PATCH 06/13] Add a way to stop if fail step and improve report accordingly --- .../bimtester/features/environment.py | 2 +- src/ifcbimtester/bimtester/reports.py | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/ifcbimtester/bimtester/features/environment.py b/src/ifcbimtester/bimtester/features/environment.py index 167171f90a..b1bd18adfc 100644 --- a/src/ifcbimtester/bimtester/features/environment.py +++ b/src/ifcbimtester/bimtester/features/environment.py @@ -3,5 +3,5 @@ from behave.model import Scenario def before_all(context): userdata = context.config.userdata - continue_after_failed = True + continue_after_failed = userdata.getbool("runner.continue_after_failed_step", True) Scenario.continue_after_failed_step = continue_after_failed diff --git a/src/ifcbimtester/bimtester/reports.py b/src/ifcbimtester/bimtester/reports.py index bb82ea3b71..0aa7edd400 100644 --- a/src/ifcbimtester/bimtester/reports.py +++ b/src/ifcbimtester/bimtester/reports.py @@ -4,8 +4,8 @@ import os import pystache -def generate_report(adir="."): - print("# Generating HTML reports now.") +def generate_report(adir=".", use_report_folder=True, report_file_name="", html_template_file_path=""): + #print("# Generating HTML reports now.") # get html template html_template_file = os.path.join( @@ -13,11 +13,20 @@ def generate_report(adir="."): "features/template.html" ) + if html_template_file_path: + html_template_file = os.path.join(html_template_file_path, "template.html") + # get report file - report_dir = os.path.join(adir, "report") + report_dir = adir + if use_report_folder: + report_dir = os.path.join(adir, "report") if not os.path.exists(report_dir): return print("No report directory was found.") - report_path = os.path.join(report_dir, "report.json") + + if report_file_name: + report_path = os.path.join(report_dir, report_file_name) + else: + report_path = os.path.join(report_dir, "report.json") # print(report_path) if not os.path.exists(report_path): return print("No report data was found.") @@ -58,7 +67,12 @@ def generate_report(adir="."): if "match" in step and "arguments" in step["match"]: for a in step["match"]["arguments"]: name = name.replace(a["value"], "" + a["value"] + "") - if "result" not in step or step["result"]["status"] == "undefined": + if "result" not in step: + step["result"] = {} + step["result"]["status"] = "skipped" + step["result"]["duration"] = 0 + step["result"]["error_message"] = "This requirement has been skipped due to a previous failing step." + elif step["result"]["status"] == "undefined": step["result"] = {} step["result"]["status"] = "undefined" step["result"]["duration"] = 0 @@ -68,7 +82,8 @@ def generate_report(adir="."): "name": name, "time": round(step["result"]["duration"], 2), "is_success": step["result"]["status"] == "passed", - "is_unspecified": "result" not in step or step["result"]["status"] == "undefined", + "is_unspecified": step["result"]["status"] == "undefined", + "is_skipped": step["result"]["status"] == "skipped", "error_message": None if step["result"]["status"] == "passed" else step["result"]["error_message"], From 854a0fe8632cc9bf5f8501b28402e4395f2f5f55 Mon Sep 17 00:00:00 2001 From: Roch Bertucat Date: Fri, 18 Dec 2020 11:22:51 +0100 Subject: [PATCH 07/13] Add a way to specify a path to the arguments --- src/ifcbimtester/bimtester/run.py | 4 ++++ src/ifcbimtester/startbimtester.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/ifcbimtester/bimtester/run.py b/src/ifcbimtester/bimtester/run.py index 0f012bd2cf..bb46ab5373 100644 --- a/src/ifcbimtester/bimtester/run.py +++ b/src/ifcbimtester/bimtester/run.py @@ -31,6 +31,10 @@ def run_tests(args): behave_args.extend(args["advanced_arguments"].split()) elif not args["console"]: behave_args.extend(["--format", "json.pretty", "--outfile", "report/report.json"]) + if args["ifcfile"]: + behave_args.extend(["--define", "ifcfile={}".format(args["ifcfile"])]) + if args["path"]: + behave_args.extend(["--define", "path={}".format(args["path"])]) behave_main(behave_args) print("# All tests are finished.") return True diff --git a/src/ifcbimtester/startbimtester.py b/src/ifcbimtester/startbimtester.py index 88b755ae6a..d0510ae66b 100644 --- a/src/ifcbimtester/startbimtester.py +++ b/src/ifcbimtester/startbimtester.py @@ -48,6 +48,15 @@ if __name__ == "__main__": action="store_true", help="Generate a HTML report after running the tests" ) + parser.add_argument( + "-path", + "--path", + type=str, + help=( + "Specify a path to prepend to feature and ifc file" + ), + default="" + ) parser.add_argument( "-c", "--console", @@ -101,6 +110,12 @@ if __name__ == "__main__": args = vars(parser.parse_args()) print(args) + if args["path"]: + if args["feature"]: + args["feature"] = os.path.join(args["path"], args["feature"]) + if not args["gui"]: + args["ifcfile"] = os.path.join(args["path"], args["ifcfile"]) + if args["purge"]: clean.TestPurger().purge() elif args["report"]: From fc7d05c691ccd18ce1b6c884b5fd3cb2cb11589d Mon Sep 17 00:00:00 2001 From: Roch Bertucat Date: Fri, 18 Dec 2020 11:24:01 +0100 Subject: [PATCH 08/13] Improve referencing files using the path if available --- .../bimtester/features/steps/aggregation.py | 7 ++++++- .../bimtester/features/steps/ifcdata.py | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/ifcbimtester/bimtester/features/steps/aggregation.py b/src/ifcbimtester/bimtester/features/steps/aggregation.py index f3bbec37b8..e35373ae02 100644 --- a/src/ifcbimtester/bimtester/features/steps/aggregation.py +++ b/src/ifcbimtester/bimtester/features/steps/aggregation.py @@ -20,10 +20,15 @@ def step_impl(context): @given(u'a set of specific related elements taken from the file "{path_file}"') def step_impl(context, path_file): import csv + import os model = getattr(context, "model", None) if not model: context.model = TableModel() - with open(path_file, encoding="utf-8") as csvfile: + if context.config.userdata.get('path'): + path_file = os.path.join(context.config.userdata.get('path'), path_file) + if not os.path.exists(path_file): + assert False, "File {} not found".format(path_file) + with open(path_file, 'r', encoding="utf-8-sig") as csvfile: reader = csv.DictReader(csvfile) for row in reader: context.model.add_row(row["RelatedObjects"], row["RelatingGroup"]) diff --git a/src/ifcbimtester/bimtester/features/steps/ifcdata.py b/src/ifcbimtester/bimtester/features/steps/ifcdata.py index a6e69e2bf8..8a3b90beb7 100644 --- a/src/ifcbimtester/bimtester/features/steps/ifcdata.py +++ b/src/ifcbimtester/bimtester/features/steps/ifcdata.py @@ -1,4 +1,4 @@ -from behave import step +from behave import step, given from utils import IfcFile @@ -7,8 +7,7 @@ def step_impl(context, schema): try: IfcFile.load_schema(schema) except: - assert False - + assert False, f"The schema {schema} could not be loaded" @step('The IFC file "{file}" must be provided') def step_impl(context, file): @@ -17,6 +16,19 @@ def step_impl(context, file): except: assert False, f"The file {file} could not be loaded" +@given('The IFC file has been provided through an argument') +def step_impl(context): + try: + IfcFile.load(context.config.userdata.get("ifcfile")) + except: + assert False, f"The IFC {context.config.userdata.get('ifcfile')} file could not be loaded" + +@given('A file path has been provided through an argument') +def step_impl(context): + try: + assert context.config.userdata.get("path") + except: + assert False, f"The path {context.config.userdata.get('path')} could not be loaded" @step("IFC data must use the {schema} schema") def step_impl(context, schema): From 03dd4c39b77d11fc0a266a02e71a2e64347027cb Mon Sep 17 00:00:00 2001 From: Roch Bertucat Date: Fri, 18 Dec 2020 11:24:33 +0100 Subject: [PATCH 09/13] Add skipped step to the HTML template --- src/ifcbimtester/bimtester/features/template.html | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ifcbimtester/bimtester/features/template.html b/src/ifcbimtester/bimtester/features/template.html index 66a2ebb0ba..b83fbed566 100644 --- a/src/ifcbimtester/bimtester/features/template.html +++ b/src/ifcbimtester/bimtester/features/template.html @@ -4,21 +4,23 @@ - BlenderBIM + {{file_name}} {{time}}