Option C oc_diagnostics
Loading...
Searching...
No Matches
test_id_instrumentation.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/test_id_instrumentation.py
4
5"""
6Regression Tests — id_instrumentation
7=======================================
8
9Purpose
10-------
11Verify the full behaviour of ``id_instrumentation``: structural-ID protection,
12dict-ID passthrough, depth-limit guarding, recursive children traversal, and
13project-defined custom structural ID extensions.
14
15Responsibilities
16----------------
17- Confirm ``is_structural_id()`` correctly classifies built-in structural and
18 non-structural IDs.
19- Confirm ``instrument_ids()`` rewrites non-structural string IDs to
20 ``{"event": "<id>"}``.
21- Confirm dict IDs are never rewritten.
22- Confirm components without an ``id`` attribute are returned unchanged.
23- Confirm recursion stops at ``MAX_INSTRUMENT_DEPTH`` without raising.
24- Confirm nested children are instrumented recursively.
25- Confirm ``ADDITIONAL_STRUCTURAL_PREFIXES`` prevents instrumentation.
26- Confirm ``ADDITIONAL_STRUCTURAL_EXACT`` prevents instrumentation.
27
28Diagnostics
29-----------
30Domain: TEST
31Levels:
32 L2 — lifecycle
33 L3 — semantic details
34 L4 — deep tracing
35
36Contracts
37---------
38- All Dash-dependent tests are skipped automatically when Dash is not installed.
39- Must not permanently modify oc_diagnostics global state.
40- Must restore all patched globals in finally blocks.
41- Must not start servers or require network access.
42- Must run deterministically across repeated invocations.
43"""
44
45import os
46import sys
47
48# ---------------------------------------------------------------------------
49# Ensure oc_diagnostics is importable when run from the project root or the
50# tests/ subdirectory.
51# ---------------------------------------------------------------------------
52_REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
53if _REPO_ROOT not in sys.path:
54 sys.path.insert(0, _REPO_ROOT)
55
56import pytest
57
58# Skip the entire module when Dash is not installed
59dash_html = pytest.importorskip("dash.html", reason="Dash not installed")
60html = dash_html
61
64 instrument_ids,
65 is_structural_id,
66 MAX_INSTRUMENT_DEPTH,
67)
68
69
70# ============================================================
71# is_structural_id()
72# ============================================================
73
75 """is_structural_id() correctly classifies structural and non-structural IDs."""
76 structural_ids = [
77 "root-group-main",
78 "tab-group-settings",
79 "tab-content-data",
80 "pane-group-left",
81 "theme-root",
82 "ui-theme-store",
83 "init-once",
84 ]
85 for cid in structural_ids:
86 assert is_structural_id(cid), f"Expected structural: {cid!r}"
87
88 non_structural_ids = [
89 "my-button",
90 "dropdown-filter",
91 "input-name",
92 "custom123",
93 ]
94 for cid in non_structural_ids:
95 assert not is_structural_id(cid), f"Expected non-structural: {cid!r}"
96
97
98# ============================================================
99# instrument_ids() — basic rewriting
100# ============================================================
101
103 """instrument_ids() rewrites a non-structural string ID to {"event": "<id>"}."""
104 old_enabled = _ud.INSTRUMENTATION_ENABLED
105 old_level = _ud.DEBUG_LEVELS.get("INSTRUMENTATION", 0)
106 _ud.INSTRUMENTATION_ENABLED = True
107 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = 9
108 try:
109 comp = html.Button("Click me", id="btn-test")
110 result = instrument_ids(comp)
111 assert result.id == {"event": "btn-test"}, (
112 f"Expected {{'event': 'btn-test'}}, got: {result.id!r}"
113 )
114 finally:
115 _ud.INSTRUMENTATION_ENABLED = old_enabled
116 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = old_level
117
118
119# ============================================================
120# Dict ID passthrough
121# ============================================================
122
124 """Components with dict IDs are returned unchanged — no double-wrapping."""
125 old_enabled = _ud.INSTRUMENTATION_ENABLED
126 old_level = _ud.DEBUG_LEVELS.get("INSTRUMENTATION", 0)
127 _ud.INSTRUMENTATION_ENABLED = True
128 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = 9
129 try:
130 original_id = {"event": "already-instrumented"}
131 comp = html.Button("X", id=original_id)
132 result = instrument_ids(comp)
133 assert result.id == original_id, (
134 f"Dict ID must not be rewritten, got: {result.id!r}"
135 )
136 finally:
137 _ud.INSTRUMENTATION_ENABLED = old_enabled
138 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = old_level
139
140
141# ============================================================
142# No-ID component passthrough
143# ============================================================
144
146 """Components without an id attribute have their ID left as None."""
147 old_enabled = _ud.INSTRUMENTATION_ENABLED
148 old_level = _ud.DEBUG_LEVELS.get("INSTRUMENTATION", 0)
149 _ud.INSTRUMENTATION_ENABLED = True
150 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = 9
151 try:
152 comp = html.Div("no id")
153 result = instrument_ids(comp)
154 # id should remain None (not rewritten to a dict)
155 cid = getattr(result, "id", None)
156 assert not isinstance(cid, dict), (
157 f"Component without id must not receive a dict ID, got: {cid!r}"
158 )
159 finally:
160 _ud.INSTRUMENTATION_ENABLED = old_enabled
161 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = old_level
162
163
164# ============================================================
165# Depth limit
166# ============================================================
167
169 """instrument_ids() stops recursion at MAX_INSTRUMENT_DEPTH without RecursionError."""
170 old_enabled = _ud.INSTRUMENTATION_ENABLED
171 old_level = _ud.DEBUG_LEVELS.get("INSTRUMENTATION", 0)
172 _ud.INSTRUMENTATION_ENABLED = True
173 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = 9
174 try:
175 comp = html.Div("deep-component", id="deep-comp")
176 # Calling with _depth > MAX_INSTRUMENT_DEPTH triggers the guard directly
177 result = instrument_ids(comp, _depth=MAX_INSTRUMENT_DEPTH + 1)
178 # Component must be returned unchanged (guard returns early)
179 assert result is comp, (
180 "instrument_ids must return the component unchanged at MAX_INSTRUMENT_DEPTH"
181 )
182 assert result.id == "deep-comp", (
183 f"ID must not be rewritten at depth limit, got: {result.id!r}"
184 )
185 finally:
186 _ud.INSTRUMENTATION_ENABLED = old_enabled
187 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = old_level
188
189
190# ============================================================
191# Nested children traversal
192# ============================================================
193
195 """instrument_ids() recursively instruments children of a component."""
196 old_enabled = _ud.INSTRUMENTATION_ENABLED
197 old_level = _ud.DEBUG_LEVELS.get("INSTRUMENTATION", 0)
198 _ud.INSTRUMENTATION_ENABLED = True
199 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = 9
200 try:
201 inner = html.Button("Click", id="inner-btn")
202 outer = html.Div(inner, id="outer-div")
203 result = instrument_ids(outer)
204 assert result.id == {"event": "outer-div"}, (
205 f"Outer ID must be instrumented, got: {result.id!r}"
206 )
207 assert result.children.id == {"event": "inner-btn"}, (
208 f"Inner child ID must be instrumented, got: {result.children.id!r}"
209 )
210 finally:
211 _ud.INSTRUMENTATION_ENABLED = old_enabled
212 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = old_level
213
214
215# ============================================================
216# Custom structural prefixes (FS-02 / OI-17)
217# ============================================================
218
220 """ADDITIONAL_STRUCTURAL_PREFIXES prevents ID rewriting for matching components."""
221 old_enabled = _ud.INSTRUMENTATION_ENABLED
222 old_level = _ud.DEBUG_LEVELS.get("INSTRUMENTATION", 0)
223 old_prefixes = _ud.ADDITIONAL_STRUCTURAL_PREFIXES
224 _ud.INSTRUMENTATION_ENABLED = True
225 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = 9
226 _ud.ADDITIONAL_STRUCTURAL_PREFIXES = ("custom-struct-",)
227 try:
228 comp = html.Div("protected", id="custom-struct-main")
229 result = instrument_ids(comp)
230 assert result.id == "custom-struct-main", (
231 f"Custom structural prefix must prevent ID rewriting, got: {result.id!r}"
232 )
233 finally:
234 _ud.INSTRUMENTATION_ENABLED = old_enabled
235 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = old_level
236 _ud.ADDITIONAL_STRUCTURAL_PREFIXES = old_prefixes
237
238
239# ============================================================
240# Custom structural exact matches (FS-02 / OI-17)
241# ============================================================
242
244 """ADDITIONAL_STRUCTURAL_EXACT prevents ID rewriting for exact-match IDs."""
245 old_enabled = _ud.INSTRUMENTATION_ENABLED
246 old_level = _ud.DEBUG_LEVELS.get("INSTRUMENTATION", 0)
247 old_exact = _ud.ADDITIONAL_STRUCTURAL_EXACT
248 _ud.INSTRUMENTATION_ENABLED = True
249 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = 9
250 _ud.ADDITIONAL_STRUCTURAL_EXACT = frozenset(["my-special-store"])
251 try:
252 comp = html.Div("protected-exact", id="my-special-store")
253 result = instrument_ids(comp)
254 assert result.id == "my-special-store", (
255 f"Custom structural exact ID must prevent rewriting, got: {result.id!r}"
256 )
257 finally:
258 _ud.INSTRUMENTATION_ENABLED = old_enabled
259 _ud.DEBUG_LEVELS["INSTRUMENTATION"] = old_level
260 _ud.ADDITIONAL_STRUCTURAL_EXACT = old_exact