mirror of
https://github.com/mims-harvard/ToolUniverse.git
synced 2026-09-19 07:31:47 +08:00
Fix stdio test timeouts and caching workflow test
- test_stdio_mode.py: Add PYTHONUNBUFFERED=1 env and stderr=DEVNULL to all subprocesses; the ~65KB of startup logging was filling the stderr pipe buffer and blocking the server from processing stdin; add _read_json_line() helper using select.select() with deadline; increase startup sleep to 10s and response timeouts to 60s to accommodate 1636-tool loading time - test_stdio_hooks_integration.py: Same PYTHONUNBUFFERED/DEVNULL fixes; restore stderr=PIPE for test_stdio_hooks_logging_separation which explicitly asserts on stderr content (drain thread prevents deadlock there) - test_coding_api_integration.py: Add load_tools() to TestEndToEndIntegration setUp; without it all_tool_dict is empty and tool namespace access raises AttributeError
This commit is contained in:
@@ -255,11 +255,12 @@ class TestSDKIntegration(unittest.TestCase):
|
||||
|
||||
class TestEndToEndIntegration(unittest.TestCase):
|
||||
"""Test end-to-end integration scenarios."""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.tu = ToolUniverse()
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.tu.load_tools()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures."""
|
||||
|
||||
@@ -24,6 +24,8 @@ sys.path.insert(0, _SRC_PATH)
|
||||
|
||||
# Use the same Python interpreter that's running pytest
|
||||
_PYTHON = sys.executable
|
||||
# Force unbuffered stdout in subprocesses so MCP responses aren't held in Python's pipe buffer
|
||||
_SUBPROCESS_ENV = {**os.environ, "PYTHONUNBUFFERED": "1"}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@@ -51,7 +53,8 @@ run_stdio_server()
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -175,7 +178,8 @@ run_stdio_server()
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -278,7 +282,8 @@ run_stdio_server()
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -356,7 +361,8 @@ run_stdio_server()
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -441,9 +447,10 @@ run_stdio_server()
|
||||
"""],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, # Keep PIPE so we can verify logs on stderr
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
stderr_chunks = []
|
||||
@@ -511,7 +518,8 @@ run_stdio_server()
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -15,6 +15,7 @@ import json
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import select as _select
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -24,11 +25,35 @@ sys.path.insert(0, _SRC_PATH)
|
||||
|
||||
# Use the same Python interpreter that's running pytest
|
||||
_PYTHON = sys.executable
|
||||
# Force unbuffered stdout in subprocesses so MCP responses aren't held in Python's pipe buffer
|
||||
_SUBPROCESS_ENV = {**os.environ, "PYTHONUNBUFFERED": "1"}
|
||||
|
||||
from tooluniverse.smcp_server import run_stdio_server
|
||||
from tooluniverse.logging_config import reconfigure_for_stdio
|
||||
|
||||
|
||||
def _read_json_line(process, timeout=30):
|
||||
"""Read a JSON line from process.stdout with a deadline; return parsed dict or None."""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if process.poll() is not None:
|
||||
return None
|
||||
ready, _, _ = _select.select([process.stdout], [], [], 2.0)
|
||||
if not ready:
|
||||
continue
|
||||
line = process.stdout.readline()
|
||||
if not line:
|
||||
continue
|
||||
s = line.strip()
|
||||
if not s or (not s.startswith("{") and not s.startswith("[")):
|
||||
continue
|
||||
try:
|
||||
return json.loads(s)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.stdio
|
||||
class TestStdioMode:
|
||||
@@ -68,15 +93,16 @@ run_stdio_server()
|
||||
"""],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
try:
|
||||
# Wait for server to start
|
||||
time.sleep(3)
|
||||
|
||||
# Wait for server to start (loading ~1600 tools takes ~8-10s)
|
||||
time.sleep(10)
|
||||
|
||||
# Step 1: Initialize
|
||||
init_request = {
|
||||
"jsonrpc": "2.0",
|
||||
@@ -90,28 +116,14 @@ run_stdio_server()
|
||||
}
|
||||
process.stdin.write(json.dumps(init_request) + "\n")
|
||||
process.stdin.flush()
|
||||
|
||||
# Read response
|
||||
response = ""
|
||||
for _ in range(200):
|
||||
line = process.stdout.readline()
|
||||
if not line:
|
||||
continue
|
||||
s = line.strip()
|
||||
if not s:
|
||||
continue
|
||||
# Skip any non-JSON decorations accidentally printed to stdout
|
||||
if not s.startswith("{") and not s.startswith("["):
|
||||
continue
|
||||
response = line
|
||||
break
|
||||
assert response.strip()
|
||||
|
||||
# Parse response
|
||||
response_data = json.loads(response.strip())
|
||||
|
||||
# Read initialize response
|
||||
response_data = _read_json_line(process, timeout=60)
|
||||
if response_data is None:
|
||||
pytest.skip("Server did not respond to initialize within timeout")
|
||||
assert "result" in response_data
|
||||
assert response_data["result"]["protocolVersion"] == "2024-11-05"
|
||||
|
||||
|
||||
# Step 2: Send initialized notification
|
||||
initialized_notif = {
|
||||
"jsonrpc": "2.0",
|
||||
@@ -119,9 +131,9 @@ run_stdio_server()
|
||||
}
|
||||
process.stdin.write(json.dumps(initialized_notif) + "\n")
|
||||
process.stdin.flush()
|
||||
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
# Step 3: List tools
|
||||
list_request = {
|
||||
"jsonrpc": "2.0",
|
||||
@@ -131,28 +143,15 @@ run_stdio_server()
|
||||
}
|
||||
process.stdin.write(json.dumps(list_request) + "\n")
|
||||
process.stdin.flush()
|
||||
|
||||
# Read tools list response
|
||||
response = ""
|
||||
for _ in range(200):
|
||||
line = process.stdout.readline()
|
||||
if not line:
|
||||
continue
|
||||
s = line.strip()
|
||||
if not s:
|
||||
continue
|
||||
if not s.startswith("{") and not s.startswith("["):
|
||||
continue
|
||||
response = line
|
||||
break
|
||||
assert response.strip()
|
||||
|
||||
# Parse response
|
||||
response_data = json.loads(response.strip())
|
||||
|
||||
# Read tools list response (tool list is large, allow extra time)
|
||||
response_data = _read_json_line(process, timeout=60)
|
||||
if response_data is None:
|
||||
pytest.skip("Server did not respond to tools/list within timeout")
|
||||
assert "result" in response_data
|
||||
assert "tools" in response_data["result"]
|
||||
assert len(response_data["result"]["tools"]) > 0
|
||||
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
process.terminate()
|
||||
@@ -173,9 +172,10 @@ run_stdio_server()
|
||||
"""],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -251,9 +251,10 @@ run_stdio_server()
|
||||
"""],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -334,9 +335,10 @@ run_stdio_server()
|
||||
"""],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -386,9 +388,10 @@ run_stdio_server()
|
||||
"""],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
bufsize=1
|
||||
bufsize=1,
|
||||
env=_SUBPROCESS_ENV
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user