diff --git a/ARCHITECTURE.rst b/ARCHITECTURE.rst index 8d1f04a7..e38562bc 100644 --- a/ARCHITECTURE.rst +++ b/ARCHITECTURE.rst @@ -11,7 +11,7 @@ Generating mutants This phase creates a ``./mutants/`` directory, which will be used by all following phases. -We start by copying ``paths_to_mutate`` to ``mutants/`` and then mutate the ``*.py`` files in there. Finally, we also copy ``also_copy`` paths to ``mutants/``, including the (guessed) test directories and some project files. +We start by copying ``source_paths`` to ``mutants/`` and then mutate the ``*.py`` files in there. Finally, we also copy ``also_copy`` paths to ``mutants/``, including the (guessed) test directories and some project files. The mutated files contains the original code and the mutants. With the ``MUTANT_UNDER_TEST`` environment variable, we can specify (among other things) which mutant should be enabled. If a mutant is not enabled, it will run the original code. diff --git a/README.rst b/README.rst index e4de28ee..b226758c 100644 --- a/README.rst +++ b/README.rst @@ -88,7 +88,7 @@ In `setup.cfg` in the root of your project you can configure mutmut if you need .. code-block:: ini [mutmut] - paths_to_mutate=src/ + source_paths=src/ pytest_add_cli_args_test_selection=tests/ If you use `pyproject.toml`, you must specify the paths as array in a `tool.mutmut` section: @@ -96,7 +96,7 @@ If you use `pyproject.toml`, you must specify the paths as array in a `tool.mutm .. code-block:: toml [tool.mutmut] - paths_to_mutate = [ "src/" ] + source_paths = [ "src/" ] pytest_add_cli_args_test_selection= [ "tests/" ] See below for more options for configuring mutmut. @@ -143,10 +143,14 @@ caught. Exclude files from mutation ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +By default mutmut mutates all python files in `source_paths`. You can exclude files from mutation in `setup.cfg`: .. code-block:: + only_mutate= + src/api/* + src/services/* do_not_mutate= *__tests.py diff --git a/e2e_projects/config/config_pkg/logic/__init__.py b/e2e_projects/config/config_pkg/logic/__init__.py new file mode 100644 index 00000000..74b0dc93 --- /dev/null +++ b/e2e_projects/config/config_pkg/logic/__init__.py @@ -0,0 +1,2 @@ +def hello() -> str: + return "Hello from config!" diff --git a/e2e_projects/config/config_pkg/ignore_me.py b/e2e_projects/config/config_pkg/logic/ignore_me.py similarity index 100% rename from e2e_projects/config/config_pkg/ignore_me.py rename to e2e_projects/config/config_pkg/logic/ignore_me.py diff --git a/e2e_projects/config/config_pkg/math.py b/e2e_projects/config/config_pkg/logic/math.py similarity index 100% rename from e2e_projects/config/config_pkg/math.py rename to e2e_projects/config/config_pkg/logic/math.py diff --git a/e2e_projects/config/config_pkg/utils/__init__.py b/e2e_projects/config/config_pkg/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/e2e_projects/config/config_pkg/utils/utils.py b/e2e_projects/config/config_pkg/utils/utils.py new file mode 100644 index 00000000..69b940ac --- /dev/null +++ b/e2e_projects/config/config_pkg/utils/utils.py @@ -0,0 +1,2 @@ +def util_that_should_NOT_be_mutated(): + return 1 + 2 + 3 + 4 + 5 - 7 - 8 diff --git a/e2e_projects/config/pyproject.toml b/e2e_projects/config/pyproject.toml index 923268da..8409dc9f 100644 --- a/e2e_projects/config/pyproject.toml +++ b/e2e_projects/config/pyproject.toml @@ -24,15 +24,15 @@ dev = [ [tool.mutmut] debug = true -paths_to_mutate = [ "config_pkg/" ] +source_paths = [ "config_pkg/" ] +only_mutate = [ "config_pkg/logic/*" ] do_not_mutate = [ "*ignore*" ] also_copy = [ "data" ] max_stack_depth=8 # Includes frames by mutmut, see https://github.com/boxed/mutmut/issues/378 -tests_dir = [ "tests/main/" ] # verify that we can override options with pytest_add_cli_args pytest_add_cli_args = ["-o", "xfail_strict=False"] # verify test exclusion (-m 'not fail') and test inclusion (-k=test_include) -pytest_add_cli_args_test_selection = [ "-m", "not fail", "-k=test_include"] +pytest_add_cli_args_test_selection = [ "-m", "not fail", "-k=test_include", "tests/main/"] [tool.pytest.ini_options] xfail_strict = true diff --git a/e2e_projects/config/tests/ignored/test_ignored.py b/e2e_projects/config/tests/ignored/test_ignored.py index 8a5f51ee..8008e2c4 100644 --- a/e2e_projects/config/tests/ignored/test_ignored.py +++ b/e2e_projects/config/tests/ignored/test_ignored.py @@ -1,5 +1,5 @@ -from config_pkg.math import func_with_no_tests +from config_pkg.logic.math import func_with_no_tests -# ignored, because tests_dir specifies only the main directory +# ignored, because pytest_add_cli_args_test_selection specifies only the main directory def test_include_func_with_no_tests(): assert func_with_no_tests() == 420 diff --git a/e2e_projects/config/tests/main/test_main.py b/e2e_projects/config/tests/main/test_main.py index 15500b61..018a9ea0 100644 --- a/e2e_projects/config/tests/main/test_main.py +++ b/e2e_projects/config/tests/main/test_main.py @@ -1,9 +1,10 @@ import json import pytest from pathlib import Path -from config_pkg import hello -from config_pkg.math import add, call_depth_two -from config_pkg.ignore_me import this_function_shall_NOT_be_mutated +from config_pkg.logic import hello +from config_pkg.logic.math import add, call_depth_two +from config_pkg.logic.ignore_me import this_function_shall_NOT_be_mutated +from config_pkg.utils.utils import util_that_should_NOT_be_mutated def test_include_hello(): assert hello() == "Hello from config!" @@ -14,6 +15,9 @@ def test_include_add(): def test_include_non_mutated_function(): assert this_function_shall_NOT_be_mutated() == 3 +def test_include_non_mutated_util(): + assert util_that_should_NOT_be_mutated() == 0 + def test_include_max_stack_depth(): # This test should only cover functions up to some depth # For more context, see https://github.com/boxed/mutmut/issues/378 diff --git a/e2e_projects/mutate_only_covered_lines/pyproject.toml b/e2e_projects/mutate_only_covered_lines/pyproject.toml index e51233f0..0d1d866c 100644 --- a/e2e_projects/mutate_only_covered_lines/pyproject.toml +++ b/e2e_projects/mutate_only_covered_lines/pyproject.toml @@ -24,9 +24,9 @@ omit = [ [tool.mutmut] debug = true mutate_only_covered_lines = true -tests_dir = [ "tests/main/" ] +pytest_add_cli_args_test_selection = [ "tests/main/" ] do_not_mutate = [ "*ignore*" ] -# do not add paths_to_mutate because we want to test src guessing +# do not add source_paths because we want to test src guessing [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" diff --git a/e2e_projects/mutate_only_covered_lines/tests/other/test_that_should_not_be_included.py b/e2e_projects/mutate_only_covered_lines/tests/other/test_that_should_not_be_included.py index 0e697978..5b00ba66 100644 --- a/e2e_projects/mutate_only_covered_lines/tests/other/test_that_should_not_be_included.py +++ b/e2e_projects/mutate_only_covered_lines/tests/other/test_that_should_not_be_included.py @@ -1,6 +1,6 @@ from mutate_only_covered_lines import hello_mutate_only_covered_lines -"""This test should be ignored, because of the tests_dir config.""" +"""This test should be ignored, because of the pytest_add_cli_args_test_selection config.""" def test_mutate_only_covered_lines(): assert hello_mutate_only_covered_lines(False) == "Hello from mutate_only_covered_lines! (false)" diff --git a/src/mutmut/__main__.py b/src/mutmut/__main__.py index 8c6f1f04..26261ec4 100644 --- a/src/mutmut/__main__.py +++ b/src/mutmut/__main__.py @@ -133,7 +133,7 @@ def record_trampoline_hit(name: str) -> None: def walk_all_files() -> Iterator[tuple[str, str]]: - for path in Config.get().paths_to_mutate: + for path in Config.get().source_paths: if not isdir(path): if isfile(path): yield "", str(path) @@ -149,6 +149,13 @@ def walk_source_files() -> Iterator[Path]: yield Path(root) / filename +def walk_mutatable_files() -> Iterator[Path]: + config = Config.get() + for path in walk_source_files(): + if config.should_mutate(path): + yield path + + class MutmutProgrammaticFailException(Exception): pass @@ -228,11 +235,11 @@ def create_file_mutants(path: Path) -> FileMutationResult: output_path = Path("mutants") / path makedirs(output_path.parent, exist_ok=True) - if Config.get().should_ignore_for_mutation(path): + if Config.get().should_mutate(path): + return create_mutants_for_file(path, output_path) + else: shutil.copy(path, output_path) return FileMutationResult(ignored=True) - else: - return create_mutants_for_file(path, output_path) except Exception as e: return FileMutationResult(error=e) @@ -388,10 +395,6 @@ def __init__(self) -> None: self._pytest_add_cli_args: list[str] = Config.get().pytest_add_cli_args self._pytest_add_cli_args_test_selection: list[str] = Config.get().pytest_add_cli_args_test_selection - # tests_dir is a special case of a test selection option, - # so also use pytest_add_cli_args_test_selection for the implementation - self._pytest_add_cli_args_test_selection += Config.get().tests_dir - # noinspection PyMethodMayBeStatic def execute_pytest(self, params: list[str], **kwargs: Any) -> int: import pytest @@ -463,7 +466,6 @@ def pytest_deselected(self, items: Any) -> None: collector = TestsCollector() - tests_dir = Config.get().tests_dir pytest_args = ["-x", "-q", "--collect-only"] + self._pytest_add_cli_args_test_selection with change_cwd("mutants"): @@ -828,10 +830,7 @@ def export_cicd_stats() -> None: source_file_mutation_data_by_path: dict[str, SourceFileMutationData] = {} - for path in walk_source_files(): - if Config.get().should_ignore_for_mutation(path): - continue - + for path in walk_mutatable_files(): meta_path = Path("mutants") / (str(path) + ".meta") if not meta_path.exists(): continue @@ -859,9 +858,7 @@ def collect_source_file_mutation_data( ]: source_file_mutation_data_by_path: dict[str, SourceFileMutationData] = {} - for path in walk_source_files(): - if Config.get().should_ignore_for_mutation(path): - continue + for path in walk_mutatable_files(): assert str(path) not in source_file_mutation_data_by_path m = SourceFileMutationData(path=path) m.load() @@ -1060,7 +1057,9 @@ def read_one_child_exit_status() -> None: if not sorted_tests: os._exit(33) - cpu_time_limit_s = ceil((estimated_time_of_tests + config.timeout_constant) * config.timeout_multiplier * 2 + process_time()) + cpu_time_limit_s = ceil( + (estimated_time_of_tests + config.timeout_constant) * config.timeout_multiplier * 2 + process_time() + ) # signal SIGXCPU after . One second later signal SIGKILL if it is still running resource.setrlimit(resource.RLIMIT_CPU, (cpu_time_limit_s, cpu_time_limit_s + 1)) @@ -1133,9 +1132,7 @@ def tests_for_mutant_names(mutant_names: tuple[str, ...] | list[str]) -> set[str @click.option("--all", default=False) def results(all: bool) -> None: Config.ensure_loaded() - for path in walk_source_files(): - if not str(path).endswith(".py"): - continue + for path in walk_mutatable_files(): m = SourceFileMutationData(path=path) m.load() for k, v in m.exit_code_by_key.items(): @@ -1188,10 +1185,7 @@ def read_mutant_function(module: cst.Module, mutant_name: str) -> cst.FunctionDe def find_mutant(mutant_name: str) -> SourceFileMutationData: - for path in walk_source_files(): - if Config.get().should_ignore_for_mutation(path): - continue - + for path in walk_mutatable_files(): m = SourceFileMutationData(path=path) m.load() if mutant_name in m.exit_code_by_key: @@ -1334,9 +1328,7 @@ def read_data(self) -> None: self.source_file_mutation_data_and_stat_by_path = {} self.path_by_name: dict[str, Path] = {} - for p in walk_source_files(): - if Config.get().should_ignore_for_mutation(p): - continue + for p in walk_mutatable_files(): source_file_mutation_data = SourceFileMutationData(path=p) source_file_mutation_data.load() stat = collect_stat(source_file_mutation_data) diff --git a/src/mutmut/configuration.py b/src/mutmut/configuration.py index 59f62d3c..d3a8ef01 100644 --- a/src/mutmut/configuration.py +++ b/src/mutmut/configuration.py @@ -4,6 +4,7 @@ import os import platform import sys +import warnings from collections.abc import Callable from configparser import ConfigParser from configparser import NoOptionError @@ -64,7 +65,7 @@ def setup_cfg_conf(key: str, default: Any) -> Any: return setup_cfg_conf -def _guess_paths_to_mutate() -> list[str]: +def _guess_source_paths() -> list[str]: """Guess the path to source code to mutate :rtype: str @@ -88,15 +89,38 @@ def _guess_paths_to_mutate() -> list[str]: return [this_dir + ".py"] raise FileNotFoundError( "Could not figure out where the code to mutate is. " - 'Please specify it by adding "paths_to_mutate=code_dir" in setup.cfg to the [mutmut] section.' + 'Please specify it by adding "source_paths=code_dir" in setup.cfg to the [mutmut] section.' ) def _load_config() -> Config: s = _config_reader() + paths_to_mutate = [Path(path) for path in s("paths_to_mutate", [])] + if paths_to_mutate: + warnings.warn("The config paths_to_mutate is deprecated. Please rename it to source_paths") + source_paths = [Path(path) for path in s("source_paths", [])] + source_paths = source_paths or paths_to_mutate or [Path(path) for path in _guess_source_paths()] + + tests_dir = s("tests_dir", []) + if tests_dir: + warnings.warn( + "The config tests_dir is deprecated. Please add the path to pytest_add_cli_args_test_selection instead" + ) + pytest_add_cli_args_test_selection = s("pytest_add_cli_args_test_selection", []) + tests_dir + + only_mutate = s("only_mutate", []) + do_not_mutate = s("do_not_mutate", []) + # only patterns for python files are valid: must end with ".py" or "*" + invalid_patterns = [p for p in only_mutate + do_not_mutate if not (p.endswith("*") or p.endswith(".py"))] + if invalid_patterns: + warnings.warn( + f'The configs only_mutate and do_not_mutate expect glob patterns like "src/api/*" or "src/main.py". Following patterns are likely invalid: {invalid_patterns}' + ) + return Config( - do_not_mutate=s("do_not_mutate", []), + only_mutate=only_mutate, + do_not_mutate=do_not_mutate, also_copy=[Path(y) for y in s("also_copy", [])] + [ Path("tests/"), @@ -108,10 +132,9 @@ def _load_config() -> Config: max_stack_depth=s("max_stack_depth", -1), debug=s("debug", False), mutate_only_covered_lines=s("mutate_only_covered_lines", False), - paths_to_mutate=[Path(y) for y in s("paths_to_mutate", [])] or [Path(p) for p in _guess_paths_to_mutate()], - tests_dir=s("tests_dir", []), + source_paths=source_paths, pytest_add_cli_args=s("pytest_add_cli_args", []), - pytest_add_cli_args_test_selection=s("pytest_add_cli_args_test_selection", []), + pytest_add_cli_args_test_selection=pytest_add_cli_args_test_selection, timeout_multiplier=s("timeout_multiplier", 15.0), timeout_constant=s("timeout_constant", 1.0), type_check_command=s("type_check_command", []), @@ -127,20 +150,34 @@ def _load_config() -> Config: @dataclass class Config: also_copy: list[Path] + only_mutate: list[str] do_not_mutate: list[str] max_stack_depth: int debug: bool - paths_to_mutate: list[Path] + source_paths: list[Path] pytest_add_cli_args: list[str] pytest_add_cli_args_test_selection: list[str] - tests_dir: list[str] mutate_only_covered_lines: bool timeout_multiplier: float timeout_constant: float type_check_command: list[str] use_setproctitle: bool - def should_ignore_for_mutation(self, path: Path | str) -> bool: + def should_mutate(self, path: Path | str) -> bool: + return self._should_include_for_mutation(path) and not self._should_ignore_for_mutation(path) + + def _should_include_for_mutation(self, path: Path | str) -> bool: + if not self.only_mutate: + return True + path_str = str(path) + if not path_str.endswith(".py"): + return True + for p in self.only_mutate: + if fnmatch.fnmatch(path_str, p): + return True + return False + + def _should_ignore_for_mutation(self, path: Path | str) -> bool: path_str = str(path) if not path_str.endswith(".py"): return True diff --git a/tests/e2e/e2e_utils.py b/tests/e2e/e2e_utils.py index c7a6ae71..1b7ca2d4 100644 --- a/tests/e2e/e2e_utils.py +++ b/tests/e2e/e2e_utils.py @@ -30,7 +30,7 @@ def read_all_stats_for_project(project_path: Path) -> dict[str, dict]: stats = {} for p in walk_source_files(): - if Config.get().should_ignore_for_mutation(p): + if not Config.get().should_mutate(p): continue data = SourceFileMutationData(path=p) data.load() diff --git a/tests/e2e/test_e2e_config.py b/tests/e2e/test_e2e_config.py index 84b712b2..8e383e22 100644 --- a/tests/e2e/test_e2e_config.py +++ b/tests/e2e/test_e2e_config.py @@ -6,21 +6,21 @@ def test_config_result_snapshot(): assert run_mutmut_on_project("config") == snapshot( { - "mutants/config_pkg/__init__.py.meta": { - "config_pkg.x_hello__mutmut_1": 1, - "config_pkg.x_hello__mutmut_2": 1, - "config_pkg.x_hello__mutmut_3": 1, + "mutants/config_pkg/logic/__init__.py.meta": { + "config_pkg.logic.x_hello__mutmut_1": 1, + "config_pkg.logic.x_hello__mutmut_2": 1, + "config_pkg.logic.x_hello__mutmut_3": 1, }, - "mutants/config_pkg/math.py.meta": { - "config_pkg.math.x_add__mutmut_1": 0, - "config_pkg.math.x_call_depth_two__mutmut_1": 1, - "config_pkg.math.x_call_depth_two__mutmut_2": 1, - "config_pkg.math.x_call_depth_three__mutmut_1": 1, - "config_pkg.math.x_call_depth_three__mutmut_2": 1, - "config_pkg.math.x_call_depth_four__mutmut_1": 33, - "config_pkg.math.x_call_depth_four__mutmut_2": 33, - "config_pkg.math.x_call_depth_five__mutmut_1": 33, - "config_pkg.math.x_func_with_no_tests__mutmut_1": 33, + "mutants/config_pkg/logic/math.py.meta": { + "config_pkg.logic.math.x_add__mutmut_1": 0, + "config_pkg.logic.math.x_call_depth_two__mutmut_1": 1, + "config_pkg.logic.math.x_call_depth_two__mutmut_2": 1, + "config_pkg.logic.math.x_call_depth_three__mutmut_1": 1, + "config_pkg.logic.math.x_call_depth_three__mutmut_2": 1, + "config_pkg.logic.math.x_call_depth_four__mutmut_1": 33, + "config_pkg.logic.math.x_call_depth_four__mutmut_2": 33, + "config_pkg.logic.math.x_call_depth_five__mutmut_1": 33, + "config_pkg.logic.math.x_func_with_no_tests__mutmut_1": 33, }, } ) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 445ba5bc..e8de4c54 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -4,7 +4,7 @@ from mutmut.configuration import Config from mutmut.configuration import _config_reader -from mutmut.configuration import _guess_paths_to_mutate +from mutmut.configuration import _guess_source_paths from mutmut.configuration import _load_config @@ -52,103 +52,90 @@ def test_ensure_loaded_is_idempotent(self, in_tmp_dir: Path): assert config1 is config2 -class TestShouldIgnoreForMutation: - def test_ignores_non_python_files(self): - config = Config( +class TestShouldMutateFile: + @staticmethod + def _get_config(only_mutate: list[str], do_not_mutate: list[str]) -> Config: + # only the "only_mutate" and "do_not_mutate" configs are important for these tests + return Config( + only_mutate=only_mutate, + do_not_mutate=do_not_mutate, also_copy=[], - do_not_mutate=[], max_stack_depth=-1, debug=False, - paths_to_mutate=[], + source_paths=[], pytest_add_cli_args=[], pytest_add_cli_args_test_selection=[], - tests_dir=[], mutate_only_covered_lines=False, timeout_multiplier=15.0, timeout_constant=1.0, type_check_command=[], use_setproctitle=False, ) - assert config.should_ignore_for_mutation("foo.txt") is True - assert config.should_ignore_for_mutation("foo.js") is True - assert config.should_ignore_for_mutation("foo") is True - def test_does_not_ignore_python_files(self): - config = Config( - also_copy=[], + def test_ignores_non_python_files(self): + config = self._get_config( + only_mutate=[], do_not_mutate=[], - max_stack_depth=-1, - debug=False, - paths_to_mutate=[], - pytest_add_cli_args=[], - pytest_add_cli_args_test_selection=[], - tests_dir=[], - mutate_only_covered_lines=False, - timeout_multiplier=15.0, - timeout_constant=1.0, - type_check_command=[], - use_setproctitle=False, ) - assert config.should_ignore_for_mutation("foo.py") is False - assert config.should_ignore_for_mutation("src/foo.py") is False + assert config.should_mutate("foo.txt") is False + assert config.should_mutate("foo.js") is False + assert config.should_mutate("foo") is False + + def test_includes_python_files(self): + config = self._get_config( + only_mutate=[], + do_not_mutate=[], + ) + assert config.should_mutate("foo.py") is True + assert config.should_mutate("src/foo.py") is True def test_respects_do_not_mutate_exact_match(self): - config = Config( - also_copy=[], + config = self._get_config( + only_mutate=[], do_not_mutate=["foo.py"], - max_stack_depth=-1, - debug=False, - paths_to_mutate=[], - pytest_add_cli_args=[], - pytest_add_cli_args_test_selection=[], - tests_dir=[], - mutate_only_covered_lines=False, - timeout_multiplier=15.0, - timeout_constant=1.0, - type_check_command=[], - use_setproctitle=False, ) - assert config.should_ignore_for_mutation("foo.py") is True - assert config.should_ignore_for_mutation("bar.py") is False + assert config.should_mutate("foo.py") is False + assert config.should_mutate("bar.py") is True def test_respects_do_not_mutate_glob_pattern(self): - config = Config( - also_copy=[], + config = self._get_config( + only_mutate=[], do_not_mutate=["**/test_*.py", "src/ignore_*.py"], - max_stack_depth=-1, - debug=False, - paths_to_mutate=[], - pytest_add_cli_args=[], - pytest_add_cli_args_test_selection=[], - tests_dir=[], - mutate_only_covered_lines=False, - timeout_multiplier=15.0, - timeout_constant=1.0, - type_check_command=[], - use_setproctitle=False, ) - assert config.should_ignore_for_mutation("tests/test_foo.py") is True - assert config.should_ignore_for_mutation("src/ignore_me.py") is True - assert config.should_ignore_for_mutation("src/keep_me.py") is False + assert config.should_mutate("tests/test_foo.py") is False + assert config.should_mutate("src/ignore_me.py") is False + assert config.should_mutate("src/keep_me.py") is True + + def test_respects_only_mutate(self): + config = self._get_config( + # without a glob, the `src/` is pointless + only_mutate=["src/", "foo/*"], + do_not_mutate=[], + ) + assert config.should_mutate("tests/test_foo.py") is False + assert config.should_mutate("src/main.py") is False + assert config.should_mutate("foo/main.py") is True + assert config.should_mutate("foo/nested/main.py") is True + + def test_respects_only_mutate_with_do_not_mutate(self): + config = self._get_config( + only_mutate=["src/api/*"], + do_not_mutate=["src/api/models/*"], + ) + # matched by only_mutate + assert config.should_mutate("src/api/endpoints/user.py") is True + # matched by only_mutate but excluded by do_not_mutate + assert config.should_mutate("src/api/models/user.py") is False + # not matched by only_mutate + assert config.should_mutate("src/services/user.py") is False def test_accepts_path_objects(self): - config = Config( - also_copy=[], + config = self._get_config( + only_mutate=[], do_not_mutate=["foo.py"], - max_stack_depth=-1, - debug=False, - paths_to_mutate=[], - pytest_add_cli_args=[], - pytest_add_cli_args_test_selection=[], - tests_dir=[], - mutate_only_covered_lines=False, - timeout_multiplier=15.0, - timeout_constant=1.0, - type_check_command=[], - use_setproctitle=False, ) - assert config.should_ignore_for_mutation(Path("foo.py")) is True - assert config.should_ignore_for_mutation(Path("bar.py")) is False + assert config.should_mutate(Path("foo.py")) is False + assert config.should_mutate(Path("bar.py")) is True class TestConfigReaderPyprojectToml: @@ -157,13 +144,13 @@ def test_reads_from_pyproject_toml(self, in_tmp_dir: Path): [tool.mutmut] debug = true max_stack_depth = 10 -paths_to_mutate = ["src", "lib"] +source_paths = ["src", "lib"] do_not_mutate = ["**/migrations/*"] """) reader = _config_reader() assert reader("debug", False) is True assert reader("max_stack_depth", -1) == 10 - assert reader("paths_to_mutate", []) == ["src", "lib"] + assert reader("source_paths", []) == ["src", "lib"] assert reader("do_not_mutate", []) == ["**/migrations/*"] def test_returns_default_for_missing_key(self, in_tmp_dir: Path): @@ -191,12 +178,12 @@ def test_reads_from_setup_cfg(self, in_tmp_dir: Path): [mutmut] debug = true max_stack_depth = 5 -paths_to_mutate = src +source_paths = src """) reader = _config_reader() assert reader("debug", False) is True assert reader("max_stack_depth", -1) == 5 - assert reader("paths_to_mutate", []) == ["src"] + assert reader("source_paths", []) == ["src"] def test_parses_multiline_list(self, in_tmp_dir: Path): (in_tmp_dir / "setup.cfg").write_text(""" @@ -277,31 +264,31 @@ def test_pyproject_toml_takes_precedence(self, in_tmp_dir: Path): class TestGuessPathsToMutate: def test_guesses_lib_directory(self, in_tmp_dir: Path): (in_tmp_dir / "lib").mkdir() - assert _guess_paths_to_mutate() == ["lib"] + assert _guess_source_paths() == ["lib"] def test_guesses_src_directory(self, in_tmp_dir: Path): (in_tmp_dir / "src").mkdir() - assert _guess_paths_to_mutate() == ["src"] + assert _guess_source_paths() == ["src"] def test_prefers_lib_over_src(self, in_tmp_dir: Path): (in_tmp_dir / "lib").mkdir() (in_tmp_dir / "src").mkdir() - assert _guess_paths_to_mutate() == ["lib"] + assert _guess_source_paths() == ["lib"] def test_guesses_directory_matching_cwd_name(self, in_tmp_dir: Path): # tmp_path has a random name, create a subdir matching it dir_name = in_tmp_dir.name (in_tmp_dir / dir_name).mkdir() - assert _guess_paths_to_mutate() == [dir_name] + assert _guess_source_paths() == [dir_name] def test_guesses_py_file_matching_cwd_name(self, in_tmp_dir: Path): dir_name = in_tmp_dir.name (in_tmp_dir / f"{dir_name}.py").touch() - assert _guess_paths_to_mutate() == [f"{dir_name}.py"] + assert _guess_source_paths() == [f"{dir_name}.py"] def test_raises_when_cannot_guess(self, in_tmp_dir: Path): with pytest.raises(FileNotFoundError, match="Could not figure out"): - _guess_paths_to_mutate() + _guess_source_paths() class TestLoadConfig: @@ -310,9 +297,9 @@ def test_loads_all_config_values(self, in_tmp_dir: Path): [tool.mutmut] debug = true max_stack_depth = 10 -paths_to_mutate = ["src"] +source_paths = ["src"] +only_mutate=["**/foo.py"] do_not_mutate = ["**/test_*.py"] -tests_dir = ["tests/unit"] pytest_add_cli_args = ["-x", "--tb=short"] pytest_add_cli_args_test_selection = ["--no-header"] also_copy = ["fixtures"] @@ -327,9 +314,9 @@ def test_loads_all_config_values(self, in_tmp_dir: Path): assert config.debug is True assert config.max_stack_depth == 10 - assert config.paths_to_mutate == [Path("src")] + assert config.source_paths == [Path("src")] + assert config.only_mutate == ["**/foo.py"] assert config.do_not_mutate == ["**/test_*.py"] - assert config.tests_dir == ["tests/unit"] assert config.pytest_add_cli_args == ["-x", "--tb=short"] assert config.pytest_add_cli_args_test_selection == ["--no-header"] assert Path("fixtures") in config.also_copy @@ -345,7 +332,8 @@ def test_uses_defaults_when_no_config(self, in_tmp_dir: Path): assert config.debug is False assert config.max_stack_depth == -1 - assert config.paths_to_mutate == [Path("src")] + assert config.source_paths == [Path("src")] + assert config.only_mutate == [] assert config.do_not_mutate == [] assert config.mutate_only_covered_lines is False assert config.timeout_multiplier == 15.0 diff --git a/tests/test_generation_error_handling.py b/tests/test_generation_error_handling.py index f556e392..ad3e087f 100644 --- a/tests/test_generation_error_handling.py +++ b/tests/test_generation_error_handling.py @@ -25,7 +25,7 @@ def test_mutant_generation_raises_exception_on_invalid_syntax(monkeypatch): source_dir / "invalid_syntax.py", ] monkeypatch.setattr(mutmut.__main__, "walk_source_files", lambda: source_files) - monkeypatch.setattr(Config.get(), "should_ignore_for_mutation", lambda _path: False) + monkeypatch.setattr(Config.get(), "should_mutate", lambda _path: True) # should raise an exception, because we copy the invalid_syntax.py file and then verify # if it is valid syntax