826 lines
36 KiB
Python
826 lines
36 KiB
Python
import asyncio
|
|
import logging
|
|
import os
|
|
import argparse
|
|
import threading
|
|
import time
|
|
import platform
|
|
from datetime import datetime
|
|
from computer import Computer
|
|
from reportportal_client import RPClient
|
|
from reportportal_client.helpers import timestamp
|
|
|
|
from utils import scan_test_files
|
|
from test_runner import run_single_test_with_timeout
|
|
from individual_migration_runner import run_individual_migration_test, run_all_migration_tests, MIGRATION_TEST_CASES
|
|
from reliability_runner import run_reliability_test, run_reliability_tests
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.StreamHandler()
|
|
]
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Platform detection
|
|
IS_WINDOWS = platform.system() == "Windows"
|
|
IS_LINUX = platform.system() == "Linux"
|
|
IS_MACOS = platform.system() == "Darwin"
|
|
|
|
def get_computer_config():
|
|
"""Get computer configuration based on platform"""
|
|
if IS_WINDOWS:
|
|
return {
|
|
"os_type": "windows"
|
|
}
|
|
elif IS_LINUX:
|
|
return {
|
|
"os_type": "linux"
|
|
}
|
|
elif IS_MACOS:
|
|
return {
|
|
"os_type": "macos"
|
|
}
|
|
else:
|
|
# Default fallback
|
|
logger.warning(f"Unknown platform {platform.system()}, using Linux config as fallback")
|
|
return {
|
|
"os_type": "linux"
|
|
}
|
|
|
|
def get_default_jan_path():
|
|
"""Get default Jan app path based on OS"""
|
|
if IS_WINDOWS:
|
|
# Try multiple common locations on Windows
|
|
possible_paths = [
|
|
os.path.expanduser(r"~\AppData\Local\Programs\jan\Jan.exe"),
|
|
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'jan', 'Jan.exe'),
|
|
os.path.join(os.environ.get('APPDATA', ''), 'jan', 'Jan.exe'),
|
|
r"C:\Program Files\jan\Jan.exe",
|
|
r"C:\Program Files (x86)\jan\Jan.exe"
|
|
]
|
|
|
|
# Return first existing path, or first option as default
|
|
for path in possible_paths:
|
|
if os.path.exists(path):
|
|
return path
|
|
|
|
# If none exist, return the most likely default
|
|
return possible_paths[0]
|
|
|
|
elif IS_LINUX:
|
|
# Linux possible locations
|
|
possible_paths = [
|
|
"/usr/bin/Jan",
|
|
"/usr/local/bin/Jan",
|
|
os.path.expanduser("~/Applications/Jan/Jan"),
|
|
"/opt/Jan/Jan"
|
|
]
|
|
|
|
# Return first existing path, or first option as default
|
|
for path in possible_paths:
|
|
if os.path.exists(path):
|
|
return path
|
|
|
|
# Default to nightly build path
|
|
return "/usr/bin/Jan"
|
|
|
|
elif IS_MACOS:
|
|
# macOS defaults
|
|
possible_paths = [
|
|
"/Applications/Jan.app/Contents/MacOS/Jan",
|
|
os.path.expanduser("~/Applications/Jan.app/Contents/MacOS/Jan")
|
|
]
|
|
|
|
for path in possible_paths:
|
|
if os.path.exists(path):
|
|
return path
|
|
|
|
return possible_paths[0]
|
|
|
|
else:
|
|
# Unknown platform
|
|
return "jan"
|
|
|
|
def start_computer_server():
|
|
"""Start computer server in background thread"""
|
|
try:
|
|
logger.info("Starting computer server in background...")
|
|
|
|
# Import computer_server module
|
|
import computer_server
|
|
import sys
|
|
|
|
# Start server in a separate thread
|
|
def run_server():
|
|
try:
|
|
# Save original sys.argv to avoid argument conflicts
|
|
original_argv = sys.argv.copy()
|
|
|
|
# Override sys.argv for computer_server to use default args
|
|
sys.argv = ['computer_server'] # Reset to minimal args
|
|
|
|
# Use the proper entry point
|
|
logger.info("Calling computer_server.run_cli()...")
|
|
computer_server.run_cli()
|
|
logger.info("Computer server.run_cli() completed")
|
|
except KeyboardInterrupt:
|
|
logger.info("Computer server interrupted")
|
|
except Exception as e:
|
|
logger.error(f"Computer server error: {e}")
|
|
import traceback
|
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
finally:
|
|
# Restore original sys.argv
|
|
try:
|
|
sys.argv = original_argv
|
|
except:
|
|
pass
|
|
|
|
server_thread = threading.Thread(target=run_server, daemon=True)
|
|
server_thread.start()
|
|
|
|
logger.info("Computer server thread started")
|
|
|
|
# Give server more time to start up
|
|
time.sleep(5)
|
|
|
|
# Check if thread is still alive (server is running)
|
|
if server_thread.is_alive():
|
|
logger.info("Computer server is running successfully")
|
|
return server_thread
|
|
else:
|
|
logger.error("Computer server thread died unexpectedly")
|
|
return None
|
|
|
|
except ImportError as e:
|
|
logger.error(f"Cannot import computer_server module: {e}")
|
|
logger.error("Please install computer_server package")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Error starting computer server: {e}")
|
|
import traceback
|
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
return None
|
|
|
|
def parse_arguments():
|
|
"""Parse command line arguments"""
|
|
parser = argparse.ArgumentParser(
|
|
description="E2E Test Runner with ReportPortal integration",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
# Run locally without ReportPortal
|
|
python main.py
|
|
|
|
# Run with ReportPortal integration
|
|
python main.py --enable-reportportal --rp-token YOUR_TOKEN
|
|
|
|
# Run with custom Jan app path
|
|
python main.py --jan-app-path "C:/Custom/Path/Jan.exe"
|
|
|
|
# Run with different model
|
|
python main.py --model-name "gpt-4" --model-base-url "https://api.openai.com/v1"
|
|
|
|
# Reliability testing - development phase (5 runs)
|
|
python main.py --enable-reliability-test --reliability-phase development
|
|
|
|
# Reliability testing - deployment phase (20 runs)
|
|
python main.py --enable-reliability-test --reliability-phase deployment
|
|
|
|
# Reliability testing - custom number of runs
|
|
python main.py --enable-reliability-test --reliability-runs 10
|
|
|
|
# Reliability testing - specific test file
|
|
python main.py --enable-reliability-test --reliability-test-path "tests/base/default-jan-assistant.txt"
|
|
|
|
# Using environment variables
|
|
ENABLE_REPORTPORTAL=true RP_TOKEN=xxx MODEL_NAME=gpt-4 python main.py
|
|
ENABLE_RELIABILITY_TEST=true RELIABILITY_PHASE=deployment python main.py
|
|
"""
|
|
)
|
|
|
|
# Get default Jan path
|
|
default_jan_path = get_default_jan_path()
|
|
|
|
# Computer server arguments
|
|
server_group = parser.add_argument_group('Computer Server Configuration')
|
|
server_group.add_argument(
|
|
'--skip-server-start',
|
|
action='store_true',
|
|
default=os.getenv('SKIP_SERVER_START', 'false').lower() == 'true',
|
|
help='Skip automatic computer server startup (env: SKIP_SERVER_START, default: false)'
|
|
)
|
|
|
|
# ReportPortal arguments
|
|
rp_group = parser.add_argument_group('ReportPortal Configuration')
|
|
rp_group.add_argument(
|
|
'--enable-reportportal',
|
|
action='store_true',
|
|
default=os.getenv('ENABLE_REPORTPORTAL', 'false').lower() == 'true',
|
|
help='Enable ReportPortal integration (env: ENABLE_REPORTPORTAL, default: false)'
|
|
)
|
|
rp_group.add_argument(
|
|
'--rp-endpoint',
|
|
default=os.getenv('RP_ENDPOINT', 'https://reportportal.menlo.ai'),
|
|
help='ReportPortal endpoint URL (env: RP_ENDPOINT, default: %(default)s)'
|
|
)
|
|
rp_group.add_argument(
|
|
'--rp-project',
|
|
default=os.getenv('RP_PROJECT', 'default_personal'),
|
|
help='ReportPortal project name (env: RP_PROJECT, default: %(default)s)'
|
|
)
|
|
rp_group.add_argument(
|
|
'--rp-token',
|
|
default=os.getenv('RP_TOKEN'),
|
|
help='ReportPortal API token (env: RP_TOKEN, required when --enable-reportportal is used)'
|
|
)
|
|
rp_group.add_argument(
|
|
'--launch-name',
|
|
default=os.getenv('LAUNCH_NAME'),
|
|
help='Custom launch name for ReportPortal (env: LAUNCH_NAME, default: auto-generated with timestamp)'
|
|
)
|
|
|
|
# Jan app arguments
|
|
jan_group = parser.add_argument_group('Jan Application Configuration')
|
|
jan_group.add_argument(
|
|
'--jan-app-path',
|
|
default=os.getenv('JAN_APP_PATH', default_jan_path),
|
|
help=f'Path to Jan application executable (env: JAN_APP_PATH, default: auto-detected or {default_jan_path})'
|
|
)
|
|
jan_group.add_argument(
|
|
'--jan-process-name',
|
|
default=os.getenv('JAN_PROCESS_NAME', 'Jan.exe' if IS_WINDOWS else ('Jan' if IS_MACOS else 'Jan-nightly')),
|
|
help='Jan process name for monitoring (env: JAN_PROCESS_NAME, default: platform-specific)'
|
|
)
|
|
|
|
# Model/Agent arguments
|
|
model_group = parser.add_argument_group('Model Configuration')
|
|
model_group.add_argument(
|
|
'--model-loop',
|
|
default=os.getenv('MODEL_LOOP', 'uitars'),
|
|
help='Agent loop type (env: MODEL_LOOP, default: %(default)s)'
|
|
)
|
|
model_group.add_argument(
|
|
'--model-provider',
|
|
default=os.getenv('MODEL_PROVIDER', 'oaicompat'),
|
|
help='Model provider (env: MODEL_PROVIDER, default: %(default)s)'
|
|
)
|
|
model_group.add_argument(
|
|
'--model-name',
|
|
default=os.getenv('MODEL_NAME', 'ByteDance-Seed/UI-TARS-1.5-7B'),
|
|
help='Model name (env: MODEL_NAME, default: %(default)s)'
|
|
)
|
|
model_group.add_argument(
|
|
'--model-base-url',
|
|
default=os.getenv('MODEL_BASE_URL', 'http://10.200.108.58:1234/v1'),
|
|
help='Model base URL (env: MODEL_BASE_URL, default: %(default)s)'
|
|
)
|
|
|
|
# Test execution arguments
|
|
test_group = parser.add_argument_group('Test Execution Configuration')
|
|
test_group.add_argument(
|
|
'--max-turns',
|
|
type=int,
|
|
default=int(os.getenv('MAX_TURNS', '30')),
|
|
help='Maximum number of turns per test (env: MAX_TURNS, default: %(default)s)'
|
|
)
|
|
test_group.add_argument(
|
|
'--tests-dir',
|
|
default=os.getenv('TESTS_DIR', 'tests/base'),
|
|
help='Directory containing test files for current version testing (env: TESTS_DIR, default: %(default)s for base tests, tests/migration for migration tests)'
|
|
)
|
|
test_group.add_argument(
|
|
'--delay-between-tests',
|
|
type=int,
|
|
default=int(os.getenv('DELAY_BETWEEN_TESTS', '3')),
|
|
help='Delay in seconds between tests (env: DELAY_BETWEEN_TESTS, default: %(default)s)'
|
|
)
|
|
|
|
# Migration testing arguments
|
|
migration_group = parser.add_argument_group('Migration Testing Configuration')
|
|
migration_group.add_argument(
|
|
'--enable-migration-test',
|
|
action='store_true',
|
|
default=os.getenv('ENABLE_MIGRATION_TEST', 'false').lower() == 'true',
|
|
help='Enable migration testing mode (env: ENABLE_MIGRATION_TEST, default: false)'
|
|
)
|
|
migration_group.add_argument(
|
|
'--old-version',
|
|
default=os.getenv('OLD_VERSION'),
|
|
help='Path to old version installer for migration testing (env: OLD_VERSION)'
|
|
)
|
|
migration_group.add_argument(
|
|
'--new-version',
|
|
default=os.getenv('NEW_VERSION'),
|
|
help='Path to new version installer for migration testing (env: NEW_VERSION)'
|
|
)
|
|
migration_group.add_argument(
|
|
'--migration-test-case',
|
|
default=os.getenv('MIGRATION_TEST_CASE'),
|
|
help='Specific migration test case(s) to run. Can be a single case or comma-separated list (e.g., "assistants" or "models,threads"). Available cases: appearance, threads, models, assistants, assistant-chat, assistants-complete, mcp-servers, local-api, proxy-settings, thread-conversations. Note: "assistants-complete" is only supported in batch mode. If not specified, runs all test cases. Use --list-migration-tests to see all available cases. (env: MIGRATION_TEST_CASE)'
|
|
)
|
|
migration_group.add_argument(
|
|
'--migration-batch-mode',
|
|
action='store_true',
|
|
default=os.getenv('MIGRATION_BATCH_MODE', 'false').lower() == 'true',
|
|
help='Use batch migration mode: setup all → upgrade → verify all (env: MIGRATION_BATCH_MODE, default: false - uses individual mode)'
|
|
)
|
|
migration_group.add_argument(
|
|
'--list-migration-tests',
|
|
action='store_true',
|
|
help='List available migration test cases and exit'
|
|
)
|
|
|
|
# Reliability testing arguments
|
|
reliability_group = parser.add_argument_group('Reliability Testing Configuration')
|
|
reliability_group.add_argument(
|
|
'--enable-reliability-test',
|
|
action='store_true',
|
|
default=os.getenv('ENABLE_RELIABILITY_TEST', 'false').lower() == 'true',
|
|
help='Enable reliability testing mode (env: ENABLE_RELIABILITY_TEST, default: false)'
|
|
)
|
|
reliability_group.add_argument(
|
|
'--reliability-phase',
|
|
choices=['development', 'deployment'],
|
|
default=os.getenv('RELIABILITY_PHASE', 'development'),
|
|
help='Reliability testing phase: development (5 runs) or deployment (20 runs) (env: RELIABILITY_PHASE, default: development)'
|
|
)
|
|
reliability_group.add_argument(
|
|
'--reliability-runs',
|
|
type=int,
|
|
default=int(os.getenv('RELIABILITY_RUNS', '0')),
|
|
help='Custom number of runs for reliability testing (overrides phase setting) (env: RELIABILITY_RUNS, default: 0)'
|
|
)
|
|
reliability_group.add_argument(
|
|
'--reliability-test-path',
|
|
default=os.getenv('RELIABILITY_TEST_PATH'),
|
|
help='Specific test file path for reliability testing (env: RELIABILITY_TEST_PATH, if not specified, uses --tests-dir)'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Handle list migration tests
|
|
if args.list_migration_tests:
|
|
print("Available migration test cases:")
|
|
print("=" * 50)
|
|
for key, test_case in MIGRATION_TEST_CASES.items():
|
|
print(f" {key}:")
|
|
print(f" Name: {test_case['name']}")
|
|
print(f" Description: {test_case['description']}")
|
|
print()
|
|
exit(0)
|
|
|
|
# Validate ReportPortal token if ReportPortal is enabled
|
|
if args.enable_reportportal and not args.rp_token:
|
|
parser.error("--rp-token (or RP_TOKEN env var) is required when --enable-reportportal is used")
|
|
|
|
# Validate migration test arguments
|
|
if args.enable_migration_test:
|
|
if not args.old_version:
|
|
parser.error("--old-version (or OLD_VERSION env var) is required when --enable-migration-test is used")
|
|
if not args.new_version:
|
|
parser.error("--new-version (or NEW_VERSION env var) is required when --enable-migration-test is used")
|
|
if not os.path.exists(args.old_version):
|
|
parser.error(f"Old version installer not found: {args.old_version}")
|
|
if not os.path.exists(args.new_version):
|
|
parser.error(f"New version installer not found: {args.new_version}")
|
|
|
|
# Validate specific test case if provided
|
|
if args.migration_test_case and args.migration_test_case not in MIGRATION_TEST_CASES:
|
|
parser.error(f"Unknown migration test case: {args.migration_test_case}. Use --list-migration-tests to see available test cases.")
|
|
|
|
return args
|
|
|
|
async def main():
|
|
"""
|
|
Main function to scan and run all test files with optional ReportPortal integration
|
|
"""
|
|
# Parse command line arguments
|
|
args = parse_arguments()
|
|
|
|
# Initialize final exit code
|
|
final_exit_code = 0
|
|
|
|
# Start computer server if not skipped
|
|
server_thread = None
|
|
if not args.skip_server_start:
|
|
server_thread = start_computer_server()
|
|
if server_thread is None:
|
|
logger.error("Failed to start computer server. Exiting...")
|
|
return
|
|
else:
|
|
logger.info("Skipping computer server startup (assuming it's already running)")
|
|
|
|
try:
|
|
# Build agent config from arguments
|
|
agent_config = {
|
|
"loop": args.model_loop,
|
|
"model_provider": args.model_provider,
|
|
"model_name": args.model_name,
|
|
"model_base_url": args.model_base_url
|
|
}
|
|
|
|
# Log configuration
|
|
logger.info("=== Configuration ===")
|
|
logger.info(f"Testing Mode: {'MIGRATION (old → new version)' if args.enable_migration_test else 'BASE (current version)'}")
|
|
logger.info(f"Computer server: {'STARTED' if server_thread else 'EXTERNAL'}")
|
|
logger.info(f"Tests directory: {args.tests_dir}")
|
|
logger.info(f"Max turns per test: {args.max_turns}")
|
|
logger.info(f"Delay between tests: {args.delay_between_tests}s")
|
|
logger.info(f"Jan app path: {args.jan_app_path}")
|
|
logger.info(f"Jan app exists: {os.path.exists(args.jan_app_path)}")
|
|
logger.info(f"Jan process name: {args.jan_process_name}")
|
|
logger.info(f"Model: {args.model_name}")
|
|
logger.info(f"Model URL: {args.model_base_url}")
|
|
logger.info(f"Model provider: {args.model_provider}")
|
|
logger.info(f"ReportPortal integration: {'ENABLED' if args.enable_reportportal else 'DISABLED'}")
|
|
if args.enable_reportportal:
|
|
logger.info(f"ReportPortal endpoint: {args.rp_endpoint}")
|
|
logger.info(f"ReportPortal project: {args.rp_project}")
|
|
logger.info(f"ReportPortal token: {'SET' if args.rp_token else 'NOT SET'}")
|
|
logger.info(f"Launch name: {args.launch_name if args.launch_name else 'AUTO-GENERATED'}")
|
|
logger.info(f"Migration testing: {'ENABLED' if args.enable_migration_test else 'DISABLED'}")
|
|
if args.enable_migration_test:
|
|
logger.info(f"Old version installer: {args.old_version}")
|
|
logger.info(f"New version installer: {args.new_version}")
|
|
logger.info(f"Reliability testing: {'ENABLED' if args.enable_reliability_test else 'DISABLED'}")
|
|
if args.enable_reliability_test:
|
|
logger.info(f"Reliability phase: {args.reliability_phase}")
|
|
if args.reliability_runs > 0:
|
|
logger.info(f"Custom runs: {args.reliability_runs}")
|
|
else:
|
|
logger.info(f"Phase runs: {5 if args.reliability_phase == 'development' else 20}")
|
|
if args.reliability_test_path:
|
|
logger.info(f"Specific test path: {args.reliability_test_path}")
|
|
else:
|
|
logger.info(f"Tests directory: {args.tests_dir}")
|
|
logger.info("======================")
|
|
|
|
# Initialize ReportPortal client only if enabled
|
|
rp_client = None
|
|
launch_id = None
|
|
|
|
if args.enable_reportportal:
|
|
try:
|
|
rp_client = RPClient(
|
|
endpoint=args.rp_endpoint,
|
|
project=args.rp_project,
|
|
api_key=args.rp_token
|
|
)
|
|
|
|
# Start ReportPortal launch
|
|
current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
# Use custom launch name if provided, otherwise generate default
|
|
if args.launch_name:
|
|
launch_name = args.launch_name
|
|
logger.info(f"Using custom launch name: {launch_name}")
|
|
else:
|
|
launch_name = f"E2E Test Run - {current_time}"
|
|
logger.info(f"Using auto-generated launch name: {launch_name}")
|
|
|
|
launch_id = rp_client.start_launch(
|
|
name=launch_name,
|
|
start_time=timestamp(),
|
|
description=f"Automated E2E test run with {len(test_files)} test cases\n"
|
|
f"Model: {args.model_name}\n"
|
|
f"Max turns: {args.max_turns}"
|
|
)
|
|
|
|
logger.info(f"Started ReportPortal launch: {launch_name}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize ReportPortal: {e}")
|
|
logger.warning("Continuing without ReportPortal integration...")
|
|
rp_client = None
|
|
launch_id = None
|
|
else:
|
|
logger.info("Running in local development mode - results will not be uploaded to ReportPortal")
|
|
|
|
# Start computer environment
|
|
logger.info("Initializing computer environment...")
|
|
|
|
# Get platform-specific computer configuration
|
|
computer_config = get_computer_config()
|
|
logger.info(f"Using computer config: {computer_config}")
|
|
|
|
computer = Computer(
|
|
os_type=computer_config["os_type"],
|
|
use_host_computer_server=True
|
|
)
|
|
await computer.run()
|
|
logger.info("Computer environment ready")
|
|
|
|
# Check if reliability testing is enabled
|
|
if args.enable_reliability_test:
|
|
logger.info("=" * 60)
|
|
logger.info("RELIABILITY TESTING MODE ENABLED")
|
|
logger.info("=" * 60)
|
|
logger.info(f"Phase: {args.reliability_phase}")
|
|
if args.reliability_runs > 0:
|
|
logger.info(f"Custom runs: {args.reliability_runs}")
|
|
else:
|
|
logger.info(f"Phase runs: {5 if args.reliability_phase == 'development' else 20}")
|
|
|
|
# Determine test paths for reliability testing
|
|
if args.reliability_test_path:
|
|
# Use specific test path
|
|
if not os.path.exists(args.reliability_test_path):
|
|
logger.error(f"Reliability test file not found: {args.reliability_test_path}")
|
|
final_exit_code = 1
|
|
return final_exit_code
|
|
test_paths = [args.reliability_test_path]
|
|
logger.info(f"Running reliability test on specific file: {args.reliability_test_path}")
|
|
else:
|
|
# Use tests directory
|
|
test_files = scan_test_files(args.tests_dir)
|
|
if not test_files:
|
|
logger.warning(f"No test files found in directory: {args.tests_dir}")
|
|
return
|
|
test_paths = [test_data['path'] for test_data in test_files]
|
|
logger.info(f"Running reliability tests on {len(test_paths)} test files from: {args.tests_dir}")
|
|
|
|
# Run reliability tests
|
|
reliability_results = await run_reliability_tests(
|
|
computer=computer,
|
|
test_paths=test_paths,
|
|
rp_client=rp_client,
|
|
launch_id=launch_id,
|
|
max_turns=args.max_turns,
|
|
jan_app_path=args.jan_app_path,
|
|
jan_process_name=args.jan_process_name,
|
|
agent_config=agent_config,
|
|
enable_reportportal=args.enable_reportportal,
|
|
phase=args.reliability_phase,
|
|
runs=args.reliability_runs if args.reliability_runs > 0 else None
|
|
)
|
|
|
|
# Handle reliability test results
|
|
if reliability_results and reliability_results.get("overall_success", False):
|
|
logger.info(f"[SUCCESS] Reliability testing completed successfully!")
|
|
final_exit_code = 0
|
|
else:
|
|
logger.error(f"[FAILED] Reliability testing failed!")
|
|
if reliability_results and reliability_results.get("error_message"):
|
|
logger.error(f"Error: {reliability_results['error_message']}")
|
|
final_exit_code = 1
|
|
|
|
# Skip regular test execution in reliability mode
|
|
logger.info("Reliability testing completed. Skipping regular test execution.")
|
|
|
|
# Check if migration testing is enabled
|
|
elif args.enable_migration_test:
|
|
logger.info("=" * 60)
|
|
logger.info("MIGRATION TESTING MODE ENABLED")
|
|
logger.info("=" * 60)
|
|
logger.info(f"Old version installer: {args.old_version}")
|
|
logger.info(f"New version installer: {args.new_version}")
|
|
logger.info(f"Migration mode: {'BATCH (all setups → upgrade → all verifies)' if args.migration_batch_mode else 'INDIVIDUAL (setup → upgrade → verify per test)'}")
|
|
|
|
if args.migration_test_case:
|
|
# Parse comma-separated test cases
|
|
test_cases = [case.strip() for case in args.migration_test_case.split(',')]
|
|
logger.info(f"Running specific test case(s): {', '.join(test_cases)}")
|
|
|
|
# Validate all test cases exist
|
|
for test_case in test_cases:
|
|
if test_case not in MIGRATION_TEST_CASES:
|
|
logger.error(f"Unknown test case: {test_case}")
|
|
logger.error(f"Available test cases: {', '.join(MIGRATION_TEST_CASES.keys())}")
|
|
final_exit_code = 1
|
|
return final_exit_code
|
|
|
|
if args.migration_batch_mode:
|
|
# Import and run batch migration with specified test cases
|
|
from batch_migration_runner import run_batch_migration_test
|
|
|
|
migration_results = await run_batch_migration_test(
|
|
computer=computer,
|
|
old_version_path=args.old_version,
|
|
new_version_path=args.new_version,
|
|
rp_client=rp_client,
|
|
launch_id=launch_id,
|
|
max_turns=args.max_turns,
|
|
agent_config=agent_config,
|
|
enable_reportportal=args.enable_reportportal,
|
|
test_cases=test_cases # Multiple test cases in batch mode
|
|
)
|
|
|
|
# Handle batch test result
|
|
if migration_results and migration_results.get("overall_success", False):
|
|
logger.info(f"[SUCCESS] Batch migration test '{', '.join(test_cases)}' completed successfully!")
|
|
final_exit_code = 0
|
|
else:
|
|
logger.error(f"[FAILED] Batch migration test '{', '.join(test_cases)}' failed!")
|
|
if migration_results and migration_results.get("error_message"):
|
|
logger.error(f"Error: {migration_results['error_message']}")
|
|
final_exit_code = 1
|
|
else:
|
|
# Run individual migration tests for each specified test case
|
|
all_individual_results = []
|
|
overall_individual_success = True
|
|
|
|
for test_case in test_cases:
|
|
logger.info(f"Running individual migration test for: {test_case}")
|
|
migration_result = await run_individual_migration_test(
|
|
computer=computer,
|
|
test_case_key=test_case,
|
|
old_version_path=args.old_version,
|
|
new_version_path=args.new_version,
|
|
rp_client=rp_client,
|
|
launch_id=launch_id,
|
|
max_turns=args.max_turns,
|
|
agent_config=agent_config,
|
|
enable_reportportal=args.enable_reportportal
|
|
)
|
|
|
|
all_individual_results.append(migration_result)
|
|
if not (migration_result and migration_result.get("overall_success", False)):
|
|
overall_individual_success = False
|
|
|
|
# Handle individual test results
|
|
if overall_individual_success:
|
|
logger.info(f"[SUCCESS] All individual migration tests '{', '.join(test_cases)}' completed successfully!")
|
|
final_exit_code = 0
|
|
else:
|
|
logger.error(f"[FAILED] One or more individual migration tests '{', '.join(test_cases)}' failed!")
|
|
for i, result in enumerate(all_individual_results):
|
|
if result and result.get("error_message"):
|
|
logger.error(f"Error in {test_cases[i]}: {result['error_message']}")
|
|
final_exit_code = 1
|
|
else:
|
|
logger.info("Running all migration test cases")
|
|
|
|
if args.migration_batch_mode:
|
|
# Import and run batch migration runner
|
|
from batch_migration_runner import run_batch_migration_test
|
|
|
|
migration_results = await run_batch_migration_test(
|
|
computer=computer,
|
|
old_version_path=args.old_version,
|
|
new_version_path=args.new_version,
|
|
rp_client=rp_client,
|
|
launch_id=launch_id,
|
|
max_turns=args.max_turns,
|
|
agent_config=agent_config,
|
|
enable_reportportal=args.enable_reportportal
|
|
)
|
|
else:
|
|
# Run all migration tests individually
|
|
migration_results = await run_all_migration_tests(
|
|
computer=computer,
|
|
old_version_path=args.old_version,
|
|
new_version_path=args.new_version,
|
|
rp_client=rp_client,
|
|
launch_id=launch_id,
|
|
max_turns=args.max_turns,
|
|
agent_config=agent_config,
|
|
enable_reportportal=args.enable_reportportal
|
|
)
|
|
|
|
# Handle overall results
|
|
if migration_results and migration_results.get("overall_success", False):
|
|
logger.info("[SUCCESS] All migration tests completed successfully!")
|
|
final_exit_code = 0
|
|
else:
|
|
logger.error("[FAILED] One or more migration tests failed!")
|
|
if migration_results:
|
|
logger.error(f"Failed {migration_results.get('failed', 0)} out of {migration_results.get('total_tests', 0)} tests")
|
|
final_exit_code = 1
|
|
|
|
# Skip regular test execution in migration mode
|
|
logger.info("Migration testing completed. Skipping regular test execution.")
|
|
|
|
else:
|
|
# Regular test execution mode (base/current version testing)
|
|
logger.info("Running base test execution mode (current version testing)")
|
|
|
|
# Use base tests directory if default tests_dir is being used
|
|
base_tests_dir = args.tests_dir
|
|
if args.tests_dir == 'tests/base' and not os.path.exists(args.tests_dir):
|
|
# Fallback to old structure if base directory doesn't exist
|
|
if os.path.exists('tests'):
|
|
base_tests_dir = 'tests'
|
|
logger.warning("tests/base directory not found, using 'tests' as fallback")
|
|
|
|
logger.info(f"Using test directory: {base_tests_dir}")
|
|
|
|
# Scan all test files
|
|
test_files = scan_test_files(base_tests_dir)
|
|
|
|
if not test_files:
|
|
logger.warning(f"No test files found in directory: {base_tests_dir}")
|
|
return
|
|
|
|
logger.info(f"Found {len(test_files)} test files")
|
|
|
|
# Track test results for final exit code
|
|
test_results = {"passed": 0, "failed": 0, "total": len(test_files)}
|
|
|
|
# Run each test sequentially with turn monitoring
|
|
for i, test_data in enumerate(test_files, 1):
|
|
logger.info(f"Running test {i}/{len(test_files)}: {test_data['path']}")
|
|
|
|
try:
|
|
# Pass all configs to test runner
|
|
test_result = await run_single_test_with_timeout(
|
|
computer=computer,
|
|
test_data=test_data,
|
|
rp_client=rp_client, # Can be None
|
|
launch_id=launch_id, # Can be None
|
|
max_turns=args.max_turns,
|
|
jan_app_path=args.jan_app_path,
|
|
jan_process_name=args.jan_process_name,
|
|
agent_config=agent_config,
|
|
enable_reportportal=args.enable_reportportal
|
|
)
|
|
|
|
# Track test result - properly handle different return formats
|
|
test_passed = False
|
|
|
|
if test_result:
|
|
# Check different possible return formats
|
|
if isinstance(test_result, dict):
|
|
# Dictionary format: check 'success' key
|
|
test_passed = test_result.get('success', False)
|
|
elif isinstance(test_result, bool):
|
|
# Boolean format: direct boolean value
|
|
test_passed = test_result
|
|
elif hasattr(test_result, 'success'):
|
|
# Object format: check success attribute
|
|
test_passed = getattr(test_result, 'success', False)
|
|
else:
|
|
# Any truthy value is considered success
|
|
test_passed = bool(test_result)
|
|
else:
|
|
test_passed = False
|
|
|
|
# Update counters and log result
|
|
if test_passed:
|
|
test_results["passed"] += 1
|
|
logger.info(f"[SUCCESS] Test {i} PASSED: {test_data['path']}")
|
|
else:
|
|
test_results["failed"] += 1
|
|
logger.error(f"[FAILED] Test {i} FAILED: {test_data['path']}")
|
|
|
|
# Debug log for troubleshooting
|
|
logger.info(f"[INFO] Debug - Test result: type={type(test_result)}, value={test_result}, success_field={test_result.get('success', 'N/A') if isinstance(test_result, dict) else 'N/A'}, final_passed={test_passed}")
|
|
|
|
except Exception as e:
|
|
test_results["failed"] += 1
|
|
logger.error(f"[FAILED] Test {i} FAILED with exception: {test_data['path']} - {e}")
|
|
|
|
# Add delay between tests
|
|
if i < len(test_files):
|
|
logger.info(f"Waiting {args.delay_between_tests} seconds before next test...")
|
|
await asyncio.sleep(args.delay_between_tests)
|
|
|
|
# Log final test results summary
|
|
logger.info("=" * 50)
|
|
logger.info("TEST EXECUTION SUMMARY")
|
|
logger.info("=" * 50)
|
|
logger.info(f"Total tests: {test_results['total']}")
|
|
logger.info(f"Passed: {test_results['passed']}")
|
|
logger.info(f"Failed: {test_results['failed']}")
|
|
logger.info(f"Success rate: {(test_results['passed']/test_results['total']*100):.1f}%")
|
|
logger.info("=" * 50)
|
|
|
|
if test_results["failed"] > 0:
|
|
logger.error(f"[FAILED] Test execution completed with {test_results['failed']} failures!")
|
|
final_exit_code = 1
|
|
else:
|
|
logger.info("[SUCCESS] All tests completed successfully!")
|
|
final_exit_code = 0
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("Test execution interrupted by user")
|
|
final_exit_code = 1
|
|
except Exception as e:
|
|
logger.error(f"Error in main execution: {e}")
|
|
final_exit_code = 1
|
|
finally:
|
|
# Finish ReportPortal launch only if it was started
|
|
if args.enable_reportportal and rp_client and launch_id:
|
|
try:
|
|
rp_client.finish_launch(
|
|
launch_id=launch_id,
|
|
end_time=timestamp()
|
|
)
|
|
rp_client.session.close()
|
|
logger.info("ReportPortal launch finished and session closed")
|
|
except Exception as e:
|
|
logger.error(f"Error finishing ReportPortal launch: {e}")
|
|
|
|
# Note: daemon thread will automatically terminate when main program ends
|
|
if server_thread:
|
|
logger.info("Computer server will stop when main program exits (daemon thread)")
|
|
|
|
# Exit with appropriate code based on test results
|
|
logger.info(f"Exiting with code: {final_exit_code}")
|
|
exit(final_exit_code)
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main()) |