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
134 changes: 132 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,33 @@ it will try to figure out where the code to mutate is.


You can stop the mutation run at any time and mutmut will restart where you
left off. It will continue where it left off, and re-test functions that were
modified since last run.
left off.

Incremental Testing
~~~~~~~~~~~~~~~~~~~
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet with this PR :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol getting ahead of myself, rebasing got a little messy as you can see thanks for catching it 😅


Mutmut is designed for incremental workflows. It remembers which mutants have
been tested and their results, so subsequent runs skip already-tested mutants.

**Function-level change detection:** Mutmut computes a hash of each function's
source code. When you modify a function, mutmut detects the change and
automatically re-tests all mutants in that function. Unchanged functions keep
their previous results.

**Limitation:** Change detection only tracks direct function changes, not
transitive dependencies. If function A calls function B, and you modify B,
mutants in A are not automatically re-tested. For significant changes to
shared utilities, use ``mutmut run "module*"`` to re-test affected modules,
or delete the ``mutants/`` directory for a full re-run.

This means you can:

- Run ``mutmut run``, stop partway through, and continue later
- Modify your source code and re-run - only changed functions are re-tested
- Update your tests and use ``mutmut browse`` to selectively re-test mutants

The mutation data is stored in the ``mutants/`` directory. Delete this
directory to start completely fresh.

To work with the results, use `mutmut browse` where you can see the mutants,
retest them when you've updated your tests.
Expand Down Expand Up @@ -209,6 +234,25 @@ to failing tests.
debug=true


Disable setproctitle (macOS)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Mutmut uses ``setproctitle`` to show the current mutant name in the process
list, which is helpful for monitoring long runs. However, ``setproctitle``
uses CoreFoundation APIs on macOS that are not fork-safe, causing segfaults
in child processes.

By default, mutmut automatically disables ``setproctitle`` on macOS and
enables it on other platforms. If you need to override this (e.g. to enable it on
macOS at your own risk, or to disable it on other platforms), set ``use_setproctitle``:

.. code-block:: toml

# pyproject.toml
[tool.mutmut]
use_setproctitle = false


Whitelisting
~~~~~~~~~~~~

Expand All @@ -226,6 +270,92 @@ whitelist lines are:
to continue, but it's slower.


Enum Classes and Metaclass Compatibility
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels more like something for HISTORY.rst than the README. The README is already pretty long (we should split it up into multiple files at some point imo) and mentioning everything that mutmut supports seems unnecessary (we also don't mention that we support methods, classes, subclasses, etc.).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, probably a case of over-eager documentation here.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Mutmut 3.x fully supports mutating enum classes. Methods inside enum classes
(``Enum``, ``IntEnum``, ``Flag``, ``IntFlag``, ``StrEnum``) are automatically
mutated using an external injection pattern that avoids conflicts with the
enum metaclass.

This means enums with methods like:

.. code-block:: python

from enum import Enum

class Color(Enum):
RED = 1
GREEN = 2

def describe(self):
return self.name.lower()

@staticmethod
def count():
return 3

...will have their methods mutated just like regular class methods.

**Disabling Enum Mutation**

If you prefer to skip enum mutation entirely, you can disable it in your config:

.. code-block:: toml

# pyproject.toml
[tool.mutmut]
mutate_enums = false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an option you need? I don't feel like disabling enum mutations is a use case we need, and would probably be better part of #47

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was me trying to adhere (probably a bit too much) to leaving at least the option of having the base behaviour be relatively unaffected.

A codebase which uses mutmut and a large number of dataclasses with basic data manipulation functions, which are of generally low mutant value, may see a very large increase in the number of mutants for little gain for example, so I wanted to give them an easy out of what would become the new default which I in general I tried to leave to current behaviour. I just figured core language features like Enums merit a more "opt out" type approach, though I can see the case to remove this entirely and leave it as a defacto truth that mutmut will try and support more language features over time.


Or skip a specific enum class using the pragma:

.. code-block:: python

class Color(Enum): # pragma: no mutate class
RED = 1
GREEN = 2

def describe(self):
return f"Color is {self.name}"

This tells mutmut to completely skip the class—no mutations will be created
for any methods.

Both syntax styles are supported:

- ``# pragma: no mutate class``
- ``# pragma: no mutate: class``

**Note:** The regular ``# pragma: no mutate`` on a class line only prevents
mutations on that specific line. It does NOT prevent mutations inside methods.
Use ``# pragma: no mutate class`` to skip the entire class (kept for backward
compatibility with <v3.5.0).


Skipping Entire Functions
~~~~~~~~~~~~~~~~~~~~~~~~~

Similarly, you can skip an entire function from mutation using
``# pragma: no mutate function``:

.. code-block:: python

def complex_algorithm(): # pragma: no mutate function
# This function won't be mutated at all
return some_complex_calculation()

Both syntax styles are supported:

- ``# pragma: no mutate function``
- ``# pragma: no mutate: function``

This is useful for functions that:

- Have complex side effects that make mutation testing impractical
- Are performance-critical and you want to avoid trampoline overhead
- Are known to cause issues with the mutation testing framework


Modifying pytest arguments
~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Empty file added core
Empty file.
15 changes: 11 additions & 4 deletions docker/Dockerfile.test
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
FROM python:3.10.19-slim-trixie AS base
ARG PYTHON_VERSION=3.10

FROM python:${PYTHON_VERSION}-slim-trixie AS base

WORKDIR /mutmut

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

