Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
196 changes: 196 additions & 0 deletions test/pytests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import itertools
import shutil
from types import SimpleNamespace
from typing import Any, cast

import click
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand All @@ -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'}})
Expand Down Expand Up @@ -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='<null>'))

_, kwargs = cli.main_formatter.calls[-1]
assert kwargs['missing_value'] == '<null>'


def test_format_sqlresult_for_cursor_sets_column_types_and_alignment(monkeypatch: pytest.MonkeyPatch) -> None:
cli = make_bare_mycli()
cli.main_formatter = DummyFormatter()
Expand All @@ -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()
Expand All @@ -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))
Expand Down
Loading