Flush the stdio subprocess's coverage data before the clean-exit line (#2840)
This commit is contained in:
parent
7267818e44
commit
535fc1d0fb
2 changed files with 27 additions and 6 deletions
|
|
@ -9,6 +9,7 @@ test-only-functions convention.
|
|||
import sys
|
||||
|
||||
import anyio
|
||||
import coverage
|
||||
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
|
@ -54,9 +55,27 @@ server = Server("stdio-echo", on_list_tools=list_tools, on_call_tool=call_tool,
|
|||
async def main() -> None:
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(read_stream, write_stream, server.create_initialization_options())
|
||||
# Flush this process's coverage data before the clean-exit line below. Without this, the
|
||||
# data is only written by coverage's atexit hook during interpreter teardown -- and on a
|
||||
# slow Windows runner that can overrun the transport's termination grace, so the kill
|
||||
# silently destroys the data file and the 100% gate trips on this module's subprocess-only
|
||||
# lines. Saving here puts the write before the line the test synchronizes on: once the
|
||||
# parent has seen "clean exit", the data is durably on disk and the escalation is harmless.
|
||||
# Nothing measured may execute after the save (it would be unrecordable by construction),
|
||||
# hence the excluded lines below. The branch is pragma'd because under coverage the
|
||||
# instance always exists, and without coverage nothing is measured anyway.
|
||||
cov = getattr(coverage.process_startup, "coverage", None)
|
||||
if cov is not None: # pragma: no branch
|
||||
# stop() is load-bearing twice over: it ends tracing, making itself the last
|
||||
# recordable line, and it leaves nothing new for coverage's atexit re-save to flush --
|
||||
# so a kill landing during interpreter teardown cannot corrupt the file save() wrote
|
||||
# (coverage opens it with sqlite journaling off; a torn rewrite would not roll back).
|
||||
cov.stop()
|
||||
cov.save() # pragma: lax no cover - untraced: stop() above already ended measurement
|
||||
# Reached only when the run loop exits because stdin closed; if the process were terminated
|
||||
# the test's stderr capture would not see this line.
|
||||
print("stdio-echo: clean exit", file=sys.stderr, flush=True)
|
||||
# the test's stderr capture would not see this line. lax no cover: runs after the coverage
|
||||
# save by design, so it can never appear covered.
|
||||
print("stdio-echo: clean exit", file=sys.stderr, flush=True) # pragma: lax no cover
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -56,10 +56,12 @@ async def test_tool_call_and_notification_round_trip_over_a_stdio_subprocess(
|
|||
notification before the call returns; the server exits when the transport closes its
|
||||
stdin.
|
||||
"""
|
||||
# After stdin closes, the child must unwind, write the clean-exit line, and let coverage's
|
||||
# atexit hook persist its subprocess data file before escalation. The production 2s default
|
||||
# was too tight on slow Windows runners: the child was killed mid-atexit (test stayed green)
|
||||
# and the silently missing data file tripped the 100% coverage gate. Not under test.
|
||||
# After stdin closes, the child must unwind, flush its subprocess coverage data, and write
|
||||
# the clean-exit line before escalation (the server saves coverage *before* printing, so a
|
||||
# post-print kill can no longer silently lose the data file -- see _stdio_server.main). The
|
||||
# production 2s default is too tight for the unwind+save tail on loaded Windows runners
|
||||
# (measured in-situ p99 of the whole test is ~7s); a kill before the print fails the stderr
|
||||
# assertion below loudly rather than tripping the coverage gate. Not under test.
|
||||
monkeypatch.setattr(stdio, "PROCESS_TERMINATION_TIMEOUT", 10.0)
|
||||
|
||||
received: list[LoggingMessageNotificationParams] = []
|
||||
|
|
|
|||
Loading…
Reference in a new issue