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
169 lines
7.2 KiB
Go
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)
|
|
}
|
|
}
|