Files
feiqiu-ETL/etl_billiards/scripts/run_tests.py
2025-11-30 07:19:05 +08:00

196 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
灵活的测试执行脚本,可像搭积木一样组合不同参数或预置命令(模式/数据库/归档路径等),
直接运行本文件即可触发 pytest。
示例:
python scripts/run_tests.py --suite online --flow FULL --keyword ORDERS
python scripts/run_tests.py --preset fetch_only
python scripts/run_tests.py --suite online --json-source tmp/archives
"""
from __future__ import annotations
import argparse
import importlib.util
import os
import shlex
import sys
from typing import Dict, List
import pytest
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
# 确保项目根目录在 sys.path便于 tests 内部 import config / tasks 等模块
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
SUITE_MAP: Dict[str, str] = {
"online": "tests/unit/test_etl_tasks_online.py",
"integration": "tests/integration/test_database.py",
}
PRESETS: Dict[str, Dict] = {}
def _load_presets():
preset_path = os.path.join(os.path.dirname(__file__), "test_presets.py")
if not os.path.exists(preset_path):
return
spec = importlib.util.spec_from_file_location("test_presets", preset_path)
if not spec or not spec.loader:
return
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore[attr-defined]
presets = getattr(module, "PRESETS", {})
if isinstance(presets, dict):
PRESETS.update(presets)
_load_presets()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="ETL 测试执行器(支持参数化调配)")
parser.add_argument(
"--suite",
choices=sorted(SUITE_MAP.keys()),
nargs="+",
help="预置测试套件,可多选(默认全部 online/offline",
)
parser.add_argument(
"--tests",
nargs="+",
help="自定义测试路径(可与 --suite 混用),例如 tests/unit/test_config.py",
)
parser.add_argument(
"--flow",
choices=["FETCH_ONLY", "INGEST_ONLY", "FULL"],
help="覆盖 PIPELINE_FLOW在线抓取/本地清洗/全流程)",
)
parser.add_argument("--json-source", help="设置 JSON_SOURCE_DIR本地清洗入库使用的 JSON 目录)")
parser.add_argument("--json-fetch-root", help="设置 JSON_FETCH_ROOT在线抓取输出根目录")
parser.add_argument(
"--keyword",
"-k",
help="pytest -k 关键字过滤(例如 ORDERS只运行包含该字符串的用例",
)
parser.add_argument(
"--pytest-args",
help="附加 pytest 参数,格式与命令行一致(例如 \"-vv --maxfail=1\"",
)
parser.add_argument(
"--env",
action="append",
metavar="KEY=VALUE",
help="自定义环境变量,可重复传入,例如 --env STORE_ID=123",
)
parser.add_argument("--preset", choices=sorted(PRESETS.keys()) if PRESETS else None, nargs="+",
help="从 scripts/test_presets.py 中选择一个或多个组合命令")
parser.add_argument("--list-presets", action="store_true", help="列出可用预置命令后退出")
parser.add_argument("--dry-run", action="store_true", help="仅打印将要执行的命令与环境,不真正运行 pytest")
return parser.parse_args()
def apply_presets_to_args(args: argparse.Namespace):
if not args.preset:
return
for name in args.preset:
preset = PRESETS.get(name, {})
if not preset:
continue
for key, value in preset.items():
if key in ("suite", "tests"):
if not value:
continue
existing = getattr(args, key)
if existing is None:
setattr(args, key, list(value))
else:
existing.extend(value)
elif key == "env":
args.env = (args.env or []) + list(value)
elif key == "pytest_args":
args.pytest_args = " ".join(filter(None, [value, args.pytest_args or ""]))
elif key == "keyword":
if args.keyword is None:
args.keyword = value
else:
if getattr(args, key, None) is None:
setattr(args, key, value)
def apply_env(args: argparse.Namespace) -> Dict[str, str]:
env_updates = {}
if args.flow:
env_updates["PIPELINE_FLOW"] = args.flow
if args.json_source:
env_updates["JSON_SOURCE_DIR"] = args.json_source
if args.json_fetch_root:
env_updates["JSON_FETCH_ROOT"] = args.json_fetch_root
if args.env:
for item in args.env:
if "=" not in item:
raise SystemExit(f"--env 参数格式错误: {item!r},应为 KEY=VALUE")
key, value = item.split("=", 1)
env_updates[key.strip()] = value.strip()
for key, value in env_updates.items():
os.environ[key] = value
return env_updates
def build_pytest_args(args: argparse.Namespace) -> List[str]:
targets: List[str] = []
if args.suite:
for suite in args.suite:
targets.append(SUITE_MAP[suite])
if args.tests:
targets.extend(args.tests)
if not targets:
targets = list(SUITE_MAP.values())
pytest_args: List[str] = targets
if args.keyword:
pytest_args += ["-k", args.keyword]
if args.pytest_args:
pytest_args += shlex.split(args.pytest_args)
return pytest_args
def main() -> int:
os.chdir(PROJECT_ROOT)
args = parse_args()
if args.list_presets:
print("可用预置命令:")
if not PRESETS:
print("(暂无,可编辑 scripts/test_presets.py 添加)")
else:
for name in sorted(PRESETS):
print(f"- {name}")
return 0
apply_presets_to_args(args)
env_updates = apply_env(args)
pytest_args = build_pytest_args(args)
print("=== 环境变量覆盖 ===")
if env_updates:
for k, v in env_updates.items():
print(f"{k}={v}")
else:
print("(无覆盖,沿用系统默认)")
print("\n=== Pytest 参数 ===")
print(" ".join(pytest_args))
print()
if args.dry_run:
print("Dry-run 模式,未真正执行 pytest")
return 0
exit_code = pytest.main(pytest_args)
return int(exit_code)
if __name__ == "__main__":
sys.exit(main())