Add more async lifecycle tests and fix potential POSIX cancellation fd leak#93
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #93 +/- ##
==========================================
+ Coverage 91.46% 92.21% +0.74%
==========================================
Files 22 22
Lines 3599 3621 +22
==========================================
+ Hits 3292 3339 +47
+ Misses 307 282 -25 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Hardens async serial transport lifecycle: introduces shared state-tracking in BaseSerialTransport (made/lost/user-close flags), shields the open executor future on POSIX and Windows so a cancellation cannot leak the fd/HANDLE, detaches the Windows PyHANDLE to surface (rather than mask) missing explicit closes, and adds a comprehensive asyncio.Protocol lifecycle test suite plus a hub4com stdout drainer to keep its pipes from filling up.
Changes:
- Centralize
connection_made_called/connection_lost_called/user_initiated_closeinBaseSerialTransportand useasyncio.shieldaround the open executor future to plug cancellation-race resource leaks (POSIX fd, Windows HANDLE via newCreateFile_detached). - Make per-backend transports (
serial_posix,serial_win32,serial_socket,serial_rfc2217,serial_esphome,serial_pyodide) route through the new helpers and respect the user-initiated-close flag to avoid spurious "broken" reports. - Add
tests/test_async_lifecycle.pycovering normal close, abort, cancellation during connect, drain vs abort, re-entrant close inconnection_made, fd-leak race, etc.; drain hub4com stdout/stderr in tests to prevent stalls.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| serialx/common.py | Adds shared lifecycle flags and idempotent _call_protocol_connection_made/_lost helpers. |
| serialx/descriptor_transport.py | Shields the os.open executor future and delegates close/abort/connection-lost to shared helpers. |
| serialx/platforms/serial_posix.py | Switches to shared _call_protocol_connection_made helper. |
| serialx/platforms/serial_win32.py | Adds CreateFile_detached, shields executor future, adds _connection_made_waiter, uses shared helpers. |
| serialx/platforms/serial_socket.py | Uses shared flags; only marks broken on non-user-initiated close. |
| serialx/platforms/serial_rfc2217/init.py | Uses shared flags; failures of pending waiters still produce some exception. |
| serialx/platforms/serial_esphome.py | Uses shared helpers; drops writes after close; notes TODO for sync force-disconnect. |
| serialx/platforms/serial_pyodide/init.py | Drops writes after close; clears write queue on abort. |
| tests/common.py | Drains hub4com stdout/stderr via threads to avoid pipe buffer stalls. |
| tests/test_async_lifecycle.py | New extensive Protocol-state lifecycle test suite incl. cancellation race. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This PR adds an explicit test suite for the
asyncio.Protocolstate machine pattern and aligns platform behavior around poorly-timed cancellation. These aren't things normally hit at runtime often but it's not good practice to leak file descriptors, especially when we useflock.We also plug a few more cancellation point leaks. This pattern is a bit annoying to deal with because any
awaitpoint can potentially be cancelled but with the asterisk that we are often awaiting on the result of a thread pool executor, which cannot be cancelled. This requires a bit of coordination with future callbacks and such.For Windows, we now intentionally detach the file handle from
PyHANDLEand let it "leak" for consistency with POSIX, since the cleanup code should work without a magical__del__.