Files
mesh-delivery/modules/mesh-delivery-builder/internal/versions/reserve_test.go
T
jschoubben 8ea78f709a BL1: the whole package-published per artifact on the registry seat's published, from the reservation and the step result
When a registry seat says published, the builder says the seat mesh-builds' package-published once per artifact the reservation
newly marks: the artifact reference, version and digest published, the watched branch and the source revision kept on the
reservation from the entry the build came from (A-57), and the dependencies of kind 1 (resolved) and 3 (toolchain) the step result
named (§466, §673), their parts the artifact references the entry's edges name. The reservation keeps the dependencies from the
build's end until the publication.

Builds: design 47 G §52, §334, §349, §371, §466, §673, A-57
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-11 23:30:45 +01:00

169 lines
7.2 KiB
Go

package versions
import (
"encoding/json"
"context"
"errors"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
"reflect"
"testing"
"time"
)
type reserveMem struct {
*mem
onUpdate func() // runs once before the next Update
}
func (m *reserveMem) Delete(_ context.Context, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.vals, key)
delete(m.revs, key)
return nil
}
func (m *reserveMem) Update(ctx context.Context, key string, value []byte, rev uint64) (uint64, error) {
if f := m.onUpdate; f != nil {
m.onUpdate = nil
f()
}
return m.mem.Update(ctx, key, value, rev)
}
// TestReservation: design 47 G §369 (test 102): two builds close together reserve two versions, the key goes on
// version-released, a built reservation with an artifact unpublished past the bound is unreleased, and nothing is reserved
// before versions-filled.
func TestReservation(t *testing.T) {
ctx := context.Background()
now := time.Date(2026, 11, 11, 12, 0, 0, 0, time.UTC)
released := []string{"0.1.18", "0.1.19", "0.2.7"}
filled := true
store := &reserveMem{mem: newMem()}
r := &Reserver{Store: store, Filled: func() bool { return filled }, Now: func() time.Time { return now },
ReleasedOf: func(context.Context, string) ([]string, error) { return released, nil }}
arts := []string{"m:a", "m:b"}
filled = false
if _, err := r.Reserve(ctx, "m", "0.1", arts, Source{}); !errors.Is(err, ErrNotFilled) {
t.Fatalf("before versions-filled: %v, want ErrNotFilled", err)
}
if got, _ := r.All(ctx); len(got) != 0 {
t.Fatalf("a reservation was written before the fill: %v", got)
}
filled = true
first, err := r.Reserve(ctx, "m", "0.1", arts, Source{})
if err != nil || first.Version != "0.1.20" {
t.Fatalf("first build reserves %q (%v), want 0.1.20 after the highest released 0.1.19", first.Version, err)
}
second, err := r.Reserve(ctx, "m", "0.1", arts, Source{})
if err != nil || second.Version != "0.1.21" {
t.Fatalf("second build reserves %q (%v), want 0.1.21: never the first's", second.Version, err)
}
// Two builds at once: the rival reserves between this one's read and write, and this one takes the next.
store.onUpdate = func() {
if _, err := r.Reserve(ctx, "m", "0.1", arts, Source{}); err != nil {
t.Error(err)
}
}
third, err := r.Reserve(ctx, "m", "0.1", arts, Source{})
if err != nil || third.Version != "0.1.23" {
t.Fatalf("a build that lost the compare-and-set reserves %q (%v), want 0.1.23", third.Version, err)
}
if v, _ := r.Reserve(ctx, "m", "0.3", arts, Source{}); v.Version != "0.3.0" {
t.Errorf("a series with nothing released reserves %q, want 0.3.0", v.Version)
}
// The artifacts' digests, then the bound.
for _, a := range arts {
if ok, err := r.Built(ctx, "m", "0.1.23", a, "sha256:"+a, nil); err != nil || !ok {
t.Fatalf("built %s: %v %v", a, ok, err)
}
}
if ok, _ := r.Built(ctx, "m", "0.1.22", "m:a", "x", nil); ok {
t.Error("a digest was written under a version a newer build superseded")
}
published := func(_, _, a string) bool { return a == "m:a" }
if got, _ := r.Overdue(ctx, published); len(got) != 0 {
t.Fatalf("unreleased before the bound: %v", got)
}
now = now.Add(UnreleasedBound)
got, err := r.Overdue(ctx, published)
want := []Unreleased{{Module: "m", Version: "0.1.23", Missing: []string{"m:b"}}}
if err != nil || !reflect.DeepEqual(got, want) {
t.Fatalf("past the bound: %v (%v), want %v", got, err, want)
}
// version-released: its key goes, and the other series' keys of the module (the manifest moved on).
if gone, err := r.Released(ctx, "m", "0.1.23"); err != nil || len(gone) != 2 {
t.Fatalf("released 0.1.23 deleted %v (%v), want the 0.1 key and the 0.3 key", gone, err)
}
if all, _ := r.All(ctx); len(all) != 0 {
t.Errorf("reservations left: %v", all)
}
if again, _ := r.Reserve(ctx, "m", "0.1", arts, Source{}); again.Version != "0.1.20" {
t.Logf("with the catalog list unchanged the next is %s", again.Version)
}
if ok, _ := r.Drop(ctx, "m", "0.1"); !ok {
t.Error("builder_reserved_drop found no reservation")
}
}
// TestReservationIsReadAndMarkedPublished: design 47 G §369 — the reservation is read by builder_reserved and by the seat's
// verb reserved with each artifact's digest, a registry seat's published marks the artifacts of that version and digest, and
// the missing artifacts it names are the unpublished ones.
func TestReservationIsReadAndMarkedPublished(t *testing.T) {
ctx := context.Background()
store := &reserveMem{mem: newMem()}
r := &Reserver{Store: store, ReleasedOf: func(context.Context, string) ([]string, error) { return []string{"1.4.2"}, nil }}
v, err := r.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, Source{Branch: "main", Revision: 41})
if err != nil || v.Version != "1.4.3" {
t.Fatalf("reserved %q (%v), want 1.4.3", v.Version, err)
}
deps := []meshbuilds.Dependency{{Kind: 1, Part: "mesh-sdk:go", Version: "0.1.14", Digest: "sha256:dd"}, {Kind: 3, Part: "go-toolchain:program", Version: "go1.27", Digest: "sha256:tt"}}
if _, err := r.Built(ctx, "m", "1.4.3", "m:a", "sha256:aa", deps); err != nil {
t.Fatal(err)
}
rd := Reader{
Keys: func() ([]string, error) { return store.KeysOf(ctx, AllFilter) },
Get: func(key string) (json.RawMessage, bool, error) {
raw, _, ok, err := store.Get(ctx, key)
return raw, ok, err
}}
ans, err := AnswerReservedVerb(rd, "m", "")
want := meshbuilds.ReservedAnswer{Module: "m", Version: "1.4.3", Artifacts: []meshbuilds.ReservedAnswerArtifactsItem{{Artifact: "m:a", Digest: "sha256:aa"}, {Artifact: "m:b"}}}
if err != nil || !reflect.DeepEqual(ans, want) {
t.Fatalf("reserved answers %+v (%v), want %+v", ans, err, want)
}
if _, err := AnswerReservedVerb(rd, "m", "1.4.9"); err == nil {
t.Error("a version never reserved was answered")
}
if got, err := AnswerReserved(rd, "other"); err != nil || len(got["reserved"].([]ReservedVersion)) != 0 {
t.Errorf("builder_reserved of another module = %v (%v), want none", got, err)
}
marked, err := r.Published(ctx, "v1.4.3", "sha256:aa")
if err != nil || len(marked) != 1 {
t.Fatalf("a published of 1.4.3 with a built digest marked %v (%v)", marked, err)
}
// Design 47 G §52, §673, A-57: the package-published of that artifact carries the digest published, the branch and the source
// revision the reservation was made for, and the dependencies its step result named.
at := time.Unix(100, 0)
said := marked[0].PackagePublished("sha256:aa", at)
atUTC := at.UTC()
if wantSaid := (meshbuilds.PackagePublished{V: 1, Artifact: "m:a", At: &atUTC, Branch: "main", Source: 41, Module: "m", Version: "1.4.3", Digest: "sha256:aa", Dependencies: deps}); !reflect.DeepEqual(said, wantSaid) {
t.Errorf("package-published = %+v, want %+v", said, wantSaid)
}
if marked, _ := r.Published(ctx, "1.4.3", "sha256:other"); len(marked) != 0 {
t.Errorf("a published with another digest marked %v", marked)
}
cur, _, _ := r.At(ctx, ReservedKey("m", "1.4"))
if got := cur.Missing(); !reflect.DeepEqual(got, []string{"m:b"}) {
t.Errorf("missing = %v, want m:b", got)
}
if key, ok := ReservedBoundOf(ReservedBoundKey("m", "1.4")); !ok || key != ReservedKey("m", "1.4") || ReservedBoundFor(key) != ReservedBoundKey("m", "1.4") {
t.Errorf("the bound key does not name the reservation back: %q %v", key, ok)
}
}