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