Skip to content

Commit 6c71bf2

Browse files
committed
✅ increase test coverage
1 parent 5314fb8 commit 6c71bf2

File tree

5 files changed

+756
-0
lines changed

5 files changed

+756
-0
lines changed

tests/func/test_cli.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,135 @@ def test_keyboard_interrupt(
498498
assert result.exit_code > 0
499499
assert "Process interrupted" in result.output
500500

501+
def test_keyboard_interrupt_debug_mode(
502+
self,
503+
cli_runner: CliRunner,
504+
sqlite_database: str,
505+
mysql_credentials: MySQLCredentials,
506+
mocker: MockFixture,
507+
) -> None:
508+
"""Test keyboard interrupt handling in debug mode."""
509+
mocker.patch.object(SQLite3toMySQL, "transfer", side_effect=KeyboardInterrupt())
510+
result: Result = cli_runner.invoke(
511+
sqlite3mysql,
512+
[
513+
"-f",
514+
sqlite_database,
515+
"-d",
516+
mysql_credentials.database,
517+
"-u",
518+
mysql_credentials.user,
519+
"--mysql-password",
520+
mysql_credentials.password,
521+
"-h",
522+
mysql_credentials.host,
523+
"-P",
524+
str(mysql_credentials.port),
525+
"--debug", # Enable debug mode
526+
],
527+
)
528+
# In debug mode, the KeyboardInterrupt should be raised
529+
# However, Click's testing framework converts it to SystemExit
530+
assert result.exit_code > 0
531+
assert isinstance(result.exception, SystemExit)
532+
533+
def test_exception_debug_mode(
534+
self,
535+
cli_runner: CliRunner,
536+
sqlite_database: str,
537+
mysql_credentials: MySQLCredentials,
538+
mocker: MockFixture,
539+
) -> None:
540+
"""Test exception handling in debug mode."""
541+
# Mock the transfer method to raise an exception
542+
mocker.patch.object(SQLite3toMySQL, "transfer", side_effect=ValueError("Test error"))
543+
result: Result = cli_runner.invoke(
544+
sqlite3mysql,
545+
[
546+
"-f",
547+
sqlite_database,
548+
"-d",
549+
mysql_credentials.database,
550+
"-u",
551+
mysql_credentials.user,
552+
"--mysql-password",
553+
mysql_credentials.password,
554+
"-h",
555+
mysql_credentials.host,
556+
"-P",
557+
str(mysql_credentials.port),
558+
"--debug", # Enable debug mode
559+
],
560+
)
561+
# In debug mode, the exception should be raised
562+
assert result.exit_code > 0
563+
assert isinstance(result.exception, ValueError)
564+
assert str(result.exception) == "Test error"
565+
566+
def test_exception_normal_mode(
567+
self,
568+
cli_runner: CliRunner,
569+
sqlite_database: str,
570+
mysql_credentials: MySQLCredentials,
571+
mocker: MockFixture,
572+
) -> None:
573+
"""Test exception handling in normal mode (non-debug)."""
574+
# Mock the transfer method to raise an exception
575+
mocker.patch.object(SQLite3toMySQL, "transfer", side_effect=ValueError("Test error"))
576+
result: Result = cli_runner.invoke(
577+
sqlite3mysql,
578+
[
579+
"-f",
580+
sqlite_database,
581+
"-d",
582+
mysql_credentials.database,
583+
"-u",
584+
mysql_credentials.user,
585+
"--mysql-password",
586+
mysql_credentials.password,
587+
"-h",
588+
mysql_credentials.host,
589+
"-P",
590+
str(mysql_credentials.port),
591+
# No debug flag
592+
],
593+
)
594+
# In normal mode, the exception should be caught and the error message printed
595+
assert result.exit_code > 0
596+
assert "Test error" in result.output
597+
598+
def test_invalid_mysql_collation(
599+
self,
600+
cli_runner: CliRunner,
601+
sqlite_database: str,
602+
mysql_credentials: MySQLCredentials,
603+
) -> None:
604+
"""Test validation of mysql_collation against charset_collations."""
605+
result: Result = cli_runner.invoke(
606+
sqlite3mysql,
607+
[
608+
"-f",
609+
sqlite_database,
610+
"-d",
611+
mysql_credentials.database,
612+
"-u",
613+
mysql_credentials.user,
614+
"--mysql-password",
615+
mysql_credentials.password,
616+
"-h",
617+
mysql_credentials.host,
618+
"-P",
619+
str(mysql_credentials.port),
620+
"--mysql-charset",
621+
"utf8mb4",
622+
"--mysql-collation",
623+
"invalid_collation", # Invalid collation for utf8mb4
624+
],
625+
)
626+
assert result.exit_code > 0
627+
assert "Error: Invalid value for '--mysql-collation'" in result.output
628+
assert "invalid_collation" in result.output
629+
501630
def test_transfer_specific_tables_only(
502631
self,
503632
cli_runner: CliRunner,

tests/unit/click_utils_test.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import typing as t
2+
from unittest.mock import MagicMock, patch
3+
4+
import click
5+
import pytest
6+
from click.parser import Option, OptionParser
7+
8+
from sqlite3_to_mysql.click_utils import OptionEatAll, prompt_password
9+
10+
11+
class TestClickUtils:
12+
def test_option_eat_all_init(self) -> None:
13+
"""Test OptionEatAll initialization."""
14+
option = OptionEatAll(["--test"], save_other_options=True)
15+
assert option.save_other_options is True
16+
17+
option = OptionEatAll(["--test"], save_other_options=False)
18+
assert option.save_other_options is False
19+
20+
# Test with invalid nargs
21+
with pytest.raises(ValueError):
22+
OptionEatAll(["--test"], nargs=1)
23+
24+
def test_option_eat_all_parser_process(self) -> None:
25+
"""Test OptionEatAll parser_process function."""
26+
# This is a simplified test that just verifies the parser_process function works
27+
# Create a mock state object
28+
state = MagicMock()
29+
state.rargs = ["value1", "value2", "--next-option"]
30+
31+
# Create a mock parser process function
32+
process_mock = MagicMock()
33+
34+
# Create a parser_process function similar to the one in OptionEatAll
35+
def parser_process(value, state_obj):
36+
done = False
37+
value = [value]
38+
# Grab everything up to the next option
39+
while state_obj.rargs and not done:
40+
if state_obj.rargs[0].startswith("--"):
41+
done = True
42+
if not done:
43+
value.append(state_obj.rargs.pop(0))
44+
value = tuple(value)
45+
process_mock(value, state_obj)
46+
47+
# Call the function
48+
parser_process("initial", state)
49+
50+
# Check that the process_mock was called with the expected values
51+
process_mock.assert_called_once()
52+
args, kwargs = process_mock.call_args
53+
assert args[0] == ("initial", "value1", "value2")
54+
assert args[1] == state
55+
assert state.rargs == ["--next-option"]
56+
57+
def test_prompt_password_with_password(self) -> None:
58+
"""Test prompt_password function with password provided."""
59+
ctx = MagicMock()
60+
ctx.params = {"mysql_password": "test_password"}
61+
62+
result = prompt_password(ctx, None, True)
63+
assert result == "test_password"
64+
65+
def test_prompt_password_without_password(self) -> None:
66+
"""Test prompt_password function without password provided."""
67+
ctx = MagicMock()
68+
ctx.params = {"mysql_password": None}
69+
70+
with patch("click.prompt", return_value="prompted_password"):
71+
result = prompt_password(ctx, None, True)
72+
assert result == "prompted_password"
73+
74+
def test_prompt_password_not_used(self) -> None:
75+
"""Test prompt_password function when not used."""
76+
ctx = MagicMock()
77+
ctx.params = {"mysql_password": "test_password"}
78+
79+
result = prompt_password(ctx, None, False)
80+
assert result is None
81+
82+
def test_prompt_password_not_used_no_password(self) -> None:
83+
"""Test prompt_password function when not used and no password is provided."""
84+
ctx = MagicMock()
85+
ctx.params = {"mysql_password": None}
86+
87+
result = prompt_password(ctx, None, False)
88+
assert result is None
89+
90+
def test_option_eat_all_add_to_parser(self) -> None:
91+
"""Test OptionEatAll add_to_parser method with a real parser."""
92+
# Create a real parser
93+
parser = OptionParser()
94+
ctx = MagicMock()
95+
96+
# Create an OptionEatAll instance
97+
option = OptionEatAll(["--test"], save_other_options=True)
98+
99+
# Add it to the parser
100+
option.add_to_parser(parser, ctx)
101+
102+
# Verify that the parser has our option
103+
assert "--test" in parser._long_opt
104+
105+
# Verify that the process method has been replaced
106+
assert parser._long_opt["--test"].process != option._previous_parser_process
107+
108+
def test_option_eat_all_save_other_options_false(self) -> None:
109+
"""Test OptionEatAll parser_process function with save_other_options=False."""
110+
# Create a mock state object
111+
state = MagicMock()
112+
state.rargs = ["value1", "value2", "--next-option"]
113+
114+
# Create a mock process function
115+
process_mock = MagicMock()
116+
117+
# Create a simplified parser_process function that directly tests the behavior
118+
# we're interested in (save_other_options=False)
119+
def parser_process(value, state_obj):
120+
done = False
121+
value = [value]
122+
# This is the branch we want to test (save_other_options=False)
123+
# grab everything remaining
124+
value += state_obj.rargs
125+
state_obj.rargs[:] = []
126+
value = tuple(value)
127+
process_mock(value, state_obj)
128+
129+
# Call the function
130+
parser_process("initial", state)
131+
132+
# Check that the process_mock was called with the expected values
133+
process_mock.assert_called_once()
134+
args, kwargs = process_mock.call_args
135+
# With save_other_options=False, all remaining args should be consumed
136+
assert args[0] == ("initial", "value1", "value2", "--next-option")
137+
assert args[1] == state
138+
# The state.rargs should be empty
139+
assert state.rargs == []
140+
141+
def test_option_eat_all_actual_implementation(self) -> None:
142+
"""Test the actual implementation of OptionEatAll parser_process method."""
143+
# Create a real OptionEatAll instance
144+
option = OptionEatAll(["--test"], save_other_options=True)
145+
146+
# Create a mock parser with prefixes
147+
parser = MagicMock()
148+
parser.prefixes = ["--", "-"]
149+
150+
# Create a mock option that will be returned by parser._long_opt.get()
151+
mock_option = MagicMock()
152+
mock_option.prefixes = ["--", "-"] # This is needed for the parser_process method
153+
parser._long_opt = {"--test": mock_option}
154+
parser._short_opt = {}
155+
156+
# Create a state object with rargs
157+
state = MagicMock()
158+
159+
# Test case 1: save_other_options=True, with non-option arguments
160+
state.rargs = ["value1", "value2", "--next-option"]
161+
162+
# Call the parser_process method
163+
option.add_to_parser(parser, MagicMock()) # This sets up the parser_process method
164+
165+
# Now mock_option.process should have been replaced with our parser_process
166+
# Call it directly to simulate what would happen in the real parser
167+
mock_option.process("initial", state)
168+
169+
# Check that the previous_parser_process was called with the expected values
170+
option._previous_parser_process.assert_called_once()
171+
args, kwargs = option._previous_parser_process.call_args
172+
assert args[0] == ("initial", "value1", "value2")
173+
assert args[1] == state
174+
assert state.rargs == ["--next-option"]
175+
176+
# Reset mocks
177+
option._previous_parser_process.reset_mock()
178+
179+
# Test case 2: save_other_options=False
180+
option.save_other_options = False
181+
state.rargs = ["value1", "value2", "--next-option"]
182+
183+
# Call the parser_process method
184+
mock_option.process("initial", state)
185+
186+
# Check that the previous_parser_process was called with the expected values
187+
option._previous_parser_process.assert_called_once()
188+
args, kwargs = option._previous_parser_process.call_args
189+
assert args[0] == ("initial", "value1", "value2", "--next-option")
190+
assert args[1] == state
191+
assert state.rargs == []
192+
193+
def test_option_eat_all_add_to_parser_with_short_opt(self) -> None:
194+
"""Test OptionEatAll add_to_parser method with short option."""
195+
# Create a real parser
196+
parser = OptionParser()
197+
ctx = MagicMock()
198+
199+
# Set up the parser with a short option
200+
mock_option = MagicMock()
201+
parser._short_opt = {"-t": mock_option}
202+
parser._long_opt = {}
203+
204+
# Create an OptionEatAll instance with a short option
205+
option = OptionEatAll(["-t"], save_other_options=True)
206+
207+
# Add it to the parser
208+
option.add_to_parser(parser, ctx)
209+
210+
# Verify that the parser has our option
211+
assert "-t" in parser._short_opt
212+
213+
# Verify that the process method has been replaced
214+
assert hasattr(option, "_previous_parser_process")
215+
assert option._eat_all_parser is not None

0 commit comments

Comments
 (0)