97_negative_test.sh
8.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#!/usr/bin/env bash
#
# `97_verify_sample.sql` 的**反向用例**:证明那五段断言**真的会报红**。
#
# 为什么必须有这个文件(AGENTS.md §1.1 推论 1):
# 一份"跑出来是空的"的校验,**不能**证明数据是对的——它同时兼容"数据确实没问题"
# 和"这条断言根本不可能有输出"两种情形,而这两种情形在终端上长得一模一样。
# 本地第一次跑 97 时,§2a 报了上万行(5.7 的 `int(11)` vs 8.0 的 `int`)——
# 那次是**假红**,但它顺带证明了 §2a 不是空转的。这个脚本把这类证据固定下来:
# 对每一段各造一个**必然命中**的破坏,断言它确实报出来,而且**报的是该段该报的东西**。
#
# ⚠️ 断言为什么用"特征串"而不是"输出非空":
# 本脚本会故意把库弄坏,mysql 会往 stderr 打 `ERROR 1146 ... connector_info doesn't exist`,
# 而下面每条都 `2>&1` 一起收——若只判"非空",那个 ERROR 就能把五条用例全部"喂饱",
# 于是五条都通过,而真实情况是五段断言一个都没响。所以特征串必须**只可能来自该段的输出行**:
# §1 → `zt_charge<TAB>connector_info`(ERROR 里是 `parking_cloud.connector_info`,不同)
# §2a → `<TAB>bigint<TAB>int<TAB>` (只有 §2a 印类型)
# §2b → `zz_negative_test_col` (破坏时新加的列名)
# §3/§4 → `不该有数据` / `抽样没落地` (只有这两段印这两个结论词)
# §5 → `utf8mb3` (只有 §5 印排序规则)
# 特征串选得够"窄",ERROR 行就不可能冒充它——这是**选特征串**的责任,不是靠
# 另外加一条"输出里不许出现 ERROR"来兜。
# (第一版真加了那条禁令,结果 A 用例永远失败:A 故意删表,§4 的 `COUNT(*)`
# 必然 `ERROR 1146`,而 §1 其实已经正确报出来了。那条禁令把"预期内的噪声"
# 和"这段没跑成"混为一谈——所以现在 ERROR 只作为提示打印,不作判据。)
#
# 用法(**只能对本地开发库跑**,它会改表;选 zt_charge 是因为重灌一次只要 2 秒):
# MYSQL_BIN=/opt/homebrew/opt/mysql-client/bin/mysql \
# DUMP_BIN=/opt/homebrew/opt/mysql-client/bin/mysqldump \
# TARGET_HOST=127.0.0.1 TARGET_USER=root TARGET_PASSWORD= \
# bash tests/97_negative_test.sh
# 依赖:目标库上已经有 SAMPLE=N 的抽样镜像(先跑 10_dump_legacy.sh dump && load)。
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MIG="$(cd "$HERE/.." && pwd)"
VERIFY="$MIG/97_verify_sample.sql"
DB="${DB:-zt_charge}"
[[ -f "$VERIFY" ]] || { echo "缺少 ${VERIFY}(先跑 tools/gen_migration_sql.py)" >&2; exit 1; }
MYSQL_BIN="${MYSQL_BIN:-mysql}"
command -v "$MYSQL_BIN" >/dev/null || { echo "缺少 mysql 客户端(${MYSQL_BIN})" >&2; exit 1; }
: "${TARGET_HOST:?请设置 TARGET_HOST}"
: "${TARGET_USER:?请设置 TARGET_USER}"
TARGET_DB="${TARGET_DB:-parking_cloud}"
TARGET_PORT="${TARGET_PORT:-3306}"
: "${TARGET_PASSWORD-}"
export MYSQL_PWD="$TARGET_PASSWORD"
mysql_() { "$MYSQL_BIN" --host="$TARGET_HOST" --port="$TARGET_PORT" \
--user="$TARGET_USER" --default-character-set=utf8mb4 "$@"; }
# 跑校验时加两个开关:
# --force —— 一段出错(比如 §4 撞到被删掉的表 `ERROR 1146`)**不要中止**,
# 否则 §5 永远不会执行,一份校验只报出前半截。
# -N —— 不输出列名。要的是"干净时**完全没有任何输出**",
# 有列名的话空结果集也会印一行表头,"空=通过"这个判读就不成立了。
mysql_verify() { mysql_ --force -N "$@"; }
pass=0; fail=0
ok() { printf ' \033[32m✅\033[0m %s\n' "$*"; pass=$((pass+1)); }
bad() { printf ' \033[31m❌\033[0m %s\n' "$*"; fail=$((fail+1)); }
# 还原:重新灌这一个库(dump 里是 DROP TABLE IF EXISTS + CREATE + INSERT,幂等)
#
# ⚠️ 重灌**只能**还原"dump 里有的那几张表"——它永远**不会**删掉多出来的东西。
# 所以破坏手法要挑**重灌能完全吃掉的**:DROP TABLE / ALTER(都被
# `DROP TABLE IF EXISTS` + `CREATE TABLE` 覆盖),**不要用 RENAME**——
# 改名会留下一张 dump 里没有的表(本文件的第一个版本就踩了,
# 在库里留了个 `connector_info_hidden`,而且失败路径上还没还原)。
restore() {
( cd "$MIG" && ONLY="$DB" SAMPLE="${SAMPLE:-500}" bash 10_dump_legacy.sh load >/dev/null 2>&1 )
}
# 基准:装载完整时 97 必须是**静默**的(否则后面的"报红"断言毫无意义)
baseline_must_be_silent() {
local out
out=$(mysql_verify < "$VERIFY" 2>&1) || true
if [[ -z "$out" ]]; then ok "基准:抽样镜像完整时 97 静默(§1~§5 全 0 行)"
else bad "基准就不干净,后面的断言没法判读:${out:0:300}"; return 1; fi
}
# $1=用例名 $2=该段输出里只可能由它产生的特征串 $3=破坏语句
#
# ⚠️ **失败路径也必须 restore**:不还原的话,后面每个用例都会因为上一条的破坏而连环失败,
# 于是看到的是"五条全错",真实原因却只有第一条——排查方向被带偏。
expect_red() {
local name="$1" want="$2" ddl="$3" out err
if ! err=$(mysql_ -e "$ddl" 2>&1); then
bad "${name}:破坏语句没执行成功(错误原文:${err})"
restore
return
fi
out=$(mysql_verify < "$VERIFY" 2>&1) || true
if [[ "$out" == *"$want"* ]]; then
ok "$name —— 报红了,且报的是该段该报的东西"
# ERROR 只提示、不判负:删表的用例里 §4 的 COUNT(*) 撞到它属预期,
# 而 §1 已经正确报出来了。把"预期内的噪声"当成失败会造出假故障。
if [[ "$out" == *ERROR* ]]; then
printf ' ⚠️ 校验输出里有 ERROR(多段校验是 `--force` 跑的,一段出错不中止其余):%s\n' \
"$(printf '%s' "$out" | grep -m1 'ERROR')"
fi
else
bad "${name} —— **没报红**!这段断言是空转的。期望输出含「${want}」,实际:${out:0:300}"
fi
restore
}
TAB=$'\t'
echo "== 起点:先把这一个库重灌成已知good状态 =="
# ⚠️ 这一步不是多余的:本脚本上一次如果在半途死掉(比如被 Ctrl-C),
# 库里会留着上一条破坏语句的效果,于是"基准"一上来就是脏的,
# 后面的用例也跟着连环失败——看到的全是假故障。先自愈,再开始。
restore
echo "== 基准 =="
baseline_must_be_silent
echo "== A. §1 表都在(把一张表删掉)=="
expect_red "§1 目标库缺表" "zt_charge${TAB}connector_info" \
"DROP TABLE \`$TARGET_DB\`.\`connector_info\`"
echo "== B. §2a 列类型(bigint → int,是真正改类型)=="
expect_red "§2a 列类型变了" "${TAB}bigint${TAB}int${TAB}" \
"ALTER TABLE \`$TARGET_DB\`.\`connector_info\` MODIFY COLUMN \`id\` int NOT NULL AUTO_INCREMENT"
echo "== C. §2b 目标库多出来的列 =="
expect_red "§2b 多了一列" "zz_negative_test_col" \
"ALTER TABLE \`$TARGET_DB\`.\`connector_info\` ADD COLUMN \`zz_negative_test_col\` int NULL"
echo "== D. §3/§4 —— 最值钱的一段,两个方向都要验 =="
# D1「不该有数据」:只搬结构的表(含密钥/口令)在抽样镜像里必须 0 行
expect_red "§3 只搬结构的表被灌进了数据" "不该有数据" \
"INSERT INTO \`$TARGET_DB\`.\`operator_info\` (\`operator_id\`,\`operator_name\`,\`operator_tell\`) VALUES ('zz','zz','zz')"
# D2「抽样没落地」:这张表源库明明有数据,必须 >= 1 行
expect_red "§3 抽样表一行都没落地" "抽样没落地" \
"DELETE FROM \`$TARGET_DB\`.\`connector_info\`"
echo "== E. §5 字符集(把一张表转回 utf8mb3)=="
expect_red "§5 有表不是 utf8mb4" "utf8mb3" \
"ALTER TABLE \`$TARGET_DB\`.\`connector_info\` CONVERT TO CHARACTER SET utf8mb3"
echo
echo "== 收尾:还原后再跑一次基准 =="
restore
baseline_must_be_silent
echo
if [[ "$fail" -eq 0 ]]; then
printf '\033[32m全部通过\033[0m ✅ (%d 项)—— 五段断言都能报红,不是空转\n' "$pass"
else
printf '\033[31m%d 项失败\033[0m ❌\n' "$fail"
exit 1
fi