Skip to content

Coverage doesn't track lines after awaiting cancelled asyncio tasks with caught CancelledError #2124

@ioannis-balo

Description

@ioannis-balo

Describe the bug
When using coverage.py with asyncio code that cancels tasks and catches CancelledError, lines immediately following the await are not tracked as covered, even though they execute.

To Reproduce

"""
Run with: coverage run --branch repro.py && coverage report -m
"""
import asyncio
from asyncio import CancelledError, Task, create_task


class TaskManager:
    def __init__(self):
        self._tasks: list[Task] = []
        self.cleanup_completed = False

    async def start_tasks(self, count: int):
        for i in range(count):
            self._tasks.append(create_task(self._long_running_task(i)))

    async def _long_running_task(self, task_id: int):
        await asyncio.sleep(10)
        return f"task_{task_id}_done"

    async def cancel_remaining_tasks(self):
        for task in self._tasks:
            if not task.done():
                task.cancel()
                try:
                    await task
                except CancelledError:
                    pass  # Swallow the cancellation
        self._tasks.clear()

    async def stop(self):
        if self._tasks:
            await self.cancel_remaining_tasks()
            self.cleanup_completed = True  # <-- LINE 35: NOT COVERED but executes


async def main():
    manager = TaskManager()
    await manager.start_tasks(5)
    await asyncio.sleep(0.01)
    await manager.stop()
    
    assert manager.cleanup_completed, "This passes, proving line 35 executed!"
    print(f"cleanup_completed = {manager.cleanup_completed}")


if __name__ == "__main__":
    asyncio.run(main())

Expected behavior
Line 35 (self.cleanup_completed = True) should be marked as covered since it executes (proven by the assertion passing and the print output).

Environment

coverage_version: 7.13.2
core: -none-
CTracer: available
python: 3.11.11
platform: macOS-26.2-arm64-arm-64bit
implementation: CPython

Relevant Installed Dependencies

coverage==7.13.2
greenlet==3.1.1

Additional context

  • The issue also occurs with --concurrency=thread,greenlet
  • The pattern is: after awaiting a coroutine that cancels tasks and catches CancelledError, the next line is not tracked
  • This affects real-world code that needs to gracefully shut down async task pools

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions