package main import ( "fmt" "testing" "time" pb "git.ipng.ch/ipng/nginx-logtail/proto/logtailpb" ) func makeStore() *Store { return NewStore("test") } func ingestN(s *Store, website, prefix, uri, status string, n int) { for i := 0; i < n; i++ { s.ingest(LogRecord{website, prefix, uri, status}) } } func TestIngestAndRotate(t *testing.T) { s := makeStore() ingestN(s, "example.com", "1.2.3.0/24", "/", "200", 100) ingestN(s, "other.com", "5.6.7.0/24", "/api", "429", 50) s.rotate(time.Now()) s.mu.RLock() defer s.mu.RUnlock() if s.fineFilled != 1 { t.Fatalf("fineFilled = %d, want 1", s.fineFilled) } snap := s.fineRing[(s.fineHead-1+fineRingSize)%fineRingSize] if len(snap.Entries) != 2 { t.Fatalf("snapshot has %d entries, want 2", len(snap.Entries)) } if snap.Entries[0].Count != 100 { t.Errorf("top entry count = %d, want 100", snap.Entries[0].Count) } } func TestLiveMapCap(t *testing.T) { s := makeStore() // Ingest liveMapCap+100 distinct keys; only liveMapCap should be tracked for i := 0; i < liveMapCap+100; i++ { s.ingest(LogRecord{ Website: fmt.Sprintf("site%d.com", i), ClientPrefix: "1.2.3.0/24", URI: "/", Status: "200", }) } if s.liveLen != liveMapCap { t.Errorf("liveLen = %d, want %d", s.liveLen, liveMapCap) } } func TestQueryTopN(t *testing.T) { s := makeStore() ingestN(s, "busy.com", "1.0.0.0/24", "/", "200", 300) ingestN(s, "medium.com", "2.0.0.0/24", "/", "200", 100) ingestN(s, "quiet.com", "3.0.0.0/24", "/", "200", 10) s.rotate(time.Now()) entries := s.QueryTopN(nil, pb.GroupBy_WEBSITE, 2, pb.Window_W1M) if len(entries) != 2 { t.Fatalf("got %d entries, want 2", len(entries)) } if entries[0].Label != "busy.com" { t.Errorf("top entry = %q, want busy.com", entries[0].Label) } if entries[0].Count != 300 { t.Errorf("top count = %d, want 300", entries[0].Count) } } func TestQueryTopNWithFilter(t *testing.T) { s := makeStore() ingestN(s, "example.com", "1.0.0.0/24", "/api", "429", 200) ingestN(s, "example.com", "2.0.0.0/24", "/api", "200", 500) ingestN(s, "other.com", "3.0.0.0/24", "/", "429", 100) s.rotate(time.Now()) status429 := int32(429) entries := s.QueryTopN(&pb.Filter{HttpResponse: &status429}, pb.GroupBy_WEBSITE, 10, pb.Window_W1M) if len(entries) != 2 { t.Fatalf("got %d entries, want 2", len(entries)) } // example.com has 200 × 429, other.com has 100 × 429 if entries[0].Label != "example.com" || entries[0].Count != 200 { t.Errorf("unexpected top: %+v", entries[0]) } } func TestQueryTrend(t *testing.T) { s := makeStore() now := time.Now() // Rotate 3 buckets with different counts ingestN(s, "x.com", "1.0.0.0/24", "/", "200", 10) s.rotate(now.Add(-2 * time.Minute)) ingestN(s, "x.com", "1.0.0.0/24", "/", "200", 20) s.rotate(now.Add(-1 * time.Minute)) ingestN(s, "x.com", "1.0.0.0/24", "/", "200", 30) s.rotate(now) points := s.QueryTrend(nil, pb.Window_W5M) if len(points) != 3 { t.Fatalf("got %d points, want 3", len(points)) } // Points are oldest-first; counts should be 10, 20, 30 if points[0].Count != 10 || points[1].Count != 20 || points[2].Count != 30 { t.Errorf("unexpected counts: %v", points) } } func TestCoarseRingPopulated(t *testing.T) { s := makeStore() now := time.Now() // Rotate coarseEvery fine buckets to trigger one coarse bucket for i := 0; i < coarseEvery; i++ { ingestN(s, "x.com", "1.0.0.0/24", "/", "200", 10) s.rotate(now.Add(time.Duration(i) * time.Minute)) } s.mu.RLock() defer s.mu.RUnlock() if s.coarseFilled != 1 { t.Fatalf("coarseFilled = %d, want 1", s.coarseFilled) } coarse := s.coarseRing[(s.coarseHead-1+coarseRingSize)%coarseRingSize] if len(coarse.Entries) == 0 { t.Fatal("coarse snapshot is empty") } // 5 fine buckets × 10 counts = 50 if coarse.Entries[0].Count != 50 { t.Errorf("coarse count = %d, want 50", coarse.Entries[0].Count) } } func TestSubscribeBroadcast(t *testing.T) { s := makeStore() ch := s.Subscribe() ingestN(s, "x.com", "1.0.0.0/24", "/", "200", 5) s.rotate(time.Now()) select { case snap := <-ch: if len(snap.Entries) != 1 { t.Errorf("got %d entries, want 1", len(snap.Entries)) } case <-time.After(time.Second): t.Fatal("no snapshot received within 1s") } s.Unsubscribe(ch) } func TestTopKOrdering(t *testing.T) { m := map[string]int64{ "a": 5, "b": 100, "c": 1, "d": 50, } entries := topKFromMap(m, 3) if len(entries) != 3 { t.Fatalf("got %d entries, want 3", len(entries)) } if entries[0].Label != "b" || entries[0].Count != 100 { t.Errorf("wrong top: %+v", entries[0]) } if entries[1].Label != "d" || entries[1].Count != 50 { t.Errorf("wrong second: %+v", entries[1]) } }