Long code lines were clipped at the terminal width, making them unreadable. Pass word_wrap=True to all Syntax() calls so lines wrap instead of being cut off.
328 lines
11 KiB
Python
328 lines
11 KiB
Python
"""Tests for the rendering module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from rich.json import JSON as RichJSON
|
|
from rich.markdown import Markdown
|
|
from rich.padding import Padding
|
|
from rich.syntax import Syntax
|
|
|
|
from rp.pager import MousePager
|
|
from rp.render import RenderOptions, render
|
|
|
|
|
|
class TestRenderOptions:
|
|
"""RenderOptions dataclass defaults."""
|
|
|
|
def test_default_theme(self) -> None:
|
|
opts = RenderOptions()
|
|
assert opts.theme == "monokai"
|
|
|
|
def test_default_line_numbers_is_none(self) -> None:
|
|
opts = RenderOptions()
|
|
assert opts.line_numbers is None
|
|
|
|
def test_default_pager_is_none(self) -> None:
|
|
opts = RenderOptions()
|
|
assert opts.pager is None
|
|
|
|
def test_custom_values(self) -> None:
|
|
opts = RenderOptions(theme="github-dark", line_numbers=True, pager=False)
|
|
assert opts.theme == "github-dark"
|
|
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."""
|
|
|
|
@pytest.mark.parametrize("file_type", ["markdown", "md"])
|
|
def test_renders_markdown_types(
|
|
self,
|
|
capture_console,
|
|
sample_markdown: str,
|
|
file_type: str,
|
|
) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
with patch("rp.render.Markdown", wraps=Markdown) as mock_markdown:
|
|
render(sample_markdown, file_type, capture_console, opts)
|
|
|
|
output = capture_console.file.getvalue()
|
|
assert len(output) > 0
|
|
mock_markdown.assert_called_once_with(sample_markdown)
|
|
|
|
|
|
class TestRenderJson:
|
|
"""JSON rendering."""
|
|
|
|
@pytest.mark.parametrize("content", ['{"a": 1}', "[1, 2, 3]"])
|
|
def test_renders_valid_json(
|
|
self,
|
|
capture_console,
|
|
content: str,
|
|
) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
with patch("rp.render.RichJSON", wraps=RichJSON) as mock_json:
|
|
render(content, "json", capture_console, opts)
|
|
|
|
output = capture_console.file.getvalue()
|
|
assert len(output) > 0
|
|
mock_json.assert_called_once_with(content)
|
|
|
|
def test_invalid_json_falls_back_to_syntax(
|
|
self,
|
|
capture_console,
|
|
) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
|
|
render("{invalid json}", "json", capture_console, opts)
|
|
|
|
output = capture_console.file.getvalue()
|
|
assert len(output) > 0
|
|
mock_syntax.assert_called_once_with(
|
|
"{invalid json}",
|
|
"json",
|
|
theme="monokai",
|
|
line_numbers=False,
|
|
word_wrap=True,
|
|
)
|
|
|
|
|
|
class TestRenderCode:
|
|
"""Code rendering with Syntax."""
|
|
|
|
def test_renders_python(self, capture_console, sample_python: str) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
|
|
render(sample_python, "python", capture_console, opts)
|
|
|
|
output = capture_console.file.getvalue()
|
|
assert len(output) > 0
|
|
mock_syntax.assert_called_once_with(
|
|
sample_python,
|
|
"python",
|
|
theme="monokai",
|
|
line_numbers=True,
|
|
word_wrap=True,
|
|
)
|
|
|
|
def test_renders_unknown_type_as_text_fallback(self, capture_console) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
def syntax_side_effect(*args, **kwargs):
|
|
lexer = args[1]
|
|
if lexer == "xyz_nonexistent_lexer":
|
|
raise Exception("unknown lexer")
|
|
return Syntax(*args, **kwargs)
|
|
|
|
with patch("rp.render.Syntax", side_effect=syntax_side_effect) as mock_syntax:
|
|
render("some content", "xyz_nonexistent_lexer", capture_console, opts)
|
|
|
|
output = capture_console.file.getvalue()
|
|
assert len(output) > 0
|
|
assert mock_syntax.call_args_list[0].args == (
|
|
"some content",
|
|
"xyz_nonexistent_lexer",
|
|
)
|
|
assert mock_syntax.call_args_list[0].kwargs == {
|
|
"theme": "monokai",
|
|
"line_numbers": True,
|
|
"word_wrap": True,
|
|
}
|
|
assert mock_syntax.call_args_list[1].args == ("some content", "text")
|
|
assert mock_syntax.call_args_list[1].kwargs == {
|
|
"theme": "monokai",
|
|
"line_numbers": True,
|
|
"word_wrap": True,
|
|
}
|
|
|
|
def test_renders_rust(self, capture_console) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
|
|
render("fn main() {}", "rust", capture_console, opts)
|
|
|
|
output = capture_console.file.getvalue()
|
|
assert len(output) > 0
|
|
mock_syntax.assert_called_once_with(
|
|
"fn main() {}",
|
|
"rust",
|
|
theme="monokai",
|
|
line_numbers=True,
|
|
word_wrap=True,
|
|
)
|
|
|
|
|
|
class TestCodeWrap:
|
|
"""Code wrapping (word_wrap) for syntax-highlighted output."""
|
|
|
|
def test_python_syntax_has_word_wrap(
|
|
self, capture_console, sample_python: str
|
|
) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
|
|
render(sample_python, "python", capture_console, opts)
|
|
|
|
mock_syntax.assert_called_once()
|
|
assert mock_syntax.call_args.kwargs["word_wrap"] is True
|
|
|
|
def test_long_line_wraps_in_output(self, capture_console) -> None:
|
|
long_line = "x = " + "'a' * " * 200 + "1"
|
|
opts = RenderOptions(pager=False, line_numbers=False)
|
|
render(long_line, "python", capture_console, opts)
|
|
output = capture_console.file.getvalue()
|
|
assert len(output) > 0
|
|
lines = output.split("\n")
|
|
non_empty = [line for line in lines if line.strip()]
|
|
assert len(non_empty) > 1, "Long line should wrap across multiple visual lines"
|
|
|
|
def test_text_fallback_has_word_wrap(self, capture_console) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
def raise_on_bad_lexer(*args, **kwargs):
|
|
if args[1] == "unknown_xyz":
|
|
raise Exception("bad lexer")
|
|
return Syntax(*args, **kwargs)
|
|
|
|
with patch("rp.render.Syntax", side_effect=raise_on_bad_lexer) as mock_syntax:
|
|
render("some text", "unknown_xyz", capture_console, opts)
|
|
|
|
fallback_call = mock_syntax.call_args_list[-1]
|
|
assert fallback_call.args[1] == "text"
|
|
assert fallback_call.kwargs["word_wrap"] is True
|
|
|
|
def test_json_fallback_has_word_wrap(self, capture_console) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
|
|
render("{invalid}", "json", capture_console, opts)
|
|
|
|
mock_syntax.assert_called_once()
|
|
assert mock_syntax.call_args.kwargs["word_wrap"] is True
|
|
|
|
|
|
class TestLineNumbers:
|
|
"""Line number auto-detection logic."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("content", "file_type", "expected"),
|
|
[
|
|
("x = 1", "python", True),
|
|
("# Heading", "markdown", False),
|
|
("{}", "json", False),
|
|
],
|
|
)
|
|
def test_default_line_number_behavior(
|
|
self,
|
|
capture_console,
|
|
content: str,
|
|
file_type: str,
|
|
expected: bool,
|
|
) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
|
|
render(content, file_type, capture_console, opts)
|
|
|
|
if file_type == "python":
|
|
mock_syntax.assert_called_once()
|
|
assert mock_syntax.call_args.kwargs["line_numbers"] is expected
|
|
else:
|
|
assert len(capture_console.file.getvalue()) > 0
|
|
|
|
@pytest.mark.parametrize("line_numbers", [True, False])
|
|
def test_explicit_line_numbers(
|
|
self,
|
|
capture_console,
|
|
sample_python: str,
|
|
line_numbers: bool,
|
|
) -> None:
|
|
opts = RenderOptions(line_numbers=line_numbers, pager=False)
|
|
|
|
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
|
|
render(sample_python, "python", capture_console, opts)
|
|
|
|
mock_syntax.assert_called_once()
|
|
assert mock_syntax.call_args.kwargs["line_numbers"] is line_numbers
|
|
|
|
|
|
class TestPagerBehavior:
|
|
"""Pager auto-detection."""
|
|
|
|
def test_pager_disabled(self, capture_console, sample_python: str) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
with patch.object(capture_console, "pager") as mock_pager:
|
|
render(sample_python, "python", capture_console, opts)
|
|
|
|
assert len(capture_console.file.getvalue()) > 0
|
|
mock_pager.assert_not_called()
|
|
|
|
def test_pager_auto_tty(self, capture_console, sample_python: str) -> None:
|
|
opts = RenderOptions(pager=None)
|
|
mock_context = MagicMock()
|
|
mock_context.__enter__.return_value = None
|
|
mock_context.__exit__.return_value = None
|
|
|
|
with patch("sys.stdout.isatty", return_value=True):
|
|
with patch.object(
|
|
capture_console, "pager", return_value=mock_context
|
|
) as mock_pager:
|
|
render(sample_python, "python", capture_console, opts)
|
|
|
|
mock_pager.assert_called_once()
|
|
assert mock_pager.call_args.kwargs["styles"] is True
|
|
assert isinstance(mock_pager.call_args.kwargs["pager"], MousePager)
|
|
|
|
def test_pager_auto_non_tty(self, capture_console, sample_python: str) -> None:
|
|
opts = RenderOptions(pager=None)
|
|
|
|
with patch("sys.stdout.isatty", return_value=False):
|
|
with patch.object(capture_console, "pager") as mock_pager:
|
|
render(sample_python, "python", capture_console, opts)
|
|
|
|
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."""
|
|
|
|
def test_wraps_renderable_in_padding(
|
|
self,
|
|
capture_console,
|
|
sample_python: str,
|
|
) -> None:
|
|
opts = RenderOptions(pager=False)
|
|
|
|
with patch.object(capture_console, "print") as mock_print:
|
|
render(sample_python, "python", capture_console, opts)
|
|
|
|
printed = mock_print.call_args.args[0]
|
|
assert isinstance(printed, Padding)
|
|
assert printed.expand is True
|