carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "testing"
7
8 "github.com/carlosframework/platform/internal/release"
9)
10
11// bsGetCounter counts reads, so a test can see a write that needed none.
12type bsGetCounter struct {
13 release.Store
14 gets int
15}
16
17func (c *bsGetCounter) Get(ctx context.Context, key string) ([]byte, string, error) {
18 c.gets++
19 return c.Store.Get(ctx, key)
20}
21
22func countingBuildStates(t *testing.T) (*buildStates, *bsGetCounter) {
23 t.Helper()
24 b, s, _ := testBuildStates(t)
25 cs := &bsGetCounter{Store: s}
26 b.storeFor = storesOf(map[string]release.Store{"hib": cs})
27 return b, cs
28}
29
30// The state this agent last wrote is the one a conditional write can go
31// on: a wake's record is then one round trip, not a read and a write.
32func TestBuildStateWritesOnItsOwnLastCopyWithoutARead(t *testing.T) {
33 b, cs := countingBuildStates(t)
34 ctx := context.Background()
35 if err := put(t, b, ctx, "a.example.com", release.BuildState{LastExecuted: "v1", Completed: 1}); err != nil {
36 t.Fatal(err)
37 }
38 cs.gets = 0
39 got, err := b.update(ctx, "a.example.com", bump)
40 if err != nil {
41 t.Fatal(err)
42 }
43 if cs.gets != 0 {
44 t.Errorf("the write read the object %d time(s); its own last copy was current", cs.gets)
45 }
46 if got.Completed != 2 || got.LastExecuted != "v1" {
47 t.Errorf("state = %+v, want the cached copy bumped", got)
48 }
49 cur, _, err := getState(ctx, cs.Store, "inst/a.example.com/build-state.json", "a.example.com")
50 if err != nil || cur == nil || cur.Completed != 2 {
51 t.Fatalf("object = %+v, %v", cur, err)
52 }
53}
54
55// Another writer moved the object on: the conditional write fails, and the
56// write reads it and merges on what is really there.
57func TestBuildStateCopyThatMovedOnIsReadAgain(t *testing.T) {
58 b, cs := countingBuildStates(t)
59 ctx := context.Background()
60 if err := put(t, b, ctx, "a.example.com", release.BuildState{LastExecuted: "v1", Completed: 1}); err != nil {
61 t.Fatal(err)
62 }
63 key := "inst/a.example.com/build-state.json"
64 if _, err := cs.Store.Put(ctx, key, []byte(`{"schema":1,"host":"a.example.com","last_executed":"v9","completed":5}`), mustEtag(t, cs.Store, key)); err != nil {
65 t.Fatal(err)
66 }
67 cs.gets = 0
68 got, err := b.update(ctx, "a.example.com", bump)
69 if err != nil {
70 t.Fatal(err)
71 }
72 if cs.gets == 0 {
73 t.Error("the write did not read an object that had moved on")
74 }
75 if got.Completed != 6 || got.LastExecuted != "v9" {
76 t.Errorf("state = %+v, want the other writer's state bumped", got)
77 }
78}
79
80// A merge that refuses on the cached copy is asked again on a fresh read:
81// the copy may be stale, and a refusal on stale state would be wrong.
82func TestBuildStateRefusalOnTheCachedCopyIsCheckedAgainstAFreshRead(t *testing.T) {
83 b, cs := countingBuildStates(t)
84 ctx := context.Background()
85 if err := put(t, b, ctx, "a.example.com", release.BuildState{LastExecuted: "v1", Completed: 1}); err != nil {
86 t.Fatal(err)
87 }
88 key := "inst/a.example.com/build-state.json"
89 if _, err := cs.Store.Put(ctx, key, []byte(`{"schema":1,"host":"a.example.com","last_executed":"v1","completed":2}`), mustEtag(t, cs.Store, key)); err != nil {
90 t.Fatal(err)
91 }
92 errStale := errors.New("stale")
93 got, err := b.write(ctx, "a.example.com", func(cur *release.BuildState) (release.BuildState, bool, error) {
94 if cur != nil && cur.Completed == 1 {
95 return release.BuildState{}, false, errStale
96 }
97 return *cur, true, nil
98 }, nil)
99 if err != nil {
100 t.Fatalf("write = %v; the refusal came from the cached copy, not the object", err)
101 }
102 if got.Completed != 2 {
103 t.Errorf("state = %+v", got)
104 }
105}
106
107// An object that no longer parses is never written over (R-BS-CORRUPT),
108// cached copy or not: the conditional write fails and the read refuses.
109func TestBuildStateCachedCopyNeverWritesOverACorruptObject(t *testing.T) {
110 b, cs := countingBuildStates(t)
111 ctx := context.Background()
112 if err := put(t, b, ctx, "a.example.com", release.BuildState{LastExecuted: "v1"}); err != nil {
113 t.Fatal(err)
114 }
115 key := "inst/a.example.com/build-state.json"
116 if _, err := cs.Store.Put(ctx, key, []byte("{"), mustEtag(t, cs.Store, key)); err != nil {
117 t.Fatal(err)
118 }
119 if _, err := b.update(ctx, "a.example.com", bump); err == nil {
120 t.Fatal("wrote over an object that does not parse")
121 }
122 raw, _, err := cs.Store.Get(ctx, key)
123 if err != nil || string(raw) != "{" {
124 t.Fatalf("object = %q, %v", raw, err)
125 }
126}
127