carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package objstore
2
3import (
4 "context"
5 "errors"
6 "testing"
7 "time"
8)
9
10// scannerContract is run against every Scanner implementation. Idempotence is
11// the property under test: every method is called twice and the second call
12// must be a success, because the sweep re-runs after a half-failure at any
13// point and a walk that only works once is not re-runnable.
14func scannerContract(t *testing.T, s Scanner, bucket string) {
15 t.Helper()
16 ctx := context.Background()
17
18 for i := 0; i < 2; i++ {
19 if err := s.EnsureEnabled(ctx, bucket); err != nil {
20 t.Fatalf("EnsureEnabled call %d: %v", i+1, err)
21 }
22 }
23
24 // An object nobody has scanned has no verdict, and that is a state rather
25 // than an error — the same idiom Get uses for an absent record.
26 v, err := s.Verdict(ctx, bucket, "absent.txt", "etag-1")
27 if err != nil {
28 t.Fatalf("Verdict on an unscanned object: %v", err)
29 }
30 if v != VerdictUnknown {
31 t.Fatalf("Verdict on an unscanned object = %q, want %q", v, VerdictUnknown)
32 }
33
34 for i := 0; i < 2; i++ {
35 if _, _, err := s.Scan(ctx, bucket, "a.txt", "etag-1", 10); err != nil {
36 t.Fatalf("Scan call %d: %v", i+1, err)
37 }
38 }
39
40 // A verdict is bound to the bytes it judged. The same key at a different
41 // etag has no verdict, so the sweep rescans rather than letting a
42 // re-uploaded object inherit its predecessor's result.
43 if v, err := s.Verdict(ctx, bucket, "a.txt", "etag-2"); err != nil || v != VerdictUnknown {
44 t.Fatalf("Verdict at a changed etag = (%q, %v), want (%q, nil)", v, err, VerdictUnknown)
45 }
46
47 for i := 0; i < 2; i++ {
48 if err := s.EnsureDisabled(ctx, bucket); err != nil {
49 t.Fatalf("EnsureDisabled call %d: %v", i+1, err)
50 }
51 }
52}
53
54func TestMemScannerContract(t *testing.T) {
55 scannerContract(t, NewMemScanner(), "carlos-store-tito-bab-titogo")
56}
57
58// Status and Verdict answer DIFFERENT QUESTIONS, and the fake has to hold that
59// line as firmly as the real engine does — otherwise the drain's own re-issue
60// test passes against a fake that already re-issued for it, and the split is
61// unpinned on the only implementation the queue tests ever see.
62//
63// Verdict applies the sweep's retry policy: a stored `failed` or `denied`
64// becomes Unknown so the next sweep re-issues it. Status reports what the tag
65// says, because "what is known about this object" is not "what should the
66// sweep do next" — left the other way, a failed scan reads as pending forever.
67func TestMemScannerStatusReportsWhatIsStored(t *testing.T) {
68 ctx := context.Background()
69 s := NewMemScanner()
70 const bucket, key = "b", "uploads/x"
71 mod := time.Date(2026, 9, 2, 8, 0, 0, 0, time.UTC)
72
73 // An object the scanner has never heard of is GONE, not unknown: the
74 // drain deletes the item rather than queueing an engine read.
75 if _, _, err := s.Status(ctx, bucket, key); !errors.Is(err, ErrObjectGone) {
76 t.Fatalf("Status on an absent object = %v, want ErrObjectGone", err)
77 }
78
79 s.PutObject(bucket, key, "etag-1", mod, 10)
80 v, obj, err := s.Status(ctx, bucket, key)
81 if err != nil {
82 t.Fatal(err)
83 }
84 if v != VerdictUnknown {
85 t.Fatalf("an unscanned object = %q, want unknown", v)
86 }
87 // The object comes back so the caller can Scan it without a head of its
88 // own — the whole reason Status returns one.
89 if obj.ETag != "etag-1" || obj.Size != 10 || obj.Version() == "" {
90 t.Fatalf("Status returned %+v, want the object a listing would have described", obj)
91 }
92
93 for _, stored := range []Verdict{VerdictFailed, VerdictDenied} {
94 s.Put(bucket, key, obj.Version(), stored)
95 if v, _, err := s.Status(ctx, bucket, key); err != nil || v != stored {
96 t.Fatalf("Status over a stored %q = (%q, %v) — Status is applying the re-issue rule", stored, v, err)
97 }
98 if v, err := s.Verdict(ctx, bucket, key, obj.Version()); err != nil || v != VerdictUnknown {
99 t.Fatalf("Verdict over a stored %q = (%q, %v), want unknown so the sweep re-issues it", stored, v, err)
100 }
101 }
102
103 // A terminal verdict is reported by both, unchanged.
104 s.Put(bucket, key, obj.Version(), VerdictInfected)
105 if v, _, _ := s.Status(ctx, bucket, key); v != VerdictInfected {
106 t.Fatalf("Status over infected = %q", v)
107 }
108 if v, _ := s.Verdict(ctx, bucket, key, obj.Version()); v != VerdictInfected {
109 t.Fatalf("Verdict over infected = %q", v)
110 }
111}
112
113// The seeded-verdict path gets the same etag treatment as the scanned one, so
114// a fixture cannot accidentally set up a verdict that outlives its bytes.
115func TestMemScannerSeededVerdictIsETagBound(t *testing.T) {
116 ctx := context.Background()
117 s := NewMemScanner()
118 s.Put("b", "k", "etag-1", VerdictInfected)
119
120 if v, _ := s.Verdict(ctx, "b", "k", "etag-1"); v != VerdictInfected {
121 t.Fatalf("same etag = %q, want %q", v, VerdictInfected)
122 }
123 if v, _ := s.Verdict(ctx, "b", "k", "etag-2"); v != VerdictUnknown {
124 t.Fatalf("changed etag = %q, want %q", v, VerdictUnknown)
125 }
126}
127
128// The failure hook arms exactly one failure, so a walk test can prove the
129// sweep survives a mid-flight engine error and does not retry forever.
130func TestMemScannerFailureIsOneShot(t *testing.T) {
131 ctx := context.Background()
132 s := NewMemScanner()
133 s.Fail("EnsureEnabled", context.DeadlineExceeded)
134
135 if err := s.EnsureEnabled(ctx, "b"); err == nil {
136 t.Fatal("armed failure did not fire")
137 }
138 if err := s.EnsureEnabled(ctx, "b"); err != nil {
139 t.Fatalf("failure fired twice: %v", err)
140 }
141}
142