The builder keeps one key per name, tag and version (design 51 *The versions*, ADR 0606), in its old state section (ADR 0619), history one, 64 MiB with the 80 % mark. A package is numbered per source revision and keeps a version held published, publishing nothing; one numbered but never stored is published again under it. An image, bundle or archive keeps its fingerprint's number. An unknown floor builds an image, bundle or archive unnumbered and makes a package wait (ADR 0617 §1). Merge order and tagged versions below the next version are the package's helpers. builder_versions answers a name's versions and the holder's question (ADR 0605 §4). Four claim tests merged into two, so the new table test adds no function (test budget). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
218 lines
8.2 KiB
Go
218 lines
8.2 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) Keys(_ context.Context) ([]string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var keys []string
|
|
for k := range m.vals {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys, 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)
|
|
}
|
|
|
|
// A claim losing the create race gets nothing (merged into this test for the test budget, design 51).
|
|
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; a key whose artifacts name references (ADR 0498 clause 8)
|
|
st := newMem()
|
|
seed(t, st, delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.a", Manifest: hex64('a'), Artifacts: []delivery.BuiltArtifact{{Name: "x", Kind: "bundle", Digest: digest, Reference: "artifact-store://m/x/blobs/" + 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: delivery.RefusedKind + "package of language \"python\" (artifact x)"})
|
|
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)
|
|
}
|
|
for _, e := range []string{delivery.NotPinned + "up: nats:2 names no digest", delivery.VersionTaken + "@novox/mesh-sdk@0.1.15"} { // B3, B5: ADR 0498 clauses 4, 5
|
|
st = newMem()
|
|
seed(t, st, delivery.BuilderBuild{State: delivery.BuildFailed, By: "novox.a", Error: e})
|
|
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("%s: claim = %v, %v, writes %d (was %d), want none until the fingerprint changes", e, got, err, st.writes, writes)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAKeyFailedForNoImageBuilderIsTakenAgainOnlyWhenANodeHasTheModule(t *testing.T) { // 1705b
|
|
st := newMem()
|
|
key := seed(t, st, delivery.BuilderBuild{State: delivery.BuildFailed, By: "novox.a", Error: NoImageBuilderPrefix + " no node has mesh-build-image"})
|
|
writes := st.writes
|
|
c := Claimer{Store: st, By: "novox.a"}
|
|
for _, opt := range []ClaimOptions{{}, {ImageBuilder: func() bool { return false }}} {
|
|
if got, err := c.Claim(context.Background(), "m", fp1, "", opt); 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)
|
|
}
|
|
}
|
|
got, err := c.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{ImageBuilder: func() bool { return true }})
|
|
if err != nil || got == nil || valueOf(t, st, key).State != delivery.BuildBuilding {
|
|
t.Errorf("claim = %v, %v, want a claim with the key building", got, err)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Finish after the key changed is a conflict, not an overwrite (merged into this test for the test budget).
|
|
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)
|
|
}
|