| 1 | package apperr |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/release" |
| 9 | ) |
| 10 | |
| 11 | func TestReadWindowMergesAndFilters(t *testing.T) { |
| 12 | ctx := context.Background() |
| 13 | s := release.NewDirStore(t.TempDir()) |
| 14 | now := time.Date(2026, 9, 7, 10, 30, 0, 0, time.UTC) |
| 15 | put := func(box string, recs []Record) { |
| 16 | body, err := EncodeChunk(recs) |
| 17 | if err != nil { |
| 18 | t.Fatal(err) |
| 19 | } |
| 20 | if _, err := s.Put(ctx, must(t)(ChunkKey("bab", "web", box, HourOf(recs[0].TS), recs[0].Seq, len(body))), body, ""); err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | } |
| 24 | put("box1", []Record{ |
| 25 | {TS: now, Box: "box1", Host: "h1", Source: SourceEdge, Name: "a", FP: "fa", Count: 3, First: now.Add(-4 * time.Minute), Last: now, Seq: 1}, |
| 26 | {TS: now, Box: "box1", Host: "h1", Source: SourceServer, Name: "b", FP: "fb", Count: 1, First: now, Last: now, Seq: 2}, |
| 27 | }) |
| 28 | put("box2", []Record{ |
| 29 | {TS: now.Add(-3 * time.Hour), Box: "box2", Host: "h2", Source: SourceEdge, Name: "old", FP: "fo", Count: 1, First: now.Add(-3 * time.Hour), Last: now.Add(-3 * time.Hour), Seq: 1}, |
| 30 | }) |
| 31 | recs, trimmed, err := ReadWindow(ctx, s, "bab", "web", Filter{Since: now.Add(-time.Hour)}, 100) |
| 32 | if err != nil || trimmed { |
| 33 | t.Fatalf("read: %v %v", err, trimmed) |
| 34 | } |
| 35 | if len(recs) != 2 || recs[0].FP != "fa" || recs[1].FP != "fb" { |
| 36 | t.Fatalf("window = %+v", recs) |
| 37 | } |
| 38 | // A record whose [First, Last] overlaps the window is in, whole. |
| 39 | recs, _, _ = ReadWindow(ctx, s, "bab", "web", Filter{Since: now.Add(-2 * time.Minute), Until: now.Add(-time.Minute)}, 100) |
| 40 | if len(recs) != 1 || recs[0].FP != "fa" || recs[0].Count != 3 { |
| 41 | t.Fatalf("overlap = %+v", recs) |
| 42 | } |
| 43 | recs, _, _ = ReadWindow(ctx, s, "bab", "web", Filter{Since: now.Add(-time.Hour), Source: SourceServer}, 100) |
| 44 | if len(recs) != 1 || recs[0].FP != "fb" { |
| 45 | t.Fatalf("source filter = %+v", recs) |
| 46 | } |
| 47 | recs, _, _ = ReadWindow(ctx, s, "bab", "web", Filter{Since: now.Add(-4 * time.Hour), Host: "h2"}, 100) |
| 48 | if len(recs) != 1 || recs[0].FP != "fo" { |
| 49 | t.Fatalf("host filter = %+v", recs) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | // TestReadWindowListsHourBeforeSince: a record's chunk sits under the hour |
| 54 | // of its FIRST occurrence, which can be well before the window if the group |
| 55 | // is still open. Since 11:00 must still find a chunk filed under hour 10 — |
| 56 | // the listing is widened by one hour behind Since — while a record in that |
| 57 | // same chunk whose Last never reaches the window stays excluded by Match. |
| 58 | func TestReadWindowListsHourBeforeSince(t *testing.T) { |
| 59 | ctx := context.Background() |
| 60 | s := release.NewDirStore(t.TempDir()) |
| 61 | base := time.Date(2026, 9, 7, 10, 50, 0, 0, time.UTC) // hour dir "…-10" |
| 62 | recs := []Record{ |
| 63 | // Opened at 10:50, still being added to at 11:05: inside the |
| 64 | // [Since,Until] window by overlap, but filed under hour 10. |
| 65 | {TS: base, Box: "box1", Host: "h1", Source: SourceEdge, Name: "long", FP: "flong", |
| 66 | Count: 5, First: base, Last: base.Add(15 * time.Minute), Seq: 1}, |
| 67 | // Opened and closed at 10:50-10:55, entirely before the window. |
| 68 | {TS: base, Box: "box1", Host: "h1", Source: SourceEdge, Name: "gone", FP: "fgone", |
| 69 | Count: 1, First: base, Last: base.Add(5 * time.Minute), Seq: 2}, |
| 70 | } |
| 71 | body, err := EncodeChunk(recs) |
| 72 | if err != nil { |
| 73 | t.Fatal(err) |
| 74 | } |
| 75 | if _, err := s.Put(ctx, must(t)(ChunkKey("bab", "web", "box1", HourOf(recs[0].TS), recs[0].Seq, len(body))), body, ""); err != nil { |
| 76 | t.Fatal(err) |
| 77 | } |
| 78 | since := time.Date(2026, 9, 7, 11, 0, 0, 0, time.UTC) |
| 79 | until := time.Date(2026, 9, 7, 11, 30, 0, 0, time.UTC) |
| 80 | got, trimmed, err := ReadWindow(ctx, s, "bab", "web", Filter{Since: since, Until: until}, 100) |
| 81 | if err != nil || trimmed { |
| 82 | t.Fatalf("read: %v %v", err, trimmed) |
| 83 | } |
| 84 | if len(got) != 1 || got[0].FP != "flong" { |
| 85 | t.Fatalf("window = %+v, want only flong", got) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | // must unwraps a key builder's (key, error) for fixtures whose names are |
| 90 | // known good: must(t)(ChunkKey(...)). |
| 91 | func must(t testing.TB) func(string, error) string { |
| 92 | return func(k string, err error) string { |
| 93 | t.Helper() |
| 94 | if err != nil { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | return k |
| 98 | } |
| 99 | } |
| 100 | |