Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔧 Error and display format improvements #24

Merged
merged 4 commits into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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