package main import ( "fmt" "testing" ) // 这些用例钉住的是 bounded.go 的三条性质:封顶、FIFO、以及 // 「add 的返回值就是查重结果」—— 后者让调用方能在一把锁里查重 + 登记。 func TestBoundedSetCapsSize(t *testing.T) { s := newBoundedIDSet(3) for _, id := range []string{"a", "b", "c", "d", "e"} { s.add(id) } if s.size() != 3 { t.Fatalf("上限之后 size 必须封顶,得到 %d —— 这正是泄露的反面", s.size()) } if s.has("a") || s.has("b") { t.Error("最早插入的两条应当被淘汰") } for _, id := range []string{"c", "d", "e"} { if !s.has(id) { t.Errorf("%s 应当还在", id) } } if s.evicted != 2 { t.Errorf("evicted 应为 2,得到 %d", s.evicted) } } func TestBoundedSetAddReportsFreshness(t *testing.T) { // 调用方(plugin.go 的两处去重)依赖这个返回值在同一把锁里完成 // 「查重 + 登记」。分两步做需要两次加锁,中间那个窗口正是原来的竞态。 s := newBoundedIDSet(10) if !s.add("m1") { t.Error("首次 add 应当返回 true(新加入)") } if s.add("m1") { t.Error("重复 add 应当返回 false(已存在)") } if s.size() != 1 { t.Errorf("重复 add 不该占额外位置,size=%d", s.size()) } } func TestBoundedSetFIFONotLRU(t *testing.T) { // Go 的 map 不保证遍历顺序,所以这里是显式的 FIFO 而不是 LRU。 // 这条用例把那个决定钉住:反复 has 不会让条目留得更久。 s := newBoundedIDSet(2) s.add("a") s.add("b") for i := 0; i < 5; i++ { s.has("a") // 在 LRU 语义下这会保住 a } s.add("c") if s.has("a") { t.Error("FIFO 语义下最早插入的 a 应当被淘汰,has() 不刷新活跃度") } if !s.has("b") || !s.has("c") { t.Error("b 与 c 应当都在") } } func TestBoundedSetReAddDoesNotRefreshPosition(t *testing.T) { // 重复 add 也不改变位置(FIFO 由首次插入决定)。写成「已存在时移到队尾」 // 会让一条被反复推送的邮件把别的条目挤出去。 s := newBoundedIDSet(2) s.add("a") s.add("b") s.add("a") // 已存在,位置不变 s.add("c") if s.has("a") { t.Error("a 是最早插入的,重复 add 不该救回它") } } func TestBoundedSetIgnoresEmptyID(t *testing.T) { // 空 mail_id 是「事件残缺」而不是「一封 id 为空的邮件」。 // 记住它会让第二封残缺事件被误判成重复。 s := newBoundedIDSet(5) if s.add("") { t.Error("空 id 不该被记住") } if s.has("") { t.Error("空 id 永远不算已见过") } if s.size() != 0 { t.Errorf("空 id 不该占位置,size=%d", s.size()) } } func TestBoundedSetNilSafe(t *testing.T) { // 构造函数失败或字段没初始化时不该 panic —— 去重是优化项, // 退化成「什么都不记得」(多几次重复投递)远好过插件崩溃。 var s *boundedIDSet if s.has("x") { t.Error("nil 上 has 应为 false") } if s.add("x") { t.Error("nil 上 add 应为 false") } if s.size() != 0 { t.Error("nil 上 size 应为 0") } } func TestBoundedSetIllegalLimitFallsBackToOne(t *testing.T) { // 上限非法时回落到 1 而不是 panic 或 0。 // 0 的后果最隐蔽:每次 add 之后立刻把自己淘汰掉 → 去重全失效且不报错。 for _, limit := range []int{0, -1, -100} { s := newBoundedIDSet(limit) s.add("a") if !s.has("a") { t.Errorf("limit=%d:刚加入的那条必须还在(回落到 1,而不是 0)", limit) } s.add("b") if s.size() != 1 { t.Errorf("limit=%d:size 应为 1,得到 %d", limit, s.size()) } } } func TestBoundedSetCompactsOrderSlice(t *testing.T) { // order 切片的底层数组不能随插入次数无限增长 —— 那正是这张表要修的病, // 只是换了个地方(map 有界了,切片没有)。 s := newBoundedIDSet(10) for i := 0; i < 10_000; i++ { s.add(fmt.Sprintf("mail-%d", i)) } if s.size() != 10 { t.Fatalf("size 应当封顶在 10,得到 %d", s.size()) } // 压实之后 order 的有效长度不该远大于上限 if live := len(s.order) - s.head; live > 10 { t.Errorf("order 有效长度 %d 超过上限 10", live) } if len(s.order) > 10*4 { t.Errorf("order 底层长度 %d 相对上限 10 增长失控(压实没生效)", len(s.order)) } if cap(s.order) > 10*8 { t.Errorf("order 容量 %d 相对上限 10 增长失控", cap(s.order)) } // 最新的必须还在,最老的必须没了 if !s.has("mail-9999") { t.Error("最新的那条必须还在") } if s.has("mail-0") { t.Error("最老的那条必须已被淘汰") } if s.evicted != 9990 { t.Errorf("evicted 应为 9990,得到 %d", s.evicted) } } func TestBoundedSetLimitMatchesNodeSide(t *testing.T) { // 与 Node 侧 lib/bounded.js 的 MAX_TRACKED_MAILS 取同一个数。 // 四个平台在同一套语义下运行,一侧偷偷调小会让「重复投递」只在那个平台出现。 if maxTrackedMails != 2000 { t.Errorf("maxTrackedMails 应为 2000(与 Node 侧一致),得到 %d", maxTrackedMails) } }