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"])
|
bc, tc = set(base["cols"]), set(target["cols"])
|
||||||
from collections import defaultdict
|
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 = {}
|
base_colmap = {}
|
||||||
for c in base["cols"]:
|
for c in base["cols"]:
|
||||||
p = c.split("|")
|
p = c.split("|")
|
||||||
if len(p) == 3:
|
if len(p) == 3:
|
||||||
base_colmap[(p[0], p[1])] = p[2]
|
base_colmap[(p[0], p[1])] = p[2]
|
||||||
type_diff = []
|
target_colmap = {}
|
||||||
for c in target["cols"]:
|
for c in target["cols"]:
|
||||||
p = c.split("|")
|
p = c.split("|")
|
||||||
if len(p) == 3 and (p[0], p[1]) in base_colmap:
|
if len(p) == 3:
|
||||||
bt2 = base_colmap[(p[0], p[1])]
|
target_colmap[(p[0], p[1])] = p[2]
|
||||||
if bt2 != p[2]:
|
type_diff = []
|
||||||
type_diff.append("%s.%s: 对照=%s 本环境=%s" % (p[0], p[1], bt2, p[2]))
|
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 ""))
|
print("\n── 列类型不同 [%d] %s" % (len(type_diff), "【需迁移】" if type_diff else ""))
|
||||||
for d in type_diff:
|
for d in type_diff:
|
||||||
print(" ", d)
|
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