package build import ( "context" "errors" "sync" "testing" "git.novox.be/novox/mesh-sdk/go/delivery" ) // memStore is a bucket's create and compare-and-set in memory, with a revision per write. type memStore struct { mu sync.Mutex vals map[string][]byte revs map[string]uint64 next uint64 writes int // before runs once ahead of the next write, so a test changes the key under the writer. before func() } func newMem() *memStore { return &memStore{vals: map[string][]byte{}, revs: map[string]uint64{}} } func (m *memStore) Get(_ context.Context, key string) ([]byte, uint64, bool, error) { m.mu.Lock() defer m.mu.Unlock() v, ok := m.vals[key] return v, m.revs[key], ok, nil } func (m *memStore) hook() { if h := m.before; h != nil { m.before = nil h() } } func (m *memStore) put(key string, value []byte) uint64 { m.next++ m.writes++ m.vals[key], m.revs[key] = value, m.next return m.next } func (m *memStore) Create(_ context.Context, key string, value []byte) (uint64, error) { m.mu.Lock() defer m.mu.Unlock() m.hook() if _, ok := m.vals[key]; ok { return 0, ErrConflict } return m.put(key, value), nil } func (m *memStore) Update(_ context.Context, key string, value []byte, rev uint64) (uint64, error) { m.mu.Lock() defer m.mu.Unlock() m.hook() if _, ok := m.vals[key]; !ok || m.revs[key] != rev { return 0, ErrConflict } return m.put(key, value), nil } const ( fp1 = "src2:1111111111111111111111111111111111111111111111111111111111111111" digest = "sha256:2222222222222222222222222222222222222222222222222222222222222222" ) func seed(t *testing.T, st *memStore, b delivery.BuilderBuild) string { t.Helper() key, err := delivery.BuilderBuildKey("m", fp1) if err != nil { t.Fatal(err) } data, err := delivery.EncodeBuilderBuild(b) if err != nil { t.Fatal(err) } st.put(key, data) return key } func valueOf(t *testing.T, st *memStore, key string) delivery.BuilderBuild { t.Helper() b, err := delivery.DecodeBuilderBuild(st.vals[key]) if err != nil { t.Fatal(err) } return b } func TestClaimCreatesTheKeyBuildingAndASecondBuilderGetsNothing(t *testing.T) { // 1607/AC3 st := newMem() a, b := Claimer{Store: st, By: "novox.a"}, Claimer{Store: st, By: "novox.b"} got, err := a.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{}) if err != nil || got == nil { t.Fatalf("first claim = %v, %v, want a claim", got, err) } if v := valueOf(t, st, got.Key); v.State != delivery.BuildBuilding || v.By != "novox.a" || v.Manifest != hex64('a') { t.Errorf("the key holds %+v, want building by novox.a with the manifest digest", v) } if second, err := b.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{}); err != nil || second != nil { t.Errorf("second builder's claim = %v, %v, want none: the same source is never built twice", second, err) } } func TestClaimLosingTheCreateRaceGetsNothing(t *testing.T) { // 1607/AC3 st := newMem() st.before = func() { seed(t, st, delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.b"}) } got, err := Claimer{Store: st, By: "novox.a"}.Claim(context.Background(), "m", fp1, "", ClaimOptions{}) if err != nil || got != nil { t.Errorf("claim = %v, %v, want none: the create failed because the key exists", got, err) } } func TestADoneKeyIsNeverTakenAgain(t *testing.T) { // 1607/AC3 st := newMem() seed(t, st, delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.a", Manifest: hex64('a'), Artifacts: []delivery.BuiltArtifact{{Name: "x", Digest: digest}}}) writes := st.writes got, err := Claimer{Store: st, By: "novox.a"}.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{}) if err != nil || got != nil || st.writes != writes { t.Errorf("claim = %v, %v, writes %d (was %d), want none and no write", got, err, st.writes, writes) } } func TestAFailedKeyIsTakenAgainAsANewRevisionExceptARefusedKind(t *testing.T) { // 1607/AC3, ADR 0478 clause 2 st := newMem() key := seed(t, st, delivery.BuilderBuild{State: delivery.BuildFailed, By: "novox.a", Error: "the step failed"}) rev := st.revs[key] got, err := Claimer{Store: st, By: "novox.a"}.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{}) if err != nil || got == nil || st.revs[key] <= rev { t.Fatalf("claim = %v, %v, revision %d (was %d), want a claim as a new revision of the same key", got, err, st.revs[key], rev) } if v := valueOf(t, st, key); v.State != delivery.BuildBuilding || v.Error != "" { t.Errorf("the key holds %+v, want building with no error", v) } st = newMem() key = seed(t, st, delivery.BuilderBuild{State: delivery.BuildFailed, By: "novox.a", Error: RefusedPrefix + " image (artifact x): build-agent builds it"}) writes := st.writes if got, err := (Claimer{Store: st, By: "novox.a"}).Claim(context.Background(), "m", fp1, "", ClaimOptions{}); err != nil || got != nil || st.writes != writes { t.Errorf("a refused kind: claim = %v, %v, writes %d (was %d), want none and no write", got, err, st.writes, writes) } } func TestFinishMovesTheKeyByCompareAndSetAndWritesOnlyADifferentValue(t *testing.T) { // 1607/AC3 st := newMem() c := Claimer{Store: st, By: "novox.a"} got, _ := c.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{}) done := delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.a", Manifest: hex64('a'), Artifacts: []delivery.BuiltArtifact{{Name: "x", Kind: "archive", Digest: digest}}} if err := c.Finish(context.Background(), got, done); err != nil { t.Fatal(err) } if v := valueOf(t, st, got.Key); v.State != delivery.BuildDone || v.Artifacts[0].Digest != digest { t.Errorf("the key holds %+v", v) } writes := st.writes if err := c.Finish(context.Background(), got, done); err != nil && !errors.Is(err, ErrConflict) { t.Fatal(err) } if st.writes != writes { t.Errorf("the same value was written again (%d writes, was %d)", st.writes, writes) } } func TestFinishAfterTheKeyChangedIsAConflictNotAnOverwrite(t *testing.T) { // 1607/AC3 st := newMem() c := Claimer{Store: st, By: "novox.a"} got, _ := c.Claim(context.Background(), "m", fp1, "", ClaimOptions{}) st.put(got.Key, []byte(`{"v":1,"state":"failed","error":"someone else"}`)) err := c.Finish(context.Background(), got, delivery.BuilderBuild{State: delivery.BuildFailed, Error: "mine"}) if !errors.Is(err, ErrConflict) { t.Errorf("Finish = %v, want ErrConflict", err) } } func hex64(c byte) string { b := make([]byte, 64) for i := range b { b[i] = c } return string(b) }