6Regression Tests — unified_debug
7=================================
11Verify the full public and internal API surface of ``oc_diagnostics.unified_debug``,
12covering configuration loading, the ``_dbg`` debug logger, async logging, runtime
13overrides, profiling helpers, production mode, log rotation, and I/O fallback.
17- Test config loading returns the expected 4-tuple structure.
18- Test ``_dbg()`` domain-level filtering and ``override=True`` bypass.
19- Test ``_adbg()`` completes without raising.
20- Test ``init()`` idempotency and the pre-init RuntimeWarning.
21- Test runtime API: ``set_debug_level()``, ``set_global_setting()``.
22- Test profiling helpers: ``dbg_timed()``, ``dbg_scope()``, ``dbg_profile()``.
23- Test JSONL output mode and ``_StreamInterceptor`` interface.
24- Test production mode activation via env var and via config file.
25- Test log-file count-based rotation via ``_prune_old_log_files()``.
26- Test config hot-reload stale-key pruning via ``_reload_config()``.
27- Test I/O degradation fallback on ``OSError`` (FS-25).
39- Must not permanently modify oc_diagnostics global state.
40- Must restore all patched globals and mocked internals in finally blocks.
41- Must not start servers, threads, or require network access.
42- Must run deterministically across repeated invocations.
54import unittest.mock
as mock
61_REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__),
".."))
62if _REPO_ROOT
not in sys.path:
63 sys.path.insert(0, _REPO_ROOT)
78 """Return a minimal settings dict compatible with _reload_config()."""
80 "instrumentation_enabled": _ud.INSTRUMENTATION_ENABLED,
81 "silent_mode": _ud.SILENT_MODE,
82 "structural_id_prefixes": list(_ud.ADDITIONAL_STRUCTURAL_PREFIXES),
83 "structural_id_exact": list(_ud.ADDITIONAL_STRUCTURAL_EXACT),
84 "enable_timestamps": _ud.ENABLE_TIMESTAMPS,
85 "enable_colors": _ud.ENABLE_COLORS,
86 "include_filename": _ud.INCLUDE_FILENAME,
87 "output_mode": _ud.OUTPUT_MODE,
88 "max_log_files": _ud.MAX_LOG_FILES,
90 float(_ud.MAX_LOG_SIZE_BYTES) / (1024 * 1024)
91 if _ud.MAX_LOG_SIZE_BYTES
else 0.0
93 "instrumentation_allow_all": _ud.INSTRUMENTATION_ALL,
94 "ui_event_properties": list(_ud.UI_EVENT_PROPERTIES),
95 "http_redact_headers": list(_ud.HTTP_REDACT_HEADERS),
105 """_load_debug_config() returns a 4-tuple with expected types."""
106 result = _load_debug_config()
107 assert isinstance(result, tuple)
and len(result) == 4, (
108 f
"Expected 4-tuple, got {type(result)} len={len(result) if isinstance(result, tuple) else 'N/A'}"
110 levels, colors, settings, source = result
111 assert isinstance(levels, dict), f
"levels must be dict, got {type(levels)}"
112 assert isinstance(colors, dict), f
"colors must be dict, got {type(colors)}"
113 assert isinstance(settings, dict), f
"settings must be dict, got {type(settings)}"
114 assert isinstance(source, str)
and source,
"config_source must be a non-empty string"
118 """CONFIG_SOURCE is a non-empty string after module load."""
119 assert isinstance(_ud.CONFIG_SOURCE, str)
and _ud.CONFIG_SOURCE, (
120 f
"Expected non-empty string, got: {_ud.CONFIG_SOURCE!r}"
129 """_dbg() with SILENT_MODE=True must not raise."""
130 old = _ud.SILENT_MODE
131 _ud.SILENT_MODE =
True
133 _ud._dbg(
"TEST",
"test_dbg_silent",
"should not crash", level=2)
135 _ud.SILENT_MODE = old
139 """_dbg() message is suppressed when the call level exceeds the domain level."""
142 old_silent = _ud.SILENT_MODE
143 old_level = _ud.DEBUG_LEVELS.get(
"_FILTER_TEST_")
144 old_init = _ud._initialized
145 old_warned = _ud._init_warned
147 def _capture(line, level, record=None):
148 captured.append(line)
151 _ud.SILENT_MODE =
False
152 _ud.DEBUG_LEVELS[
"_FILTER_TEST_"] = 3
153 _ud._initialized =
True
156 _ud._dbg(
"_FILTER_TEST_",
"test_fn",
"should-not-appear", level=5)
157 assert not captured, (
158 f
"Level-5 message should be filtered at domain level 3; got: {captured!r}"
162 _ud._dbg(
"_FILTER_TEST_",
"test_fn",
"should-appear", level=2)
163 assert any(
"should-appear" in l
for l
in captured), (
164 f
"Level-2 message should pass at domain level 3; got: {captured!r}"
168 _ud.SILENT_MODE = old_silent
169 _ud._initialized = old_init
170 _ud._init_warned = old_warned
171 if old_level
is None:
172 _ud.DEBUG_LEVELS.pop(
"_FILTER_TEST_",
None)
174 _ud.DEBUG_LEVELS[
"_FILTER_TEST_"] = old_level
178 """_dbg(..., override=True) always emits regardless of domain level."""
181 old_silent = _ud.SILENT_MODE
182 old_level = _ud.DEBUG_LEVELS.get(
"_OVERRIDE_TEST_")
183 old_init = _ud._initialized
185 def _capture(line, level, record=None):
186 captured.append(line)
189 _ud.SILENT_MODE =
False
190 _ud.DEBUG_LEVELS[
"_OVERRIDE_TEST_"] = 0
191 _ud._initialized =
True
193 _ud._dbg(
"_OVERRIDE_TEST_",
"test_fn",
"override-message", level=9, override=
True)
194 assert any(
"override-message" in l
for l
in captured), (
195 f
"override=True must bypass level filter; got: {captured!r}"
199 _ud.SILENT_MODE = old_silent
200 _ud._initialized = old_init
201 if old_level
is None:
202 _ud.DEBUG_LEVELS.pop(
"_OVERRIDE_TEST_",
None)
204 _ud.DEBUG_LEVELS[
"_OVERRIDE_TEST_"] = old_level
208 """_dbg() before init() emits a one-shot RuntimeWarning; second call does not repeat it."""
209 old_init = _ud._initialized
210 old_warned = _ud._init_warned
211 old_silent = _ud.SILENT_MODE
216 def _capture(line, level, record=None):
217 captured_lines.append(line)
219 _ud._initialized =
False
220 _ud._init_warned =
False
221 _ud.SILENT_MODE =
False
224 with warnings.catch_warnings(record=
True)
as w:
225 warnings.simplefilter(
"always")
226 _ud._dbg(
"GENERAL",
"test_fn",
"pre-init call", level=2, override=
True)
228 assert len(w) == 1, (
229 f
"Expected exactly 1 RuntimeWarning on first pre-init call, got {len(w)}: "
230 f
"{[str(x.message) for x in w]}"
232 assert issubclass(w[0].category, RuntimeWarning), (
233 f
"Expected RuntimeWarning, got {w[0].category}"
235 assert _ud._init_warned
is True, (
236 "_init_warned must be True after the first warning"
240 with warnings.catch_warnings(record=
True)
as w2:
241 warnings.simplefilter(
"always")
242 _ud._dbg(
"GENERAL",
"test_fn",
"second call", level=2, override=
True)
244 assert len(w2) == 0, (
245 f
"Expected no second RuntimeWarning, got: {[str(x.message) for x in w2]}"
248 _ud._initialized = old_init
249 _ud._init_warned = old_warned
250 _ud.SILENT_MODE = old_silent
259 """_adbg() completes without raising, in silent mode for cleanliness."""
260 old_silent = _ud.SILENT_MODE
261 old_level = _ud.DEBUG_LEVELS.get(
"_ADBG_TEST_")
262 _ud.SILENT_MODE =
True
263 _ud.DEBUG_LEVELS[
"_ADBG_TEST_"] = 9
265 asyncio.run(_ud._adbg(
"_ADBG_TEST_",
"test_fn",
"async message", level=2))
267 _ud.SILENT_MODE = old_silent
268 if old_level
is None:
269 _ud.DEBUG_LEVELS.pop(
"_ADBG_TEST_",
None)
271 _ud.DEBUG_LEVELS[
"_ADBG_TEST_"] = old_level
279 """init() can be called multiple times without error or side-effect duplication."""
280 old_init = _ud._initialized
281 old_stdout = sys.stdout
282 old_stderr = sys.stderr
283 old_excepthook = sys.excepthook
284 _ud._initialized =
False
287 _ud.init(intercept_streams=
False, intercept_exceptions=
False)
288 _ud.init(intercept_streams=
False, intercept_exceptions=
False)
289 assert _ud._initialized
is True,
"init() must set _initialized=True"
291 _ud._initialized = old_init
292 sys.stdout = old_stdout
293 sys.stderr = old_stderr
294 sys.excepthook = old_excepthook
302 """_StreamInterceptor exposes the full io.TextIOBase-compatible interface."""
304 assert si.writable()
is True,
"writable() must return True"
305 assert si.readable()
is False,
"readable() must return False"
306 assert si.seekable()
is False,
"seekable() must return False"
307 assert isinstance(si.encoding, str), f
"encoding must be str, got {type(si.encoding)}"
308 assert isinstance(si.errors, str), f
"errors must be str, got {type(si.errors)}"
310 assert isinstance(result, bool), f
"isatty() must return bool, got {type(result)}"
318 """DEBUG_LEVELS is a mutable dict and changes are reflected globally."""
319 _save = _ud.DEBUG_LEVELS.get(
"_TEST_DOMAIN_")
320 _ud.DEBUG_LEVELS[
"_TEST_DOMAIN_"] = 7
321 assert _ud.DEBUG_LEVELS[
"_TEST_DOMAIN_"] == 7,
"DEBUG_LEVELS mutation not reflected"
323 del _ud.DEBUG_LEVELS[
"_TEST_DOMAIN_"]
325 _ud.DEBUG_LEVELS[
"_TEST_DOMAIN_"] = _save
333 """set_debug_level() updates DEBUG_LEVELS at runtime."""
334 old = _ud.DEBUG_LEVELS.get(
"_FS03_TEST_")
335 _ud.set_debug_level(
"_FS03_TEST_", 5)
337 assert _ud.DEBUG_LEVELS.get(
"_FS03_TEST_") == 5, (
338 f
"Expected 5, got {_ud.DEBUG_LEVELS.get('_FS03_TEST_')!r}"
342 _ud.DEBUG_LEVELS.pop(
"_FS03_TEST_",
None)
344 _ud.DEBUG_LEVELS[
"_FS03_TEST_"] = old
348 """set_debug_level() with an out-of-range value emits UserWarning and does not update."""
349 old = _ud.DEBUG_LEVELS.get(
"_FS03_TEST2_")
350 with warnings.catch_warnings(record=
True)
as w:
351 warnings.simplefilter(
"always")
352 _ud.set_debug_level(
"_FS03_TEST2_", 99)
354 assert len(w) == 1, f
"Expected 1 warning, got {len(w)}"
355 assert issubclass(w[0].category, UserWarning),
"Expected UserWarning"
356 assert _ud.DEBUG_LEVELS.get(
"_FS03_TEST2_")
is old, (
357 "Invalid level must not update DEBUG_LEVELS"
360 if "_FS03_TEST2_" in _ud.DEBUG_LEVELS
and old
is None:
361 del _ud.DEBUG_LEVELS[
"_FS03_TEST2_"]
365 """set_global_setting() updates SILENT_MODE at runtime."""
366 old = _ud.SILENT_MODE
367 _ud.set_global_setting(
"silent_mode",
True)
369 assert _ud.SILENT_MODE
is True, f
"Expected True, got {_ud.SILENT_MODE!r}"
371 _ud.SILENT_MODE = old
375 """set_global_setting() with an unknown key emits UserWarning."""
376 with warnings.catch_warnings(record=
True)
as w:
377 warnings.simplefilter(
"always")
378 _ud.set_global_setting(
"nonexistent_key_xyz", 42)
379 assert len(w) == 1, f
"Expected 1 warning, got {len(w)}"
380 assert issubclass(w[0].category, UserWarning),
"Expected UserWarning"
388 """OUTPUT_MODE='json' writes valid JSONL records to the log file."""
390 old_mode = _ud.OUTPUT_MODE
391 old_log_dir = _ud.LOG_DIR
392 old_log_file = _ud.LOG_FILE
393 old_header = _ud._header_written
394 old_silent = _ud.SILENT_MODE
395 old_level = _ud.DEBUG_LEVELS.get(
"_FSJSON_")
396 old_init = _ud._initialized
397 with tempfile.TemporaryDirectory()
as tmpdir:
398 _ud.OUTPUT_MODE =
"json"
401 _ud._header_written =
False
402 _ud.SILENT_MODE =
False
403 _ud.DEBUG_LEVELS[
"_FSJSON_"] = 9
404 _ud._initialized =
True
406 _ud._dbg(
"_FSJSON_",
"test_fn",
"hello json", level=2, override=
True)
408 _ud.OUTPUT_MODE = old_mode
409 _ud.LOG_DIR = old_log_dir
410 _ud.LOG_FILE = old_log_file
411 _ud._header_written = old_header
412 _ud.SILENT_MODE = old_silent
413 _ud._initialized = old_init
414 if old_level
is None:
415 _ud.DEBUG_LEVELS.pop(
"_FSJSON_",
None)
417 _ud.DEBUG_LEVELS[
"_FSJSON_"] = old_level
418 files = _glob.glob(os.path.join(tmpdir,
"*.txt"))
419 assert files,
"Expected at least one log file to be created"
420 with open(files[0], encoding=
"utf-8")
as f:
421 raw_lines = f.readlines()
423 for raw
in raw_lines:
424 stripped = raw.strip()
425 if stripped.startswith(
"{"):
427 json_lines.append(json.loads(stripped))
428 except json.JSONDecodeError:
431 f
"Expected at least one valid JSONL record, got lines: {raw_lines!r}"
434 assert rec.get(
"domain") ==
"_FSJSON_", (
435 f
"Expected domain '_FSJSON_', got {rec!r}"
437 assert rec.get(
"msg") ==
"hello json", (
438 f
"Expected msg 'hello json', got {rec!r}"
447 """dbg_timed() context manager emits ENTER and EXIT lines with elapsed ms."""
450 old_silent = _ud.SILENT_MODE
451 old_level = _ud.DEBUG_LEVELS.get(
"GENERAL")
452 old_init = _ud._initialized
454 def _capture(line, level, record=None):
455 captured.append(line)
458 _ud.SILENT_MODE =
False
459 _ud.DEBUG_LEVELS[
"GENERAL"] = 9
460 _ud._initialized =
True
462 with _ud.dbg_timed(
"GENERAL",
"timing_test", label=
"myop", level=2):
466 _ud.SILENT_MODE = old_silent
467 _ud._initialized = old_init
468 if old_level
is None:
469 _ud.DEBUG_LEVELS.pop(
"GENERAL",
None)
471 _ud.DEBUG_LEVELS[
"GENERAL"] = old_level
473 enter_lines = [l
for l
in captured
if "ENTER" in l]
474 exit_lines = [l
for l
in captured
if "EXIT" in l
and "ms" in l]
475 assert enter_lines, f
"Expected ENTER line in output, got: {captured!r}"
476 assert exit_lines, f
"Expected EXIT line with 'ms' in output, got: {captured!r}"
480 """dbg_scope() emits ENTER and EXIT lines without timing information (FS-19)."""
483 old_silent = _ud.SILENT_MODE
484 old_level = _ud.DEBUG_LEVELS.get(
"GENERAL")
485 old_init = _ud._initialized
487 def _capture(line, level, record=None):
488 captured.append(line)
491 _ud.SILENT_MODE =
False
492 _ud.DEBUG_LEVELS[
"GENERAL"] = 9
493 _ud._initialized =
True
495 with _ud.dbg_scope(
"GENERAL",
"scope_test", label=
"myblock", level=2):
499 _ud.SILENT_MODE = old_silent
500 _ud._initialized = old_init
501 if old_level
is None:
502 _ud.DEBUG_LEVELS.pop(
"GENERAL",
None)
504 _ud.DEBUG_LEVELS[
"GENERAL"] = old_level
506 enter_lines = [l
for l
in captured
if "ENTER" in l]
507 exit_lines = [l
for l
in captured
if "EXIT" in l]
508 assert enter_lines, f
"Expected ENTER line, got: {captured!r}"
509 assert exit_lines, f
"Expected EXIT line, got: {captured!r}"
510 assert not any(
"ms" in l
for l
in exit_lines), (
511 f
"dbg_scope EXIT must not include timing info, got: {exit_lines!r}"
516 """dbg_profile() decorator wraps a function and preserves its return value."""
519 old_silent = _ud.SILENT_MODE
520 old_level = _ud.DEBUG_LEVELS.get(
"GENERAL")
521 old_init = _ud._initialized
523 def _capture(line, level, record=None):
524 captured.append(line)
527 _ud.SILENT_MODE =
False
528 _ud.DEBUG_LEVELS[
"GENERAL"] = 9
529 _ud._initialized =
True
531 @_ud.dbg_profile("GENERAL", label="profiled_fn", level=2)
538 _ud.SILENT_MODE = old_silent
539 _ud._initialized = old_init
540 if old_level
is None:
541 _ud.DEBUG_LEVELS.pop(
"GENERAL",
None)
543 _ud.DEBUG_LEVELS[
"GENERAL"] = old_level
545 assert result == 42, (
546 f
"Decorated function return value must be preserved, got {result!r}"
548 enter_lines = [l
for l
in captured
if "ENTER" in l]
549 exit_lines = [l
for l
in captured
if "EXIT" in l
and "ms" in l]
550 assert enter_lines, f
"Expected ENTER line in output, got: {captured!r}"
551 assert exit_lines, f
"Expected EXIT line with 'ms' in output, got: {captured!r}"
559 """OC_DIAGNOSTICS_MODE=prod sets PROD_MODE=True at module load."""
560 result = subprocess.run(
562 sys.executable,
"-c",
563 "import oc_diagnostics.unified_debug as _ud; "
564 "assert _ud.PROD_MODE is True, f'PROD_MODE={_ud.PROD_MODE!r}'"
566 env={**os.environ,
"OC_DIAGNOSTICS_MODE":
"prod"},
571 assert result.returncode == 0, (
572 f
"PROD_MODE not set True by env var; stderr: {result.stderr.strip()!r}"
577 """mode='prod' in debug_config.json global_settings sets PROD_MODE=True."""
578 config = {
"global_settings": {
"mode":
"prod"}}
581 with tempfile.NamedTemporaryFile(
582 mode=
"w", suffix=
".json", delete=
False, encoding=
"utf-8"
587 env = dict(os.environ)
588 env.pop(
"OC_DIAGNOSTICS_MODE",
None)
589 env[
"OC_DIAGNOSTICS_CONFIG"] = tmp_path
591 result = subprocess.run(
593 sys.executable,
"-c",
594 "import oc_diagnostics.unified_debug as _ud; "
595 "assert _ud.PROD_MODE is True, f'PROD_MODE={_ud.PROD_MODE!r}'"
602 assert result.returncode == 0, (
603 f
"PROD_MODE not set by config mode='prod'; stderr: {result.stderr.strip()!r}"
606 if tmp_path
and os.path.exists(tmp_path):
615 """_prune_old_log_files() removes the oldest files when count exceeds max."""
618 with tempfile.TemporaryDirectory()
as tmpdir:
622 tmpdir, f
"unified_debug_logs_202401{i:02d}_000000.txt"
624 with open(path,
"w")
as f:
627 os.utime(path, (time.time() - (5 - i), time.time() - (5 - i)))
629 _prune_old_log_files(tmpdir, 3)
632 _glob.glob(os.path.join(tmpdir,
"unified_debug_logs_*.txt"))
634 assert len(remaining) == 3, (
635 f
"Expected 3 files after pruning max_count=3, got {len(remaining)}: {remaining}"
644 """_reload_config() prunes stale keys; DEBUG_LEVELS is never empty."""
645 stale_key =
"_OI29_TEST_STALE"
646 _ud.DEBUG_LEVELS[stale_key] = 7
647 new_levels = {
"DATA": 3,
"UI": 2}
648 with mock.patch.object(
649 _ud,
"_load_debug_config",
655 assert stale_key
not in _ud.DEBUG_LEVELS, (
656 f
"Stale key '{stale_key}' must be removed after _reload_config()"
658 assert "DATA" in _ud.DEBUG_LEVELS,
"New key 'DATA' must be present after reload"
659 assert len(_ud.DEBUG_LEVELS) > 0,
"DEBUG_LEVELS must never be empty after reload"
667 """FS-25: _write_to_file() OSError triggers one-time warning and mode fallback."""
668 old_mode = _ud.OUTPUT_MODE
669 old_warned = _ud._io_fallback_warned
670 old_stdout = _ud.ORIGINAL_STDOUT
671 stdout_capture = io.StringIO()
672 _ud.OUTPUT_MODE =
"file"
673 _ud._io_fallback_warned =
False
674 _ud.ORIGINAL_STDOUT = stdout_capture
676 with mock.patch(
"builtins.open", side_effect=OSError(
"No space left on device")):
677 _ud._write_to_file(
"test line")
678 assert _ud.OUTPUT_MODE ==
"terminal", (
679 "OUTPUT_MODE must switch to 'terminal' after OSError"
681 assert _ud._io_fallback_warned
is True, (
682 "_io_fallback_warned must be True after first OSError"
684 warning_text = stdout_capture.getvalue()
685 assert "WARNING" in warning_text, (
686 f
"Expected 'WARNING' in stdout, got: {warning_text!r}"
689 stdout_capture.truncate(0)
690 stdout_capture.seek(0)
691 _ud._write_to_file(
"second line")
692 assert stdout_capture.getvalue() ==
"", (
693 "No second warning should be emitted for subsequent failures"
696 _ud.OUTPUT_MODE = old_mode
697 _ud._io_fallback_warned = old_warned
698 _ud.ORIGINAL_STDOUT = old_stdout
706 """_dbg_assert() with truthy condition does not raise or emit."""
709 old_init = _ud._initialized
710 old_prod = _ud.PROD_MODE
711 _ud._emit =
lambda line, level, record=
None: captured.append(line)
712 _ud._initialized =
True
713 _ud.PROD_MODE =
False
715 _ud._dbg_assert(
"TEST",
True,
"should not fire", func=
"test_fn")
716 assert not captured, f
"Truthy condition should emit nothing, got: {captured!r}"
719 _ud._initialized = old_init
720 _ud.PROD_MODE = old_prod
724 """_dbg_assert() with falsy condition emits L1 override + raises AssertionError."""
727 old_init = _ud._initialized
728 old_prod = _ud.PROD_MODE
729 _ud._emit =
lambda line, level, record=
None: captured.append(line)
730 _ud._initialized =
True
731 _ud.PROD_MODE =
False
735 _ud._dbg_assert(
"TEST",
False,
"batch must not be empty", func=
"process")
736 except AssertionError
as e:
738 assert "batch must not be empty" in str(e)
739 assert raised,
"_dbg_assert with False must raise AssertionError"
740 assert any(
"ASSERT FAILED" in l
for l
in captured), (
741 f
"Expected 'ASSERT FAILED' in output, got: {captured!r}"
745 _ud._initialized = old_init
746 _ud.PROD_MODE = old_prod
750 """_dbg_assert() in PROD_MODE is a complete no-op — condition not evaluated."""
751 old_prod = _ud.PROD_MODE
755 _ud._dbg_assert(
"TEST",
False,
"should be suppressed", func=
"test_fn")
757 _ud.PROD_MODE = old_prod
765 """@dbg_trace emits START and END messages."""
768 old_init = _ud._initialized
769 old_level = _ud.DEBUG_LEVELS.get(
"_TRACE_")
770 _ud._emit =
lambda line, level, record=
None: captured.append(line)
771 _ud._initialized =
True
772 _ud.DEBUG_LEVELS[
"_TRACE_"] = 9
774 @_ud.dbg_trace("_TRACE_", level=2)
779 assert result == 42, f
"Return value must be preserved, got {result!r}"
780 assert any(
"START" in l
for l
in captured), f
"Expected START, got: {captured!r}"
781 assert any(
"END" in l
for l
in captured), f
"Expected END, got: {captured!r}"
784 _ud._initialized = old_init
785 if old_level
is None:
786 _ud.DEBUG_LEVELS.pop(
"_TRACE_",
None)
788 _ud.DEBUG_LEVELS[
"_TRACE_"] = old_level
792 """@dbg_trace logs exception at L1 and re-raises."""
795 old_init = _ud._initialized
796 old_level = _ud.DEBUG_LEVELS.get(
"_TRACE_")
797 _ud._emit =
lambda line, level, record=
None: captured.append(line)
798 _ud._initialized =
True
799 _ud.DEBUG_LEVELS[
"_TRACE_"] = 9
801 @_ud.dbg_trace("_TRACE_")
803 raise ValueError(
"test error")
810 assert raised,
"Exception must be re-raised"
811 assert any(
"EXCEPTION" in l
and "ValueError" in l
for l
in captured), (
812 f
"Expected EXCEPTION log with ValueError, got: {captured!r}"
816 _ud._initialized = old_init
817 if old_level
is None:
818 _ud.DEBUG_LEVELS.pop(
"_TRACE_",
None)
820 _ud.DEBUG_LEVELS[
"_TRACE_"] = old_level
824 """@dbg_trace preserves __name__ via functools.wraps."""
825 @_ud.dbg_trace("TEST")
826 def named_function():
829 assert named_function.__name__ ==
"named_function", (
830 f
"Expected 'named_function', got {named_function.__name__!r}"
839 """@dbg_metrics emits structured JSON with duration_ms and memory_mb."""
842 old_init = _ud._initialized
843 old_level = _ud.DEBUG_LEVELS.get(
"_METRICS_")
844 _ud._emit =
lambda line, level, record=
None: captured.append(line)
845 _ud._initialized =
True
846 _ud.DEBUG_LEVELS[
"_METRICS_"] = 9
848 @_ud.dbg_metrics("_METRICS_", level=3)
850 return sum(range(1000))
853 assert result == 499500, f
"Return value must be preserved, got {result!r}"
854 metrics_lines = [l
for l
in captured
if "METRICS:" in l]
855 assert metrics_lines, f
"Expected METRICS line, got: {captured!r}"
857 json_str = metrics_lines[0].split(
"METRICS: ", 1)[1]
859 json_str = _ud._ANSI_ESCAPE_RE.sub(
"", json_str)
860 data = json.loads(json_str)
861 assert "duration_ms" in data, f
"Expected duration_ms key, got: {data!r}"
862 assert "memory_mb" in data, f
"Expected memory_mb key, got: {data!r}"
863 assert isinstance(data[
"duration_ms"], (int, float)),
"duration_ms must be numeric"
866 _ud._initialized = old_init
867 if old_level
is None:
868 _ud.DEBUG_LEVELS.pop(
"_METRICS_",
None)
870 _ud.DEBUG_LEVELS[
"_METRICS_"] = old_level
878 """AI-Trace messages (level='ai_trace') do not reach the console."""
879 console_captured = []
880 old_stdout = _ud.ORIGINAL_STDOUT
881 old_mode = _ud.OUTPUT_MODE
882 old_ai = _ud.AI_TRACE_ENABLED
883 old_silent = _ud.SILENT_MODE
884 old_init = _ud._initialized
886 fake_stdout = io.StringIO()
887 _ud.ORIGINAL_STDOUT = fake_stdout
888 _ud.OUTPUT_MODE =
"both"
889 _ud.AI_TRACE_ENABLED =
True
890 _ud.SILENT_MODE =
False
891 _ud._initialized =
True
893 _ud._dbg(
"TEST",
"test_fn",
"ai trace msg", level=
"ai_trace")
894 console_output = fake_stdout.getvalue()
895 assert "ai trace msg" not in console_output, (
896 f
"AI-Trace must not reach console, got: {console_output!r}"
899 _ud.ORIGINAL_STDOUT = old_stdout
900 _ud.OUTPUT_MODE = old_mode
901 _ud.AI_TRACE_ENABLED = old_ai
902 _ud.SILENT_MODE = old_silent
903 _ud._initialized = old_init
907 """AI-Trace messages are written to the log file."""
908 old_mode = _ud.OUTPUT_MODE
909 old_ai = _ud.AI_TRACE_ENABLED
910 old_log_dir = _ud.LOG_DIR
911 old_log_file = _ud.LOG_FILE
912 old_header = _ud._header_written
913 old_silent = _ud.SILENT_MODE
914 old_init = _ud._initialized
916 with tempfile.TemporaryDirectory()
as tmpdir:
917 _ud.OUTPUT_MODE =
"both"
918 _ud.AI_TRACE_ENABLED =
True
921 _ud._header_written =
False
922 _ud.SILENT_MODE =
False
923 _ud._initialized =
True
925 _ud._dbg(
"TEST",
"test_fn",
"ai shadow message", level=
"ai_trace")
927 _ud.OUTPUT_MODE = old_mode
928 _ud.AI_TRACE_ENABLED = old_ai
929 _ud.LOG_DIR = old_log_dir
930 _ud.LOG_FILE = old_log_file
931 _ud._header_written = old_header
932 _ud.SILENT_MODE = old_silent
933 _ud._initialized = old_init
936 files = _glob.glob(os.path.join(tmpdir,
"*.txt"))
937 assert files,
"Expected at least one log file"
938 content = open(files[0], encoding=
"utf-8").read()
939 assert "ai shadow message" in content, (
940 f
"AI-Trace message must be in log file, got: {content!r}"
945 """AI-Trace messages are silently discarded when AI_TRACE_ENABLED is False."""
948 old_ai = _ud.AI_TRACE_ENABLED
949 old_silent = _ud.SILENT_MODE
950 _ud._emit =
lambda line, level, record=
None: captured.append(line)
951 _ud.AI_TRACE_ENABLED =
False
952 _ud.SILENT_MODE =
False
954 _ud._dbg(
"TEST",
"test_fn",
"should vanish", level=
"ai_trace")
955 assert not captured, f
"Disabled AI-Trace must emit nothing, got: {captured!r}"
958 _ud.AI_TRACE_ENABLED = old_ai
959 _ud.SILENT_MODE = old_silent
967 """oc_diagnostics version must be 2.0.0."""
968 import oc_diagnostics
969 assert oc_diagnostics.__version__ ==
"2.0.0", (
970 f
"Expected version 2.0.0, got {oc_diagnostics.__version__!r}"
979 """V2.0 config with 'DEBUG_LEVELS' key loads correctly."""
981 "_schema_version":
"2.0",
982 "DEBUG_LEVELS": {
"DATA": {
"level": 4,
"color":
"cyan"},
"UI": 2},
983 "global_settings": {
"mode":
"dev"},
987 with tempfile.NamedTemporaryFile(
988 mode=
"w", suffix=
".json", delete=
False, encoding=
"utf-8"
993 env = dict(os.environ)
994 env[
"OC_DIAGNOSTICS_CONFIG"] = tmp_path
995 result = subprocess.run(
997 sys.executable,
"-c",
998 "import oc_diagnostics.unified_debug as _ud; "
999 "assert _ud.DEBUG_LEVELS.get('DATA') == 4, f'DATA={_ud.DEBUG_LEVELS.get(\"DATA\")}'; "
1000 "assert _ud.DEBUG_LEVELS.get('UI') == 2, f'UI={_ud.DEBUG_LEVELS.get(\"UI\")}'"
1003 capture_output=
True,
1007 assert result.returncode == 0, (
1008 f
"DEBUG_LEVELS key not loaded; stderr: {result.stderr.strip()!r}"
1011 if tmp_path
and os.path.exists(tmp_path):
1016 """V2.0 nested log_rotation object is parsed correctly."""
1018 "DEBUG_LEVELS": {
"GENERAL": 2},
1019 "global_settings": {
1020 "log_rotation": {
"max_files": 10,
"max_size_mb": 2.5},
1025 with tempfile.NamedTemporaryFile(
1026 mode=
"w", suffix=
".json", delete=
False, encoding=
"utf-8"
1028 json.dump(config, f)
1031 env = dict(os.environ)
1032 env[
"OC_DIAGNOSTICS_CONFIG"] = tmp_path
1033 result = subprocess.run(
1035 sys.executable,
"-c",
1036 "import oc_diagnostics.unified_debug as _ud; "
1037 "assert _ud.MAX_LOG_FILES == 10, f'max_files={_ud.MAX_LOG_FILES}'; "
1038 f
"expected_bytes = int(2.5 * 1024 * 1024); "
1039 "assert _ud.MAX_LOG_SIZE_BYTES == expected_bytes, "
1040 "f'max_size={_ud.MAX_LOG_SIZE_BYTES}'"
1043 capture_output=
True,
1047 assert result.returncode == 0, (
1048 f
"Nested log_rotation not parsed; stderr: {result.stderr.strip()!r}"
1051 if tmp_path
and os.path.exists(tmp_path):
1060 """OC_DIAGNOSTICS_LEVEL env var overrides all domain levels."""
1062 "DEBUG_LEVELS": {
"DATA": 2,
"UI": 3},
1063 "global_settings": {},
1067 with tempfile.NamedTemporaryFile(
1068 mode=
"w", suffix=
".json", delete=
False, encoding=
"utf-8"
1070 json.dump(config, f)
1073 env = dict(os.environ)
1074 env[
"OC_DIAGNOSTICS_CONFIG"] = tmp_path
1075 env[
"OC_DIAGNOSTICS_LEVEL"] =
"7"
1076 result = subprocess.run(
1078 sys.executable,
"-c",
1079 "import oc_diagnostics.unified_debug as _ud; "
1080 "assert _ud.DEBUG_LEVELS.get('DATA') == 7, f'DATA={_ud.DEBUG_LEVELS.get(\"DATA\")}'; "
1081 "assert _ud.DEBUG_LEVELS.get('UI') == 7, f'UI={_ud.DEBUG_LEVELS.get(\"UI\")}'"
1084 capture_output=
True,
1088 assert result.returncode == 0, (
1089 f
"OC_DIAGNOSTICS_LEVEL override failed; stderr: {result.stderr.strip()!r}"
1092 if tmp_path
and os.path.exists(tmp_path):
1097 """OC_DIAGNOSTICS_LOG_FILE env var sets LOG_FILE at load time."""
1098 result = subprocess.run(
1100 sys.executable,
"-c",
1101 "import oc_diagnostics.unified_debug as _ud; "
1102 "assert _ud.LOG_FILE == '/tmp/test_log.txt', f'LOG_FILE={_ud.LOG_FILE!r}'"
1104 env={**os.environ,
"OC_DIAGNOSTICS_LOG_FILE":
"/tmp/test_log.txt"},
1105 capture_output=
True,
1109 assert result.returncode == 0, (
1110 f
"OC_DIAGNOSTICS_LOG_FILE override failed; stderr: {result.stderr.strip()!r}"
1119 """Redaction patterns scrub matching key=value pairs in production mode."""
1120 old_prod = _ud.PROD_MODE
1121 old_patterns = _ud.REDACTION_PATTERNS
1122 _ud.PROD_MODE =
True
1123 _ud.REDACTION_PATTERNS = [
1124 re.compile(
r'(?i)(password)\s*[=:]\s*\S+'),
1125 re.compile(
r'(?i)(api_key)\s*[=:]\s*\S+'),
1128 result = _ud._apply_redaction(
"user login password=secret123 api_key=abc-xyz")
1129 assert "secret123" not in result, f
"password value not redacted: {result!r}"
1130 assert "abc-xyz" not in result, f
"api_key value not redacted: {result!r}"
1131 assert "[REDACTED]" in result, f
"Expected [REDACTED] in output: {result!r}"
1133 _ud.PROD_MODE = old_prod
1134 _ud.REDACTION_PATTERNS = old_patterns
1138 """Redaction patterns are not applied when PROD_MODE is False."""
1139 old_prod = _ud.PROD_MODE
1140 old_patterns = _ud.REDACTION_PATTERNS
1141 _ud.PROD_MODE =
False
1142 _ud.REDACTION_PATTERNS = [
1143 re.compile(
r'(?i)(password)\s*[=:]\s*\S+'),
1146 text =
"password=secret123"
1147 result = _ud._apply_redaction(text)
1148 assert result == text, f
"Dev mode should not redact: {result!r}"
1150 _ud.PROD_MODE = old_prod
1151 _ud.REDACTION_PATTERNS = old_patterns
1155 """Redaction handles double-quoted multi-word values (OI-401)."""
1156 old_prod = _ud.PROD_MODE
1157 old_patterns = _ud.REDACTION_PATTERNS
1158 _ud.PROD_MODE =
True
1159 _ud.REDACTION_PATTERNS = [
1160 re.compile(
r'(?i)(password)\s*[=:]\s*(?:"[^"]*"|\'[^\']*\'|\S+)'),
1163 result = _ud._apply_redaction(
'password: "my secret value"')
1164 assert "my secret value" not in result
1165 assert "[REDACTED]" in result
1167 _ud.PROD_MODE = old_prod
1168 _ud.REDACTION_PATTERNS = old_patterns
1172 """Redaction handles single-quoted multi-word values (OI-401)."""
1173 old_prod = _ud.PROD_MODE
1174 old_patterns = _ud.REDACTION_PATTERNS
1175 _ud.PROD_MODE =
True
1176 _ud.REDACTION_PATTERNS = [
1177 re.compile(
r"(?i)(api_key)\s*[=:]\s*(?:\"[^\"]*\"|'[^']*'|\S+)"),
1180 result = _ud._apply_redaction(
"api_key='my secret key'")
1181 assert "my secret key" not in result
1182 assert "[REDACTED]" in result
1184 _ud.PROD_MODE = old_prod
1185 _ud.REDACTION_PATTERNS = old_patterns
1189 """Original bare-value redaction still works after regex update (OI-401)."""
1190 old_prod = _ud.PROD_MODE
1191 old_patterns = _ud.REDACTION_PATTERNS
1192 _ud.PROD_MODE =
True
1193 _ud.REDACTION_PATTERNS = [
1194 re.compile(
r'(?i)(password)\s*[=:]\s*(?:"[^"]*"|\'[^\']*\'|\S+)'),
1197 result = _ud._apply_redaction(
"password=secret123")
1198 assert "secret123" not in result
1199 assert "[REDACTED]" in result
1201 _ud.PROD_MODE = old_prod
1202 _ud.REDACTION_PATTERNS = old_patterns
1210 """global_debug_level in config overrides all domain levels."""
1212 "DEBUG_LEVELS": {
"DATA": 2,
"UI": 3,
"NET": 1},
1213 "global_settings": {
"global_debug_level": 5},
1217 with tempfile.NamedTemporaryFile(
1218 mode=
"w", suffix=
".json", delete=
False, encoding=
"utf-8"
1220 json.dump(config, f)
1223 env = dict(os.environ)
1224 env[
"OC_DIAGNOSTICS_CONFIG"] = tmp_path
1225 env.pop(
"OC_DIAGNOSTICS_LEVEL",
None)
1226 result = subprocess.run(
1228 sys.executable,
"-c",
1229 "import oc_diagnostics.unified_debug as _ud; "
1230 "assert all(v == 5 for v in _ud.DEBUG_LEVELS.values()), "
1231 "f'Not all levels are 5: {dict(_ud.DEBUG_LEVELS)}'"
1234 capture_output=
True,
1238 assert result.returncode == 0, (
1239 f
"global_debug_level override failed; stderr: {result.stderr.strip()!r}"
1242 if tmp_path
and os.path.exists(tmp_path):
1251 """Bundled debug_config.json uses V2.0 schema with DEBUG_LEVELS key."""
1252 config_path = os.path.join(
1253 os.path.dirname(_ud.__file__),
"debug_config.json"
1255 with open(config_path, encoding=
"utf-8")
as f:
1256 config = json.load(f)
1257 assert config.get(
"_schema_version") ==
"2.0", (
1258 f
"Expected schema version 2.0, got {config.get('_schema_version')!r}"
1260 assert "DEBUG_LEVELS" in config,
"Bundled config must use DEBUG_LEVELS key"
1268 _ud._prod_downgrade_warned.clear()
1272 """In PROD_MODE, _dbg() calls above PROD_MAX_LEVEL are suppressed (OI-407)."""
1274 old_emit = _ud._emit
1275 old_prod = _ud.PROD_MODE
1276 old_max = _ud.PROD_MAX_LEVEL
1277 old_levels = dict(_ud.DEBUG_LEVELS)
1278 _ud._emit =
lambda line, level, record=
None: captured.append((level, line))
1279 _ud.PROD_MODE =
True
1280 _ud.PROD_MAX_LEVEL = 2
1281 _ud.DEBUG_LEVELS[
"GENERAL"] = 9
1284 _ud._dbg(
"GENERAL",
"test_fn",
"verbose L3 detail", level=3)
1286 assert len(captured) == 1
1287 assert "suppressed in production mode" in captured[0][1]
1288 assert "L3" in captured[0][1]
1290 _ud._emit = old_emit
1291 _ud.PROD_MODE = old_prod
1292 _ud.PROD_MAX_LEVEL = old_max
1293 _ud.DEBUG_LEVELS.clear()
1294 _ud.DEBUG_LEVELS.update(old_levels)
1299 """L1/L2 calls in PROD_MODE are still emitted normally."""
1301 old_emit = _ud._emit
1302 old_prod = _ud.PROD_MODE
1303 old_max = _ud.PROD_MAX_LEVEL
1304 old_levels = dict(_ud.DEBUG_LEVELS)
1305 _ud._emit =
lambda line, level, record=
None: captured.append((level, line))
1306 _ud.PROD_MODE =
True
1307 _ud.PROD_MAX_LEVEL = 2
1308 _ud.DEBUG_LEVELS[
"GENERAL"] = 2
1311 _ud._dbg(
"GENERAL",
"test_fn",
"normal L2", level=2)
1312 assert len(captured) == 1
1313 assert "normal L2" in captured[0][1]
1314 assert "suppressed" not in captured[0][1]
1316 _ud._emit = old_emit
1317 _ud.PROD_MODE = old_prod
1318 _ud.PROD_MAX_LEVEL = old_max
1319 _ud.DEBUG_LEVELS.clear()
1320 _ud.DEBUG_LEVELS.update(old_levels)
1325 """Repeated L3 calls in PROD_MODE emit only one warning per (domain, level)."""
1327 old_emit = _ud._emit
1328 old_prod = _ud.PROD_MODE
1329 old_max = _ud.PROD_MAX_LEVEL
1330 old_levels = dict(_ud.DEBUG_LEVELS)
1331 _ud._emit =
lambda line, level, record=
None: captured.append(line)
1332 _ud.PROD_MODE =
True
1333 _ud.PROD_MAX_LEVEL = 2
1334 _ud.DEBUG_LEVELS[
"GENERAL"] = 9
1338 _ud._dbg(
"GENERAL",
"f",
"m", level=3)
1339 warn_lines = [c
for c
in captured
if "suppressed" in c]
1340 assert len(warn_lines) == 1, f
"expected 1 warning, got {len(warn_lines)}"
1342 _ud._emit = old_emit
1343 _ud.PROD_MODE = old_prod
1344 _ud.PROD_MAX_LEVEL = old_max
1345 _ud.DEBUG_LEVELS.clear()
1346 _ud.DEBUG_LEVELS.update(old_levels)
1351 """Setting PROD_MAX_LEVEL=3 allows L3 but still blocks L4."""
1353 old_emit = _ud._emit
1354 old_prod = _ud.PROD_MODE
1355 old_max = _ud.PROD_MAX_LEVEL
1356 old_levels = dict(_ud.DEBUG_LEVELS)
1357 _ud._emit =
lambda line, level, record=
None: captured.append((level, line))
1358 _ud.PROD_MODE =
True
1359 _ud.PROD_MAX_LEVEL = 3
1360 _ud.DEBUG_LEVELS[
"GENERAL"] = 9
1363 _ud._dbg(
"GENERAL",
"f",
"l3 ok", level=3)
1364 _ud._dbg(
"GENERAL",
"f",
"l4 blocked", level=4)
1365 bodies = [c[1]
for c
in captured]
1366 assert any(
"l3 ok" in b
for b
in bodies)
1367 assert not any(
"l4 blocked" in b
for b
in bodies)
1368 assert any(
"suppressed" in b
and "L4" in b
for b
in bodies)
1370 _ud._emit = old_emit
1371 _ud.PROD_MODE = old_prod
1372 _ud.PROD_MAX_LEVEL = old_max
1373 _ud.DEBUG_LEVELS.clear()
1374 _ud.DEBUG_LEVELS.update(old_levels)
1379 """Outside PROD_MODE, PROD_MAX_LEVEL has no effect."""
1381 old_emit = _ud._emit
1382 old_prod = _ud.PROD_MODE
1383 old_max = _ud.PROD_MAX_LEVEL
1384 old_levels = dict(_ud.DEBUG_LEVELS)
1385 _ud._emit =
lambda line, level, record=
None: captured.append((level, line))
1386 _ud.PROD_MODE =
False
1387 _ud.PROD_MAX_LEVEL = 1
1388 _ud.DEBUG_LEVELS[
"GENERAL"] = 9
1391 _ud._dbg(
"GENERAL",
"f",
"l3 allowed in dev", level=3)
1392 bodies = [c[1]
for c
in captured]
1393 assert any(
"l3 allowed in dev" in b
for b
in bodies)
1394 assert not any(
"suppressed" in b
for b
in bodies)
1396 _ud._emit = old_emit
1397 _ud.PROD_MODE = old_prod
1398 _ud.PROD_MAX_LEVEL = old_max
1399 _ud.DEBUG_LEVELS.clear()
1400 _ud.DEBUG_LEVELS.update(old_levels)
test_prod_mode_suppresses_high_level()
test_config_v2_nested_log_rotation()
test_ai_trace_file_written()
test_log_rotation_count()
test_set_debug_level_valid()
test_dbg_assert_prod_mode_noop()
test_ai_trace_disabled_discards()
test_dbg_trace_preserves_name()
test_dbg_assert_raises_on_false()
test_reload_stale_key_removed()
test_debug_levels_dict_mutable()
test_prod_mode_warning_one_shot()
test_env_var_level_override()
test_set_global_setting_valid()
test_redaction_quoted_single()
test_dbg_level_filtering()
test_global_debug_level_override()
test_io_degradation_oserror()
test_prod_mode_via_config()
test_dbg_trace_start_end()
test_dbg_before_init_warning()
test_prod_mode_allows_low_level()
test_dbg_silent_no_exception()
test_bundled_config_schema_version()
test_stream_interceptor_interface()
test_redaction_bare_value_still_works()
test_adbg_does_not_raise()
test_set_global_setting_invalid()
test_prod_max_level_configurable()
test_ai_trace_console_suppressed()
test_config_source_populated()
test_redaction_quoted_double()
test_env_var_log_file_override()
test_config_v2_debug_levels_key()
test_dbg_metrics_output()
test_set_debug_level_invalid()
test_redaction_disabled_in_dev_mode()
test_dbg_trace_exception()
test_non_prod_mode_unaffected()
test_dbg_assert_passes_silently()
test_redaction_in_prod_mode()