From e10964f8f1bf53db314ce1d8ee088aee8e1a52d4 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:07:39 -0700 Subject: [PATCH] fix: forward generator return value through the trampoline wrap_in_trampoline's generator branch used a bare "yield from trampoline(...)", so the wrapper generator always finished with an implicit None. A caller doing "result = yield from decorated_gen()" therefore got None instead of the generator's return value, and any test asserting on that value failed under "mutmut run" while passing under plain pytest. Returning the delegation result matches the PEP 380 expansion and mirrors the equivalent fix already applied to the async-generator branch. Adds a returning generator to the trampoline test module plus original and mutated regression tests that consume it with "yield from". --- HISTORY.rst | 2 ++ src/mutmut/mutation/trampoline.py | 5 +++- tests/mutation/test_trampoline.py | 40 +++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 05d8ba4c..eba5ea0b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -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 diff --git a/src/mutmut/mutation/trampoline.py b/src/mutmut/mutation/trampoline.py index d70f98c4..7cbd4cf4 100644 --- a/src/mutmut/mutation/trampoline.py +++ b/src/mutmut/mutation/trampoline.py @@ -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) diff --git a/tests/mutation/test_trampoline.py b/tests/mutation/test_trampoline.py index bd81bf56..e76fcbf9 100644 --- a/tests/mutation/test_trampoline.py +++ b/tests/mutation/test_trampoline.py @@ -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 = {} @@ -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) @@ -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 @@ -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", "")