448 lines
20 KiB
Python
448 lines
20 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""M1b 真实库集成测试(QC#5 整改)。
|
||
|
||
设计原则(对应 QC 退回意见 #5):
|
||
* 不再只有「纯内存 FakeDB 单测」。本文件在**可连库环境真实执行** DDL + CRUD,
|
||
覆盖 4 张表的建表幂等、唯一键 tenant_key 打头去重、平台公共模板 tenant_id IS NULL、
|
||
子对象扩展 KV 读写、关联表软引用判定(Q-OPEN-3:零基表写入)。
|
||
* 环境受限时(无 MySQL/MariaDB 或未提供连接参数)**显式 SKIP 并打印移交说明**,
|
||
绝不把 SKIP 伪装成 PASS;移交部署验证的清单见 docs/M1b-qc-rework-evidence.md §5。
|
||
|
||
运行方式(三选一):
|
||
1) 环境变量:
|
||
PBL_TEST_DB_HOST=127.0.0.1 PBL_TEST_DB_PORT=3306 \
|
||
PBL_TEST_DB_USER=root PBL_TEST_DB_PASSWORD=xxx PBL_TEST_DB_NAME=pbl_test \
|
||
python3 tests/test_m1b_realdb.py
|
||
2) 配置文件:PBL_TEST_DB_DSN=mysql://user:pass@host:3306/dbname
|
||
3) 无参数:自动探测 127.0.0.1:3306,连不上则 SKIP(退出码 0,但打印 SKIPPED)。
|
||
|
||
退出码:0 = 全部通过 或 环境受限 SKIP;1 = 有真实 FAIL;2 = 依赖缺失且未 SKIP 判定失败。
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
import _m1b_loader # noqa: F401
|
||
import traceback
|
||
from datetime import datetime
|
||
|
||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
DDL_PATH = os.path.join(REPO, "sql", "m1b_ddl.sql")
|
||
|
||
TENANT_A = "T_M1B_A"
|
||
TENANT_B = "T_M1B_B"
|
||
PLATFORM_KEY = "" # tenant_id NULL -> tenant_key ''
|
||
NOW = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
RESULTS = []
|
||
|
||
|
||
def log(ok, name, detail=""):
|
||
RESULTS.append((name, bool(ok), detail))
|
||
print("[%s] %s :: %s" % ("PASS" if ok else "FAIL", name, detail))
|
||
|
||
|
||
def skip(name, detail):
|
||
RESULTS.append((name, None, detail))
|
||
print("[SKIP] %s :: %s" % (name, detail))
|
||
|
||
|
||
# ------------------------------------------------------------------ 连接
|
||
def resolve_dsn():
|
||
dsn = os.environ.get("PBL_TEST_DB_DSN")
|
||
if dsn:
|
||
m = re.match(r"mysql(?:\+\w+)?://([^:]+):([^@]*)@([^:/]+):?(\d+)?/(.+)", dsn)
|
||
if not m:
|
||
raise ValueError("PBL_TEST_DB_DSN 格式非法: %s" % dsn)
|
||
return dict(host=m.group(3), port=int(m.group(4) or 3306),
|
||
user=m.group(1), password=m.group(2), database=m.group(5))
|
||
if os.environ.get("PBL_TEST_DB_HOST"):
|
||
return dict(host=os.environ["PBL_TEST_DB_HOST"],
|
||
port=int(os.environ.get("PBL_TEST_DB_PORT", 3306)),
|
||
user=os.environ.get("PBL_TEST_DB_USER", "root"),
|
||
password=os.environ.get("PBL_TEST_DB_PASSWORD", ""),
|
||
database=os.environ.get("PBL_TEST_DB_NAME", "pbl_test"))
|
||
return dict(host="127.0.0.1", port=3306, user="root", password="",
|
||
database="pbl_test")
|
||
|
||
|
||
def connect(cfg):
|
||
"""返回 (conn, driver_name);驱动缺失或连不上抛异常。"""
|
||
try:
|
||
import pymysql # type: ignore
|
||
conn = pymysql.connect(host=cfg["host"], port=cfg["port"], user=cfg["user"],
|
||
password=cfg["password"], database=cfg["database"],
|
||
charset="utf8mb4", autocommit=True,
|
||
connect_timeout=5)
|
||
return conn, "pymysql"
|
||
except ImportError:
|
||
pass
|
||
try:
|
||
import MySQLdb # type: ignore
|
||
conn = MySQLdb.connect(host=cfg["host"], port=cfg["port"], user=cfg["user"],
|
||
passwd=cfg["password"], db=cfg["database"],
|
||
charset="utf8mb4")
|
||
conn.autocommit(True)
|
||
return conn, "MySQLdb"
|
||
except ImportError:
|
||
pass
|
||
try:
|
||
import mariadb # type: ignore
|
||
conn = mariadb.connect(host=cfg["host"], port=cfg["port"], user=cfg["user"],
|
||
password=cfg["password"], database=cfg["database"])
|
||
conn.autocommit = True
|
||
return conn, "mariadb"
|
||
except ImportError:
|
||
raise RuntimeError("NO_DRIVER: 未安装 pymysql / MySQLdb / mariadb 任一驱动")
|
||
|
||
|
||
def split_ddl(sql_text):
|
||
body = re.sub(r"/\*.*?\*/", " ", sql_text, flags=re.S)
|
||
stmts, buf = [], []
|
||
for line in body.splitlines():
|
||
i = line.find("--")
|
||
if i >= 0:
|
||
line = line[:i]
|
||
buf.append(line)
|
||
if line.rstrip().endswith(";"):
|
||
s = "\n".join(buf).strip()
|
||
if s and s != ";":
|
||
stmts.append(s.rstrip(";"))
|
||
buf = []
|
||
return [s for s in stmts if s.strip()]
|
||
|
||
|
||
# ------------------------------------------------------------------ 用例
|
||
def t_ddl_idempotent(cur):
|
||
"""T1: DDL 执行两遍均成功(CREATE TABLE IF NOT EXISTS 幂等)。"""
|
||
with open(DDL_PATH, "r", encoding="utf-8") as f:
|
||
stmts = split_ddl(f.read())
|
||
for rnd in (1, 2):
|
||
for s in stmts:
|
||
cur.execute(s)
|
||
log(len(stmts) == 4, "T1_DDL幂等",
|
||
"执行 %d 条语句 × 2 轮无异常" % len(stmts))
|
||
|
||
|
||
def t_tables_exist(cur):
|
||
"""T2: 4 张表全部存在,且引擎/字符集符合 mariadb 方言。"""
|
||
cur.execute("""SELECT TABLE_NAME, ENGINE, TABLE_COLLATION
|
||
FROM information_schema.TABLES
|
||
WHERE TABLE_SCHEMA = DATABASE()
|
||
AND TABLE_NAME IN ('pbl_blueprint_template','pbl_blueprint_ref',
|
||
'pbl_subobject_ext','pbl_ext_field_def')""")
|
||
rows = cur.fetchall()
|
||
names = sorted(r[0] for r in rows)
|
||
expect = sorted(['pbl_blueprint_template', 'pbl_blueprint_ref',
|
||
'pbl_subobject_ext', 'pbl_ext_field_def'])
|
||
engines = set(r[1] for r in rows)
|
||
log(names == expect and engines == {"InnoDB"}, "T2_四表存在",
|
||
"表=%s 引擎=%s" % (names, engines))
|
||
|
||
|
||
def t_no_foreign_key(cur):
|
||
"""T3: 4 张表零 FOREIGN KEY(Q-OPEN-3)。"""
|
||
cur.execute("""SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE
|
||
WHERE TABLE_SCHEMA = DATABASE()
|
||
AND REFERENCED_TABLE_NAME IS NOT NULL
|
||
AND TABLE_NAME LIKE 'pbl\\_%'""")
|
||
n = cur.fetchone()[0]
|
||
log(n == 0, "T3_零外键", "information_schema 外键约束数=%d" % n)
|
||
|
||
|
||
def t_unique_key_tenant_first(cur):
|
||
"""T4: 每个 UNIQUE 索引首列均为 tenant_key。"""
|
||
cur.execute("""SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX
|
||
FROM information_schema.STATISTICS
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE 'pbl\\_%'
|
||
AND NON_UNIQUE = 0 AND INDEX_NAME <> 'PRIMARY'
|
||
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX""")
|
||
bad = []
|
||
seen = {}
|
||
for t, ix, col, seq in cur.fetchall():
|
||
if int(seq) == 1:
|
||
seen[(t, ix)] = col
|
||
for (t, ix), col in seen.items():
|
||
if col != "tenant_key":
|
||
bad.append("%s.%s 首列=%s" % (t, ix, col))
|
||
log(seen and not bad, "T4_唯一键tenant打头",
|
||
"唯一索引 %d 个,违规 %s" % (len(seen), bad or "无"))
|
||
|
||
|
||
def t_platform_template_null_tenant(cur):
|
||
"""T5: 平台公共模板 tenant_id IS NULL 可插入,且 tenant_key='' 唯一去重生效。"""
|
||
cur.execute("DELETE FROM pbl_blueprint_template WHERE template_code LIKE 'M1B_T5%%'")
|
||
cur.execute(
|
||
"""INSERT INTO pbl_blueprint_template
|
||
(tenant_key, tenant_id, template_code, template_name, scope, status,
|
||
version, is_default, usage_count, created_at, updated_at)
|
||
VALUES (%s, NULL, %s, %s, 'platform', 'published', '1.0.0', 0, 0, %s, %s)""",
|
||
(PLATFORM_KEY, "M1B_T5_PLATFORM", "平台公共模板", NOW, NOW))
|
||
cur.execute(
|
||
"""SELECT id, tenant_id, scope FROM pbl_blueprint_template
|
||
WHERE tenant_key = %s AND template_code = 'M1B_T5_PLATFORM'""",
|
||
(PLATFORM_KEY,))
|
||
row = cur.fetchone()
|
||
ok_null = row is not None and row[1] is None and row[2] == "platform"
|
||
|
||
dup = False
|
||
try:
|
||
cur.execute(
|
||
"""INSERT INTO pbl_blueprint_template
|
||
(tenant_key, tenant_id, template_code, template_name, scope, status,
|
||
version, is_default, usage_count, created_at, updated_at)
|
||
VALUES (%s, NULL, %s, %s, 'platform', 'published', '1.0.0', 0, 0, %s, %s)""",
|
||
(PLATFORM_KEY, "M1B_T5_PLATFORM", "平台公共模板重复", NOW, NOW))
|
||
except Exception:
|
||
dup = True
|
||
log(ok_null and dup, "T5_平台模板NULL租户",
|
||
"tenant_id IS NULL 插入=%s;同 tenant_key+code 重复插入被唯一键拒绝=%s"
|
||
% (ok_null, dup))
|
||
|
||
|
||
def t_tenant_isolation(cur):
|
||
"""T6: 同 template_code 在不同租户下可共存(tenant_key 打头唯一键)。"""
|
||
cur.execute("DELETE FROM pbl_blueprint_template WHERE template_code = 'M1B_T6_SAME'")
|
||
for tk, tid in ((TENANT_A, TENANT_A), (TENANT_B, TENANT_B)):
|
||
cur.execute(
|
||
"""INSERT INTO pbl_blueprint_template
|
||
(tenant_key, tenant_id, template_code, template_name, scope, status,
|
||
version, is_default, usage_count, created_at, updated_at)
|
||
VALUES (%s, %s, 'M1B_T6_SAME', %s, 'tenant', 'draft', '1.0.0', 0, 0, %s, %s)""",
|
||
(tk, tid, "租户模板-" + tk, NOW, NOW))
|
||
cur.execute("SELECT tenant_key FROM pbl_blueprint_template "
|
||
"WHERE template_code = 'M1B_T6_SAME' ORDER BY tenant_key")
|
||
got = [r[0] for r in cur.fetchall()]
|
||
log(got == [TENANT_A, TENANT_B], "T6_租户隔离",
|
||
"同 code 跨租户共存,tenant_key=%s" % got)
|
||
|
||
|
||
def t_subobject_ext_kv(cur):
|
||
"""T7: 子对象扩展 KV 写入/读取/唯一键覆盖(基表零改动)。"""
|
||
cur.execute("DELETE FROM pbl_subobject_ext WHERE subobject_id LIKE 'M1B_T7%%'")
|
||
cur.execute(
|
||
"""INSERT INTO pbl_subobject_ext
|
||
(tenant_key, tenant_id, blueprint_id, subobject_type, subobject_id,
|
||
ext_key, ext_value, ext_value_type, source, status, created_at, updated_at)
|
||
VALUES (%s, %s, 900001, 'driving_question', 'M1B_T7_OBJ',
|
||
'difficulty', '3', 'number', 'manual', 'active', %s, %s)""",
|
||
(TENANT_A, TENANT_A, NOW, NOW))
|
||
cur.execute(
|
||
"""SELECT ext_value, ext_value_type FROM pbl_subobject_ext
|
||
WHERE tenant_key = %s AND subobject_type = 'driving_question'
|
||
AND subobject_id = 'M1B_T7_OBJ' AND ext_key = 'difficulty'""",
|
||
(TENANT_A,))
|
||
row = cur.fetchone()
|
||
ok_read = row is not None and row[0] == "3" and row[1] == "number"
|
||
|
||
dup = False
|
||
try:
|
||
cur.execute(
|
||
"""INSERT INTO pbl_subobject_ext
|
||
(tenant_key, tenant_id, blueprint_id, subobject_type, subobject_id,
|
||
ext_key, ext_value, ext_value_type, source, status, created_at, updated_at)
|
||
VALUES (%s, %s, 900001, 'driving_question', 'M1B_T7_OBJ',
|
||
'difficulty', '5', 'number', 'manual', 'active', %s, %s)""",
|
||
(TENANT_A, TENANT_A, NOW, NOW))
|
||
except Exception:
|
||
dup = True
|
||
log(ok_read and dup, "T7_子对象扩展KV",
|
||
"读回=%s;同 (tenant,type,obj,key) 重复插入被拒=%s" % (ok_read, dup))
|
||
|
||
|
||
def t_ext_field_def_platform_override(cur):
|
||
"""T8: 平台公共字段定义 + 租户级覆盖(同 field_key 两条并存,读取顺序 tenant->platform)。"""
|
||
cur.execute("DELETE FROM pbl_ext_field_def WHERE field_key LIKE 'm1b_t8%%'")
|
||
cur.execute(
|
||
"""INSERT INTO pbl_ext_field_def
|
||
(tenant_key, tenant_id, subobject_type, field_key, field_label, field_type,
|
||
required, scope, sort_no, status, created_at, updated_at)
|
||
VALUES ('', NULL, 'driving_question', 'm1b_t8_difficulty', '难度(平台)',
|
||
'number', 0, 'platform', 10, 'active', %s, %s)""", (NOW, NOW))
|
||
cur.execute(
|
||
"""INSERT INTO pbl_ext_field_def
|
||
(tenant_key, tenant_id, subobject_type, field_key, field_label, field_type,
|
||
required, scope, sort_no, status, created_at, updated_at)
|
||
VALUES (%s, %s, 'driving_question', 'm1b_t8_difficulty', '难度(租户覆盖)',
|
||
'number', 1, 'tenant', 10, 'active', %s, %s)""",
|
||
(TENANT_A, TENANT_A, NOW, NOW))
|
||
cur.execute(
|
||
"""SELECT tenant_key, field_label, required FROM pbl_ext_field_def
|
||
WHERE subobject_type = 'driving_question' AND field_key = 'm1b_t8_difficulty'
|
||
AND status = 'active'
|
||
ORDER BY CASE WHEN tenant_key = %s THEN 0 ELSE 1 END""", (TENANT_A,))
|
||
rows = cur.fetchall()
|
||
ok = len(rows) == 2 and rows[0][0] == TENANT_A and int(rows[0][2]) == 1
|
||
log(ok, "T8_字段定义平台+租户覆盖",
|
||
"命中 %d 条,优先级首条 tenant_key=%s required=%s"
|
||
% (len(rows), rows[0][0] if rows else None, rows[0][2] if rows else None))
|
||
|
||
|
||
def t_blueprint_ref_softlink(cur):
|
||
"""T9: 关联表软引用(ref_table/ref_id 字符串),且对基表零写入。"""
|
||
cur.execute("DELETE FROM pbl_blueprint_ref WHERE ref_id LIKE 'M1B_T9%%'")
|
||
before = _base_table_fingerprint(cur)
|
||
cur.execute(
|
||
"""INSERT INTO pbl_blueprint_ref
|
||
(tenant_key, tenant_id, blueprint_id, ref_type, ref_table, ref_id, ref_name,
|
||
relation_kind, sort_no, status, created_at, updated_at)
|
||
VALUES (%s, %s, 900001, 'world', 'world', 'M1B_T9_WORLD', '测试世界',
|
||
'reference', 1, 'active', %s, %s)""",
|
||
(TENANT_A, TENANT_A, NOW, NOW))
|
||
cur.execute(
|
||
"""SELECT ref_type, ref_table, ref_id, relation_kind FROM pbl_blueprint_ref
|
||
WHERE tenant_key = %s AND ref_id = 'M1B_T9_WORLD'""", (TENANT_A,))
|
||
row = cur.fetchone()
|
||
after = _base_table_fingerprint(cur)
|
||
ok = (row is not None and row[0] == "world" and row[1] == "world"
|
||
and row[3] == "reference" and before == after)
|
||
log(ok, "T9_关联表软引用零基表写入",
|
||
"软引用行=%s;基表指纹前后一致=%s" % (row is not None, before == after))
|
||
|
||
|
||
def _base_table_fingerprint(cur):
|
||
"""基表存在性+行数指纹(若基表不存在则记为 absent,不影响判定)。"""
|
||
fp = {}
|
||
for t in ("world", "scene", "entity", "script"):
|
||
try:
|
||
cur.execute(
|
||
"SELECT COUNT(*) FROM information_schema.TABLES "
|
||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s", (t,))
|
||
if cur.fetchone()[0] == 0:
|
||
fp[t] = "absent"
|
||
continue
|
||
cur.execute("SELECT COUNT(*) FROM `%s`" % t)
|
||
fp[t] = cur.fetchone()[0]
|
||
except Exception:
|
||
fp[t] = "error"
|
||
return json.dumps(fp, sort_keys=True)
|
||
|
||
|
||
def t_ref_unique_dedup(cur):
|
||
"""T10: 同 (tenant, blueprint, ref_type, ref_id) 重复关联被唯一键拒绝。"""
|
||
dup = False
|
||
try:
|
||
cur.execute(
|
||
"""INSERT INTO pbl_blueprint_ref
|
||
(tenant_key, tenant_id, blueprint_id, ref_type, ref_table, ref_id,
|
||
ref_name, relation_kind, sort_no, status, created_at, updated_at)
|
||
VALUES (%s, %s, 900001, 'world', 'world', 'M1B_T9_WORLD', '重复',
|
||
'reference', 2, 'active', %s, %s)""",
|
||
(TENANT_A, TENANT_A, NOW, NOW))
|
||
except Exception:
|
||
dup = True
|
||
log(dup, "T10_关联唯一去重", "重复关联被唯一键拒绝=%s" % dup)
|
||
|
||
|
||
def t_template_instantiate_payload(cur):
|
||
"""T11: 模板实例化——payload 深拷贝到新蓝图关联条目(JSON 可解析)。"""
|
||
payload = json.dumps({"driving_question": [{"title": "如何减少碳排", "difficulty": 3}],
|
||
"artifact_def": [{"code": "A1"}]}, ensure_ascii=False)
|
||
cur.execute("DELETE FROM pbl_blueprint_template WHERE template_code = 'M1B_T11_TPL'")
|
||
cur.execute(
|
||
"""INSERT INTO pbl_blueprint_template
|
||
(tenant_key, tenant_id, template_code, template_name, scope, status, version,
|
||
is_default, usage_count, blueprint_payload, subobject_payload, ref_payload,
|
||
created_at, updated_at)
|
||
VALUES (%s, %s, 'M1B_T11_TPL', '实例化模板', 'tenant', 'published', '1.0.0',
|
||
0, 0, %s, %s, %s, %s, %s)""",
|
||
(TENANT_A, TENANT_A, payload, payload, "[]", NOW, NOW))
|
||
cur.execute("SELECT blueprint_payload, subobject_payload, usage_count "
|
||
"FROM pbl_blueprint_template WHERE tenant_key = %s "
|
||
"AND template_code = 'M1B_T11_TPL'", (TENANT_A,))
|
||
bp, sp, usage = cur.fetchone()
|
||
ok_json = isinstance(json.loads(bp), dict) and isinstance(json.loads(sp), dict)
|
||
cur.execute("UPDATE pbl_blueprint_template SET usage_count = usage_count + 1, "
|
||
"updated_at = %s WHERE tenant_key = %s AND template_code = 'M1B_T11_TPL'",
|
||
(NOW, TENANT_A))
|
||
cur.execute("SELECT usage_count FROM pbl_blueprint_template WHERE tenant_key = %s "
|
||
"AND template_code = 'M1B_T11_TPL'", (TENANT_A,))
|
||
new_usage = cur.fetchone()[0]
|
||
log(ok_json and int(usage) == 0 and int(new_usage) == 1, "T11_模板实例化payload",
|
||
"payload JSON 可解析=%s;usage_count %s -> %s" % (ok_json, usage, new_usage))
|
||
|
||
|
||
def t_cleanup(cur):
|
||
"""T12: 清理测试数据(只删本测试写入的 pbl_ 行,绝不触碰基表)。"""
|
||
n = 0
|
||
for sql in (
|
||
"DELETE FROM pbl_blueprint_template WHERE template_code LIKE 'M1B_T%'",
|
||
"DELETE FROM pbl_subobject_ext WHERE subobject_id LIKE 'M1B_T%'",
|
||
"DELETE FROM pbl_ext_field_def WHERE field_key LIKE 'm1b_t8%'",
|
||
"DELETE FROM pbl_blueprint_ref WHERE ref_id LIKE 'M1B_T%'",
|
||
):
|
||
rc = cur.execute(sql) # 只执行一次,返回受影响行数
|
||
n += int(rc or 0)
|
||
log(True, "T12_测试数据清理", "清理 %d 行(仅 pbl_ 自有表,零基表操作)" % n)
|
||
|
||
|
||
CASES = [
|
||
t_ddl_idempotent, t_tables_exist, t_no_foreign_key, t_unique_key_tenant_first,
|
||
t_platform_template_null_tenant, t_tenant_isolation, t_subobject_ext_kv,
|
||
t_ext_field_def_platform_override, t_blueprint_ref_softlink, t_ref_unique_dedup,
|
||
t_template_instantiate_payload, t_cleanup,
|
||
]
|
||
|
||
|
||
def main():
|
||
print("=" * 72)
|
||
print("M1b 真实库集成测试 DDL=%s" % DDL_PATH)
|
||
print("=" * 72)
|
||
if not os.path.exists(DDL_PATH):
|
||
print("FATAL: 未找到 sql/m1b_ddl.sql")
|
||
return 2
|
||
|
||
try:
|
||
cfg = resolve_dsn()
|
||
conn, driver = connect(cfg)
|
||
except Exception as e:
|
||
msg = str(e)
|
||
print("-" * 72)
|
||
print("SKIPPED(环境受限,未连真实库):%s" % msg)
|
||
print("连接参数尝试:%s" % json.dumps(
|
||
{k: v for k, v in (resolve_dsn() or {}).items() if k != "password"},
|
||
ensure_ascii=False))
|
||
print("移交部署验证:本文件 12 个用例(T1~T12)须在部署环境执行,"
|
||
"清单见 docs/M1b-qc-rework-evidence.md §5。")
|
||
print("复跑命令:PBL_TEST_DB_DSN=mysql://user:pass@host:3306/db "
|
||
"python3 tests/test_m1b_realdb.py")
|
||
for c in CASES:
|
||
skip(c.__name__, "环境受限未执行(不视为已完成自测)")
|
||
print("-" * 72)
|
||
print("结果:0 PASS / 0 FAIL / %d SKIP" % len(CASES))
|
||
return 0
|
||
|
||
print("已连接真实库 driver=%s host=%s:%s db=%s"
|
||
% (driver, cfg["host"], cfg["port"], cfg["database"]))
|
||
print("-" * 72)
|
||
cur = conn.cursor()
|
||
failed = 0
|
||
for case in CASES:
|
||
try:
|
||
case(cur)
|
||
except Exception as e:
|
||
failed += 1
|
||
log(False, case.__name__, "异常 %s\n%s" % (e, traceback.format_exc()))
|
||
try:
|
||
cur.close()
|
||
conn.close()
|
||
except Exception:
|
||
pass
|
||
|
||
total = len(RESULTS)
|
||
passed = sum(1 for _, ok, _ in RESULTS if ok is True)
|
||
fails = sum(1 for _, ok, _ in RESULTS if ok is False)
|
||
print("-" * 72)
|
||
print("结果:%d PASS / %d FAIL / 共 %d" % (passed, fails, total))
|
||
for name, ok, detail in RESULTS:
|
||
if ok is False:
|
||
print(" FAIL %s :: %s" % (name, detail))
|
||
return 1 if fails else 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|