#!/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) # 手动解析 Set-Cookie(cookiejar 在 http:// 下会拒绝带 Secure 属性的 cookie) names = [] if headers is not None: for hv in headers.get_all("Set-Cookie", []): pair = hv.split(";", 1)[0].strip() if "=" in pair: names.append(pair) session_cookie = "; ".join(names) if names else None if status != 200 or not session_cookie: print("[LOGIN-FAIL] 登录用例返回 %s cookie=%s,后续需登录的用例将跳过: %s" % (status, bool(session_cookie), (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()