|
@@ -813,19 +813,74 @@ _money ← _balance
|
|
|
|
|
|
|
|
### 13.2 存量影响面(`t_user_expense_school`,`balance > 0`,共 **566** 个账户)
|
|
### 13.2 存量影响面(`t_user_expense_school`,`balance > 0`,共 **566** 个账户)
|
|
|
|
|
|
|
|
|
|
+先定义两个口径,**别混**(这是本节唯一容易搞错的地方):
|
|
|
|
|
+
|
|
|
|
|
+- `raw` = 旧库列原值(`decimal(10,3)`,3 位)
|
|
|
|
|
+- `r0` = `ROUND(raw, 0)` —— legacy 的 `_balance`(第一层整数化)
|
|
|
|
|
+- `r2` = `ROUND(raw, 2)` —— **迁移后** target 读到的值(2 位)
|
|
|
|
|
+- **缺陷面** = `r0` 与 `raw` 的差(旧系统自己的问题,与迁移无关)
|
|
|
|
|
+- **差异面** = `r0` 与 `r2` 的差(割接后新旧系统会不会表现出不同)
|
|
|
|
|
+
|
|
|
| 形态 | 账户数 | 金额 | legacy 的行为 | target 的行为 |
|
|
| 形态 | 账户数 | 金额 | legacy 的行为 | target 的行为 |
|
|
|
|---|---:|---:|---|---|
|
|
|---|---:|---:|---|---|
|
|
|
-| `r0 = 0`(余额 < 0.5) | **166** | **¥18.177** | 判"余额不能≤0"、**拒绝退费**(零写入) | 照常退,退得出来 |
|
|
|
|
|
-| `r0 > raw`(分位 ≥ 0.5) | **95** | **¥21.155** | 多扣到整数,余额变**负数**(最多 −0.5) | 精确扣到 0.00 |
|
|
|
|
|
-| ‣ 其中"迁移已吸收" | 7 | ¥0.025 | 3 位→2 位收敛后 `r0 = r2`,**与 SP 无关** | — |
|
|
|
|
|
-| ‣ 纯属变量类型 | **88** | **¥21.130** | 同上 | 同上 |
|
|
|
|
|
-| `r0 < raw`(分位 < 0.5) | **37** | **¥6.986** | 少扣、**留正零头**,坏账被低估 | 精确扣到 0.00 |
|
|
|
|
|
-| `r0 = raw`(整数余额) | 268 | — | 与 target 逐分一致 | — |
|
|
|
|
|
-
|
|
|
|
|
-> ⚠ 那 **7 个**单独列出来,是因为它们是**另一个成因**:3 位小数向上进位恰好跨过整数
|
|
|
|
|
-> (如 `3.9995 → 4.00`),2 位迁移**已经把它修好了**。把它和"变量类型"混在一起讲,
|
|
|
|
|
-> 会让"我们修好了多少"和"旧 SP 本来就错多少"两笔账算不清。
|
|
|
|
|
-> 选 case 时那条 `ROUND(balance,0) == ROUND(ROUND(balance,2),0)` 的过滤,就是为了不让这两种成因互相污染。
|
|
|
|
|
|
|
+| `r0 = raw`(整数余额) | 268 | — | 与 target 逐分一致 | 同左 |
|
|
|
|
|
+| `r0 = 0`(`0 < raw < 0.5`) | **166** | **¥18.177** | 判"余额不能≤0"、**拒绝退费**(零写入) | 照常退,退得出来 |
|
|
|
|
|
+| `r0 > raw`(**多扣**) | **95** | **¥21.155** | 多扣到整数,余额变**负数**(最多 −0.5) | 精确扣到 0.00 |
|
|
|
|
|
+| `r0 < raw`(**少扣**) | **37** | **¥6.986** | 少扣、**留正零头**,坏账被低估 | 精确扣到 0.00 |
|
|
|
|
|
+
|
|
|
|
|
+加法闭合(**每一步都要能相加等于分母**,这是最廉价也最容易被跳过的检查):
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+166 + 95 + 37 + 268 = 566 ← 缺陷面
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+再按**差异面**切一次(同一批账户,换一个判据):
|
|
|
|
|
+
|
|
|
|
|
+| 差异面 | 账户数 | 差额 | 说明 |
|
|
|
|
|
+|---|---:|---:|---|
|
|
|
|
|
+| `r0 > r2`(legacy 比 target 多扣) | **88** | ¥21.130 | 95 减 7 |
|
|
|
|
|
+| `r0 < r2`(legacy 比 target 少扣) | **33** | ¥7.000 | 37 减 4 |
|
|
|
|
|
+| `r0 = r2` 但 `r0 ≠ raw`(**重合**) | **11** | — | `0.996 → 1.00` 这类 |
|
|
|
|
|
+| `r0 = 0`(拒绝 vs 退款) | 166 | — | 差异是"退不出 vs 退得出",不是金额 |
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+88 + 33 + 11 + 166 + 268 = 566 ← 差异面,同样必须闭合
|
|
|
|
|
+95 − 88 = 7,37 − 33 = 4,7 + 4 = 11 ← 两个口径的差恰好是"重合"那 11 个
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+那 **11 个重合**是什么:3 位小数**向上进位恰好跨过整数**,2 位迁移后 `r2` 就等于 legacy 算出来的
|
|
|
|
|
+`r0`,于是两侧扣的一样多。真实样本(已列出,可直接复跑):
|
|
|
|
|
+
|
|
|
|
|
+| userid | raw | `r2` | `r0` |
|
|
|
|
|
+|---|---:|---:|---:|
|
|
|
|
|
+| 48510 | 0.996 | 1.00 | 1 |
|
|
|
|
|
+| 66449 | 0.999 | 1.00 | 1 |
|
|
|
|
|
+| 66743 | 0.998 | 1.00 | 1 |
|
|
|
|
|
+| 27676 | 1.999 | 2.00 | 2 |
|
|
|
|
|
+| 67983 | 7.998 | 8.00 | 8 |
|
|
|
|
|
+| 68584 | 99.996 | 100.00 | 100 |
|
|
|
|
|
+| 47613 | 301.996 | 302.00 | 302 |
|
|
|
|
|
+
|
|
|
|
|
+复跑判据(**注意不是** `ROUND(balance,0) <> ROUND(ROUND(balance,2),0)`,那个在全库是 0 行):
|
|
|
|
|
+
|
|
|
|
|
+```sql
|
|
|
|
|
+-- 多扣侧的重合:r0 > raw(缺陷面里)但 r0 = r2(差异面里没有)
|
|
|
|
|
+SELECT userid, balance FROM t_user_expense_school
|
|
|
|
|
+ WHERE balance > 0
|
|
|
|
|
+ AND ROUND(balance,0) > balance -- raw 口径:整数化后更大
|
|
|
|
|
+ AND ROUND(balance,0) = ROUND(balance,2); -- 但 2 位口径下已重合
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> ⚠ 两个坑,都是我自己踩过的:
|
|
|
|
|
+>
|
|
|
|
|
+> 1. **举例用了 4 位小数**(早先写成 `3.9995 → 4.00`)。旧列是 `decimal(10,3)`,**存不下 4 位** ——
|
|
|
|
|
+> 例子本身就不成立,等于用了一个真实数据里不存在的值域。真实的形状是 `0.996 → 1.00`。
|
|
|
|
|
+> 2. **把"选 case 的过滤条件"说成了"标识这 11 个"**。`ROUND(balance,0) = ROUND(ROUND(balance,2),0)`
|
|
|
|
|
+> 在真实数据上**对所有账户都成立**(要让两者不等,分位必须落在 `[0.495, 0.5)`,全库 0 个),
|
|
|
|
|
+> 所以它不能用来找重合账户 —— 它只是当时选 case 时"确认不会混成因"的一个**始终为真**的检查。
|
|
|
|
|
+>
|
|
|
|
|
+> 教训:**引用一个判据时,要说清它筛的是什么、以及在真实数据上会不会全真/全假。**
|
|
|
|
|
+> 一个恒真的过滤条件看起来像"检查过",实际什么都没筛。
|
|
|
|
|
|
|
|
### 13.3 断言形态:**per-side + 共同不变量**
|
|
### 13.3 断言形态:**per-side + 共同不变量**
|
|
|
|
|
|
|
@@ -859,7 +914,7 @@ IF(_badDate > 0) THEN ... ELSE UPDATE ... `balance` - inputMoney ... END IF;
|
|
|
|
|
|
|
|
### 13.5 case 与分母
|
|
### 13.5 case 与分母
|
|
|
|
|
|
|
|
-`DefaultCases` **25 → 34**(C2 共 9 条):
|
|
|
|
|
|
|
+`DefaultCases` **25 → 35**(C2 共 10 条):
|
|
|
|
|
|
|
|
| case | 余额 | 请求 | 结论 |
|
|
| case | 余额 | 请求 | 结论 |
|
|
|
|---|---:|---:|---|
|
|
|---|---:|---:|---|
|
|
@@ -872,11 +927,31 @@ IF(_badDate > 0) THEN ... ELSE UPDATE ... `balance` - inputMoney ... END IF;
|
|
|
| C2-6 | 699.980 | 699.99 | 缝区间:`699.99 > 700` 为假 ⇒ **连分支都不同** |
|
|
| C2-6 | 699.980 | 699.99 | 缝区间:`699.99 > 700` 为假 ⇒ **连分支都不同** |
|
|
|
| C2-8 | 无校区行 | 100.00 | legacy 静默 `success=0` / target 3102 |
|
|
| C2-8 | 无校区行 | 100.00 | legacy 静默 `success=0` / target 3102 |
|
|
|
| C2-9 | 5000.00 | 300.00×2 | 幂等:target 扣 1 次、legacy 扣 2 次 |
|
|
| C2-9 | 5000.00 | 300.00×2 | 幂等:target 扣 1 次、legacy 扣 2 次 |
|
|
|
|
|
+| **C2-10** | **4950.00** | 5300.00 | **活动失效**:整数余额 + 自营 ⇒ 两侧**一致**,且本校区与全机构池都置 4 |
|
|
|
|
|
|
|
|
**C2-7(E)不在其中**,只在 `dualrun stage2 -probe` 里跑:它靠"legacy 侧余额置 NULL"构造,
|
|
**C2-7(E)不在其中**,只在 `dualrun stage2 -probe` 里跑:它靠"legacy 侧余额置 NULL"构造,
|
|
|
而 target 的列是 `not null default 0.00`、**NULL 存不下**,所以第二侧**不同构**。
|
|
而 target 的列是 `not null default 0.00`、**NULL 存不下**,所以第二侧**不同构**。
|
|
|
"两侧都拒绝"这个绿的含金量是 0(换任何一条余额为 0 的记录都能绿),算进分母等于用必然绿的 case 抬通过率。
|
|
"两侧都拒绝"这个绿的含金量是 0(换任何一条余额为 0 的记录都能绿),算进分母等于用必然绿的 case 抬通过率。
|
|
|
|
|
|
|
|
|
|
+#### C2-10 为什么必须单独存在(即使 SQL 与 cleared_balance 逐字相同)
|
|
|
|
|
+
|
|
|
|
|
+SP9 里那段 `tmp_user_activity` + `UPDATE t_courses_activity_signed SET status=4` 与
|
|
|
|
|
+`sp_user_expense_school_cleared_balance` **逐字相同**。但有两条理由不能"因为一样就不测":
|
|
|
|
|
+
|
|
|
|
|
+1. **执行位置不同**:清零在写流水**之前**跑这段,退费在写流水**之后**跑。
|
|
|
|
|
+ 事务内顺序不同 ⇒ 失败时的终态组合不同("流水写了、报名没失效"只有这一支才可能)。
|
|
|
|
|
+2. **复制粘贴的 SQL 最怕改了一个字**:同一段逻辑出现在两处,就该在两处各验一次 ——
|
|
|
|
|
+ 否则哪天有人在 c2 那份里手滑改了 `campusid IN (x, 0)` 的括号,没人会发现。
|
|
|
|
|
+
|
|
|
|
|
+它是**第二条 equality case**(整数余额 + 有可失效报名):legacy `_balance=4950`、
|
|
|
|
|
+`_badDate=round0(5300−4950)=350`;target `refunded=4950`、`badDebt=350` —— 逐分相同。
|
|
|
|
|
+
|
|
|
|
|
+> ⚠ **活动侧必然是种子,资金侧才是真实构型。** 实测 target 侧活动域是空的
|
|
|
|
|
+> (`t_courses_activity`=0、`t_courses_activity_signed`=0;legacy 侧 90 / 1288),
|
|
|
|
|
+> 所以**任何真实报名都只有 legacy 有**、target 看不到。要两侧看到同一批活动,只能像 C1 那样
|
|
|
|
|
+> 在事务内种(`C1ActivityPlan`),并用"旧库该用户没有别的可失效报名"这条前置断言钉住有效性。
|
|
|
|
|
+> 这不是偷懒,是活动域未迁移的必然结果;想改成"用真实报名",得先迁活动域。
|
|
|
|
|
+
|
|
|
## 14. `Assertion.Skipped`:断言的第三态
|
|
## 14. `Assertion.Skipped`:断言的第三态
|
|
|
|
|
|
|
|
`Diff.Skipped` 打 `—`(§ 里已述)之后,**断言**也需要同一个第三态,否则会出现
|
|
`Diff.Skipped` 打 `—`(§ 里已述)之后,**断言**也需要同一个第三态,否则会出现
|
|
@@ -891,6 +966,36 @@ IF(_badDate > 0) THEN ... ELSE UPDATE ... `balance` - inputMoney ... END IF;
|
|
|
所以每个 `Skipped` 的产生条件都必须是"从 case 的构造就能看出结构上不适用",
|
|
所以每个 `Skipped` 的产生条件都必须是"从 case 的构造就能看出结构上不适用",
|
|
|
**不能**是"实测值和我预期的不一样,所以跳过"。
|
|
**不能**是"实测值和我预期的不一样,所以跳过"。
|
|
|
|
|
|
|
|
|
|
+### 14.1 闸:`Skipped` 不许把整条 case 掏空
|
|
|
|
|
+
|
|
|
|
|
+`FailCount` 跳过 `Skipped` 之后,出现了一个**新的空过通道**:
|
|
|
|
|
+
|
|
|
|
|
+> **一条 case 的全部断言都是 `Skipped`** ⇒ 0 FAIL 的 PASS。
|
|
|
|
|
+> 分母没变(case 还在),但分子全是空的 —— 这是**分母收缩的又一个变种**。
|
|
|
|
|
+
|
|
|
|
|
+它可能真的发生:有人给某条 case 改了构型,把所有断言都变成结构性不适用,报告照样全绿。
|
|
|
|
|
+
|
|
|
|
|
+所以 `RunOne` 末尾加了一道闸 `CaseResult.EnsureNotAllSkipped()`:
|
|
|
|
|
+
|
|
|
|
|
+| 情形 | 处置 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| 有 ≥1 条非 `Skipped` 断言 | 放行 |
|
|
|
|
|
+| 断言一条都没有 | **FATAL**(退出码 3) |
|
|
|
|
|
+| 断言全 `Skipped`(**即使 Pass 全是 true**) | **FATAL**(退出码 3) |
|
|
|
|
|
+| 断言全 `Skipped` 但 Diff 里有非 `Skipped` 比对 | **仍然 FATAL** |
|
|
|
|
|
+
|
|
|
|
|
+报错而不记 FAIL 的理由:这不是"行为不对",是"这条 case 什么都没测"——
|
|
|
|
|
+与 `-case` 匹配不到、分母收缩同级,属于**测试基础设施失效**。
|
|
|
|
|
+记 FAIL 会被当成"待修的红"排进队列,而它根本不该被容忍。
|
|
|
|
|
+
|
|
|
|
|
+于是报数从**六个数**变成**七个数**:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+hash / DefaultCases / 实跑 / PASS / FAIL / 崩溃 / Skipped
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`Skipped` 必须一起报,否则"PASS 34"和"PASS 34 但有 60 项结构性不适用"看起来一样。
|
|
|
|
|
+
|
|
|
## 15. 一次真实的连接泄漏(34 条 case 才暴露)
|
|
## 15. 一次真实的连接泄漏(34 条 case 才暴露)
|
|
|
|
|
|
|
|
`RunOne` 原来每个 case 打开 **5 个** gorm 句柄(legacy / target / 天然基线 / 残留检查 ×2),
|
|
`RunOne` 原来每个 case 打开 **5 个** gorm 句柄(legacy / target / 天然基线 / 残留检查 ×2),
|