package host import ( "context" "errors" "testing" "github.com/carlosframework/platform/internal/release" ) // bsGetCounter counts reads, so a test can see a write that needed none. type bsGetCounter struct { release.Store gets int } func (c *bsGetCounter) Get(ctx context.Context, key string) ([]byte, string, error) { c.gets++ return c.Store.Get(ctx, key) } func countingBuildStates(t *testing.T) (*buildStates, *bsGetCounter) { t.Helper() b, s, _ := testBuildStates(t) cs := &bsGetCounter{Store: s} b.storeFor = storesOf(map[string]release.Store{"hib": cs}) return b, cs } // The state this agent last wrote is the one a conditional write can go // on: a wake's record is then one round trip, not a read and a write. func TestBuildStateWritesOnItsOwnLastCopyWithoutARead(t *testing.T) { b, cs := countingBuildStates(t) ctx := context.Background() if err := put(t, b, ctx, "a.example.com", release.BuildState{LastExecuted: "v1", Completed: 1}); err != nil { t.Fatal(err) } cs.gets = 0 got, err := b.update(ctx, "a.example.com", bump) if err != nil { t.Fatal(err) } if cs.gets != 0 { t.Errorf("the write read the object %d time(s); its own last copy was current", cs.gets) } if got.Completed != 2 || got.LastExecuted != "v1" { t.Errorf("state = %+v, want the cached copy bumped", got) } cur, _, err := getState(ctx, cs.Store, "inst/a.example.com/build-state.json", "a.example.com") if err != nil || cur == nil || cur.Completed != 2 { t.Fatalf("object = %+v, %v", cur, err) } } // Another writer moved the object on: the conditional write fails, and the // write reads it and merges on what is really there. func TestBuildStateCopyThatMovedOnIsReadAgain(t *testing.T) { b, cs := countingBuildStates(t) ctx := context.Background() if err := put(t, b, ctx, "a.example.com", release.BuildState{LastExecuted: "v1", Completed: 1}); err != nil { t.Fatal(err) } key := "inst/a.example.com/build-state.json" 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 { t.Fatal(err) } cs.gets = 0 got, err := b.update(ctx, "a.example.com", bump) if err != nil { t.Fatal(err) } if cs.gets == 0 { t.Error("the write did not read an object that had moved on") } if got.Completed != 6 || got.LastExecuted != "v9" { t.Errorf("state = %+v, want the other writer's state bumped", got) } } // A merge that refuses on the cached copy is asked again on a fresh read: // the copy may be stale, and a refusal on stale state would be wrong. func TestBuildStateRefusalOnTheCachedCopyIsCheckedAgainstAFreshRead(t *testing.T) { b, cs := countingBuildStates(t) ctx := context.Background() if err := put(t, b, ctx, "a.example.com", release.BuildState{LastExecuted: "v1", Completed: 1}); err != nil { t.Fatal(err) } key := "inst/a.example.com/build-state.json" 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 { t.Fatal(err) } errStale := errors.New("stale") got, err := b.write(ctx, "a.example.com", func(cur *release.BuildState) (release.BuildState, bool, error) { if cur != nil && cur.Completed == 1 { return release.BuildState{}, false, errStale } return *cur, true, nil }, nil) if err != nil { t.Fatalf("write = %v; the refusal came from the cached copy, not the object", err) } if got.Completed != 2 { t.Errorf("state = %+v", got) } } // An object that no longer parses is never written over (R-BS-CORRUPT), // cached copy or not: the conditional write fails and the read refuses. func TestBuildStateCachedCopyNeverWritesOverACorruptObject(t *testing.T) { b, cs := countingBuildStates(t) ctx := context.Background() if err := put(t, b, ctx, "a.example.com", release.BuildState{LastExecuted: "v1"}); err != nil { t.Fatal(err) } key := "inst/a.example.com/build-state.json" if _, err := cs.Store.Put(ctx, key, []byte("{"), mustEtag(t, cs.Store, key)); err != nil { t.Fatal(err) } if _, err := b.update(ctx, "a.example.com", bump); err == nil { t.Fatal("wrote over an object that does not parse") } raw, _, err := cs.Store.Get(ctx, key) if err != nil || string(raw) != "{" { t.Fatalf("object = %q, %v", raw, err) } }