diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 69dbedf3..2d50d4d1 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -21,7 +21,7 @@ Running the tests uv run pytest -This also runs E2E tests that verify that `mutmut run` produces the same output as before. If your code changes should change the output of `mutmut run` and this test fails, try to delete the `snapshots/*.json` files (as described in the test errors). +We use `inline-snapshot` for E2E and integration tests, to prevent unexpected changes in the output. If the output _should_ change, you can use `uv run pytest --inline-snapshot=fix` to update the snapshots. If pytest terminates before reporting the test failures, it likely hit a case where mutmut calls `os._exit(...)`. Try looking at these calls first for troubleshooting. diff --git a/e2e_projects/my_lib/src/my_lib/__init__.py b/e2e_projects/my_lib/src/my_lib/__init__.py index 6cad97d2..8ca0c381 100644 --- a/e2e_projects/my_lib/src/my_lib/__init__.py +++ b/e2e_projects/my_lib/src/my_lib/__init__.py @@ -104,9 +104,9 @@ def some_func(a, b: str = "111", c: Callable[[str], int] | None = None) -> int | return c(b) return None -def func_with_star_clone(a, *, b, **kwargs): pass # pragma: no mutate -def func_with_star(a, *, b, **kwargs): - return a + b + len(kwargs) +def func_with_star_clone(a, /, b, *, c, **kwargs): pass # pragma: no mutate +def func_with_star(a, /, b, *, c, **kwargs): + return a + b + c + len(kwargs) def func_with_arbitrary_args_clone(*args, **kwargs): pass # pragma: no mutate def func_with_arbitrary_args(*args, **kwargs): diff --git a/e2e_projects/my_lib/tests/test_my_lib.py b/e2e_projects/my_lib/tests/test_my_lib.py index ed72a964..3609cc9c 100644 --- a/e2e_projects/my_lib/tests/test_my_lib.py +++ b/e2e_projects/my_lib/tests/test_my_lib.py @@ -1,6 +1,7 @@ import inspect from my_lib import * import pytest +import asyncio """These tests are flawed on purpose, some mutants survive and some are killed.""" @@ -60,5 +61,9 @@ def test_that_signatures_are_preserved(): def test_signature_functions_are_callable(): assert some_func(True, c=lambda s: int(s), b="222") == 222 - assert func_with_star(1, b=2, x='x', y='y', z='z') == 6 + assert func_with_star(1, b=2, c=3, x='x', y='y', z='z') == 9 assert func_with_arbitrary_args('a', 'b', foo=123, bar=456) == 4 + +def test_signature_is_coroutine(): + assert asyncio.iscoroutinefunction(async_consumer) + diff --git a/pyproject.toml b/pyproject.toml index 32d17836..dd03dcb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,9 @@ source-include = ["HISTORY.rst"] [dependency-groups] dev = [ + "inline-snapshot>=0.32.0", "pytest-asyncio>=1.0.0", + "ruff>=0.15.1", ] [tool.pytest.ini_options] @@ -61,3 +63,6 @@ testpaths = [ "tests", ] asyncio_default_fixture_loop_scope = "function" + +[tool.inline-snapshot] +format-command="ruff format --stdin-filename {filename}" diff --git a/src/mutmut/code_coverage.py b/src/mutmut/code_coverage.py index 8878aa3c..65ebbda0 100644 --- a/src/mutmut/code_coverage.py +++ b/src/mutmut/code_coverage.py @@ -2,7 +2,6 @@ import importlib import sys from pathlib import Path -import json # Returns a set of lines that are covered in this file gvein the covered_lines dict diff --git a/src/mutmut/file_mutation.py b/src/mutmut/file_mutation.py index 2fa50ba6..fa28e0ec 100644 --- a/src/mutmut/file_mutation.py +++ b/src/mutmut/file_mutation.py @@ -3,13 +3,11 @@ from collections import defaultdict from collections.abc import Iterable, Sequence, Mapping from dataclasses import dataclass -from pathlib import Path from typing import Union -import warnings import libcst as cst from libcst.metadata import PositionProvider, MetadataWrapper import libcst.matchers as m -from mutmut.trampoline_templates import build_trampoline, mangle_function_name, trampoline_impl +from mutmut.trampoline_templates import create_trampoline_lookup, mangle_function_name, trampoline_impl from mutmut.node_mutation import mutation_operators, OPERATORS_TYPE NEVER_MUTATE_FUNCTION_NAMES = { "__getattribute__", "__setattr__", "__new__" } @@ -248,13 +246,88 @@ def function_trampoline_arrangement(function: cst.FunctionDef, mutants: Iterable nodes.append(mutated_method) # type: ignore # trampoline that forwards the calls - trampoline = list(cst.parse_module(build_trampoline(orig_name=name, mutants=mutant_names, class_name=class_name)).body) - trampoline[0] = trampoline[0].with_changes(leading_lines=[cst.EmptyLine()]) - nodes.extend(trampoline) + trampoline = create_trampoline_wrapper(function, mangled_name, class_name) + mutants_dict = list(cst.parse_module(create_trampoline_lookup(orig_name=name, mutants=mutant_names, class_name=class_name)).body) + mutants_dict[0] = mutants_dict[0].with_changes(leading_lines=[cst.EmptyLine()]) + + nodes.append(trampoline) + nodes.extend(mutants_dict) return nodes, mutant_names +def create_trampoline_wrapper(function: cst.FunctionDef, mangled_name: str, class_name: str | None) -> cst.FunctionDef: + args: list[cst.Element | cst.StarredElement] = [] + for pos_only_param in function.params.posonly_params: + args.append(cst.Element(pos_only_param.name)) + for param in function.params.params: + args.append(cst.Element(param.name)) + if isinstance(function.params.star_arg, cst.Param): + args.append(cst.StarredElement(function.params.star_arg.name)) + + if class_name is not None: + # remove self arg (handled by the trampoline function) + args = args[1:] + + args_assignemnt = cst.Assign([cst.AssignTarget(cst.Name(value='args'))], cst.List(args)) + + kwargs: list[cst.DictElement | cst.StarredDictElement] = [] + for param in function.params.kwonly_params: + kwargs.append(cst.DictElement(cst.SimpleString(f"'{param.name.value}'"), param.name)) + if isinstance(function.params.star_kwarg, cst.Param): + kwargs.append(cst.StarredDictElement(function.params.star_kwarg.name)) + + kwargs_assignment = cst.Assign([cst.AssignTarget(cst.Name(value='kwargs'))], cst.Dict(kwargs)) + + def _get_local_name(func_name: str) -> cst.BaseExpression: + # for top level, simply return the name + if class_name is None: + return cst.Name(func_name) + # for class methods, use object.__getattribute__(self, name) + return cst.Call( + func=cst.Attribute(cst.Name('object'), cst.Name('__getattribute__')), + args=[cst.Arg(cst.Name('self')), cst.Arg(cst.SimpleString(f"'{func_name}'"))] + ) + + result: cst.BaseExpression = cst.Call( + func=cst.Name('_mutmut_trampoline'), + args=[ + cst.Arg(_get_local_name(f'{mangled_name}_orig')), + cst.Arg(_get_local_name(f'{mangled_name}_mutants')), + cst.Arg(cst.Name('args')), + cst.Arg(cst.Name('kwargs')), + cst.Arg(cst.Name('None' if class_name is None else 'self')), + ], + ) + # for non-async functions, simply return the value or generator + result_statement = cst.SimpleStatementLine([cst.Return(result)]) + + if function.asynchronous: + is_generator = _is_generator(function) + if is_generator: + # async for i in _mutmut_trampoline(...): yield i + result_statement = cst.For( + target=cst.Name('i'), + iter=result, + body=cst.IndentedBlock([cst.SimpleStatementLine([cst.Expr(cst.Yield(cst.Name('i')))])]), + asynchronous=cst.Asynchronous(), + ) + else: + # return await _mutmut_trampoline(...) + result_statement = cst.SimpleStatementLine([cst.Return(cst.Await(result))]) + + function.whitespace_after_type_parameters + return function.with_changes( + body=cst.IndentedBlock( + [ + cst.SimpleStatementLine([args_assignemnt]), + cst.SimpleStatementLine([kwargs_assignment]), + result_statement, + ], + ), + ) + + def get_statements_until_func_or_class(statements: Sequence[MODULE_STATEMENT]) -> list[MODULE_STATEMENT]: """Get all statements until we encounter the first function or class definition""" result = [] @@ -302,3 +375,25 @@ def on_leave(self, original_node: cst.CSTNode, updated_node: cst.CSTNode) -> cst self.replaced_node = True return self.new_node return updated_node + +def _is_generator(function: cst.FunctionDef) -> bool: + """Return True if the function has yield statement(s).""" + visitor = IsGeneratorVisitor(function) + function.visit(visitor) + return visitor.is_generator + +class IsGeneratorVisitor(cst.CSTVisitor): + """Check if a function is a generator. + We do so by checking if any child is a Yield statement, but not looking into inner function definitions.""" + def __init__(self, original_function: cst.FunctionDef): + self.is_generator = False + self.original_function: cst.FunctionDef = original_function + + def visit_FunctionDef(self, node): + # do not recurse into inner function definitions + if self.original_function != node: + return False + + def visit_Yield(self, node): + self.is_generator = True + return False diff --git a/src/mutmut/trampoline_templates.py b/src/mutmut/trampoline_templates.py index 1d41408d..1f5c6a7b 100644 --- a/src/mutmut/trampoline_templates.py +++ b/src/mutmut/trampoline_templates.py @@ -1,27 +1,11 @@ CLASS_NAME_SEPARATOR = 'ǁ' -def build_trampoline(*, orig_name, mutants, class_name): +def create_trampoline_lookup(*, orig_name, mutants, class_name): mangled_name = mangle_function_name(name=orig_name, class_name=class_name) mutants_dict = f'{mangled_name}__mutmut_mutants : ClassVar[MutantDict] = {{\n' + ', \n '.join(f'{repr(m)}: {m}' for m in mutants) + '\n}' - access_prefix = '' - access_suffix = '' - self_arg = '' - if class_name is not None: - access_prefix = f'object.__getattribute__(self, "' - access_suffix = '")' - self_arg = ', self' - - trampoline_name = '_mutmut_trampoline' - return f""" {mutants_dict} - -def {orig_name}({'self, ' if class_name is not None else ''}*args, **kwargs): - result = {trampoline_name}({access_prefix}{mangled_name}__mutmut_orig{access_suffix}, {access_prefix}{mangled_name}__mutmut_mutants{access_suffix}, args, kwargs{self_arg}) - return result - -_mutmut_copy_signature({orig_name}, {mangled_name}__mutmut_orig) {mangled_name}__mutmut_orig.__name__ = '{mangled_name}' """ @@ -37,31 +21,10 @@ def mangle_function_name(*, name, class_name): # noinspection PyUnresolvedReferences # language=python trampoline_impl = """ -import inspect as _mutmut_inspect -import sys as _mutmut_sys from typing import Annotated from typing import Callable from typing import ClassVar -def _mutmut_copy_signature(trampoline, original_method): - if _mutmut_sys.version_info >= (3, 14): - # PEP 649 introduced deferred loading for annotations - # When some_method.__annotations__ is accessed, Python uses some_method.__annotate__(format) to compute it - # By copying the original __annotate__ method, we provide get the original annotations - trampoline.__annotate__ = original_method.__annotate__ - else: - # On Python 3.13 and earlier, __annotations__ can be accessed immediately - trampoline.__annotations__ = original_method.__annotations__ - - try: - trampoline.__signature__ = _mutmut_inspect.signature(original_method) - except NameError: - # Also, because of PEP 649, it can happen that we cannot eagerly evaluate the signature - # In this case, fall back to stringifying the signature (which could cause different behaviour with runtime introspection) - trampoline.__signature__ = _mutmut_inspect.signature(original_method, annotation_format=_mutmut_inspect.Format.STRING) - - - MutantDict = Annotated[dict[str, Callable], "Mutant"] @@ -75,6 +38,7 @@ def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None): elif mutant_under_test == 'stats': from mutmut.__main__ import record_trampoline_hit record_trampoline_hit(orig.__module__ + '.' + orig.__name__) + # (for class methods, orig is bound and thus does not need the explicit self argument) result = orig(*call_args, **call_kwargs) return result prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_' diff --git a/tests/e2e/test_e2e_result_snapshots.py b/tests/e2e/e2e_utils.py similarity index 55% rename from tests/e2e/test_e2e_result_snapshots.py rename to tests/e2e/e2e_utils.py index c79e500c..9f6338e2 100644 --- a/tests/e2e/test_e2e_result_snapshots.py +++ b/tests/e2e/e2e_utils.py @@ -4,8 +4,6 @@ from contextlib import contextmanager from pathlib import Path from typing import Any -import pytest -import sys import mutmut from mutmut.__main__ import SourceFileMutationData, _run, ensure_config_loaded, walk_source_files @@ -47,8 +45,10 @@ def write_json_file(path: Path, data: Any): json.dump(data, file, indent=2) -def asserts_results_did_not_change(project: str): +def run_mutmut_on_project(project: str) -> dict: """Runs mutmut on this project and verifies that the results stay the same for all mutations.""" + mutmut._reset_globals() + project_path = Path("..").parent / "e2e_projects" / project mutants_path = project_path / "mutants" @@ -58,37 +58,4 @@ def asserts_results_did_not_change(project: str): with change_cwd(project_path): _run([], None) - results = read_all_stats_for_project(project_path) - - snapshot_path = Path("tests") / "e2e" / "snapshots" / (project + ".json") - - if snapshot_path.exists(): - # compare results against previous snapshot - previous_snapshot = read_json_file(snapshot_path) - - err_msg = f'Mutmut results changed for the E2E project \'{project}\'. If this change was on purpose, delete {snapshot_path} and rerun the tests.' - assert results == previous_snapshot, err_msg - else: - # create the first snapshot - write_json_file(snapshot_path, results) - - -def test_my_lib_result_snapshot(): - mutmut._reset_globals() - asserts_results_did_not_change("my_lib") - - -def test_config_result_snapshot(): - mutmut._reset_globals() - asserts_results_did_not_change("config") - - -def test_mutate_only_covered_lines_result_snapshot(): - mutmut._reset_globals() - asserts_results_did_not_change("mutate_only_covered_lines") - - -@pytest.mark.skipif(sys.version_info < (3, 14), reason="Can only test python 3.14 features on 3.14") -def test_python_3_14_result_snapshot(): - mutmut._reset_globals() - asserts_results_did_not_change("py3_14_features") + return read_all_stats_for_project(project_path) diff --git a/tests/e2e/snapshots/config.json b/tests/e2e/snapshots/config.json deleted file mode 100644 index 2617dc47..00000000 --- a/tests/e2e/snapshots/config.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/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 - } -} \ No newline at end of file diff --git a/tests/e2e/snapshots/mutate_only_covered_lines.json b/tests/e2e/snapshots/mutate_only_covered_lines.json deleted file mode 100644 index 38cbeb3a..00000000 --- a/tests/e2e/snapshots/mutate_only_covered_lines.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "mutants/src/mutate_only_covered_lines/__init__.py.meta": { - "mutate_only_covered_lines.x_hello_mutate_only_covered_lines__mutmut_1": 1, - "mutate_only_covered_lines.x_hello_mutate_only_covered_lines__mutmut_2": 1, - "mutate_only_covered_lines.x_hello_mutate_only_covered_lines__mutmut_3": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_1": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_2": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_3": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_4": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_5": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_6": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_7": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_8": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_9": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_10": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_11": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_12": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_13": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_14": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_15": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_16": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_17": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_18": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_19": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_20": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_21": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_22": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_23": 0, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_24": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_25": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_26": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_27": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_28": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_29": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_30": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_31": 1, - "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_32": 1 - } -} \ No newline at end of file diff --git a/tests/e2e/snapshots/my_lib.json b/tests/e2e/snapshots/my_lib.json deleted file mode 100644 index d11b6661..00000000 --- a/tests/e2e/snapshots/my_lib.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "mutants/src/my_lib/__init__.py.meta": { - "my_lib.x_hello__mutmut_1": 1, - "my_lib.x_hello__mutmut_2": 1, - "my_lib.x_hello__mutmut_3": 1, - "my_lib.x_badly_tested__mutmut_1": 0, - "my_lib.x_badly_tested__mutmut_2": 0, - "my_lib.x_badly_tested__mutmut_3": 0, - "my_lib.x_untested__mutmut_1": 33, - "my_lib.x_untested__mutmut_2": 33, - "my_lib.x_untested__mutmut_3": 33, - "my_lib.x_make_greeter__mutmut_1": 1, - "my_lib.x_make_greeter__mutmut_2": 1, - "my_lib.x_make_greeter__mutmut_3": 1, - "my_lib.x_make_greeter__mutmut_4": 1, - "my_lib.x_make_greeter__mutmut_5": 0, - "my_lib.x_make_greeter__mutmut_6": 0, - "my_lib.x_make_greeter__mutmut_7": 0, - "my_lib.x_fibonacci__mutmut_1": 1, - "my_lib.x_fibonacci__mutmut_2": 0, - "my_lib.x_fibonacci__mutmut_3": 0, - "my_lib.x_fibonacci__mutmut_4": 0, - "my_lib.x_fibonacci__mutmut_5": 0, - "my_lib.x_fibonacci__mutmut_6": 0, - "my_lib.x_fibonacci__mutmut_7": 0, - "my_lib.x_fibonacci__mutmut_8": 0, - "my_lib.x_fibonacci__mutmut_9": 0, - "my_lib.x_async_consumer__mutmut_1": 1, - "my_lib.x_async_consumer__mutmut_2": 1, - "my_lib.x_async_generator__mutmut_1": 1, - "my_lib.x_async_generator__mutmut_2": 1, - "my_lib.x_simple_consumer__mutmut_1": 1, - "my_lib.x_simple_consumer__mutmut_2": 1, - "my_lib.x_simple_consumer__mutmut_3": 1, - "my_lib.x_simple_consumer__mutmut_4": 1, - "my_lib.x_simple_consumer__mutmut_5": 1, - "my_lib.x_simple_consumer__mutmut_6": 0, - "my_lib.x_simple_consumer__mutmut_7": 1, - "my_lib.x_double_generator__mutmut_1": 1, - "my_lib.x_double_generator__mutmut_2": 1, - "my_lib.x_double_generator__mutmut_3": 0, - "my_lib.x_double_generator__mutmut_4": 0, - "my_lib.x\u01c1Point\u01c1__init____mutmut_1": 1, - "my_lib.x\u01c1Point\u01c1__init____mutmut_2": 1, - "my_lib.x\u01c1Point\u01c1abs__mutmut_1": 33, - "my_lib.x\u01c1Point\u01c1abs__mutmut_2": 33, - "my_lib.x\u01c1Point\u01c1abs__mutmut_3": 33, - "my_lib.x\u01c1Point\u01c1abs__mutmut_4": 33, - "my_lib.x\u01c1Point\u01c1abs__mutmut_5": 33, - "my_lib.x\u01c1Point\u01c1abs__mutmut_6": 33, - "my_lib.x\u01c1Point\u01c1add__mutmut_1": 0, - "my_lib.x\u01c1Point\u01c1add__mutmut_2": 1, - "my_lib.x\u01c1Point\u01c1add__mutmut_3": 1, - "my_lib.x\u01c1Point\u01c1add__mutmut_4": 0, - "my_lib.x\u01c1Point\u01c1to_origin__mutmut_1": 1, - "my_lib.x\u01c1Point\u01c1to_origin__mutmut_2": 1, - "my_lib.x\u01c1Point\u01c1to_origin__mutmut_3": 0, - "my_lib.x\u01c1Point\u01c1to_origin__mutmut_4": 0, - "my_lib.x\u01c1Point\u01c1__len____mutmut_1": 33, - "my_lib.x_escape_sequences__mutmut_1": 1, - "my_lib.x_escape_sequences__mutmut_2": 0, - "my_lib.x_escape_sequences__mutmut_3": 1, - "my_lib.x_escape_sequences__mutmut_4": 0, - "my_lib.x_escape_sequences__mutmut_5": 0, - "my_lib.x_create_a_segfault_when_mutated__mutmut_1": -11, - "my_lib.x_create_a_segfault_when_mutated__mutmut_2": 0, - "my_lib.x_create_a_segfault_when_mutated__mutmut_3": 0, - "my_lib.x_some_func__mutmut_1": 0, - "my_lib.x_some_func__mutmut_2": 0, - "my_lib.x_some_func__mutmut_3": 1, - "my_lib.x_func_with_star__mutmut_1": 1, - "my_lib.x_func_with_star__mutmut_2": 1, - "my_lib.x_func_with_arbitrary_args__mutmut_1": 1 - } -} \ No newline at end of file diff --git a/tests/e2e/snapshots/py3_14_features.json b/tests/e2e/snapshots/py3_14_features.json deleted file mode 100644 index c25801da..00000000 --- a/tests/e2e/snapshots/py3_14_features.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mutants/src/py3_14_features/__init__.py.meta": { - "py3_14_features.x_get_len__mutmut_1": 0, - "py3_14_features.x_get_len__mutmut_2": 1, - "py3_14_features.x_get_foo_len__mutmut_1": 0, - "py3_14_features.x_get_foo_len__mutmut_2": 1 - } -} \ No newline at end of file diff --git a/tests/e2e/test_e2e_config.py b/tests/e2e/test_e2e_config.py new file mode 100644 index 00000000..84b712b2 --- /dev/null +++ b/tests/e2e/test_e2e_config.py @@ -0,0 +1,26 @@ +from inline_snapshot import snapshot + +from tests.e2e.e2e_utils import run_mutmut_on_project + + +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/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, + }, + } + ) diff --git a/tests/e2e/test_e2e_coverage.py b/tests/e2e/test_e2e_coverage.py new file mode 100644 index 00000000..0bb351c0 --- /dev/null +++ b/tests/e2e/test_e2e_coverage.py @@ -0,0 +1,47 @@ +from inline_snapshot import snapshot + +from tests.e2e.e2e_utils import run_mutmut_on_project + + +def test_mutate_only_covered_lines_result_snapshot(): + assert run_mutmut_on_project("mutate_only_covered_lines") == snapshot( + { + "mutants/src/mutate_only_covered_lines/__init__.py.meta": { + "mutate_only_covered_lines.x_hello_mutate_only_covered_lines__mutmut_1": 1, + "mutate_only_covered_lines.x_hello_mutate_only_covered_lines__mutmut_2": 1, + "mutate_only_covered_lines.x_hello_mutate_only_covered_lines__mutmut_3": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_1": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_2": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_3": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_4": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_5": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_6": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_7": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_8": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_9": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_10": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_11": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_12": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_13": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_14": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_15": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_16": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_17": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_18": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_19": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_20": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_21": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_22": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_23": 0, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_24": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_25": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_26": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_27": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_28": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_29": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_30": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_31": 1, + "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_32": 1, + } + } + ) diff --git a/tests/e2e/test_e2e_my_lib.py b/tests/e2e/test_e2e_my_lib.py new file mode 100644 index 00000000..c2aac7e1 --- /dev/null +++ b/tests/e2e/test_e2e_my_lib.py @@ -0,0 +1,84 @@ +from inline_snapshot import snapshot + +from tests.e2e.e2e_utils import run_mutmut_on_project + + +def test_my_lib_result_snapshot(): + assert run_mutmut_on_project("my_lib") == snapshot( + { + "mutants/src/my_lib/__init__.py.meta": { + "my_lib.x_hello__mutmut_1": 1, + "my_lib.x_hello__mutmut_2": 1, + "my_lib.x_hello__mutmut_3": 1, + "my_lib.x_badly_tested__mutmut_1": 0, + "my_lib.x_badly_tested__mutmut_2": 0, + "my_lib.x_badly_tested__mutmut_3": 0, + "my_lib.x_untested__mutmut_1": 33, + "my_lib.x_untested__mutmut_2": 33, + "my_lib.x_untested__mutmut_3": 33, + "my_lib.x_make_greeter__mutmut_1": 1, + "my_lib.x_make_greeter__mutmut_2": 1, + "my_lib.x_make_greeter__mutmut_3": 1, + "my_lib.x_make_greeter__mutmut_4": 1, + "my_lib.x_make_greeter__mutmut_5": 0, + "my_lib.x_make_greeter__mutmut_6": 0, + "my_lib.x_make_greeter__mutmut_7": 0, + "my_lib.x_fibonacci__mutmut_1": 1, + "my_lib.x_fibonacci__mutmut_2": 0, + "my_lib.x_fibonacci__mutmut_3": 0, + "my_lib.x_fibonacci__mutmut_4": 0, + "my_lib.x_fibonacci__mutmut_5": 0, + "my_lib.x_fibonacci__mutmut_6": 0, + "my_lib.x_fibonacci__mutmut_7": 0, + "my_lib.x_fibonacci__mutmut_8": 0, + "my_lib.x_fibonacci__mutmut_9": 0, + "my_lib.x_async_consumer__mutmut_1": 1, + "my_lib.x_async_consumer__mutmut_2": 1, + "my_lib.x_async_generator__mutmut_1": 1, + "my_lib.x_async_generator__mutmut_2": 1, + "my_lib.x_simple_consumer__mutmut_1": 1, + "my_lib.x_simple_consumer__mutmut_2": 1, + "my_lib.x_simple_consumer__mutmut_3": 1, + "my_lib.x_simple_consumer__mutmut_4": 1, + "my_lib.x_simple_consumer__mutmut_5": 1, + "my_lib.x_simple_consumer__mutmut_6": 0, + "my_lib.x_simple_consumer__mutmut_7": 1, + "my_lib.x_double_generator__mutmut_1": 1, + "my_lib.x_double_generator__mutmut_2": 1, + "my_lib.x_double_generator__mutmut_3": 0, + "my_lib.x_double_generator__mutmut_4": 0, + "my_lib.xǁPointǁ__init____mutmut_1": 1, + "my_lib.xǁPointǁ__init____mutmut_2": 1, + "my_lib.xǁPointǁabs__mutmut_1": 33, + "my_lib.xǁPointǁabs__mutmut_2": 33, + "my_lib.xǁPointǁabs__mutmut_3": 33, + "my_lib.xǁPointǁabs__mutmut_4": 33, + "my_lib.xǁPointǁabs__mutmut_5": 33, + "my_lib.xǁPointǁabs__mutmut_6": 33, + "my_lib.xǁPointǁadd__mutmut_1": 0, + "my_lib.xǁPointǁadd__mutmut_2": 1, + "my_lib.xǁPointǁadd__mutmut_3": 1, + "my_lib.xǁPointǁadd__mutmut_4": 0, + "my_lib.xǁPointǁto_origin__mutmut_1": 1, + "my_lib.xǁPointǁto_origin__mutmut_2": 1, + "my_lib.xǁPointǁto_origin__mutmut_3": 0, + "my_lib.xǁPointǁto_origin__mutmut_4": 0, + "my_lib.xǁPointǁ__len____mutmut_1": 33, + "my_lib.x_escape_sequences__mutmut_1": 1, + "my_lib.x_escape_sequences__mutmut_2": 0, + "my_lib.x_escape_sequences__mutmut_3": 1, + "my_lib.x_escape_sequences__mutmut_4": 0, + "my_lib.x_escape_sequences__mutmut_5": 0, + "my_lib.x_create_a_segfault_when_mutated__mutmut_1": -11, + "my_lib.x_create_a_segfault_when_mutated__mutmut_2": 0, + "my_lib.x_create_a_segfault_when_mutated__mutmut_3": 0, + "my_lib.x_some_func__mutmut_1": 0, + "my_lib.x_some_func__mutmut_2": 0, + "my_lib.x_some_func__mutmut_3": 1, + "my_lib.x_func_with_star__mutmut_1": 1, + "my_lib.x_func_with_star__mutmut_2": 1, + "my_lib.x_func_with_star__mutmut_3": 1, + "my_lib.x_func_with_arbitrary_args__mutmut_1": 1, + } + } + ) diff --git a/tests/e2e/test_e2e_py3_14.py b/tests/e2e/test_e2e_py3_14.py new file mode 100644 index 00000000..9ea2ba00 --- /dev/null +++ b/tests/e2e/test_e2e_py3_14.py @@ -0,0 +1,21 @@ +from inline_snapshot import snapshot +import pytest +import sys + +from tests.e2e.e2e_utils import run_mutmut_on_project + + +@pytest.mark.skipif( + sys.version_info < (3, 14), reason="Can only test python 3.14 features on 3.14" +) +def test_python_3_14_result_snapshot(): + assert run_mutmut_on_project("py3_14_features") == snapshot( + { + "mutants/src/py3_14_features/__init__.py.meta": { + "py3_14_features.x_get_len__mutmut_1": 0, + "py3_14_features.x_get_len__mutmut_2": 1, + "py3_14_features.x_get_foo_len__mutmut_1": 0, + "py3_14_features.x_get_foo_len__mutmut_2": 1, + } + } + ) diff --git a/tests/test_mutation regression.py b/tests/test_mutation regression.py new file mode 100644 index 00000000..2aaded4c --- /dev/null +++ b/tests/test_mutation regression.py @@ -0,0 +1,198 @@ +from inline_snapshot import snapshot +import libcst as cst + +from mutmut.file_mutation import mutate_file_contents, create_trampoline_wrapper + + +def _get_trampoline_wrapper( + source: str, mangled_name: str, class_name: str | None = None +) -> str: + function = cst.ensure_type(cst.parse_statement(source), cst.FunctionDef) + trampoline = create_trampoline_wrapper( + function, mangled_name, class_name=class_name + ) + return cst.Module([trampoline]).code.strip() + + +def test_create_trampoline_wrapper_async_method(): + source = "async def foo(a: str, b, *args, **kwargs) -> dict[str, int]: pass" + + assert _get_trampoline_wrapper(source, "x_foo__mutmut") == snapshot("""\ +async def foo(a: str, b, *args, **kwargs) -> dict[str, int]: + args = [a, b, *args] + kwargs = {**kwargs} + return await _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None)\ +""") + + +def test_create_trampoline_wrapper_async_generator(): + source = """ +async def foo(): + for i in range(10): + yield i + """ + + assert _get_trampoline_wrapper(source, "x_foo__mutmut") == snapshot("""\ +async def foo(): + args = [] + kwargs = {} + async for i in _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None): + yield i\ +""") + + +def test_create_trampoline_wrapper_with_positionals_only_args(): + source = "def foo(p1, p2=None, /, p_or_kw=None, *, kw): pass" + + assert _get_trampoline_wrapper(source, "x_foo__mutmut") == snapshot("""\ +def foo(p1, p2=None, /, p_or_kw=None, *, kw): + args = [p1, p2, p_or_kw] + kwargs = {'kw': kw} + return _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None)\ +""") + + +def test_create_trampoline_wrapper_for_class_method(): + source = "def foo(self, a, b): pass" + + assert _get_trampoline_wrapper( + source, "x_foo__mutmut", class_name="Person" + ) == snapshot("""\ +def foo(self, a, b): + args = [a, b] + kwargs = {} + return _mutmut_trampoline(object.__getattribute__(self, 'x_foo__mutmut_orig'), object.__getattribute__(self, 'x_foo__mutmut_mutants'), args, kwargs, self)\ +""") + + +def test_module_mutation(): + """Regression test, for a complete module with functions, type annotations and a class""" + + source = """from __future__ import division +import lib + +lib.foo() + +def foo(a: list[int], b): + return a[0] > b + +def bar(): + yield 1 + +class Adder: + def __init__(self, amount): + self.amount = amount + + def add(self, value): + return self.amount + value + +print(Adder(1).add(2))""" + + src, _ = mutate_file_contents("file.py", source) + + assert src == snapshot('''\ +from __future__ import division +import lib + +lib.foo() +from typing import Annotated +from typing import Callable +from typing import ClassVar + +MutantDict = Annotated[dict[str, Callable], "Mutant"] + + +def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None): + """Forward call to original or mutated function, depending on the environment""" + import os + mutant_under_test = os.environ['MUTANT_UNDER_TEST'] + if mutant_under_test == 'fail': + from mutmut.__main__ import MutmutProgrammaticFailException + raise MutmutProgrammaticFailException('Failed programmatically') \n\ + elif mutant_under_test == 'stats': + from mutmut.__main__ import record_trampoline_hit + record_trampoline_hit(orig.__module__ + '.' + orig.__name__) + # (for class methods, orig is bound and thus does not need the explicit self argument) + result = orig(*call_args, **call_kwargs) + return result + prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_' + if not mutant_under_test.startswith(prefix): + result = orig(*call_args, **call_kwargs) + return result + mutant_name = mutant_under_test.rpartition('.')[-1] + if self_arg is not None: + # call to a class method where self is not bound + result = mutants[mutant_name](self_arg, *call_args, **call_kwargs) + else: + result = mutants[mutant_name](*call_args, **call_kwargs) + return result + +def x_foo__mutmut_orig(a: list[int], b): + return a[0] > b + +def x_foo__mutmut_1(a: list[int], b): + return a[1] > b + +def x_foo__mutmut_2(a: list[int], b): + return a[0] >= b + +def foo(a: list[int], b): + args = [a, b] + kwargs = {} + return _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None) + +x_foo__mutmut_mutants : ClassVar[MutantDict] = { +'x_foo__mutmut_1': x_foo__mutmut_1, \n\ + 'x_foo__mutmut_2': x_foo__mutmut_2 +} +x_foo__mutmut_orig.__name__ = 'x_foo' + +def x_bar__mutmut_orig(): + yield 1 + +def x_bar__mutmut_1(): + yield 2 + +def bar(): + args = [] + kwargs = {} + return _mutmut_trampoline(x_bar__mutmut_orig, x_bar__mutmut_mutants, args, kwargs, None) + +x_bar__mutmut_mutants : ClassVar[MutantDict] = { +'x_bar__mutmut_1': x_bar__mutmut_1 +} +x_bar__mutmut_orig.__name__ = 'x_bar' + +class Adder: + def xǁAdderǁ__init____mutmut_orig(self, amount): + self.amount = amount + def xǁAdderǁ__init____mutmut_1(self, amount): + self.amount = None + def __init__(self, amount): + args = [amount] + kwargs = {} + return _mutmut_trampoline(object.__getattribute__(self, 'xǁAdderǁ__init____mutmut_orig'), object.__getattribute__(self, 'xǁAdderǁ__init____mutmut_mutants'), args, kwargs, self) + \n\ + xǁAdderǁ__init____mutmut_mutants : ClassVar[MutantDict] = { + 'xǁAdderǁ__init____mutmut_1': xǁAdderǁ__init____mutmut_1 + } + xǁAdderǁ__init____mutmut_orig.__name__ = 'xǁAdderǁ__init__' + + def xǁAdderǁadd__mutmut_orig(self, value): + return self.amount + value + + def xǁAdderǁadd__mutmut_1(self, value): + return self.amount - value + + def add(self, value): + args = [value] + kwargs = {} + return _mutmut_trampoline(object.__getattribute__(self, 'xǁAdderǁadd__mutmut_orig'), object.__getattribute__(self, 'xǁAdderǁadd__mutmut_mutants'), args, kwargs, self) + \n\ + xǁAdderǁadd__mutmut_mutants : ClassVar[MutantDict] = { + 'xǁAdderǁadd__mutmut_1': xǁAdderǁadd__mutmut_1 + } + xǁAdderǁadd__mutmut_orig.__name__ = 'xǁAdderǁadd' + +print(Adder(1).add(2))\ +''') diff --git a/tests/test_mutation.py b/tests/test_mutation.py index b83c671d..831b83e0 100644 --- a/tests/test_mutation.py +++ b/tests/test_mutation.py @@ -4,6 +4,7 @@ import libcst as cst import pytest +from inline_snapshot import snapshot import mutmut from mutmut.__main__ import ( @@ -657,13 +658,14 @@ def inner(): def test_module_mutation(): + """Regression test, for a complete module with functions, type annotations and a class""" source = """from __future__ import division import lib lib.foo() -def foo(a, b): - return a > b +def foo(a: list[int], b): + return a[0] > b def bar(): yield 1 @@ -679,27 +681,61 @@ def add(self, value): src, _ = mutate_file_contents("file.py", source) - assert src == f"""from __future__ import division + assert src == snapshot('''\ +from __future__ import division import lib lib.foo() -{trampoline_impl.strip()} - -def x_foo__mutmut_orig(a, b): - return a > b - -def x_foo__mutmut_1(a, b): - return a >= b - -x_foo__mutmut_mutants : ClassVar[MutantDict] = {{ -'x_foo__mutmut_1': x_foo__mutmut_1 -}} - -def foo(*args, **kwargs): - result = _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs) - return result - -_mutmut_copy_signature(foo, x_foo__mutmut_orig) +from typing import Annotated +from typing import Callable +from typing import ClassVar + +MutantDict = Annotated[dict[str, Callable], "Mutant"] + + +def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None): + """Forward call to original or mutated function, depending on the environment""" + import os + mutant_under_test = os.environ['MUTANT_UNDER_TEST'] + if mutant_under_test == 'fail': + from mutmut.__main__ import MutmutProgrammaticFailException + raise MutmutProgrammaticFailException('Failed programmatically') \n\ + elif mutant_under_test == 'stats': + from mutmut.__main__ import record_trampoline_hit + record_trampoline_hit(orig.__module__ + '.' + orig.__name__) + # (for class methods, orig is bound and thus does not need the explicit self argument) + result = orig(*call_args, **call_kwargs) + return result + prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_' + if not mutant_under_test.startswith(prefix): + result = orig(*call_args, **call_kwargs) + return result + mutant_name = mutant_under_test.rpartition('.')[-1] + if self_arg is not None: + # call to a class method where self is not bound + result = mutants[mutant_name](self_arg, *call_args, **call_kwargs) + else: + result = mutants[mutant_name](*call_args, **call_kwargs) + return result + +def x_foo__mutmut_orig(a: list[int], b): + return a[0] > b + +def x_foo__mutmut_1(a: list[int], b): + return a[1] > b + +def x_foo__mutmut_2(a: list[int], b): + return a[0] >= b + +def foo(a: list[int], b): + args = [a, b] + kwargs = {} + return _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None) + +x_foo__mutmut_mutants : ClassVar[MutantDict] = { +'x_foo__mutmut_1': x_foo__mutmut_1, \n\ + 'x_foo__mutmut_2': x_foo__mutmut_2 +} x_foo__mutmut_orig.__name__ = 'x_foo' def x_bar__mutmut_orig(): @@ -708,15 +744,14 @@ def x_bar__mutmut_orig(): def x_bar__mutmut_1(): yield 2 -x_bar__mutmut_mutants : ClassVar[MutantDict] = {{ -'x_bar__mutmut_1': x_bar__mutmut_1 -}} - -def bar(*args, **kwargs): - result = _mutmut_trampoline(x_bar__mutmut_orig, x_bar__mutmut_mutants, args, kwargs) - return result +def bar(): + args = [] + kwargs = {} + return _mutmut_trampoline(x_bar__mutmut_orig, x_bar__mutmut_mutants, args, kwargs, None) -_mutmut_copy_signature(bar, x_bar__mutmut_orig) +x_bar__mutmut_mutants : ClassVar[MutantDict] = { +'x_bar__mutmut_1': x_bar__mutmut_1 +} x_bar__mutmut_orig.__name__ = 'x_bar' class Adder: @@ -724,16 +759,14 @@ def xǁAdderǁ__init____mutmut_orig(self, amount): self.amount = amount def xǁAdderǁ__init____mutmut_1(self, amount): self.amount = None - - xǁAdderǁ__init____mutmut_mutants : ClassVar[MutantDict] = {{ + def __init__(self, amount): + args = [amount] + kwargs = {} + return _mutmut_trampoline(object.__getattribute__(self, 'xǁAdderǁ__init____mutmut_orig'), object.__getattribute__(self, 'xǁAdderǁ__init____mutmut_mutants'), args, kwargs, self) + \n\ + xǁAdderǁ__init____mutmut_mutants : ClassVar[MutantDict] = { 'xǁAdderǁ__init____mutmut_1': xǁAdderǁ__init____mutmut_1 - }} - - def __init__(self, *args, **kwargs): - result = _mutmut_trampoline(object.__getattribute__(self, "xǁAdderǁ__init____mutmut_orig"), object.__getattribute__(self, "xǁAdderǁ__init____mutmut_mutants"), args, kwargs, self) - return result - - _mutmut_copy_signature(__init__, xǁAdderǁ__init____mutmut_orig) + } xǁAdderǁ__init____mutmut_orig.__name__ = 'xǁAdderǁ__init__' def xǁAdderǁadd__mutmut_orig(self, value): @@ -741,16 +774,16 @@ def xǁAdderǁadd__mutmut_orig(self, value): def xǁAdderǁadd__mutmut_1(self, value): return self.amount - value - - xǁAdderǁadd__mutmut_mutants : ClassVar[MutantDict] = {{ + + def add(self, value): + args = [value] + kwargs = {} + return _mutmut_trampoline(object.__getattribute__(self, 'xǁAdderǁadd__mutmut_orig'), object.__getattribute__(self, 'xǁAdderǁadd__mutmut_mutants'), args, kwargs, self) + \n\ + xǁAdderǁadd__mutmut_mutants : ClassVar[MutantDict] = { 'xǁAdderǁadd__mutmut_1': xǁAdderǁadd__mutmut_1 - }} - - def add(self, *args, **kwargs): - result = _mutmut_trampoline(object.__getattribute__(self, "xǁAdderǁadd__mutmut_orig"), object.__getattribute__(self, "xǁAdderǁadd__mutmut_mutants"), args, kwargs, self) - return result - - _mutmut_copy_signature(add, xǁAdderǁadd__mutmut_orig) + } xǁAdderǁadd__mutmut_orig.__name__ = 'xǁAdderǁadd' -print(Adder(1).add(2))""" +print(Adder(1).add(2))\ +''') diff --git a/tests/test_mutmut3.py b/tests/test_mutmut3.py deleted file mode 100644 index 643be059..00000000 --- a/tests/test_mutmut3.py +++ /dev/null @@ -1,75 +0,0 @@ -from mutmut.file_mutation import mutate_file_contents -from mutmut.trampoline_templates import trampoline_impl - - -def mutated_module(source: str) -> str: - mutated_code, _ = mutate_file_contents('', source) - return mutated_code - - -def test_mutate_file_contents(): - source = """ -a + 1 - -def foo(a, b, c): - return a + b * c -""" - trampolines = trampoline_impl.removesuffix('\n\n') - - expected = f""" -a + 1{trampolines} - -def x_foo__mutmut_orig(a, b, c): - return a + b * c - -def x_foo__mutmut_1(a, b, c): - return a - b * c - -def x_foo__mutmut_2(a, b, c): - return a + b / c - -x_foo__mutmut_mutants : ClassVar[MutantDict] = {{ -'x_foo__mutmut_1': x_foo__mutmut_1, - 'x_foo__mutmut_2': x_foo__mutmut_2 -}} - -def foo(*args, **kwargs): - result = _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs) - return result - -_mutmut_copy_signature(foo, x_foo__mutmut_orig) -x_foo__mutmut_orig.__name__ = 'x_foo' -""" - - result = mutated_module(source) - - assert result == expected - - -def test_avoid_annotations(): - source = """ -def foo(a: List[int]) -> int: - return 1 -""" - - expected = trampoline_impl.removesuffix('\n\n') + """ -def x_foo__mutmut_orig(a: List[int]) -> int: - return 1 -def x_foo__mutmut_1(a: List[int]) -> int: - return 2 - -x_foo__mutmut_mutants : ClassVar[MutantDict] = { -'x_foo__mutmut_1': x_foo__mutmut_1 -} - -def foo(*args, **kwargs): - result = _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs) - return result - -_mutmut_copy_signature(foo, x_foo__mutmut_orig) -x_foo__mutmut_orig.__name__ = 'x_foo' -""" - - result = mutated_module(source) - - assert result == expected diff --git a/uv.lock b/uv.lock index 7c1ecbac..58b84f53 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,24 @@ resolution-markers = [ "python_full_version < '3.13'", ] +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + [[package]] name = "click" version = "8.0.0" @@ -78,6 +96,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -87,6 +114,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "inline-snapshot" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pytest" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/74/19294067d5b5b78144eab2aacec85b998c2d2ea6b3d24eefd9a90255d7aa/inline_snapshot-0.32.0.tar.gz", hash = "sha256:57fa3df325284d0d14def5dab9ac5da89e383f085bea9a7be51fdeab65e59ced", size = 2623331, upload-time = "2026-02-13T19:51:54.469Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/25/0e84a6322e5fdb1bf67870b2269151449f4894987b26c78718918dd64ea6/inline_snapshot-0.32.0-py3-none-any.whl", hash = "sha256:b522ae2c891f666e80213c5f9677ec6fd4a2a7d334ab9d6ce745675bec6a40f0", size = 84087, upload-time = "2026-02-13T19:51:52.604Z" }, +] + [[package]] name = "libcst" version = "1.8.5" @@ -224,7 +268,9 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "inline-snapshot" }, { name = "pytest-asyncio" }, + { name = "ruff" }, ] [package.metadata] @@ -239,7 +285,11 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "pytest-asyncio", specifier = ">=1.0.0" }] +dev = [ + { name = "inline-snapshot", specifier = ">=0.32.0" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "ruff", specifier = ">=0.15.1" }, +] [[package]] name = "packaging" @@ -279,7 +329,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.2.0" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -287,23 +337,26 @@ dependencies = [ { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/9d/78b3785134306efe9329f40815af45b9215068d6ae4747ec0bc91ff1f4aa/pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f", size = 1422883, upload-time = "2024-04-27T23:34:55.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/43/6b1debd95ecdf001bc46789a933f658da3f9738c65f32db3f4e8f2a4ca97/pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233", size = 339229, upload-time = "2024-04-27T23:34:52.413Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.0.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] [[package]] @@ -407,6 +460,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, ] +[[package]] +name = "ruff" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, +] + [[package]] name = "setproctitle" version = "1.1"