carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "testing"
8)
9
10func TestChangeScope(t *testing.T) {
11 cases := map[string]string{
12 "apps/bab/shop/instances/a.example.com.json": "apps/bab/shop",
13 "apps/bab/shop/config-gen.json": "apps/bab/shop",
14 "apps/bab/shop/channels/canary/x": "apps/bab/shop",
15 "apps/bab/shop/sidecars.json": "apps/bab/shop",
16 "apps/bab/shop/schedules.json": "apps/bab/shop",
17 "accounts/bab/account.json": "accounts/bab",
18 "accounts/bab/telemetry.json": "accounts/bab",
19 "control/certs/example.com": "control/certs",
20 "control/edges/box-a.json": "control/edges",
21 }
22 for k, want := range cases {
23 got, ok := ChangeScope(k)
24 if !ok || got != want {
25 t.Errorf("ChangeScope(%q) = %q,%v want %q", k, got, ok, want)
26 }
27 }
28 for _, k := range []string{
29 "apps/bab/shop/logs/box/2026-10-21-10/x", "apps/bab/shop/errors/x",
30 "apps/bab/shop/analytics/x", "apps/bab/shop/runs/r1/request.json",
31 "apps/bab/shop/metrics/x", "apps/bab/shop/schedule-status/shop.example.com.json",
32 "apps/bab/shop", "apps/bab/", "apps/",
33 ChangeIndexKey, "control/serial", "control/edge-mesh-key",
34 "control/edge-mesh-key.next", "releases/x", "status/boxes/a.json",
35 "fleets/f/apps/bab/shop/config.json",
36 } {
37 if s, ok := ChangeScope(k); ok {
38 t.Errorf("ChangeScope(%q) = %q, want none", k, s)
39 }
40 }
41}
42
43// bumpFuncs adapts a Store to the raw get/put pair bumpChangeIndex takes.
44func bumpFuncs(s Store) (func(context.Context, string) ([]byte, string, error), func(context.Context, string, []byte, string) (string, error)) {
45 return s.Get, s.Put
46}
47
48func TestBumpChangeIndexCreatesThenIncrements(t *testing.T) {
49 t.Parallel()
50 ctx := context.Background()
51 s := NewDirStore(t.TempDir())
52 get, put := bumpFuncs(s)
53 if err := bumpChangeIndex(ctx, get, put, "control/certs"); err != nil {
54 t.Fatal(err)
55 }
56 for i := 0; i < 2; i++ {
57 if err := bumpChangeIndex(ctx, get, put, "apps/bab/shop"); err != nil {
58 t.Fatal(err)
59 }
60 }
61 idx, etag, err := ReadChangeIndex(ctx, s)
62 if err != nil {
63 t.Fatal(err)
64 }
65 if etag == "" || idx.Schema != 1 {
66 t.Fatalf("etag %q schema %d", etag, idx.Schema)
67 }
68 if idx.Entries["apps/bab/shop"] != 2 || idx.Entries["control/certs"] != 1 || len(idx.Entries) != 2 {
69 t.Fatalf("entries = %v", idx.Entries)
70 }
71}
72
73func TestBumpChangeIndexAppliesAllScopesInOneWrite(t *testing.T) {
74 t.Parallel()
75 ctx := context.Background()
76 s := NewDirStore(t.TempDir())
77 puts := 0
78 get := s.Get
79 put := func(ctx context.Context, k string, b []byte, m string) (string, error) {
80 puts++
81 return s.Put(ctx, k, b, m)
82 }
83 if err := bumpChangeIndex(ctx, get, put, "apps/bab/shop", "accounts/bab", "apps/bab/shop"); err != nil {
84 t.Fatal(err)
85 }
86 idx, _, _ := ReadChangeIndex(ctx, s)
87 if puts != 1 || idx.Entries["accounts/bab"] != 1 || idx.Entries["apps/bab/shop"] != 1 {
88 t.Fatalf("puts %d entries %v", puts, idx.Entries)
89 }
90}
91
92func TestBumpChangeIndexRetriesOnConflict(t *testing.T) {
93 t.Parallel()
94 ctx := context.Background()
95 s := NewDirStore(t.TempDir())
96 puts := 0
97 put := func(ctx context.Context, k string, b []byte, m string) (string, error) {
98 puts++
99 if puts <= 2 {
100 return "", ErrCASConflict
101 }
102 return s.Put(ctx, k, b, m)
103 }
104 if err := bumpChangeIndex(ctx, s.Get, put, "apps/bab/shop"); err != nil {
105 t.Fatal(err)
106 }
107 idx, _, _ := ReadChangeIndex(ctx, s)
108 if puts != 3 || idx.Entries["apps/bab/shop"] != 1 {
109 t.Fatalf("puts %d entries %v", puts, idx.Entries)
110 }
111}
112
113func TestBumpChangeIndexGivesUpAfterFourAttempts(t *testing.T) {
114 t.Parallel()
115 s := NewDirStore(t.TempDir())
116 puts := 0
117 put := func(context.Context, string, []byte, string) (string, error) {
118 puts++
119 return "", ErrCASConflict
120 }
121 err := bumpChangeIndex(context.Background(), s.Get, put, "apps/bab/shop")
122 if err == nil || !errors.Is(err, ErrCASConflict) {
123 t.Fatalf("err = %v", err)
124 }
125 if puts != 4 {
126 t.Fatalf("puts = %d, want 4", puts)
127 }
128}
129
130func TestBumpChangeIndexRefusesUnknownSchema(t *testing.T) {
131 t.Parallel()
132 ctx := context.Background()
133 s := NewDirStore(t.TempDir())
134 if _, err := s.Put(ctx, ChangeIndexKey, []byte(`{"schema":2,"entries":{}}`), ""); err != nil {
135 t.Fatal(err)
136 }
137 puts := 0
138 put := func(ctx context.Context, k string, b []byte, m string) (string, error) {
139 puts++
140 return s.Put(ctx, k, b, m)
141 }
142 err := bumpChangeIndex(ctx, s.Get, put, "apps/bab/shop")
143 if !errors.Is(err, ErrChangeIndexSchema) || puts != 0 {
144 t.Fatalf("err = %v, puts = %d", err, puts)
145 }
146}
147
148func TestReadChangeIndexAbsentIsEmpty(t *testing.T) {
149 t.Parallel()
150 idx, etag, err := ReadChangeIndex(context.Background(), NewDirStore(t.TempDir()))
151 if err != nil {
152 t.Fatal(err)
153 }
154 if idx.Schema != 1 || idx.Entries == nil || len(idx.Entries) != 0 || etag != "" {
155 t.Fatalf("idx %+v etag %q", idx, etag)
156 }
157}
158
159func TestReadChangeIndexMalformedAndSchema(t *testing.T) {
160 t.Parallel()
161 ctx := context.Background()
162 s := NewDirStore(t.TempDir())
163 if _, err := s.Put(ctx, ChangeIndexKey, []byte(`not json`), ""); err != nil {
164 t.Fatal(err)
165 }
166 if _, _, err := ReadChangeIndex(ctx, s); err == nil || errors.Is(err, ErrChangeIndexSchema) {
167 t.Fatalf("malformed err = %v", err)
168 }
169 body, _ := json.Marshal(ChangeIndex{Schema: 3})
170 if _, err := s.Put(ctx, ChangeIndexKey, body, mustEtag(t, s)); err != nil {
171 t.Fatal(err)
172 }
173 if _, _, err := ReadChangeIndex(ctx, s); !errors.Is(err, ErrChangeIndexSchema) {
174 t.Fatalf("schema err = %v", err)
175 }
176}
177
178func mustEtag(t *testing.T, s Store) string {
179 t.Helper()
180 _, e, err := s.Get(context.Background(), ChangeIndexKey)
181 if err != nil {
182 t.Fatal(err)
183 }
184 return e
185}
186