Files
wehub-resource-sync c3bf08ac8d
K8s Workspace Integration Tests / k8s-workspace-tests (push) Has been cancelled
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Web UI / check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:27 +08:00

586 lines
18 KiB
Python

# -*- 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())