Skip to content

Commit

Permalink
🔧 Fix problems with generating Unified Diff
Browse files Browse the repository at this point in the history
  • Loading branch information
WiredNerd committed Jan 8, 2024
1 parent bf35be0 commit 6f91038
Show file tree
Hide file tree
Showing 16 changed files with 174 additions and 73 deletions.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ filterwarnings = [
[tool.poodle]
file_filters = ["test_*.py", "*_test.py", 'cli.py', 'run.py', '__init__.py']
reporters = ["summary", "html"]
# skip_delete_folder = true
skip_mutators = ["all"]
add_mutators = ["Decorator"]

[tool.poodle.runner_opts]
command_line = "pytest -x --assert=plain -o pythonpath='{PYTHONPATH}' --sort-mode=mutcov"
Expand Down
1 change: 1 addition & 0 deletions src/poodle/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def build_config( # noqa: PLR0913
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),
fail_under=get_float_from_config("fail_under", config_file_data, command_line=cmd_fail_under),
skip_delete_folder=get_bool_from_config("skip_delete_folder", config_file_data, default=False),
)


Expand Down
26 changes: 11 additions & 15 deletions src/poodle/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,13 @@
from __future__ import annotations

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, display_percent, pprint_str

if TYPE_CHECKING:
from pathlib import Path
from .util import calc_timeout, create_temp_zips, create_unified_diff, delete_folder, display_percent, pprint_str

logger = logging.getLogger(__name__)

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

delete_folder(config.work_folder)
delete_folder(config.work_folder, config)
create_temp_zips(work)

work.mutators = initialize_mutators(work)
work.runner = get_runner(config)
work.reporters = list(generate_reporters(config))

mutants = create_mutants_for_all_mutators(work)
mutants.sort(
key=lambda mutant: (
mutant.source_folder,
str(mutant.source_file) or "",
mutant.lineno,
mutant.mutator_name,
)
)
if not mutants:
raise PoodleNoMutantsFoundError("No mutants were found to test!")
work.echo(f"Identified {len(mutants)} mutants")
Expand All @@ -47,7 +50,7 @@ def main_process(config: PoodleConfig) -> None:
for reporter in work.reporters:
reporter(config=config, echo=work.echo, testing_results=results)

delete_folder(config.work_folder)
delete_folder(config.work_folder, config)

if config.fail_under and results.summary.success_rate < config.fail_under / 100:
display_fail_under = display_percent(config.fail_under / 100)
Expand Down Expand Up @@ -80,10 +83,3 @@ def print_header(work: PoodleWork) -> None:
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)
3 changes: 2 additions & 1 deletion src/poodle/data_types/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ class PoodleConfig:
file_copy_flags: int | None
file_copy_filters: list[str]
work_folder: Path

max_workers: int | None

log_format: str
Expand All @@ -65,6 +64,8 @@ class PoodleConfig:

fail_under: float | None

skip_delete_folder: bool | None


@dataclass
class FileMutation:
Expand Down
8 changes: 8 additions & 0 deletions src/poodle/data_types/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ def is_annotation(cls, node: ast.AST, child_node: ast.AST | None = None) -> bool

return cls.is_annotation(node.parent, child_node=node)

@classmethod
def unparse(cls, node: ast.AST, indent: int) -> str:
"""Unparse AST node to string. Indent any lines that are not the first line."""
lines = ast.unparse(node).splitlines(keepends=True)
if len(lines) > 1:
lines[1:] = [f"{' ' * indent}{line}" for line in lines[1:]]
return "".join(lines)


