Skip to content

Commit e7feffd

Browse files
authored
Merge pull request #1823 from dbcli/RW/restore-full-main-py-coverage
Restore full `main.py` test coverage
2 parents a57bc1f + fb0dd85 commit e7feffd

3 files changed

Lines changed: 340 additions & 155 deletions

File tree

test/pytests/test_main.py

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88
import shutil
99
from tempfile import NamedTemporaryFile
1010
from textwrap import dedent
11+
from types import SimpleNamespace
12+
from typing import Any, cast
1113

1214
import click
1315
from click.testing import CliRunner
1416
from pymysql.err import OperationalError
17+
import pytest
1518

19+
from mycli import main
1620
from mycli.constants import (
1721
DEFAULT_DATABASE,
1822
DEFAULT_HOST,
@@ -26,7 +30,20 @@
2630
from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS
2731
from mycli.packages.sqlresult import SQLResult
2832
from mycli.sqlexecute import ServerInfo, SQLExecute
29-
from test.utils import DATABASE, HOST, PASSWORD, PORT, TEMPFILE_PREFIX, USER, dbtest, run
33+
from test.utils import (
34+
DATABASE,
35+
HOST,
36+
PASSWORD,
37+
PORT,
38+
TEMPFILE_PREFIX,
39+
USER,
40+
ReusableLock,
41+
call_click_entrypoint_direct,
42+
dbtest,
43+
make_bare_mycli,
44+
make_dummy_mycli_class,
45+
run,
46+
)
3047

3148
pytests_dir = os.path.abspath(os.path.dirname(__file__))
3249
project_root_dir = os.path.abspath(os.path.join(pytests_dir, '..', '..'))
@@ -2150,3 +2167,160 @@ def test_null_string_config(monkeypatch):
21502167
os.remove(myclirc.name)
21512168
except Exception as e:
21522169
print(f'An error occurred while attempting to delete the file: {e}')
2170+
2171+
2172+
def test_change_prompt_format_requires_argument() -> None:
2173+
cli = make_bare_mycli()
2174+
assert main.MyCli.change_prompt_format(cli, '')[0].status == 'Missing required argument, format.'
2175+
2176+
2177+
def test_change_prompt_format_updates_prompt() -> None:
2178+
cli = make_bare_mycli()
2179+
assert main.MyCli.change_prompt_format(cli, '\\u@\\h> ')[0].status == 'Changed prompt format to \\u@\\h> '
2180+
2181+
2182+
def test_output_timing_logs_and_prints_with_warning_style(monkeypatch: pytest.MonkeyPatch) -> None:
2183+
cli = make_bare_mycli()
2184+
timings_logged: list[str] = []
2185+
cli.log_output = lambda text: timings_logged.append(text) # type: ignore[assignment]
2186+
printed: list[tuple[Any, Any]] = []
2187+
monkeypatch.setattr(main, 'print_formatted_text', lambda text, style=None: printed.append((text, style)))
2188+
main.MyCli.output_timing(cli, 'Time: 1.000s', is_warnings_style=True)
2189+
assert timings_logged == ['Time: 1.000s']
2190+
assert printed[-1][1] == cli.ptoolkit_style
2191+
2192+
2193+
def test_run_cli_delegates_to_main_repl(monkeypatch: pytest.MonkeyPatch) -> None:
2194+
cli = make_bare_mycli()
2195+
run_cli_calls: list[Any] = []
2196+
monkeypatch.setattr(main, 'main_repl', lambda target: run_cli_calls.append(target))
2197+
main.MyCli.run_cli(cli)
2198+
assert run_cli_calls == [cli]
2199+
2200+
2201+
def test_get_output_margin_uses_prompt_session_render_counter(monkeypatch: pytest.MonkeyPatch) -> None:
2202+
cli = make_bare_mycli()
2203+
render_counters: list[int] = []
2204+
cli.prompt_lines = 0
2205+
cli.get_reserved_space = lambda: 2 # type: ignore[assignment]
2206+
cli.prompt_session = cast(
2207+
Any,
2208+
SimpleNamespace(app=SimpleNamespace(render_counter=7)),
2209+
)
2210+
2211+
def fake_get_prompt(mycli: Any, string: str, render_counter: int) -> str:
2212+
render_counters.append(render_counter)
2213+
return 'line1\nline2'
2214+
2215+
monkeypatch.setattr(main, 'get_prompt', fake_get_prompt)
2216+
monkeypatch.setattr(main.special, 'is_timing_enabled', lambda: False)
2217+
assert main.MyCli.get_output_margin(cli, 'ok') == 5
2218+
assert render_counters == [7]
2219+
2220+
2221+
def test_on_completions_refreshed_updates_completer_and_invalidates_prompt() -> None:
2222+
cli = make_bare_mycli()
2223+
entered_lock = {'count': 0}
2224+
invalidated: list[bool] = []
2225+
cli._completer_lock = cast(Any, ReusableLock(lambda: entered_lock.__setitem__('count', entered_lock['count'] + 1)))
2226+
cli.prompt_session = cast(Any, SimpleNamespace(app=SimpleNamespace(invalidate=lambda: invalidated.append(True))))
2227+
new_completer = cast(Any, SimpleNamespace(get_completions=lambda document, event: ['done']))
2228+
main.MyCli._on_completions_refreshed(cli, new_completer)
2229+
assert cli.completer is new_completer
2230+
assert invalidated == [True]
2231+
assert entered_lock['count'] == 1
2232+
2233+
2234+
def test_get_completions_uses_current_completer() -> None:
2235+
cli = make_bare_mycli()
2236+
entered_lock = {'count': 0}
2237+
cli._completer_lock = cast(Any, ReusableLock(lambda: entered_lock.__setitem__('count', entered_lock['count'] + 1)))
2238+
cli.completer = cast(Any, SimpleNamespace(get_completions=lambda document, event: ['done']))
2239+
assert list(main.MyCli.get_completions(cli, 'select', 6)) == ['done']
2240+
assert entered_lock['count'] == 1
2241+
2242+
2243+
def test_click_entrypoint_callback_covers_dsn_list_init_commands(monkeypatch: pytest.MonkeyPatch) -> None:
2244+
dummy_class = make_dummy_mycli_class(
2245+
config={
2246+
'main': {'use_keyring': 'false', 'my_cnf_transition_done': 'true'},
2247+
'connection': {'default_keepalive_ticks': 0},
2248+
'alias_dsn': {'prod': 'mysql://u:p@h/db'},
2249+
'alias_dsn.init-commands': {'prod': ['set a=1', 'set b=2']},
2250+
}
2251+
)
2252+
monkeypatch.setattr(main, 'MyCli', dummy_class)
2253+
monkeypatch.setattr(main.sys, 'stdin', SimpleNamespace(isatty=lambda: True))
2254+
monkeypatch.setattr(main.sys.stderr, 'isatty', lambda: True)
2255+
2256+
cli_args = main.CliArgs()
2257+
cli_args.dsn = 'prod'
2258+
cli_args.init_command = 'set c=3'
2259+
call_click_entrypoint_direct(cli_args)
2260+
2261+
dummy = dummy_class.last_instance
2262+
assert dummy is not None
2263+
assert dummy.connect_calls[-1]['init_command'] == 'set a=1; set b=2; set c=3'
2264+
2265+
2266+
def test_click_entrypoint_callback_uses_batch_with_progress_path(monkeypatch: pytest.MonkeyPatch) -> None:
2267+
dummy_class = make_dummy_mycli_class(
2268+
config={
2269+
'main': {'use_keyring': 'false', 'my_cnf_transition_done': 'true'},
2270+
'connection': {'default_keepalive_ticks': 0},
2271+
'alias_dsn': {},
2272+
}
2273+
)
2274+
monkeypatch.setattr(main, 'MyCli', dummy_class)
2275+
monkeypatch.setattr(main.sys, 'stdin', SimpleNamespace(isatty=lambda: True))
2276+
monkeypatch.setattr(main.sys.stderr, 'isatty', lambda: True)
2277+
monkeypatch.setattr(main, 'main_batch_with_progress_bar', lambda mycli, cli_args: 12)
2278+
2279+
cli_args = main.CliArgs()
2280+
cli_args.batch = 'queries.sql'
2281+
cli_args.progress = True
2282+
with pytest.raises(SystemExit) as excinfo:
2283+
call_click_entrypoint_direct(cli_args)
2284+
assert excinfo.value.code == 12
2285+
2286+
2287+
def test_click_entrypoint_callback_uses_batch_without_progress_path(monkeypatch: pytest.MonkeyPatch) -> None:
2288+
dummy_class = make_dummy_mycli_class(
2289+
config={
2290+
'main': {'use_keyring': 'false', 'my_cnf_transition_done': 'true'},
2291+
'connection': {'default_keepalive_ticks': 0},
2292+
'alias_dsn': {},
2293+
}
2294+
)
2295+
monkeypatch.setattr(main, 'MyCli', dummy_class)
2296+
monkeypatch.setattr(main.sys, 'stdin', SimpleNamespace(isatty=lambda: True))
2297+
monkeypatch.setattr(main.sys.stderr, 'isatty', lambda: True)
2298+
monkeypatch.setattr(main, 'main_batch_without_progress_bar', lambda mycli, cli_args: 13)
2299+
2300+
cli_args = main.CliArgs()
2301+
cli_args.batch = 'queries.sql'
2302+
cli_args.progress = False
2303+
with pytest.raises(SystemExit) as excinfo:
2304+
call_click_entrypoint_direct(cli_args)
2305+
assert excinfo.value.code == 13
2306+
2307+
2308+
def test_click_entrypoint_callback_covers_mycnf_underscore_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
2309+
click_lines: list[str] = []
2310+
monkeypatch.setattr(click, 'secho', lambda message='', **kwargs: click_lines.append(str(message)))
2311+
monkeypatch.setattr(main.sys, 'stdin', SimpleNamespace(isatty=lambda: True))
2312+
monkeypatch.setattr(main.sys.stderr, 'isatty', lambda: False)
2313+
2314+
dummy_class = make_dummy_mycli_class(
2315+
config={
2316+
'main': {'use_keyring': 'false', 'my_cnf_transition_done': 'false'},
2317+
'connection': {'default_keepalive_ticks': 0},
2318+
'alias_dsn': {},
2319+
},
2320+
my_cnf={'client': {'ssl_ca': '/tmp/ca.pem'}, 'mysqld': {}},
2321+
config_without_package_defaults={'main': {}},
2322+
)
2323+
monkeypatch.setattr(main, 'MyCli', dummy_class)
2324+
2325+
call_click_entrypoint_direct(main.CliArgs())
2326+
assert any('ssl-ca = /tmp/ca.pem' in line for line in click_lines)

test/pytests/test_main_regression.py

Lines changed: 8 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from pathlib import Path
2424
import sys
2525
from types import ModuleType, SimpleNamespace
26-
from typing import Any, Callable, Literal, cast
26+
from typing import Any, cast
2727

2828
import click
2929
from click.testing import CliRunner
@@ -34,42 +34,13 @@
3434
from mycli import main
3535
import mycli.key_bindings
3636
from mycli.packages.sqlresult import SQLResult
37-
38-
39-
class DummyLogger:
40-
def __init__(self) -> None:
41-
self.debug_calls: list[tuple[tuple[Any, ...], dict[str, Any]]] = []
42-
self.error_calls: list[tuple[tuple[Any, ...], dict[str, Any]]] = []
43-
self.warning_calls: list[tuple[tuple[Any, ...], dict[str, Any]]] = []
44-
45-
def debug(self, *args: Any, **kwargs: Any) -> None:
46-
self.debug_calls.append((args, kwargs))
47-
48-
def error(self, *args: Any, **kwargs: Any) -> None:
49-
self.error_calls.append((args, kwargs))
50-
51-
def warning(self, *args: Any, **kwargs: Any) -> None:
52-
self.warning_calls.append((args, kwargs))
53-
54-
55-
class DummyFormatter:
56-
def __init__(self, format_name: str = 'ascii') -> None:
57-
self.format_name = format_name
58-
self.query = ''
59-
self.supported_formats = ['ascii', 'csv', 'tsv', 'vertical']
60-
self._output_formats = {
61-
'ascii': SimpleNamespace(formatter_args={'missing_value': main.DEFAULT_MISSING_VALUE}),
62-
'csv': SimpleNamespace(formatter_args={'missing_value': main.DEFAULT_MISSING_VALUE}),
63-
'tsv': SimpleNamespace(formatter_args={'missing_value': main.DEFAULT_MISSING_VALUE}),
64-
'vertical': SimpleNamespace(formatter_args={'missing_value': main.DEFAULT_MISSING_VALUE}),
65-
}
66-
self.calls: list[tuple[tuple[Any, ...], dict[str, Any]]] = []
67-
68-
def format_output(self, rows: Any, header: Any, format_name: str | None = None, **kwargs: Any) -> list[str] | str:
69-
self.calls.append(((rows, header, format_name), kwargs))
70-
if format_name == 'vertical':
71-
return ['vertical output']
72-
return ['plain output']
37+
from test.utils import ( # type: ignore[attr-defined]
38+
DummyFormatter,
39+
DummyLogger,
40+
call_click_entrypoint_direct,
41+
make_bare_mycli,
42+
make_dummy_mycli_class,
43+
)
7344

7445

7546
class FakeCursorBase:
@@ -100,19 +71,6 @@ def ping(self, reconnect: bool = False) -> None:
10071
raise self.ping_exc
10172

10273

103-
class ReusableLock:
104-
def __init__(self, on_enter: Callable[[], Any] | None = None) -> None:
105-
self.on_enter = on_enter
106-
107-
def __enter__(self) -> 'ReusableLock':
108-
if self.on_enter is not None:
109-
self.on_enter()
110-
return self
111-
112-
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Literal[False]:
113-
return False
114-
115-
11674
class BoolSection(dict[str, Any]):
11775
def as_bool(self, key: str) -> bool:
11876
return str(self[key]).lower() == 'true'
@@ -154,63 +112,6 @@ def __int__(self) -> int:
154112
raise ValueError('bad int')
155113

156114

157-
def make_bare_mycli() -> Any:
158-
cli = object.__new__(main.MyCli)
159-
cli.logger = cast(Any, DummyLogger())
160-
cli.main_formatter = DummyFormatter()
161-
cli.redirect_formatter = DummyFormatter()
162-
cli.helpers_style = 'helpers-style'
163-
cli.helpers_warnings_style = 'helpers-warnings-style'
164-
cli.ptoolkit_style = cast(Any, 'pt-style')
165-
cli.syntax_style = 'native'
166-
cli.cli_style = {}
167-
cli.null_string = '<null>'
168-
cli.numeric_alignment = 'right'
169-
cli.binary_display = None
170-
cli.show_warnings = False
171-
cli.query_history = []
172-
cli.toolbar_error_message = None
173-
cli.prompt_session = None
174-
cli.last_prompt_message = main.ANSI('')
175-
cli.last_custom_toolbar_message = main.ANSI('')
176-
cli.prompt_lines = 0
177-
cli.prompt_format = main.MyCli.default_prompt
178-
cli.multiline_continuation_char = '>'
179-
cli.toolbar_format = 'default'
180-
cli.destructive_warning = False
181-
cli.destructive_keywords = ['drop']
182-
cli.keepalive_ticks = None
183-
cli._keepalive_counter = 0
184-
cli.less_chatty = True
185-
cli.smart_completion = False
186-
cli.key_bindings = 'emacs'
187-
cli.auto_vertical_output = False
188-
cli.wider_completion_menu = False
189-
cli.explicit_pager = False
190-
cli._completer_lock = cast(Any, ReusableLock())
191-
cli.terminal_tab_title_format = ''
192-
cli.terminal_window_title_format = ''
193-
cli.multiplex_window_title_format = ''
194-
cli.multiplex_pane_title_format = ''
195-
cli.dsn_alias = None
196-
cli.login_path = None
197-
cli.login_path_as_host = False
198-
cli.post_redirect_command = None
199-
cli.logfile = None
200-
cli.emacs_ttimeoutlen = 1.0
201-
cli.vi_ttimeoutlen = 1.0
202-
cli.beep_after_seconds = 0.0
203-
cli.config = {'history_file': '~/.mycli-history-testing'}
204-
cli.output = lambda *args, **kwargs: None # type: ignore[assignment]
205-
cli.echo = lambda *args, **kwargs: None # type: ignore[assignment]
206-
cli.log_query = lambda *args, **kwargs: None # type: ignore[assignment]
207-
cli.log_output = lambda *args, **kwargs: None # type: ignore[assignment]
208-
cli.configure_pager = lambda: None # type: ignore[assignment]
209-
cli.refresh_completions = lambda reset=False: [SQLResult(status='refresh')] # type: ignore[assignment]
210-
cli.reconnect = lambda database='': False # type: ignore[assignment]
211-
return cli
212-
213-
214115
def load_main_variant(monkeypatch: pytest.MonkeyPatch, *, fail_pwd: bool = False) -> ModuleType:
215116
import builtins
216117

@@ -232,53 +133,6 @@ def fake_import(name: str, globals: Any = None, locals: Any = None, fromlist: An
232133
return module
233134

234135

235-
def make_dummy_mycli_class(
236-
*,
237-
config: dict[str, Any] | None = None,
238-
my_cnf: dict[str, Any] | None = None,
239-
config_without_package_defaults: dict[str, Any] | None = None,
240-
) -> Any:
241-
class DummyMyCli:
242-
last_instance: Any = None
243-
244-
def __init__(self, **kwargs: Any) -> None:
245-
type(self).last_instance = self
246-
self.init_kwargs = dict(kwargs)
247-
self.config = config or {'main': {}, 'alias_dsn': {}}
248-
self.my_cnf = my_cnf or {'client': {}, 'mysqld': {}}
249-
self.config_without_package_defaults = config_without_package_defaults or {}
250-
self.default_keepalive_ticks = 5
251-
self.ssl_mode = None
252-
self.logger = DummyLogger()
253-
self.main_formatter = SimpleNamespace(format_name=None)
254-
self.destructive_warning = False
255-
self.destructive_keywords = ['drop']
256-
self.dsn_alias = None
257-
self.connect_calls: list[dict[str, Any]] = []
258-
self.run_query_calls: list[tuple[str, Any, bool]] = []
259-
self.run_cli_called = False
260-
self.close_called = False
261-
262-
def connect(self, **kwargs: Any) -> None:
263-
self.connect_calls.append(dict(kwargs))
264-
265-
def run_query(self, query: str, checkpoint: Any = None, new_line: bool = True) -> None:
266-
self.run_query_calls.append((query, checkpoint, new_line))
267-
268-
def run_cli(self) -> None:
269-
self.run_cli_called = True
270-
271-
def close(self) -> None:
272-
self.close_called = True
273-
274-
return DummyMyCli
275-
276-
277-
def call_click_entrypoint_direct(cli_args: main.CliArgs) -> None:
278-
assert main.click_entrypoint.callback is not None
279-
cast(Any, main.click_entrypoint.callback).__wrapped__(cli_args)
280-
281-
282136
def test_import_fallbacks_for_pwd(monkeypatch: pytest.MonkeyPatch) -> None:
283137
module = load_main_variant(monkeypatch, fail_pwd=True)
284138

0 commit comments

Comments
 (0)