# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2026
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
#
# This file was generated with the assistance of an AI coding tool.
"""Registration smoke test for `tool.Parametric.EDIT_TYPES`.
The registry is the single source of truth for which parametric element types
exist. Every consumer (auto-commit on save, finish/cancel chains, the
``PointerProperty`` attachment, the ``GizmoPreferences`` per-feature toggle)
derives identifiers from each entry's short ``name`` token. Forget any
downstream registration and the silent-desync the framework exists to prevent
will ship.
These tests pin the registry-to-runtime contract: for every entry the operator
``bl_idname``s resolve to registered ``bpy.ops.bim.*`` callables, the
``PropertyGroup`` class is attached to ``bpy.types.Object``, and the per-type
predicate exists on `tool.Parametric`."""
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)")
@pytest.fixture
def registry():
from bonsai import tool
return tool.Parametric.EDIT_TYPES
def test_registry_is_non_empty(registry):
assert len(registry) >= 1
def test_every_entry_has_enable_op_registered(registry):
missing = [e.enable_op for e in registry if not hasattr(bpy.ops.bim, e.enable_op.removeprefix("bim."))]
assert not missing, f"Missing enable operators: {missing}"
def test_every_entry_has_finish_op_registered(registry):
missing = [e.finish_op for e in registry if not hasattr(bpy.ops.bim, e.finish_op.removeprefix("bim."))]
assert not missing, f"Missing finish operators: {missing}"
def test_every_entry_has_cancel_op_registered(registry):
missing = [e.cancel_op for e in registry if not hasattr(bpy.ops.bim, e.cancel_op.removeprefix("bim."))]
assert not missing, f"Missing cancel operators: {missing}"
def test_every_entry_has_property_group_attached(registry):
# ``register_object_properties`` runs at addon enable; if any entry's
# PropertyGroup class is missing on prop module the attribute is skipped.
missing = [e.props_attr for e in registry if not hasattr(bpy.types.Object, e.props_attr)]
assert not missing, (
f"bpy.types.Object missing attributes: {missing} — "
f"verify the matching PropertyGroup classes exist in bim.module.model.prop"
)
def test_every_entry_has_parametric_predicate(registry):
from bonsai import tool
missing = [e.name for e in registry if getattr(tool.Parametric, f"is_{e.name}", None) is None]
assert not missing, f"tool.Parametric missing is_ predicates: {missing}"
def test_every_predicate_does_not_raise_on_non_matching_element(registry):
"""Each ``is_`` predicate must be **total**: accept any IFC entity
and return a truthy/falsy value, never raise.
The registry iterates every predicate against the active IFC element on
save; a raising predicate (e.g. ``AttributeError`` from a missing pset
accessor when handed a non-matching element type) propagates upward and
breaks the save path for *all* parametric types, not just its own.
This test probes each predicate with an ``IfcAnnotation`` (an element
that carries none of the BBIM_ psets the predicates look up) and
asserts the call does not raise. Falsy returns are acceptable — the
registry treats them as 'no match'. What's forbidden is raising."""
import ifcopenshell
from bonsai import tool
probe = ifcopenshell.file(schema="IFC4").create_entity("IfcAnnotation")
raised = []
for feature in registry:
predicate = getattr(tool.Parametric, f"is_{feature.name}", None)
if predicate is None:
continue
try:
predicate(probe)
except Exception as e:
raised.append((feature.name, type(e).__name__, str(e)))
assert not raised, (
f"is_ predicates raised on a non-matching IfcAnnotation: {raised}. "
f"Predicates must be total — return bool, never raise. Add an "
f"`if not element.is_a('IfcXxx'): return False` short-circuit or guard the pset lookup."
)
def test_default_parameters_field_per_registry_entry_with_defaults(registry):
"""Every entry flagged ``has_default_parameters=True`` must have a matching
``: PointerProperty`` field on ``ui.DefaultParameters`` pointing at
its ``BIMProperties`` class.
The addon-preferences ``Default Parameters`` panel iterates flagged entries
to render per-type defaults sections, and the matching create operator
(``bim.add_door``, ``bim.add_window``, …) reads the field to seed new
instances from the user's preset values. A missing field means the preset
silently never reaches the operator.
Entries WITHOUT the flag are not required to appear — the contract is
one-directional: ``has_default_parameters=True`` implies a field, but
``False`` allows absence."""
from bonsai.bim import ui
annotations = getattr(ui.DefaultParameters, "__annotations__", {})
missing = [e.name for e in registry if e.has_default_parameters and e.name not in annotations]
assert not missing, (
f"ui.DefaultParameters missing PointerProperty field(s) for: {missing} — "
f"each EDIT_TYPES entry with has_default_parameters=True must have a matching "
f": PointerProperty(type=BIMProperties) field"
)
def test_gizmo_preferences_field_per_registry_entry(registry):
"""Every registry entry must have a matching ``: BoolProperty`` field
on ``ui.GizmoPreferences`` so the addon-preferences UI auto-renders a
toggle for it and ``BaseParametricGizmoGroup.poll`` can gate the whole
gizmo group on ``prefs.gizmos.``.
Checks ``__annotations__`` rather than ``hasattr`` because Blender's
PropertyGroup syntax (``field: bpy.props.BoolProperty(...)``) is an
annotation-only assignment — the attribute only materialises on the
class after Blender's metaclass installs the bpy_struct descriptor,
which depends on registration timing. Reading ``__annotations__``
pins the source-level contract independently of when register() ran."""
from bonsai.bim import ui
annotations = getattr(ui.GizmoPreferences, "__annotations__", {})
missing = [feature.name for feature in registry if feature.name not in annotations]
assert not missing, (
f"ui.GizmoPreferences missing BoolProperty field(s) for: {missing} — "
f"each registry entry must have a matching ``: BoolProperty(...)`` "
f"field on ``ui.GizmoPreferences`` so the preferences UI surfaces a toggle"
)