Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

631 lines
23 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Comprehensive Test Runner for Local Deep Research
This script orchestrates all test types and provides different test profiles
for various scenarios (CI/CD, development, full validation).
Usage:
python tests/run_all_tests.py [profile]
Profiles:
fast - Quick health checks and unit tests (< 30s)
standard - Standard development testing (< 5min)
full - Optimized full testing, no redundant runs (< 10min)
comprehensive - Legacy full testing, includes redundant runs (< 15min)
ci - CI/CD optimized tests
unit-only - Unit tests only, no server dependencies (< 2min)
"""
import argparse
import os
import subprocess
from subprocess import CalledProcessError
import sys
import time
from pathlib import Path
from typing import List, Tuple
from functools import cache
@cache
def find_exe(tool_name: str) -> Path:
"""
Finds a particular command-line tool.
Args:
tool_name: The name of the tool.
Returns:
The path to the tool.
"""
which_output = ""
try:
which_result = subprocess.run(
["/usr/bin/which", tool_name], check=True, capture_output=True
)
which_output = which_result.stdout.decode("utf8")
except CalledProcessError as err:
if err.returncode != 1: # pragma: no cover
# If it returns 1, it's probably because the executable does not
# exist. Otherwise, something weird happened.
raise err
pass
if not which_output:
raise OSError(f"Could not find '{tool_name}'. Is it installed?")
tool_path = Path(which_output.rstrip("\n"))
return tool_path
class TestRunner:
def __init__(self):
self.project_root = Path(__file__).parent.parent.absolute()
self.tests_dir = self.project_root / "tests"
self.results: List[Tuple[str, bool, float]] = []
self.server_process = None
def log(self, message: str, level: str = "INFO"):
"""Log a message with timestamp."""
timestamp = time.strftime("%H:%M:%S")
prefix = (
"✅" if level == "SUCCESS" else "❌" if level == "ERROR" else "📋"
)
print(f"[{timestamp}] {prefix} {message}")
def run_command(
self, cmd: List[str], name: str, cwd: str = None, timeout: int = 300
) -> bool:
"""Run a command and track its result."""
self.log(f"Running {name}...")
start_time = time.time()
try:
# Set environment
env = os.environ.copy()
env["PYTHONPATH"] = str(self.project_root)
result = subprocess.run(
cmd,
cwd=cwd or self.project_root,
env=env,
capture_output=True,
text=True,
timeout=timeout,
)
duration = time.time() - start_time
success = result.returncode == 0
if success:
self.log(
f"{name} completed successfully ({duration:.1f}s)",
"SUCCESS",
)
else:
self.log(f"{name} failed ({duration:.1f}s)", "ERROR")
print(f"\n{'=' * 60}")
print(f"DETAILED ERROR OUTPUT FOR: {name}")
print(f"{'=' * 60}")
# Print detailed output for failures with full context
if result.stdout:
print("STDOUT:")
lines = result.stdout.strip().split("\n")
# Show test summary/results
for line in lines[-15:]: # Check last 15 lines for summary
if any(
keyword in line.lower()
for keyword in [
"failed",
"passed",
"error",
"collected",
"warnings",
]
):
print(f" {line}")
# Show specific failed tests with more context
print("\nFAILED TESTS WITH CONTEXT:")
for i, line in enumerate(lines):
if "FAILED" in line and "::" in line:
print(f" {line.strip()}")
# Show next few lines for error details
for j in range(i + 1, min(i + 10, len(lines))):
if lines[j].strip() and not lines[j].startswith(
"===="
):
print(f" {lines[j].strip()}")
if (
"KeyError" in lines[j]
or "Exception" in lines[j]
):
break
# Show full stack traces for exceptions
print("\nFULL STACK TRACES:")
in_traceback = False
traceback_lines = []
for line in lines:
if "Traceback (most recent call last):" in line:
in_traceback = True
traceback_lines = [line]
elif in_traceback:
traceback_lines.append(line)
if line.strip() and not line.startswith(
(" ", "\t", "File ", " ")
):
# End of traceback
for tb_line in traceback_lines:
print(f" {tb_line}")
print()
in_traceback = False
traceback_lines = []
elif any(
error_type in line
for error_type in [
"KeyError:",
"ImportError:",
"ModuleNotFoundError:",
"AttributeError:",
"Exception:",
]
):
print(f" ERROR: {line.strip()}")
# Show import errors with context
print("\nIMPORT ERRORS:")
for i, line in enumerate(lines):
if (
"ImportError" in line
or "ModuleNotFoundError" in line
):
print(f" {line.strip()}")
# Show preceding line for context
if i > 0:
print(f" Context: {lines[i - 1].strip()}")
# Show KeyError details specifically
print("\nKEYERROR DETAILS:")
for i, line in enumerate(lines):
if "KeyError" in line:
print(f" {line.strip()}")
# Show surrounding context
for j in range(
max(0, i - 3), min(len(lines), i + 4)
):
if j != i:
print(
f" [{j - i:+d}] {lines[j].strip()}"
)
# Show pytest short test summary if available
print("\nPYTEST SHORT SUMMARY:")
in_summary = False
for line in lines:
if "short test summary info" in line.lower():
in_summary = True
elif in_summary:
if line.startswith("="):
break
print(f" {line.strip()}")
if result.stderr:
print("\nSTDERR:")
stderr_lines = result.stderr.strip().split("\n")
for line in stderr_lines[-10:]: # Last 10 lines
print(f" {line}")
print(f"{'=' * 60}\n")
self.results.append((name, success, duration))
return success
except subprocess.TimeoutExpired:
duration = time.time() - start_time
self.log(f"{name} timed out after {timeout}s", "ERROR")
self.results.append((name, False, duration))
return False
except Exception as e:
duration = time.time() - start_time
self.log(f"{name} failed with exception: {e}", "ERROR")
self.results.append((name, False, duration))
return False
def check_server_running(self) -> bool:
"""Check if the web server is running."""
try:
import requests
response = requests.get(
"http://127.0.0.1:5000/api/v1/health", timeout=5
)
return response.status_code == 200
except Exception:
return False
def start_server(self) -> bool:
"""Start the web server using pdm run ldr-web."""
if self.check_server_running():
self.log("Server already running at http://127.0.0.1:5000")
return True
self.log("Starting web server with 'pdm run ldr-web'...")
try:
import subprocess
import time
# Start server in background
self.server_process = subprocess.Popen(
["pdm", "run", "ldr-web"],
cwd=self.project_root,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# Wait for server to start (max 45 seconds)
for i in range(45):
time.sleep(1) # allow: unmarked-sleep
if self.check_server_running():
self.log(
f"Server started successfully after {i + 1} seconds"
)
return True
if self.server_process.poll() is not None:
# Process died
stdout, stderr = self.server_process.communicate()
self.log(
f"Server failed to start. Exit code: {self.server_process.returncode}",
"ERROR",
)
if stderr:
print("STDERR:", stderr[-500:])
return False
self.log("Server startup timed out after 45 seconds", "ERROR")
# Try to get output for debugging
if self.server_process.poll() is None:
self.server_process.terminate()
try:
stdout, stderr = self.server_process.communicate(timeout=5)
if stderr:
print("STDERR after timeout:", stderr[-500:])
except Exception:
pass
return False
except Exception as e:
self.log(f"Failed to start server: {e}", "ERROR")
return False
def stop_server(self):
"""Stop the web server if we started it."""
if hasattr(self, "server_process") and self.server_process:
self.log("Stopping web server...")
self.server_process.terminate()
try:
self.server_process.wait(timeout=10)
except subprocess.TimeoutExpired:
self.server_process.kill()
self.server_process.wait()
def run_health_checks(self, start_server_if_needed: bool = True) -> bool:
"""Run health check tests."""
self.log("=== HEALTH CHECKS ===")
# Start server if needed for health checks
if start_server_if_needed and not self.check_server_running():
if not self.start_server():
self.log("⚠️ Could not start server for health checks", "ERROR")
return False
# Try Python version first
python_success = self.run_command(
[sys.executable, "tests/health_check/run_quick_health_check.py"],
"Health Check (Python)",
timeout=120,
)
# Try shell version as backup
if not python_success:
shell_script = (
self.tests_dir / "health_check" / "test_endpoints_health.sh"
)
if shell_script.exists():
self.run_command(
["bash", str(shell_script)],
"Health Check (Shell)",
timeout=120,
)
return python_success
def run_unit_tests(self) -> bool:
"""Run unit and feature tests."""
self.log("=== UNIT & FEATURE TESTS ===")
# Add extra debugging for CI environments
pytest_args = [
find_exe("pdm"),
"run",
"pytest",
"-v", # Verbose output
"tests/test_settings_manager.py",
"tests/test_google_pse.py",
"tests/test_wikipedia_url_security.py",
"tests/test_search_engines_enhanced.py",
"tests/test_utils.py",
"tests/rate_limiting/", # Rate limiting test suite
"tests/retriever_integration/", # LangChain retriever integration tests
"tests/feature_tests/",
"tests/fix_tests/",
"tests/infrastructure_tests/", # Infrastructure and architecture tests
"tests/content_fetcher/", # Content fetcher tests (URL classifier, HTML downloader)
# Critical stability tests - prevent breaking changes
"tests/database/test_encryption_constants.py", # Encryption params MUST NOT change
"tests/database/test_schema_stability.py", # Table names MUST NOT change
"tests/performance/database/test_backwards_compatibility.py", # DB compatibility tests
"--cov=src",
"--cov-report=term-missing",
]
# In CI, add even more debugging
if os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS"):
pytest_args.extend(
[
"--tb=short", # Short traceback format
"--durations=10", # Show 10 slowest tests
]
)
self.log("Running with enhanced CI debugging options")
# Log environment info for debugging
self.log(
f"CI Environment detected: CI={os.environ.get('CI')}, GITHUB_ACTIONS={os.environ.get('GITHUB_ACTIONS')}"
)
self.log(f"Python path: {sys.executable}")
self.log(f"Working directory: {os.getcwd()}")
return self.run_command(
pytest_args,
"Unit & Feature Tests",
timeout=300,
)
def run_integration_tests(self) -> bool:
"""Run integration tests (external dependencies)."""
self.log("=== INTEGRATION TESTS ===")
return self.run_command(
[
sys.executable,
"-m",
"pytest",
"-v",
"tests/searxng/",
"-k",
"not slow",
],
"Integration Tests",
timeout=120,
)
def run_ui_tests(self, start_server_if_needed: bool = True) -> bool:
"""Run UI tests with Puppeteer."""
self.log("=== UI TESTS ===")
if start_server_if_needed and not self.check_server_running():
if not self.start_server():
self.log(
"⚠️ Could not start server - skipping UI tests", "ERROR"
)
return False
elif not self.check_server_running():
self.log(
"⚠️ Warning: Server not running at http://127.0.0.1:5000",
"ERROR",
)
self.log("Please start the server with: pdm run ldr-web")
return False
# Check for Node.js and run UI tests
try:
subprocess.run(
["node", "--version"], capture_output=True, check=True
)
except (subprocess.CalledProcessError, FileNotFoundError):
self.log("Node.js not found - skipping UI tests", "ERROR")
return False
# Check if we're in a headless environment (CI)
cmd = ["node", "run_all_tests.js"]
if os.environ.get("CI") or not os.environ.get("DISPLAY"):
# If no display is available, use xvfb-run if available
try:
subprocess.run(
["which", "xvfb-run"], capture_output=True, check=True
)
cmd = ["xvfb-run", "-a", "-s", "-screen 0 1920x1080x24"] + cmd
self.log(
"Running UI tests with xvfb-run for headless environment"
)
except (subprocess.CalledProcessError, FileNotFoundError):
self.log("Warning: No display available and xvfb-run not found")
# Set headless environment variable for Puppeteer
os.environ["PUPPETEER_HEADLESS"] = "true"
return self.run_command(
cmd,
"UI Tests (Puppeteer)",
cwd=self.tests_dir / "ui_tests",
timeout=300,
)
def run_all_pytest_tests(self) -> bool:
"""Run all pytest tests with coverage (unit, integration, feature tests)."""
self.log("=== ALL PYTEST TESTS ===")
# Run all tests with coverage
cmd = [
sys.executable,
"-m",
"pytest",
"--verbose",
"--color=yes",
"--cov=src",
"--cov-report=term",
"--cov-report=html:coverage_html",
"--cov-config=.coveragerc",
]
timeout = 600 # 10 minutes for all tests
if os.environ.get("CI"):
cmd.extend(
[
"--tb=line", # Show one line per failure for easier debugging
"--timeout=60", # Per-test timeout of 60 seconds
# Removed -x and --maxfail to show all test results
]
)
self.log(
"Running in CI mode with verbose output - showing all test results"
)
return self.run_command(cmd, "All Pytest Tests", timeout=timeout)
def print_summary(self):
"""Print test execution summary."""
self.log("=" * 60)
self.log("TEST EXECUTION SUMMARY")
self.log("=" * 60)
total_tests = len(self.results)
passed = sum(1 for _, success, _ in self.results if success)
failed = total_tests - passed
total_time = sum(duration for _, _, duration in self.results)
for name, success, duration in self.results:
status = "✅ PASS" if success else "❌ FAIL"
self.log(f"{status} {name} ({duration:.1f}s)")
self.log("-" * 60)
self.log(
f"📊 Total: {total_tests} | ✅ Passed: {passed} | ❌ Failed: {failed}"
)
self.log(f"⏱️ Total Time: {total_time:.1f}s")
self.log(f"📈 Success Rate: {(passed / total_tests * 100):.1f}%")
if failed == 0:
self.log("🎉 All tests passed!", "SUCCESS")
else:
self.log(f"⚠️ {failed} test(s) failed", "ERROR")
return failed == 0
def main():
parser = argparse.ArgumentParser(
description="Run Local Deep Research tests"
)
parser.add_argument(
"profile",
nargs="?",
default="standard",
choices=[
"fast",
"standard",
"full",
"comprehensive",
"ci",
"unit-only",
],
help="Test profile to run",
)
parser.add_argument(
"--no-server-start",
action="store_true",
help="Skip automatic server startup - assume server is already running",
)
args = parser.parse_args()
runner = TestRunner()
runner.log(f"🚀 Starting {args.profile.upper()} test profile")
start_time = time.time()
success = True
try:
if args.profile == "fast":
# Quick tests for rapid feedback (< 30s)
success &= runner.run_health_checks(
start_server_if_needed=not args.no_server_start
)
success &= runner.run_unit_tests()
elif args.profile == "standard":
# Standard development testing (< 5min)
success &= runner.run_health_checks(
start_server_if_needed=not args.no_server_start
)
success &= runner.run_unit_tests()
success &= runner.run_ui_tests(
start_server_if_needed=not args.no_server_start
)
elif args.profile == "full":
# Optimized full testing - no duplicate test runs (< 10min)
success &= runner.run_health_checks(
start_server_if_needed=not args.no_server_start
)
# Run all pytest tests (includes unit, integration, feature tests)
success &= runner.run_all_pytest_tests()
# UI tests require special setup so run separately
success &= runner.run_ui_tests(
start_server_if_needed=not args.no_server_start
)
elif args.profile == "comprehensive":
# Legacy comprehensive testing - runs some tests multiple times (< 15min)
# Kept for backwards compatibility
success &= runner.run_health_checks(
start_server_if_needed=not args.no_server_start
)
success &= runner.run_all_pytest_tests()
success &= (
runner.run_integration_tests()
) # Redundant but kept for compatibility
success &= runner.run_ui_tests(
start_server_if_needed=not args.no_server_start
)
elif args.profile == "ci":
# CI/CD optimized tests
# Skip health checks in CI as they require a running server
# which is not available in the CI environment
success &= runner.run_unit_tests()
# Skip UI tests in CI unless explicitly configured
elif args.profile == "unit-only":
# Unit tests only - no server dependencies
success &= runner.run_unit_tests()
finally:
# Always cleanup server if we started it
runner.stop_server()
total_duration = time.time() - start_time
runner.log(f"🏁 Test suite completed in {total_duration:.1f}s")
overall_success = runner.print_summary()
sys.exit(0 if overall_success else 1)
if __name__ == "__main__":
main()