Skip to content

Commit ef86860

Browse files
committed
✅ increase test coverage
1 parent 1d84557 commit ef86860

File tree

8 files changed

+861
-5
lines changed

8 files changed

+861
-5
lines changed

src/mysql_to_sqlite3/debug_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def info() -> t.List[t.List[str]]:
8383
["MySQL", _mysql_version()],
8484
["SQLite", sqlite3.sqlite_version],
8585
["", ""],
86-
["click", click.__version__],
86+
["click", str(click.__version__)],
8787
["mysql-connector-python", mysql.connector.__version__],
8888
["python-slugify", slugify.__version__],
8989
["pytimeparse2", pytimeparse2.__version__],

src/mysql_to_sqlite3/sqlite_utils.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,18 @@ def convert_timedelta(value: t.Any) -> timedelta:
3434

3535
def encode_data_for_sqlite(value: t.Any) -> t.Any:
3636
"""Fix encoding bytes."""
37-
try:
38-
return value.decode()
39-
except (UnicodeDecodeError, AttributeError):
40-
return sqlite3.Binary(value)
37+
if isinstance(value, bytes):
38+
try:
39+
return value.decode()
40+
except (UnicodeDecodeError, AttributeError):
41+
return sqlite3.Binary(value)
42+
elif isinstance(value, str):
43+
return value
44+
else:
45+
try:
46+
return sqlite3.Binary(value)
47+
except TypeError:
48+
return value
4149

4250

4351
class CollatingSequences:

tests/func/test_cli.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,3 +578,67 @@ def test_version(self, cli_runner: CliRunner) -> None:
578578
"tqdm",
579579
}
580580
)
581+
582+
def test_invalid_mysql_charset_collation(
583+
self,
584+
cli_runner: CliRunner,
585+
sqlite_database: "os.PathLike[t.Any]",
586+
mysql_credentials: MySQLCredentials,
587+
mysql_database: Database,
588+
) -> None:
589+
"""Test CLI with invalid collation for the specified charset."""
590+
result: Result = cli_runner.invoke(
591+
mysql2sqlite,
592+
[
593+
"-f",
594+
str(sqlite_database),
595+
"-d",
596+
mysql_credentials.database,
597+
"-u",
598+
mysql_credentials.user,
599+
"--mysql-password",
600+
mysql_credentials.password,
601+
"-h",
602+
mysql_credentials.host,
603+
"-P",
604+
str(mysql_credentials.port),
605+
"--mysql-charset",
606+
"utf8mb4",
607+
"--mysql-collation",
608+
"invalid_collation",
609+
],
610+
)
611+
assert result.exit_code > 0
612+
assert "Error: Invalid value for '--mysql-collation': 'invalid_collation'" in result.output
613+
614+
def test_without_tables_and_without_data_flags(
615+
self,
616+
cli_runner: CliRunner,
617+
sqlite_database: "os.PathLike[t.Any]",
618+
mysql_credentials: MySQLCredentials,
619+
) -> None:
620+
"""Test CLI with both --without-tables and --without-data flags set."""
621+
result: Result = cli_runner.invoke(
622+
mysql2sqlite,
623+
[
624+
"-f",
625+
str(sqlite_database),
626+
"-d",
627+
mysql_credentials.database,
628+
"-u",
629+
mysql_credentials.user,
630+
"--mysql-password",
631+
mysql_credentials.password,
632+
"-h",
633+
mysql_credentials.host,
634+
"-P",
635+
str(mysql_credentials.port),
636+
"--without-tables",
637+
"--without-data",
638+
],
639+
)
640+
assert result.exit_code > 0
641+
assert (
642+
"Error: Both -Z/--without-tables and -W/--without-data are set. There is nothing to do. Exiting..."
643+
in result.output
644+
)

