Skip to content
Merged
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: 2 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Changelog
Unreleased
~~~~~~~~~~

* Fix the trampoline dropping a generator's return value, so ``yield from`` on a mutated generator no longer yields ``None`` instead of its result

* Per-function source hashing for incremental cache invalidation — only re-test mutants in functions that changed

* Cross-call dependency tracking — invalidate mutants in callers when a called function changes
Expand Down
5 changes: 4 additions & 1 deletion src/mutmut/mutation/trampoline.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ def trampoline(*args: P.args, **kwargs: P.kwargs) -> R:

@wraps(decorated_func)
def _trampoline_wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # type: ignore
yield from trampoline(*args, **kwargs) # type: ignore
# ``return`` the delegation result so a generator's StopIteration value
# (``return`` inside the generator) is forwarded to the caller's
# ``yield from``, matching the PEP 380 expansion.
return (yield from trampoline(*args, **kwargs)) # type: ignore
elif inspect.iscoroutinefunction(decorated_func):

@wraps(decorated_func)
Expand Down
40 changes: 40 additions & 0 deletions tests/mutation/test_trampoline.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

mutants_simple_func = {}
mutants_generator_func = {}
mutants_returning_generator_func = {}
mutants_async_func = {}
mutants_async_generator_func = {}
mutants_cleanup_async_gen = {}
Expand Down Expand Up @@ -53,6 +54,25 @@ def generator_func_1(numbers: list[int]):
yield n * 3


@wrap_in_trampoline(mutants_returning_generator_func)
def returning_generator_func(numbers: list[int]):
for n in numbers:
yield n * 2
return "done"


def returning_generator_func_orig(numbers: list[int]):
for n in numbers:
yield n * 2
return "done"


def returning_generator_func_1(numbers: list[int]):
for n in numbers:
yield n * 3
return "done"


@wrap_in_trampoline(mutants_async_func)
async def async_func(a: int, b: int):
await asyncio.sleep(0)
Expand Down Expand Up @@ -199,6 +219,8 @@ def __init__(self, number: int):
mutants_simple_func["simple_func__mutmut_1"] = simple_func_1
mutants_generator_func["_mutmut_orig"] = generator_func_orig
mutants_generator_func["generator_func__mutmut_1"] = generator_func_1
mutants_returning_generator_func["_mutmut_orig"] = returning_generator_func_orig
mutants_returning_generator_func["returning_generator_func__mutmut_1"] = returning_generator_func_1
mutants_async_func["_mutmut_orig"] = async_func_orig
mutants_async_func["async_func__mutmut_1"] = async_func_1
mutants_async_generator_func["_mutmut_orig"] = async_generator_func_orig
Expand Down Expand Up @@ -240,6 +262,24 @@ def test_generator_func_mutated(self, monkeypatch):
monkeypatch.setenv("MUTANT_UNDER_TEST", "test_trampoline.generator_func__mutmut_1")
assert list(generator_func([1, 2, 3])) == [3, 6, 9], "Should call mutated func"

def test_generator_func_return_value_original(self, monkeypatch):
monkeypatch.setenv("MUTANT_UNDER_TEST", "")

def consume():
result = yield from returning_generator_func([1, 2, 3])
yield result

assert list(consume()) == [2, 4, 6, "done"], "Should forward the return value"

def test_generator_func_return_value_mutated(self, monkeypatch):
monkeypatch.setenv("MUTANT_UNDER_TEST", "test_trampoline.returning_generator_func__mutmut_1")

def consume():
result = yield from returning_generator_func([1, 2, 3])
yield result

assert list(consume()) == [3, 6, 9, "done"], "Should forward the return value"

@pytest.mark.asyncio
async def test_async_func_original(self, monkeypatch):
monkeypatch.setenv("MUTANT_UNDER_TEST", "")
Expand Down
Loading