Option C oc_diagnostics
Loading...
Searching...
No Matches
run_tests.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/run_tests.py
4
5"""
6Test Runner — oc_diagnostics
7=============================
8
9Purpose
10-------
11Execute the full ``oc_diagnostics`` test suite from the command line by
12importing and running all individual per-module test files via a lightweight
13PASS/FAIL/SKIP reporting infrastructure.
14
15Responsibilities
16----------------
17- Import test functions from each per-module test file and run them via the
18 internal ``_run()`` wrapper, which catches ``AssertionError`` and
19 unexpected exceptions and records PASS / FAIL / SKIP accordingly.
20- Handle Dash-optional test modules cleanly: attempt import and skip with a
21 clear message when Dash is absent.
22- Invoke ``instrumentation_selftest_cli`` as a subprocess when Dash is
23 available; treat a non-zero exit code as a test failure.
24- Collect PASS/SKIP/FAIL per test, print a structured summary, and exit
25 with code ``0`` on full success or ``1`` on any failure.
26
27Diagnostics
28-----------
29Domain: TEST
30Levels:
31 L2 — lifecycle
32 L3 — semantic details
33 L4 — deep tracing
34
35Contracts
36---------
37- Must exit ``0`` only when all non-skipped tests pass.
38- Must exit ``1`` when any test fails or raises an unexpected exception.
39- Core tests must run without Dash, FastAPI, or any optional dependency.
40- Must not start a Dash server or produce interactive UI.
41- Must not modify global oc_diagnostics state permanently; any state
42 changes made during tests must be restored before the test exits.
43
44Modules covered by this test suite
45-----------------------------------
46- unified_debug
47- logging_handler
48- fastapi_middleware
49- id_instrumentation
50- instrumentation_middleware
51- ui_event_logger
52- instrumentation_selftest_cli
53- instrumentation_selftest_app
54"""
55
56import os
57import subprocess
58import sys
59
60# ---------------------------------------------------------------------------
61# Ensure oc_diagnostics is importable when run from the project root or the
62# tests/ subdirectory.
63# ---------------------------------------------------------------------------
64_REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
65if _REPO_ROOT not in sys.path:
66 sys.path.insert(0, _REPO_ROOT)
67
68# Ensure this tests/ directory is importable so the individual test modules
69# can be imported by name (e.g. import test_unified_debug).
70_TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
71if _TESTS_DIR not in sys.path:
72 sys.path.insert(0, _TESTS_DIR)
73
74
75# ============================================================
76# Helpers
77# ============================================================
78
79PASS = "PASS"
80FAIL = "FAIL"
81SKIP = "SKIP"
82
83_results: list[tuple[str, str, str]] = [] # (status, name, detail)
84
85
86def _record(status: str, name: str, detail: str = ""):
87 _results.append((status, name, detail))
88 label = f" {status:<5} {name}"
89 if detail:
90 label += f": {detail}"
91 print(label)
92
93
94def _run(name: str, fn):
95 try:
96 fn()
97 _record(PASS, name)
98 except AssertionError as exc:
99 _record(FAIL, name, str(exc))
100 except Exception as exc:
101 _record(FAIL, name, f"{type(exc).__name__}: {exc}")
102
103
104# ============================================================
105# CLI self-test (Dash-optional subprocess)
106# ============================================================
107
109 """Run instrumentation_selftest_cli as a subprocess."""
110 try:
111 import dash # noqa — check Dash is available before attempting subprocess
112 except ImportError:
113 _record(SKIP, "instrumentation_selftest_cli", "Dash not installed")
114 return
115
116 result = subprocess.run(
117 [sys.executable, "-m", "oc_diagnostics.instrumentation_selftest_cli"],
118 cwd=_REPO_ROOT,
119 capture_output=True,
120 text=True,
121 timeout=60,
122 )
123
124 if result.returncode == 0:
125 _record(PASS, "instrumentation_selftest_cli")
126 else:
127 detail = (result.stdout + result.stderr).strip().splitlines()
128 last_lines = "\n ".join(detail[-5:]) if detail else "(no output)"
129 _record(
130 FAIL,
131 "instrumentation_selftest_cli",
132 f"exit {result.returncode}\n {last_lines}",
133 )
134
135
136# ============================================================
137# MAIN
138# ============================================================
139
140def main():
141 print("\noc_diagnostics — Test Suite")
142 print("=" * 60)
143
144 # Suppress the one-shot pre-init RuntimeWarning. Tests deliberately omit
145 # init() to avoid installing stream interceptors or the exception hook.
146 import oc_diagnostics.unified_debug as _ud_runner
147 _ud_runner._init_warned = True
148
149 # ------------------------------------------------------------------
150 # Core tests — no optional dependencies required
151 # ------------------------------------------------------------------
152 print("\n[Core tests — unified_debug]")
153 import test_unified_debug as _tud
154 _run("config_loading", _tud.test_config_loading)
155 _run("config_source_populated", _tud.test_config_source_populated)
156 _run("dbg_silent_no_exception", _tud.test_dbg_silent_no_exception)
157 _run("dbg_level_filtering", _tud.test_dbg_level_filtering)
158 _run("dbg_override_flag", _tud.test_dbg_override_flag)
159 _run("dbg_before_init_warning", _tud.test_dbg_before_init_warning)
160 _run("adbg_does_not_raise", _tud.test_adbg_does_not_raise)
161 _run("init_idempotent", _tud.test_init_idempotent)
162 _run("stream_interceptor_interface",_tud.test_stream_interceptor_interface)
163 _run("debug_levels_dict_mutable", _tud.test_debug_levels_dict_mutable)
164 _run("set_debug_level_valid", _tud.test_set_debug_level_valid)
165 _run("set_debug_level_invalid", _tud.test_set_debug_level_invalid)
166 _run("set_global_setting_valid", _tud.test_set_global_setting_valid)
167 _run("set_global_setting_invalid", _tud.test_set_global_setting_invalid)
168 _run("json_output_mode", _tud.test_json_output_mode)
169 _run("dbg_timed_context_manager", _tud.test_dbg_timed)
170 _run("dbg_scope_enter_exit", _tud.test_dbg_scope)
171 _run("dbg_profile_decorator", _tud.test_dbg_profile)
172 _run("prod_mode_env_var", _tud.test_prod_mode_env_var)
173 _run("prod_mode_via_config", _tud.test_prod_mode_via_config)
174 _run("log_rotation_count", _tud.test_log_rotation_count)
175 _run("reload_stale_key_removed", _tud.test_reload_stale_key_removed)
176 _run("io_degradation_oserror", _tud.test_io_degradation_oserror)
177
178 # ------------------------------------------------------------------
179 # Logging handler tests
180 # ------------------------------------------------------------------
181 print("\n[Logging handler tests]")
182 import test_logging_handler as _tlh
183 _run("handler_emit_info", _tlh.test_handler_emit_info)
184 _run("handler_level_mapping", _tlh.test_handler_level_mapping)
185 _run("handler_custom_domain", _tlh.test_handler_custom_domain)
186 _run("handler_never_raises", _tlh.test_handler_never_raises)
187
188 # ------------------------------------------------------------------
189 # FastAPI middleware tests — pure ASGI contract, no optional deps
190 # ------------------------------------------------------------------
191 print("\n[FastAPI middleware tests — pure ASGI contract]")
192 import test_fastapi_middleware as _tfm
193 _run("fastapi_http_scope_status", _tfm.test_http_scope_status_passthrough)
194 _run("fastapi_websocket_passthrough", _tfm.test_websocket_passthrough)
195 _run("fastapi_lifespan_passthrough", _tfm.test_lifespan_scope_passthrough)
196 _run("fastapi_exception_reraise", _tfm.test_exception_reraise)
197 _run("fastapi_http_header_redaction", _tfm.test_http_header_redaction)
198 _run("fastapi_elapsed_time_in_log", _tfm.test_elapsed_time_in_log)
199
200 # ------------------------------------------------------------------
201 # Dash tests — skipped automatically when Dash is not installed
202 # ------------------------------------------------------------------
203 print("\n[Dash tests — skipped automatically when Dash is not installed]")
204 try:
205 import dash # noqa — availability check only
206 _dash_available = True
207 except ImportError:
208 _dash_available = False
209
210 if not _dash_available:
211 _record(SKIP, "dash_tests", "Dash not installed — skipping all Dash tests")
212 else:
213 import test_id_instrumentation as _tid
214 _run("structural_id_protection", _tid.test_structural_id_protection)
215 _run("instrument_ids_basic", _tid.test_instrument_ids_basic)
216 _run("dict_id_not_rewritten", _tid.test_dict_id_not_rewritten)
217 _run("no_id_component_unchanged", _tid.test_no_id_component_unchanged)
218 _run("max_depth_limit", _tid.test_max_depth_limit)
219 _run("nested_children_traversal", _tid.test_nested_children_traversal)
220 _run("additional_structural_prefixes", _tid.test_additional_structural_prefixes)
221 _run("additional_structural_exact", _tid.test_additional_structural_exact)
222
223 import test_instrumentation_middleware as _tim
224 _run("apply_instrumentation_no_op", _tim.test_apply_instrumentation_no_op)
225 _run("apply_list_return", _tim.test_apply_list_return)
226 _run("apply_tuple_return", _tim.test_apply_tuple_return)
227 _run("apply_single_component", _tim.test_apply_single_component)
228 _run("no_double_wrapping", _tim.test_no_double_wrapping)
229
230 import test_ui_event_logger as _tuel
231 _run("ui_event_logger_import", _tuel.test_ui_event_logger_import)
232 _run("required_store_id_constant", _tuel.test_required_store_id_constant)
233 _run("ui_logger_custom_props", _tuel.test_ui_logger_custom_props)
234
236
237 # ------------------------------------------------------------------
238 # Summary
239 # ------------------------------------------------------------------
240 passed = sum(1 for s, _, _ in _results if s == PASS)
241 failed = sum(1 for s, _, _ in _results if s == FAIL)
242 skipped = sum(1 for s, _, _ in _results if s == SKIP)
243 total = len(_results)
244
245 print("\n" + "=" * 60)
246 print(
247 f"Results: {passed} passed, {failed} failed, {skipped} skipped ({total} total)"
248 )
249
250 if failed:
251 print("OVERALL: FAILED")
252 print("=" * 60)
253 sys.exit(1)
254 else:
255 print("OVERALL: PASSED")
256 print("=" * 60)
257 sys.exit(0)
258
259
260if __name__ == "__main__":
261 main()
_run(str name, fn)
Definition run_tests.py:94
_record(str status, str name, str detail="")
Definition run_tests.py:86
_run_cli_selftest()
Definition run_tests.py:108