Quick/full mode smoke test for deployment verification. Covers all API endpoint groups with response validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
868 lines
29 KiB
Python
868 lines
29 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Paper Dynasty Smoke Test
|
|
|
|
Comprehensive deployment verification for the Database API and Discord Bot.
|
|
Tests endpoint availability, data integrity, and key features like card rendering
|
|
and the Refractor (evolution) system.
|
|
|
|
Usage:
|
|
python smoke_test.py # Test dev environment
|
|
python smoke_test.py --env prod # Test production
|
|
python smoke_test.py --env dev --verbose # Show response details
|
|
python smoke_test.py --category core # Run only core tests
|
|
|
|
Exit codes:
|
|
0 = all tests passed
|
|
1 = one or more tests failed
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
from api_client import PaperDynastyAPI
|
|
|
|
# ANSI colors
|
|
GREEN = "\033[92m"
|
|
RED = "\033[91m"
|
|
YELLOW = "\033[93m"
|
|
CYAN = "\033[96m"
|
|
DIM = "\033[2m"
|
|
BOLD = "\033[1m"
|
|
RESET = "\033[0m"
|
|
|
|
|
|
@dataclass
|
|
class TestResult:
|
|
name: str
|
|
category: str
|
|
passed: bool
|
|
status_code: Optional[int] = None
|
|
detail: str = ""
|
|
duration_ms: float = 0
|
|
|
|
|
|
@dataclass
|
|
class SmokeTestRunner:
|
|
env: str = "dev"
|
|
verbose: bool = False
|
|
categories: Optional[list] = None
|
|
mode: str = "quick"
|
|
results: list = field(default_factory=list)
|
|
api: PaperDynastyAPI = field(init=False)
|
|
|
|
def __post_init__(self):
|
|
self.api = PaperDynastyAPI(environment=self.env, verbose=self.verbose)
|
|
|
|
def check(
|
|
self,
|
|
name: str,
|
|
category: str,
|
|
endpoint: str,
|
|
*,
|
|
params: Optional[list] = None,
|
|
expect_list: bool = False,
|
|
min_count: int = 0,
|
|
expect_keys: Optional[list] = None,
|
|
timeout: int = 30,
|
|
requires_auth: bool = False,
|
|
) -> Optional[TestResult]:
|
|
"""Run a single endpoint check."""
|
|
if self.categories and category not in self.categories:
|
|
return None
|
|
|
|
import requests
|
|
|
|
if requires_auth and not self.api.token:
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=True,
|
|
detail="skipped (no API_TOKEN)",
|
|
duration_ms=0,
|
|
)
|
|
|
|
start = time.time()
|
|
try:
|
|
url = self.api._build_url(endpoint, params=params)
|
|
raw = requests.get(url, headers=self.api.headers, timeout=timeout)
|
|
elapsed = (time.time() - start) * 1000
|
|
status = raw.status_code
|
|
|
|
if status != 200:
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
status_code=status,
|
|
detail=f"HTTP {status}",
|
|
duration_ms=elapsed,
|
|
)
|
|
|
|
data = raw.json()
|
|
|
|
# API returns {"count": N, "<resource>": [...]} — unwrap
|
|
if expect_list and isinstance(data, dict):
|
|
# Find the list value in the response dict
|
|
lists = [v for v in data.values() if isinstance(v, list)]
|
|
if lists:
|
|
data = lists[0]
|
|
else:
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
status_code=status,
|
|
detail=f"no list found in response keys: {list(data.keys())}",
|
|
duration_ms=elapsed,
|
|
)
|
|
|
|
# Validate response shape
|
|
if expect_list:
|
|
if not isinstance(data, list):
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
status_code=status,
|
|
detail=f"expected list, got {type(data).__name__}",
|
|
duration_ms=elapsed,
|
|
)
|
|
if len(data) < min_count:
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
status_code=status,
|
|
detail=f"expected >= {min_count} items, got {len(data)}",
|
|
duration_ms=elapsed,
|
|
)
|
|
detail = f"{len(data)} items"
|
|
elif expect_keys:
|
|
if isinstance(data, list):
|
|
obj = data[0] if data else {}
|
|
else:
|
|
obj = data
|
|
missing = [k for k in expect_keys if k not in obj]
|
|
if missing:
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
status_code=status,
|
|
detail=f"missing keys: {missing}",
|
|
duration_ms=elapsed,
|
|
)
|
|
detail = "schema ok"
|
|
else:
|
|
detail = "ok"
|
|
|
|
if self.verbose and isinstance(data, list):
|
|
detail += (
|
|
f" (first: {json.dumps(data[0], default=str)[:80]}...)"
|
|
if data
|
|
else ""
|
|
)
|
|
elif self.verbose and isinstance(data, dict):
|
|
detail += f" ({json.dumps(data, default=str)[:80]}...)"
|
|
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=True,
|
|
status_code=status,
|
|
detail=detail,
|
|
duration_ms=elapsed,
|
|
)
|
|
|
|
except requests.exceptions.Timeout:
|
|
elapsed = (time.time() - start) * 1000
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
detail=f"timeout after {timeout}s",
|
|
duration_ms=elapsed,
|
|
)
|
|
except requests.exceptions.ConnectionError:
|
|
elapsed = (time.time() - start) * 1000
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
detail="connection refused",
|
|
duration_ms=elapsed,
|
|
)
|
|
except Exception as e:
|
|
elapsed = (time.time() - start) * 1000
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
detail=str(e)[:100],
|
|
duration_ms=elapsed,
|
|
)
|
|
|
|
def check_url(
|
|
self,
|
|
name: str,
|
|
category: str,
|
|
url: str,
|
|
*,
|
|
timeout: int = 30,
|
|
expect_content_type: Optional[str] = None,
|
|
) -> Optional[TestResult]:
|
|
"""Check a raw URL (not through the API client)."""
|
|
if self.categories and category not in self.categories:
|
|
return None
|
|
|
|
import requests
|
|
|
|
start = time.time()
|
|
try:
|
|
raw = requests.get(url, timeout=timeout)
|
|
elapsed = (time.time() - start) * 1000
|
|
|
|
if raw.status_code != 200:
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
status_code=raw.status_code,
|
|
detail=f"HTTP {raw.status_code}",
|
|
duration_ms=elapsed,
|
|
)
|
|
|
|
detail = f"{len(raw.content)} bytes"
|
|
if expect_content_type:
|
|
ct = raw.headers.get("content-type", "")
|
|
if expect_content_type not in ct:
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
status_code=raw.status_code,
|
|
detail=f"expected {expect_content_type}, got {ct}",
|
|
duration_ms=elapsed,
|
|
)
|
|
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=True,
|
|
status_code=raw.status_code,
|
|
detail=detail,
|
|
duration_ms=elapsed,
|
|
)
|
|
except Exception as e:
|
|
elapsed = (time.time() - start) * 1000
|
|
return TestResult(
|
|
name=name,
|
|
category=category,
|
|
passed=False,
|
|
detail=str(e)[:100],
|
|
duration_ms=elapsed,
|
|
)
|
|
|
|
def _fetch_id(
|
|
self, endpoint: str, params: Optional[list] = None, requires_auth: bool = False
|
|
) -> Optional[int]:
|
|
"""Fetch the first item's ID from a list endpoint."""
|
|
import requests
|
|
|
|
if requires_auth and not self.api.token:
|
|
return None
|
|
try:
|
|
url = self.api._build_url(endpoint, params=params)
|
|
raw = requests.get(url, headers=self.api.headers, timeout=10)
|
|
if raw.status_code != 200:
|
|
return None
|
|
data = raw.json()
|
|
if isinstance(data, dict):
|
|
lists = [v for v in data.values() if isinstance(v, list)]
|
|
data = lists[0] if lists else []
|
|
if data and isinstance(data, list) and "id" in data[0]:
|
|
return data[0]["id"]
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def run_all(self, mode: str = "quick"):
|
|
"""Run smoke test checks. mode='quick' for core, 'full' for everything."""
|
|
base = self.api.base_url
|
|
t = 10 if mode == "quick" else 30 # quick should be fast
|
|
|
|
# Pre-fetch IDs for by-ID lookups (full mode only)
|
|
if mode == "full":
|
|
team_id = self._fetch_id("teams", params=[("limit", 1)])
|
|
player_id = self._fetch_id("players", params=[("limit", 1)])
|
|
card_id = self._fetch_id("cards", params=[("limit", 1)])
|
|
game_id = self._fetch_id("games", params=[("limit", 1)])
|
|
result_id = self._fetch_id("results", params=[("limit", 1)])
|
|
track_id = self._fetch_id("evolution/tracks", requires_auth=True)
|
|
else:
|
|
team_id = player_id = card_id = game_id = result_id = track_id = None
|
|
|
|
# ── QUICK: fast, reliable endpoints — deployment canary ──
|
|
tests = [
|
|
self.check_url("API docs", "core", f"{base}/docs", timeout=5),
|
|
self.check(
|
|
"Current season/week", "core", "current", expect_keys=["season", "week"]
|
|
),
|
|
self.check("Rarities", "core", "rarities", expect_list=True, min_count=5),
|
|
self.check("Cardsets", "core", "cardsets", expect_list=True, min_count=1),
|
|
self.check(
|
|
"Pack types", "core", "packtypes", expect_list=True, min_count=1
|
|
),
|
|
self.check_url(
|
|
"OpenAPI schema",
|
|
"core",
|
|
f"{base}/openapi.json",
|
|
expect_content_type="application/json",
|
|
),
|
|
self.check(
|
|
"Teams",
|
|
"teams",
|
|
"teams",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
min_count=1,
|
|
),
|
|
self.check(
|
|
"Team by abbrev",
|
|
"teams",
|
|
"teams",
|
|
params=[("abbrev", "SKB")],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Players",
|
|
"players",
|
|
"players",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
min_count=1,
|
|
),
|
|
self.check(
|
|
"Player search",
|
|
"players",
|
|
"players/search",
|
|
params=[("q", "Judge"), ("limit", 3)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Batting cards",
|
|
"cards",
|
|
"battingcards",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
min_count=1,
|
|
),
|
|
self.check(
|
|
"Pitching cards",
|
|
"cards",
|
|
"pitchingcards",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
min_count=1,
|
|
),
|
|
self.check(
|
|
"Packs",
|
|
"economy",
|
|
"packs",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
min_count=1,
|
|
),
|
|
self.check(
|
|
"Events", "economy", "events", params=[("limit", 5)], expect_list=True
|
|
),
|
|
self.check(
|
|
"Scout opportunities",
|
|
"scouting",
|
|
"scout_opportunities",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Evolution tracks",
|
|
"refractor",
|
|
"evolution/tracks",
|
|
expect_list=True,
|
|
min_count=1,
|
|
requires_auth=True,
|
|
),
|
|
]
|
|
|
|
# ── FULL: all endpoints, by-ID lookups, sub-resources ──
|
|
if mode == "full":
|
|
tests.extend(
|
|
[
|
|
# Core extras
|
|
self.check(
|
|
"Cardset search",
|
|
"core",
|
|
"cardsets/search",
|
|
params=[("q", "Live"), ("limit", 3)],
|
|
expect_list=True,
|
|
),
|
|
# Team sub-resources
|
|
*(
|
|
[
|
|
self.check(
|
|
f"Team by ID ({team_id})",
|
|
"teams",
|
|
f"teams/{team_id}",
|
|
expect_keys=["id", "abbrev"],
|
|
),
|
|
self.check(
|
|
"Team cards",
|
|
"teams",
|
|
f"teams/{team_id}/cards",
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Team evolutions",
|
|
"teams",
|
|
f"teams/{team_id}/evolutions",
|
|
expect_list=True,
|
|
requires_auth=True,
|
|
),
|
|
self.check(
|
|
"Team lineup",
|
|
"teams",
|
|
f"teams/{team_id}/lineup/default",
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Team SP lineup",
|
|
"teams",
|
|
f"teams/{team_id}/sp/default",
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Team RP lineup",
|
|
"teams",
|
|
f"teams/{team_id}/rp/default",
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Team season record",
|
|
"teams",
|
|
f"teams/{team_id}/season-record/11",
|
|
),
|
|
]
|
|
if team_id
|
|
else []
|
|
),
|
|
# Player extras
|
|
self.check(
|
|
"Random player",
|
|
"players",
|
|
"players/random",
|
|
expect_keys=["id", "p_name"],
|
|
),
|
|
*(
|
|
[
|
|
self.check(
|
|
f"Player by ID ({player_id})",
|
|
"players",
|
|
f"players/{player_id}",
|
|
expect_keys=["id", "p_name"],
|
|
),
|
|
]
|
|
if player_id
|
|
else []
|
|
),
|
|
# Card extras
|
|
self.check(
|
|
"Cards list",
|
|
"cards",
|
|
"cards",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
min_count=1,
|
|
),
|
|
self.check(
|
|
"Batting card ratings",
|
|
"cards",
|
|
"battingcardratings",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
requires_auth=True,
|
|
),
|
|
self.check(
|
|
"Pitching card ratings",
|
|
"cards",
|
|
"pitchingcardratings",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
requires_auth=True,
|
|
),
|
|
self.check(
|
|
"Card positions",
|
|
"cards",
|
|
"cardpositions",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
*(
|
|
[
|
|
self.check(
|
|
f"Card by ID ({card_id})",
|
|
"cards",
|
|
f"cards/{card_id}",
|
|
expect_keys=["id"],
|
|
),
|
|
]
|
|
if card_id
|
|
else []
|
|
),
|
|
*(
|
|
[
|
|
self.check(
|
|
"Batting card by player",
|
|
"cards",
|
|
f"battingcards/player/{player_id}",
|
|
expect_list=True,
|
|
),
|
|
]
|
|
if player_id
|
|
else []
|
|
),
|
|
# Games & results
|
|
self.check(
|
|
"Games list",
|
|
"games",
|
|
"games",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
min_count=1,
|
|
),
|
|
self.check(
|
|
"Results list",
|
|
"games",
|
|
"results",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
min_count=1,
|
|
),
|
|
self.check(
|
|
"Plays list",
|
|
"games",
|
|
"plays",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
min_count=1,
|
|
),
|
|
self.check(
|
|
"Plays batting agg",
|
|
"games",
|
|
"plays/batting",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Plays pitching agg",
|
|
"games",
|
|
"plays/pitching",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
*(
|
|
[
|
|
self.check(
|
|
f"Game by ID ({game_id})",
|
|
"games",
|
|
f"games/{game_id}",
|
|
expect_keys=["id"],
|
|
),
|
|
self.check(
|
|
"Game summary", "games", f"plays/game-summary/{game_id}"
|
|
),
|
|
]
|
|
if game_id
|
|
else []
|
|
),
|
|
*(
|
|
[
|
|
self.check(
|
|
f"Result by ID ({result_id})",
|
|
"games",
|
|
f"results/{result_id}",
|
|
expect_keys=["id"],
|
|
),
|
|
]
|
|
if result_id
|
|
else []
|
|
),
|
|
*(
|
|
[
|
|
self.check(
|
|
"Team W/L record",
|
|
"games",
|
|
f"results/team/{team_id}",
|
|
params=[("season", 11)],
|
|
),
|
|
]
|
|
if team_id
|
|
else []
|
|
),
|
|
# Economy extras
|
|
self.check(
|
|
"Rewards",
|
|
"economy",
|
|
"rewards",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Game rewards",
|
|
"economy",
|
|
"gamerewards",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
min_count=1,
|
|
),
|
|
self.check(
|
|
"Gauntlet rewards",
|
|
"economy",
|
|
"gauntletrewards",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Gauntlet runs",
|
|
"economy",
|
|
"gauntletruns",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Awards",
|
|
"economy",
|
|
"awards",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Notifications",
|
|
"economy",
|
|
"notifs",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
# Scouting extras
|
|
self.check(
|
|
"Scout claims",
|
|
"scouting",
|
|
"scout_claims",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"MLB players",
|
|
"scouting",
|
|
"mlbplayers",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Paperdex",
|
|
"scouting",
|
|
"paperdex",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
*(
|
|
[
|
|
self.check(
|
|
"Scouting player keys",
|
|
"scouting",
|
|
"scouting/playerkeys",
|
|
params=[("player_id", player_id)],
|
|
expect_list=True,
|
|
requires_auth=True,
|
|
),
|
|
]
|
|
if player_id
|
|
else []
|
|
),
|
|
# Stats
|
|
self.check(
|
|
"Batting stats",
|
|
"stats",
|
|
"batstats",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Pitching stats",
|
|
"stats",
|
|
"pitstats",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Decisions",
|
|
"stats",
|
|
"decisions",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
self.check(
|
|
"Decisions rest",
|
|
"stats",
|
|
"decisions/rest",
|
|
params=[("limit", 5)],
|
|
expect_list=True,
|
|
),
|
|
# Refractor extras
|
|
*(
|
|
[
|
|
self.check(
|
|
f"Evolution track ({track_id})",
|
|
"refractor",
|
|
f"evolution/tracks/{track_id}",
|
|
expect_keys=["id", "name"],
|
|
requires_auth=True,
|
|
),
|
|
]
|
|
if track_id
|
|
else []
|
|
),
|
|
*(
|
|
[
|
|
self.check(
|
|
"Evolution card state",
|
|
"refractor",
|
|
f"evolution/cards/{card_id}",
|
|
requires_auth=True,
|
|
),
|
|
]
|
|
if card_id
|
|
else []
|
|
),
|
|
]
|
|
)
|
|
|
|
# Filter out None results (skipped categories)
|
|
self.results = [t for t in tests if t is not None]
|
|
|
|
def print_results(self):
|
|
"""Print formatted test results."""
|
|
passed = sum(1 for r in self.results if r.passed)
|
|
failed = sum(1 for r in self.results if not r.passed)
|
|
total = len(self.results)
|
|
|
|
print(
|
|
f"\n{BOLD}Paper Dynasty Smoke Test — {self.env.upper()} ({self.mode}){RESET}"
|
|
)
|
|
print(f"{DIM}{self.api.base_url}{RESET}\n")
|
|
|
|
current_category = None
|
|
for r in self.results:
|
|
if r.category != current_category:
|
|
current_category = r.category
|
|
print(f" {CYAN}{current_category.upper()}{RESET}")
|
|
|
|
icon = f"{GREEN}PASS{RESET}" if r.passed else f"{RED}FAIL{RESET}"
|
|
timing = f"{DIM}{r.duration_ms:6.0f}ms{RESET}"
|
|
|
|
if "skipped" in r.detail:
|
|
icon = f"{YELLOW}SKIP{RESET}"
|
|
|
|
print(f" {icon} {r.name:<30} {timing} {DIM}{r.detail}{RESET}")
|
|
|
|
print(f"\n {BOLD}Results:{RESET} ", end="")
|
|
if failed == 0:
|
|
print(f"{GREEN}{passed}/{total} passed{RESET}")
|
|
else:
|
|
print(
|
|
f"{RED}{failed} failed{RESET}, {GREEN}{passed} passed{RESET} of {total}"
|
|
)
|
|
|
|
return failed == 0
|
|
|
|
def as_json(self) -> str:
|
|
"""Return results as JSON for programmatic use."""
|
|
return json.dumps(
|
|
[
|
|
{
|
|
"name": r.name,
|
|
"category": r.category,
|
|
"passed": r.passed,
|
|
"status_code": r.status_code,
|
|
"detail": r.detail,
|
|
"duration_ms": round(r.duration_ms, 1),
|
|
}
|
|
for r in self.results
|
|
],
|
|
indent=2,
|
|
)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Paper Dynasty deployment smoke test")
|
|
parser.add_argument(
|
|
"--env",
|
|
default="dev",
|
|
choices=["dev", "prod"],
|
|
help="Environment to test (default: dev)",
|
|
)
|
|
parser.add_argument(
|
|
"--verbose", "-v", action="store_true", help="Show response details"
|
|
)
|
|
parser.add_argument("--json", action="store_true", help="Output results as JSON")
|
|
parser.add_argument(
|
|
"mode",
|
|
nargs="?",
|
|
default="quick",
|
|
choices=["quick", "full"],
|
|
help="Test mode: quick (16 checks, ~5s) or full (50+ checks, ~2min)",
|
|
)
|
|
parser.add_argument(
|
|
"--category",
|
|
"-c",
|
|
action="append",
|
|
choices=[
|
|
"core",
|
|
"teams",
|
|
"players",
|
|
"cards",
|
|
"games",
|
|
"economy",
|
|
"scouting",
|
|
"stats",
|
|
"refractor",
|
|
],
|
|
help="Run only specific categories (can repeat)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
runner = SmokeTestRunner(
|
|
env=args.env,
|
|
verbose=args.verbose,
|
|
categories=args.category,
|
|
mode=args.mode,
|
|
)
|
|
runner.run_all(mode=args.mode)
|
|
|
|
if args.json:
|
|
print(runner.as_json())
|
|
all_passed = all(r.passed for r in runner.results)
|
|
else:
|
|
all_passed = runner.print_results()
|
|
|
|
sys.exit(0 if all_passed else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|