tests/unit/test_click_utils.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import typing as t
2+
3+
import click
4+
import pytest
5+
from click.testing import CliRunner
6+
from pytest_mock import MockFixture
7+
8+
from mysql_to_sqlite3.click_utils import OptionEatAll, prompt_password, validate_positive_integer
9+
10+
11+
class TestOptionEatAll:
12+
def test_init_with_invalid_nargs(self) -> None:
13+
"""Test OptionEatAll initialization with invalid nargs."""
14+
with pytest.raises(ValueError) as excinfo:
15+
OptionEatAll("--test", nargs=1)
16+
assert "nargs, if set, must be -1 not 1" in str(excinfo.value)
17+
18+
def test_init_with_valid_nargs(self) -> None:
19+
"""Test OptionEatAll initialization with valid nargs and behavior."""
20+
21+
@click.command()
22+
@click.option("--test", cls=OptionEatAll, nargs=-1, help="Test option")
23+
def cli(test: t.Optional[t.Tuple[str, ...]] = None) -> None:
24+
# This just verifies that the option works when nargs=-1
25+
assert test is not None
26+
click.echo(f"Success: {len(test)} values")
27+
28+
runner = CliRunner()
29+
result = runner.invoke(cli, ["--test", "value1", "value2", "value3"])
30+
assert result.exit_code == 0
31+
assert "Success:" in result.output
32+
assert "values" in result.output
33+
34+
def test_add_to_parser(self) -> None:
35+
"""Test add_to_parser method."""
36+
37+
@click.command()
38+
@click.option("--test", cls=OptionEatAll, help="Test option")
39+
def cli(test: t.Optional[t.Tuple[str, ...]] = None) -> None:
40+
click.echo(f"Test: {test}")
41+
42+
runner = CliRunner()
43+
result = runner.invoke(cli, ["--test", "value1", "value2", "value3"])
44+
assert result.exit_code == 0
45+
assert "Test: ('value1', 'value2', 'value3')" in result.output
46+
47+
def test_add_to_parser_with_other_options(self) -> None:
48+
"""Test add_to_parser method with other options."""
49+
50+
@click.command()
51+
@click.option("--test", cls=OptionEatAll, help="Test option")
52+
@click.option("--other", help="Other option")
53+
def cli(test: t.Optional[t.Tuple[str, ...]] = None, other: t.Optional[str] = None) -> None:
54+
click.echo(f"Test: {test}, Other: {other}")
55+
56+
runner = CliRunner()
57+
result = runner.invoke(cli, ["--test", "value1", "value2", "--other", "value3"])
58+
assert result.exit_code == 0
59+
assert "Test: ('value1', 'value2'), Other: value3" in result.output
60+
61+
def test_add_to_parser_without_save_other_options(self) -> None:
62+
"""Test add_to_parser method without saving other options."""
63+
64+
@click.command()
65+
@click.option("--test", cls=OptionEatAll, save_other_options=False, help="Test option")
66+
@click.option("--other", help="Other option")
67+
def cli(test: t.Optional[t.Tuple[str, ...]] = None, other: t.Optional[str] = None) -> None:
68+
click.echo(f"Test: {test}, Other: {other}")
69+
70+
runner = CliRunner()
71+
result = runner.invoke(cli, ["--test", "value1", "value2", "--other", "value3"])
72+
assert result.exit_code == 0
73+
# All remaining args should be consumed by --test
74+
assert "Test: ('value1', 'value2', '--other', 'value3'), Other: None" in result.output
75+
76+
77+
class TestPromptPassword:
78+
def test_prompt_password_with_password(self) -> None:
79+
"""Test prompt_password with password already provided."""
80+
ctx = click.Context(click.Command("test"))
81+
ctx.params = {"mysql_password": "test_password"}
82+
83+
result = prompt_password(ctx, None, True)
84+
assert result == "test_password"
85+
86+
def test_prompt_password_without_password(self, mocker: MockFixture) -> None:
87+
"""Test prompt_password without password provided."""
88+
ctx = click.Context(click.Command("test"))
89+
ctx.params = {"mysql_password": None}
90+
91+
mocker.patch("click.prompt", return_value="prompted_password")
92+
93+
result = prompt_password(ctx, None, True)
94+
assert result == "prompted_password"
95+
96+
def test_prompt_password_use_password_false(self) -> None:
97+
"""Test prompt_password with use_password=False."""
98+
ctx = click.Context(click.Command("test"))
99+
ctx.params = {"mysql_password": "test_password"}
100+
101+
result = prompt_password(ctx, None, False)
102+
assert result is None
103+
104+
105+
class TestValidatePositiveInteger:
106+
def test_validate_positive_integer_valid(self) -> None:
107+
"""Test validate_positive_integer with valid values."""
108+
ctx = click.Context(click.Command("test"))
109+
110+
assert validate_positive_integer(ctx, None, 0) == 0
111+
assert validate_positive_integer(ctx, None, 1) == 1
112+
assert validate_positive_integer(ctx, None, 100) == 100
113+
114+
def test_validate_positive_integer_invalid(self) -> None:
115+
"""Test validate_positive_integer with invalid values."""
116+
ctx = click.Context(click.Command("test"))
117+
118+
with pytest.raises(click.BadParameter) as excinfo:
119+
validate_positive_integer(ctx, None, -1)
120+
assert "Should be a positive integer or 0." in str(excinfo.value)
121+
122+
with pytest.raises(click.BadParameter) as excinfo:
123+
validate_positive_integer(ctx, None, -100)
124+
assert "Should be a positive integer or 0." in str(excinfo.value)

