From 6e152d6bbf97c9c5c2d4f21eecd97a21282dfeeb Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Mon, 20 Jul 2026 13:41:03 +0300 Subject: [PATCH] Bonsai: remove the dead Gherkin runner from test/bim/bootstrap.py bootstrap.py still provides the NewFile / NewIfc / NewIfc4X3 fixtures that 52 test modules depend on, but everything below them was a hand-rolled Gherkin runner (scenario/scenario_debug decorators, ~45 step functions, the definitions regex table and run/run_debug) left behind by the migration to pytest-bdd. Nothing imports or calls any of it: the live feature suite is driven by test/bim/test_feature.py via scenarios("feature"), which defines its own steps and does not reference bootstrap at all. The dead half was actively misleading. 38 of its 44 step definitions share a name with a live step in test_feature.py while behaving differently, so reading bootstrap.py gives a plausible but wrong picture of what a scenario does. "I duplicate the selected objects" is the clearest case: the dead copy calls bpy.ops.object.duplicate_move(), the live one calls bpy.ops.bim.override_object_duplicate_move(). That divergence already produced an incorrect review objection on a maintainer's PR. The remaining 6 steps exist only in bootstrap.py and are used by none of the 40 feature files, so no scenario loses coverage. Imports that only the removed code used are dropped; the headless webbrowser and tool.Drawing.open_with_user_command monkey-patches stay, since the modules importing the fixtures rely on them. Automated-AI-generated commit. --- src/bonsai/test/bim/bootstrap.py | 377 ------------------------------- 1 file changed, 377 deletions(-) diff --git a/src/bonsai/test/bim/bootstrap.py b/src/bonsai/test/bim/bootstrap.py index 587b8f5a2d..71d7750dfd 100644 --- a/src/bonsai/test/bim/bootstrap.py +++ b/src/bonsai/test/bim/bootstrap.py @@ -17,16 +17,10 @@ # along with Bonsai. If not, see . -import os -import re import webbrowser import bpy -import ifcopenshell -import ifcopenshell.util.element -import ifcopenshell.util.representation import pytest -from mathutils import Vector import bonsai.bim.handler import bonsai.tool as tool @@ -37,9 +31,6 @@ webbrowser.open = lambda x: True tool.Drawing.open_with_user_command = lambda x, y: True -variables = {"cwd": os.getcwd(), "ifc": "IfcStore.get_file()"} - - class NewFile: @pytest.fixture(autouse=True) def setup(self): @@ -73,371 +64,3 @@ class NewIfc4X3: props = tool.Project.get_project_props() props.export_schema = "IFC4X3_ADD2" bpy.ops.bim.create_project() - - -def scenario(function): - def subfunction(self): - run(function(self)) - - return subfunction - - -def scenario_debug(function): - def subfunction(self): - run_debug(function(self)) - - return subfunction - - -def an_empty_ifc_project(): - bpy.ops.bim.create_project() - - -def i_add_a_cube(): - bpy.ops.mesh.primitive_cube_add() - - -def i_add_a_cube_of_size_size_at_location(size, location): - bpy.ops.mesh.primitive_cube_add(size=float(size), location=[float(co) for co in location.split(",")]) - - -def the_object_name_is_selected(name): - i_deselect_all_objects() - additionally_the_object_name_is_selected(name) - - -def additionally_the_object_name_is_selected(name): - obj = bpy.context.scene.objects.get(name) - if not obj: - assert False, 'The object "{name}" could not be selected' - bpy.context.view_layer.objects.active = obj - obj.select_set(True) - - -def i_deselect_all_objects(): - bpy.context.view_layer.objects.active = None - bpy.ops.object.select_all(action="DESELECT") - - -def i_am_on_frame_number(number): - bpy.context.scene.frame_set(int(number)) - - -def i_set_prop_to_value(prop, value): - try: - eval(f"bpy.context.{prop}") - except: - assert False, "Property does not exist" - try: - exec(f'bpy.context.{prop} = "{value}"') - except: - exec(f"bpy.context.{prop} = {value}") - - -def prop_is_value(prop, value): - is_value = False - try: - exec(f'assert bpy.context.{prop} == "{value}"') - is_value = True - except: - try: - exec(f"assert bpy.context.{prop} == {value}") - is_value = True - except: - try: - exec(f"assert list(bpy.context.{prop}) == {value}") - is_value = True - except: - pass - if not is_value: - actual_value = eval(f"bpy.context.{prop}") - assert False, f"Value is {actual_value}" - - -def i_enable_prop(prop): - exec(f"bpy.context.{prop} = True") - - -def i_press_operator(operator): - if "(" in operator: - exec(f"bpy.ops.{operator}") - else: - exec(f"bpy.ops.{operator}()") - - -def i_rename_the_object_name1_to_name2(name1, name2): - the_object_name_exists(name1).name = name2 - - -def the_object_name_exists(name): - obj = bpy.data.objects.get(name) - if not obj: - assert False, f'The object "{name}" does not exist' - return obj - - -def an_ifc_file_exists(): - ifc = tool.Ifc.get() - if not ifc: - assert False, "No IFC file is available" - return ifc - - -def an_ifc_file_does_not_exist(): - ifc = tool.Ifc.get() - if ifc: - assert False, "An IFC is available" - - -def the_object_name_does_not_exist(name): - assert bpy.data.objects.get(name) is None, "Object exists" - - -def the_object_name_is_an_ifc_class(name, ifc_class): - ifc = an_ifc_file_exists() - element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) - assert element.is_a(ifc_class), f'Object "{name}" is an {element.is_a()}' - - -def the_object_name_is_not_an_ifc_element(name): - id = tool.Blender.get_ifc_definition_id(the_object_name_exists(name)) - assert id == 0, f"The ID is {id}" - - -def the_object_name_is_in_the_collection_collection(name, collection): - assert collection in [c.name for c in the_object_name_exists(name).users_collection] - - -def the_object_name_is_not_in_the_collection_collection(name, collection): - assert collection not in [c.name for c in the_object_name_exists(name).users_collection] - - -def the_object_name_has_a_body_of_value(name, value): - assert the_object_name_exists(name).data.body == value - - -def the_collection_name1_is_in_the_collection_name2(name1, name2): - assert bpy.data.collections.get(name2).children.get(name1) - - -def the_collection_name1_is_not_in_the_collection_name2(name1, name2): - assert not bpy.data.collections.get(name2).children.get(name1) - - -def the_object_name_is_placed_in_the_collection_collection(name, collection): - obj = the_object_name_exists(name) - [c.objects.unlink(obj) for c in obj.users_collection] - bpy.data.collections.get(collection).objects.link(obj) - - -def the_object_name_has_a_type_representation_of_context(name, type, context): - ifc = an_ifc_file_exists() - element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) - context, subcontext, target_view = context.split("/") - assert ifcopenshell.util.representation.get_representation( - element, context, subcontext or None, target_view or None - ) - - -def the_object_name_is_contained_in_container_name(name, container_name): - ifc = an_ifc_file_exists() - element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) - container = ifcopenshell.util.element.get_container(element) - if not container: - assert False, f'Object "{name}" is not in any container' - assert container.Name == container_name, f'Object "{name}" is in {container}' - - -def i_duplicate_the_selected_objects(): - bpy.ops.object.duplicate_move() - bonsai.bim.handler.active_object_callback() - - -def i_delete_the_selected_objects(): - bpy.ops.object.delete() - bonsai.bim.handler.active_object_callback() - - -def the_object_name1_and_name2_are_different_elements(name1, name2): - ifc = an_ifc_file_exists() - element1 = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name1))) - element2 = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(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() - - -def the_object_name1_has_a_boolean_difference_by_name2(name1, name2): - obj = the_object_name_exists(name1) - for modifier in obj.modifiers: - if modifier.type == "BOOLEAN" and modifier.object and modifier.object.name == name2: - return True - assert False, "No boolean found" - - -def the_object_name1_has_no_boolean_difference_by_name2(name1, name2): - obj = the_object_name_exists(name1) - for modifier in obj.modifiers: - if modifier.type == "BOOLEAN" and modifier.object and modifier.object.name == name2: - assert False, "A boolean was found" - - -def the_object_name_is_voided_by_void(name, void): - ifc = tool.Ifc.get() - element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) - for rel in element.HasOpenings: - if rel.RelatedOpeningElement.Name == void: - return True - assert False, "No void found" - - -def the_object_name_is_not_voided_by_void(name, void): - ifc = tool.Ifc.get() - element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) - for rel in element.HasOpenings: - if rel.RelatedOpeningElement.Name == void: - assert False, "A void was found" - - -def the_object_name_is_not_voided(name): - ifc = tool.Ifc.get() - element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) - if any(element.HasOpenings): - assert False, "An opening was found" - - -def the_object_name_is_not_a_void(name): - ifc = tool.Ifc.get() - element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) - if any(element.VoidsElements): - assert False, "A void was found" - - -def the_void_name_is_filled_by_filling(name, filling): - ifc = tool.Ifc.get() - element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) - if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings): - return True - assert False, "No filling found" - - -def the_void_name_is_not_filled_by_filling(name, filling): - ifc = tool.Ifc.get() - element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) - if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings): - assert False, "A filling was found" - - -def the_object_name_is_not_a_filling(name): - ifc = tool.Ifc.get() - element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) - if any(element.FillsVoids): - assert False, "A filling was found" - - -def the_object_name_should_display_as_mode(name, mode): - assert the_object_name_exists(name).display_type == mode - - -def the_object_name_has_number_vertices(name, number): - total = len(the_object_name_exists(name).data.vertices) - assert total == int(number), f"We found {total} vertices" - - -def the_object_name_is_at_location(name, location): - obj_location = the_object_name_exists(name).location - assert ( - obj_location - Vector([float(co) for co in location.split(",")]) - ).length < 0.1, f"Object is at {obj_location}" - - -def the_variable_key_is_value(key, value): - variables[key] = eval(value) - - -definitions = { - 'the variable "(.*)" is "(.*)"': the_variable_key_is_value, - "an empty IFC project": an_empty_ifc_project, - "I add a cube": i_add_a_cube, - 'I add a cube of size "([0-9]+)" at "(.*)"': i_add_a_cube_of_size_size_at_location, - 'the object "(.*)" is selected': the_object_name_is_selected, - 'additionally the object "(.*)" is selected': additionally_the_object_name_is_selected, - "I deselect all objects": i_deselect_all_objects, - 'I am on frame "([0-9]+)"': i_am_on_frame_number, - 'I set "(.*)" to "(.*)"': i_set_prop_to_value, - '"(.*)" is "(.*)"': prop_is_value, - 'I enable "(.*)"': i_enable_prop, - 'I press "(.*)"': i_press_operator, - 'I rename the object "(.*)" to "(.*)"': i_rename_the_object_name1_to_name2, - 'the object "(.*)" exists': the_object_name_exists, - 'the object "(.*)" does not exist': the_object_name_does_not_exist, - 'the object "(.*)" is an "(.*)"': the_object_name_is_an_ifc_class, - 'the object "(.*)" is not an IFC element': the_object_name_is_not_an_ifc_element, - 'the object "(.*)" is in the collection "(.*)"': the_object_name_is_in_the_collection_collection, - 'the object "(.*)" is not in the collection "(.*)"': the_object_name_is_not_in_the_collection_collection, - 'the object "(.*)" has a body of "(.*)"': the_object_name_has_a_body_of_value, - 'the collection "(.*)" is in the collection "(.*)"': the_collection_name1_is_in_the_collection_name2, - 'the collection "(.*)" is not in the collection "(.*)"': the_collection_name1_is_not_in_the_collection_name2, - "an IFC file exists": an_ifc_file_exists, - "an IFC file does not exist": an_ifc_file_does_not_exist, - 'the object "(.*)" has a "(.*)" representation of "(.*)"': the_object_name_has_a_type_representation_of_context, - 'the object "(.*)" is placed in the collection "(.*)"': the_object_name_is_placed_in_the_collection_collection, - 'the object "(.*)" is contained in "(.*)"': the_object_name_is_contained_in_container_name, - "I duplicate the selected objects": i_duplicate_the_selected_objects, - "I delete the selected objects": i_delete_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, - 'the object "(.*)" has a boolean difference by "(.*)"': the_object_name1_has_a_boolean_difference_by_name2, - 'the object "(.*)" has no boolean difference by "(.*)"': the_object_name1_has_no_boolean_difference_by_name2, - 'the object "(.*)" is voided by "(.*)"': the_object_name_is_voided_by_void, - 'the object "(.*)" is not voided by "(.*)"': the_object_name_is_not_voided_by_void, - 'the object "(.*)" is not a void': the_object_name_is_not_a_void, - 'the object "(.*)" is not voided': the_object_name_is_not_voided, - 'the object "(.*)" should display as "(.*)"': the_object_name_should_display_as_mode, - 'the object "(.*)" has "([0-9]+)" vertices': the_object_name_has_number_vertices, - 'the object "(.*)" is at "(.*)"': the_object_name_is_at_location, - "nothing interesting happens": lambda: None, - 'the void "(.*)" is filled by "(.*)"': the_void_name_is_filled_by_filling, - 'the void "(.*)" is not filled by "(.*)"': the_void_name_is_not_filled_by_filling, - 'the object "(.*)" is not a filling': the_object_name_is_not_a_filling, -} - - -# Super lightweight Gherkin implementation -def run(scenario): - keywords = ["Given", "When", "Then", "And", "But"] - for line in scenario.split("\n"): - for key, value in variables.items(): - line = line.replace("{" + key + "}", str(value)) - for keyword in keywords: - line = line.replace(keyword, "") - line = line.strip() - if not line: - continue - match = None - for definition, callback in definitions.items(): - match = re.search("^" + definition + "$", line) - if match: - try: - callback(*match.groups()) - except AssertionError as e: - assert False, f"Failed: {line}, with error: {e}" - break - if not match: - assert False, f"Definition not implemented: {line}" - return True - - -def run_debug(scenario, blend_filepath=None): - try: - result = run(scenario) - except Exception as e: - if blend_filepath: - bpy.ops.wm.save_as_mainfile(filepath=blend_filepath) - assert False, e - if blend_filepath: - bpy.ops.wm.save_as_mainfile(filepath=blend_filepath) - return result