carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package analytics
2
3import (
4 "context"
5 "encoding/json"
6 "strings"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/release"
10)
11
12func delta(box string, seq int64, pv int64) Delta {
13 h := &HLL{}
14 h.Add(h64(box + itoa(int(seq))))
15 p := NewTopK()
16 p.Add("/", pv)
17 return Delta{Date: "2026-09-07", Box: box, Seq: seq, Hours: map[string]HourRow{"09": {PV: pv, Bytes: pv * 100}},
18 Visitors: h, Pages: p, Referers: NewTopK(), Events: NewTopK()}
19}
20
21func TestDayMergeIsSeqIdempotent(t *testing.T) {
22 var d Day
23 if ok, err := d.Merge(delta("box-a", 5, 10)); !ok || err != nil {
24 t.Fatalf("first merge: %v %v", ok, err)
25 }
26 if ok, _ := d.Merge(delta("box-a", 5, 10)); ok {
27 t.Fatal("replaying the same seq must be a no-op")
28 }
29 if ok, _ := d.Merge(delta("box-a", 4, 10)); ok {
30 t.Fatal("an older seq must be a no-op")
31 }
32 if ok, _ := d.Merge(delta("box-b", 1, 7)); !ok {
33 t.Fatal("another box's seq is independent")
34 }
35 if d.Hours["09"].PV != 17 || d.Pages["/"] != 17 || d.Seq["box-a"] != 5 || d.Seq["box-b"] != 1 || d.V != 1 || d.Date != "2026-09-07" {
36 t.Fatalf("merged = %+v", d)
37 }
38 vis, _ := DecodeHLL(d.Visitors)
39 if c := vis.Count(); c < 1.5 || c > 2.5 {
40 t.Fatalf("visitors ≈ 2, got %.2f", c)
41 }
42 b, _ := json.Marshal(d)
43 for _, want := range []string{`"v":1`, `"hours"`, `"visitors"`, `"pages"`, `"referers"`, `"events"`, `"seq"`, `"dropped"`, `"countries"`} {
44 if !contains(string(b), want) {
45 t.Errorf("wire missing %s in %s", want, b)
46 }
47 }
48}
49
50func TestDayMergeReplacesCorruptSketch(t *testing.T) {
51 d := Day{Visitors: "garbage!"}
52 ok, err := d.Merge(delta("box-a", 1, 5))
53 if !ok || err != nil {
54 t.Fatalf("merge with corrupt sketch: %v %v", ok, err)
55 }
56 if d.Hours["09"].PV != 5 || d.Pages["/"] != 5 {
57 t.Fatalf("day = %+v", d)
58 }
59 vis, err := DecodeHLL(d.Visitors)
60 if err != nil {
61 t.Fatalf("decode replaced sketch: %v", err)
62 }
63 if c := vis.Count(); c < 0.5 {
64 t.Fatalf("expected at least one visitor recorded, got %.2f", c)
65 }
66}
67
68func TestUpsertDayCASAcrossBoxes(t *testing.T) {
69 ctx := context.Background()
70 s := release.NewDirStore(t.TempDir())
71 if err := UpsertDay(ctx, s, "bab", "web", delta("box-a", 1, 3)); err != nil {
72 t.Fatal(err)
73 }
74 if err := UpsertDay(ctx, s, "bab", "web", delta("box-a", 2, 4)); err != nil {
75 t.Fatal(err)
76 }
77 if err := UpsertDay(ctx, s, "bab", "web", delta("box-a", 2, 4)); err != nil { // retry of an ambiguous put
78 t.Fatal(err)
79 }
80 raw, _, err := s.Get(ctx, must(t)(DayKey("bab", "web", "box-a", "2026-09-07")))
81 if err != nil {
82 t.Fatal(err)
83 }
84 var d Day
85 json.Unmarshal(raw, &d)
86 if d.Hours["09"].PV != 7 || d.Seq["box-a"] != 2 {
87 t.Fatalf("day = %+v", d)
88 }
89 if k := must(t)(DayKey("bab", "web", "box-a", "2026-09-07")); k != "apps/bab/web/analytics/box-a/days/2026-09-07.json" {
90 t.Fatal(k)
91 }
92}
93
94func contains(s, sub string) bool { return strings.Contains(s, sub) }
95
96// must unwraps a key builder's (key, error) for fixtures whose names are
97// known good: must(t)(ChunkKey(...)).
98func must(t testing.TB) func(string, error) string {
99 return func(k string, err error) string {
100 t.Helper()
101 if err != nil {
102 t.Fatal(err)
103 }
104 return k
105 }
106}
107