Coverage for tests/unit/test_dbg.py: 99%
287 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-28 17:24 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-28 17:24 +0000
1import inspect
2import tempfile
3from pathlib import Path
4import importlib
5from typing import Any, Callable, Optional, List, Tuple
6import re
8import pytest
10from muutils.dbg import (
11 dbg,
12 _NoExpPassed,
13 _process_path,
14 _CWD,
15 # we do use this as a global in `test_dbg_counter_increments`
16 _COUNTER, # noqa: F401
17 grep_repr,
18 _normalize_for_loose,
19 _compile_pattern,
20)
23DBG_MODULE_NAME: str = "muutils.dbg"
25# ============================================================================
26# Dummy Tensor classes for testing tensor_info* functions
27# ============================================================================
30class DummyTensor:
31 """A dummy tensor whose sum is NaN."""
33 shape: Tuple[int, ...] = (2, 3)
34 dtype: str = "float32"
35 device: str = "cpu"
36 requires_grad: bool = False
38 def sum(self) -> float:
39 return float("nan")
42class DummyTensorNormal:
43 """A dummy tensor with a normal sum."""
45 shape: Tuple[int, ...] = (4, 5)
46 dtype: str = "int32"
47 device: str = "cuda"
48 requires_grad: bool = True
50 def sum(self) -> float:
51 return 20.0
54class DummyTensorPartial:
55 """A dummy tensor with only a shape attribute."""
57 shape: Tuple[int, ...] = (7,)
60# ============================================================================
61# Additional Tests for dbg and tensor_info functionality
62# ============================================================================
65# --- Tests for _process_path (existing ones) ---
66def test_process_path_absolute(monkeypatch: pytest.MonkeyPatch) -> None:
67 monkeypatch.setattr(
68 importlib.import_module(DBG_MODULE_NAME), "PATH_MODE", "absolute"
69 )
70 test_path: Path = Path("somefile.txt")
71 expected: str = test_path.absolute().as_posix()
72 result: str = _process_path(test_path)
73 assert result == expected
76def test_process_path_relative_inside_common(monkeypatch: pytest.MonkeyPatch) -> None:
77 monkeypatch.setattr(
78 importlib.import_module(DBG_MODULE_NAME), "PATH_MODE", "relative"
79 )
80 test_path: Path = _CWD / "file.txt"
81 expected: str = "file.txt"
82 result: str = _process_path(test_path)
83 assert result == expected
86def test_process_path_relative_outside_common(monkeypatch: pytest.MonkeyPatch) -> None:
87 monkeypatch.setattr(
88 importlib.import_module(DBG_MODULE_NAME), "PATH_MODE", "relative"
89 )
90 with tempfile.TemporaryDirectory() as tmp_dir:
91 test_path: Path = Path(tmp_dir) / "file.txt"
92 expected: str = test_path.absolute().as_posix()
93 result: str = _process_path(test_path)
94 assert result == expected
97def test_process_path_invalid_mode(monkeypatch: pytest.MonkeyPatch) -> None:
98 monkeypatch.setattr(
99 importlib.import_module(DBG_MODULE_NAME), "PATH_MODE", "invalid"
100 )
101 with pytest.raises(
102 ValueError, match="PATH_MODE must be either 'relative' or 'absolute"
103 ):
104 _process_path(Path("anything.txt"))
107# --- Tests for dbg ---
108def test_dbg_with_expression(capsys: pytest.CaptureFixture) -> None:
109 result: int = dbg(1 + 2)
110 captured: str = capsys.readouterr().err
111 assert "= 3" in captured
112 # check that the printed string includes some form of "1+2"
113 assert "1+2" in captured.replace(" ", "") or "1 + 2" in captured
114 assert result == 3
117def test_dbg_without_expression(
118 monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
119) -> None:
120 monkeypatch.setattr(importlib.import_module(DBG_MODULE_NAME), "_COUNTER", 0)
121 result: Any = dbg()
122 captured: str = capsys.readouterr().err.strip()
123 assert "<dbg 0>" in captured
124 no_exp_passed: Any = _NoExpPassed
125 assert result is no_exp_passed
128def test_dbg_custom_formatter(capsys: pytest.CaptureFixture) -> None:
129 custom_formatter: Callable[[Any], str] = lambda x: "custom" # noqa: E731
130 result: str = dbg("anything", formatter=custom_formatter)
131 captured: str = capsys.readouterr().err
132 assert "custom" in captured
133 assert result == "anything"
136def test_dbg_complex_expression(capsys: pytest.CaptureFixture) -> None:
137 # Test a complex expression (lambda invocation)
138 result: int = dbg((lambda x: x * x)(5))
139 captured: str = capsys.readouterr().err
140 assert (
141 "lambda" in captured
142 ) # expecting the extracted code snippet to include 'lambda'
143 assert "25" in captured # evaluated result is 25
144 assert result == 25
147def test_dbg_multiline_code_context(
148 monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
149) -> None:
150 # Create a fake stack with two frames; the first frame does not contain "dbg",
151 # but the second does.
152 class FakeFrame:
153 def __init__(
154 self, code_context: Optional[List[str]], filename: str, lineno: int
155 ) -> None:
156 self.code_context = code_context
157 self.filename = filename
158 self.lineno = lineno
160 def fake_inspect_stack() -> List[Any]:
161 return [
162 FakeFrame(["not line"], "frame1.py", 20),
163 FakeFrame(["dbg(2+2)", "ignored line"], "frame2.py", 30),
164 ]
166 monkeypatch.setattr(inspect, "stack", fake_inspect_stack)
167 result: int = dbg(2 + 2)
168 captured: str = capsys.readouterr().err
169 print(captured)
170 assert "2+2" in captured
171 assert "4" in captured
172 assert result == 4
175def test_dbg_counter_increments(capsys: pytest.CaptureFixture) -> None:
176 global _COUNTER
177 _COUNTER = 0
178 dbg()
179 out1: str = capsys.readouterr().err
180 dbg()
181 out2: str = capsys.readouterr().err
182 assert "<dbg 0>" in out1
183 assert "<dbg 1>" in out2
186def test_dbg_formatter_exception() -> None:
187 def bad_formatter(x: Any) -> str:
188 raise ValueError("formatter error")
190 with pytest.raises(ValueError, match="formatter error"):
191 dbg(123, formatter=bad_formatter)
194def test_dbg_incomplete_expression(
195 monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
196) -> None:
197 # Simulate a frame with an incomplete expression (no closing parenthesis)
198 class FakeFrame:
199 def __init__(
200 self, code_context: Optional[List[str]], filename: str, lineno: int
201 ) -> None:
202 self.code_context = code_context
203 self.filename = filename
204 self.lineno = lineno
206 def fake_inspect_stack() -> List[Any]:
207 return [FakeFrame(["dbg(42"], "fake_incomplete.py", 100)]
209 monkeypatch.setattr(inspect, "stack", fake_inspect_stack)
210 result: int = dbg(42)
211 captured: str = capsys.readouterr().err
212 # The extracted expression should be "42" (since there's no closing parenthesis)
213 assert "42" in captured
214 assert result == 42
217def test_dbg_non_callable_formatter() -> None:
218 with pytest.raises(TypeError):
219 dbg(42, formatter="not callable") # type: ignore
222# # --- Tests for tensor_info_dict and tensor_info ---
223# def test_tensor_info_dict_with_nan() -> None:
224# tensor: DummyTensor = DummyTensor()
225# info: Dict[str, str] = tensor_info_dict(tensor)
226# expected: Dict[str, str] = {
227# "shape": repr((2, 3)),
228# "sum": repr(float("nan")),
229# "dtype": repr("float32"),
230# "device": repr("cpu"),
231# "requires_grad": repr(False),
232# }
233# assert info == expected
236# def test_tensor_info_dict_normal() -> None:
237# tensor: DummyTensorNormal = DummyTensorNormal()
238# info: Dict[str, str] = tensor_info_dict(tensor)
239# expected: Dict[str, str] = {
240# "shape": repr((4, 5)),
241# "dtype": repr("int32"),
242# "device": repr("cuda"),
243# "requires_grad": repr(True),
244# }
245# assert info == expected
248# def test_tensor_info_dict_partial() -> None:
249# tensor: DummyTensorPartial = DummyTensorPartial()
250# info: Dict[str, str] = tensor_info_dict(tensor)
251# expected: Dict[str, str] = {"shape": repr((7,))}
252# assert info == expected
255# def test_tensor_info() -> None:
256# tensor: DummyTensorNormal = DummyTensorNormal()
257# info_str: str = tensor_info(tensor)
258# expected: str = ", ".join(
259# [
260# f"shape={repr((4, 5))}",
261# f"dtype={repr('int32')}",
262# f"device={repr('cuda')}",
263# f"requires_grad={repr(True)}",
264# ]
265# )
266# assert info_str == expected
269# def test_tensor_info_dict_no_attributes() -> None:
270# class DummyEmpty:
271# pass
273# dummy = DummyEmpty()
274# info: Dict[str, str] = tensor_info_dict(dummy)
275# assert info == {}
278# def test_tensor_info_no_attributes() -> None:
279# class DummyEmpty:
280# pass
282# dummy = DummyEmpty()
283# info_str: str = tensor_info(dummy)
284# assert info_str == ""
287# def test_dbg_tensor(capsys: pytest.CaptureFixture) -> None:
288# tensor: DummyTensorPartial = DummyTensorPartial()
289# result: DummyTensorPartial = dbg_tensor(tensor) # type: ignore
290# captured: str = capsys.readouterr().err
291# assert "shape=(7,)" in captured
292# assert result is tensor
295# ============================================================================
296# Tests for grep_repr functionality
297# ============================================================================
300def test_normalize_for_loose() -> None:
301 assert _normalize_for_loose("hello_world") == "hello world"
302 assert _normalize_for_loose("doThing") == "doThing" # camelCase preserved
303 assert _normalize_for_loose("DO-THING") == "DO THING"
304 assert _normalize_for_loose("a__b__c") == "a b c"
305 assert _normalize_for_loose("test.method()") == "test method"
306 assert _normalize_for_loose(" spaces ") == "spaces"
309def test_compile_pattern_case_sensitivity() -> None:
310 # String patterns default to case-insensitive
311 pattern = _compile_pattern("hello")
312 assert pattern.match("HELLO") is not None
313 assert pattern.match("Hello") is not None
315 # With cased=True, string patterns are case-sensitive
316 pattern_cased = _compile_pattern("hello", cased=True)
317 assert pattern_cased.match("HELLO") is None
318 assert pattern_cased.match("hello") is not None
321def test_compile_pattern_loose() -> None:
322 pattern = _compile_pattern("hello world", loose=True)
323 # Pattern should be normalized
324 assert pattern.pattern == "hello world"
327def test_grep_repr_basic_match(capsys: pytest.CaptureFixture) -> None:
328 test_list = [1, 2, 42, 3, 4]
329 grep_repr(test_list, "42")
330 captured = capsys.readouterr().out
331 assert "42" in captured
332 assert captured.count("42") >= 1
335def test_grep_repr_no_match(capsys: pytest.CaptureFixture) -> None:
336 test_list = [1, 2, 3, 4]
337 grep_repr(test_list, "999")
338 captured = capsys.readouterr().out
339 assert captured.strip() == ""
342def test_grep_repr_case_insensitive_default(capsys: pytest.CaptureFixture) -> None:
343 test_dict = {"Hello": "World"}
344 grep_repr(test_dict, "hello") # Should match "Hello" by default
345 captured = capsys.readouterr().out
346 assert "Hello" in captured
349def test_grep_repr_case_sensitive(capsys: pytest.CaptureFixture) -> None:
350 test_dict = {"Hello": "World"}
351 grep_repr(test_dict, "hello", cased=True) # Should NOT match
352 captured = capsys.readouterr().out
353 assert captured.strip() == ""
356def test_grep_repr_loose_matching(capsys: pytest.CaptureFixture) -> None:
357 class TestObj:
358 def __repr__(self) -> str:
359 return "method_name(arg_value)"
361 obj = TestObj()
362 grep_repr(obj, "method name", loose=True)
363 captured = capsys.readouterr().out
364 # With loose=True, both pattern and text are normalized, so we should see "method name" highlighted
365 assert "method name" in captured
368def test_grep_repr_char_context(capsys: pytest.CaptureFixture) -> None:
369 test_string = "prefix_42_suffix"
370 grep_repr(test_string, "42", char_context=3)
371 captured = capsys.readouterr().out
372 # Should show 3 chars before and after: "ix_42_su"
373 assert "ix_" in captured and "_su" in captured
376def test_grep_repr_char_context_zero(capsys: pytest.CaptureFixture) -> None:
377 test_string = "prefix_42_suffix"
378 grep_repr(test_string, "42", char_context=0)
379 captured = capsys.readouterr().out
380 # Should only show the match itself
381 lines = captured.strip().split("\n")
382 assert len(lines) == 1
383 assert "42" in lines[0]
386def test_grep_repr_line_context(capsys: pytest.CaptureFixture) -> None:
387 test_obj = {"line1": 1, "line2": 2, "target": 42, "line4": 4}
388 grep_repr(test_obj, "42", line_context=1)
389 captured = capsys.readouterr().out
390 assert "42" in captured
393def test_grep_repr_before_after_context(capsys: pytest.CaptureFixture) -> None:
394 class MultilineRepr:
395 def __repr__(self) -> str:
396 return "line1\nline2\ntarget_line\nline4\nline5"
398 multiline_obj = MultilineRepr()
399 grep_repr(multiline_obj, "target", before_context=1, after_context=1)
400 captured = capsys.readouterr().out
401 assert "line2" in captured
402 assert "target" in captured
403 assert "line4" in captured
406def test_grep_repr_context_shortcut(capsys: pytest.CaptureFixture) -> None:
407 class MultilineRepr:
408 def __repr__(self) -> str:
409 return "line1\nline2\ntarget_line\nline4\nline5"
411 multiline_obj = MultilineRepr()
412 grep_repr(multiline_obj, "target", context=1)
413 captured = capsys.readouterr().out
414 assert "line2" in captured
415 assert "target" in captured
416 assert "line4" in captured
419def test_grep_repr_max_count(capsys: pytest.CaptureFixture) -> None:
420 test_list = [42, 42, 42, 42, 42] # 5 matches in repr
421 grep_repr(
422 test_list, "42", max_count=2, char_context=0
423 ) # No context to avoid duplicates
424 captured = capsys.readouterr().out
425 # Should only show 2 match blocks due to max_count=2
426 lines = [line for line in captured.strip().split("\n") if line and line != "--"]
427 assert len(lines) == 2
430def test_grep_repr_line_numbers(capsys: pytest.CaptureFixture) -> None:
431 # Create an object whose repr contains actual newlines
432 class MultilineRepr:
433 def __repr__(self) -> str:
434 return "line1\nline2\ntarget_line\nline4"
436 multiline_obj = MultilineRepr()
437 grep_repr(multiline_obj, "target", line_context=1, line_numbers=True)
438 captured = capsys.readouterr().out
439 assert "3:" in captured # Line number for target line
440 assert "2:" in captured # Line number for context
441 assert "4:" in captured # Line number for context
444def test_grep_repr_no_highlight(capsys: pytest.CaptureFixture) -> None:
445 test_list = [1, 2, 42, 3]
446 grep_repr(test_list, "42", highlight=False)
447 captured = capsys.readouterr().out
448 # Should not contain ANSI escape sequences
449 assert "\033[" not in captured
450 assert "42" in captured
453def test_grep_repr_custom_color(capsys: pytest.CaptureFixture) -> None:
454 test_list = [1, 2, 42, 3]
455 grep_repr(test_list, "42", color="32") # Green instead of red
456 captured = capsys.readouterr().out
457 assert "\033[1;32m" in captured # Green color code
460def test_grep_repr_custom_separator(capsys: pytest.CaptureFixture) -> None:
461 test_list = [42, 99, 42] # Multiple matches
462 grep_repr(test_list, r"\d+", separator="---")
463 captured = capsys.readouterr().out
464 assert "---" in captured
467def test_grep_repr_quiet_mode() -> None:
468 test_list = [1, 2, 42, 3]
469 result = grep_repr(test_list, "42", quiet=True)
470 assert result is not None
471 assert isinstance(result, list)
472 assert len(result) >= 1
473 assert any("42" in line for line in result)
476def test_grep_repr_multiple_matches(capsys: pytest.CaptureFixture) -> None:
477 test_dict = {"key1": 42, "key2": 24, "key3": 42}
478 grep_repr(test_dict, "42")
479 captured = capsys.readouterr().out
480 # Should show multiple matches
481 assert captured.count("42") >= 2
484def test_grep_repr_regex_pattern(capsys: pytest.CaptureFixture) -> None:
485 test_list = [1, 22, 333, 4444]
486 grep_repr(test_list, r"\d{3}") # Exactly 3 digits
487 captured = capsys.readouterr().out
488 assert "333" in captured
489 # Note: "4444" contains "444" which matches \d{3}, so it will be highlighted
490 assert "444" in captured
491 # The whole list repr is shown, so "22" will be in the output but not highlighted
492 # Check that 333 and 444 are highlighted (contain ANSI codes) but 22 is not
493 assert "\033[1;31m333\033[0m" in captured
494 assert "\033[1;31m444\033[0m" in captured
497def test_grep_repr_compiled_regex(capsys: pytest.CaptureFixture) -> None:
498 import re
500 test_string = "Hello World"
501 pattern = re.compile(r"hello", re.IGNORECASE)
502 grep_repr(test_string, pattern)
503 captured = capsys.readouterr().out
504 assert "Hello" in captured
507def test_grep_repr_empty_pattern(capsys: pytest.CaptureFixture) -> None:
508 test_list = [1, 2, 3]
509 # Empty pattern matches everything, should not raise an error but show all content
510 grep_repr(test_list, "", char_context=0)
511 captured = capsys.readouterr().out
512 # Empty pattern should match at every position, but with char_context=0 should show minimal output
513 assert len(captured.strip()) > 0
516def test_grep_repr_invalid_regex() -> None:
517 test_list = [1, 2, 3]
518 with pytest.raises(re.error):
519 grep_repr(test_list, "[invalid")
522def test_grep_repr_large_object() -> None:
523 large_dict = {f"key_{i}": i for i in range(1000)}
524 large_dict["special_key"] = 42
526 # Should handle large objects without issues
527 result = grep_repr(large_dict, "special_key", quiet=True)
528 assert result is not None
529 assert any("special_key" in line for line in result)
532def test_grep_repr_nested_objects(capsys: pytest.CaptureFixture) -> None:
533 nested = {"outer": {"inner": {"value": 42}}}
534 grep_repr(nested, "42")
535 captured = capsys.readouterr().out
536 assert "42" in captured
539def test_grep_repr_custom_objects(capsys: pytest.CaptureFixture) -> None:
540 class CustomObject:
541 def __repr__(self) -> str:
542 return "CustomObject(special_value=123)"
544 obj = CustomObject()
545 grep_repr(obj, "special_value")
546 captured = capsys.readouterr().out
547 assert "special_value" in captured