Skip to content

Commit 79ebf22

Browse files
Fix Windows path escaping in regression tests
The regression tests were failing on Windows because file paths containing backslashes weren't properly escaped when inserted into Python script strings. This commit: - Adds a shared test utility function escape_path_for_python() that converts backslashes to forward slashes before quoting paths - Updates the regression tests to use this helper for all file paths - Refactors test_stdio.py to use the same shared utility This ensures Windows paths like C:\Users\... are converted to C:/Users/... which work correctly in Python strings on all platforms.
1 parent d9c4b2a commit 79ebf22

File tree

3 files changed

+25
-19
lines changed

3 files changed

+25
-19
lines changed

tests/client/test_stdio.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from mcp.shared.exceptions import McpError
1818
from mcp.shared.message import SessionMessage
1919
from mcp.types import CONNECTION_CLOSED, JSONRPCMessage, JSONRPCRequest, JSONRPCResponse
20+
from tests.shared.test_win32_utils import escape_path_for_python
2021

2122
# Timeout for cleanup of processes that ignore SIGTERM
2223
# This timeout ensures the test fails quickly if the cleanup logic doesn't have
@@ -249,12 +250,6 @@ class TestChildProcessCleanup:
249250
This is a fundamental difference between Windows and Unix process termination.
250251
"""
251252

252-
@staticmethod
253-
def _escape_path_for_python(path: str) -> str:
254-
"""Escape a file path for use in Python code strings."""
255-
# Use forward slashes which work on all platforms and don't need escaping
256-
return repr(path.replace("\\", "/"))
257-
258253
@pytest.mark.anyio
259254
@pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default")
260255
async def test_basic_child_process_cleanup(self):
@@ -280,13 +275,13 @@ async def test_basic_child_process_cleanup(self):
280275
import os
281276
282277
# Mark that parent started
283-
with open({self._escape_path_for_python(parent_marker)}, 'w') as f:
278+
with open({escape_path_for_python(parent_marker)}, 'w') as f:
284279
f.write('parent started\\n')
285280
286281
# Child script that writes continuously
287282
child_script = f'''
288283
import time
289-
with open({self._escape_path_for_python(marker_file)}, 'a') as f:
284+
with open({escape_path_for_python(marker_file)}, 'a') as f:
290285
while True:
291286
f.write(f"{time.time()}")
292287
f.flush()
@@ -335,9 +330,9 @@ async def test_basic_child_process_cleanup(self):
335330
final_size = os.path.getsize(marker_file)
336331

337332
print(f"After cleanup: file size {size_after_cleanup} -> {final_size}")
338-
assert final_size == size_after_cleanup, (
339-
f"Child process still running! File grew by {final_size - size_after_cleanup} bytes"
340-
)
333+
assert (
334+
final_size == size_after_cleanup
335+
), f"Child process still running! File grew by {final_size - size_after_cleanup} bytes"
341336

342337
print("SUCCESS: Child process was properly terminated")
343338

@@ -381,7 +376,7 @@ async def test_nested_process_tree(self):
381376
382377
# Grandchild just writes to file
383378
grandchild_script = \"\"\"import time
384-
with open({self._escape_path_for_python(grandchild_file)}, 'a') as f:
379+
with open({escape_path_for_python(grandchild_file)}, 'a') as f:
385380
while True:
386381
f.write(f"gc {{time.time()}}")
387382
f.flush()
@@ -391,7 +386,7 @@ async def test_nested_process_tree(self):
391386
subprocess.Popen([sys.executable, '-c', grandchild_script])
392387
393388
# Child writes to its file
394-
with open({self._escape_path_for_python(child_file)}, 'a') as f:
389+
with open({escape_path_for_python(child_file)}, 'a') as f:
395390
while True:
396391
f.write(f"c {time.time()}")
397392
f.flush()
@@ -401,7 +396,7 @@ async def test_nested_process_tree(self):
401396
subprocess.Popen([sys.executable, '-c', child_script])
402397
403398
# Parent writes to its file
404-
with open({self._escape_path_for_python(parent_file)}, 'a') as f:
399+
with open({escape_path_for_python(parent_file)}, 'a') as f:
405400
while True:
406401
f.write(f"p {time.time()}")
407402
f.flush()
@@ -470,7 +465,7 @@ async def test_early_parent_exit(self):
470465
471466
# Child that continues running
472467
child_script = f'''import time
473-
with open({self._escape_path_for_python(marker_file)}, 'a') as f:
468+
with open({escape_path_for_python(marker_file)}, 'a') as f:
474469
while True:
475470
f.write(f"child {time.time()}")
476471
f.flush()

tests/issues/test_1027_win_unreachable_cleanup.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from mcp import ClientSession, StdioServerParameters
2121
from mcp.client.stdio import _create_platform_compatible_process, stdio_client
22+
from tests.shared.test_win32_utils import escape_path_for_python
2223

2324

2425
@pytest.mark.anyio
@@ -53,8 +54,8 @@ async def test_lifespan_cleanup_executed():
5354
from contextlib import asynccontextmanager
5455
from mcp.server.fastmcp import FastMCP
5556
56-
STARTUP_MARKER = {repr(startup_marker)}
57-
CLEANUP_MARKER = {repr(cleanup_marker)}
57+
STARTUP_MARKER = {escape_path_for_python(startup_marker)}
58+
CLEANUP_MARKER = {escape_path_for_python(cleanup_marker)}
5859
5960
@asynccontextmanager
6061
async def lifespan(server):
@@ -159,8 +160,8 @@ async def test_stdin_close_triggers_cleanup():
159160
from contextlib import asynccontextmanager
160161
from mcp.server.fastmcp import FastMCP
161162
162-
STARTUP_MARKER = {repr(startup_marker)}
163-
CLEANUP_MARKER = {repr(cleanup_marker)}
163+
STARTUP_MARKER = {escape_path_for_python(startup_marker)}
164+
CLEANUP_MARKER = {escape_path_for_python(cleanup_marker)}
164165
165166
@asynccontextmanager
166167
async def lifespan(server):

tests/shared/test_win32_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Windows-specific test utilities."""
2+
3+
4+
def escape_path_for_python(path: str) -> str:
5+
"""Escape a file path for use in Python code strings.
6+
7+
Converts backslashes to forward slashes which work on all platforms
8+
and don't need escaping in Python strings.
9+
"""
10+
return repr(path.replace("\\", "/"))

0 commit comments

Comments
 (0)