carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package leasetest
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "strings"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/lease"
12)
13
14// Maker builds a store under test, the model backing it, the bucket name,
15// and (for wire stores) a request counter.
16type Maker func(t *testing.T) (st lease.VersionedStore, m *VersionedBucket, bucket string, served func() int)
17
18// RunVersionedStoreContract is the behaviour every lease.VersionedStore
19// must have (spec §13, as covered by Plan 2).
20func RunVersionedStoreContract(t *testing.T, mk Maker) {
21 ctx := context.Background()
22
23 t.Run("listing: key order, latest first, markers included, prefix bounded", func(t *testing.T) {
24 st, m, b, _ := mk(t)
25 m.Put(b, "p/a", []byte("one"))
26 m.Put(b, "p/a", []byte("two"))
27 m.BareDelete(b, "p/a")
28 m.Put(b, "p/b", []byte("x"))
29 m.Put(b, "q/c", []byte("other prefix"))
30 vs, err := st.ListVersions(ctx, b, "p/")
31 if err != nil {
32 t.Fatal(err)
33 }
34 if len(vs) != 4 || vs[0].Key != "p/a" || !vs[0].IsLatest || !vs[0].DeleteMarker || vs[3].Key != "p/b" || !vs[3].IsLatest {
35 t.Fatalf("listing = %+v", vs)
36 }
37 for _, v := range vs[1:3] {
38 if v.Key != "p/a" || v.IsLatest || v.DeleteMarker || v.VersionID == "" {
39 t.Fatalf("non-latest entries = %+v", vs[1:3])
40 }
41 }
42 })
43
44 t.Run("listing spans pages, including many versions of one key", func(t *testing.T) {
45 st, m, b, _ := mk(t)
46 m.PageSize = 3
47 for i := 0; i < 5; i++ {
48 m.Put(b, "p/same", []byte{byte(i)})
49 }
50 for i := 0; i < 4; i++ {
51 m.Put(b, fmt.Sprintf("p/k%02d", i), []byte{byte(i)})
52 }
53 vs, err := st.ListVersions(ctx, b, "p/")
54 if err != nil || len(vs) != 9 {
55 t.Fatalf("paged listing = %d, %v; want 9", len(vs), err)
56 }
57 seen := map[string]bool{}
58 for _, v := range vs {
59 if seen[v.Key+"@"+v.VersionID] {
60 t.Fatalf("duplicate %s@%s", v.Key, v.VersionID)
61 }
62 seen[v.Key+"@"+v.VersionID] = true
63 }
64 })
65
66 t.Run("listing fails whole on a page error", func(t *testing.T) {
67 st, m, b, _ := mk(t)
68 m.PageSize = 2
69 for i := 0; i < 6; i++ {
70 m.Put(b, fmt.Sprintf("p/k%d", i), []byte{byte(i)})
71 }
72 m.FailListAfterPages = 2
73 if vs, err := st.ListVersions(ctx, b, "p/"); err == nil || vs != nil {
74 t.Fatalf("got %d entries and error %v; want no entries and an error", len(vs), err)
75 }
76 })
77
78 t.Run("read and stream by exact version; markers and missing", func(t *testing.T) {
79 st, m, b, _ := mk(t)
80 k := "dir/a b+c%?d"
81 v1 := m.Put(b, k, []byte("old"))
82 m.Put(b, k, []byte("new"))
83 if body, err := st.GetVersion(ctx, b, k, v1); err != nil || string(body) != "old" {
84 t.Fatalf("GetVersion = %q, %v", body, err)
85 }
86 rc, err := st.OpenVersion(ctx, b, k, v1)
87 if err != nil {
88 t.Fatal(err)
89 }
90 body, _ := io.ReadAll(rc)
91 rc.Close()
92 if string(body) != "old" {
93 t.Fatalf("OpenVersion = %q", body)
94 }
95 mk1 := m.BareDelete(b, k)
96 if _, err := st.GetVersion(ctx, b, k, mk1); !errors.Is(err, lease.ErrIsDeleteMarker) {
97 t.Fatalf("GetVersion(marker) = %v, want ErrIsDeleteMarker", err)
98 }
99 if _, err := st.GetVersion(ctx, b, k, "v999999"); !errors.Is(err, lease.ErrMirrorNotFound) {
100 t.Fatalf("GetVersion(missing) = %v, want ErrMirrorNotFound", err)
101 }
102 // A version hidden behind a marker is still readable by id.
103 if body, err := st.GetVersion(ctx, b, k, v1); err != nil || string(body) != "old" {
104 t.Fatalf("GetVersion behind a marker = %q, %v", body, err)
105 }
106 })
107
108 t.Run("range by exact version", func(t *testing.T) {
109 st, m, b, served := mk(t)
110 ranged, ok := st.(lease.VersionRangeOpener)
111 if !ok {
112 t.Skip("store does not support ranged version reads")
113 }
114 key := "dir/a b+c%?d"
115 id := m.Put(b, key, []byte("original"))
116 m.Put(b, key, []byte("replacement"))
117 marker := m.BareDelete(b, key)
118 for _, tc := range []struct {
119 name string
120 offset, length int64
121 want string
122 }{
123 {"bounded", 2, 3, "igi"},
124 {"to end", 3, -1, "ginal"},
125 {"past end", 6, 100, "al"},
126 {"whole", 0, -1, "original"},
127 } {
128 t.Run(tc.name, func(t *testing.T) {
129 rc, err := ranged.OpenVersionRange(ctx, b, key, id, tc.offset, tc.length)
130 if err != nil {
131 t.Fatal(err)
132 }
133 body, err := io.ReadAll(rc)
134 closeErr := rc.Close()
135 if err != nil || closeErr != nil || string(body) != tc.want {
136 t.Fatalf("range = %q, %v, close %v; want %q", body, err, closeErr, tc.want)
137 }
138 })
139 }
140 for _, tc := range []struct {
141 name, bucket, version string
142 offset, length int64
143 want error
144 local bool
145 }{
146 {"missing version ID", b, "", 0, 1, lease.ErrNoVersionID, true},
147 {"negative offset", b, id, -1, 1, nil, true},
148 {"zero length", b, id, 0, 0, nil, true},
149 {"overflow", b, id, 2, 1<<63 - 1, nil, true},
150 {"unsatisfiable", b, id, 8, -1, nil, false},
151 {"missing version", b, "missing", 0, 1, lease.ErrMirrorNotFound, false},
152 {"marker", b, marker, 0, 1, lease.ErrIsDeleteMarker, false},
153 {"missing bucket", "nobucket", id, 0, 1, nil, false},
154 } {
155 t.Run(tc.name, func(t *testing.T) {
156 before := 0
157 if served != nil {
158 before = served()
159 }
160 rc, err := ranged.OpenVersionRange(ctx, tc.bucket, key, tc.version, tc.offset, tc.length)
161 if rc != nil {
162 rc.Close()
163 }
164 if err == nil || (tc.want != nil && !errors.Is(err, tc.want)) {
165 t.Fatalf("range error = %v; want %v", err, tc.want)
166 }
167 if tc.want == nil && errors.Is(err, lease.ErrMirrorNotFound) {
168 t.Fatalf("range error was treated as missing version: %v", err)
169 }
170 if tc.local && served != nil && served() != before {
171 t.Fatal("invalid range sent a request")
172 }
173 })
174 }
175 if err := st.DeleteVersion(ctx, b, key, id); err != nil {
176 t.Fatal(err)
177 }
178 if rc, err := ranged.OpenVersionRange(ctx, b, key, id, 2, 3); !errors.Is(err, lease.ErrMirrorNotFound) {
179 if rc != nil {
180 rc.Close()
181 }
182 t.Fatalf("deleted version range = %v", err)
183 }
184 m.FailOn = func(op, key string) error { return errors.New("offline") }
185 if rc, err := ranged.OpenVersionRange(ctx, b, key, id, 2, 3); err == nil || errors.Is(err, lease.ErrMirrorNotFound) {
186 if rc != nil {
187 rc.Close()
188 }
189 t.Fatalf("failed range = %v", err)
190 }
191 })
192
193 t.Run("copy by exact version; create-only; markers", func(t *testing.T) {
194 st, m, b, _ := mk(t)
195 k := "src/a b+c%?d"
196 v1 := m.Put(b, k, []byte("old"))
197 m.Put(b, k, []byte("new"))
198 dv, etag, err := st.CopyVersion(ctx, b, k, v1, b, "dst/x", true)
199 if err != nil || dv == "" || etag == "" || strings.Contains(etag, "-") {
200 t.Fatalf("CopyVersion = %q %q %v", dv, etag, err)
201 }
202 if cur, ok := m.Current(b, "dst/x"); !ok || string(cur) != "old" {
203 t.Fatalf("copied %q", cur)
204 }
205 if _, _, err := st.CopyVersion(ctx, b, k, v1, b, "dst/x", true); !errors.Is(err, lease.ErrDestinationExists) {
206 t.Fatalf("create-only copy onto existing = %v", err)
207 }
208 m.BareDelete(b, "dst/x")
209 if _, _, err := st.CopyVersion(ctx, b, k, v1, b, "dst/x", true); err != nil {
210 t.Fatalf("create-only copy over a delete marker = %v", err)
211 }
212 mid := m.BareDelete(b, k)
213 if _, _, err := st.CopyVersion(ctx, b, k, mid, b, "dst/y", false); !errors.Is(err, lease.ErrIsDeleteMarker) {
214 t.Fatalf("copy of a marker = %v, want ErrIsDeleteMarker", err)
215 }
216 mpID := m.PutMultipart(b, "src/mp", []byte("payload"))
217 if _, etag, err := st.CopyVersion(ctx, b, "src/mp", mpID, b, "dst/mp", false); err != nil || strings.Contains(etag, "-") {
218 t.Fatalf("multipart copy etag = %q, %v; want single-part", etag, err)
219 }
220 })
221
222 t.Run("delete by version: exact, idempotent, never bare", func(t *testing.T) {
223 st, m, b, served := mk(t)
224 v1 := m.Put(b, "k", []byte("one"))
225 v2 := m.Put(b, "k", []byte("two"))
226 if err := st.DeleteVersion(ctx, b, "k", v2); err != nil {
227 t.Fatal(err)
228 }
229 if cur, ok := m.Current(b, "k"); !ok || string(cur) != "one" {
230 t.Fatalf("after deleting the current version: %q %v; want the older one current", cur, ok)
231 }
232 if err := st.DeleteVersion(ctx, b, "k", v2); err != nil {
233 t.Fatalf("re-delete = %v", err)
234 }
235 if err := st.DeleteVersion(ctx, b, "never", "v000001"); err != nil {
236 t.Fatalf("delete on a missing key = %v", err)
237 }
238 before, reqs := m.Snapshot(), 0
239 if served != nil {
240 reqs = served()
241 }
242 if err := st.DeleteVersion(ctx, b, "k", ""); !errors.Is(err, lease.ErrNoVersionID) {
243 t.Fatalf("empty version id = %v, want ErrNoVersionID", err)
244 }
245 if _, err := st.GetVersion(ctx, b, "k", ""); !errors.Is(err, lease.ErrNoVersionID) {
246 t.Fatalf("GetVersion(\"\") = %v, want ErrNoVersionID", err)
247 }
248 if _, err := st.OpenVersion(ctx, b, "k", ""); !errors.Is(err, lease.ErrNoVersionID) {
249 t.Fatalf("OpenVersion(\"\") = %v, want ErrNoVersionID", err)
250 }
251 if _, _, err := st.CopyVersion(ctx, b, "k", "", b, "k2", false); !errors.Is(err, lease.ErrNoVersionID) {
252 t.Fatalf("CopyVersion(\"\") = %v, want ErrNoVersionID", err)
253 }
254 if m.Snapshot() != before {
255 t.Fatal("state changed on an empty-version delete")
256 }
257 if served != nil && served() != reqs {
258 t.Fatal("a request was sent for an empty version id")
259 }
260 _ = v1
261 })
262
263 t.Run("delete a noncurrent version keeps the current one", func(t *testing.T) {
264 st, m, b, _ := mk(t)
265 v1 := m.Put(b, "k", []byte("one"))
266 v2 := m.Put(b, "k", []byte("two"))
267 if err := st.DeleteVersion(ctx, b, "k", v1); err != nil {
268 t.Fatal(err)
269 }
270 if cur, ok := m.Current(b, "k"); !ok || string(cur) != "two" || m.LatestID(b, "k") != v2 {
271 t.Fatalf("current after a noncurrent delete = %q %v %s", cur, ok, m.LatestID(b, "k"))
272 }
273 if _, err := st.GetVersion(ctx, b, "k", v1); !errors.Is(err, lease.ErrMirrorNotFound) {
274 t.Fatalf("deleted version still readable: %v", err)
275 }
276 })
277
278 t.Run("a missing bucket is an error, never absence", func(t *testing.T) {
279 st, _, _, _ := mk(t)
280 if err := st.DeleteVersion(ctx, "nobucket", "k", "v000001"); err == nil {
281 t.Error("delete in a missing bucket succeeded")
282 }
283 if _, err := st.GetVersion(ctx, "nobucket", "k", "v000001"); err == nil || errors.Is(err, lease.ErrMirrorNotFound) {
284 t.Errorf("get in a missing bucket = %v", err)
285 }
286 if _, err := st.ListVersions(ctx, "nobucket", ""); err == nil {
287 t.Error("list of a missing bucket succeeded")
288 }
289 })
290
291 t.Run("put if absent", func(t *testing.T) {
292 st, m, b, _ := mk(t)
293 if v, err := st.PutIfAbsent(ctx, b, "m/closed.json", []byte("{}")); err != nil || v == "" {
294 t.Fatalf("first = %q %v", v, err)
295 }
296 if _, err := st.PutIfAbsent(ctx, b, "m/closed.json", []byte("{}")); !errors.Is(err, lease.ErrExists) {
297 t.Fatalf("second = %v, want ErrExists", err)
298 }
299 m.BareDelete(b, "m/closed.json")
300 if _, err := st.PutIfAbsent(ctx, b, "m/closed.json", []byte("{}")); err != nil {
301 t.Fatalf("over a marker = %v", err)
302 }
303 })
304
305 t.Run("injected failures surface as errors from every operation", func(t *testing.T) {
306 st, m, b, _ := mk(t)
307 v := m.Put(b, "k", []byte("x"))
308 boom := errors.New("boom")
309 m.FailOn = func(op, key string) error { return boom }
310 if _, err := st.ListVersions(ctx, b, ""); err == nil {
311 t.Error("list: no error")
312 }
313 if _, err := st.GetVersion(ctx, b, "k", v); err == nil || errors.Is(err, lease.ErrMirrorNotFound) {
314 t.Errorf("get: %v", err)
315 }
316 if _, _, err := st.CopyVersion(ctx, b, "k", v, b, "k2", false); err == nil || errors.Is(err, lease.ErrMirrorNotFound) {
317 t.Errorf("copy: %v", err)
318 }
319 if err := st.DeleteVersion(ctx, b, "k", v); err == nil {
320 t.Error("delete: no error")
321 }
322 if _, err := st.PutIfAbsent(ctx, b, "new", []byte("x")); err == nil {
323 t.Error("put: no error")
324 }
325 m.FailOn = nil
326 if _, ok := m.Current(b, "k"); !ok {
327 t.Fatal("a failed delete removed data")
328 }
329 })
330}
331