Skip to content

Commit 9be54eb

Browse files
authored
Smoketest2 (#528)
* Move smoketest2.py to smoketest.py * Added smoketest2.py Signed-off-by: Mihai Criveti <[email protected]> * Added smoketest2.py Signed-off-by: Mihai Criveti <[email protected]> --------- Signed-off-by: Mihai Criveti <[email protected]>
1 parent 6b81a6b commit 9be54eb

File tree

2 files changed

+729
-27
lines changed

2 files changed

+729
-27
lines changed

smoketest.py

Lines changed: 189 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -223,17 +223,32 @@ def request(method: str, path: str, *, json_data=None, **kw):
223223

224224
# ───────────────────────────── Cleanup logic ─────────────────────────────
225225
_supergw_proc: subprocess.Popen | None = None
226+
_supergw_log_file = None
226227

227228

228229
def cleanup():
229230
log_section("Cleanup", "🧹")
230-
global _supergw_proc
231+
global _supergw_proc, _supergw_log_file
232+
233+
# Clean up the supergateway process
231234
if _supergw_proc and _supergw_proc.poll() is None:
235+
logging.info("🔄 Terminating supergateway process (PID: %d)", _supergw_proc.pid)
232236
_supergw_proc.terminate()
233237
try:
234238
_supergw_proc.wait(timeout=5)
239+
logging.info("✅ Supergateway process terminated cleanly")
235240
except subprocess.TimeoutExpired:
241+
logging.warning("⚠️ Supergateway didn't terminate in time, killing it")
236242
_supergw_proc.kill()
243+
_supergw_proc.wait()
244+
245+
# Close log file if open
246+
if _supergw_log_file:
247+
_supergw_log_file.close()
248+
_supergw_log_file = None
249+
250+
# Stop docker container
251+
logging.info("🐋 Stopping Docker container")
237252
subprocess.run(MAKE_DOCKER_STOP, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
238253
logging.info("✅ Cleanup complete")
239254

@@ -270,49 +285,137 @@ def step_4_docker_run():
270285
full = f"https://localhost:{PORT_GATEWAY}{ep}"
271286
need_auth = os.getenv("AUTH_REQUIRED", "true").lower() == "true"
272287
headers = auth_headers if (ep == "/version" or need_auth) else None
288+
logging.info("🔍 Waiting for endpoint %s (auth: %s)", ep, bool(headers))
273289
if not wait_http_ok(full, 45, headers=headers):
274290
raise RuntimeError(f"Gateway endpoint {ep} not ready")
291+
logging.info("✅ Endpoint %s is ready", ep)
275292

276293
logging.info("✅ Gateway /health /ready /version all OK")
277294

278295

279296
def step_5_start_time_server(restart=False):
280-
global _supergw_proc
297+
global _supergw_proc, _supergw_log_file
298+
299+
# Check if npx is available
300+
try:
301+
npx_version = subprocess.check_output(["npx", "--version"], text=True, stderr=subprocess.DEVNULL).strip()
302+
logging.info("🔍 Found npx version: %s", npx_version)
303+
except (subprocess.CalledProcessError, FileNotFoundError):
304+
raise RuntimeError("npx not found. Please install Node.js and npm.")
305+
306+
# Check if uvx is available
307+
try:
308+
uvx_check = subprocess.run(["uvx", "--version"], capture_output=True, text=True)
309+
if uvx_check.returncode == 0:
310+
logging.info("🔍 Found uvx version: %s", uvx_check.stdout.strip())
311+
else:
312+
logging.warning("⚠️ uvx not found or not working. This may cause issues.")
313+
except FileNotFoundError:
314+
logging.warning("⚠️ uvx not found. Please install uv (pip install uv) if the time server fails.")
315+
281316
if port_open(PORT_TIME_SERVER):
282317
if restart:
283318
logging.info("🔄 Restarting process on port %d", PORT_TIME_SERVER)
284319
try:
285320
pid = int(subprocess.check_output(["lsof", "-ti", f"TCP:{PORT_TIME_SERVER}"], text=True).strip())
321+
logging.info("🔍 Found existing process PID: %d", pid)
286322
os.kill(pid, signal.SIGTERM)
287323
time.sleep(2)
288324
except Exception as e:
289325
logging.warning("Could not stop existing server: %s", e)
290326
else:
291327
logging.info("ℹ️ Re-using MCP-Time-Server on port %d", PORT_TIME_SERVER)
328+
return
329+
292330
if not port_open(PORT_TIME_SERVER):
293331
log_section("Launching MCP-Time-Server", "⏰")
294-
_supergw_proc = subprocess.Popen(SUPERGW_CMD, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
295-
for _ in range(20):
332+
logging.info("🚀 Command: %s", " ".join(shlex.quote(c) for c in SUPERGW_CMD))
333+
334+
# Create a log file for the time server output
335+
log_filename = f"supergateway_{int(time.time())}.log"
336+
_supergw_log_file = open(log_filename, "w")
337+
logging.info("📝 Logging supergateway output to: %s", log_filename)
338+
339+
# Start the process with output capture
340+
_supergw_proc = subprocess.Popen(
341+
SUPERGW_CMD,
342+
stdout=_supergw_log_file,
343+
stderr=subprocess.STDOUT,
344+
text=True,
345+
bufsize=1
346+
)
347+
348+
logging.info("🔍 Started supergateway process with PID: %d", _supergw_proc.pid)
349+
350+
# Wait for the server to start
351+
start_time = time.time()
352+
timeout = 30
353+
check_interval = 0.5
354+
355+
while time.time() - start_time < timeout:
356+
# Check if process is still running
357+
exit_code = _supergw_proc.poll()
358+
if exit_code is not None:
359+
# Process exited, read the log file
360+
_supergw_log_file.close()
361+
with open(log_filename, "r") as f:
362+
output = f.read()
363+
logging.error("❌ Time-Server process exited with code %d", exit_code)
364+
logging.error("📋 Process output:\n%s", output)
365+
raise RuntimeError(f"Time-Server exited with code {exit_code}. Check the logs above.")
366+
367+
# Check if port is open
296368
if port_open(PORT_TIME_SERVER):
297-
break
298-
if _supergw_proc.poll() is not None:
299-
raise RuntimeError("Time-Server exited")
300-
time.sleep(1)
369+
elapsed = time.time() - start_time
370+
logging.info("✅ Time-Server is listening on port %d (took %.1fs)", PORT_TIME_SERVER, elapsed)
371+
372+
# Give it a moment to fully initialize
373+
time.sleep(1)
374+
375+
# Double-check it's still running
376+
if _supergw_proc.poll() is None:
377+
return
378+
else:
379+
raise RuntimeError("Time-Server started but then immediately exited")
380+
381+
# Log progress
382+
if int(time.time() - start_time) % 5 == 0:
383+
logging.info("⏳ Still waiting for Time-Server to start... (%.0fs elapsed)", time.time() - start_time)
384+
385+
time.sleep(check_interval)
386+
387+
# Timeout reached
388+
if _supergw_proc.poll() is None:
389+
_supergw_proc.terminate()
390+
_supergw_proc.wait()
391+
392+
_supergw_log_file.close()
393+
with open(log_filename, "r") as f:
394+
output = f.read()
395+
logging.error("📋 Process output:\n%s", output)
396+
raise RuntimeError(f"Time-Server failed to start within {timeout}s")
301397

302398

303399
def step_6_register_gateway() -> int:
304400
log_section("Registering gateway", "🛂")
305401
payload = {"name": "smoketest_time_server", "url": f"http://localhost:{PORT_TIME_SERVER}/sse"}
402+
logging.info("📤 Registering gateway with payload: %s", json.dumps(payload, indent=2))
403+
306404
r = request("POST", "/gateways", json_data=payload)
307405
if r.status_code in (200, 201):
308406
gid = r.json()["id"]
309407
logging.info("✅ Gateway ID %s registered", gid)
310408
return gid
311409
# 409 conflict → find existing
312410
if r.status_code == 409:
313-
gw = next(g for g in request("GET", "/gateways").json() if g["name"] == payload["name"])
314-
logging.info("ℹ️ Gateway already present - using ID %s", gw["id"])
315-
return gw["id"]
411+
logging.info("⚠️ Gateway already exists, fetching existing one")
412+
gateways = request("GET", "/gateways").json()
413+
gw = next((g for g in gateways if g["name"] == payload["name"]), None)
414+
if gw:
415+
logging.info("ℹ️ Gateway already present - using ID %s", gw["id"])
416+
return gw["id"]
417+
else:
418+
raise RuntimeError("Gateway conflict but not found in list")
316419
# other error
317420
msg = r.text
318421
try:
@@ -323,42 +426,88 @@ def step_6_register_gateway() -> int:
323426

324427

325428
def step_7_verify_tools():
326-
names = [t["name"] for t in request("GET", "/tools").json()]
327-
assert f"smoketest-time-server{settings.gateway_tool_name_separator}get-current-time" in names, f"smoketest-time-server{settings.gateway_tool_name_separator}get-current-time absent"
328-
logging.info("✅ Tool visible in /tools")
429+
logging.info("🔍 Fetching tool list")
430+
tools = request("GET", "/tools").json()
431+
tool_names = [t["name"] for t in tools]
432+
433+
expected_tool = f"smoketest-time-server{settings.gateway_tool_name_separator}get-current-time"
434+
logging.info("📋 Found %d tools total", len(tool_names))
435+
logging.debug("📋 All tools: %s", json.dumps(tool_names, indent=2))
436+
437+
if expected_tool not in tool_names:
438+
# Log similar tools to help debug
439+
similar = [t for t in tool_names if "time" in t.lower() or "smoketest" in t.lower()]
440+
if similar:
441+
logging.error("❌ Expected tool not found. Similar tools: %s", similar)
442+
raise AssertionError(f"{expected_tool} not found in tools list")
443+
444+
logging.info("✅ Tool '%s' visible in /tools", expected_tool)
329445

330446

331447
def step_8_invoke_tool():
332-
body = {"jsonrpc": "2.0", "id": 1, "method": "smoketest-time-server-get-current-time", "params": {"timezone": "Europe/Dublin"}}
448+
log_section("Invoking remote tool", "🔧")
449+
body = {
450+
"jsonrpc": "2.0",
451+
"id": 1,
452+
"method": f"smoketest-time-server{settings.gateway_tool_name_separator}get-current-time",
453+
"params": {"timezone": "Europe/Dublin"}
454+
}
455+
logging.info("📤 RPC request: %s", json.dumps(body, indent=2))
456+
333457
j = request("POST", "/rpc", json_data=body).json()
458+
logging.info("📥 RPC response: %s", json.dumps(j, indent=2))
334459

335460
if "error" in j:
336-
raise RuntimeError(j["error"])
461+
raise RuntimeError(f"RPC error: {j['error']}")
337462

338463
result = j.get("result", j)
339464
if "content" not in result:
340-
raise RuntimeError("Missing 'content' in tool response")
465+
raise RuntimeError(f"Missing 'content' in tool response. Got: {result}")
466+
467+
content = result["content"]
468+
if not content or not isinstance(content, list):
469+
raise RuntimeError(f"Invalid content format. Expected list, got: {type(content)}")
470+
471+
text = content[0].get("text", "")
472+
if not text:
473+
raise RuntimeError(f"No text in content. Content: {content}")
341474

342-
text = result["content"][0]["text"]
343-
assert "datetime" in text, "datetime missing"
344-
logging.info("✅ Tool invocation returned time")
475+
if "datetime" not in text:
476+
raise RuntimeError(f"Expected 'datetime' in response, got: {text}")
477+
478+
logging.info("✅ Tool invocation returned time: %s", text[:100])
345479

346480

347481
def step_9_version_health():
348-
health = request("GET", "/health").json()["status"].lower()
482+
log_section("Final health check", "🏥")
483+
484+
health_resp = request("GET", "/health").json()
485+
logging.info("📥 Health response: %s", json.dumps(health_resp, indent=2))
486+
health = health_resp.get("status", "").lower()
349487
assert health in ("ok", "healthy"), f"Unexpected health status: {health}"
350-
ver = request("GET", "/version").json()["app"]["name"]
488+
489+
ver_resp = request("GET", "/version").json()
490+
logging.info("📥 Version response: %s", json.dumps(ver_resp, indent=2))
491+
ver = ver_resp.get("app", {}).get("name", "Unknown")
351492
logging.info("✅ Health OK - app %s", ver)
352493

353494

354495
def step_10_cleanup_gateway(gid: int | None = None):
496+
log_section("Cleanup gateway registration", "🧹")
497+
355498
if gid is None:
356499
logging.warning("🧹 No gateway ID; nothing to delete")
357500
return
358501

502+
logging.info("🗑️ Deleting gateway ID: %s", gid)
359503
request("DELETE", f"/gateways/{gid}")
360-
assert all(g["id"] != gid for g in request("GET", "/gateways").json())
361-
logging.info("✅ Gateway deleted")
504+
505+
# Verify it's gone
506+
gateways = request("GET", "/gateways").json()
507+
if any(g["id"] == gid for g in gateways):
508+
raise RuntimeError(f"Gateway {gid} still exists after deletion")
509+
510+
logging.info("✅ Gateway deleted successfully")
362511

363512

364513
# ───────────────────────────── Step registry ─────────────────────────────
@@ -391,7 +540,7 @@ def main():
391540

392541
logging.basicConfig(
393542
level=logging.DEBUG if args.verbose else logging.INFO,
394-
format="%(asctime)s %(levelname)-7s %(message)s",
543+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
395544
datefmt="%H:%M:%S",
396545
)
397546

@@ -414,6 +563,13 @@ def main():
414563
failed = False
415564

416565
try:
566+
logging.info("🚀 Starting MCP Gateway smoke test")
567+
logging.info("📋 Environment:")
568+
logging.info(" - Gateway port: %d", PORT_GATEWAY)
569+
logging.info(" - Time server port: %d", PORT_TIME_SERVER)
570+
logging.info(" - Docker container: %s", DOCKER_CONTAINER)
571+
logging.info(" - Selected steps: %s", [s[0] for s in sel])
572+
417573
for no, (name, fn) in enumerate(sel, 1):
418574
logging.info("\n🔸 Step %s/%s - %s", no, len(sel), name)
419575
if name == "start_time_server":
@@ -431,9 +587,17 @@ def main():
431587
except Exception as e:
432588
failed = True
433589
logging.error("❌ Failure: %s", e, exc_info=args.verbose)
590+
logging.error("\n💡 Troubleshooting tips:")
591+
logging.error(" - Check if npx is installed: npx --version")
592+
logging.error(" - Check if uvx is installed: uvx --version")
593+
logging.error(" - Check if port %d is already in use: lsof -i :%d", PORT_TIME_SERVER, PORT_TIME_SERVER)
594+
logging.error(" - Look for supergateway_*.log files for detailed output")
595+
logging.error(" - Try running with -v for verbose output")
434596

435597
if not failed:
436598
cleanup()
599+
else:
600+
logging.warning("⚠️ Skipping cleanup due to failure. Run with --cleanup-only to clean up manually.")
437601

438602

439603
if __name__ == "__main__":

0 commit comments

Comments
 (0)