Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
---------

Unreleased
~~~~~~~~~~

* Add ``mutmut.is_mutmut_generated_name`` so tests that inspect modules or classes can skip the functions and dicts mutmut adds to mutated code

3.8.0
~~~~~~~~~~

Expand Down
23 changes: 23 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,29 @@ directory. If you want to make sure you run a full mutmut run you can delete
this directory to start from scratch.


Tests that inspect modules or classes
-------------------------------------

To switch between mutants, mutmut adds functions and dicts to the mutated
modules and classes, such as ``x_foo__mutmut_orig``,
``xǁMyClassǁmethod__mutmut_1`` and ``mutants_x_foo__mutmut``. Their names
don't start with an underscore, so a test that walks ``vars()``, ``dir()`` or
``inspect.getmembers()`` sees them as public attributes. A test that checks
every public method has a docstring, for example, fails in the stats run
and no mutants get tested.

Use ``mutmut.is_mutmut_generated_name`` to skip them:

.. code-block:: python

import mutmut

public = [
name for name in vars(MyClass)
if not name.startswith("_") and not mutmut.is_mutmut_generated_name(name)
]


Mutation score badges
---------------------

Expand Down
1 change: 1 addition & 0 deletions src/mutmut/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from mutmut.configuration import reset_config
from mutmut.state import reset_state
from mutmut.state import state
from mutmut.utils.format_utils import is_mutmut_generated_name as is_mutmut_generated_name

__version__ = importlib.metadata.version("mutmut")

Expand Down
11 changes: 11 additions & 0 deletions src/mutmut/utils/format_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ def is_mutated_method_name(name: str) -> bool:
return name.startswith(("x_", "xǁ")) and "__mutmut" in name


def is_mutmut_generated_name(name: str) -> bool:
"""Return True if ``name`` is a module or class attribute that mutmut added to the mutated code.

Mutated files define extra functions (``x_foo__mutmut_orig``, ``xǁClsǁmethod__mutmut_1``, ...)
and the ``mutants_...`` dicts that map mutant names to them. Their names do not start with an
underscore, so tests that inspect a module or class (``vars()``, ``dir()``, ``inspect.getmembers()``)
see them as public attributes. Filter with this function to ignore them.
"""
return is_mutated_method_name(name) or (name.startswith("mutants_x") and name.endswith("__mutmut"))


def strip_prefix(s: str, *, prefix: str, strict: bool = False) -> str:
if s.startswith(prefix):
return s[len(prefix) :]
Expand Down
86 changes: 86 additions & 0 deletions tests/utils/test_format_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import types

import pytest

import mutmut
from mutmut.mutation.file_mutation import mutate_file_contents
from mutmut.utils.format_utils import is_mutmut_generated_name

SOURCE = '''
from dataclasses import dataclass


@dataclass
class DateRange:
"""A closed date interval."""

start: int
end: int

def __post_init__(self):
if self.end < self.start:
raise ValueError("end before start")

def length(self):
"""Number of days in the range."""
return self.end - self.start + 1


def span(r):
"""Length of the range."""
return r.end - r.start + 1
'''


def _load_mutated_module() -> types.ModuleType:
module = types.ModuleType("demo")
exec(compile(mutate_file_contents("demo.py", SOURCE).code, "demo.py", "exec"), module.__dict__)
return module


def _public(namespace: dict[str, object]) -> set[str]:
return {name for name in namespace if not name.startswith("_")}


def test_is_mutmut_generated_name_is_public_api():
assert mutmut.is_mutmut_generated_name is is_mutmut_generated_name


def test_filters_every_generated_class_member():
module = _load_mutated_module()

public_members = _public(vars(module.DateRange))
assert any(is_mutmut_generated_name(name) for name in public_members), "precondition: mutmut adds members"
assert {name for name in public_members if not is_mutmut_generated_name(name)} == {"length"}


def test_filters_every_generated_module_attribute():
module = _load_mutated_module()

public_names = _public(vars(module))
assert {name for name in public_names if not is_mutmut_generated_name(name)} == {
"DateRange",
"MutantDict",
"dataclass",
"span",
}


@pytest.mark.parametrize(
"name",
[
"length",
"span",
"x",
"x_",
"xǁ",
"x_value",
"mutants",
"mutants_x",
"mutants_count",
"mutmut_orig",
"__post_init__",
],
)
def test_does_not_match_user_names(name):
assert not is_mutmut_generated_name(name)