akg-factor-bridge/test_xxl_trigger.py

209 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 -*-
"""平台触发入口xxl.py的单测——子进程与回调都打桩不跑真链、不发真请求。
运行: docker compose exec -T akg-factor-bridge python test_xxl_trigger.py
全过输出 "ALL PASS (n cases)",任一失败退出码 1。
"""
import sys
import time
import traceback
from fastapi import HTTPException
import config
import xxl
RESULTS = []
def case(name):
def deco(fn):
RESULTS.append((name, fn))
return fn
return deco
def _reset(key="test-key"):
"""每例开跑前清场:配置好 key、清任务表、装默认桩。"""
config.XXL_TRIGGER_KEY = key
config.XXL_CALLBACK_MAX_RETRIES = 0
xxl._jobs.clear()
xxl._running_id = None
ran, sent = [], []
xxl._run_step = lambda cmd: (ran.append(list(cmd)), (0, "ok"))[1]
xxl._post_callback = lambda url, data: (sent.append((url, data)), "delivered http=200")[1]
return ran, sent
def _wait_done(task_id, timeout=5.0):
"""等到任务连同结案回调都处理完closed=True避免断言撞上后台线程的时间差。"""
t0 = time.time()
while time.time() - t0 < timeout:
if xxl._jobs[task_id].get("closed"):
return xxl._jobs[task_id]
time.sleep(0.02)
raise AssertionError("任务超时未结束")
def _trigger(**kw):
args = dict(callbackUrl=None, steps="build,plan,push-pool", date=None,
key="test-key", x_job_key=None)
args.update(kw)
return xxl.trigger_daily_build(**args)
@case("没配 XXL_TRIGGER_KEY → 整组端点禁用 (403),与决策系统同款安全默认")
def _():
_reset(key="")
try:
_trigger(key="whatever")
raise AssertionError("该 403 没 403")
except HTTPException as e:
assert e.status_code == 403, e
@case("key 不对 → 401对了 → 受理并返回 task_id")
def _():
ran, _ = _reset()
try:
_trigger(key="wrong")
raise AssertionError("该 401 没 401")
except HTTPException as e:
assert e.status_code == 401, e
r = _trigger()
assert r["status"] == "accepted" and r["task_id"], r
_wait_done(r["task_id"])
@case("三步按固定顺序跑完 → success成功回调是空包")
def _():
ran, sent = _reset()
r = _trigger(callbackUrl="http://xxl/callback?logId=1")
job = _wait_done(r["task_id"])
assert job["state"] == "success", job
assert [c[1] for c in ran] == ["build", "plan", "push-pool"], ran
assert sent and sent[0][0].startswith("http://xxl/callback"), sent
assert sent[0][1] == b"", sent # 成功=空包,对端按成功结案
@case("中间一步失败 → 后面的步骤不跑,失败回调带 handleCode=500 与原因")
def _():
ran, sent = _reset()
xxl._run_step = lambda cmd: (ran.append(list(cmd)),
(1, "boom") if cmd[1] == "plan" else (0, "ok"))[1]
r = _trigger(callbackUrl="http://xxl/cb")
job = _wait_done(r["task_id"])
assert job["state"] == "failed" and "plan" in job["note"], job
assert [c[1] for c in ran] == ["build", "plan"], ran # push-pool 没跑
body = sent[0][1].decode()
assert "handleCode=500" in body and "plan" in body, body
@case("同一时刻只允许一条链: 在跑时再触发 → 409")
def _():
_reset()
gate = {"open": False}
def slow(cmd):
while not gate["open"]:
time.sleep(0.01)
return 0, "ok"
xxl._run_step = slow
r1 = _trigger(steps="build")
try:
_trigger(steps="build")
raise AssertionError("该 409 没 409")
except HTTPException as e:
assert e.status_code == 409, e
finally:
gate["open"] = True
_wait_done(r1["task_id"])
r2 = _trigger(steps="build") # 跑完后可以再触发
_wait_done(r2["task_id"])
@case("steps 白名单与顺序: 乱序请求被摆正, 未知步骤 400")
def _():
ran, _ = _reset()
r = _trigger(steps="push-pool,build")
_wait_done(r["task_id"])
assert [c[1] for c in ran] == ["build", "push-pool"], ran # 顺序固定
try:
_trigger(steps="build,发大财")
raise AssertionError("该 400 没 400")
except HTTPException as e:
assert e.status_code == 400, e
@case("date 参数透传到每一步命令")
def _():
ran, _ = _reset()
r = _trigger(steps="build,plan", date="2026-08-01")
_wait_done(r["task_id"])
for cmd in ran:
assert cmd[-2:] == ["--date", "2026-08-01"], cmd
@case("无 callbackUrl 也能正常结案 (只记日志, 平台走状态查询)")
def _():
_, sent = _reset()
r = _trigger(callbackUrl=None, steps="build")
job = _wait_done(r["task_id"])
assert job["state"] == "success" and not sent
@case("状态接口: 能查到 state/steps/tail; 不认识的 task_id 404; 也要带 key")
def _():
_reset()
r = _trigger(steps="build")
_wait_done(r["task_id"])
st = xxl.job_status(r["task_id"], key="test-key", x_job_key=None)
assert st["state"] == "success" and st["steps"] == ["build"] and st["tail"]
try:
xxl.job_status("no-such-id", key="test-key", x_job_key=None)
raise AssertionError("该 404 没 404")
except HTTPException as e:
assert e.status_code == 404, e
try:
xxl.job_status(r["task_id"], key="wrong", x_job_key=None)
raise AssertionError("该 401 没 401")
except HTTPException as e:
assert e.status_code == 401, e
@case("回调连接层失败: 重试次数用尽后放弃, 任务状态不受影响")
def _():
_reset()
config.XXL_CALLBACK_MAX_RETRIES = 0 # 失败一次即放弃, 免得测试睡 30 秒
def dead(url, data):
raise OSError("connect refused")
xxl._post_callback = dead
r = _trigger(callbackUrl="http://xxl/cb", steps="build")
job = _wait_done(r["task_id"])
assert job["state"] == "success", job # 回调失败不改任务结论
assert any("放弃" in ln for ln in job["tail"]), job["tail"]
# ---------------------------------------------------------------- runner
def main():
passed, failed = 0, 0
for name, fn in RESULTS:
try:
fn()
print(f" PASS {name}")
passed += 1
except Exception:
print(f" FAIL {name}")
traceback.print_exc()
failed += 1
print("-" * 60)
if failed:
print(f"FAILED: {failed} / {passed + failed}")
sys.exit(1)
print(f"ALL PASS ({passed} cases)")
if __name__ == "__main__":
main()