coverage_server.py
1 """ 2 Simple HTTP server for serving coverage reports. 3 4 This allows the coverage HTML report to be displayed in an iframe 5 without browser security restrictions on file:// URLs. 6 """ 7 8 import http.server 9 import socketserver 10 import threading 11 import os 12 from pathlib import Path 13 14 15 class CoverageServer: 16 """Singleton HTTP server for serving coverage reports.""" 17 18 _instance = None 19 _server = None 20 _thread = None 21 PORT = 8503 22 23 def __new__(cls): 24 if cls._instance is None: 25 cls._instance = super().__new__(cls) 26 return cls._instance 27 28 def start(self, coverage_dir: str = "coverage"): 29 """Start the HTTP server if not already running.""" 30 if self._server is not None: 31 return # Already running 32 33 # Get absolute path to coverage directory 34 coverage_path = Path(coverage_dir).resolve() 35 36 if not coverage_path.exists(): 37 raise FileNotFoundError(f"Coverage directory not found: {coverage_path}") 38 39 # Change to coverage directory 40 original_dir = os.getcwd() 41 42 def serve(): 43 try: 44 os.chdir(coverage_path) 45 handler = http.server.SimpleHTTPRequestHandler 46 handler.extensions_map.update({ 47 ".js": "application/javascript", 48 ".css": "text/css", 49 ".html": "text/html", 50 }) 51 52 with socketserver.TCPServer(("", self.PORT), handler) as httpd: 53 self._server = httpd 54 print(f"Coverage server started on http://localhost:{self.PORT}") 55 httpd.serve_forever() 56 except OSError as e: 57 if e.errno == 98: # Address already in use 58 print(f"Coverage server port {self.PORT} already in use - reusing existing server") 59 # Mark as running even though we didn't start it (assumes another instance is serving) 60 self._server = True 61 else: 62 raise 63 64 # Start server in background thread 65 self._thread = threading.Thread(target=serve, daemon=True) 66 self._thread.start() 67 68 def get_url(self, path: str = "index.html") -> str: 69 """Get the URL for accessing a coverage file.""" 70 return f"http://localhost:{self.PORT}/{path}" 71 72 def is_running(self) -> bool: 73 """Check if the server is running.""" 74 return self._server is not None 75 76 77 # Global instance 78 _coverage_server = CoverageServer() 79 80 81 def get_coverage_server() -> CoverageServer: 82 """Get the global coverage server instance.""" 83 return _coverage_server