feat(deploy): 冒泡测试套件 run_smoke.py+13关键用例 + ddiff类型差异不再误报缺列
This commit is contained in:
parent
704c3f6495
commit
b86b39e051
@ -87,27 +87,31 @@ def compare(base, target):
|
||||
|
||||
bc, tc = set(base["cols"]), set(target["cols"])
|
||||
from collections import defaultdict
|
||||
miss_cols = defaultdict(list)
|
||||
for c in bc - tc:
|
||||
p = c.split("|")
|
||||
if len(p) == 3:
|
||||
miss_cols[p[0]].append(p[1] + " " + p[2])
|
||||
print("\n── 缺列 [%d 表] %s" % (len(miss_cols), "【需迁移】" if miss_cols else ""))
|
||||
for t in sorted(miss_cols):
|
||||
print(" %s: %s" % (t, ", ".join(sorted(miss_cols[t]))))
|
||||
# 列宽/类型不同(同名不同型)
|
||||
# 列类型不同(同名不同型)先算出来——这些不算"缺列",避免重复报告
|
||||
base_colmap = {}
|
||||
for c in base["cols"]:
|
||||
p = c.split("|")
|
||||
if len(p) == 3:
|
||||
base_colmap[(p[0], p[1])] = p[2]
|
||||
type_diff = []
|
||||
target_colmap = {}
|
||||
for c in target["cols"]:
|
||||
p = c.split("|")
|
||||
if len(p) == 3 and (p[0], p[1]) in base_colmap:
|
||||
bt2 = base_colmap[(p[0], p[1])]
|
||||
if bt2 != p[2]:
|
||||
type_diff.append("%s.%s: 对照=%s 本环境=%s" % (p[0], p[1], bt2, p[2]))
|
||||
if len(p) == 3:
|
||||
target_colmap[(p[0], p[1])] = p[2]
|
||||
type_diff = []
|
||||
for (t, col), bt2 in base_colmap.items():
|
||||
if (t, col) in target_colmap and target_colmap[(t, col)] != bt2:
|
||||
type_diff.append("%s.%s: 对照=%s 本环境=%s" % (t, col, bt2, target_colmap[(t, col)]))
|
||||
type_diff_keys = set((d.split(".")[0], d.split(".")[1].split(":")[0]) for d in type_diff)
|
||||
|
||||
miss_cols = defaultdict(list)
|
||||
for c in bc - tc:
|
||||
p = c.split("|")
|
||||
if len(p) == 3 and (p[0], p[1]) not in type_diff_keys:
|
||||
miss_cols[p[0]].append(p[1] + " " + p[2])
|
||||
print("\n── 缺列 [%d 表] %s" % (len(miss_cols), "【需迁移】" if miss_cols else ""))
|
||||
for t in sorted(miss_cols):
|
||||
print(" %s: %s" % (t, ", ".join(sorted(miss_cols[t]))))
|
||||
print("\n── 列类型不同 [%d] %s" % (len(type_diff), "【需迁移】" if type_diff else ""))
|
||||
for d in type_diff:
|
||||
print(" ", d)
|
||||
|
||||
34
deploy/smoke/cases.json
Normal file
34
deploy/smoke/cases.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"_说明": "冒泡测试用例——增量部署后关键功能点验收。runner: deploy/dsmoke.py。auth=login 的用例先登录拿会话。expect: status 状态码 / contains 响应含串 / not_contains 不含串 / json_field 存在字段。凭据从环境变量 SMOKE_USER/SMOKE_PASS 读(不入库)。",
|
||||
"base": "",
|
||||
"cases": [
|
||||
{"id": "s01", "name": "健康检查", "url": "/", "auth": "none",
|
||||
"expect": {"status": 200}},
|
||||
{"id": "s02", "name": "登录", "url": "/rbac/user/up_login.dspy", "method": "POST",
|
||||
"body": {"username": "${SMOKE_USER}", "password": "***"}, "auth": "none",
|
||||
"expect": {"status": 200}},
|
||||
{"id": "s03", "name": "主页壳", "url": "/", "auth": "login",
|
||||
"expect": {"status": 200, "contains": "产线平台"}},
|
||||
{"id": "s04", "name": "agent菜单", "url": "/pipeline_core/api/agent_menus.dspy", "auth": "login",
|
||||
"expect": {"status": 200, "contains": "工作空间"}},
|
||||
{"id": "s05", "name": "商机产线壳", "url": "/pipeline-opportunity/agent/index.ui", "auth": "login",
|
||||
"expect": {"status": 200, "contains": "商机产线"}},
|
||||
{"id": "s06", "name": "投标产线壳", "url": "/pipeline-bidding/agent/index.ui", "auth": "login",
|
||||
"expect": {"status": 200, "contains": "投标产线"}},
|
||||
{"id": "s07", "name": "开发产线壳", "url": "/pipeline-sdlc/agent/index.ui", "auth": "login",
|
||||
"expect": {"status": 200}},
|
||||
{"id": "s08", "name": "运营产线壳", "url": "/pipeline-ops/agent/index.ui", "auth": "login",
|
||||
"expect": {"status": 200}},
|
||||
{"id": "s09", "name": "营销产线壳", "url": "/pipeline-dist/agent/index.ui", "auth": "login",
|
||||
"expect": {"status": 200}},
|
||||
{"id": "s10", "name": "商机数据参考(爬虫平台连通)", "url": "/pipeline-opportunity/api/opp_references_popup.dspy", "auth": "login",
|
||||
"expect": {"status": 200}},
|
||||
{"id": "s11", "name": "项目管理列表", "url": "/pipeline_core/api/agent_menu_open.dspy", "method": "POST",
|
||||
"body": {"menu": "workspace"}, "auth": "login",
|
||||
"expect": {"status": 200}},
|
||||
{"id": "s12", "name": "待办", "url": "/pipeline_core/api/human_todo_list.dspy", "auth": "login",
|
||||
"expect": {"status": 200}},
|
||||
{"id": "s13", "name": "i18n资源", "url": "/i18n/zh/i18n.json", "auth": "none",
|
||||
"expect": {"status": 200, "contains": "产线"}}
|
||||
]
|
||||
}
|
||||
136
deploy/smoke/run_smoke.py
Normal file
136
deploy/smoke/run_smoke.py
Normal file
@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""run_smoke.py — 增量部署冒泡测试执行器。
|
||||
|
||||
用法(在应用根目录,凭据走环境变量或参数,不落库):
|
||||
SMOKE_USER=admin SMOKE_PASS=*** ./py3/bin/python deploy/smoke/run_smoke.py
|
||||
./py3/bin/python deploy/smoke/run_smoke.py --base http://127.0.0.1:9090 --user admin --pass ***
|
||||
./py3/bin/python deploy/smoke/run_smoke.py --https https://doit.opencomputing.cn # 走外网域名
|
||||
|
||||
退出码:0=全部通过;1=有用例失败。增量部署第3步验证必跑。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import ssl
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
CASES = os.path.join(HERE, "cases.json")
|
||||
|
||||
|
||||
def req(opener, base, case, session_cookie, verify_ssl):
|
||||
url = base.rstrip("/") + case["url"]
|
||||
method = case.get("method", "GET").upper()
|
||||
body = case.get("body")
|
||||
data = None
|
||||
headers = {}
|
||||
if body is not None:
|
||||
# 变量替换
|
||||
b = {}
|
||||
for k, v in body.items():
|
||||
if isinstance(v, str):
|
||||
v = v.replace("${SMOKE_USER}", os.environ.get("SMOKE_USER", ""))
|
||||
v = v.replace("${SMOKE_PASS}", os.environ.get("SMOKE_PASS", ""))
|
||||
b[k] = v
|
||||
data = json.dumps(b).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
if case.get("auth") == "login" and session_cookie:
|
||||
headers["Cookie"] = session_cookie
|
||||
r = urllib.request.Request(url, data=data, method=method, headers=headers)
|
||||
ctx = None
|
||||
if url.startswith("https"):
|
||||
ctx = ssl.create_default_context()
|
||||
if not verify_ssl:
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
try:
|
||||
resp = opener.open(r, timeout=20, context=ctx) if ctx else opener.open(r, timeout=20)
|
||||
status = resp.getcode()
|
||||
text = resp.read().decode("utf-8", errors="replace")
|
||||
return status, text, resp.headers, None
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, (e.read() or b"").decode("utf-8", errors="replace"), e.headers, None
|
||||
except Exception as e:
|
||||
return 0, "", None, str(e)
|
||||
|
||||
|
||||
def main():
|
||||
base = "http://127.0.0.1:9090"
|
||||
verify_ssl = True
|
||||
args = sys.argv[1:]
|
||||
if "--https" in args:
|
||||
base = args[args.index("--https") + 1]
|
||||
verify_ssl = False # 自签/内网域名常见
|
||||
elif "--base" in args:
|
||||
base = args[args.index("--base") + 1]
|
||||
if "--user" in args:
|
||||
os.environ["SMOKE_USER"] = args[args.index("--user") + 1]
|
||||
if "--pass" in args:
|
||||
os.environ["SMOKE_PASS"] = args[args.index("--pass") + 1]
|
||||
|
||||
cfg = json.load(open(CASES))
|
||||
cases = cfg["cases"]
|
||||
|
||||
import http.cookiejar
|
||||
cj = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||||
|
||||
# 第一步:登录拿会话
|
||||
session_cookie = None
|
||||
login_case = None
|
||||
for c in cases:
|
||||
if c.get("auth") == "none" and c.get("method", "GET") == "POST" and "login" in c["url"]:
|
||||
login_case = c
|
||||
break
|
||||
if login_case and os.environ.get("SMOKE_PASS"):
|
||||
status, text, headers, err = req(opener, base, login_case, None, verify_ssl)
|
||||
# 从 cookiejar 提取会话名
|
||||
names = []
|
||||
for ck in cj:
|
||||
names.append("%s=%s" % (ck.name, ck.value))
|
||||
session_cookie = "; ".join(names)
|
||||
if status != 200:
|
||||
print("[LOGIN-FAIL] 登录用例返回 %s,后续需登录的用例将跳过: %s" % (status, (err or text[:100])))
|
||||
|
||||
passed, failed, skipped = [], [], []
|
||||
for c in cases:
|
||||
cid, name = c["id"], c["name"]
|
||||
if c.get("auth") == "login" and not session_cookie:
|
||||
skipped.append(cid)
|
||||
print(" SKIP %-4s %s(无登录态)" % (cid, name))
|
||||
continue
|
||||
status, text, headers, err = req(opener, base, c, session_cookie, verify_ssl)
|
||||
exp = c.get("expect", {})
|
||||
ok = True
|
||||
reason = []
|
||||
if err:
|
||||
ok = False
|
||||
reason.append("请求异常:" + err[:80])
|
||||
if "status" in exp and status != exp["status"]:
|
||||
ok = False
|
||||
reason.append("状态 %s≠%s" % (status, exp["status"]))
|
||||
if "contains" in exp and exp["contains"] not in text:
|
||||
ok = False
|
||||
reason.append("缺内容:%s" % exp["contains"][:20])
|
||||
if "not_contains" in exp and exp["not_contains"] in text:
|
||||
ok = False
|
||||
reason.append("含禁止内容:%s" % exp["not_contains"][:20])
|
||||
if ok:
|
||||
passed.append(cid)
|
||||
print(" PASS %-4s %s" % (cid, name))
|
||||
else:
|
||||
failed.append(cid)
|
||||
print(" FAIL %-4s %s [%s]" % (cid, name, "; ".join(reason)))
|
||||
|
||||
print("\n══ 冒泡结果: %d 通过 / %d 失败 / %d 跳过(共 %d)══"
|
||||
% (len(passed), len(failed), len(skipped), len(cases)))
|
||||
sys.exit(0 if not failed else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user