diff --git a/Tests/LibWeb/Fixtures/http-test-server.py b/Tests/LibWeb/Fixtures/http-test-server.py index bee07c34435..f0a8030e7fd 100755 --- a/Tests/LibWeb/Fixtures/http-test-server.py +++ b/Tests/LibWeb/Fixtures/http-test-server.py @@ -1,76 +1,89 @@ #!/usr/bin/env python3 -import http.client +import argparse import http.server import json import os -import signal import socketserver -import subprocess -import argparse import sys import time +from typing import Dict + +""" +Description: + This script starts a simple HTTP echo server on localhost for use in our in-tree tests. + The port is assigned by the OS on startup and printed to stdout. + +Endpoints: + - POST /echo , Creates an echo response for later use. See "Echo" class below for body properties. +""" + + +class Echo: + method: str + path: str + status: int + headers: Dict[str, str] | None + body: str | None + delay_ms: int | None + headers: dict | None + # In-memory store for echo responses -echo_store = {} +echo_store: Dict[str, Echo] = {} class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): - def __init__(self, *args, **kwargs): - super().__init__(*args, directory=self.static_directory, **kwargs) + def __init__(self, *arguments, **kwargs): + super().__init__(*arguments, directory=None, **kwargs) def do_GET(self): - if self.path == "/shutdown": - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - self.wfile.write(b"Goodbye") - self.server.server_close() - print("Goodbye") - sys.exit(0) - elif self.path == "/ping": - self.send_response(200) - self.send_header("Content-Type", "text/plain") - self.end_headers() - self.wfile.write(b"pong") - elif self.path.startswith("/static/"): - # Remove '/static/' prefix and use built-in method + if self.path.startswith("/static/"): + # Remove "/static/" prefix and use built-in method self.path = self.path[7:] return super().do_GET() else: self.handle_echo() def do_POST(self): - if self.path == "/create": + if self.path == "/echo": content_length = int(self.headers["Content-Length"]) post_data = self.rfile.read(content_length) - response_def = json.loads(post_data.decode("utf-8")) + data = json.loads(post_data.decode("utf-8")) - method = response_def.get("method", "GET").upper() - path = response_def.get("path", "") - key = f'{method} {path}' + echo = Echo() + echo.method = data.get("method", None) + echo.path = data.get("path", None) + echo.status = data.get("status", None) + echo.body = data.get("body", None) + echo.delay_ms = data.get("delay_ms", None) + echo.headers = data.get("headers", None) - is_invalid_path = path.startswith('/static') or path == '/create' or path == '/shutdown' or path == '/ping' - if (is_invalid_path or key in echo_store): + is_using_reserved_path = echo.path.startswith("/static") or echo.path.startswith("/echo") + + # Return 400: Bad Request if invalid params are given or a reserved path is given + if echo.method is None or echo.path is None or echo.status is None or is_using_reserved_path: self.send_response(400) self.send_header("Content-Type", "text/plain") self.end_headers() - - if is_invalid_path: - self.wfile.write(b"invalid path, must not be /static, /create, /shutdown, /ping") - else: - self.wfile.write(b"invalid path, already registered") - return - echo_store[key] = response_def + # Return 409: Conflict if the method+path combination already exists + key = f"{echo.method} {echo.path}" + if key in echo_store: + self.send_response(409) + self.send_header("Content-Type", "text/plain") + self.end_headers() + return - host = self.headers.get('host', 'localhost') - path = path.lstrip('/') - fetch_url = f'http://{host}/{path}' + echo_store[key] = echo + + host = self.headers.get("host", "localhost") + path = echo.path.lstrip("/") + fetch_url = f"http://{host}/{path}" # The params to use on the client when making a request to the newly created echo endpoint fetch_config = { - "method": method, + "method": echo.method, "url": fetch_url, } @@ -85,7 +98,7 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): self.handle_echo() def do_OPTIONS(self): - if self.path.startswith("/create"): + if self.path.startswith("/echo"): self.send_response(204) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "*") @@ -105,23 +118,25 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): def handle_echo(self): method = self.command.upper() - key = f'{method} {self.path}' + key = f"{method} {self.path}" if key in echo_store: - response_def = echo_store[key] + echo = echo_store[key] - if "delay" in response_def: - time.sleep(response_def["delay"]) + if echo.delay_ms is not None: + time.sleep(echo.delay_ms / 1000) # Send the status code without any default headers - self.send_response_only(response_def.get("status", 200)) + self.send_response_only(echo.status) # Set only the headers defined in the echo definition - for header, value in response_def.get("headers", {}).items(): - self.send_header(header, value) - self.end_headers() + if echo.headers is not None: + for header, value in echo.headers.items(): + self.send_header(header, value) + self.end_headers() - self.wfile.write(response_def.get("body", "").encode("utf-8")) + response_body = echo.body or "" + self.wfile.write(response_body.encode("utf-8")) else: self.send_error(404, f"Echo response not found for {key}") @@ -132,22 +147,12 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): self.handle_echo() -pid_file_path = "http-test-server.pid.txt" -log_file_path = "http-test-server.log" - - -def run_server(port=8000, static_directory="."): +def start_server(port, static_directory): TestHTTPRequestHandler.static_directory = os.path.abspath(static_directory) - httpd = socketserver.TCPServer(("", port), TestHTTPRequestHandler) + httpd = socketserver.TCPServer(("127.0.0.1", port), TestHTTPRequestHandler) - print(f"Serving at http://localhost:{port}/") - print( - f"Serving static files from directory: {TestHTTPRequestHandler.static_directory}" - ) - - # Save pid to file - with open(pid_file_path, "w") as f: - f.write(str(os.getpid())) + print(httpd.socket.getsockname()[1]) + sys.stdout.flush() try: httpd.serve_forever() @@ -156,70 +161,9 @@ def run_server(port=8000, static_directory="."): finally: httpd.server_close() - print("Goodbye") - - -def stop_server(quiet=False): - if os.path.exists(pid_file_path): - with open(pid_file_path, "r") as f: - pid = int(f.read().strip()) - try: - os.kill(pid, signal.SIGTERM) - os.remove(pid_file_path) - print("Server stopped") - except ProcessLookupError: - print("Server not running") - except PermissionError: - print("Permission denied when trying to stop the server") - elif not quiet: - print("No server running") - - -def start_server_in_background(port, directory): - # Launch the server as a detached subprocess - with open(log_file_path, "w") as log_file: - stop_server(True) - subprocess.Popen( - [sys.executable, __file__, "start", "-p", str(port), "-d", directory], - stdout=log_file, - stderr=log_file, - preexec_fn=os.setpgrp, - ) - - # Sleep to give the server time to start - time.sleep(0.05) - - # Verify that the server is up by sending a GET request to /ping - max_retries = 3 - for i in range(max_retries): - try: - conn = http.client.HTTPConnection("localhost", port, timeout=1) - conn.request("GET", "/ping") - response = conn.getresponse() - if response.status == 200 and response.read().decode().strip() == "pong": - print(f"Server successfully started on port {port}") - return True - except (http.client.HTTPException, ConnectionRefusedError, OSError): - if i < max_retries - 1: - print( - f"Server not ready, retrying in 1 second... (Attempt {i+1}/{max_retries})" - ) - time.sleep(1) - else: - print(f"Failed to start server after {max_retries} attempts") - return False - finally: - conn.close() - - print(f"Server verification failed after {max_retries} attempts") - return False - if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Run a test HTTP server") - parser.add_argument( - "-p", "--port", type=int, default=8123, help="Port to run the server on" - ) + parser = argparse.ArgumentParser(description="Run a HTTP echo server") parser.add_argument( "-d", "--directory", @@ -228,21 +172,12 @@ if __name__ == "__main__": help="Directory to serve static files from", ) parser.add_argument( - "-b", - "--background", - action="store_true", - help="Run the server in the background", + "-p", + "--port", + type=int, + default=0, + help="Port to run the server on", ) - parser.add_argument("action", choices=["start", "stop"], help="Action to perform") args = parser.parse_args() - if args.action == "start": - if args.background: - # Detach the server and run in the background - start_server_in_background(args.port, args.directory) - print(f"Server started in the background, check '{log_file_path}' for details.") - else: - # Run normally - run_server(port=args.port, static_directory=args.directory) - elif args.action == "stop": - stop_server() + start_server(port=args.port, static_directory=args.directory) diff --git a/Tests/LibWeb/Text/input/include.js b/Tests/LibWeb/Text/input/include.js index 2b643c7649d..7734ca34f15 100644 --- a/Tests/LibWeb/Text/input/include.js +++ b/Tests/LibWeb/Text/input/include.js @@ -104,7 +104,7 @@ class HTTPTestServer { this.baseURL = baseURL; } async createEcho(method, path, options) { - const result = await fetch(`${this.baseURL}/create`, { + const result = await fetch(`${this.baseURL}/echo`, { method: "POST", headers: { "Content-Type": "application/json",