bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
161 lines
4.0 KiB
Go
161 lines
4.0 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package event
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDedupFilter_FirstSeen(t *testing.T) {
|
|
d := NewDedupFilter()
|
|
if d.IsDuplicate("evt-1") {
|
|
t.Error("first occurrence should not be duplicate")
|
|
}
|
|
}
|
|
|
|
func TestDedupFilter_SecondSeen(t *testing.T) {
|
|
d := NewDedupFilter()
|
|
d.IsDuplicate("evt-1")
|
|
if !d.IsDuplicate("evt-1") {
|
|
t.Error("second occurrence within TTL should be duplicate")
|
|
}
|
|
}
|
|
|
|
func TestDedupFilter_TTLExpiry(t *testing.T) {
|
|
d := NewDedupFilterWithSize(defaultRingSize, 10*time.Millisecond)
|
|
d.IsDuplicate("evt-1")
|
|
time.Sleep(20 * time.Millisecond)
|
|
if d.IsDuplicate("evt-1") {
|
|
t.Error("should not be duplicate after TTL expires")
|
|
}
|
|
}
|
|
|
|
func TestDedupFilter_RingBuffer(t *testing.T) {
|
|
d := NewDedupFilterWithSize(5, 10*time.Millisecond)
|
|
for i := 0; i < 5; i++ {
|
|
d.IsDuplicate("evt-" + string(rune('a'+i)))
|
|
}
|
|
for i := 0; i < 5; i++ {
|
|
if !d.IsDuplicate("evt-" + string(rune('a'+i))) {
|
|
t.Errorf("evt-%c should still be duplicate", rune('a'+i))
|
|
}
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
for i := 5; i < 10; i++ {
|
|
d.IsDuplicate("evt-" + string(rune('a'+i)))
|
|
}
|
|
for i := 0; i < 5; i++ {
|
|
if d.IsDuplicate("evt-" + string(rune('a'+i))) {
|
|
t.Errorf("evt-%c should not be duplicate after ring eviction + TTL expiry", rune('a'+i))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDedupFilter_ConcurrentSafe(t *testing.T) {
|
|
d := NewDedupFilter()
|
|
done := make(chan struct{})
|
|
for i := 0; i < 100; i++ {
|
|
go func(id string) {
|
|
d.IsDuplicate(id)
|
|
done <- struct{}{}
|
|
}("evt-" + string(rune(i)))
|
|
}
|
|
for i := 0; i < 100; i++ {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
// Under N concurrent writers, exactly N IsDuplicate calls must observe first-seen.
|
|
func TestDedupFilter_ConcurrentFirstSeenExactlyOnce(t *testing.T) {
|
|
const n = 200
|
|
d := NewDedupFilter()
|
|
|
|
ids := make([]string, n)
|
|
for i := 0; i < n; i++ {
|
|
ids[i] = "evt-unique-" + string(rune('A'+i%26)) + string(rune('a'+(i/26)%26)) + string(rune('0'+i%10))
|
|
}
|
|
|
|
results := make(chan bool, n)
|
|
for i := 0; i < n; i++ {
|
|
go func(id string) {
|
|
results <- d.IsDuplicate(id)
|
|
}(ids[i])
|
|
}
|
|
|
|
firstSeen := 0
|
|
for i := 0; i < n; i++ {
|
|
if !<-results {
|
|
firstSeen++
|
|
}
|
|
}
|
|
if firstSeen != n {
|
|
t.Errorf("first-seen count = %d, want %d", firstSeen, n)
|
|
}
|
|
|
|
for _, id := range ids {
|
|
if !d.IsDuplicate(id) {
|
|
t.Errorf("ID %q not flagged as duplicate on second call", id)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reinserting an ID that already occupies its own ring slot must not delete the fresh seen entry.
|
|
func TestDedupFilter_SelfEvictionPreservesFreshEntry(t *testing.T) {
|
|
d := NewDedupFilterWithSize(2, time.Hour)
|
|
d.ring[0] = "X"
|
|
d.pos = 0
|
|
|
|
if d.IsDuplicate("X") {
|
|
t.Fatal("first call should not be duplicate (seen empty)")
|
|
}
|
|
if !d.IsDuplicate("X") {
|
|
t.Error("self-slot reinsert wiped seen[X] — duplicate signal lost")
|
|
}
|
|
}
|
|
|
|
// After cleanupExpired, an ID past its TTL must not be reported as duplicate even if still in the ring.
|
|
func TestDedupFilter_TTLExpiryAfterCleanupRunRespected(t *testing.T) {
|
|
d := NewDedupFilterWithSize(10, 10*time.Millisecond)
|
|
if d.IsDuplicate("A") {
|
|
t.Fatal("first IsDuplicate(A) should be false")
|
|
}
|
|
time.Sleep(25 * time.Millisecond)
|
|
for i := 0; i < 9; i++ {
|
|
d.IsDuplicate("f" + string(rune('0'+i)))
|
|
}
|
|
if d.IsDuplicate("A") {
|
|
t.Error("A is past TTL — must NOT be reported as duplicate")
|
|
}
|
|
}
|
|
|
|
func TestDedupFilter_ConcurrentRingEviction(t *testing.T) {
|
|
const ringSize = 16
|
|
const writers = 8
|
|
const perWriter = 40
|
|
d := NewDedupFilterWithSize(ringSize, 5*time.Millisecond)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(writers)
|
|
for w := 0; w < writers; w++ {
|
|
go func(w int) {
|
|
defer wg.Done()
|
|
for i := 0; i < perWriter; i++ {
|
|
d.IsDuplicate("evt-w" + string(rune('0'+w)) + "-" + string(rune('0'+i%10)) + string(rune('a'+i/10)))
|
|
}
|
|
}(w)
|
|
}
|
|
wg.Wait()
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
for i := 0; i < ringSize*4; i++ {
|
|
d.IsDuplicate("evt-fill-" + string(rune('0'+i%10)) + string(rune('a'+i/10)))
|
|
}
|
|
if d.IsDuplicate("evt-w0-0a") {
|
|
t.Error("evicted ID should not be reported as duplicate")
|
|
}
|
|
}
|