196 lines
6.4 KiB
Python
196 lines
6.4 KiB
Python
# -*- 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())
|