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()
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")
89 proc = subprocess.Popen(
90 [str(runner_path), str(dll_path)],
91 stdout=subprocess.PIPE,
92 stderr=subprocess.STDOUT,
97 time.sleep(config.server_start_delay)
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:
105 except KeyboardInterrupt:
108 _thread.interrupt_main()
109 except Exception
as e:
112 console.print(f
"[red]✗ Server not responsive: {e}[/red]")
117 console.print(
"[red]✗ Server failed to start[/red]")
122 config: StressTestConfig, results: StressTestResults, client: httpx.Client
124 """Make a single HTTP request and record results."""
125 url = f
"http://{config.host}:{config.port}/"
128 start_time = time.perf_counter()
129 response = client.get(url, timeout=config.timeout)
130 elapsed_ms = (time.perf_counter() - start_time) * 1000
132 if response.status_code == 200:
133 results.record_success(elapsed_ms)
135 results.record_failure(
"http_error")
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()
144 results.record_failure(
"other")
148 config: StressTestConfig, results: StressTestResults
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...")
156 TextColumn(
"[progress.description]{task.description}"),
158 TaskProgressColumn(),
161 task = progress.add_task(
"Requesting...", total=config.num_connections)
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)
170 threads.append(thread)
172 for thread
in threads:
174 progress.update(task, advance=1)
177 (results.successful_requests / results.total_requests * 100)
178 if results.total_requests > 0
182 f
" Success rate: {success_rate:.1f}% ({results.successful_requests}/{results.total_requests})"
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...")
191 initial_count = results.total_requests
195 TextColumn(
"[progress.description]{task.description}"),
197 TaskProgressColumn(),
200 task = progress.add_task(
"Requesting...", total=config.num_requests)
202 with httpx.Client()
as client:
203 for _
in range(config.num_requests):
205 progress.update(task, advance=1)
207 test_requests = results.total_requests - initial_count
208 test_success = results.successful_requests - (
209 initial_count - (initial_count - results.successful_requests)
211 success_rate = (test_success / test_requests * 100)
if test_requests > 0
else 0
213 f
" Success rate: {success_rate:.1f}% ({test_success}/{test_requests})"
218 """Display test results in a formatted table."""
219 console.print(
"\n[bold]Stress Test Results[/bold]")
221 table = Table(title=
"Summary")
222 table.add_column(
"Metric", style=
"cyan")
223 table.add_column(
"Value", style=
"magenta")
225 table.add_row(
"Total Requests", str(results.total_requests))
228 f
"{results.successful_requests} ({results.successful_requests / results.total_requests * 100:.1f}%)",
232 f
"{results.failed_requests} ({results.failed_requests / results.total_requests * 100:.1f}%)",
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))
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")
243 f
"{sum(results.response_times) / len(results.response_times):.1f} ms",
250 (results.successful_requests / results.total_requests * 100)
251 if results.total_requests > 0
255 if success_rate >= 95:
257 "\n[green bold]✓ Phase 3 PASSED - Server handled stress test successfully (≥95% success)[/green bold]"
260 elif success_rate >= 80:
262 f
"\n[yellow bold]⚠ Phase 3 MARGINAL - Server mostly stable ({success_rate:.1f}% success, threshold: 95%)[/yellow bold]"
267 f
"\n[red bold]✗ Phase 3 FAILED - Server unstable under stress ({success_rate:.1f}% success)[/red bold]"
273 parser = argparse.ArgumentParser(
274 description=
"Stress test FastLED Network HTTP server"
276 parser.add_argument(
"--host", default=
"localhost", help=
"Server host")
277 parser.add_argument(
"--port", type=int, default=8080, help=
"Server port")
279 "--connections", type=int, default=20, help=
"Number of concurrent connections"
282 "--requests", type=int, default=50, help=
"Number of sequential requests"
284 args = parser.parse_args()
289 num_connections=args.connections,
290 num_requests=args.requests,
293 console.print(
"[bold]FastLED Network HTTP Server - Phase 3 Stress Test[/bold]")
294 console.print(f
"Server: http://{config.host}:{config.port}\n")
296 console.print(
"[cyan]Starting server...[/cyan]")
298 console.print(
"[green]✓ Server started and responding[/green]")
314 except KeyboardInterrupt:
315 console.print(
"\n[yellow]Test interrupted by user[/yellow]")
316 _thread.interrupt_main()
320 console.print(
"\n[cyan]Stopping server...[/cyan]")
321 server_proc.terminate()
323 server_proc.wait(timeout=5)
324 except subprocess.TimeoutExpired:
326 console.print(
"[green]✓ Server stopped[/green]")