Skip to content

Async functions get synchronous trampolines causing "coroutine was never awaited" errors #454

@DamienGR

Description

@DamienGR

Description

When using mutmut 3.4.0 with async functions (async def), the generated trampoline wrapper is synchronous (def), which causes the coroutine to not be awaited.

Environment

  • mutmut version: 3.4.0
  • Python version: 3.12.3
  • OS: Linux (WSL2)

Steps to Reproduce

  1. Have a codebase with async functions like:
async def get_current_user(
    credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
    db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
    """Get current authenticated user from JWT token."""
    # ... async code
    user = await service.get_user_by_id(uuid.UUID(user_id))
    return user
  1. Run mutmut run

  2. Observe the generated trampoline in mutants/ directory

Expected Behavior

The trampoline for async functions should be:

async def get_current_user(*args, **kwargs):
    result = await _mutmut_trampoline(x_get_current_user__mutmut_orig, x_get_current_user__mutmut_mutants, args, kwargs)
    return result

Actual Behavior

The generated trampoline is synchronous:

def get_current_user(*args, **kwargs):
    result = _mutmut_trampoline(x_get_current_user__mutmut_orig, x_get_current_user__mutmut_mutants, args, kwargs)
    return result 

This causes the error:

RuntimeWarning: coroutine 'x_get_current_user__mutmut_orig' was never awaited

And tests fail because the coroutine is returned instead of being awaited.

Root Cause

In trampoline_templates.py, the build_trampoline function always generates a synchronous def:

def {orig_name}({'self, ' if class_name is not None else ''}*args, **kwargs):
    result = {trampoline_name}(...)
    return result 

It should detect if the original function is async and generate:

async def {orig_name}({'self, ' if class_name is not None else ''}*args, **kwargs):
    result = await {trampoline_name}(...)
    return result 

Related

Workaround

Currently excluding files with async functions using do_not_mutate in pyproject.toml:

[tool.mutmut]
do_not_mutate = [
    "**/dependencies.py",
    "**/routes.py", 
    "**/service.py",
    "**/database.py",
]

This significantly limits mutation testing coverage for async Python codebases (FastAPI, asyncio, etc.).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions