|
|
@@ -0,0 +1,133 @@
|
|
|
+package stage2
|
|
|
+
|
|
|
+import (
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "testing"
|
|
|
+)
|
|
|
+
|
|
|
+// 本文件锁住「一条活动域 case 只能有一个形态」这件事。
|
|
|
+//
|
|
|
+// 起因是一次真实报数错误:形态分布报成 12 + 2 + 1 + 2 = 17,而该批只有 15 条 ——
|
|
|
+// SA-2 / SC-2 同时置了 `IntentionalStrict` 与 `ExpectActivityZeroWrite`,
|
|
|
+// 旧的 `activityChainVerdict` 把两个标签 `" + "` 拼起来,于是两条 case 各被数两次。
|
|
|
+//
|
|
|
+// 结论:**只要形态不是唯一的,"按形态计数"就永远需要一个加法校验,而人一定会忘。**
|
|
|
+
|
|
|
+// TestActivityShapePriority 锁住优先级:LEGACY-DEFECT > strict > 零写入 > equality。
|
|
|
+//
|
|
|
+// 每个组合都写成表,改优先级必须改这张表 —— 否则"哪条 case 被计成什么"会静默漂移。
|
|
|
+func TestActivityShapePriority(t *testing.T) {
|
|
|
+ tt := []struct {
|
|
|
+ name string
|
|
|
+ c Case
|
|
|
+ want ActivityChainShape
|
|
|
+ }{
|
|
|
+ {"什么都没声明 → equality", Case{}, ActivityShapeEquality},
|
|
|
+ {"只 零写入", Case{ExpectActivityZeroWrite: true}, ActivityShapeZeroWrite},
|
|
|
+ {"只 strict", Case{IntentionalStrict: true}, ActivityShapeIntentionalStrict},
|
|
|
+ {"strict + 零写入 → strict 优先", Case{IntentionalStrict: true, ExpectActivityZeroWrite: true},
|
|
|
+ ActivityShapeIntentionalStrict},
|
|
|
+ {"只 游标抽干 → legacy-defect",
|
|
|
+ Case{LegacyDrainsCursorAfterFirst: true}, ActivityShapeLegacyDefect},
|
|
|
+ {"只 按面额退 → legacy-defect",
|
|
|
+ Case{LegacyRefundTakesFullFace: true}, ActivityShapeLegacyDefect},
|
|
|
+ {"游标抽干 + strict → legacy-defect 压过 strict",
|
|
|
+ Case{LegacyDrainsCursorAfterFirst: true, IntentionalStrict: true}, ActivityShapeLegacyDefect},
|
|
|
+ {"按面额退 + 零写入 → legacy-defect 压过零写入",
|
|
|
+ Case{LegacyRefundTakesFullFace: true, ExpectActivityZeroWrite: true}, ActivityShapeLegacyDefect},
|
|
|
+ {"三者全置 → 只算 legacy-defect",
|
|
|
+ Case{LegacyDrainsCursorAfterFirst: true, LegacyRefundTakesFullFace: true,
|
|
|
+ IntentionalStrict: true, ExpectActivityZeroWrite: true}, ActivityShapeLegacyDefect},
|
|
|
+ }
|
|
|
+ for _, tc := range tt {
|
|
|
+ if got := tc.c.ActivityShape(); got != tc.want {
|
|
|
+ t.Errorf("%s:期望 %s,得到 %s", tc.name, tc.want.Label(), got.Label())
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TestActivityShapeLabelsAreDistinct 锁住标签互不为子串。
|
|
|
+//
|
|
|
+// 报告是纯文本,统计脚本只能靠 grep 标签。若 A 的标签是 B 的子串,
|
|
|
+// `grep -c A` 会把 B 那些行也算进去 —— 又是一次"数出来的数对不上"。
|
|
|
+func TestActivityShapeLabelsAreDistinct(t *testing.T) {
|
|
|
+ all := []ActivityChainShape{
|
|
|
+ ActivityShapeLegacyDefect, ActivityShapeIntentionalStrict,
|
|
|
+ ActivityShapeZeroWrite, ActivityShapeEquality,
|
|
|
+ }
|
|
|
+ for i, a := range all {
|
|
|
+ for j, b := range all {
|
|
|
+ if i == j {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if strings.Contains(b.Label(), a.Label()) {
|
|
|
+ t.Errorf("标签 %q 是 %q 的子串 —— grep 计数会串行", a.Label(), b.Label())
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TestActivityChainVerdictIsSingleLabel 是这次修复的**核心断言**:
|
|
|
+// 任意一条 case 的判定文案里,**恰好命中一个**形态标签。
|
|
|
+//
|
|
|
+// 旧实现下这条测试会在 SA-2 / SC-2 上失败(命中 2 个),而那正是报数错的根因。
|
|
|
+// 顺带锁住:文案里的标签必须就是 `ActivityShape()` 的结果,不允许"文案写 A、分类是 B"。
|
|
|
+func TestActivityChainVerdictIsSingleLabel(t *testing.T) {
|
|
|
+ all := []ActivityChainShape{
|
|
|
+ ActivityShapeLegacyDefect, ActivityShapeIntentionalStrict,
|
|
|
+ ActivityShapeZeroWrite, ActivityShapeEquality,
|
|
|
+ }
|
|
|
+ // 用真实注册表跑一遍:新加的 case 若同时置多个 flag,这里立刻红。
|
|
|
+ for _, c := range DefaultCases("") {
|
|
|
+ if !isActivityChainOp(c.Op) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ text := activityChainVerdict(c)
|
|
|
+ hits, hit := 0, ActivityShapeUnclassified
|
|
|
+ for _, s := range all {
|
|
|
+ if strings.Contains(text, s.Label()) {
|
|
|
+ hits++
|
|
|
+ hit = s
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if hits != 1 {
|
|
|
+ t.Errorf("case %q 的判定文案命中 %d 个形态标签(必须恰好 1 个):%s", c.Name, hits, text)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if hit != c.ActivityShape() {
|
|
|
+ t.Errorf("case %q:文案写的是 %s,但 ActivityShape() 是 %s", c.Name, hit.Label(), c.ActivityShape().Label())
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TestActivityShapeTallyAddsUp 锁住恒等式 Σ N == len(cases),并且未分类恒为 0。
|
|
|
+//
|
|
|
+// 断言对象是**真实注册表**而不是编造的样例:编造样例只能证明函数会加法,
|
|
|
+// 证明不了"这一批 case 现在记的账是对的"。
|
|
|
+func TestActivityShapeTallyAddsUp(t *testing.T) {
|
|
|
+ act := make([]Case, 0)
|
|
|
+ for _, c := range DefaultCases("") {
|
|
|
+ if isActivityChainOp(c.Op) {
|
|
|
+ act = append(act, c)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if len(act) == 0 {
|
|
|
+ t.Fatal("活动域 case 为 0 条 —— 注册表接线断了,后面每个数都不可信")
|
|
|
+ }
|
|
|
+ tally := ActivityShapeTally(act)
|
|
|
+ sum := 0
|
|
|
+ parts := make([]string, 0, len(tally))
|
|
|
+ for _, tc := range tally {
|
|
|
+ if tc.Shape == ActivityShapeUnclassified {
|
|
|
+ t.Errorf("出现未分类形态 %d 条 —— 有人加了 flag 却没接进 ActivityShape()", tc.N)
|
|
|
+ }
|
|
|
+ sum += tc.N
|
|
|
+ parts = append(parts, tc.Shape.Label()+" "+strconv.Itoa(tc.N))
|
|
|
+ }
|
|
|
+ // 打出来:这是**当前注册表**的真实分布,报告里那一行就是从这来的。
|
|
|
+ t.Logf("活动域形态分布:%d 条 = %s", len(act), strings.Join(parts, " + "))
|
|
|
+ if sum != len(act) {
|
|
|
+ t.Errorf("形态计数之和 %d ≠ 活动域 case 数 %d —— 分类不唯一(这正是 3a 报数错的成因)", sum, len(act))
|
|
|
+ }
|
|
|
+}
|