FastLED 3.10.6
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 sys
19import threading
20import time
21from dataclasses import dataclass
22from pathlib import Path
23
24import httpx
25from rich.console import Console
26from rich.progress import (
27 BarColumn,
28 Progress,
29 SpinnerColumn,
30 TaskProgressColumn,
31 TextColumn,
32)
33from rich.table import Table
34from running_process import STDOUT, RunningProcess
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) -> RunningProcess:
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 = RunningProcess(
90 [str(runner_path), str(dll_path)],
91 auto_run=True,
92 capture=True,
93 stderr=STDOUT,
94 encoding="utf-8",
95 errors="replace",
96 )
97
98 # Wait for server to start
99 time.sleep(config.server_start_delay)
100
101 # Verify server is responsive
102 try:
103 with httpx.Client(timeout=config.timeout) as client:
104 response = client.get(f"http://{config.host}:{config.port}/ping")
105 if response.status_code == 200:
106 return proc
107 except KeyboardInterrupt:
108 proc.terminate()
109 proc.wait()
110 _thread.interrupt_main()
111 except Exception as e:
112 proc.terminate()
113 proc.wait()
114 console.print(f"[red]✗ Server not responsive: {e}[/red]")
115 sys.exit(1)
116
117 proc.terminate()
118 proc.wait()
119 console.print("[red]✗ Server failed to start[/red]")
120 sys.exit(1)
121
122
124 config: StressTestConfig, results: StressTestResults, client: httpx.Client
125):
126 """Make a single HTTP request and record results."""
127 url = f"http://{config.host}:{config.port}/"
128
129 try:
130 start_time = time.perf_counter()
131 response = client.get(url, timeout=config.timeout)
132 elapsed_ms = (time.perf_counter() - start_time) * 1000
133
134 if response.status_code == 200:
135 results.record_success(elapsed_ms)
136 else:
137 results.record_failure("http_error")
138
139 except httpx.ConnectError:
140 results.record_failure("connection")
141 except httpx.TimeoutException:
142 results.record_failure("timeout")
143 except KeyboardInterrupt:
144 _thread.interrupt_main()
145 except Exception:
146 results.record_failure("other")
147
148
150 config: StressTestConfig, results: StressTestResults
151) -> None:
152 """Test: Multiple concurrent connections."""
153 console.print("\n[bold cyan]Test 1: Concurrent Connections[/bold cyan]")
154 console.print(f"Making {config.num_connections} concurrent requests...")
155
156 with Progress(
157 SpinnerColumn(),
158 TextColumn("[progress.description]{task.description}"),
159 BarColumn(),
160 TaskProgressColumn(),
161 console=console,
162 ) as progress:
163 task = progress.add_task("Requesting...", total=config.num_connections)
164
165 threads: list[threading.Thread] = []
166 with httpx.Client() as client:
167 for _ in range(config.num_connections):
168 thread: threading.Thread = threading.Thread(
169 target=make_request, args=(config, results, client)
170 )
171 thread.start()
172 threads.append(thread)
173
174 for thread in threads:
175 thread.join()
176 progress.update(task, advance=1)
177
178 success_rate = (
179 (results.successful_requests / results.total_requests * 100)
180 if results.total_requests > 0
181 else 0
182 )
183 console.print(
184 f" Success rate: {success_rate:.1f}% ({results.successful_requests}/{results.total_requests})"
185 )
186
187
188def stress_test_rapid(config: StressTestConfig, results: StressTestResults) -> None:
189 """Test: Rapid sequential requests."""
190 console.print("\n[bold cyan]Test 2: Rapid Sequential Requests[/bold cyan]")
191 console.print(f"Making {config.num_requests} rapid sequential requests...")
192
193 initial_count = results.total_requests
194
195 with Progress(
196 SpinnerColumn(),
197 TextColumn("[progress.description]{task.description}"),
198 BarColumn(),
199 TaskProgressColumn(),
200 console=console,
201 ) as progress:
202 task = progress.add_task("Requesting...", total=config.num_requests)
203
204 with httpx.Client() as client:
205 for _ in range(config.num_requests):
206 make_request(config, results, client)
207 progress.update(task, advance=1)
208
209 test_requests = results.total_requests - initial_count
210 test_success = results.successful_requests - (
211 initial_count - (initial_count - results.successful_requests)
212 )
213 success_rate = (test_success / test_requests * 100) if test_requests > 0 else 0
214 console.print(
215 f" Success rate: {success_rate:.1f}% ({test_success}/{test_requests})"
216 )
217
218
219def display_results(results: StressTestResults):
220 """Display test results in a formatted table."""
221 console.print("\n[bold]Stress Test Results[/bold]")
222
223 table = Table(title="Summary")
224 table.add_column("Metric", style="cyan")
225 table.add_column("Value", style="magenta")
226
227 table.add_row("Total Requests", str(results.total_requests))
228 table.add_row(
229 "Successful",
230 f"{results.successful_requests} ({results.successful_requests / results.total_requests * 100:.1f}%)",
231 )
232 table.add_row(
233 "Failed",
234 f"{results.failed_requests} ({results.failed_requests / results.total_requests * 100:.1f}%)",
235 )
236 table.add_row("Connection Errors", str(results.connection_errors))
237 table.add_row("Timeout Errors", str(results.timeout_errors))
238 table.add_row("Other Errors", str(results.other_errors))
239
240 if results.response_times:
241 table.add_row("Min Response Time", f"{min(results.response_times):.1f} ms")
242 table.add_row("Max Response Time", f"{max(results.response_times):.1f} ms")
243 table.add_row(
244 "Avg Response Time",
245 f"{sum(results.response_times) / len(results.response_times):.1f} ms",
246 )
247
248 console.print()
249 console.print(table)
250
251 success_rate = (
252 (results.successful_requests / results.total_requests * 100)
253 if results.total_requests > 0
254 else 0
255 )
256
257 if success_rate >= 95:
258 console.print(
259 "\n[green bold]✓ Phase 3 PASSED - Server handled stress test successfully (≥95% success)[/green bold]"
260 )
261 return 0
262 elif success_rate >= 80:
263 console.print(
264 f"\n[yellow bold]⚠ Phase 3 MARGINAL - Server mostly stable ({success_rate:.1f}% success, threshold: 95%)[/yellow bold]"
265 )
266 return 1
267 else:
268 console.print(
269 f"\n[red bold]✗ Phase 3 FAILED - Server unstable under stress ({success_rate:.1f}% success)[/red bold]"
270 )
271 return 1
272
273
274def main() -> int:
275 parser = argparse.ArgumentParser(
276 description="Stress test FastLED Network HTTP server"
277 )
278 parser.add_argument("--host", default="localhost", help="Server host")
279 parser.add_argument("--port", type=int, default=8080, help="Server port")
280 parser.add_argument(
281 "--connections", type=int, default=20, help="Number of concurrent connections"
282 )
283 parser.add_argument(
284 "--requests", type=int, default=50, help="Number of sequential requests"
285 )
286 args = parser.parse_args()
287
288 config = StressTestConfig(
289 host=args.host,
290 port=args.port,
291 num_connections=args.connections,
292 num_requests=args.requests,
293 )
294
295 console.print("[bold]FastLED Network HTTP Server - Phase 3 Stress Test[/bold]")
296 console.print(f"Server: http://{config.host}:{config.port}\n")
297
298 console.print("[cyan]Starting server...[/cyan]")
299 server_proc = start_server(config)
300 console.print("[green]✓ Server started and responding[/green]")
301
302 results = StressTestResults()
303
304 try:
305 # Test 1: Concurrent connections
306 stress_test_concurrent(config, results)
307
308 # Test 2: Rapid sequential requests
309 stress_test_rapid(config, results)
310
311 # Display results
312 exit_code = display_results(results)
313
314 return exit_code
315
316 except KeyboardInterrupt:
317 console.print("\n[yellow]Test interrupted by user[/yellow]")
318 _thread.interrupt_main()
319 return 130
320
321 finally:
322 console.print("\n[cyan]Stopping server...[/cyan]")
323 server_proc.terminate()
324 try:
325 server_proc.wait(timeout=5)
326 except TimeoutError:
327 # RunningProcess.wait(timeout=) already killed the process.
328 server_proc.kill()
329 console.print("[green]✓ Server stopped[/green]")
330
331
332if __name__ == "__main__":
333 sys.exit(main())
record_failure(self, str error_type)
record_success(self, float response_time_ms)
RunningProcess start_server(StressTestConfig config)
make_request(StressTestConfig config, StressTestResults results, httpx.Client client)
None stress_test_rapid(StressTestConfig config, StressTestResults results)
None stress_test_concurrent(StressTestConfig config, StressTestResults results)
display_results(StressTestResults results)