mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Centralise model test fixtures via conftest
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.
This commit is contained in:
committed by
Thomas Krijnen
parent
9e35db593e
commit
a9512492f0
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user