mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 12:03:55 +00:00
Add lifecycle-mixin tests + predicate-total registry guard
test_parametric_lifecycle.py covers the door/window/railing/roof state-transition contracts (enable/finish/cancel; no-op on non-matching elements; draft preserved on finish-time failure) that the registry smoke test never exercised. test_parametric_registry.py gains a check that every is_<name> predicate stays total (never raises on a non-matching IFC entity) — a raising predicate would break the save path for unrelated types. Also rewrites the gizmo-prefs check to read __annotations__ instead of hasattr, which depended on Blender registration timing. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Registration smoke test for :attr:`tool.Parametric.EDIT_TYPES`.
|
||||
"""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
|
||||
@@ -29,7 +29,7 @@ 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 :class:`tool.Blender.Modifier`."""
|
||||
predicate exists on `tool.Blender.Modifier`."""
|
||||
|
||||
import types
|
||||
|
||||
@@ -88,24 +88,66 @@ def test_every_entry_has_modifier_predicate(registry):
|
||||
assert not missing, f"tool.Blender.Modifier missing is_<name> predicates: {missing}"
|
||||
|
||||
|
||||
def test_every_predicate_does_not_raise_on_non_matching_element(registry):
|
||||
"""Each ``is_<name>`` 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_<Type> 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.Blender.Modifier, 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_<name> 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_gizmo_preferences_attached_when_class_exists(registry):
|
||||
"""For every registry entry whose ``GizmoPreferences<Name>`` class exists in
|
||||
``bonsai.bim.ui``, the matching sub-PointerProperty must be attached to
|
||||
``bonsai.bim.ui``, the matching sub-PointerProperty must be declared on
|
||||
``ui.GizmoPreferences`` under the registry entry's ``name`` token.
|
||||
|
||||
Catches the silent-skip behaviour of
|
||||
``Parametric.iter_gizmo_preference_classes``: a typo in the class name
|
||||
or a dropped registration would otherwise produce a missing sub-panel at
|
||||
runtime with no error. Entries without a ``GizmoPreferences<Name>``
|
||||
class are allowed — not every parametric type ships gizmo prefs."""
|
||||
Catches the silent-skip behaviour of the registry-driven gizmo-prefs
|
||||
discovery: a typo in the class name or a dropped registration would
|
||||
otherwise produce a missing sub-panel at runtime with no error.
|
||||
Entries without a ``GizmoPreferences<Name>`` class are allowed — not
|
||||
every parametric type ships gizmo prefs.
|
||||
|
||||
Checks ``__annotations__`` rather than ``hasattr`` because Blender's
|
||||
PropertyGroup syntax (``field: bpy.props.PointerProperty(...)``) 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 = []
|
||||
for feature in registry:
|
||||
prefs_class_name = f"GizmoPreferences{feature.name.capitalize()}"
|
||||
if not hasattr(ui, prefs_class_name):
|
||||
continue
|
||||
if not hasattr(ui.GizmoPreferences, feature.name):
|
||||
if feature.name not in annotations:
|
||||
missing.append((feature.name, prefs_class_name))
|
||||
assert not missing, (
|
||||
f"ui.GizmoPreferences missing sub-PointerProperty field(s) for: {missing} — "
|
||||
|
||||
Reference in New Issue
Block a user