Skip to content

Commit 3d8fe70

Browse files
author
Sebastian
committed
Add new async testing files
Signed-off-by: Sebastian <[email protected]>
1 parent e08e9b1 commit 3d8fe70

11 files changed

+826
-0
lines changed

async_testing/async_validator.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""
2+
Validate async code patterns and detect common pitfalls.
3+
"""
4+
5+
import ast
6+
import argparse
7+
import json
8+
from pathlib import Path
9+
from typing import List, Dict, Any
10+
11+
class AsyncCodeValidator:
12+
"""Validate async code for common patterns and pitfalls."""
13+
14+
def __init__(self):
15+
self.issues = []
16+
self.suggestions = []
17+
18+
def validate_directory(self, source_dir: Path) -> Dict[str, Any]:
19+
"""Validate all Python files in directory."""
20+
21+
validation_results = {
22+
'files_checked': 0,
23+
'issues_found': 0,
24+
'suggestions': 0,
25+
'details': []
26+
}
27+
28+
python_files = list(source_dir.rglob("*.py"))
29+
30+
for file_path in python_files:
31+
if self._should_skip_file(file_path):
32+
continue
33+
34+
file_results = self._validate_file(file_path)
35+
validation_results['details'].append(file_results)
36+
validation_results['files_checked'] += 1
37+
validation_results['issues_found'] += len(file_results['issues'])
38+
validation_results['suggestions'] += len(file_results['suggestions'])
39+
40+
return validation_results
41+
42+
def _validate_file(self, file_path: Path) -> Dict[str, Any]:
43+
"""Validate a single Python file."""
44+
45+
file_results = {
46+
'file': str(file_path),
47+
'issues': [],
48+
'suggestions': []
49+
}
50+
51+
try:
52+
with open(file_path, 'r', encoding='utf-8') as f:
53+
source_code = f.read()
54+
55+
tree = ast.parse(source_code, filename=str(file_path))
56+
57+
# Analyze AST for async patterns
58+
validator = AsyncPatternVisitor(file_path)
59+
validator.visit(tree)
60+
61+
file_results['issues'] = validator.issues
62+
file_results['suggestions'] = validator.suggestions
63+
64+
except Exception as e:
65+
file_results['issues'].append({
66+
'type': 'parse_error',
67+
'message': f"Failed to parse file: {str(e)}",
68+
'line': 0
69+
})
70+
71+
return file_results
72+
73+
74+
def _should_skip_file(self, file_path: Path) -> bool:
75+
"""Determine if a file should be skipped (e.g., __init__.py files)."""
76+
return file_path.name == "__init__.py"
77+
78+
class AsyncPatternVisitor(ast.NodeVisitor):
79+
"""AST visitor to detect async patterns and issues."""
80+
81+
def __init__(self, file_path: Path):
82+
self.file_path = file_path
83+
self.issues = []
84+
self.suggestions = []
85+
self.in_async_function = False
86+
87+
def visit_AsyncFunctionDef(self, node):
88+
"""Visit async function definitions."""
89+
90+
self.in_async_function = True
91+
92+
# Check for blocking operations in async functions
93+
self._check_blocking_operations(node)
94+
95+
# Check for proper error handling
96+
self._check_error_handling(node)
97+
98+
self.generic_visit(node)
99+
self.in_async_function = False
100+
101+
def visit_Call(self, node):
102+
"""Visit function calls."""
103+
104+
if self.in_async_function:
105+
# Check for potentially unawaited async calls
106+
self._check_unawaited_calls(node)
107+
108+
# Check for blocking I/O operations
109+
self._check_blocking_io(node)
110+
111+
self.generic_visit(node)
112+
113+
def _check_blocking_operations(self, node):
114+
"""Check for blocking operations in async functions."""
115+
116+
blocking_patterns = [
117+
'time.sleep',
118+
'requests.get', 'requests.post',
119+
'subprocess.run', 'subprocess.call',
120+
'open' # File I/O without async
121+
]
122+
123+
for child in ast.walk(node):
124+
if isinstance(child, ast.Call):
125+
call_name = self._get_call_name(child)
126+
if call_name in blocking_patterns:
127+
self.issues.append({
128+
'type': 'blocking_operation',
129+
'message': f"Blocking operation '{call_name}' in async function",
130+
'line': child.lineno,
131+
'suggestion': f"Use async equivalent of {call_name}"
132+
})
133+
134+
def _check_unawaited_calls(self, node):
135+
"""Check for potentially unawaited async calls."""
136+
137+
# Look for calls that might return coroutines
138+
async_patterns = [
139+
'aiohttp', 'asyncio', 'asyncpg',
140+
'websockets', 'motor' # Common async libraries
141+
]
142+
143+
call_name = self._get_call_name(node)
144+
145+
for pattern in async_patterns:
146+
if pattern in call_name:
147+
# Check if this call is awaited
148+
parent = getattr(node, 'parent', None)
149+
if not isinstance(parent, ast.Await):
150+
self.suggestions.append({
151+
'type': 'potentially_unawaited',
152+
'message': f"Call to '{call_name}' might need await",
153+
'line': node.lineno
154+
})
155+
break
156+
157+
def _get_call_name(self, node):
158+
"""Extract the name of a function call."""
159+
160+
if isinstance(node.func, ast.Name):
161+
return node.func.id
162+
elif isinstance(node.func, ast.Attribute):
163+
if isinstance(node.func.value, ast.Name):
164+
return f"{node.func.value.id}.{node.func.attr}"
165+
else:
166+
return node.func.attr
167+
return "unknown"
168+
169+
170+
if __name__ == "__main__":
171+
parser = argparse.ArgumentParser(description="Validate async code patterns and detect common pitfalls.")
172+
parser.add_argument("--source", type=Path, required=True, help="Source directory to validate.")
173+
parser.add_argument("--report", type=Path, required=True, help="Path to the output validation report.")
174+
175+
args = parser.parse_args()
176+
177+
validator = AsyncCodeValidator()
178+
results = validator.validate_directory(args.source)
179+
180+
with open(args.report, 'w') as f:
181+
json.dump(results, f, indent=4)
182+
183+
print(f"Validation report saved to {args.report}")

async_testing/benchmarks.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Run async performance benchmarks and output results.
3+
"""
4+
import asyncio
5+
import time
6+
import json
7+
import argparse
8+
from pathlib import Path
9+
from typing import Any, Dict
10+
11+
class AsyncBenchmark:
12+
"""Run async performance benchmarks."""
13+
14+
def __init__(self, iterations: int):
15+
self.iterations = iterations
16+
self.results: Dict[str, Any] = {
17+
'iterations': self.iterations,
18+
'benchmarks': []
19+
}
20+
21+
async def run_benchmarks(self) -> None:
22+
"""Run all benchmarks."""
23+
24+
# Example benchmarks
25+
await self._benchmark_example("Example Benchmark 1", self.example_benchmark_1)
26+
await self._benchmark_example("Example Benchmark 2", self.example_benchmark_2)
27+
28+
async def _benchmark_example(self, name: str, benchmark_func) -> None:
29+
"""Run a single benchmark and record its performance."""
30+
31+
start_time = time.perf_counter()
32+
33+
for _ in range(self.iterations):
34+
await benchmark_func()
35+
36+
end_time = time.perf_counter()
37+
total_time = end_time - start_time
38+
avg_time = total_time / self.iterations
39+
40+
self.results['benchmarks'].append({
41+
'name': name,
42+
'total_time': total_time,
43+
'average_time': avg_time
44+
})
45+
46+
async def example_benchmark_1(self) -> None:
47+
"""An example async benchmark function."""
48+
await asyncio.sleep(0.001)
49+
50+
async def example_benchmark_2(self) -> None:
51+
"""Another example async benchmark function."""
52+
await asyncio.sleep(0.002)
53+
54+
def save_results(self, output_path: Path) -> None:
55+
"""Save benchmark results to a file."""
56+
57+
with open(output_path, 'w') as f:
58+
json.dump(self.results, f, indent=4)
59+
60+
print(f"Benchmark results saved to {output_path}")
61+
62+
63+
if __name__ == "__main__":
64+
parser = argparse.ArgumentParser(description="Run async performance benchmarks.")
65+
parser.add_argument("--output", type=Path, required=True, help="Path to the output benchmark results file.")
66+
parser.add_argument("--iterations", type=int, default=1000, help="Number of iterations to run each benchmark.")
67+
68+
args = parser.parse_args()
69+
70+
benchmark = AsyncBenchmark(args.iterations)
71+
asyncio.run(benchmark.run_benchmarks())
72+
benchmark.save_results(args.output)

