Add opt-in debug logging across the CLI pipeline

Thread RP_DEBUG-driven logging through input, detection, rendering, and pager decisions so CLI behavior is inspectable without polluting stdout. Add focused coverage for debug wiring, env parsing, and stderr-only output.
This commit is contained in:
2026-04-03 19:09:24 -07:00
parent 37502c3083
commit 93c777cf6a
10 changed files with 296 additions and 11 deletions

View File

@@ -75,6 +75,7 @@ class TestFileInput:
content="x = 1\n",
explicit_type=None,
guess_content=False,
debug=None,
)
def test_render_called_with_correct_options(self, tmp_file) -> None:
@@ -182,8 +183,41 @@ class TestStdinInput:
content="hello\n",
explicit_type=None,
guess_content=False,
debug=None,
)
def test_debug_env_enables_debug_logger(self) -> None:
with patch("rp.cli.detect_type") as mock_detect_type:
mock_detect_type.return_value = "text"
with patch("rp.cli.render"):
runner.invoke(
app, ["--no-pager"], input="hello\n", env={"RP_DEBUG": "1"}
)
_, kwargs = mock_detect_type.call_args
assert kwargs["debug"] is not None
def test_debug_output_stays_on_stderr(self, tmp_file) -> None:
file = tmp_file("# Hello\n", suffix=".md")
result = runner.invoke(app, [str(file), "--no-pager"], env={"RP_DEBUG": "1"})
normalized_stderr = result.stderr.replace("\n", "")
assert result.exit_code == 0
assert "[rp debug]" not in result.stdout
assert "Hello" in result.stdout
assert "[rp debug] input: reading file" in result.stderr
assert repr(str(file)) in normalized_stderr
assert (
"[rp debug] type: inferred 'markdown' from extension '.md'" in result.stderr
)
assert (
"[rp debug] render: auto-set line_numbers=False for 'markdown'"
in result.stderr
)
assert "[rp debug] pager: explicitly disabled" in result.stderr
assert "[rp debug] pager: writing directly to the console" in result.stderr
def test_stdin_shows_tty_prompt(self) -> None:
"""When stdin is a TTY and no input is piped, show the reading hint."""
with patch("rp.cli.sys.stdin.isatty", return_value=True):

60
tests/test_debug.py Normal file
View File

@@ -0,0 +1,60 @@
"""Tests for debug helpers."""
from __future__ import annotations
import io
from rich.console import Console
from rp.debug import DEBUG_ENV_VAR, env_debug_enabled, log_debug, make_debug_logger
class TestEnvDebugEnabled:
"""Environment-based debug mode toggles."""
def test_disabled_by_default(self) -> None:
assert env_debug_enabled({}) is False
def test_false_like_values_disable_debug(self) -> None:
for value in ["", "0", "false", "no", "off"]:
assert env_debug_enabled({DEBUG_ENV_VAR: value}) is False
def test_false_like_values_are_trimmed_and_case_insensitive(self) -> None:
for value in [" False ", "\tNO\n", " Off "]:
assert env_debug_enabled({DEBUG_ENV_VAR: value}) is False
def test_truthy_values_enable_debug(self) -> None:
assert env_debug_enabled({DEBUG_ENV_VAR: "1"}) is True
assert env_debug_enabled({DEBUG_ENV_VAR: "true"}) is True
def test_truthy_values_are_trimmed_and_case_insensitive(self) -> None:
for value in [" TRUE ", " Yes ", "\ton\n"]:
assert env_debug_enabled({DEBUG_ENV_VAR: value}) is True
class TestMakeDebugLogger:
"""Console-backed debug logger output."""
def test_logs_visible_prefix(self) -> None:
stream = io.StringIO()
console = Console(file=stream, force_terminal=False)
logger = make_debug_logger(console, enabled=True)
assert logger is not None
logger("hello")
assert stream.getvalue() == "[rp debug] hello\n"
class TestLogDebug:
"""Nil-safe debug logger forwarding."""
def test_noop_when_logger_is_none(self) -> None:
log_debug(None, "hello")
def test_forwards_to_logger(self) -> None:
messages: list[str] = []
log_debug(messages.append, "hello")
assert messages == ["hello"]

View File

@@ -198,6 +198,30 @@ class TestGuessContent:
assert detect_type(content=sample_json, guess_content=True) == "json"
class TestDebugLogging:
"""Debug logging describes inference decisions."""
def test_logs_extension_source(self) -> None:
messages: list[str] = []
assert detect_type(path="script.py", debug=messages.append) == "python"
assert messages == ["type: inferred 'python' from extension '.py'"]
def test_logs_content_guess_and_score(self) -> None:
messages: list[str] = []
diff = "--- a/file\n+++ b/file\n@@ -1 +1 @@\n-old\n+new\n"
assert (
detect_type(content=diff, guess_content=True, debug=messages.append)
== "diff"
)
assert any(
"type: content guess -> lexer='diff', score=" in message
for message in messages
)
assert "type: inferred 'diff' from heuristic content guessing" in messages
class TestFallback:
"""Priority 7: fallback to 'text'."""

View File

@@ -25,6 +25,14 @@ class TestMouseCapableLess:
with patch("rp.pager.shutil.which", return_value=None):
assert _mouse_capable_less() is None
def test_logs_less_not_found(self) -> None:
messages: list[str] = []
with patch("rp.pager.shutil.which", return_value=None):
assert _mouse_capable_less(messages.append) is None
assert messages == ["pager: 'less' was not found"]
def test_returns_none_when_less_too_old(self) -> None:
with patch("rp.pager.shutil.which", return_value="/usr/bin/less"):
mock_result = MagicMock()
@@ -99,6 +107,19 @@ class TestMousePager:
mock_instance.show.assert_called_once_with("content")
def test_logs_system_pager_fallback(self) -> None:
messages: list[str] = []
pager = MousePager(debug=messages.append)
with patch("rp.pager._mouse_capable_less", return_value=None):
with patch("rp.pager.SystemPager") as mock_system_pager:
mock_instance = MagicMock()
mock_system_pager.return_value = mock_instance
pager.show("content")
assert messages == ["pager: falling back to Rich's SystemPager"]
def test_uses_less_when_available(self) -> None:
pager = MousePager()
mock_result = MagicMock()

View File

@@ -35,6 +35,10 @@ class TestRenderOptions:
assert opts.line_numbers is True
assert opts.pager is False
def test_default_debug_is_none(self) -> None:
opts = RenderOptions()
assert opts.debug is None
class TestRenderMarkdown:
"""Markdown rendering."""
@@ -238,6 +242,18 @@ class TestPagerBehavior:
assert len(capture_console.file.getvalue()) > 0
mock_pager.assert_not_called()
def test_logs_auto_pager_decision(
self, capture_console, sample_python: str
) -> None:
messages: list[str] = []
opts = RenderOptions(pager=None, debug=messages.append)
with patch("sys.stdout.isatty", return_value=False):
render(sample_python, "python", capture_console, opts)
assert "pager: auto-detected disabled because stdout is not a TTY" in messages
assert "pager: writing directly to the console" in messages
class TestPadding:
"""Rendered output is wrapped in padding."""