From e1a69fd0c568e3becb73855a4136cc90f04355f7 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Mon, 8 Jun 2026 05:44:56 -0400 Subject: [PATCH] 100% test coverage for output.py --- changelog.md | 1 + test/pytests/test_output.py | 196 ++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) diff --git a/changelog.md b/changelog.md index 738aeb29..e3903832 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ Internal -------- * Improve test coverage for `completion_refresher.py`. * Add test coverage for `client_query.py`. +* Improve test coverage for `output.py`. 1.74.0 (2026/06/06) diff --git a/test/pytests/test_output.py b/test/pytests/test_output.py index 47f7e0f5..2cfa49f9 100644 --- a/test/pytests/test_output.py +++ b/test/pytests/test_output.py @@ -2,6 +2,7 @@ import itertools import shutil +from types import SimpleNamespace from typing import Any, cast import click @@ -93,6 +94,24 @@ def test_get_output_margin_renders_prompt_once_and_counts_status_lines(monkeypat assert cli.prompt_lines == 2 +def test_get_output_margin_uses_prompt_session_render_counter(monkeypatch: pytest.MonkeyPatch) -> None: + cli = make_bare_mycli() + cli.prompt_lines = 0 + cli.prompt_session = cast(Any, SimpleNamespace(app=SimpleNamespace(render_counter=9))) + cli.get_reserved_space = lambda: 1 # type: ignore[assignment] + render_counters: list[int] = [] + + def render_prompt_string(_cli: Any, _prompt_format: str, render_counter: int) -> FormattedText: + render_counters.append(render_counter) + return FormattedText([('', 'prompt')]) + + monkeypatch.setattr(output_module.repl_mode, 'render_prompt_string', render_prompt_string) + monkeypatch.setattr(output_module.special, 'is_timing_enabled', lambda: False) + + assert OutputMixin.get_output_margin(cli) == 2 + assert render_counters == [9] + + def test_output_writes_lines_sinks_and_status(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() cli.prompt_session = None @@ -124,6 +143,84 @@ def test_output_writes_lines_sinks_and_status(monkeypatch: pytest.MonkeyPatch) - assert list(printed_status[0])[0][0].strip() == 'class:output.status' +def test_output_uses_prompt_session_size(monkeypatch: pytest.MonkeyPatch) -> None: + cli = make_bare_mycli() + cli.prompt_session = cast( + Any, + SimpleNamespace(output=SimpleNamespace(get_size=lambda: SimpleNamespace(columns=80, rows=24))), + ) + cli.explicit_pager = False + cli.log_output = lambda value: None # type: ignore[assignment] + cli.get_output_margin = lambda status=None: 1 # type: ignore[assignment] + printed_lines: list[str] = [] + monkeypatch.setattr(output_module.special, 'write_tee', lambda value: None) + monkeypatch.setattr(output_module.special, 'write_once', lambda value: None) + monkeypatch.setattr(output_module.special, 'write_pipe_once', lambda value: None) + monkeypatch.setattr(output_module.special, 'is_redirected', lambda: False) + monkeypatch.setattr(output_module.special, 'is_pager_enabled', lambda: False) + monkeypatch.setattr(click, 'secho', lambda value, **_kwargs: printed_lines.append(value)) + + OutputMixin.output(cli, itertools.chain(['row']), SQLResult()) + + assert printed_lines == ['row'] + + +def test_output_flushes_buffer_when_content_does_not_fit(monkeypatch: pytest.MonkeyPatch) -> None: + cli = make_bare_mycli() + cli.prompt_session = None + cli.explicit_pager = False + cli.log_output = lambda value: None # type: ignore[assignment] + cli.get_output_margin = lambda status=None: output_module.DEFAULT_HEIGHT # type: ignore[assignment] + printed_lines: list[str] = [] + monkeypatch.setattr(output_module.special, 'write_tee', lambda value: None) + monkeypatch.setattr(output_module.special, 'write_once', lambda value: None) + monkeypatch.setattr(output_module.special, 'write_pipe_once', lambda value: None) + monkeypatch.setattr(output_module.special, 'is_redirected', lambda: False) + monkeypatch.setattr(output_module.special, 'is_pager_enabled', lambda: False) + monkeypatch.setattr(click, 'secho', lambda value, **_kwargs: printed_lines.append(value)) + + OutputMixin.output(cli, itertools.chain(['row 1', 'row 2']), SQLResult()) + + assert printed_lines == ['row 1', 'row 2'] + + +def test_output_switches_to_pager_when_content_does_not_fit(monkeypatch: pytest.MonkeyPatch) -> None: + cli = make_bare_mycli() + cli.prompt_session = None + cli.explicit_pager = False + cli.log_output = lambda value: None # type: ignore[assignment] + cli.get_output_margin = lambda status=None: output_module.DEFAULT_HEIGHT # type: ignore[assignment] + paged_lines: list[str] = [] + monkeypatch.setattr(output_module.special, 'write_tee', lambda value: None) + monkeypatch.setattr(output_module.special, 'write_once', lambda value: None) + monkeypatch.setattr(output_module.special, 'write_pipe_once', lambda value: None) + monkeypatch.setattr(output_module.special, 'is_redirected', lambda: False) + monkeypatch.setattr(output_module.special, 'is_pager_enabled', lambda: True) + monkeypatch.setattr(click, 'echo_via_pager', lambda values: paged_lines.extend(list(values))) + + OutputMixin.output(cli, itertools.chain(['row']), SQLResult()) + + assert paged_lines == ['row\n'] + + +def test_output_redirected_skips_screen_printing(monkeypatch: pytest.MonkeyPatch) -> None: + cli = make_bare_mycli() + cli.prompt_session = None + cli.log_output = lambda value: None # type: ignore[assignment] + cli.get_output_margin = lambda status=None: 1 # type: ignore[assignment] + printed_lines: list[str] = [] + monkeypatch.setattr(output_module.special, 'write_tee', lambda value: None) + monkeypatch.setattr(output_module.special, 'write_once', lambda value: None) + monkeypatch.setattr(output_module.special, 'write_pipe_once', lambda value: None) + monkeypatch.setattr(output_module.special, 'is_redirected', lambda: True) + monkeypatch.setattr(output_module.special, 'is_pager_enabled', lambda: False) + monkeypatch.setattr(click, 'secho', lambda value, **_kwargs: printed_lines.append(value)) + + OutputMixin.output(cli, itertools.chain(['row']), SQLResult()) + + assert printed_lines == [] + + def test_output_uses_warning_status_style(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() cli.log_output = lambda value: None # type: ignore[assignment] @@ -136,6 +233,19 @@ def test_output_uses_warning_status_style(monkeypatch: pytest.MonkeyPatch) -> No assert list(printed_status[0])[0][0].strip() == 'class:warnings.status' +def test_output_preserves_formatted_status(monkeypatch: pytest.MonkeyPatch) -> None: + cli = make_bare_mycli() + cli.log_output = lambda value: None # type: ignore[assignment] + cli.get_output_margin = lambda status=None: 1 # type: ignore[assignment] + printed_status: list[Any] = [] + monkeypatch.setattr(prompt_toolkit, 'print_formatted_text', lambda text, style=None: printed_status.append(text)) + status = FormattedText([('class:custom', 'formatted')]) + + OutputMixin.output(cli, itertools.chain([]), SQLResult(status=status)) + + assert to_plain_text(printed_status[0]) == 'formatted' + + def test_output_sends_buffer_to_pager_when_pager_is_explicit(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() cli.prompt_session = None @@ -156,6 +266,22 @@ def test_output_sends_buffer_to_pager_when_pager_is_explicit(monkeypatch: pytest assert paged_lines == ['row 1\n', 'row 2\n'] +def test_configure_pager_uses_more_for_missing_less_on_windows(monkeypatch: pytest.MonkeyPatch) -> None: + cli = make_bare_mycli() + cli.my_cnf = ConfigObj({'client': {'pager': 'less'}}) + cli.config = ConfigObj({'main': {'pager': '', 'enable_pager': 'True'}}) + cli.read_my_cnf = lambda cnf, keys: {'pager': 'less', 'skip-pager': None} # type: ignore[assignment] + pager_calls: list[str] = [] + monkeypatch.setattr(output_module, 'WIN', True) + monkeypatch.setattr(output_module.shutil, 'which', lambda value: None) + monkeypatch.setattr(output_module.special, 'set_pager', lambda value: pager_calls.append(value)) + monkeypatch.setattr(output_module.special, 'disable_pager', lambda: None) + + OutputMixin.configure_pager(cli) + + assert pager_calls == ['more'] + + def test_configure_pager_prefers_my_cnf_pager_and_sets_less(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() cli.my_cnf = ConfigObj({'client': {'pager': 'my-pager'}}) @@ -203,6 +329,17 @@ def test_format_sqlresult_uses_redirect_formatter_and_appends_preamble_postamble assert cli.redirect_formatter.calls +def test_format_sqlresult_uses_null_string_when_default_missing_value_is_configured() -> None: + cli = make_bare_mycli() + cli.main_formatter = DummyFormatter() + result = SQLResult(header=['id'], rows=[(None,)]) + + list(OutputMixin.format_sqlresult(cli, result, null_string='')) + + _, kwargs = cli.main_formatter.calls[-1] + assert kwargs['missing_value'] == '' + + def test_format_sqlresult_for_cursor_sets_column_types_and_alignment(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() cli.main_formatter = DummyFormatter() @@ -217,6 +354,42 @@ def test_format_sqlresult_for_cursor_sets_column_types_and_alignment(monkeypatch assert kwargs['colalign'] == ['left', 'left'] +def test_format_sqlresult_for_empty_cursor_uses_empty_column_metadata(monkeypatch: pytest.MonkeyPatch) -> None: + cli = make_bare_mycli() + cli.main_formatter = DummyFormatter() + monkeypatch.setattr(output_module, 'Cursor', FakeCursorBase) + rows = FakeCursorBase(rowcount=0, description=[('id', 3)]) + result = SQLResult(header=['id'], rows=cast(Any, rows)) + + list(OutputMixin.format_sqlresult(cli, result)) + + _, kwargs = cli.main_formatter.calls[-1] + assert kwargs['column_types'] == [] + assert kwargs['colalign'] == [] + + +def test_format_sqlresult_materializes_cursor_rows_when_width_is_limited(monkeypatch: pytest.MonkeyPatch) -> None: + cli = make_bare_mycli() + cli.main_formatter = DummyFormatter() + monkeypatch.setattr(output_module, 'Cursor', FakeCursorBase) + rows = FakeCursorBase(rows=[(1,)], rowcount=1, description=[('id', 3)]) + result = SQLResult(header=['id'], rows=cast(Any, rows)) + + list(OutputMixin.format_sqlresult(cli, result, max_width=100)) + + formatted_rows = cli.main_formatter.calls[-1][0][0] + assert formatted_rows == [(1,)] + + +def test_format_sqlresult_splits_string_formatter_output() -> None: + cli = make_bare_mycli() + cli.main_formatter = DummyFormatter() + cli.main_formatter.format_output = lambda *args, **kwargs: 'one\ntwo' # type: ignore[method-assign] + result = SQLResult(header=['id'], rows=[(1,)]) + + assert list(OutputMixin.format_sqlresult(cli, result)) == ['one', 'two'] + + def test_format_sqlresult_switches_to_vertical_when_first_line_is_too_wide() -> None: cli = make_bare_mycli() cli.main_formatter = DummyFormatter() @@ -225,6 +398,29 @@ def test_format_sqlresult_switches_to_vertical_when_first_line_is_too_wide() -> assert list(OutputMixin.format_sqlresult(cli, result, max_width=2)) == ['vertical output'] +def test_format_sqlresult_splits_string_vertical_output_when_table_is_too_wide() -> None: + cli = make_bare_mycli() + cli.main_formatter = DummyFormatter() + + def format_output(rows, header, format_name=None, **kwargs): + if format_name == 'vertical': + return 'vertical one\nvertical two' + return ['too wide'] + + cli.main_formatter.format_output = format_output # type: ignore[method-assign] + result = SQLResult(header=['id'], rows=[(1,)]) + + assert list(OutputMixin.format_sqlresult(cli, result, max_width=2)) == ['vertical one', 'vertical two'] + + +def test_format_sqlresult_keeps_table_when_first_line_fits_width() -> None: + cli = make_bare_mycli() + cli.main_formatter = DummyFormatter() + result = SQLResult(header=['id'], rows=[(1,)]) + + assert list(OutputMixin.format_sqlresult(cli, result, max_width=100)) == ['plain output'] + + def test_get_reserved_space_caps_ratio(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() monkeypatch.setattr(shutil, 'get_terminal_size', lambda *args, **kwargs: (120, 40))