async_testing/config.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
async_linting:
2+
ruff_rules: ["F", "E", "B", "ASYNC"]
3+
flake8_plugins: ["flake8-bugbear", "flake8-async"]
4+
mypy_config:
5+
warn_unused_coroutine: true
6+
strict: true
7+
8+
profiling:
9+
output_dir: "async_testing/profiles"
10+
snakeviz_port: 8080
11+
profile_scenarios:
12+
- "websocket_stress_test"
13+
- "database_query_performance"
14+
- "concurrent_mcp_calls"
15+
16+
monitoring:
17+
aiomonitor_port: 50101
18+
debug_mode: true
19+
task_tracking: true
20+
21+
performance_thresholds:
22+
websocket_connection: 100 # ms
23+
database_query: 50 # ms
24+
mcp_rpc_call: 100 # ms
25+
concurrent_users: 100 # simultaneous connections

async_testing/monitor_runner.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
Runtime async monitoring with aiomonitor integration.
3+
"""
4+
import asyncio
5+
from typing import Any, Dict
6+
import aiomonitor
7+
import argparse
8+
9+
class AsyncMonitor:
10+
"""Monitor live async operations in mcpgateway."""
11+
12+
def __init__(self, webui_port: int = 50101, console_port: int = 50102, host: str = "localhost"):
13+
self.webui_port = webui_port
14+
self.console_port = console_port
15+
self.host = host
16+
self.monitor = None
17+
self.running = False
18+
19+
async def start_monitoring(self, console_enabled: bool = True):
20+
"""Start aiomonitor for live async debugging."""
21+
22+
print(f"👁️ Starting aiomonitor on http://{self.host}:{self.webui_port}")
23+
24+
# Configure aiomonitor
25+
self.monitor = aiomonitor.Monitor(
26+
asyncio.get_event_loop(),
27+
host=self.host,
28+
webui_port=self.webui_port,
29+
console_port=self.console_port, # TODO: FIX CONSOLE NOT CONNECTING TO PORT
30+
console_enabled=console_enabled,
31+
locals={'monitor': self}
32+
)
33+
34+
self.monitor.start()
35+
self.running = True
36+
37+
if console_enabled:
38+
print(f"🌐 aiomonitor console available at: http://{self.host}:{self.console_port}")
39+
print("📊 Available commands: ps, where, cancel, signal, console")
40+
print("🔍 Use 'ps' to list running tasks")
41+
print("📍 Use 'where <task_id>' to see task stack trace")
42+
43+
# Keep monitoring running
44+
try:
45+
while self.running:
46+
await asyncio.sleep(1)
47+
48+
# Periodic task summary
49+
tasks = [t for t in asyncio.all_tasks() if not t.done()]
50+
if len(tasks) % 100 == 0 and len(tasks) > 0:
51+
print(f"📈 Current active tasks: {len(tasks)}")
52+
53+
except KeyboardInterrupt: # TODO: FIX STACK TRACE STILL APPEARING ON CTRL-C
54+
print("\n🛑 Stopping aiomonitor...")
55+
finally:
56+
self.monitor.close()
57+
58+
def stop_monitoring(self):
59+
"""Stop the monitoring."""
60+
self.running = False
61+
62+
async def get_task_summary(self) -> Dict[str, Any]:
63+
"""Get summary of current async tasks."""
64+
65+
tasks = asyncio.all_tasks()
66+
67+
summary: Dict[str, Any] = {
68+
'total_tasks': len(tasks),
69+
'running_tasks': len([t for t in tasks if not t.done()]),
70+
'completed_tasks': len([t for t in tasks if t.done()]),
71+
'cancelled_tasks': len([t for t in tasks if t.cancelled()]),
72+
'task_details': []
73+
}
74+
75+
for task in tasks:
76+
if not task.done():
77+
summary['task_details'].append({
78+
'name': getattr(task, '_name', 'unnamed'),
79+
'state': task._state.name if hasattr(task, '_state') else 'unknown',
80+
'coro': str(task._coro) if hasattr(task, '_coro') else 'unknown'
81+
})
82+
83+
return summary
84+
85+
if __name__ == "__main__":
86+
parser = argparse.ArgumentParser(description="Run aiomonitor for live async debugging.")
87+
parser.add_argument("--host", type=str, default="localhost", help="Host to run aiomonitor on.")
88+
parser.add_argument("--webui_port", type=int, default=50101, help="Port to run aiomonitor on.")
89+
parser.add_argument("--console_port", type=int, default=50102, help="Port to run aiomonitor on.")
90+
parser.add_argument("--console-enabled", action="store_true", help="Enable console for aiomonitor.")
91+
92+
args = parser.parse_args()
93+
94+
monitor = AsyncMonitor(webui_port=args.webui_port, console_port=args.console_port, host=args.host)
95+
asyncio.run(monitor.start_monitoring(console_enabled=args.console_enabled))

0 commit comments

Comments
 (0)