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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Mutant files
e2e_projects/**/mutants
e2e_projects/**/mutants*
/mutants
tests/data/**/*.py.meta

Expand Down
46 changes: 44 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ 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.

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 +208,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 +244,30 @@ whitelist lines are:
to continue, but it's slower.


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
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"]
94 changes: 93 additions & 1 deletion 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
from typing import AsyncGenerator, 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,59 @@ 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

async def async_get(self) -> int:
await asyncio.sleep(0.001)
return self.value

@staticmethod
async def async_get_all() -> AsyncGenerator[Color, None]:
"""return type hint here is "wrong" (it's technically AsyncGenerator[int, None])
but using Color in this context allows us to have a forward reference to the Color class
that doesn't require quoting the class name (eg. "Color") in the type hint
that we otherwise would not be able to have in py3.10, and allows us to test that
trampoline templates are resilient to forward references when using the external trampoline
pattern.
"""
for i in (Color.RED, Color.GREEN, Color.BLUE):
await asyncio.sleep(0.001)
yield i

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
92 changes: 92 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,73 @@ 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


def test_color_darken():
assert Color.GREEN.darken() > 0


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


@pytest.mark.asyncio
async def test_color_async_get():
assert await Color.RED.async_get() == 1
assert await Color.GREEN.async_get() == 2
assert await Color.BLUE.async_get() == 3


@pytest.mark.asyncio
async def test_color_async_get_all():
results = []
async for color in Color.async_get_all():
results.append(color)
assert results == [Color.RED, Color.GREEN, Color.BLUE]
69 changes: 67 additions & 2 deletions scripts/run_tests.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,70 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/.."
docker build -t mutmut -f ./docker/Dockerfile.test .
docker run --rm -t -v "$(pwd)":/mutmut mutmut "$@"

usage() {
echo "Usage: $0 [--py 3.10,3.12,3.14] [--ff] [-- pytest args...]"
echo " --py Comma-separated Python versions to test."
echo " Default: 3.10"
echo " --ff Stop on first failure instead of running all versions."
echo ""
echo " Everything after '--' is forwarded to pytest."
exit 1
}

PY_VERSIONS="3.10"
FAIL_FAST=false

while [[ $# -gt 0 ]]; do
case "$1" in
--py)
PY_VERSIONS="$2"
shift 2
;;
--ff)
FAIL_FAST=true
shift
;;
-h|--help)
usage
;;
--)
shift
break
;;
*)
break
;;
esac
done

IFS=',' read -r -a VERSIONS <<< "$PY_VERSIONS"
RESULTS=()
EXIT_CODE=0

print_results() {
echo ""
echo "=== Results ==="
for RESULT in "${RESULTS[@]}"; do
echo " $RESULT"
done
}

for VER in "${VERSIONS[@]}"; do
IMAGE_NAME="mutmut-test-${VER}"
docker build -t "$IMAGE_NAME" --build-arg "PYTHON_VERSION=$VER" -f ./docker/Dockerfile.test .
if docker run --rm -t -v "$(pwd)":/mutmut "$IMAGE_NAME" "$@"; then
RESULTS+=("Python $VER: PASSED")
else
EXIT_CODE=1
RESULTS+=("Python $VER: FAILED")
if [[ "$FAIL_FAST" == true ]]; then
print_results
exit 1
fi
fi
done

print_results

exit $EXIT_CODE
Loading
Loading