"""Tests for the CLI entry point.""" from __future__ import annotations from pathlib import Path from unittest.mock import patch from typer.testing import CliRunner from rp import __version__ from rp.cli import app, main runner = CliRunner() class TestVersionFlag: """Tests for the version flags.""" def test_version_long_flag(self) -> None: result = runner.invoke(app, ["--version"]) assert result.exit_code == 0 assert __version__ in result.output def test_version_short_flag(self) -> None: result = runner.invoke(app, ["-v"]) assert result.exit_code == 0 assert __version__ in result.output def test_version_output_format(self) -> None: result = runner.invoke(app, ["--version"]) assert result.exit_code == 0 assert "Rich Print (rp)" in result.output assert "License: MIT" in result.output class TestFileInput: """Tests for reading from a file argument.""" def test_render_python_file(self, tmp_file) -> None: file = tmp_file("def hello(): pass\n", suffix=".py") result = runner.invoke(app, [str(file), "--no-pager"]) assert result.exit_code == 0 assert result.output def test_render_markdown_file(self, tmp_file) -> None: file = tmp_file("# Hello\n\nWorld\n", suffix=".md") result = runner.invoke(app, [str(file), "--no-pager"]) assert result.exit_code == 0 def test_render_json_file(self, tmp_file, sample_json: str) -> None: file = tmp_file(sample_json, suffix=".json") result = runner.invoke(app, [str(file), "--no-pager"]) assert result.exit_code == 0 def test_detect_type_called_with_file_path(self, tmp_file) -> None: """detect_type receives the file path string.""" file = tmp_file("x = 1\n", suffix=".py") with patch("rp.cli.detect_type") as mock_detect_type: mock_detect_type.return_value = "python" with patch("rp.cli.render"): runner.invoke(app, [str(file), "--no-pager"]) mock_detect_type.assert_called_once_with( path=str(file), content="x = 1\n", explicit_type=None, guess_content=False, debug=None, ) def test_render_called_with_correct_options(self, tmp_file) -> None: """Render receives the right theme, line_numbers, pager flags.""" file = tmp_file("x = 1\n", suffix=".py") with patch("rp.cli.detect_type", return_value="python"): with patch("rp.cli.render") as mock_render: runner.invoke( app, [ str(file), "--no-pager", "--theme", "github-dark", "--line-numbers", ], ) call_args = mock_render.call_args assert call_args is not None assert call_args[0][0] == "x = 1\n" assert call_args[0][1] == "python" options = call_args[0][3] assert options.theme == "github-dark" assert options.line_numbers is True assert options.pager is False def test_file_not_found(self) -> None: result = runner.invoke(app, ["/nonexistent/file.py"]) assert result.exit_code == 1 assert "Error" in result.output def test_directory_error(self, tmp_path: Path) -> None: result = runner.invoke(app, [str(tmp_path)]) assert result.exit_code == 1 assert "directory" in result.output.lower() or "Error" in result.output def test_empty_file(self, tmp_file) -> None: file = tmp_file("", suffix=".txt") result = runner.invoke(app, [str(file)]) assert result.exit_code == 0 assert "Empty" in result.output or "empty" in result.output.lower() def test_binary_file_error(self, tmp_file) -> None: file = tmp_file("placeholder", suffix=".bin") with patch.object( Path, "read_text", side_effect=UnicodeDecodeError("utf-8", b"x", 0, 1, "boom"), ): result = runner.invoke(app, [str(file)]) assert result.exit_code == 1 assert "Cannot read binary file" in result.output def test_os_error_when_reading_file(self, tmp_file) -> None: file = tmp_file("placeholder", suffix=".txt") with patch.object(Path, "read_text", side_effect=OSError("read failed")): result = runner.invoke(app, [str(file)]) assert result.exit_code == 1 assert "read failed" in result.output class TestStdinInput: """Tests for reading from stdin.""" def test_stdin_python(self) -> None: result = runner.invoke(app, ["--no-pager"], input="x = 1\n") assert result.exit_code == 0 def test_stdin_json(self, sample_json: str) -> None: result = runner.invoke(app, ["--no-pager"], input=sample_json) assert result.exit_code == 0 def test_stdin_empty(self) -> None: result = runner.invoke(app, [], input="") assert result.exit_code == 0 assert "Empty" in result.output or "empty" in result.output.lower() def test_stdin_with_dash(self) -> None: result = runner.invoke(app, ["-", "--no-pager"], input="hello\n") assert result.exit_code == 0 def test_stdin_passes_none_path(self) -> None: """When reading from stdin, detect_type should receive path=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") mock_detect_type.assert_called_once_with( path=None, 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): with patch("rp.cli.sys.stdin.read", return_value="hello\n"): with patch("rp.cli.render"): with patch("rp.cli.Console.print") as mock_print: main(pager=False) mock_print.assert_any_call( "[dim]Reading from stdin. Press Ctrl+D to end.[/dim]" ) class TestTypeOverride: """Tests for the type override flags.""" def test_explicit_type(self, tmp_file) -> None: file = tmp_file('{"key": "value"}\n', suffix=".txt") result = runner.invoke(app, [str(file), "--type", "json", "--no-pager"]) assert result.exit_code == 0 def test_explicit_type_is_forwarded(self, tmp_file) -> None: """The --type flag value is passed as explicit_type to detect_type.""" file = tmp_file('{"key": "value"}\n', suffix=".txt") with patch("rp.cli.detect_type") as mock_detect_type: mock_detect_type.return_value = "json" with patch("rp.cli.render"): runner.invoke(app, [str(file), "--type", "json", "--no-pager"]) _, kwargs = mock_detect_type.call_args assert kwargs["explicit_type"] == "json" assert kwargs["guess_content"] is False def test_explicit_type_short_flag(self, tmp_file) -> None: file = tmp_file("def foo(): pass\n", suffix=".txt") result = runner.invoke(app, [str(file), "-t", "python", "--no-pager"]) assert result.exit_code == 0 class TestThemeOption: """Tests for the theme option.""" def test_custom_theme(self, tmp_file) -> None: file = tmp_file("x = 1\n", suffix=".py") result = runner.invoke(app, [str(file), "--theme", "github-dark", "--no-pager"]) assert result.exit_code == 0 class TestGuessContentOption: """Tests for the guess-content flag.""" def test_guess_content_is_forwarded(self) -> None: with patch("rp.cli.detect_type") as mock_detect_type: mock_detect_type.return_value = "diff" with patch("rp.cli.render"): runner.invoke(app, ["--guess-content", "--no-pager"], input="hello\n") _, kwargs = mock_detect_type.call_args assert kwargs["guess_content"] is True def test_guess_content_disabled_by_default(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") _, kwargs = mock_detect_type.call_args assert kwargs["guess_content"] is False class TestLineNumbersOption: """Tests for the line number flags.""" def test_line_numbers_enabled(self, tmp_file) -> None: file = tmp_file("x = 1\n", suffix=".py") result = runner.invoke(app, [str(file), "--line-numbers", "--no-pager"]) assert result.exit_code == 0 def test_line_numbers_disabled(self, tmp_file) -> None: file = tmp_file("x = 1\n", suffix=".py") result = runner.invoke(app, [str(file), "--no-line-numbers", "--no-pager"]) assert result.exit_code == 0