rentgen/scripts/test_verify.py
Jacek Wielemborek 544dfcf2ad feat(verify): use Firefox Remote Debugging Protocol for verification
New approach - verifiable side effect without modifying core logic:

Extension side (background.ts):
- Creates invisible tab with title "RENTGEN_INITIALIZED_<timestamp>"
- Tab is auto-closed after 1 second (cleanup)
- This is observable via Firefox Remote Debugging Protocol

Test side (test_verify.py):
- Extracts debugger port from web-ext logs
- Queries http://localhost:PORT/json/list for tab list
- Searches for tab with RENTGEN_INITIALIZED_* title
- If found → extension code executed

This proves:
- background.ts executed
- browser.tabs.create() succeeded
- Extension has working browser API access

No WebDriver/Selenium needed - uses Firefox RDP directly via urllib

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:02:17 +02:00

279 lines
7.9 KiB
Python
Executable File

#!/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_extension_via_rdp(port: str) -> tuple[bool, str]:
"""Check if extension created marker tab via Firefox Remote Debugging Protocol.
Returns (success, message)."""
try:
import json
import urllib.request
import urllib.error
# Query Firefox Remote Debugging Protocol for list of tabs
url = f"http://127.0.0.1:{port}/json/list"
try:
with urllib.request.urlopen(url, timeout=5) as response:
tabs = json.loads(response.read().decode())
except (urllib.error.URLError, urllib.error.HTTPError) as e:
return False, f"Failed to connect to Remote Debugging Protocol: {e}"
# Look for tab with title starting with RENTGEN_INITIALIZED_
for tab in tabs:
title = tab.get('title', '')
if title.startswith('RENTGEN_INITIALIZED_'):
timestamp = title.replace('RENTGEN_INITIALIZED_', '')
return True, f"Extension created marker tab at timestamp {timestamp}"
return False, "No marker tab found (extension may not have executed)"
except Exception as e:
return False, f"RDP check failed: {e}"
def extract_debugger_port(log_path: Path) -> str | None:
"""Extract debugger port from web-ext logs."""
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 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 via Remote Debugging Protocol
print_header("Functional test: Verifying extension code execution...")
# Extract debugger port
port = extract_debugger_port(log_path)
if not port:
print_error("Could not find debugger port in logs")
print_error("Cannot connect to Firefox Remote Debugging Protocol")
cleanup(xvfb_pid, webext_pid)
return 1
print_success(f"Firefox debugger port: {port}")
# Give extension a moment to create the marker tab
time.sleep(2)
# Check via Remote Debugging Protocol
execution_verified, message = check_extension_via_rdp(port)
# Guard: Check if we found proof of execution
if not execution_verified:
print_error("Could not verify extension code execution")
print_error(f"Reason: {message}")
print_error("Extension installed but NO PROOF of code execution")
cleanup(xvfb_pid, webext_pid)
return 1
print_success("Extension code VERIFIED executing!")
print()
print(f"Proof: {message}")
print()
print("This proves:")
print(" - background.ts executed")
print(" - browser.tabs.create() succeeded")
print(" - Extension has working browser API access")
# 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)