Skip to content

Commit

Permalink
Merge pull request #22 from WiredNerd/dev
Browse files Browse the repository at this point in the history
🐩 New CLI options: --html and --json
  • Loading branch information
WiredNerd authored Jan 4, 2024
2 parents 28efbae + a6f9433 commit a8b7801
Show file tree
Hide file tree
Showing 10 changed files with 449 additions and 196 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/python-platform-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ jobs:
- name: Poodle example project
run : |
cd example
poodle --report summary --report not_found --report json --report html
poodle --report summary --report not_found --json results.json --html html_report
- name: Poodle example flat project
run : |
cd example2
poodle --report summary --report not_found --report json --report html
poodle --report summary --report not_found --json results.json --html html_report
4 changes: 2 additions & 2 deletions .github/workflows/update-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ jobs:
name: Coverage Report
path: cov-html
- name: Poodle
run: poodle --report json --report html
run: poodle --json mutation-testing-report.json --html html-report
- name: Upload Report HTML
if: ${{ always() }}
uses: actions/upload-artifact@v3
with:
name: Mutation testing report HTML
path: mutation_reports
path: html-report
- name: save-json-report
if: ${{ always() && github.ref_name == 'main' }}
uses: EndBug/add-and-commit@v9
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"fromlist",
"getaffinity",
"keepends",
"mergedeep",
"Mult",
"PYTHONDONTWRITEBYTECODE",
"redef",
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "poodle"
description = "Mutation Testing Tool"
version = "1.2.1"
version = "1.2.2"
license = { file = "LICENSE" }
keywords = [
"test",
Expand Down Expand Up @@ -33,6 +33,7 @@ dependencies = [
"wcmatch>=8.5",
"tomli>=2; python_version<'3.11'",
"jinja2>=3.0.3",
"mergedeep>=1.0",
]

[project.urls]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ tomli>=2
wcmatch>=8.5
jinja2>=3.0.3
pysass
mergedeep

pytest
pytest-sort>=1.3.0
Expand Down
2 changes: 1 addition & 1 deletion src/poodle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pathlib import Path
from typing import Any

__version__ = "1.2.1"
__version__ = "1.2.2"


class PoodleInputError(ValueError):
Expand Down
6 changes: 5 additions & 1 deletion src/poodle/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
@click.option("--exclude", help="Add a glob exclude file filter. Multiple allowed.", multiple=True)
@click.option("--only", help="Glob pattern for files to mutate. Multiple allowed.", multiple=True)
@click.option("--report", help="Enable reporter by name. Multiple allowed.", multiple=True)
@click.option("--html", help="Folder name to store HTML report in.", type=click.Path(path_type=Path))
@click.option("--json", help="File to create with JSON report.", type=click.Path(path_type=Path))
def main(
sources: tuple[Path],
config_file: Path | None,
Expand All @@ -34,10 +36,12 @@ def main(
exclude: tuple[str],
only: tuple[str],
report: tuple[str],
html: Path | None,
json: Path | None,
) -> None:
"""Poodle Mutation Test Tool."""
try:
config = build_config(sources, config_file, quiet, verbose, workers, exclude, only, report)
config = build_config(sources, config_file, quiet, verbose, workers, exclude, only, report, html, json)
except PoodleInputError as err:
click.echo(err.args)
sys.exit(4)
Expand Down
48 changes: 33 additions & 15 deletions src/poodle/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path
from typing import Any

from mergedeep import merge # type: ignore[import-untyped]
from wcmatch import glob

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

default_mutator_opts: dict[str, Any] = {}

default_min_timeout = 10
default_timeout_multiplier = 10
default_runner = "command_line"
default_runner_opts: dict[str, Any] = {}

default_reporters = ["summary", "not_found"]
default_reporter_opts: dict[str, Any] = {}


def default_max_workers() -> int:
Expand All @@ -54,6 +51,8 @@ def build_config( # noqa: PLR0913
cmd_excludes: tuple[str],
cmd_only_files: tuple[str],
cmd_report: tuple[str],
cmd_html: Path | None,
cmd_json: Path | None,
) -> PoodleConfig:
"""Build PoodleConfig object."""
config_file_path = get_config_file_path(cmd_config_file)
Expand All @@ -74,8 +73,11 @@ def build_config( # noqa: PLR0913
# file_filters += get_str_list_from_config("exclude", config_file_data, default=[]) # noqa: ERA001
file_filters += cmd_excludes

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

return PoodleConfig(
project_name=get_str_from_config("project_name", config_file_data, default=project_name),
Expand Down Expand Up @@ -107,18 +109,34 @@ def build_config( # noqa: PLR0913
command_line=get_cmd_line_echo_enabled(cmd_quiet),
),
echo_no_color=get_bool_from_config("echo_no_color", config_file_data),
mutator_opts=get_dict_from_config("mutator_opts", config_file_data, default=default_mutator_opts),
mutator_opts=get_dict_from_config("mutator_opts", config_file_data),
skip_mutators=get_str_list_from_config("skip_mutators", config_file_data, default=[]),
add_mutators=get_any_list_from_config("add_mutators", config_file_data),
min_timeout=get_int_from_config("min_timeout", config_file_data) or default_min_timeout,
timeout_multiplier=get_int_from_config("timeout_multiplier", config_file_data) or default_timeout_multiplier,
runner=get_str_from_config("runner", config_file_data, default=default_runner),
runner_opts=get_dict_from_config("runner_opts", config_file_data, default=default_runner_opts),
reporters=reporters,
reporter_opts=get_dict_from_config("reporter_opts", config_file_data, default=default_reporter_opts),
runner_opts=get_dict_from_config("runner_opts", config_file_data),
reporters=get_reporters(config_file_data, cmd_report, cmd_html, cmd_json),
reporter_opts=get_dict_from_config("reporter_opts", config_file_data, command_line=cmd_reporter_opts),
)


def get_reporters(
config_file_data: dict,
cmd_report: tuple[str],
cmd_html: Path | None,
cmd_json: Path | None,
) -> list[str]:
"""Retrieve list of reporters to use."""
reporters = get_str_list_from_config("reporters", config_file_data, default=default_reporters)
reporters += [reporter for reporter in cmd_report if reporter not in reporters]
if cmd_html:
reporters.append("html")
if cmd_json:
reporters.append("json")
return reporters


def get_cmd_line_log_level(cmd_quiet: int, cmd_verbose: int) -> int | None:
"""Map verbosity input to logging level."""
if cmd_quiet >= 3:
Expand Down Expand Up @@ -493,19 +511,19 @@ def get_dict_from_config(

if option_name in config_data:
try:
option_value.update(config_data[option_name])
except ValueError:
merge(option_value, config_data[option_name])
except TypeError:
msg = f"{option_name} in config file must be a valid dict"
raise PoodleInputError(msg) from None

if hasattr(poodle_config, option_name):
try:
option_value.update(getattr(poodle_config, option_name))
except ValueError:
merge(option_value, getattr(poodle_config, option_name))
except TypeError:
msg = f"poodle_config.{option_name} must be a valid dict"
raise PoodleInputError(msg) from None

if command_line:
option_value.update(command_line)
merge(option_value, command_line)

return option_value
40 changes: 40 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,30 @@ def test_cli_help_report(self, runner: CliRunner):
is not None
)

def test_cli_help_html(self, runner: CliRunner):
result = runner.invoke(cli.main, ["--help"])
assert result.exit_code == 0
assert (
re.match(
r".*--html PATH\s+Folder name to store HTML report in\..*",
result.output,
flags=re.DOTALL,
)
is not None
)

def test_cli_help_json(self, runner: CliRunner):
result = runner.invoke(cli.main, ["--help"])
assert result.exit_code == 0
assert (
re.match(
r".*--json PATH\s+File to create with JSON report\..*",
result.output,
flags=re.DOTALL,
)
is not None
)

def assert_build_config_called_with(
self,
build_config: mock.MagicMock,
Expand All @@ -123,6 +147,8 @@ def assert_build_config_called_with(
exclude: tuple[str] = (), # type: ignore [assignment]
only: tuple[str] = (), # type: ignore [assignment]
report: tuple[str] = (), # type: ignore [assignment]
html: Path | None = None,
json: Path | None = None,
):
build_config.assert_called_with(
sources,
Expand All @@ -133,6 +159,8 @@ def assert_build_config_called_with(
exclude,
only,
report,
html,
json,
)

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

def test_main_html(self, main_process: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner):
result = runner.invoke(cli.main, ["--html", "html_report"])
assert result.exit_code == 0
self.assert_build_config_called_with(build_config, html=Path("html_report"))
main_process.assert_called_with(build_config.return_value)

def test_main_json(self, main_process: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner):
result = runner.invoke(cli.main, ["--json", "summary.json"])
assert result.exit_code == 0
self.assert_build_config_called_with(build_config, json=Path("summary.json"))
main_process.assert_called_with(build_config.return_value)

def test_main_input_error(
self,
main_process: mock.MagicMock,
Expand Down
Loading

0 comments on commit a8b7801

Please sign in to comment.