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.
This commit is contained in:
Petru Conduraru
2026-07-20 13:41:03 +03:00
parent 55a2430d71
commit 6e152d6bbf
-377
View File
@@ -17,16 +17,10 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import re
import webbrowser import webbrowser
import bpy import bpy
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
import pytest import pytest
from mathutils import Vector
import bonsai.bim.handler import bonsai.bim.handler
import bonsai.tool as tool import bonsai.tool as tool
@@ -37,9 +31,6 @@ webbrowser.open = lambda x: True
tool.Drawing.open_with_user_command = lambda x, y: True tool.Drawing.open_with_user_command = lambda x, y: True
variables = {"cwd": os.getcwd(), "ifc": "IfcStore.get_file()"}
class NewFile: class NewFile:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup(self): def setup(self):
@@ -73,371 +64,3 @@ class NewIfc4X3:
props = tool.Project.get_project_props() props = tool.Project.get_project_props()
props.export_schema = "IFC4X3_ADD2" props.export_schema = "IFC4X3_ADD2"
bpy.ops.bim.create_project() 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