From a9512492f0e19df12d1a96214e2daa24ac820909 Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Wed, 10 Jun 2026 12:24:44 +0200 Subject: [PATCH] Centralise model test fixtures via conftest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bim/module/model/conftest.py exposes the autouse _require_real_bpy skip-guard, four make_* factories (obj / element / context / ifc_file), and a patched_tool context-manager factory that wires the half-dozen tool.* boundary patches every gizmo + decorator test was repeating. Existing test files in the directory drop their local copies of _require_real_bpy and adopt the patched_tool / make_* fixtures where the call site simplifies — test_mep_port_operators.py is the biggest beneficiary (−89 LOC). No production behaviour change. Generated with the assistance of an AI coding tool. --- src/bonsai/test/bim/module/model/conftest.py | 16 ++++ .../bim/module/model/test_door_decorator.py | 7 -- .../test/bim/module/model/test_door_gizmos.py | 7 -- .../model/test_enable_editing_parametric.py | 8 -- .../bim/module/model/test_fillet_operators.py | 8 -- .../model/test_mep_actions_visibility.py | 8 -- .../bim/module/model/test_mep_bend_preview.py | 4 +- .../model/test_mep_bend_tessellation.py | 8 -- .../module/model/test_mep_port_operators.py | 89 ++++++------------- .../bim/module/model/test_preview_base.py | 6 -- .../bim/module/model/test_stair_gizmos.py | 6 -- .../module/model/test_transform_modal_gate.py | 7 -- ..._wall_array_child_filter_forward_compat.py | 7 -- .../module/model/test_wall_gizmo_poll_gate.py | 7 -- .../test/bim/module/model/test_wall_gizmos.py | 26 ++++-- .../model/test_wall_gizmos_array_children.py | 34 ++++--- .../module/model/test_wall_header_refresh.py | 6 -- .../module/model/test_wall_preview_mesh.py | 6 -- 18 files changed, 79 insertions(+), 181 deletions(-) diff --git a/src/bonsai/test/bim/module/model/conftest.py b/src/bonsai/test/bim/module/model/conftest.py index 13be349a16..f312684cdd 100644 --- a/src/bonsai/test/bim/module/model/conftest.py +++ b/src/bonsai/test/bim/module/model/conftest.py @@ -54,15 +54,31 @@ module if the helper count grows past ~6 or any helper picks up its own non-trivial dependencies.""" import contextlib +import types from types import SimpleNamespace from unittest.mock import MagicMock, Mock, patch +import bpy import ifcopenshell import pytest from bonsai import tool +@pytest.fixture(autouse=True) +def _require_real_bpy(): + """Skip every test in this directory when ``bpy`` is mocked or absent. + + The model gizmo / decorator suite reaches into Blender's RNA layer + (``bpy.types.Operator``, registered ``bl_idname`` lookups, ``Modifier`` + predicates) that ``Mock`` cannot impersonate, so a tool-lane run with a + stubbed ``bpy`` would error rather than meaningfully exercise the + contract. The autouse scope means new test files added under this + directory inherit the gate without re-declaring it.""" + if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): + pytest.skip("requires real Blender (bpy is mocked or absent)") + + def make_obj(*, session_uid=None, selected=True, **attrs): """Mock a ``bpy.types.Object`` with attributes commonly read by gizmos. diff --git a/src/bonsai/test/bim/module/model/test_door_decorator.py b/src/bonsai/test/bim/module/model/test_door_decorator.py index 939f7b292c..9736d9f732 100644 --- a/src/bonsai/test/bim/module/model/test_door_decorator.py +++ b/src/bonsai/test/bim/module/model/test_door_decorator.py @@ -29,7 +29,6 @@ Two layers: the edit-mode gizmo would, so the two surfaces stay visually identical even when a new ``door_type`` is added.""" -import types from types import SimpleNamespace from typing import get_args @@ -39,12 +38,6 @@ import pytest pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - # ---------------------------------------------------------------------------- # _visible_arcs — per-door-type arc selection # ---------------------------------------------------------------------------- diff --git a/src/bonsai/test/bim/module/model/test_door_gizmos.py b/src/bonsai/test/bim/module/model/test_door_gizmos.py index 1f69b74721..404a2afab2 100644 --- a/src/bonsai/test/bim/module/model/test_door_gizmos.py +++ b/src/bonsai/test/bim/module/model/test_door_gizmos.py @@ -26,7 +26,6 @@ against a SimpleNamespace stand-in that records ``matrix_basis`` assignments and the tests describe the geometric contract directly rather than echoing the implementation.""" -import types from types import SimpleNamespace from unittest.mock import MagicMock @@ -37,12 +36,6 @@ from mathutils import Matrix, Vector pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def _make_props(door_type, overall_width=0.9, lining_offset=0.0, is_editing=True): return SimpleNamespace( door_type=door_type, diff --git a/src/bonsai/test/bim/module/model/test_enable_editing_parametric.py b/src/bonsai/test/bim/module/model/test_enable_editing_parametric.py index 299c6565ad..72a0c5d2dc 100644 --- a/src/bonsai/test/bim/module/model/test_enable_editing_parametric.py +++ b/src/bonsai/test/bim/module/model/test_enable_editing_parametric.py @@ -32,8 +32,6 @@ These tests exercise: - one end-to-end invocation through ``bpy.ops`` to pin the wiring between the decision and ``invoke_props_dialog``.""" -import types - import bpy import pytest @@ -42,12 +40,6 @@ from bonsai.bim.module.model.array import EnableEditingParametric pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - class TestShouldShowSharedRepDialog: """Exhaustive truth table for the pre-edit-warning decision. Keeping this pure (no bpy, no operator instance) means a future change to the dispatch diff --git a/src/bonsai/test/bim/module/model/test_fillet_operators.py b/src/bonsai/test/bim/module/model/test_fillet_operators.py index 2cbc586d18..3a8f69dff2 100644 --- a/src/bonsai/test/bim/module/model/test_fillet_operators.py +++ b/src/bonsai/test/bim/module/model/test_fillet_operators.py @@ -35,20 +35,12 @@ early-returns when ``context.screen`` is unattached and prior tests can leave the screen in that state. The behaviour is covered by the user-visible live test loop instead.""" -import types - import bpy import pytest pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def _fillet_op_names(): """Walk bpy.ops.bim for operators whose name contains ``wall_fillet`` — avoids hard-coding the five lifecycle bl_idnames so adding / renaming diff --git a/src/bonsai/test/bim/module/model/test_mep_actions_visibility.py b/src/bonsai/test/bim/module/model/test_mep_actions_visibility.py index d25520910b..107d47dde9 100644 --- a/src/bonsai/test/bim/module/model/test_mep_actions_visibility.py +++ b/src/bonsai/test/bim/module/model/test_mep_actions_visibility.py @@ -44,14 +44,6 @@ import pytest pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - import types as _types - - if not isinstance(bpy, _types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - # --------------------------------------------------------------------------- # action_configs — operator registration + name uniqueness # --------------------------------------------------------------------------- diff --git a/src/bonsai/test/bim/module/model/test_mep_bend_preview.py b/src/bonsai/test/bim/module/model/test_mep_bend_preview.py index 8616dca954..fb53284bf5 100644 --- a/src/bonsai/test/bim/module/model/test_mep_bend_preview.py +++ b/src/bonsai/test/bim/module/model/test_mep_bend_preview.py @@ -242,7 +242,7 @@ def test_bend_preview_gizmo_group_is_registered(): """``GizmoBendPreview`` polls when ``scene.BIMPreviewProperties.bend.is_active`` is True. Pin the bl_idname so a typo wouldn't silently hide the preview gizmos at runtime.""" - from bonsai.bim.module.model.mep import GizmoBendPreview + from bonsai.bim.module.model.mep_bend_preview import GizmoBendPreview assert GizmoBendPreview.bl_idname == "OBJECT_GGT_bim_bend_preview" assert issubclass(GizmoBendPreview, bpy.types.GizmoGroup) @@ -336,7 +336,7 @@ def test_finish_bend_preview_catches_runtime_error_from_dispatch(): from types import SimpleNamespace from bonsai import tool - from bonsai.bim.module.model.mep import FinishBendPreview + from bonsai.bim.module.model.mep_bend_preview import FinishBendPreview class _Stand: def __init__(self): diff --git a/src/bonsai/test/bim/module/model/test_mep_bend_tessellation.py b/src/bonsai/test/bim/module/model/test_mep_bend_tessellation.py index bb0a42c934..1c8e5c44b6 100644 --- a/src/bonsai/test/bim/module/model/test_mep_bend_tessellation.py +++ b/src/bonsai/test/bim/module/model/test_mep_bend_tessellation.py @@ -39,14 +39,6 @@ from mathutils import Vector pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - import types as _types - - if not isinstance(bpy, _types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - # --------------------------------------------------------------------------- # _bend_profile_cross_section — IFC profile → 2D sample points # --------------------------------------------------------------------------- diff --git a/src/bonsai/test/bim/module/model/test_mep_port_operators.py b/src/bonsai/test/bim/module/model/test_mep_port_operators.py index a0d2cabae6..c434c69020 100644 --- a/src/bonsai/test/bim/module/model/test_mep_port_operators.py +++ b/src/bonsai/test/bim/module/model/test_mep_port_operators.py @@ -33,14 +33,6 @@ import pytest pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - import types as _types - - if not isinstance(bpy, _types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def _segment(predefined_type=None): """Stand-in IFC entity that reports ``is_a("IfcFlowSegment")`` True.""" e = Mock() @@ -74,13 +66,30 @@ def _make_op(_cls, **fields): # --------------------------------------------------------------------------- -def test_unjoin_at_port_deletes_joining_fitting(): - """Happy path: port state is JOINED, fitting is non-OBSTRUCTION → - delete the bridging fitting via the standard delete path.""" +@pytest.mark.parametrize( + "port_state, fitting_predefined_type, expected_result, expects_delete", + [ + pytest.param("JOINED", "JUNCTION", {"FINISHED"}, True, id="joined_junction_deletes"), + pytest.param("JOINED", "OBSTRUCTION", {"CANCELLED"}, False, id="joined_obstruction_refused"), + pytest.param("FREE", None, {"CANCELLED"}, False, id="free_port_cancels"), + ], +) +def test_unjoin_at_port_dispatch_table(port_state, fitting_predefined_type, expected_result, expects_delete): + """``MEPUnjoinAtPort`` dispatch contract: result and delete-side-effect + by ``(port_state, fitting type)``. + + - ``JOINED + JUNCTION`` (or any non-OBSTRUCTION fitting): happy path, + the bridging fitting is deleted via the standard delete entry point. + - ``JOINED + OBSTRUCTION``: deliberately refused — obstructions go + through ``bim.mep_add_obstruction`` (mode=REMOVE) so the segment + extends to absorb the freed length; using delete here would leave + a visible gap. + - ``FREE``: nothing to do — no bridging fitting exists. The operator + reports a user-facing error and CANCELS rather than no-op silently.""" from bonsai.bim.module.model import mep segment = _segment() - fitting = _fitting(predefined_type="JUNCTION") + fitting = _fitting(predefined_type=fitting_predefined_type) if fitting_predefined_type else None fitting_obj = Mock() op = _make_op(mep.MEPUnjoinAtPort, segment_id=42, position="END") @@ -89,61 +98,19 @@ def test_unjoin_at_port_deletes_joining_fitting(): with patch.object(mep.tool.Ifc, "get", return_value=ifc_file), patch.object( mep.tool.Ifc, "get_object", return_value=fitting_obj - ), patch.object(mep, "port_connection_state", return_value="JOINED"), patch.object( + ), patch.object(mep, "port_connection_state", return_value=port_state), patch.object( mep, "get_connected_element_at_segment_port", return_value=fitting ), patch.object( mep.tool.Geometry, "delete_ifc_object" ) as delete: result = mep.MEPUnjoinAtPort._execute(op, context=MagicMock()) - assert result == {"FINISHED"} - delete.assert_called_once_with(fitting_obj) - - -def test_unjoin_at_port_refuses_obstruction_fitting(): - """OBSTRUCTION fittings route through ``bim.mep_add_obstruction`` - (mode=REMOVE) which extends the segment to absorb the freed length — - using unjoin here would leave a gap.""" - from bonsai.bim.module.model import mep - - segment = _segment() - obstruction = _fitting(predefined_type="OBSTRUCTION") - - op = _make_op(mep.MEPUnjoinAtPort, segment_id=42, position="END") - ifc_file = MagicMock() - ifc_file.by_id.return_value = segment - - with patch.object(mep.tool.Ifc, "get", return_value=ifc_file), patch.object( - mep, "port_connection_state", return_value="JOINED" - ), patch.object(mep, "get_connected_element_at_segment_port", return_value=obstruction), patch.object( - mep.tool.Geometry, "delete_ifc_object" - ) as delete: - result = mep.MEPUnjoinAtPort._execute(op, context=MagicMock()) - - assert result == {"CANCELLED"} - delete.assert_not_called() - op.report.assert_called() - - -def test_unjoin_at_port_cancels_when_port_is_free(): - """Port has no connection at all → no fitting to delete → CANCELLED - with a user-facing error rather than a silent no-op.""" - from bonsai.bim.module.model import mep - - segment = _segment() - - op = _make_op(mep.MEPUnjoinAtPort, segment_id=42, position="START") - ifc_file = MagicMock() - ifc_file.by_id.return_value = segment - - with patch.object(mep.tool.Ifc, "get", return_value=ifc_file), patch.object( - mep, "port_connection_state", return_value="FREE" - ), patch.object(mep.tool.Geometry, "delete_ifc_object") as delete: - result = mep.MEPUnjoinAtPort._execute(op, context=MagicMock()) - - assert result == {"CANCELLED"} - delete.assert_not_called() - op.report.assert_called() + assert result == expected_result + if expects_delete: + delete.assert_called_once_with(fitting_obj) + else: + delete.assert_not_called() + op.report.assert_called() def test_unjoin_at_port_cancels_when_active_is_not_segment(): diff --git a/src/bonsai/test/bim/module/model/test_preview_base.py b/src/bonsai/test/bim/module/model/test_preview_base.py index 9cc602e0b6..e5e6244a69 100644 --- a/src/bonsai/test/bim/module/model/test_preview_base.py +++ b/src/bonsai/test/bim/module/model/test_preview_base.py @@ -32,12 +32,6 @@ import pytest pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def _registry(): from bonsai.bim.module.model.preview_base import PREVIEW_CANCEL_OPS diff --git a/src/bonsai/test/bim/module/model/test_stair_gizmos.py b/src/bonsai/test/bim/module/model/test_stair_gizmos.py index e406bc8cb8..263d5cc6e2 100644 --- a/src/bonsai/test/bim/module/model/test_stair_gizmos.py +++ b/src/bonsai/test/bim/module/model/test_stair_gizmos.py @@ -39,12 +39,6 @@ import pytest pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def _rotation_close(a, b, tol: float = 1e-6) -> bool: for row_a, row_b in zip(a, b): for va, vb in zip(row_a, row_b): diff --git a/src/bonsai/test/bim/module/model/test_transform_modal_gate.py b/src/bonsai/test/bim/module/model/test_transform_modal_gate.py index de4723b226..139af4cdc0 100644 --- a/src/bonsai/test/bim/module/model/test_transform_modal_gate.py +++ b/src/bonsai/test/bim/module/model/test_transform_modal_gate.py @@ -27,7 +27,6 @@ BEHAVIOUR (poll returns False / draw_prepare early-returns when a transform modal is active) without pinning the name of the helper used internally.""" import importlib -import types from unittest.mock import MagicMock, patch import bpy @@ -46,12 +45,6 @@ PARAMETRIC_MODULES = ( ) -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def _discover_parametric_gizmo_groups(): """Walk each parametric-edit module for ``bpy.types.GizmoGroup`` subclasses defined locally. Preview-owning gizmo groups (bl_idname contains 'preview') diff --git a/src/bonsai/test/bim/module/model/test_wall_array_child_filter_forward_compat.py b/src/bonsai/test/bim/module/model/test_wall_array_child_filter_forward_compat.py index 5d790ece50..6c1b367b7c 100644 --- a/src/bonsai/test/bim/module/model/test_wall_array_child_filter_forward_compat.py +++ b/src/bonsai/test/bim/module/model/test_wall_array_child_filter_forward_compat.py @@ -39,7 +39,6 @@ allow-list with an explanation) fails this test.""" import ast import inspect -import types import bpy import pytest @@ -53,12 +52,6 @@ _ALLOWLIST = frozenset({"GizmoWallEdition", "GizmoWallFilletPreview"}) _REQUIRED_CALLEES = frozenset({"_wall_topology_gizmo_poll_gate", "any_selected_is_array_child"}) -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def _wall_module_source(): from bonsai.bim.module.model import wall as wall_mod diff --git a/src/bonsai/test/bim/module/model/test_wall_gizmo_poll_gate.py b/src/bonsai/test/bim/module/model/test_wall_gizmo_poll_gate.py index 444c0cdf29..048b04c54b 100644 --- a/src/bonsai/test/bim/module/model/test_wall_gizmo_poll_gate.py +++ b/src/bonsai/test/bim/module/model/test_wall_gizmo_poll_gate.py @@ -28,7 +28,6 @@ joins the test. The test then asserts the BEHAVIOUR (poll returns False when helper function the gizmo uses internally to enforce it.""" import inspect -import types from unittest.mock import patch import bpy @@ -37,12 +36,6 @@ import pytest pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def _wall_gizmo_groups(): """Walk the wall module for ``bpy.types.GizmoGroup`` subclasses defined locally (skip imported references). Returns a list of (name, cls) tuples. diff --git a/src/bonsai/test/bim/module/model/test_wall_gizmos.py b/src/bonsai/test/bim/module/model/test_wall_gizmos.py index b8a40c7b86..c3b97e9466 100644 --- a/src/bonsai/test/bim/module/model/test_wall_gizmos.py +++ b/src/bonsai/test/bim/module/model/test_wall_gizmos.py @@ -25,7 +25,6 @@ logic can be exercised without a real IFC fixture. Each test pins one of the gates ``poll()`` walks, so any silent regression in the gate order or in the LAYER3-active / LAYER2-other contract is caught by a dedicated assertion.""" -import types from types import SimpleNamespace from unittest.mock import patch @@ -35,10 +34,15 @@ import pytest pytestmark = pytest.mark.wall -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") +class _Obj: + """Hashable, name-bearing stand-in for a ``bpy.types.Object`` selection + slot. ``SimpleNamespace`` defines ``__eq__`` (and so ``__hash__ = None``) + which makes it unusable inside the ``set()`` that + ``get_selected_objects()`` returns; a plain class falls back to + identity-based hashing and works inside both ``set`` and ``list``.""" + + def __init__(self, name: str) -> None: + self.name = name def _make_context(active, selected): @@ -76,19 +80,23 @@ def _patch_tools(prefs_on, selected, active_element, other_element, active_usage patch.object(tool.Blender, "get_selected_objects", return_value=set(selected)), patch.object(tool.Ifc, "get_entity", side_effect=get_entity), patch.object(tool.Model, "get_usage_type", side_effect=get_usage_type), + # The array-child filter is pinned by its own test file; stub it here + # so these poll tests stay focused on the count / layer-usage gates + # and don't have to scaffold the memoization cache key. + patch.object(tool.Blender.Modifier, "any_selected_is_array_child", return_value=False), ] def _run_poll(prefs_on, active_is_in_selected, len_override, active_usage, other_usage, active_has_entity=True): from bonsai.bim.module.model.wall import GizmoWallExtendVertically - slab_obj = object() - wall_obj = object() - active = slab_obj if active_is_in_selected else object() + slab_obj = _Obj("slab") + wall_obj = _Obj("wall") + active = slab_obj if active_is_in_selected else _Obj("active_extra") if len_override is None: selected = [slab_obj, wall_obj] else: - selected = [object() for _ in range(len_override)] + selected = [_Obj(f"obj_{i}") for i in range(len_override)] if active_is_in_selected and selected: active = selected[0] diff --git a/src/bonsai/test/bim/module/model/test_wall_gizmos_array_children.py b/src/bonsai/test/bim/module/model/test_wall_gizmos_array_children.py index 41498c4220..81f5ef5b15 100644 --- a/src/bonsai/test/bim/module/model/test_wall_gizmos_array_children.py +++ b/src/bonsai/test/bim/module/model/test_wall_gizmos_array_children.py @@ -23,7 +23,7 @@ selection that contains a Bonsai array child. Discovers gated gizmo groups and guarded operators by source inspection so additions inherit the rule automatically.""" -import types +from types import SimpleNamespace from unittest.mock import patch import bpy @@ -32,12 +32,6 @@ import pytest pytestmark = pytest.mark.model -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def _wall_gizmo_groups_using_gate(): """Wall-module ``bpy.types.GizmoGroup`` subclasses whose ``poll`` calls ``_wall_topology_gizmo_poll_gate``. Discovered by source inspection so @@ -69,9 +63,11 @@ def _wall_gizmo_groups_using_gate(): def _wall_operators_with_array_child_guard(): - """Wall-module ``bpy.types.Operator`` subclasses whose ``poll`` references - ``any_selected_is_array_child``. The operator-level guard is defence in - depth against keymap / F3 paths that bypass the gizmo entirely.""" + """Wall-module ``bpy.types.Operator`` subclasses whose ``poll`` rejects + array-child selections, either by referencing the central predicate + directly or by routing through the shared ``_poll_reject_array_children`` + helper that wraps it. The operator-level guard is defence in depth + against keymap / F3 paths that bypass the gizmo entirely.""" import inspect from bonsai.bim.module.model import wall as wall_mod @@ -92,7 +88,7 @@ def _wall_operators_with_array_child_guard(): src = inspect.getsource(poll) except (OSError, TypeError): continue - if "any_selected_is_array_child" not in src: + if "any_selected_is_array_child" not in src and "_poll_reject_array_children" not in src: continue out.append((name, obj)) return out @@ -141,8 +137,9 @@ class TestWallOperatorsRejectArrayChildSelection: def test_discovery_finds_wall_topology_operators(self): ops = _wall_operators_with_array_child_guard() assert ops, ( - "Expected at least one wall Operator whose poll references " - "any_selected_is_array_child — discovery walk drifted out of sync?" + "Expected at least one wall Operator whose poll rejects array-child " + "selections (via any_selected_is_array_child or _poll_reject_array_children) " + "— discovery walk drifted out of sync?" ) def test_every_guarded_wall_operator_polls_false_on_array_child_selection(self): @@ -171,8 +168,9 @@ class TestWallOperatorsRejectArrayChildSelection: assert not offenders, ( "Wall topology operators that accept array-child selections: " + ", ".join(f"{n} — {why}" for n, why in offenders) - + ". Add `if tool.Blender.Modifier.any_selected_is_array_child(): " - "return False` early in the poll." + + ". Route the poll through `_poll_reject_array_children(cls)` (the " + "shared helper that sets the standard poll message and reuses the " + "central `any_selected_is_array_child` predicate)." ) @@ -190,8 +188,8 @@ class TestAnySelectedIsArrayChildHelper: def test_returns_true_when_any_selected_passes_predicate(self): from bonsai import tool - child_obj, child_element = object(), object() - parent_obj, parent_element = object(), object() + child_obj, child_element = SimpleNamespace(name="child"), object() + parent_obj, parent_element = SimpleNamespace(name="parent"), object() def get_entity(obj): return {id(child_obj): child_element, id(parent_obj): parent_element}.get(id(obj)) @@ -207,7 +205,7 @@ class TestAnySelectedIsArrayChildHelper: def test_returns_false_when_no_selected_passes_predicate(self): from bonsai import tool - parent_obj, parent_element = object(), object() + parent_obj, parent_element = SimpleNamespace(name="parent"), object() with patch.object(tool.Blender, "get_selected_objects", return_value=[parent_obj]): with patch.object(tool.Ifc, "get_entity", return_value=parent_element): with patch.object(tool.Blender.Modifier, "is_array_child", return_value=False): diff --git a/src/bonsai/test/bim/module/model/test_wall_header_refresh.py b/src/bonsai/test/bim/module/model/test_wall_header_refresh.py index cbc6c9dae4..5038c06636 100644 --- a/src/bonsai/test/bim/module/model/test_wall_header_refresh.py +++ b/src/bonsai/test/bim/module/model/test_wall_header_refresh.py @@ -39,12 +39,6 @@ import pytest pytestmark = pytest.mark.wall -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def test_refresh_post_commit_bumps_generation_for_every_operator(): """The generation counter advances on every commit, regardless of operator class — it's the cache-invalidation signal for any code diff --git a/src/bonsai/test/bim/module/model/test_wall_preview_mesh.py b/src/bonsai/test/bim/module/model/test_wall_preview_mesh.py index 405e545475..25c297ec9c 100644 --- a/src/bonsai/test/bim/module/model/test_wall_preview_mesh.py +++ b/src/bonsai/test/bim/module/model/test_wall_preview_mesh.py @@ -37,12 +37,6 @@ from mathutils import Vector pytestmark = pytest.mark.wall -@pytest.fixture(autouse=True) -def _require_real_bpy(): - if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"): - pytest.skip("requires real Blender (bpy is mocked or absent)") - - def test_regenerate_wall_mesh_from_props_outward_normals(): """Every face of the preview box must have its normal pointing away from the box centroid — the contract every other preview-mesh builder