mesh/merge-gate head bfc64ac65ea1 merged into trunk 26de6bc62558 — pass: builds mesh-delivery-indexer, new: modules/mesh-delivery-builder → novox; no bus s…
mesh/repo-check head bfc64ac65ea1 merged into trunk 26de6bc62558 — pass: its merge-check.sh passed
mesh/delivery delivered
186 lines
6.4 KiB
Go
186 lines
6.4 KiB
Go
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)
|
|
}
|