FastLED 3.9.15
Loading...
Searching...
No Matches
test_stress.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Phase 3: HTTP Server Stress Test for FastLED Network Example
4
5Tests server stability under various stress conditions:
6- Rapid connection/disconnection cycles
7- Multiple concurrent connections
8- Request flooding
9- Server start/stop cycles
10
11Usage:
12 uv run python examples/Asio/Server/test_stress.py
13 uv run python examples/Asio/Server/test_stress.py --connections 50 --requests 100
14"""
15
16import _thread
17import argparse
18import subprocess
19import sys
20import threading
21import time
22from dataclasses import dataclass
23from pathlib import Path
24
25import httpx
26from rich.console import Console
27from rich.progress import (
28 BarColumn,
29 Progress,
30 SpinnerColumn,
31 TaskProgressColumn,
32 TextColumn,
33)
34from rich.table import Table
35
36
37console = Console()
38
39
40@dataclass
42 host: str = "localhost"
43 port: int = 8080
44 num_connections: int = 20
45 num_requests: int = 50
46 timeout: float = 5.0
47 server_start_delay: float = 2.0
48
49
51 def __init__(self) -> None:
52 self.total_requests: int = 0
53 self.successful_requests: int = 0
54 self.failed_requests: int = 0
55 self.connection_errors: int = 0
56 self.timeout_errors: int = 0
57 self.other_errors: int = 0
58 self.response_times: list[float] = []
59 self.lock: threading.Lock = threading.Lock()
60
61 def record_success(self, response_time_ms: float):
62 with self.lock:
63 self.total_requests += 1
64 self.successful_requests += 1
65 self.response_times.append(response_time_ms)
66
67 def record_failure(self, error_type: str):
68 with self.lock:
69 self.total_requests += 1
70 self.failed_requests += 1
71 if error_type == "connection":
72 self.connection_errors += 1
73 elif error_type == "timeout":
74 self.timeout_errors += 1
75 else:
76 self.other_errors += 1
77
78
79def start_server(config: StressTestConfig) -> subprocess.Popen[str]:
80 """Start HTTP server in background."""
81 runner_path = Path(".build/meson-quick/examples/example_runner.exe").absolute()
82 dll_path = Path(".build/meson-quick/examples/example-Server.dll").absolute()
83
84 if not runner_path.exists() or not dll_path.exists():
85 console.print("[red]✗ Server executable not found - compile first:[/red]")
86 console.print(" bash test Server --examples --build")
87 sys.exit(1)
88
89 proc = subprocess.Popen(
90 [str(runner_path), str(dll_path)],
91 stdout=subprocess.PIPE,
92 stderr=subprocess.STDOUT,
93 text=True,
94 )
95
96 # Wait for server to start
97 time.sleep(config.server_start_delay)
98
99 # Verify server is responsive
100 try:
101 with httpx.Client(timeout=config.timeout) as client:
102 response = client.get(f"http://{config.host}:{config.port}/ping")
103 if response.status_code == 200:
104 return proc
105 except KeyboardInterrupt:
106 proc.terminate()
107 proc.wait()
108 _thread.interrupt_main()
109 except Exception as e:
110 proc.terminate()
111 proc.wait()
112 console.print(f"[red]✗ Server not responsive: {e}[/red]")
113 sys.exit(1)
114
115 proc.terminate()
116 proc.wait()
117 console.print("[red]✗ Server failed to start[/red]")
118 sys.exit(1)
119
120
122 config: StressTestConfig, results: StressTestResults, client: httpx.Client
123):
124 """Make a single HTTP request and record results."""
125 url = f"http://{config.host}:{config.port}/"
126
127 try:
128 start_time = time.perf_counter()
129 response = client.get(url, timeout=config.timeout)
130 elapsed_ms = (time.perf_counter() - start_time) * 1000
131
132 if response.status_code == 200:
133 results.record_success(elapsed_ms)
134 else:
135 results.record_failure("http_error")
136
137 except httpx.ConnectError:
138 results.record_failure("connection")
139 except httpx.TimeoutException:
140 results.record_failure("timeout")
141 except KeyboardInterrupt:
142 _thread.interrupt_main()
143 except Exception:
144 results.record_failure("other")
145
146
148 config: StressTestConfig, results: StressTestResults
149) -> None:
150 """Test: Multiple concurrent connections."""
151 console.print("\n[bold cyan]Test 1: Concurrent Connections[/bold cyan]")
152 console.print(f"Making {config.num_connections} concurrent requests...")
153
154 with Progress(
155 SpinnerColumn(),
156 TextColumn("[progress.description]{task.description}"),
157 BarColumn(),
158 TaskProgressColumn(),
159 console=console,
160 ) as progress:
161 task = progress.add_task("Requesting...", total=config.num_connections)
162
163 threads: list[threading.Thread] = []
164 with httpx.Client() as client:
165 for _ in range(config.num_connections):
166 thread: threading.Thread = threading.Thread(
167 target=make_request, args=(config, results, client)
168 )
169 thread.start()
170 threads.append(thread)
171
172 for thread in threads:
173 thread.join()
174 progress.update(task, advance=1)
175
176 success_rate = (
177 (results.successful_requests / results.total_requests * 100)
178 if results.total_requests > 0
179 else 0
180 )
181 console.print(
182 f" Success rate: {success_rate:.1f}% ({results.successful_requests}/{results.total_requests})"
183 )
184
185
186def stress_test_rapid(config: StressTestConfig, results: StressTestResults) -> None:
187 """Test: Rapid sequential requests."""
188 console.print("\n[bold cyan]Test 2: Rapid Sequential Requests[/bold cyan]")
189 console.print(f"Making {config.num_requests} rapid sequential requests...")
190
191 initial_count = results.total_requests
192
193 with Progress(
194 SpinnerColumn(),
195 TextColumn("[progress.description]{task.description}"),
196 BarColumn(),
197 TaskProgressColumn(),
198 console=console,
199 ) as progress:
200 task = progress.add_task("Requesting...", total=config.num_requests)
201
202 with httpx.Client() as client:
203 for _ in range(config.num_requests):
204 make_request(config, results, client)
205 progress.update(task, advance=1)
206
207 test_requests = results.total_requests - initial_count
208 test_success = results.successful_requests - (
209 initial_count - (initial_count - results.successful_requests)
210 )
211 success_rate = (test_success / test_requests * 100) if test_requests > 0 else 0
212 console.print(
213 f" Success rate: {success_rate:.1f}% ({test_success}/{test_requests})"
214 )
215
216
217def display_results(results: StressTestResults):
218 """Display test results in a formatted table."""
219 console.print("\n[bold]Stress Test Results[/bold]")
220
221 table = Table(title="Summary")
222 table.add_column("Metric", style="cyan")
223 table.add_column("Value", style="magenta")
224
225 table.add_row("Total Requests", str(results.total_requests))
226 table.add_row(
227 "Successful",
228 f"{results.successful_requests} ({results.successful_requests / results.total_requests * 100:.1f}%)",
229 )
230 table.add_row(
231 "Failed",
232 f"{results.failed_requests} ({results.failed_requests / results.total_requests * 100:.1f}%)",
233 )
234 table.add_row("Connection Errors", str(results.connection_errors))
235 table.add_row("Timeout Errors", str(results.timeout_errors))
236 table.add_row("Other Errors", str(results.other_errors))
237
238 if results.response_times:
239 table.add_row("Min Response Time", f"{min(results.response_times):.1f} ms")
240 table.add_row("Max Response Time", f"{max(results.response_times):.1f} ms")
241 table.add_row(
242 "Avg Response Time",
243 f"{sum(results.response_times) / len(results.response_times):.1f} ms",
244 )
245
246 console.print()
247 console.print(table)
248
249 success_rate = (
250 (results.successful_requests / results.total_requests * 100)
251 if results.total_requests > 0
252 else 0
253 )
254
255 if success_rate >= 95:
256 console.print(
257 "\n[green bold]✓ Phase 3 PASSED - Server handled stress test successfully (≥95% success)[/green bold]"
258 )
259 return 0
260 elif success_rate >= 80:
261 console.print(
262 f"\n[yellow bold]⚠ Phase 3 MARGINAL - Server mostly stable ({success_rate:.1f}% success, threshold: 95%)[/yellow bold]"
263 )
264 return 1
265 else:
266 console.print(
267 f"\n[red bold]✗ Phase 3 FAILED - Server unstable under stress ({success_rate:.1f}% success)[/red bold]"
268 )
269 return 1
270
271
272def main() -> int:
273 parser = argparse.ArgumentParser(
274 description="Stress test FastLED Network HTTP server"
275 )
276 parser.add_argument("--host", default="localhost", help="Server host")
277 parser.add_argument("--port", type=int, default=8080, help="Server port")
278 parser.add_argument(
279 "--connections", type=int, default=20, help="Number of concurrent connections"
280 )
281 parser.add_argument(
282 "--requests", type=int, default=50, help="Number of sequential requests"
283 )
284 args = parser.parse_args()
285
286 config = StressTestConfig(
287 host=args.host,
288 port=args.port,
289 num_connections=args.connections,
290 num_requests=args.requests,
291 )
292
293 console.print("[bold]FastLED Network HTTP Server - Phase 3 Stress Test[/bold]")
294 console.print(f"Server: http://{config.host}:{config.port}\n")
295
296 console.print("[cyan]Starting server...[/cyan]")
297 server_proc = start_server(config)
298 console.print("[green]✓ Server started and responding[/green]")
299
300 results = StressTestResults()
301
302 try:
303 # Test 1: Concurrent connections
304 stress_test_concurrent(config, results)
305
306 # Test 2: Rapid sequential requests
307 stress_test_rapid(config, results)
308
309 # Display results
310 exit_code = display_results(results)
311
312 return exit_code
313
314 except KeyboardInterrupt:
315 console.print("\n[yellow]Test interrupted by user[/yellow]")
316 _thread.interrupt_main()
317 return 130
318
319 finally:
320 console.print("\n[cyan]Stopping server...[/cyan]")
321 server_proc.terminate()
322 try:
323 server_proc.wait(timeout=5)
324 except subprocess.TimeoutExpired:
325 server_proc.kill()
326 console.print("[green]✓ Server stopped[/green]")
327
328
329if __name__ == "__main__":
330 sys.exit(main())
record_failure(self, str error_type)
record_success(self, float response_time_ms)
make_request(StressTestConfig config, StressTestResults results, httpx.Client client)
subprocess.Popen[str] start_server(StressTestConfig config)
None stress_test_rapid(StressTestConfig config, StressTestResults results)
None stress_test_concurrent(StressTestConfig config, StressTestResults results)
display_results(StressTestResults results)