package batcher import ( "strings" "testing" "time" ) func item(subject, host string, ts time.Time, hasTS bool, raw string) Item { return Item{Subject: subject, Host: host, Timestamp: ts, HasTS: hasTS, Raw: []byte(raw)} } func TestFullByEvents(t *testing.T) { b := New(Limits{MaxEvents: 3}) if got := b.Add(item("s", "h", time.Now(), true, "a")); got != nil { t.Fatalf("should not be full at 1") } if got := b.Add(item("s", "h", time.Now(), true, "b")); got != nil { t.Fatalf("should not be full at 2") } full := b.Add(item("s", "h", time.Now(), true, "c")) if full == nil { t.Fatalf("should be full at 3") } if len(full.Items) != 3 { t.Errorf("full batch has %d items", len(full.Items)) } // After a full flush the subject batch is reset. if b.Pending() != 0 { t.Errorf("pending after flush = %d, want 0", b.Pending()) } } func TestFullByBytes(t *testing.T) { b := New(Limits{MaxBytes: 10}) if b.Add(item("s", "h", time.Now(), true, "12345")) != nil { t.Fatalf("5 bytes should not fill") } full := b.Add(item("s", "h", time.Now(), true, "67890")) if full == nil { t.Fatalf("10 bytes should fill") } if full.RawBytes != 10 { t.Errorf("RawBytes = %d", full.RawBytes) } } func TestSeparateSubjects(t *testing.T) { b := New(Limits{MaxEvents: 2}) b.Add(item("a", "h", time.Now(), true, "x")) b.Add(item("b", "h", time.Now(), true, "y")) if b.Pending() != 2 { t.Errorf("pending = %d, want 2 across subjects", b.Pending()) } full := b.Add(item("a", "h", time.Now(), true, "z")) if full == nil || full.Subject != "a" { t.Fatalf("subject a should flush independently") } if b.Pending() != 1 { t.Errorf("pending after a flush = %d, want 1 (subject b)", b.Pending()) } } func TestDueByAge(t *testing.T) { base := time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC) b := New(Limits{MaxAge: time.Minute}) b.nowFn = func() time.Time { return base } b.Add(item("s", "h", base, true, "x")) if due := b.DueByAge(base.Add(30 * time.Second)); len(due) != 0 { t.Fatalf("not due at 30s") } due := b.DueByAge(base.Add(90 * time.Second)) if len(due) != 1 { t.Fatalf("should be due at 90s, got %d", len(due)) } if b.Pending() != 0 { t.Errorf("due batch not removed") } } func TestDrain(t *testing.T) { b := New(Limits{MaxEvents: 100}) b.Add(item("a", "h", time.Now(), true, "x")) b.Add(item("b", "h", time.Now(), true, "y")) all := b.Drain() if len(all) != 2 { t.Fatalf("drain returned %d, want 2", len(all)) } if b.Pending() != 0 { t.Errorf("pending after drain = %d", b.Pending()) } } func TestNDJSON(t *testing.T) { b := &Batch{Subject: "s"} b.Items = []Item{ {Raw: []byte(`{"a":1}`)}, {Raw: []byte(`{"b":2}` + "\n")}, // trailing newline trimmed and re-added } got := string(b.NDJSON()) want := "{\"a\":1}\n{\"b\":2}\n" if got != want { t.Errorf("NDJSON = %q, want %q", got, want) } if strings.Count(got, "\n") != 2 { t.Errorf("expected exactly 2 newlines") } } func TestSummarize(t *testing.T) { t1 := time.Date(2026, 7, 27, 1, 0, 0, 0, time.UTC) t2 := time.Date(2026, 7, 27, 3, 0, 0, 0, time.UTC) fallback := time.Date(2026, 7, 27, 9, 0, 0, 0, time.UTC) b := &Batch{Subject: "s"} b.Items = []Item{ item("s", "host-b", t2, true, "x"), item("s", "host-a", t1, true, "y"), item("s", "", time.Time{}, false, "z"), // no ts -> fallback, no host item("s", "host-a", t1, true, "w"), // dup host } s := b.Summarize(fallback) if s.EventCount != 4 { t.Errorf("EventCount = %d", s.EventCount) } if len(s.Hosts) != 2 || s.Hosts[0] != "host-a" || s.Hosts[1] != "host-b" { t.Errorf("Hosts = %v, want sorted unique [host-a host-b]", s.Hosts) } if !s.MinTS.Equal(t1) { t.Errorf("MinTS = %v, want %v", s.MinTS, t1) } // max should be the fallback (9:00) since event z used fallback which is latest if !s.MaxTS.Equal(fallback) { t.Errorf("MaxTS = %v, want fallback %v", s.MaxTS, fallback) } } func TestSummarizeAllFallback(t *testing.T) { fallback := time.Date(2026, 7, 27, 9, 0, 0, 0, time.UTC) b := &Batch{Items: []Item{{Raw: []byte("x")}}} s := b.Summarize(fallback) if !s.MinTS.Equal(fallback) || !s.MaxTS.Equal(fallback) { t.Errorf("all-fallback range wrong: %v..%v", s.MinTS, s.MaxTS) } if s.HasTS { t.Errorf("HasTS should be false") } }