Skip to content

Commit

Permalink
Merge pull request #24 from WiredNerd/dev
Browse files Browse the repository at this point in the history
🔧 Error and display format improvements
  • Loading branch information
WiredNerd authored Jan 6, 2024
2 parents 42046f5 + ef85b1e commit f1d0e0a
Show file tree
Hide file tree
Showing 14 changed files with 400 additions and 280 deletions.
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,18 @@ Usage: poodle [OPTIONS] [SOURCES]...
Poodle Mutation Test Tool.
Options:
-c PATH Configuration File.
-q Quiet mode: q, qq, or qqq
-v Verbose mode: v, vv, or vvv
-w INTEGER Maximum number of parallel workers.
--exclude TEXT Add a glob exclude file filter. Multiple allowed.
--only TEXT Glob pattern for files to mutate. Multiple allowed.
--report TEXT Enable reporter by name. Multiple allowed.
--help Show this message and exit.
-c PATH Configuration File.
-q Quiet mode: q, qq, or qqq
-v Verbose mode: v, vv, or vvv
-w INTEGER Maximum number of parallel workers.
--exclude TEXT Add a glob exclude file filter. Multiple allowed.
--only TEXT Glob pattern for files to mutate. Multiple allowed.
--report TEXT Enable reporter by name. Multiple allowed.
--html PATH Folder name to store HTML report in.
--json PATH File to create with JSON report.
--fail_under FLOAT Fail if mutation score is under this value.
--version Show the version and exit.
--help Show this message and exit.
```

## Documentation:
Expand Down
2 changes: 1 addition & 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.3.0"
version = "1.3.1"
license = { file = "LICENSE" }
keywords = [
"test",
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.3.0"
__version__ = "1.3.1"


class PoodleTestingFailedError(Exception):
Expand Down
16 changes: 8 additions & 8 deletions src/poodle/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,33 +56,33 @@ def main( # noqa: C901, PLR0912
)
except PoodleInputError as err:
for arg in err.args:
click.echo(arg)
click.secho(arg, fg="red")
sys.exit(4)

try:
core.main_process(config)
except PoodleTestingFailedError as err:
for arg in err.args:
click.echo(arg)
click.secho(arg, fg="yellow")
sys.exit(1)
except KeyboardInterrupt:
click.echo("Aborted due to Keyboard Interrupt!")
click.secho("Aborted due to Keyboard Interrupt!", fg="yellow")
sys.exit(2)
except PoodleTrialRunError as err:
for arg in err.args:
click.echo(arg)
click.secho(arg, fg="red")
sys.exit(3)
except PoodleInputError as err:
for arg in err.args:
click.echo(arg)
click.secho(arg, fg="red")
sys.exit(4)
except PoodleNoMutantsFoundError as err:
for arg in err.args:
click.echo(arg)
click.secho(arg, fg="yellow")
sys.exit(5)
except: # noqa: E722
click.echo("Aborted due to Internal Error!")
click.echo(traceback.format_exc())
click.secho("Aborted due to Internal Error!", fg="red")
click.secho(traceback.format_exc(), fg="red")
sys.exit(3)
sys.exit(0)

Expand Down
20 changes: 14 additions & 6 deletions src/poodle/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,16 +230,24 @@ def get_project_info(config_file: Path | None) -> tuple[str, str]:

def get_config_file_data_toml(config_file: Path) -> dict:
"""Retrieve Poodle configuration from a 'toml' Config File."""
config_data = tomllib.load(config_file.open(mode="rb"))
config_data: dict = config_data.get("tool", config_data) # type: ignore[no-redef]
return config_data.get("poodle", {})
try:
config_data = tomllib.load(config_file.open(mode="rb"))
config_data: dict = config_data.get("tool", config_data) # type: ignore[no-redef]
return config_data.get("poodle", {})
except tomllib.TOMLDecodeError as err:
msgs = [f"Error decoding toml file: {config_file}"]
msgs.extend(err.args)
raise PoodleInputError(*msgs) from None


def get_project_info_toml(config_file: Path) -> tuple[str, str]:
"""Retrieve Project Name and Version from a 'toml' Config File."""
config_data = tomllib.load(config_file.open(mode="rb"))
config_data: dict = config_data.get("project", config_data) # type: ignore[no-redef]
return config_data.get("name", ""), config_data.get("version", "")
try:
config_data = tomllib.load(config_file.open(mode="rb"))
config_data: dict = config_data.get("project", config_data) # type: ignore[no-redef]
return config_data.get("name", ""), config_data.get("version", "")
except tomllib.TOMLDecodeError:
return "", ""


def get_source_folders(command_line_sources: tuple[Path], config_data: dict) -> list[Path]:
Expand Down
28 changes: 19 additions & 9 deletions src/poodle/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@

import logging
import shutil
from typing import TYPE_CHECKING

from . import PoodleNoMutantsFoundError, PoodleTestingFailedError, __version__
from .data_types import PoodleConfig, PoodleWork
from .mutate import create_mutants_for_all_mutators, initialize_mutators
from .report import generate_reporters
from .run import clean_run_each_source_folder, get_runner, run_mutant_trails
from .util import calc_timeout, create_temp_zips, create_unified_diff, pprint_str
from .util import calc_timeout, create_temp_zips, create_unified_diff, display_percent, pprint_str

if TYPE_CHECKING:
from pathlib import Path

logger = logging.getLogger(__name__)

Expand All @@ -21,10 +25,7 @@ def main_process(config: PoodleConfig) -> None:
print_header(work)
logger.info("\n%s", pprint_str(config))

if config.work_folder.exists():
logger.info("delete %s", config.work_folder)
shutil.rmtree(config.work_folder)

delete_folder(config.work_folder)
create_temp_zips(work)

work.mutators = initialize_mutators(work)
Expand All @@ -46,11 +47,11 @@ def main_process(config: PoodleConfig) -> None:
for reporter in work.reporters:
reporter(config=config, echo=work.echo, testing_results=results)

logger.info("delete %s", config.work_folder)
shutil.rmtree(config.work_folder)
delete_folder(config.work_folder)

if config.fail_under and results.summary.success_rate < config.fail_under / 100:
msg = f"Mutation score {results.summary.coverage_display} is below {config.fail_under:.2f}%"
display_fail_under = display_percent(config.fail_under / 100)
msg = f"Mutation score {results.summary.coverage_display} is below goal of {display_fail_under}"
raise PoodleTestingFailedError(msg)


Expand All @@ -69,11 +70,20 @@ def main_process(config: PoodleConfig) -> None:

def print_header(work: PoodleWork) -> None:
"""Print a header to the console."""
work.echo(poodle_header_str.format(version=__version__), fg="blue")
work.echo(poodle_header_str.format(version=__version__), fg="cyan")
work.echo("Running with the following configuration:")
work.echo(f" - Source Folders: {[str(folder) for folder in work.config.source_folders]}")
work.echo(f" - Config File: {work.config.config_file}")
work.echo(f" - Max Workers: {work.config.max_workers}")
work.echo(f" - Runner: {work.config.runner}")
work.echo(f" - Reporters: {work.config.reporters}")
if work.config.fail_under:
work.echo(f" - Coverage Goal: {work.config.fail_under:.2f}%")
work.echo()


def delete_folder(folder: Path) -> None:
"""Delete a folder."""
if folder.exists():
logger.info("delete %s", folder)
shutil.rmtree(folder)
4 changes: 3 additions & 1 deletion src/poodle/data_types/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

from poodle import util

if TYPE_CHECKING:
from typing_extensions import Self

Expand Down Expand Up @@ -165,7 +167,7 @@ def success_rate(self) -> float:
@property
def coverage_display(self) -> str:
"""Return a formatted string for the coverage percentage."""
return f"{self.success_rate * 100:.2f}%"
return util.display_percent(self.success_rate)

def __iadd__(self, result: MutantTrialResult) -> Self:
"""Update Testing Summary with data from MutantTrialResult."""
Expand Down
5 changes: 5 additions & 0 deletions src/poodle/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,8 @@ def to_json(obj: PoodleSerialize, indent: int | str | None = None) -> str:
def from_json(data: str, datatype: type[PoodleSerialize]) -> PoodleSerialize:
"""Convert json string to dataclass."""
return datatype(**json.loads(data, object_hook=datatype.from_dict))


def display_percent(value: float) -> str:
"""Convert float to string with percent sign."""
return f"{value * 1000 // 1 / 10:.3g}%"
4 changes: 2 additions & 2 deletions tests/data_types/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ def test_success_rate_zero(self):

def test_coverage_display(self):
summary = TestingSummary(trials=9, found=6)
assert summary.coverage_display == "66.67%"
assert summary.coverage_display == "66.6%"

def test_iadd(self):
summary = TestingSummary(trials=10)
Expand Down Expand Up @@ -438,7 +438,7 @@ def summary_dict(self):
"timeout": 6,
"errors": 5,
"success_rate": 0.8,
"coverage_display": "80.00%",
"coverage_display": "80%",
}

def test_serialize(self):
Expand Down
56 changes: 28 additions & 28 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ def build_config():


@pytest.fixture()
def echo():
with mock.patch("poodle.cli.click.echo") as echo:
yield echo
def secho():
with mock.patch("poodle.cli.click.secho") as secho:
yield secho


@pytest.fixture()
Expand Down Expand Up @@ -295,108 +295,108 @@ class TestErrors:
def test_main_build_config_input_error(
self,
main_process: mock.MagicMock,
echo: mock.MagicMock,
secho: mock.MagicMock,
build_config: mock.MagicMock,
runner: CliRunner,
):
build_config.side_effect = PoodleInputError("bad input", "input error")
result = runner.invoke(cli.main, [])
assert result.exit_code == 4
build_config.assert_called()
echo.assert_has_calls(
secho.assert_has_calls(
[
mock.call("bad input"),
mock.call("input error"),
mock.call("bad input", fg="red"),
mock.call("input error", fg="red"),
]
)
main_process.assert_not_called()

def test_main_process_testing_failed(
self,
main_process: mock.MagicMock,
echo: mock.MagicMock,
secho: mock.MagicMock,
build_config: mock.MagicMock,
runner: CliRunner,
):
main_process.side_effect = PoodleTestingFailedError("testing failed", "error message")
result = runner.invoke(cli.main, [])
assert result.exit_code == 1
build_config.assert_called()
echo.assert_has_calls(
secho.assert_has_calls(
[
mock.call("testing failed"),
mock.call("error message"),
mock.call("testing failed", fg="yellow"),
mock.call("error message", fg="yellow"),
]
)
main_process.assert_called_with(build_config.return_value)

def test_main_process_keyboard_interrupt(
self,
main_process: mock.MagicMock,
echo: mock.MagicMock,
secho: mock.MagicMock,
build_config: mock.MagicMock,
runner: CliRunner,
):
main_process.side_effect = KeyboardInterrupt()
result = runner.invoke(cli.main, [])
assert result.exit_code == 2
build_config.assert_called()
echo.assert_called_once_with("Aborted due to Keyboard Interrupt!")
secho.assert_called_once_with("Aborted due to Keyboard Interrupt!", fg="yellow")
main_process.assert_called_with(build_config.return_value)

def test_main_process_trial_run_error(
self,
main_process: mock.MagicMock,
echo: mock.MagicMock,
secho: mock.MagicMock,
build_config: mock.MagicMock,
runner: CliRunner,
):
main_process.side_effect = PoodleTrialRunError("testing failed", "error message")
result = runner.invoke(cli.main, [])
assert result.exit_code == 3
build_config.assert_called()
echo.assert_has_calls(
secho.assert_has_calls(
[
mock.call("testing failed"),
mock.call("error message"),
mock.call("testing failed", fg="red"),
mock.call("error message", fg="red"),
]
)
main_process.assert_called_with(build_config.return_value)

def test_main_process_input_error(
self,
main_process: mock.MagicMock,
echo: mock.MagicMock,
secho: mock.MagicMock,
build_config: mock.MagicMock,
runner: CliRunner,
):
main_process.side_effect = PoodleInputError("testing failed", "error message")
result = runner.invoke(cli.main, [])
assert result.exit_code == 4
build_config.assert_called()
echo.assert_has_calls(
secho.assert_has_calls(
[
mock.call("testing failed"),
mock.call("error message"),
mock.call("testing failed", fg="red"),
mock.call("error message", fg="red"),
]
)
main_process.assert_called_with(build_config.return_value)

def test_main_process_no_mutants_error(
self,
main_process: mock.MagicMock,
echo: mock.MagicMock,
secho: mock.MagicMock,
build_config: mock.MagicMock,
runner: CliRunner,
):
main_process.side_effect = PoodleNoMutantsFoundError("testing failed", "error message")
result = runner.invoke(cli.main, [])
assert result.exit_code == 5
build_config.assert_called()
echo.assert_has_calls(
secho.assert_has_calls(
[
mock.call("testing failed"),
mock.call("error message"),
mock.call("testing failed", fg="yellow"),
mock.call("error message", fg="yellow"),
]
)
main_process.assert_called_with(build_config.return_value)
Expand All @@ -406,14 +406,14 @@ def test_main_process_other_error(
self,
traceback: mock.MagicMock,
main_process: mock.MagicMock,
echo: mock.MagicMock,
secho: mock.MagicMock,
build_config: mock.MagicMock,
runner: CliRunner,
):
main_process.side_effect = Exception()
result = runner.invoke(cli.main, [])
assert result.exit_code == 3
build_config.assert_called()
echo.assert_any_call("Aborted due to Internal Error!")
echo.assert_any_call(traceback.format_exc.return_value)
secho.assert_any_call("Aborted due to Internal Error!", fg="red")
secho.assert_any_call(traceback.format_exc.return_value, fg="red")
main_process.assert_called_with(build_config.return_value)
Loading

0 comments on commit f1d0e0a

Please sign in to comment.