# runner method signature:
def runner( # type: ignore [empty-body]
Expand Down
10 changes: 7 additions & 3 deletions src/poodle/mutate.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@

def initialize_mutators(work: PoodleWork) -> list[Callable | Mutator]:
"""Initialize all mutators from standard list and from config options."""
mutators: list[Any] = [
mutator for name, mutator in builtin_mutators.items() if name not in work.config.skip_mutators
]
skip_mutators = [name.lower() for name in work.config.skip_mutators]
mutators: list[Any] = []
if "all" not in skip_mutators:
mutators.extend([mutator for name, mutator in builtin_mutators.items() if name.lower() not in skip_mutators])
mutators.extend(work.config.add_mutators)

return [initialize_mutator(work, mut_def) for mut_def in mutators]
Expand All @@ -64,6 +65,9 @@ def initialize_mutator(work: PoodleWork, mutator_def: Any) -> Callable | Mutator
"""
logger.debug(mutator_def)

if mutator_def in builtin_mutators:
mutator_def = builtin_mutators[mutator_def]

if isinstance(mutator_def, str):
try:
mutator_def = dynamic_import(mutator_def)
Expand Down
2 changes: 1 addition & 1 deletion src/poodle/mutators/calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,4 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
for idx in range(len(node.decorator_list)):
new_node = deepcopy(node)
new_node.decorator_list.pop(idx)
self.mutants.append(self.create_file_mutation(node, ast.unparse(new_node)))
self.mutants.append(self.create_file_mutation(node, self.unparse(new_node, new_node.col_offset)))
5 changes: 2 additions & 3 deletions src/poodle/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import concurrent.futures
import logging
import shutil
import time
from typing import TYPE_CHECKING, Callable
from zipfile import ZipFile
Expand All @@ -14,7 +13,7 @@
from . import PoodleTrialRunError
from .data_types import Mutant, MutantTrial, MutantTrialResult, PoodleConfig, PoodleWork, TestingResults, TestingSummary
from .runners import command_line
from .util import dynamic_import, mutate_lines
from .util import delete_folder, dynamic_import, mutate_lines

if TYPE_CHECKING:
from pathlib import Path
Expand Down Expand Up @@ -176,7 +175,7 @@ def run_mutant_trial( # noqa: PLR0913
timeout=timeout,
)

shutil.rmtree(run_folder)
delete_folder(run_folder, config)

duration = time.time() - start
logger.debug("END: run_id=%s - Elapsed Time %.2f s", run_id, duration)
Expand Down
11 changes: 10 additions & 1 deletion src/poodle/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import difflib
import json
import logging
import shutil
from copy import deepcopy
from io import StringIO
from pprint import pprint
Expand Down Expand Up @@ -107,10 +108,11 @@ def create_unified_diff(mutant: Mutant) -> str | None:
if mutant.source_file:
file_lines = mutant.source_file.read_text("utf-8").splitlines(keepends=True)
file_name = str(mutant.source_file)
mutant_lines = "".join(mutate_lines(mutant, file_lines)).splitlines(keepends=True)
return "".join(
difflib.unified_diff(
a=file_lines,
b=mutate_lines(mutant, file_lines),
b=mutant_lines,
fromfile=file_name,
tofile=f"[Mutant] {file_name}:{mutant.lineno}",
)
Expand All @@ -131,3 +133,10 @@ def from_json(data: str, datatype: type[PoodleSerialize]) -> PoodleSerialize:
def display_percent(value: float) -> str:
"""Convert float to string with percent sign."""
return f"{value * 1000 // 1 / 10:.3g}%"


def delete_folder(folder: pathlib.Path, config: PoodleConfig) -> None:
"""Delete a folder."""
if folder.exists() and not config.skip_delete_folder:
logger.info("delete %s", folder)
shutil.rmtree(folder)
3 changes: 3 additions & 0 deletions tests/data_types/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class PoodleConfigStub(PoodleConfig):

fail_under: float | None = None

skip_delete_folder: bool = False


class TestPoodleConfig:
@staticmethod
Expand Down Expand Up @@ -84,6 +86,7 @@ def create_poodle_config():
reporters=["summary"],
reporter_opts={"summary": "value"},
fail_under=95.0,
skip_delete_folder=False,
)

def test_poodle_config(self):
Expand Down
27 changes: 13 additions & 14 deletions tests/mutators/test_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,27 +183,26 @@ def test_decorator_mutator_single(self, mock_echo):
mutator = DecoratorMutator(config=config, echo=mock_echo, other="value")
module = "\n".join( # noqa: FLY002
[
"@dec.abc",
"def example(y):",
" return y",
"class Example:",
" @dec.abc",
" def example(y):",
" return y",
],
)
file_mutants = mutator.create_mutations(ast.parse(module))

assert len(file_mutants) == 1

for mut in file_mutants:
assert mut.lineno == 1
assert mut.end_lineno == 3
assert mut.col_offset == 0
assert mut.end_col_offset == 12

assert file_mutants[0].text == "\n".join( # noqa: FLY002
[
"def example(y):",
" return y",
]
)
assert mut.lineno == 2
assert mut.end_lineno == 4
assert mut.col_offset == 4
assert mut.end_col_offset == 16

assert file_mutants[0].text.splitlines() == [
"def example(y):",
" return y",
]

def test_decorator_mutator_multi(self, mock_echo):
config = mock.MagicMock(mutator_opts={})
Expand Down
8 changes: 8 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,13 @@ def test_build_config_fail_under(self, get_float_from_config, get_config_file_da
assert config_data.fail_under == get_float_from_config.return_value
get_float_from_config.assert_any_call("fail_under", config_file_data, command_line=50)

@pytest.mark.usefixtures("_setup_build_config_mocks")
def test_build_config_skip_delete_folder(self, get_bool_from_config, get_config_file_data):
config_file_data = get_config_file_data.return_value
config_data = self.build_config_with()
assert config_data.skip_delete_folder == get_bool_from_config.return_value
get_bool_from_config.assert_any_call("skip_delete_folder", config_file_data, default=False)

@mock.patch("poodle.config.get_config_file_data")
@mock.patch("poodle.config.get_project_info")
def test_build_config_defaults(self, get_project_info, get_config_file_data):
Expand Down Expand Up @@ -504,6 +511,7 @@ def test_build_config_defaults(self, get_project_info, get_config_file_data):
reporters=["summary", "not_found"],
reporter_opts={},
fail_under=None,
skip_delete_folder=False,
)


Expand Down
27 changes: 1 addition & 26 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def test_main_process_setup(
pprint_str.assert_called_once_with(config)
logger_mock.info.assert_called_once_with("\n%s", pprint_str.return_value)

delete_folder.assert_called_with(config.work_folder)
delete_folder.assert_called_with(config.work_folder, config)
assert delete_folder.call_count == 2

create_temp_zips.assert_called_once_with(work)
Expand Down Expand Up @@ -350,28 +350,3 @@ def test_print_header_goal(self):
mock.call(),
]
)


class TestDeleteFolder:
@pytest.fixture()
def shutil(self):
with mock.patch("poodle.core.shutil") as shutil:
yield shutil

def test_delete_folder_exists(self, shutil, logger_mock):
folder = mock.MagicMock()
folder.exists.return_value = True

core.delete_folder(folder)

logger_mock.info.assert_called_once_with("delete %s", folder)
shutil.rmtree.assert_called_once_with(folder)

def test_delete_folder_not_exists(self, shutil, logger_mock):
folder = mock.MagicMock()
folder.exists.return_value = False

core.delete_folder(folder)

logger_mock.info.assert_not_called()
shutil.rmtree.assert_not_called()
37 changes: 34 additions & 3 deletions tests/test_mutate.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import importlib
from unittest import mock

import pytest
Expand All @@ -7,6 +8,11 @@
from tests.data_types.test_data import PoodleConfigStub


@pytest.fixture(autouse=True)
def _reset():
importlib.reload(mutate)


@pytest.fixture()
def logger_mock():
with mock.patch("poodle.mutate.logger") as logger_mock:
Expand Down Expand Up @@ -36,16 +42,41 @@ def fake_mutator(*_, **__) -> list[FileMutation]:


class TestInit:
@mock.patch("poodle.mutate.builtin_mutators")
def test_initialize_mutators(self, builtin_mutators):
def test_initialize_mutators(self):
config = PoodleConfigStub(skip_mutators=["m2"], add_mutators=["tests.test_mutate.fake_mutator"])
work = PoodleWork(config)
m1 = FakeMutator(config, echo=mock.MagicMock())
m2 = FakeMutator(config, echo=mock.MagicMock())
m3 = FakeMutator(config, echo=mock.MagicMock())
builtin_mutators.items.return_value = [("m1", m1), ("m2", m2), ("m3", m3)]
mutate.builtin_mutators = {"m1": m1, "m2": m2, "m3": m3}
assert mutate.initialize_mutators(work) == [m1, m3, fake_mutator]

def test_initialize_mutators_lower(self):
config = PoodleConfigStub(skip_mutators=["M1", "m2"], add_mutators=["tests.test_mutate.fake_mutator"])
work = PoodleWork(config)
m1 = FakeMutator(config, echo=mock.MagicMock())
m2 = FakeMutator(config, echo=mock.MagicMock())
m3 = FakeMutator(config, echo=mock.MagicMock())
mutate.builtin_mutators = {"m1": m1, "m2": m2, "m3": m3}
assert mutate.initialize_mutators(work) == [m3, fake_mutator]

def test_initialize_mutators_all(self):
config = PoodleConfigStub(skip_mutators=["ALL"], add_mutators=["m2", "tests.test_mutate.fake_mutator"])
work = PoodleWork(config)
m1 = FakeMutator(config, echo=mock.MagicMock())
m2 = FakeMutator(config, echo=mock.MagicMock())
m3 = FakeMutator(config, echo=mock.MagicMock())
mutate.builtin_mutators = {"m1": m1, "m2": m2, "m3": m3}
assert mutate.initialize_mutators(work) == [m2, fake_mutator]

def test_initialize_mutator_builtin(self, logger_mock):
config = PoodleConfigStub()
work = PoodleWork(config)
m1 = FakeMutator(config, echo=mock.MagicMock())
mutate.builtin_mutators = {"m1": m1}
assert mutate.initialize_mutator(work, "m1") == m1
logger_mock.debug.assert_called_with("m1")

def test_initialize_mutator_str_class(self, logger_mock):
config = PoodleConfigStub()
work = PoodleWork(config)
Expand Down
Loading

0 comments on commit 6f91038

Please sign in to comment.