Files
Stefan LohmaierandClaude Opus 4.8 c9ccb4392b 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>
2026-06-24 21:17:26 +02:00

115 lines
4.8 KiB
Python

#!/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]}"