tests/unit/test_debug_info.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import sys
2+
import typing as t
3+
from unittest.mock import MagicMock, patch
4+
5+
import pytest
6+
from pytest_mock import MockFixture
7+
8+
from mysql_to_sqlite3.debug_info import _implementation, _mysql_version, info
9+
10+
11+
class TestDebugInfo:
12+
def test_implementation_cpython(self, mocker: MockFixture) -> None:
13+
"""Test _implementation function with CPython."""
14+
mocker.patch("platform.python_implementation", return_value="CPython")
15+
mocker.patch("platform.python_version", return_value="3.8.10")
16+
17+
result = _implementation()
18+
assert result == "CPython 3.8.10"
19+
20+
def test_implementation_pypy(self, mocker: MockFixture) -> None:
21+
"""Test _implementation function with PyPy."""
22+
mocker.patch("platform.python_implementation", return_value="PyPy")
23+
24+
# Create a mock for pypy_version_info
25+
mock_version_info = MagicMock()
26+
mock_version_info.major = 3
27+
mock_version_info.minor = 7
28+
mock_version_info.micro = 4
29+
mock_version_info.releaselevel = "final"
30+
31+
# Need to use patch instead of mocker.patch for sys module attributes
32+
with patch.object(sys, "pypy_version_info", mock_version_info, create=True):
33+
result = _implementation()
34+
assert result == "PyPy 3.7.4"
35+
36+
def test_implementation_pypy_non_final(self, mocker: MockFixture) -> None:
37+
"""Test _implementation function with PyPy non-final release."""
38+
mocker.patch("platform.python_implementation", return_value="PyPy")
39+
40+
# Create a mock for pypy_version_info
41+
mock_version_info = MagicMock()
42+
mock_version_info.major = 3
43+
mock_version_info.minor = 7
44+
mock_version_info.micro = 4
45+
mock_version_info.releaselevel = "beta"
46+
47+
# Need to use patch instead of mocker.patch for sys module attributes
48+
with patch.object(sys, "pypy_version_info", mock_version_info, create=True):
49+
result = _implementation()
50+
assert result == "PyPy 3.7.4beta"
51+
52+
def test_implementation_jython(self, mocker: MockFixture) -> None:
53+
"""Test _implementation function with Jython."""
54+
mocker.patch("platform.python_implementation", return_value="Jython")
55+
mocker.patch("platform.python_version", return_value="2.7.2")
56+
57+
result = _implementation()
58+
assert result == "Jython 2.7.2"
59+
60+
def test_implementation_ironpython(self, mocker: MockFixture) -> None:
61+
"""Test _implementation function with IronPython."""
62+
mocker.patch("platform.python_implementation", return_value="IronPython")
63+
mocker.patch("platform.python_version", return_value="2.7.9")
64+
65+
result = _implementation()
66+
assert result == "IronPython 2.7.9"
67+
68+
def test_implementation_unknown(self, mocker: MockFixture) -> None:
69+
"""Test _implementation function with unknown implementation."""
70+
mocker.patch("platform.python_implementation", return_value="UnknownPython")
71+
72+
result = _implementation()
73+
assert result == "UnknownPython Unknown"
74+
75+
def test_mysql_version_success(self, mocker: MockFixture) -> None:
76+
"""Test _mysql_version function when mysql client is available."""
77+
mocker.patch("mysql_to_sqlite3.debug_info.which", return_value="/usr/bin/mysql")
78+
mocker.patch(
79+
"mysql_to_sqlite3.debug_info.check_output",
80+
return_value=b"mysql Ver 8.0.26 for Linux on x86_64",
81+
)
82+
83+
result = _mysql_version()
84+
assert result == "mysql Ver 8.0.26 for Linux on x86_64"
85+
86+
def test_mysql_version_bytes_decode_error(self, mocker: MockFixture) -> None:
87+
"""Test _mysql_version function when bytes decoding fails."""
88+
mocker.patch("mysql_to_sqlite3.debug_info.which", return_value="/usr/bin/mysql")
89+
mock_output = MagicMock()
90+
mock_output.decode.side_effect = UnicodeDecodeError("utf-8", b"", 0, 1, "invalid")
91+
mocker.patch(
92+
"mysql_to_sqlite3.debug_info.check_output",
93+
return_value=mock_output,
94+
)
95+
96+
result = _mysql_version()
97+
assert isinstance(result, str)
98+
99+
def test_mysql_version_exception(self, mocker: MockFixture) -> None:
100+
"""Test _mysql_version function when an exception occurs."""
101+
mocker.patch("mysql_to_sqlite3.debug_info.which", return_value="/usr/bin/mysql")
102+
mocker.patch(
103+
"mysql_to_sqlite3.debug_info.check_output",
104+
side_effect=Exception("Command failed"),
105+
)
106+
107+
result = _mysql_version()
108+
assert result == "MySQL client not found on the system"
109+
110+
def test_mysql_version_not_found(self, mocker: MockFixture) -> None:
111+
"""Test _mysql_version function when mysql client is not found."""
112+
mocker.patch("mysql_to_sqlite3.debug_info.which", return_value=None)
113+
114+
result = _mysql_version()
115+
assert result == "MySQL client not found on the system"
116+
117+
def test_info_success(self, mocker: MockFixture) -> None:
118+
"""Test info function."""
119+
mocker.patch("platform.system", return_value="Linux")
120+
mocker.patch("platform.release", return_value="5.4.0-80-generic")
121+
mocker.patch("mysql_to_sqlite3.debug_info._implementation", return_value="CPython 3.8.10")
122+
mocker.patch("mysql_to_sqlite3.debug_info._mysql_version", return_value="mysql Ver 8.0.26 for Linux on x86_64")
123+
124+
result = info()
125+
assert isinstance(result, list)
126+
assert len(result) > 0
127+
assert result[2] == ["Operating System", "Linux 5.4.0-80-generic"]
128+
assert result[3] == ["Python", "CPython 3.8.10"]
129+
assert result[4] == ["MySQL", "mysql Ver 8.0.26 for Linux on x86_64"]
130+
131+
def test_info_platform_error(self, mocker: MockFixture) -> None:
132+
"""Test info function when platform.system raises IOError."""
133+
mocker.patch("platform.system", side_effect=IOError("Platform error"))
134+
135+
result = info()
136+
assert isinstance(result, list)
137+
assert len(result) > 0
138+
assert result[2] == ["Operating System", "Unknown"]

0 commit comments

Comments
 (0)