Skip to content

Commit a8b7801

Browse files
authored
Merge pull request #22 from WiredNerd/dev
🐩 New CLI options: --html and --json
2 parents 28efbae + a6f9433 commit a8b7801

File tree

10 files changed

+449
-196
lines changed

10 files changed

+449
-196
lines changed

.github/workflows/python-platform-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ jobs:
3535
- name: Poodle example project
3636
run : |
3737
cd example
38-
poodle --report summary --report not_found --report json --report html
38+
poodle --report summary --report not_found --json results.json --html html_report
3939
- name: Poodle example flat project
4040
run : |
4141
cd example2
42-
poodle --report summary --report not_found --report json --report html
42+
poodle --report summary --report not_found --json results.json --html html_report
4343

.github/workflows/update-coverage.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ jobs:
3838
name: Coverage Report
3939
path: cov-html
4040
- name: Poodle
41-
run: poodle --report json --report html
41+
run: poodle --json mutation-testing-report.json --html html-report
4242
- name: Upload Report HTML
4343
if: ${{ always() }}
4444
uses: actions/upload-artifact@v3
4545
with:
4646
name: Mutation testing report HTML
47-
path: mutation_reports
47+
path: html-report
4848
- name: save-json-report
4949
if: ${{ always() && github.ref_name == 'main' }}
5050
uses: EndBug/add-and-commit@v9

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"fromlist",
3232
"getaffinity",
3333
"keepends",
34+
"mergedeep",
3435
"Mult",
3536
"PYTHONDONTWRITEBYTECODE",
3637
"redef",

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "poodle"
77
description = "Mutation Testing Tool"
8-
version = "1.2.1"
8+
version = "1.2.2"
99
license = { file = "LICENSE" }
1010
keywords = [
1111
"test",
@@ -33,6 +33,7 @@ dependencies = [
3333
"wcmatch>=8.5",
3434
"tomli>=2; python_version<'3.11'",
3535
"jinja2>=3.0.3",
36+
"mergedeep>=1.0",
3637
]
3738

3839
[project.urls]

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ tomli>=2
33
wcmatch>=8.5
44
jinja2>=3.0.3
55
pysass
6+
mergedeep
67

78
pytest
89
pytest-sort>=1.3.0

src/poodle/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pathlib import Path
99
from typing import Any
1010

11-
__version__ = "1.2.1"
11+
__version__ = "1.2.2"
1212

1313

1414
class PoodleInputError(ValueError):

src/poodle/cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
@click.option("--exclude", help="Add a glob exclude file filter. Multiple allowed.", multiple=True)
2626
@click.option("--only", help="Glob pattern for files to mutate. Multiple allowed.", multiple=True)
2727
@click.option("--report", help="Enable reporter by name. Multiple allowed.", multiple=True)
28+
@click.option("--html", help="Folder name to store HTML report in.", type=click.Path(path_type=Path))
29+
@click.option("--json", help="File to create with JSON report.", type=click.Path(path_type=Path))
2830
def main(
2931
sources: tuple[Path],
3032
config_file: Path | None,
@@ -34,10 +36,12 @@ def main(
3436
exclude: tuple[str],
3537
only: tuple[str],
3638
report: tuple[str],
39+
html: Path | None,
40+
json: Path | None,
3741
) -> None:
3842
"""Poodle Mutation Test Tool."""
3943
try:
40-
config = build_config(sources, config_file, quiet, verbose, workers, exclude, only, report)
44+
config = build_config(sources, config_file, quiet, verbose, workers, exclude, only, report, html, json)
4145
except PoodleInputError as err:
4246
click.echo(err.args)
4347
sys.exit(4)

src/poodle/config.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pathlib import Path
99
from typing import Any
1010

11+
from mergedeep import merge # type: ignore[import-untyped]
1112
from wcmatch import glob
1213

1314
from . import PoodleInputError, poodle_config, tomllib
@@ -24,15 +25,11 @@
2425
default_file_copy_filters = ["__pycache__/**"]
2526
default_work_folder = Path(".poodle-temp")
2627

27-
default_mutator_opts: dict[str, Any] = {}
28-
2928
default_min_timeout = 10
3029
default_timeout_multiplier = 10
3130
default_runner = "command_line"
32-
default_runner_opts: dict[str, Any] = {}
3331

3432
default_reporters = ["summary", "not_found"]
35-
default_reporter_opts: dict[str, Any] = {}
3633

3734

3835
def default_max_workers() -> int:
@@ -54,6 +51,8 @@ def build_config( # noqa: PLR0913
5451
cmd_excludes: tuple[str],
5552
cmd_only_files: tuple[str],
5653
cmd_report: tuple[str],
54+
cmd_html: Path | None,
55+
cmd_json: Path | None,
5756
) -> PoodleConfig:
5857
"""Build PoodleConfig object."""
5958
config_file_path = get_config_file_path(cmd_config_file)
@@ -74,8 +73,11 @@ def build_config( # noqa: PLR0913
7473
# file_filters += get_str_list_from_config("exclude", config_file_data, default=[]) # noqa: ERA001
7574
file_filters += cmd_excludes
7675

77-
reporters = get_str_list_from_config("reporters", config_file_data, default=default_reporters)
78-
reporters += [reporter for reporter in cmd_report if reporter not in reporters]
76+
cmd_reporter_opts: dict[str, Any] = {}
77+
if cmd_html:
78+
merge(cmd_reporter_opts, {"html": {"report_folder": cmd_html}})
79+
if cmd_json:
80+
merge(cmd_reporter_opts, {"json_report_file": cmd_json})
7981

8082
return PoodleConfig(
8183
project_name=get_str_from_config("project_name", config_file_data, default=project_name),
@@ -107,18 +109,34 @@ def build_config( # noqa: PLR0913
107109
command_line=get_cmd_line_echo_enabled(cmd_quiet),
108110
),
109111
echo_no_color=get_bool_from_config("echo_no_color", config_file_data),
110-
mutator_opts=get_dict_from_config("mutator_opts", config_file_data, default=default_mutator_opts),
112+
mutator_opts=get_dict_from_config("mutator_opts", config_file_data),
111113
skip_mutators=get_str_list_from_config("skip_mutators", config_file_data, default=[]),
112114
add_mutators=get_any_list_from_config("add_mutators", config_file_data),
113115
min_timeout=get_int_from_config("min_timeout", config_file_data) or default_min_timeout,
114116
timeout_multiplier=get_int_from_config("timeout_multiplier", config_file_data) or default_timeout_multiplier,
115117
runner=get_str_from_config("runner", config_file_data, default=default_runner),
116-
runner_opts=get_dict_from_config("runner_opts", config_file_data, default=default_runner_opts),
117-
reporters=reporters,
118-
reporter_opts=get_dict_from_config("reporter_opts", config_file_data, default=default_reporter_opts),
118+
runner_opts=get_dict_from_config("runner_opts", config_file_data),
119+
reporters=get_reporters(config_file_data, cmd_report, cmd_html, cmd_json),
120+
reporter_opts=get_dict_from_config("reporter_opts", config_file_data, command_line=cmd_reporter_opts),
119121
)
120122

121123

124+
def get_reporters(
125+
config_file_data: dict,
126+
cmd_report: tuple[str],
127+
cmd_html: Path | None,
128+
cmd_json: Path | None,
129+
) -> list[str]:
130+
"""Retrieve list of reporters to use."""
131+
reporters = get_str_list_from_config("reporters", config_file_data, default=default_reporters)
132+
reporters += [reporter for reporter in cmd_report if reporter not in reporters]
133+
if cmd_html:
134+
reporters.append("html")
135+
if cmd_json:
136+
reporters.append("json")
137+
return reporters
138+
139+
122140
def get_cmd_line_log_level(cmd_quiet: int, cmd_verbose: int) -> int | None:
123141
"""Map verbosity input to logging level."""
124142
if cmd_quiet >= 3:
@@ -493,19 +511,19 @@ def get_dict_from_config(
493511

494512
if option_name in config_data:
495513
try:
496-
option_value.update(config_data[option_name])
497-
except ValueError:
514+
merge(option_value, config_data[option_name])
515+
except TypeError:
498516
msg = f"{option_name} in config file must be a valid dict"
499517
raise PoodleInputError(msg) from None
500518

501519
if hasattr(poodle_config, option_name):
502520
try:
503-
option_value.update(getattr(poodle_config, option_name))
504-
except ValueError:
521+
merge(option_value, getattr(poodle_config, option_name))
522+
except TypeError:
505523
msg = f"poodle_config.{option_name} must be a valid dict"
506524
raise PoodleInputError(msg) from None
507525

508526
if command_line:
509-
option_value.update(command_line)
527+
merge(option_value, command_line)
510528

511529
return option_value

tests/test_cli.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,30 @@ def test_cli_help_report(self, runner: CliRunner):
112112
is not None
113113
)
114114

115+
def test_cli_help_html(self, runner: CliRunner):
116+
result = runner.invoke(cli.main, ["--help"])
117+
assert result.exit_code == 0
118+
assert (
119+
re.match(
120+
r".*--html PATH\s+Folder name to store HTML report in\..*",
121+
result.output,
122+
flags=re.DOTALL,
123+
)
124+
is not None
125+
)
126+
127+
def test_cli_help_json(self, runner: CliRunner):
128+
result = runner.invoke(cli.main, ["--help"])
129+
assert result.exit_code == 0
130+
assert (
131+
re.match(
132+
r".*--json PATH\s+File to create with JSON report\..*",
133+
result.output,
134+
flags=re.DOTALL,
135+
)
136+
is not None
137+
)
138+
115139
def assert_build_config_called_with(
116140
self,
117141
build_config: mock.MagicMock,
@@ -123,6 +147,8 @@ def assert_build_config_called_with(
123147
exclude: tuple[str] = (), # type: ignore [assignment]
124148
only: tuple[str] = (), # type: ignore [assignment]
125149
report: tuple[str] = (), # type: ignore [assignment]
150+
html: Path | None = None,
151+
json: Path | None = None,
126152
):
127153
build_config.assert_called_with(
128154
sources,
@@ -133,6 +159,8 @@ def assert_build_config_called_with(
133159
exclude,
134160
only,
135161
report,
162+
html,
163+
json,
136164
)
137165

138166
def test_cli(self, main_process: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner):
@@ -223,6 +251,18 @@ def test_main_report(self, main_process: mock.MagicMock, build_config: mock.Magi
223251
self.assert_build_config_called_with(build_config, report=("json",))
224252
main_process.assert_called_with(build_config.return_value)
225253

254+
def test_main_html(self, main_process: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner):
255+
result = runner.invoke(cli.main, ["--html", "html_report"])
256+
assert result.exit_code == 0
257+
self.assert_build_config_called_with(build_config, html=Path("html_report"))
258+
main_process.assert_called_with(build_config.return_value)
259+
260+
def test_main_json(self, main_process: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner):
261+
result = runner.invoke(cli.main, ["--json", "summary.json"])
262+
assert result.exit_code == 0
263+
self.assert_build_config_called_with(build_config, json=Path("summary.json"))
264+
main_process.assert_called_with(build_config.return_value)
265+
226266
def test_main_input_error(
227267
self,
228268
main_process: mock.MagicMock,

0 commit comments

Comments
 (0)