From 789194ee64193af326c738f54457727d4f937d0f Mon Sep 17 00:00:00 2001 From: Jacek Wielemborek Date: Sat, 25 Oct 2025 20:34:13 +0200 Subject: [PATCH] refactor(test): rewrite test_verify.sh to Python with guard clauses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converted bash test script to Python for better maintainability: - Guard clause pattern replaces nested if statements - Early returns for cleaner control flow - Type hints for better documentation - Proper error handling and cleanup - More readable and testable code structure Features: - Starts Xvfb and web-ext - Waits for extension installation - Checks for JavaScript errors - Verifies debugger connectivity - Clean process termination Usage: scripts/test_verify.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- scripts/test_verify.py | 249 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100755 scripts/test_verify.py diff --git a/scripts/test_verify.py b/scripts/test_verify.py new file mode 100755 index 0000000..de34623 --- /dev/null +++ b/scripts/test_verify.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +""" +test_verify.py - Verifies extension and exits (doesn't wait forever) + +This script is a Python version of test_verify.sh that: +- Starts Firefox with the extension +- Verifies extension loaded without errors +- EXITS after verification (instead of waiting forever) + +Used for automated tests in CI/Docker. +Uses guard clauses pattern for cleaner flow control. +""" + +import sys +import time +import subprocess +import os +import signal +from pathlib import Path + + +def print_header(text: str) -> None: + """Print formatted header.""" + print(f"\n{text}") + + +def print_success(text: str) -> None: + """Print success message.""" + print(f"✓ {text}") + + +def print_error(text: str) -> None: + """Print error message.""" + print(f"✗ {text}") + + +def start_xvfb() -> int: + """Start Xvfb virtual X server. Returns PID.""" + print_header("Starting Xvfb on display :99...") + + xvfb = subprocess.Popen( + ["Xvfb", ":99", "-screen", "0", "1024x768x24"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + os.environ["DISPLAY"] = ":99" + time.sleep(2) + + print_success(f"Xvfb started with PID: {xvfb.pid}") + return xvfb.pid + + +def start_webext() -> tuple[int, Path]: + """Start web-ext and log output to file. Returns (PID, log_path).""" + print_header("Starting web-ext run with verbose logging...") + print("=" * 40) + + log_path = Path("/tmp/web-ext.log") + log_file = open(log_path, "w") + + webext = subprocess.Popen( + ["npx", "web-ext", "run", "--verbose"], + stdout=log_file, + stderr=subprocess.STDOUT + ) + + return webext.pid, log_path + + +def wait_for_extension_install(log_path: Path, timeout_seconds: int = 30) -> bool: + """Wait for extension installation confirmation in logs.""" + print_header("Waiting for extension to install...") + + for _ in range(timeout_seconds): + if not log_path.exists(): + time.sleep(1) + continue + + content = log_path.read_text() + if "Installed /app as a temporary add-on" in content: + print("=" * 40) + print_success("Extension installed!") + print_success("Firefox is running in headless mode") + print_success("Extension: rentgen@internet-czas-dzialac.pl") + return True + + time.sleep(1) + + return False + + +def check_javascript_errors(log_path: Path) -> list[str]: + """Check for JavaScript errors in extension code. Returns list of errors.""" + print_header("Checking for JavaScript errors in extension code...") + + content = log_path.read_text() + + # Filter out unrelated Firefox errors + error_patterns = [ + "JavaScript error.*background.js", + "SyntaxError.*background", + "ReferenceError.*background" + ] + + errors = [] + for line in content.split("\n"): + # Skip BackupService and RSLoader errors (Firefox internal) + if "BackupService" in line or "RSLoader" in line: + continue + + for pattern in error_patterns: + import re + if re.search(pattern, line, re.IGNORECASE): + errors.append(line) + break + + return errors + + +def check_debugger_port(log_path: Path) -> str | None: + """Extract debugger port from logs. Returns port number or None.""" + content = log_path.read_text() + + import re + match = re.search(r"start-debugger-server (\d+)", content) + if match: + return match.group(1) + return None + + +def test_debugger_connectivity(port: str) -> bool: + """Test if remote debugging protocol is accessible.""" + try: + result = subprocess.run( + ["timeout", "2", "bash", "-c", f"echo > /dev/tcp/127.0.0.1/{port}"], + capture_output=True, + timeout=3 + ) + return result.returncode == 0 + except (subprocess.TimeoutExpired, subprocess.SubprocessError): + return False + + +def cleanup(xvfb_pid: int, webext_pid: int) -> None: + """Cleanup processes.""" + try: + os.kill(webext_pid, signal.SIGTERM) + except ProcessLookupError: + pass + + try: + os.kill(xvfb_pid, signal.SIGTERM) + except ProcessLookupError: + pass + + +def main() -> int: + """Main test verification logic using guard clauses.""" + + # Start Xvfb + xvfb_pid = start_xvfb() + + # Start web-ext + webext_pid, log_path = start_webext() + + # Guard: Check if extension installed + if not wait_for_extension_install(log_path): + print_error("Extension failed to install within 30s") + cleanup(xvfb_pid, webext_pid) + return 1 + + # Give extension time to initialize + time.sleep(3) + + # Guard: Check for JavaScript errors + errors = check_javascript_errors(log_path) + if errors: + print() + print("=" * 40) + print("✗✗✗ CRITICAL ERROR ✗✗✗") + print("=" * 40) + print("Found JavaScript errors in background.js!") + print("Extension installed but CODE DID NOT EXECUTE!") + print() + print("Errors:") + for error in errors[:10]: + print(f" {error}") + print("=" * 40) + + cleanup(xvfb_pid, webext_pid) + return 1 + + print_success("NO JavaScript errors in background.js") + + # Functional test: Verify extension code execution + print_header("Functional test: Verifying extension code execution...") + + port = check_debugger_port(log_path) + + # Guard: Check if debugger port found + if not port: + print("⚠ Could not find debugger port (but extension installed OK)") + else: + print_success(f"Firefox debugger port: {port}") + + # Guard: Check debugger connectivity + if not test_debugger_connectivity(port): + print("⚠ Remote debugging not accessible (but extension installed OK)") + else: + print_success("Remote debugging protocol accessible") + print_success("Extension code VERIFIED executing") + print() + print("NOTE: Verified by:") + print(" - Extension installed without errors") + print(" - Background page loaded (debugger accessible)") + print(" - No JavaScript errors detected") + + # Show process info + print() + print_success("Process info:") + subprocess.run(["ps", "aux"], stdout=subprocess.PIPE, text=True, check=False) + result = subprocess.run( + ["bash", "-c", "ps aux | grep -E '(firefox|Xvfb)' | grep -v grep | head -3"], + capture_output=True, + text=True + ) + print(result.stdout) + + print("=" * 40) + print("Extension is VERIFIED working!") + print("=" * 40) + + # Cleanup + cleanup(xvfb_pid, webext_pid) + + print("Test completed successfully.") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\nInterrupted by user") + sys.exit(130) + except Exception as e: + print(f"Unexpected error: {e}", file=sys.stderr) + sys.exit(1)