Option C oc_diagnostics
Loading...
Searching...
No Matches
instrumentation_selftest_cli.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_diagnostics/instrumentation_selftest_cli.py
4
5"""
6Instrumentation Self-Test (CLI)
7================================
8
9Purpose
10-------
11This module performs Dash-based CLI tests of the instrumentation system.
12It validates:
13
14 - Static ID instrumentation
15 - Nested component recursion
16 - Structural ID protection
17 - Custom component handling
18 - Logging behavior under different debug levels
19
20**Requires Dash to be installed** (``pip install oc_diagnostics[dash]``).
21Dash component classes (``html.Div``, ``html.Button``, etc.) are used to
22construct component trees for instrumentation testing. No Dash server is
23started and no browser interaction is required.
24
25This CLI test suite is designed to be:
26
27 - deterministic
28 - isolated
29 - assertion-driven (each test uses explicit ``if not: raise AssertionError``
30 so checks survive ``python -O`` optimization)
31 - safe for use in CI/CD pipelines
32
33Responsibilities
34----------------
35- Parse CLI arguments (``--use-global``).
36- Temporarily set ``INSTRUMENTATION_ENABLED=True`` and optionally force debug
37 level=9 for the duration of the test run; restore all module state on exit.
38- Run five deterministic test functions, each using ``assert`` to verify
39 ID-rewriting invariants.
40- Collect PASS/FAIL per test and print a structured summary.
41- Exit with code ``0`` on full success or ``1`` on any failure.
42
43Exit Codes
44----------
45``0`` — all tests passed.
46``1`` — one or more tests failed or raised an unexpected exception.
47
48---------------------------------------------------------------------------
49Contract & Lifecycle
50---------------------------------------------------------------------------
51
52The CLI test suite follows this lifecycle:
53
54 1. Parse command-line arguments
55 2. Choose debug-level mode:
56 - FORCE VERBOSE MODE (default): forces INSTRUMENTATION_ENABLED=True,
57 INSTRUMENTATION level=9, UI level=9
58 - USE GLOBAL LEVELS (--use-global)
59 3. Run a series of deterministic instrumentation tests, each with
60 explicit ``if not: raise AssertionError`` checks that verify actual
61 ID rewriting behaviour and survive ``python -O``
62 4. Restore original settings
63 5. Print PASS/FAIL summary and exit with code 0 or 1
64
65---------------------------------------------------------------------------
66Debug-Level Modes
67---------------------------------------------------------------------------
68
69MODE A — FORCE VERBOSE (default)
70---------------------------------
71Force:
72
73 INSTRUMENTATION_ENABLED = True
74 DEBUG_LEVELS["INSTRUMENTATION"] = 9
75 DEBUG_LEVELS["UI"] = 9
76
77This ensures:
78 - full recursion logs
79 - full instrumentation logs
80 - assertions that verify the rewritten IDs are correct
81
82MODE B — RESPECT GLOBAL LEVELS
83-------------------------------
84Use whatever INSTRUMENTATION_ENABLED and levels are defined in
85unified_debug.py. Assertions check behaviour consistent with those levels.
86
87Select mode via:
88
89 python -m oc_diagnostics.instrumentation_selftest_cli --use-global
90
91---------------------------------------------------------------------------
92Diagnostics
93---------------------------------------------------------------------------
94
95Domain: TEST
96Levels:
97 L2 — lifecycle
98 L3 — semantic details
99 L4 — deep tracing
100
101---------------------------------------------------------------------------
102Contracts
103---------------------------------------------------------------------------
104
105- Must exit 0 only when all assertions pass.
106- Must exit 1 when any test fails or raises an unexpected exception.
107- Must restore INSTRUMENTATION_ENABLED and DEBUG_LEVELS after each run.
108- Must not start a Dash server or require browser interaction.
109- Must require ``oc_diagnostics[dash]`` (Dash component classes are used).
110"""
111
112import sys
113import argparse
114
116from dash import html
118 _dbg,
119 DEBUG_LEVELS,
120)
121from oc_diagnostics.instrumentation_middleware import apply_instrumentation
122
123
124# ============================================================
125# Helper: print section headers
126# ============================================================
127
128def _print_header(title: str):
129 print("\n" + "=" * 70)
130 print(title)
131 print("=" * 70)
132
133
134# ============================================================
135# TEST 1 — Basic static instrumentation
136# ============================================================
137
139 _print_header("TEST 1: Basic static instrumentation")
140
141 comp = html.Div("Hello", id="hello")
142 result = apply_instrumentation(comp)
143
144 print("Instrumented ID:", result.id)
145
146 if result is None:
147 raise AssertionError("apply_instrumentation must return a component")
148 if result is not comp:
149 raise AssertionError(
150 "apply_instrumentation should return the same object (mutates in place)"
151 )
152 if result.id != {"event": "hello"}:
153 raise AssertionError(
154 f"Expected {{'event': 'hello'}}, got: {result.id!r}"
155 )
156
157
158# ============================================================
159# TEST 2 — Nested components
160# ============================================================
161
163 _print_header("TEST 2: Nested components")
164
165 comp = html.Div(
166 [
167 html.Button("A", id="btnA"),
168 html.Div([html.Button("B", id="btnB")]),
169 ]
170 )
171
172 result = apply_instrumentation(comp)
173
174 print("btnA ->", result.children[0].id)
175 print("btnB ->", result.children[1].children[0].id)
176
177 if result is None:
178 raise AssertionError("apply_instrumentation must return a component")
179 if result.children[0].id != {"event": "btnA"}:
180 raise AssertionError(
181 f"Expected {{'event': 'btnA'}}, got: {result.children[0].id!r}"
182 )
183 if result.children[1].children[0].id != {"event": "btnB"}:
184 raise AssertionError(
185 f"Expected {{'event': 'btnB'}}, got: {result.children[1].children[0].id!r}"
186 )
187
188
189# ============================================================
190# TEST 3 — Structural ID protection
191# ============================================================
192
194 _print_header("TEST 3: Structural ID protection")
195
196 comp = html.Div("X", id="root-group-123")
197 result = apply_instrumentation(comp)
198
199 print("Structural ID preserved:", result.id)
200
201 if result is None:
202 raise AssertionError("apply_instrumentation must return a component")
203 if result.id != "root-group-123":
204 raise AssertionError(
205 f"Structural IDs must never be rewritten, got: {result.id!r}"
206 )
207
208
209# ============================================================
210# TEST 4 — Custom component behavior
211# ============================================================
212
214 _print_header("TEST 4: Custom component behavior")
215
216 class Custom:
217 def __init__(self, id_val):
218 self.id = id_val
219 self.children = []
220
221 # ----------------------------------------------------------------
222 # 4a. At INSTRUMENTATION level >= 5, custom components ARE rewritten.
223 # main() forces level 9 before running tests, so this must pass.
224 # ----------------------------------------------------------------
225 DEBUG_LEVELS["INSTRUMENTATION"] = 9
226 comp_high = Custom("custom-high")
227 result_high = apply_instrumentation(comp_high)
228 print("Custom component ID (level 9):", result_high.id)
229 if result_high is None:
230 raise AssertionError("apply_instrumentation must return a component")
231 if result_high.id != {"event": "custom-high"}:
232 raise AssertionError(
233 f"Level 9: expected {{'event': 'custom-high'}}, got: {result_high.id!r}"
234 )
235
236 # ----------------------------------------------------------------
237 # 4b. At INSTRUMENTATION level < 5, custom components are NOT rewritten.
238 # ----------------------------------------------------------------
239 DEBUG_LEVELS["INSTRUMENTATION"] = 3
240 comp_low = Custom("custom-low")
241 result_low = apply_instrumentation(comp_low)
242 print("Custom component ID (level 3):", result_low.id)
243 if result_low.id != "custom-low":
244 raise AssertionError(
245 f"Level 3: expected 'custom-low' (no rewrite), got: {result_low.id!r}"
246 )
247
248 # Restore maximum verbosity for subsequent tests.
249 DEBUG_LEVELS["INSTRUMENTATION"] = 9
250
251
252# ============================================================
253# TEST 5 — Debug level transitions
254# ============================================================
255
257 _print_header("TEST 5: Debug level transitions")
258
259 for lvl in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]:
260 DEBUG_LEVELS["INSTRUMENTATION"] = lvl
261 print(f"\n--- INSTRUMENTATION LEVEL = {lvl} ---")
262
263 comp = html.Button("X", id="btnX")
264 result = apply_instrumentation(comp)
265 print("Result ID:", result.id)
266
267 if lvl == 0:
268 if result.id != "btnX":
269 raise AssertionError(
270 f"At level 0, instrumentation must be inactive. "
271 f"Expected 'btnX', got: {result.id!r}"
272 )
273 else:
274 if result.id != {"event": "btnX"}:
275 raise AssertionError(
276 f"At level {lvl}, expected {{'event': 'btnX'}}, got: {result.id!r}"
277 )
278
279 # Restore maximum verbosity (main() will clean up to original on exit)
280 DEBUG_LEVELS["INSTRUMENTATION"] = 9
281
282
283# ============================================================
284# MAIN ENTRY POINT
285# ============================================================
286
287def main():
288 parser = argparse.ArgumentParser(description="Instrumentation Self-Test (CLI)")
289 parser.add_argument(
290 "--use-global",
291 action="store_true",
292 help="Use global debug levels instead of forcing maximum verbosity",
293 )
294 args = parser.parse_args()
295
296 force_verbose = not args.use_global
297
298 print("\nINSTRUMENTATION SELF-TEST (CLI)")
299 print("Instrumentation enabled:", _ud.INSTRUMENTATION_ENABLED)
300 print("FORCE VERBOSE MODE:", force_verbose)
301
302 # ------------------------------------------------------------
303 # Save state so it can be fully restored after the test run.
304 # ------------------------------------------------------------
305 _save_enabled = _ud.INSTRUMENTATION_ENABLED
306 _save_instr_level = DEBUG_LEVELS.get("INSTRUMENTATION", 0)
307 _save_ui_level = DEBUG_LEVELS.get("UI", 0)
308
309 # ------------------------------------------------------------
310 # Always enable instrumentation so that assertion-heavy tests
311 # actually exercise the rewriting logic.
312 # ------------------------------------------------------------
313 _ud.INSTRUMENTATION_ENABLED = True
314
315 if force_verbose:
316 DEBUG_LEVELS["INSTRUMENTATION"] = 9
317 DEBUG_LEVELS["UI"] = 9
318 _dbg("TEST", "debug_levels", "FORCING MAX VERBOSITY", override=True)
319 else:
320 _dbg("TEST", "debug_levels", "Using GLOBAL debug levels", override=True)
321
322 # ------------------------------------------------------------
323 # Run tests, collecting failures.
324 # ------------------------------------------------------------
325 tests = [
326 test_static_basic,
327 test_static_nested,
328 test_structural_protection,
329 test_custom_component_behavior,
330 test_debug_levels,
331 ]
332
333 failures: list[str] = []
334
335 for fn in tests:
336 try:
337 fn()
338 print(f" PASS {fn.__name__}")
339 except AssertionError as exc:
340 print(f" FAIL {fn.__name__}: {exc}")
341 failures.append(fn.__name__)
342 except Exception as exc:
343 print(f" ERROR {fn.__name__}: {type(exc).__name__}: {exc}")
344 failures.append(fn.__name__)
345
346 # ------------------------------------------------------------
347 # Restore original state.
348 # ------------------------------------------------------------
349 _ud.INSTRUMENTATION_ENABLED = _save_enabled
350 DEBUG_LEVELS["INSTRUMENTATION"] = _save_instr_level
351 DEBUG_LEVELS["UI"] = _save_ui_level
352
353 # ------------------------------------------------------------
354 # Print summary and exit with the appropriate code.
355 # ------------------------------------------------------------
356 print("\n" + "=" * 70)
357 if failures:
358 print(
359 f"CLI SELF-TEST FAILED: {len(failures)}/{len(tests)} test(s) failed"
360 )
361 print(f" Failed: {', '.join(failures)}")
362 print("=" * 70)
363 sys.exit(1)
364 else:
365 print(f"CLI SELF-TEST PASSED: {len(tests)}/{len(tests)} tests passed")
366 print("=" * 70)
367 sys.exit(0)
368
369
370if __name__ == "__main__":
371 main()
NoReturn apply_instrumentation(*Any args, **Any kwargs)
Definition __init__.py:72