harden: per-item robustness + deadlines/caps across connectors; layered tests
Mail: _safe_decode + _iter_messages (per-mail try/except) -> eine kaputte Mail (charset x-unknown) bricht die Suche nicht mehr ab, findet so auch uralte Mails. Vollscan + Datum-Sort + MAX_SCAN/DEADLINE_S (Selbstheilung, kein 24h-Haenger mehr). Files: search_files mit SEARCH_DEADLINE_S + SEARCH_MAX_DIRS begrenzt (war unbegrenzte rekursive PROPFIND -> real 35s/449 Calls). Calendar/Contacts: PARSE_DEADLINE_S fuer client-seitiges vobject-Parsen; MAX_RESULTS-Output-Cap in get_events/get_tasks (war 10939 Zeilen bei 6000 Events); search Top-N + Hinweise. Tests: in Schichten getrennt -> Smoke (nightly), Edge + Stress (on-demand). Neu: test_smoke/test_edge/test_stress + conftest-Marker + run_full_tests.sh, nightly laeuft nur Smoke. Kontakt-Foto-Roundtrip-Test, Mail-Edge-Mails (x-unknown, kaputter Header, 2009er Alt-Mail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
936ebc2f56
commit
c9ccb4392b
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MCP Stress-/Concurrency-Tests — ON-DEMAND (nicht naechtlich).
|
||||
|
||||
Feuert viele gleichzeitige Requests pro Connector und prueft: alle kommen sauber
|
||||
durch (kein Deadlock/Haenger) und der Server ist danach sofort wieder responsiv.
|
||||
Genau diese Art Test haette den Mail-Server-Haenger (single-threaded, 24h blockiert)
|
||||
gefangen.
|
||||
|
||||
Lauf: /opt/mcp-servers/venv/bin/python -m pytest test_stress.py -q
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import concurrent.futures as cf
|
||||
|
||||
import pytest
|
||||
|
||||
from test_all import SERVERS, get_token, tool_call
|
||||
|
||||
pytestmark = pytest.mark.stress
|
||||
|
||||
N = 40 # gleichzeitige Requests pro Welle
|
||||
WORKERS = 16 # parallele Clients
|
||||
RESPONSIVE_S = 15 # Server muss nach der Last so schnell wieder antworten
|
||||
|
||||
|
||||
def _hammer(fn, n=N):
|
||||
"""fn() n-mal nebenlaeufig ausfuehren; gibt Liste der Fehlertexte zurueck."""
|
||||
errors = []
|
||||
with cf.ThreadPoolExecutor(max_workers=WORKERS) as ex:
|
||||
futs = [ex.submit(fn) for _ in range(n)]
|
||||
for f in cf.as_completed(futs):
|
||||
try:
|
||||
f.result()
|
||||
except Exception as e: # noqa: BLE001
|
||||
errors.append(repr(e)[:200])
|
||||
return errors
|
||||
|
||||
|
||||
def _assert_responsive(port, probe):
|
||||
t = time.time()
|
||||
probe()
|
||||
dt = time.time() - t
|
||||
assert dt < RESPONSIVE_S, f"Server {port} nach Last traege: {dt:.1f}s"
|
||||
|
||||
|
||||
class TestStress:
|
||||
|
||||
def test_mail_concurrent_search(self):
|
||||
port = SERVERS["mail"]; token = get_token(port)
|
||||
queries = ["mcptest", "Rechnung", "stefan", "Willkommen",
|
||||
"XUNKNOWNTOKEN", "URALTMAIL2009", "BADHEADERTOKEN", "Newsletter"]
|
||||
errors = _hammer(lambda: tool_call(port, token, "search_mail",
|
||||
{"query": random.choice(queries), "limit": 5, "account": "mcp-test-mail"}))
|
||||
assert not errors, f"{len(errors)} Fehler unter Last: {errors[:3]}"
|
||||
_assert_responsive(port, lambda: tool_call(port, token, "list_accounts"))
|
||||
|
||||
def test_calendar_concurrent_reads(self):
|
||||
port = SERVERS["calendar"]; token = get_token(port)
|
||||
def call():
|
||||
if random.random() < 0.5:
|
||||
tool_call(port, token, "list_calendars")
|
||||
else:
|
||||
tool_call(port, token, "get_events",
|
||||
{"calendar": "calendar-test", "date_from": "2026-01-01", "date_to": "2026-12-31"})
|
||||
errors = _hammer(call)
|
||||
assert not errors, f"{len(errors)} Fehler: {errors[:3]}"
|
||||
_assert_responsive(port, lambda: tool_call(port, token, "list_calendars"))
|
||||
|
||||
def test_contacts_concurrent_search(self):
|
||||
port = SERVERS["contacts"]; token = get_token(port)
|
||||
qs = ["Mustermann", "Mcp", "example", "xyzzy_nobody", "GmbH"]
|
||||
errors = _hammer(lambda: tool_call(port, token, "search_contacts",
|
||||
{"query": random.choice(qs), "limit": 5}))
|
||||
assert not errors, f"{len(errors)} Fehler: {errors[:3]}"
|
||||
_assert_responsive(port, lambda: tool_call(port, token, "search_contacts", {"query": "Mustermann", "limit": 1}))
|
||||
|
||||
def test_files_concurrent_reads(self):
|
||||
port = SERVERS["files"]; token = get_token(port)
|
||||
def call():
|
||||
if random.random() < 0.5:
|
||||
tool_call(port, token, "list_files", {"path": "/"})
|
||||
else:
|
||||
tool_call(port, token, "file_info", {"path": "/"})
|
||||
errors = _hammer(call)
|
||||
assert not errors, f"{len(errors)} Fehler: {errors[:3]}"
|
||||
_assert_responsive(port, lambda: tool_call(port, token, "list_files", {"path": "/"}))
|
||||
|
||||
def test_notes_concurrent_reads(self):
|
||||
port = SERVERS["notes"]; token = get_token(port)
|
||||
def call():
|
||||
if random.random() < 0.5:
|
||||
tool_call(port, token, "list_notebooks")
|
||||
else:
|
||||
tool_call(port, token, "list_notes", {"limit": 5})
|
||||
errors = _hammer(call)
|
||||
assert not errors, f"{len(errors)} Fehler: {errors[:3]}"
|
||||
_assert_responsive(port, lambda: tool_call(port, token, "list_notebooks"))
|
||||
|
||||
def test_all_servers_mixed_load(self):
|
||||
"""Alle 5 Connector gleichzeitig unter Last — Gesamtsystem haelt."""
|
||||
probes = {
|
||||
"mail": lambda p, t: tool_call(p, t, "search_mail", {"query": "mcptest", "limit": 3, "account": "mcp-test-mail"}),
|
||||
"calendar": lambda p, t: tool_call(p, t, "list_calendars"),
|
||||
"contacts": lambda p, t: tool_call(p, t, "search_contacts", {"query": "Mustermann", "limit": 2}),
|
||||
"files": lambda p, t: tool_call(p, t, "list_files", {"path": "/"}),
|
||||
"notes": lambda p, t: tool_call(p, t, "list_notebooks"),
|
||||
}
|
||||
tokens = {svc: get_token(SERVERS[svc]) for svc in probes}
|
||||
def call():
|
||||
svc = random.choice(list(probes))
|
||||
probes[svc](SERVERS[svc], tokens[svc])
|
||||
errors = _hammer(call, n=60)
|
||||
assert not errors, f"{len(errors)} Fehler im Mischbetrieb: {errors[:3]}"
|
||||
Reference in New Issue
Block a user