# -*- coding: utf-8 -*- """Unified test runner for AgentScope model scripts. Automatically reads API keys from environment variables and only runs tests for providers whose keys are present. Supports fine-grained control over which providers and test types to run. Usage examples: # Run all available tests (auto-detected from env vars) python scripts/model_examples/run_tests.py # Run only specific providers python scripts/model_examples/run_tests.py --providers openai_chat, anthropic,gemini # Run only specific test types python scripts/model_examples/run_tests.py --tests call,multiagent # Combine: only call tests for openai and dashscope python scripts/model_examples/run_tests.py --providers openai_chat, dashscope --tests call # List all providers and their env var / availability status python scripts/model_examples/run_tests.py --list # Include ollama even if server may not be running python scripts/model_examples/run_tests.py --providers ollama # Set a per-test timeout (seconds, default 120) python scripts/model_examples/run_tests.py --timeout 60 # Stream each script's output to the terminal (default: suppressed, shown only on failure) python scripts/model_examples/run_tests.py --verbose """ import argparse import os import subprocess import sys import textwrap import time from dataclasses import dataclass, field from pathlib import Path from typing import Optional # --------------------------------------------------------------------------- # Provider definitions # --------------------------------------------------------------------------- SCRIPTS_DIR = Path(__file__).parent @dataclass class Provider: """Metadata for a model provider.""" name: str env_var: Optional[str] # None for local providers (e.g. Ollama) file_prefix: str # e.g. "openai_chat_model" supported_tests: list[str] = field(default_factory=list) description: str = "" def is_available(self) -> bool: """Return True if the provider's credentials are present.""" if self.env_var is None: # Ollama: check if server is reachable return _ollama_is_running() return bool(os.environ.get(self.env_var, "").strip()) def script_path(self, test_type: str) -> Optional[Path]: """Return the script path for the given test type, or None if it doesn't exist.""" path = SCRIPTS_DIR / f"{self.file_prefix}_{test_type}.py" return path if path.exists() else None def _ollama_is_running() -> bool: """Check whether an Ollama server is reachable.""" import urllib.request import urllib.error host = os.environ.get("OLLAMA_HOST", "http://localhost:11434") try: with urllib.request.urlopen(f"{host}/api/tags", timeout=3): pass return True except Exception: return False ALL_PROVIDERS: list[Provider] = [ Provider( name="openai_chat", env_var="OPENAI_API_KEY", file_prefix="openai_chat", supported_tests=[ "call", "multiagent", "multimodal", "multiagent_multimodal", ], description="OpenAI Chat Completions API (gpt-4.1, etc.)", ), Provider( name="openai_response", env_var="OPENAI_API_KEY", file_prefix="openai_response", supported_tests=[ "call", "multiagent", "multimodal", "multiagent_multimodal", ], description="OpenAI Responses API (o1, o3, etc.)", ), Provider( name="anthropic", env_var="ANTHROPIC_API_KEY", file_prefix="anthropic", supported_tests=[ "call", "multiagent", "multimodal", "multiagent_multimodal", ], description="Anthropic Claude models", ), Provider( name="dashscope", env_var="DASHSCOPE_API_KEY", file_prefix="dashscope", supported_tests=[ "call", "multiagent", "multimodal", "multiagent_multimodal", ], description="Alibaba DashScope / Qwen models", ), Provider( name="deepseek", env_var="DEEPSEEK_API_KEY", file_prefix="deepseek", supported_tests=["call", "multiagent"], description="DeepSeek models (no multimodal support)", ), Provider( name="gemini", env_var="GEMINI_API_KEY", file_prefix="gemini", supported_tests=[ "call", "multiagent", "multimodal", "multiagent_multimodal", ], description="Google Gemini models", ), Provider( name="moonshot", env_var="MOONSHOT_API_KEY", file_prefix="moonshot", supported_tests=[ "call", "multiagent", "multimodal", "multiagent_multimodal", ], description="Moonshot AI (Kimi) models", ), Provider( name="xai", env_var="XAI_API_KEY", file_prefix="xai", supported_tests=[ "call", "multiagent", "multimodal", "multiagent_multimodal", ], description="xAI Grok models", ), Provider( name="ollama", env_var=None, file_prefix="ollama", supported_tests=[ "call", "multiagent", "multimodal", "multiagent_multimodal", ], description="Ollama local models (requires running server)", ), ] PROVIDER_MAP: dict[str, Provider] = {p.name: p for p in ALL_PROVIDERS} ALL_TEST_TYPES = ["call", "multiagent", "multimodal", "multiagent_multimodal"] # --------------------------------------------------------------------------- # Result tracking # --------------------------------------------------------------------------- PASS = "PASS" FAIL = "FAIL" SKIP = "SKIP" COLOR_GREEN = "\033[92m" COLOR_RED = "\033[91m" COLOR_YELLOW = "\033[93m" COLOR_BLUE = "\033[94m" COLOR_RESET = "\033[0m" COLOR_BOLD = "\033[1m" def _color(text: str, color: str) -> str: """Wrap *text* in ANSI *color* escape codes when stdout is a TTY.""" if sys.stdout.isatty(): return f"{color}{text}{COLOR_RESET}" return text @dataclass class TestResult: """Result of a single test run.""" provider: str test_type: str status: str # PASS / FAIL / SKIP reason: str = "" duration: float = 0.0 output: str = "" # captured stdout+stderr (only when not streaming) # --------------------------------------------------------------------------- # Core runner # --------------------------------------------------------------------------- def run_script( script_path: Path, timeout: int, verbose: bool, ) -> tuple[str, float, str]: """Run a script as a subprocess; return (status, elapsed_seconds, output). In verbose mode the subprocess output streams directly to the terminal and the returned output string is empty. In quiet mode (default) output is captured; it is printed only when the test fails. """ start = time.monotonic() try: if verbose: result = subprocess.run( [sys.executable, str(script_path)], timeout=timeout, text=True, check=False, ) captured = "" else: result = subprocess.run( [sys.executable, str(script_path)], timeout=timeout, capture_output=True, text=True, check=False, ) captured = (result.stdout or "") + (result.stderr or "") elapsed = time.monotonic() - start status = PASS if result.returncode == 0 else FAIL return status, elapsed, captured except subprocess.TimeoutExpired: elapsed = time.monotonic() - start return FAIL, elapsed, f"[Timed out after {timeout}s]" def print_header(text: str) -> None: """Print a prominent section header separated by a full-width rule.""" width = 72 print() print(_color("=" * width, COLOR_BLUE)) print(_color(f" {text}", COLOR_BOLD)) print(_color("=" * width, COLOR_BLUE)) def print_section(text: str) -> None: """Print a lightweight subsection heading.""" print() print(_color(f"--- {text} ---", COLOR_BLUE)) def run_all( providers: list[str], test_types: list[str], timeout: int, verbose: bool, ) -> list[TestResult]: """Run every requested test type for each provider and return all results. Providers whose credentials are absent are skipped automatically. For each (provider, test_type) pair the corresponding script file is located and executed as a subprocess. Args: providers: Ordered list of provider names to evaluate. test_types: List of test-type suffixes to run per provider. timeout: Per-script timeout in seconds. verbose: When True, stream subprocess output directly to the terminal. When False, capture it and print only on failure. Returns: A list of :class:`TestResult` objects, one per (provider, test_type). """ results: list[TestResult] = [] for pname in providers: provider = PROVIDER_MAP[pname] available = provider.is_available() if not available: if provider.env_var: reason = f"env var {provider.env_var} not set" else: reason = "Ollama server not reachable" for ttype in test_types: results.append(TestResult(pname, ttype, SKIP, reason)) print_section( f"{pname.upper()} [{_color('SKIP', COLOR_YELLOW)}] —" f" {reason}", ) continue print_section( f"{pname.upper()} — running tests: {', '.join(test_types)}", ) for ttype in test_types: if ttype not in provider.supported_tests: results.append( TestResult( pname, ttype, SKIP, f"not supported by {pname}", ), ) print( f" [{_color('SKIP', COLOR_YELLOW)}] {ttype:30s} (not " f"supported)", ) continue script = provider.script_path(ttype) if script is None: results.append( TestResult(pname, ttype, SKIP, "script not found"), ) print( f" [{_color('SKIP', COLOR_YELLOW)}] {ttype:30s} (" f"script not found)", ) continue print(f"\n >>> {script.name}") status, elapsed, captured = run_script(script, timeout, verbose) elapsed_str = f"{elapsed:.1f}s" if status == PASS: label = _color(PASS, COLOR_GREEN) else: label = _color(FAIL, COLOR_RED) # Always show captured output on failure (even in quiet mode) if captured and not verbose: print(captured, end="") print(f" [{label}] {ttype:30s} {elapsed_str}") results.append( TestResult( pname, ttype, status, duration=elapsed, output=captured, ), ) return results # --------------------------------------------------------------------------- # Summary table # --------------------------------------------------------------------------- def print_summary(results: list[TestResult]) -> None: """Print a formatted table summarising every test result. Args: results: All :class:`TestResult` objects produced by :func:`run_all`. """ print_header("TEST SUMMARY") passes = [r for r in results if r.status == PASS] fails = [r for r in results if r.status == FAIL] skips = [r for r in results if r.status == SKIP] col_p = f"{len(passes):>3}" col_f = f"{len(fails):>3}" col_s = f"{len(skips):>3}" print(f" {'Provider':<22} {'Test Type':<28} {'Status':<8} {'Time':>7}") print(f" {'-'*22} {'-'*28} {'-'*8} {'-'*7}") for r in results: if r.status == PASS: status_str = _color(r.status, COLOR_GREEN) elif r.status == FAIL: status_str = _color(r.status, COLOR_RED) else: status_str = _color(r.status, COLOR_YELLOW) time_str = ( f"{r.duration:.1f}s" if r.duration else (r.reason[:20] if r.reason else "") ) print( f" {r.provider:<22} {r.test_type:<28} {status_str:<18}" f" {time_str:>7}", ) print() total = len(results) summary_line = ( f" Total: {total} | " f"{_color('PASS', COLOR_GREEN)}: {col_p} | " f"{_color('FAIL', COLOR_RED)}: {col_f} | " f"{_color('SKIP', COLOR_YELLOW)}: {col_s}" ) print(summary_line) print() if fails: print(_color(" Failed tests:", COLOR_RED)) for r in fails: print(f" - {r.provider} / {r.test_type}") print() # --------------------------------------------------------------------------- # --list mode # --------------------------------------------------------------------------- def print_provider_list() -> None: """Print a status table of all registered providers and their availability.""" print_header("PROVIDER STATUS") print(f" {'Provider':<22} {'Env Var':<25} {'Available':<12} Description") print(f" {'-'*22} {'-'*25} {'-'*12} {'-'*30}") for p in ALL_PROVIDERS: avail = p.is_available() avail_str = ( _color("YES", COLOR_GREEN) if avail else _color("NO", COLOR_RED) ) env_str = p.env_var or "(local — ping server)" tests_str = ", ".join(p.supported_tests) print(f" {p.name:<22} {env_str:<25} {avail_str:<21} {p.description}") print(f" {'':22} {'Supported tests:':<25} {tests_str}") print() # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def build_parser() -> argparse.ArgumentParser: """Construct and return the CLI argument parser.""" parser = argparse.ArgumentParser( description=textwrap.dedent(__doc__), formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "--providers", "-p", metavar="NAME[,NAME...]", default=None, help=( "Comma-separated list of providers to test " f"(default: all). Available: {', '.join(PROVIDER_MAP)}" ), ) parser.add_argument( "--tests", "-t", metavar="TYPE[,TYPE...]", default=None, help=( "Comma-separated list of test types to run " f"(default: all). Available: {', '.join(ALL_TEST_TYPES)}" ), ) parser.add_argument( "--timeout", type=int, default=120, metavar="SECONDS", help="Per-script timeout in seconds (default: 120)", ) parser.add_argument( "--list", "-l", action="store_true", help="List all providers with their env var and availability " "status, then exit", ) parser.add_argument( "--verbose", "-v", action="store_true", help=( "Stream each script's output to the terminal in real time. " "By default output is suppressed and only shown when a test fails." ), ) return parser def main() -> int: """Entry point: parse arguments, run tests, and return an exit code. Returns: 0 if all executed tests passed (or were skipped); 1 if any test failed. """ parser = build_parser() args = parser.parse_args() if args.list: print_provider_list() return 0 # Resolve providers if args.providers: requested = [p.strip() for p in args.providers.split(",") if p.strip()] unknown = [p for p in requested if p not in PROVIDER_MAP] if unknown: print(f"Unknown providers: {', '.join(unknown)}") print(f"Available: {', '.join(PROVIDER_MAP)}") return 1 providers = requested else: providers = list(PROVIDER_MAP) # Resolve test types if args.tests: requested_tests = [ t.strip() for t in args.tests.split(",") if t.strip() ] unknown_tests = [t for t in requested_tests if t not in ALL_TEST_TYPES] if unknown_tests: print(f"Unknown test types: {', '.join(unknown_tests)}") print(f"Available: {', '.join(ALL_TEST_TYPES)}") return 1 test_types = requested_tests else: test_types = ALL_TEST_TYPES print_header( f"AgentScope Model Tests | providers: {len(providers)} | test " f"types: {len(test_types)}", ) print(f" Providers : {', '.join(providers)}") print(f" Test types: {', '.join(test_types)}") print(f" Timeout : {args.timeout}s per script") results = run_all(providers, test_types, args.timeout, args.verbose) print_summary(results) # Exit with non-zero if any test failed failed = any(r.status == FAIL for r in results) return 1 if failed else 0 if __name__ == "__main__": sys.exit(main())