ENV UV_PROJECT_ENVIRONMENT=/opt/venv
ARG PYTHON_VERSION
ENV UV_PROJECT_ENVIRONMENT=/opt/venv \
UV_PYTHON_PREFERENCE=only-system \
UV_PYTHON=${PYTHON_VERSION}

COPY . .
COPY pyproject.toml uv.lock ./

RUN uv sync --group dev
RUN uv sync --group dev --no-install-project

COPY . .

ENTRYPOINT ["uv", "run", "pytest"]
CMD ["--verbose"]
75 changes: 75 additions & 0 deletions e2e_projects/my_lib/src/my_lib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from collections.abc import Callable
from enum import Enum
from functools import cache
from typing import Union
import ctypes
import asyncio


def my_decorator(func): # pragma: no mutate: function
return func


def hello() -> str:
return "Hello from my-lib!"

Expand All @@ -14,6 +19,13 @@ def badly_tested() -> str:
def untested() -> str:
return "Mutants for this method should survive"

def skip_this_function() -> int: # pragma: no mutate: function
return 1 + 2 * 3

def also_skip_this_function() -> str: # pragma: no mutate function
return "should" + " not" + " mutate"


def make_greeter(name: Union[str, None]) -> Callable[[], str]:
def hi():
if name:
Expand Down Expand Up @@ -88,6 +100,30 @@ def from_coords(coords) -> 'Point':
def coords(self):
return self.x, self.y

@staticmethod
def skip_static_decorator_pragma(a: int, b: int) -> int: # pragma: no mutate: function
return a + b * 2

@classmethod
def skip_class_decorator_pragma(cls, value: int) -> "Point": # pragma: no mutate: function
return cls(value + 1, value * 2)

def skip_instance_method_pragma(self) -> int: # pragma: no mutate: function
return self.x + self.y * 2

@staticmethod # pragma: no mutate: function
def pragma_on_staticmethod_decorator(a: int, b: int) -> int:
return a + b * 2

@classmethod # pragma: no mutate: function
def pragma_on_classmethod_decorator(cls, value: int) -> "Point":
return cls(value + 1, value * 2)

@my_decorator
@classmethod
def skip_multi_decorator(cls, value: int) -> "Point":
return cls(value + 1, value * 2)


def escape_sequences():
return "foo" \
Expand All @@ -111,3 +147,42 @@ def func_with_star(a, /, b, *, c, **kwargs):
def func_with_arbitrary_args_clone(*args, **kwargs): pass # pragma: no mutate
def func_with_arbitrary_args(*args, **kwargs):
return len(args) + len(kwargs)


class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3

def is_primary(self) -> bool:
return self in (Color.RED, Color.GREEN, Color.BLUE)

def darken(self) -> int:
return self.value - 1

@staticmethod
def from_name(name: str) -> "Color":
return Color[name.upper()]

@classmethod
def default(cls) -> "Color":
return cls.RED


class SkipThisClass: # pragma: no mutate: class
def method_one(self) -> int:
return 1 + 2

def method_two(self) -> str:
return "hello" + " world"

@staticmethod
def static_method() -> int:
return 3 * 4


class AlsoSkipThisClass: # pragma: no mutate class
VALUE = 10 + 20

def compute(self) -> int:
return self.VALUE * 2
79 changes: 79 additions & 0 deletions e2e_projects/my_lib/tests/test_my_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,28 @@ def test_point():
def test_point_from_coords():
assert Point.from_coords((1, 2)).x == 1


def test_point_skip_static_decorator_pragma():
assert Point.skip_static_decorator_pragma(3, 4) == 11


def test_point_skip_class_decorator_pragma():
p = Point.skip_class_decorator_pragma(5)
assert p.x == 6
assert p.y == 10


def test_point_skip_instance_method_pragma():
p = Point(3, 4)
assert p.skip_instance_method_pragma() == 11


def test_point_skip_multi_decorator():
p = Point.skip_multi_decorator(5)
assert p.x == 6
assert p.y == 10


def test_fibonacci():
assert fibonacci(1) == 1
assert cached_fibonacci(1) == 1
Expand Down Expand Up @@ -66,3 +88,60 @@ def test_signature_functions_are_callable():

def test_signature_is_coroutine():
assert asyncio.iscoroutinefunction(async_consumer)


# Tests for enum mutation
def test_color_enum_values():
assert Color.RED.value == 1
assert Color.GREEN.value == 2
assert Color.BLUE.value == 3


def test_color_is_primary():
assert Color.RED.is_primary() is True
assert Color.GREEN.is_primary() is True


def test_color_darken():
assert Color.GREEN.darken() == 1
assert Color.BLUE.darken() == 2


def test_color_from_name():
assert Color.from_name("red") == Color.RED
assert Color.from_name("BLUE") == Color.BLUE


def test_color_default():
assert Color.default() == Color.RED


def test_skip_this_function():
assert skip_this_function() == 7


def test_also_skip_this_function():
assert also_skip_this_function() == "should not mutate"


def test_skip_this_class():
obj = SkipThisClass()
assert obj.method_one() == 3
assert obj.method_two() == "hello world"
assert SkipThisClass.static_method() == 12


def test_also_skip_this_class():
obj = AlsoSkipThisClass()
assert obj.VALUE == 30
assert obj.compute() == 60


def test_pragma_on_staticmethod_decorator():
assert Point.pragma_on_staticmethod_decorator(3, 4) == 11


def test_pragma_on_classmethod_decorator():
p = Point.pragma_on_classmethod_decorator(5)
assert p.x == 6
assert p.y == 10
Loading