vacuum-filter.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import { describe, expect, test } from "vitest";
  2. import { VacuumFilter } from "@/lib/vacuum-filter/vacuum-filter";
  3. describe("VacuumFilter", () => {
  4. test("add/has/delete 基本语义正确", () => {
  5. const vf = new VacuumFilter({
  6. maxItems: 1000,
  7. fingerprintBits: 32,
  8. maxKickSteps: 500,
  9. seed: "unit-test-seed",
  10. });
  11. expect(vf.has("k1")).toBe(false);
  12. expect(vf.add("k1")).toBe(true);
  13. expect(vf.has("k1")).toBe(true);
  14. expect(vf.delete("k1")).toBe(true);
  15. expect(vf.has("k1")).toBe(false);
  16. // 删除不存在的 key
  17. expect(vf.delete("k1")).toBe(false);
  18. });
  19. test("高负载下插入与查询稳定(无假阴性)", () => {
  20. const n = 20_000;
  21. const vf = new VacuumFilter({
  22. maxItems: n,
  23. fingerprintBits: 32,
  24. maxKickSteps: 1000,
  25. seed: "unit-test-high-load",
  26. });
  27. for (let i = 0; i < n; i++) {
  28. const ok = vf.add(`key_${i}`);
  29. expect(ok).toBe(true);
  30. }
  31. for (let i = 0; i < n; i++) {
  32. expect(vf.has(`key_${i}`)).toBe(true);
  33. }
  34. // 删除一小部分(碰撞概率极低;使用 32-bit fingerprint 避免测试随机性)
  35. for (let i = 0; i < 200; i++) {
  36. expect(vf.delete(`key_${i}`)).toBe(true);
  37. expect(vf.has(`key_${i}`)).toBe(false);
  38. }
  39. });
  40. test("插入失败必须回滚(不丢元素,不引入假阴性)", () => {
  41. const vf = new VacuumFilter({
  42. maxItems: 10,
  43. fingerprintBits: 32,
  44. maxKickSteps: 50,
  45. seed: "unit-test-rollback-on-failure",
  46. });
  47. const inserted: string[] = [];
  48. let failed = false;
  49. for (let i = 0; i < 5000; i++) {
  50. const key = `key_${i}`;
  51. const ok = vf.add(key);
  52. if (!ok) {
  53. failed = true;
  54. break;
  55. }
  56. inserted.push(key);
  57. }
  58. expect(failed).toBe(true);
  59. expect(vf.size()).toBe(inserted.length);
  60. // 已插入的元素必须都能查到(无假阴性)
  61. for (const key of inserted) {
  62. expect(vf.has(key)).toBe(true);
  63. }
  64. });
  65. test("构造参数包含 NaN 时应使用默认值(不崩溃)", () => {
  66. const vf = new VacuumFilter({
  67. maxItems: 1000,
  68. // @ts-expect-error: 用于覆盖运行时边界情况
  69. fingerprintBits: Number.NaN,
  70. // @ts-expect-error: 用于覆盖运行时边界情况
  71. maxKickSteps: Number.NaN,
  72. // @ts-expect-error: 用于覆盖运行时边界情况
  73. targetLoadFactor: Number.NaN,
  74. seed: "unit-test-nan-options",
  75. });
  76. expect(vf.add("k1")).toBe(true);
  77. expect(vf.has("k1")).toBe(true);
  78. });
  79. test("非 ASCII 字符串也应可用(UTF-8 编码路径)", () => {
  80. const vf = new VacuumFilter({
  81. maxItems: 1000,
  82. fingerprintBits: 32,
  83. maxKickSteps: 500,
  84. seed: "unit-test-non-ascii",
  85. });
  86. const keys = ["你好", "ключ", "テスト"];
  87. for (const key of keys) {
  88. expect(vf.add(key)).toBe(true);
  89. expect(vf.has(key)).toBe(true);
  90. }
  91. });
  92. test("超长 key 也应可用(避免 scratch32 对齐导致 RangeError)", () => {
  93. const vf = new VacuumFilter({
  94. maxItems: 1000,
  95. fingerprintBits: 32,
  96. maxKickSteps: 500,
  97. seed: "unit-test-long-key",
  98. });
  99. // 触发 scratch 扩容:> DEFAULT_SCRATCH_BYTES*2 且不是 4 的倍数
  100. const longKey = "a".repeat(1001);
  101. expect(vf.add(longKey)).toBe(true);
  102. expect(vf.has(longKey)).toBe(true);
  103. expect(vf.delete(longKey)).toBe(true);
  104. expect(vf.has(longKey)).toBe(false);
  105. });
  106. test("fast-reduce bucket index 映射不越界(非 2 的幂 numBuckets)", () => {
  107. const vf = new VacuumFilter({
  108. maxItems: 20_000,
  109. fingerprintBits: 32,
  110. maxKickSteps: 500,
  111. seed: "unit-test-fast-reduce-index",
  112. });
  113. const numBuckets = vf.capacitySlots() / 4;
  114. // @ts-expect-error: 单测需要覆盖私有字段(确保走 fast-reduce 分支)
  115. const bucketMask = vf.bucketMask as number;
  116. // @ts-expect-error: 单测需要覆盖私有字段(确保走 fast-reduce 分支)
  117. const fastReduceMul = vf.fastReduceMul as number | null;
  118. expect(bucketMask).toBe(0);
  119. expect(fastReduceMul).not.toBeNull();
  120. const indexTag = (key: string): void => {
  121. // @ts-expect-error: 单测需要覆盖私有方法的核心不变量
  122. vf.indexTag(key);
  123. };
  124. for (let i = 0; i < 10_000; i++) {
  125. indexTag(`key_${i}`);
  126. // @ts-expect-error: 单测需要覆盖私有字段的核心不变量
  127. const index = vf.tmpIndex as number;
  128. if (index < 0 || index >= numBuckets) {
  129. throw new Error(`tmpIndex out of range: ${index} (numBuckets=${numBuckets})`);
  130. }
  131. }
  132. });
  133. test("alternate index 应为可逆映射(alt(alt(i,tag),tag)=i)且不越界", () => {
  134. const vf = new VacuumFilter({
  135. maxItems: 50_000,
  136. fingerprintBits: 32,
  137. maxKickSteps: 1000,
  138. seed: "unit-test-alt-index-involution",
  139. });
  140. const numBuckets = vf.capacitySlots() / 4;
  141. // @ts-expect-error: 单测需要覆盖私有方法的核心不变量
  142. const altIndex = (index: number, tag: number) => vf.altIndex(index, tag) as number;
  143. for (let i = 0; i < 10_000; i++) {
  144. const index = i % numBuckets;
  145. const tag = (i * 2654435761) >>> 0;
  146. const alt = altIndex(index, tag);
  147. expect(alt).toBeGreaterThanOrEqual(0);
  148. expect(alt).toBeLessThan(numBuckets);
  149. const back = altIndex(alt, tag);
  150. expect(back).toBe(index);
  151. }
  152. });
  153. });