The builder finds a build module as the indexer's seed does: the build seat whose acting rule `build` has an `on` matching the artifact's language or kind:image, and that seat's holder, read from the seat list's definitions and the claims of the catalog half's manifests. Task.Module, every lane key, the unpublished modules, the toolchain sources, the ends watched, the sayer and builder_stuck (through `<seat>.steps`) derive from it; the floor's sources are the holders of the seats git and mesh-catalog. buildrun.GoBuildModule, NodeBuildModule, LanguageOf, buildstep.BuildModuleOf use, build.ImageBuildModule and StuckBuildModules are gone from the builder (the checker keeps its own names in two small files until its part reads them from the list). Builds: design 47 G §7, §428, §704, §204, A-82 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
641 lines
31 KiB
Go
641 lines
31 KiB
Go
package versions
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"maps"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms"
|
|
)
|
|
|
|
// mem is a Store in memory; before runs once before the next Create, to stage a race.
|
|
type mem struct {
|
|
mu sync.Mutex
|
|
vals map[string][]byte
|
|
revs map[string]uint64
|
|
rev uint64
|
|
before func()
|
|
down error // answered by KeysOf: a state that cannot be read
|
|
}
|
|
|
|
func newMem() *mem { return &mem{vals: map[string][]byte{}, revs: map[string]uint64{}} }
|
|
|
|
func (m *mem) 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 *mem) Create(ctx context.Context, key string, value []byte) (uint64, error) {
|
|
if f := m.before; f != nil {
|
|
m.before = nil
|
|
f()
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if _, ok := m.vals[key]; ok {
|
|
return 0, ErrConflict
|
|
}
|
|
m.rev++
|
|
m.vals[key], m.revs[key] = value, m.rev
|
|
return m.rev, nil
|
|
}
|
|
|
|
func (m *mem) Update(_ context.Context, key string, value []byte, revision uint64) (uint64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.revs[key] != revision {
|
|
return 0, ErrConflict
|
|
}
|
|
m.rev++
|
|
m.vals[key], m.revs[key] = value, m.rev
|
|
return m.rev, nil
|
|
}
|
|
|
|
func (m *mem) KeysOf(_ context.Context, filter string) ([]string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.down != nil {
|
|
return nil, m.down
|
|
}
|
|
prefix := strings.TrimSuffix(filter, "*")
|
|
var out []string
|
|
for k := range m.vals {
|
|
if filter == AllFilter {
|
|
out = append(out, k)
|
|
continue
|
|
}
|
|
if strings.HasPrefix(k, prefix) && !strings.Contains(k[len(prefix):], ".") {
|
|
out = append(out, k)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// keeperMem is the state floor-waits in memory.
|
|
type keeperMem map[string][]byte
|
|
|
|
func (k keeperMem) Put(key string, v []byte) error { k[key] = v; return nil }
|
|
func (k keeperMem) Delete(key string) error { delete(k, key); return nil }
|
|
func (k keeperMem) keys() ([]string, error) {
|
|
var out []string
|
|
for key := range k {
|
|
out = append(out, key)
|
|
}
|
|
return out, nil
|
|
}
|
|
func (k keeperMem) get(key string) (json.RawMessage, bool, error) { v, ok := k[key]; return v, ok, nil }
|
|
|
|
// packagesMem is the indexer's packages state as the builder reads it, in memory.
|
|
type packagesMem map[string][]byte
|
|
|
|
func (p packagesMem) Keys(context.Context) ([]string, error) {
|
|
if p == nil {
|
|
return nil, errors.New("not readable")
|
|
}
|
|
return slices.Collect(maps.Keys(p)), nil
|
|
}
|
|
|
|
func (p packagesMem) Get(_ context.Context, key string) ([]byte, bool, error) {
|
|
v, ok := p[key]
|
|
return v, ok, nil
|
|
}
|
|
|
|
func (p packagesMem) failing() packagesMem { return nil }
|
|
|
|
func known(seat, tags, reqs []string) Floor { return Floor{Seat: seat, Tags: tags, Requirements: reqs} }
|
|
|
|
// TestTheBuildersVersions holds design 51 *The versions*, *The floor* and *Tagged versions* (DLV-B1, DLV-B2, DLV-B3):
|
|
// one table per rule, the unit cases of the builder's numbering.
|
|
func TestTheBuildersVersions(t *testing.T) {
|
|
t.Run("keys", func(t *testing.T) {
|
|
names := []string{"@novox/mesh-sdk", "git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/mesh-sdk", "a=40b", "a@b"}
|
|
seen := map[string]string{}
|
|
for _, n := range names {
|
|
k := Key(n, "0.1.14")
|
|
if strings.Count(k, ".") != 1 {
|
|
t.Errorf("Key(%q) = %q, want two tokens", n, k)
|
|
}
|
|
if o, dup := seen[k]; dup {
|
|
t.Errorf("%q and %q escape alike: %q", n, o, k)
|
|
}
|
|
seen[k] = n
|
|
if !strings.HasPrefix(k, strings.TrimSuffix(NameFilter(n), "*")) {
|
|
t.Errorf("Key(%q) = %q is not under NameFilter %q", n, k, NameFilter(n))
|
|
}
|
|
}
|
|
})
|
|
t.Run("floor", func(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name string
|
|
floor Floor
|
|
want string
|
|
}{
|
|
{"none starts at 0", known(nil, nil, nil), "0.1.0"},
|
|
{"the Go tag of today gives v0.1.14", known(nil, []string{"go/v0.1.13"}, []string{"v0.1.14-0.20261103221822-85ba0a9a70d8"}), "0.1.14"},
|
|
{"npm's 0.1.17 on the seat gives 0.1.18", known([]string{"0.1.17", "0.1.3"}, nil, nil), "0.1.18"},
|
|
{"another series and a tagged version count nothing", known([]string{"0.2.40", "0.1.9-feat-x.1"}, nil, nil), "0.1.0"},
|
|
{"the highest of the three", known([]string{"0.1.4"}, []string{"v0.1.9"}, []string{"v0.1.7"}), "0.1.10"},
|
|
} {
|
|
if got := Next(nil, "0.1", c.floor); got != c.want {
|
|
t.Errorf("%s: Next = %s, want %s", c.name, got, c.want)
|
|
}
|
|
}
|
|
if got := Next([]Entry{{Version: "0.1.20", Series: "0.1"}, {Version: "0.1.21-x.1", Series: "0.1", Tag: "x"}}, "0.1", known([]string{"0.1.3"}, nil, nil)); got != "0.1.21" {
|
|
t.Errorf("Next above the state's own = %s, want 0.1.21", got)
|
|
}
|
|
})
|
|
t.Run("decide", func(t *testing.T) {
|
|
pub := Entry{Name: "n", Kind: KindPackage, Series: "0.1", Version: "0.1.5", Revision: "r1", Fingerprint: "fA", State: Published}
|
|
ref := pub
|
|
ref.State, ref.Version, ref.Revision = Refused, "0.1.6", "r2"
|
|
img := Entry{Name: "i", Kind: KindBundle, Series: "0.1", Version: "0.1.2", Fingerprint: "fB", State: Published}
|
|
silent := Floor{Silent: []string{SourceSeat}}
|
|
for _, c := range []struct {
|
|
name string
|
|
entries []Entry
|
|
kind, fp, rev string
|
|
floor Floor
|
|
version string
|
|
keep, publish, unnum bool
|
|
wait bool
|
|
}{
|
|
{"a package's revision held published keeps its version and publishes nothing", []Entry{pub}, KindPackage, "fX", "r1", silent, "0.1.5", true, false, false, false},
|
|
{"a revision numbered but refused is published by its second build under the same version", []Entry{pub, ref}, KindPackage, "fY", "r2", known(nil, nil, nil), "0.1.6", true, true, false, false},
|
|
{"a new revision takes the next", []Entry{pub, ref}, KindPackage, "fZ", "r3", known(nil, nil, nil), "0.1.7", false, true, false, false},
|
|
{"a bundle's fingerprint keeps its number", []Entry{img}, KindBundle, "fB", "", silent, "0.1.2", true, false, false, false},
|
|
{"an unknown floor builds a bundle unnumbered", nil, KindBundle, "fC", "", silent, "", false, true, true, false},
|
|
{"an unknown floor builds an image unnumbered", nil, KindImage, "fC", "", silent, "", false, true, true, false},
|
|
{"an unknown floor makes a package wait", nil, KindPackage, "fC", "r9", silent, "", false, false, false, true},
|
|
} {
|
|
d := Decide(c.entries, c.kind, "0.1", c.fp, c.rev, c.floor)
|
|
if d.Version != c.version || d.Keep != c.keep || d.Publish != c.publish || d.Unnumbered != c.unnum || d.Wait != c.wait || d.Why == "" {
|
|
t.Errorf("%s: Decide = %+v", c.name, d)
|
|
}
|
|
}
|
|
})
|
|
t.Run("two claims never one number", func(t *testing.T) {
|
|
st := newMem()
|
|
v := Versions{Store: st, Now: func() time.Time { return time.Unix(0, 0) }}
|
|
ctx := context.Background()
|
|
st.before = func() { // a second claim takes 0.1.0 between the first's read and its create
|
|
if _, _, err := (Versions{Store: st}).Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f2"}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
d, e, err := v.Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f1"})
|
|
if err != nil || d.Version != "0.1.1" || e.State != Numbered || e.Series != DefaultSeries {
|
|
t.Fatalf("Number = %+v, %+v, %v, want 0.1.1 after the other claim's 0.1.0", d, e, err)
|
|
}
|
|
if d, _, _ := v.Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f1"}); !d.Keep || d.Version != "0.1.1" {
|
|
t.Errorf("a numbered fingerprint claimed again = %+v, want it kept", d)
|
|
}
|
|
got, err := v.Mark(ctx, "n", "0.1.1", func(e *Entry) { e.State = Published })
|
|
if err != nil || got.State != Published {
|
|
t.Errorf("Mark = %+v, %v", got, err)
|
|
}
|
|
if _, err := v.Mark(ctx, "n", "0.9.9", func(*Entry) {}); err == nil {
|
|
t.Error("marking a version never numbered is no error")
|
|
}
|
|
})
|
|
t.Run("recorded", func(t *testing.T) {
|
|
st := newMem()
|
|
v := Versions{Store: st}
|
|
ctx := context.Background()
|
|
if _, _, err := v.Number(ctx, Claim{Name: "n", Kind: KindImage, Fingerprint: "f", Digest: "sha256:aa", Floor: known(nil, nil, nil)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range []struct {
|
|
version, digest string
|
|
ok bool
|
|
}{{"0.1.0", "sha256:aa", true}, {"0.1.0", "sha256:bb", false}, {"0.1.7", "sha256:aa", false}} {
|
|
if _, ok, why, err := v.Recorded(ctx, "n", c.version, c.digest); err != nil || ok != c.ok || (!ok && why == "") {
|
|
t.Errorf("Recorded(%s, %s) = %v, %q, %v; want %v", c.version, c.digest, ok, why, err, c.ok)
|
|
}
|
|
}
|
|
// builder_versions answers the same through the tools bundle's read-only view.
|
|
r := Reader{Keys: func() ([]string, error) {
|
|
var all []string
|
|
for k := range st.vals {
|
|
all = append(all, k)
|
|
}
|
|
return all, nil
|
|
}, Get: func(k string) (json.RawMessage, bool, error) {
|
|
v, _, ok, err := st.Get(ctx, k)
|
|
return v, ok, err
|
|
}}
|
|
a, err := AnswerVersions(ctx, r, "n", "0.1.0", "sha256:bb")
|
|
if err != nil || len(a.Versions) != 1 || a.Recorded == nil || *a.Recorded || a.Why == "" {
|
|
t.Errorf("builder_versions = %+v, %v; want one version and not recorded for another digest", a, err)
|
|
}
|
|
if _, err := AnswerVersions(ctx, r, " ", "", ""); err == nil {
|
|
t.Error("builder_versions with no name answered")
|
|
}
|
|
})
|
|
t.Run("the floor wakes on its silent source, never a timer", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
src := FloorSources{
|
|
Seat: func(context.Context, string, string) ([]string, error) { return []string{"0.1.3"}, nil },
|
|
Tags: func(context.Context, string, string) ([]string, error) { return nil, errors.New("no answer") },
|
|
Requirements: func(context.Context, string) ([]string, error) { return nil, nil },
|
|
}
|
|
f, why := src.Ask(ctx, FloorOf{Seat: "oci-artifact-registry", Name: "m/program"})
|
|
if f.Known() || !reflect.DeepEqual(f.Silent, []string{SourceTags}) || why[SourceTags] != "no answer" {
|
|
t.Fatalf("Ask = %+v, %v; want bridge_tags silent", f, why)
|
|
}
|
|
holders := map[string]string{SourceSeats[SourceTags]: "mesh-delivery-forge-bridge", SourceSeats[SourceRequirements]: "mesh-delivery-indexer"}
|
|
fw := FloorWaits{Holder: func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok }}
|
|
w := fw.Wait("m/program", "oci-artifact-registry", "distribution", f, why, time.Unix(0, 0))
|
|
if !strings.Contains(w.Finding(), "bridge_tags (no answer)") {
|
|
t.Errorf("finding = %q", w.Finding())
|
|
}
|
|
for _, c := range []struct {
|
|
kind, seat, module, name string
|
|
want []string
|
|
}{
|
|
{WakePublished, "oci-artifact-registry", "", "", nil}, // the seat answered already: a publish wakes nothing
|
|
{WakeAccepted, "", "", "", nil},
|
|
{WakeForge, "", "", "", []string{"m/program"}},
|
|
{WakeCleared, "", "mesh-delivery-forge-bridge", "", []string{"m/program"}}, // recovered with nothing to say
|
|
{WakeCleared, "", "mesh-delivery-indexer", "", nil},
|
|
{WakeByHand, "", "", "m/program", []string{"m/program"}},
|
|
{WakeByHand, "", "", "other", nil},
|
|
} {
|
|
if got := fw.Wake(c.kind, c.seat, c.module, c.name); !reflect.DeepEqual(got, c.want) {
|
|
t.Errorf("Wake(%s %s %s %s) = %v, want %v", c.kind, c.seat, c.module, c.name, got, c.want)
|
|
}
|
|
}
|
|
seatDown := Floor{Silent: []string{SourceSeat}}
|
|
fw.Wait("distribution/program", "container-image-registry", "distribution", seatDown, nil, time.Unix(0, 0))
|
|
if got := fw.Wake(WakePublished, "container-image-registry", "", ""); !reflect.DeepEqual(got, []string{"distribution/program"}) {
|
|
t.Errorf("the holder's published woke %v, want its own image's name", got)
|
|
}
|
|
if got := fw.Wake(WakeCleared, "", "distribution", ""); !reflect.DeepEqual(got, []string{"distribution/program"}) {
|
|
t.Errorf("the holder's clear woke %v", got)
|
|
}
|
|
if !fw.Answered("m/program") || fw.Answered("m/program") || len(fw.Waiting()) != 1 {
|
|
t.Error("Answered drops the wait once")
|
|
}
|
|
// DLV-B4: the waits are kept in floor-waits; a restarted builder takes them in, and its sources' events still wake them.
|
|
kept := keeperMem{}
|
|
restarted := &FloorWaits{}
|
|
restarted.Keep(kept, []FloorWait{{Name: "m/held", Seat: SeatImages, Silent: []string{SourceTags}, Since: time.Unix(5, 0)}})
|
|
if got := restarted.Wake(WakeForge, "", "", ""); !reflect.DeepEqual(got, []string{"m/held"}) {
|
|
t.Errorf("a wait held in floor-waits woke %v after a restart, want m/held", got)
|
|
}
|
|
restarted.Wait("m/new", SeatNPM, "", Floor{Silent: []string{SourceSeat}}, nil, time.Unix(9, 0))
|
|
ans, err := AnswerFloorWaits(Reader{Keys: kept.keys, Get: kept.get})
|
|
if err != nil || len(ans.Waits) != 2 || ans.Waits[0].Name != "m/held" || ans.Waits[1].Name != "m/new" || !strings.Contains(ans.Waits[1].Finding, "seat did not answer") {
|
|
t.Fatalf("builder_floor_waits = %+v, %v", ans, err)
|
|
}
|
|
restarted.Answered("m/held")
|
|
if _, ok := kept[FloorWaitKey("m/held")]; ok {
|
|
t.Error("an answered floor stays in floor-waits")
|
|
}
|
|
// The lead's ruling (review round 1): builder_bounds lists each key of the state bounds with what it bounds and
|
|
// when it ends.
|
|
at := time.Date(2026, 11, 4, 17, 0, 0, 0, time.UTC)
|
|
bounds := keeperMem{PublishBoundKey("m/program", "0.1.4"): EncodeBound("the publish of m/program@0.1.4 to oci-artifact-registry", at.Add(PublishBound)),
|
|
OldFormBoundKey("g14"): EncodeBound("the seven days g14 holds the old form's window", at.Add(FindingAfter))}
|
|
rows, err := AnswerBounds(Reader{Keys: bounds.keys, Get: bounds.get}, at)
|
|
if err != nil || len(rows) != 2 || rows[0].Key != OldFormBoundKey("g14") || rows[1].Left != "10m0s" ||
|
|
!strings.Contains(rows[1].Bounds, "m/program@0.1.4") || !rows[0].Ends.Equal(at.Add(FindingAfter)) {
|
|
t.Fatalf("builder_bounds = %+v, %v", rows, err)
|
|
}
|
|
})
|
|
t.Run("old form", func(t *testing.T) {
|
|
// One kind, a first form and a newer one.
|
|
fs := forms.ArtifactForms{{Name: "registry", First: true, Kinds: []string{"image"}}, {Name: "oci", Kinds: []string{"image"}}}
|
|
both := []string{forms.FormNeed("oci")}
|
|
for _, c := range []struct {
|
|
name string
|
|
counted map[string][]string
|
|
dropped map[string]bool
|
|
want []string
|
|
}{
|
|
{"one node lacking the new form holds the old", map[string][]string{"a": both, "b": nil}, nil, []string{"b"}},
|
|
{"an away node counts, stating its last", map[string][]string{"a": both, "c": {"bus-state"}}, nil, []string{"c"}},
|
|
{"every counted node reads the newest: closed", map[string][]string{"a": both, "c": both}, nil, nil},
|
|
{"a retired node dropped by hand counts no more", map[string][]string{"a": both, "c": nil}, map[string]bool{"c": true}, nil},
|
|
{"the builder's own forms: no window", nil, nil, nil},
|
|
} {
|
|
if got := Holding(fs, c.counted, c.dropped); !reflect.DeepEqual(got, c.want) {
|
|
t.Errorf("%s: Holding = %v, want %v", c.name, got, c.want)
|
|
}
|
|
}
|
|
// The builder's own forms: the registry seat's form of a bundle is not a first form, so a node stating nothing
|
|
// holds the window and one stating form:seat-reference does not.
|
|
if got := Holding(forms.Current, map[string][]string{"a": nil, "b": {forms.FormNeed(forms.FormSeatReference)}}, nil); !reflect.DeepEqual(got, []string{"a"}) {
|
|
t.Errorf("forms.Current, a node stating nothing and one stating form:seat-reference: Holding = %v, want a", got)
|
|
}
|
|
t0 := time.Unix(0, 0)
|
|
named := []string{"c", "d"}
|
|
w, closed, overdue := OldFormWindow{}.Step(named, []string{"c"}, t0)
|
|
if closed || len(overdue) != 0 || !w.Open {
|
|
t.Fatalf("Step = %+v %v %v", w, closed, overdue)
|
|
}
|
|
w, _, overdue = w.Step(named, []string{"c"}, t0.Add(FindingAfter))
|
|
if !reflect.DeepEqual(overdue, []string{"c"}) {
|
|
t.Errorf("seven days held: overdue %v, want c", overdue)
|
|
}
|
|
w, err := w.Drop("d", "op@shanks", "retired", t0)
|
|
if err != nil || !w.DroppedSet()["d"] {
|
|
t.Fatalf("Drop = %+v, %v", w, err)
|
|
}
|
|
if again, _ := w.Drop("d", "op@shanks", "again", t0); len(again.Dropped) != 1 {
|
|
t.Errorf("a node dropped twice is kept twice: %+v", again.Dropped)
|
|
}
|
|
if _, err := w.Drop("e", "op@shanks", "", t0); err == nil {
|
|
t.Error("a drop with no why is taken")
|
|
}
|
|
// Security-upgrade F3 (review round 1), by mesh-sdk's window.StepWindow: a read naming no node (an empty
|
|
// node_haves, the assignments not read yet) never closes the window, and its holder's seven days still count.
|
|
if held, shut, late := w.Step(nil, nil, t0.Add(FindingAfter+time.Minute)); shut || !held.Open || !reflect.DeepEqual(late, []string{"c"}) {
|
|
t.Fatalf("a read naming no node: %+v closed=%v overdue %v, want the window open and c overdue", held, shut, late)
|
|
}
|
|
w, closed, _ = w.Step(named, nil, t0.Add(FindingAfter+time.Hour))
|
|
if !closed || w.Open || !reflect.DeepEqual(w.LastHolding, []string{"c"}) || w.Closing().Dropped[0].Node != "d" {
|
|
t.Errorf("closing: %+v closed=%v", w, closed)
|
|
}
|
|
raw, _ := json.Marshal(w)
|
|
if back, err := DecodeOldFormWindow(raw); err != nil || !back.Closed.Equal(w.Closed) {
|
|
t.Errorf("the window read back = %+v, %v", back, err)
|
|
}
|
|
if _, again, _ := w.Step(named, nil, t0.Add(FindingAfter+2*time.Hour)); again {
|
|
t.Error("a closed window closes again: old-form-closed would be said twice")
|
|
}
|
|
})
|
|
t.Run("at the claim", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
tagsUp := false
|
|
var asked []string
|
|
src := FloorSources{
|
|
Seat: func(context.Context, string, string) ([]string, error) { return []string{"0.1.3"}, nil },
|
|
Tags: func(_ context.Context, repo, prefix string) ([]string, error) {
|
|
asked = append(asked, repo+" "+prefix)
|
|
if !tagsUp {
|
|
return nil, errors.New("no answer")
|
|
}
|
|
return []string{"go/v0.1.13"}, nil
|
|
},
|
|
Requirements: func(context.Context, string) ([]string, error) { return nil, nil },
|
|
}
|
|
fw := &FloorWaits{}
|
|
n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Waits: fw}
|
|
c := BuildClaim{Module: "m", Path: "go", Repository: "novox/m", Fingerprint: "fp1", Commit: "c1", Revision: "7", Merge: 7,
|
|
Artifacts: []ClaimArtifact{{Artifact: "program", Kind: KindBundle, Language: "go"}, {Artifact: "base", Kind: "upstream"}}}
|
|
got, err := n.Number(ctx, c)
|
|
if err != nil || len(got.Versions) != 0 || !reflect.DeepEqual(got.Unnumbered, []string{"program"}) || got.Wait != "" {
|
|
t.Fatalf("silent floor: %+v, %v; want program unnumbered, the upstream copy left out", got, err)
|
|
}
|
|
if !reflect.DeepEqual(asked, []string{"novox/m go/v"}) || len(fw.Waiting()) != 1 {
|
|
t.Fatalf("asked %v, waits %+v", asked, fw.Waiting())
|
|
}
|
|
pkg := c
|
|
pkg.Artifacts = []ClaimArtifact{{Artifact: "lib", Kind: KindPackage, Language: "go"}}
|
|
if got, _ := n.Number(ctx, pkg); got.Wait == "" || len(got.Versions) != 0 {
|
|
t.Fatalf("a package with no floor = %+v; want it to wait", got)
|
|
}
|
|
tagsUp = true
|
|
got, err = n.Number(ctx, c)
|
|
if err != nil || got.Versions["program"] != "0.1.14" || len(fw.Waiting()) != 1 {
|
|
t.Fatalf("floor answered: %+v, %v, waits %+v; want 0.1.14, lib still waiting", got, err, fw.Waiting())
|
|
}
|
|
got, _ = n.Number(ctx, c)
|
|
var publishing []string // review round 1: a version built and still numbered arms its publish bound
|
|
n.Publishing = func(_ context.Context, e Entry) { publishing = append(publishing, e.Name+"@"+e.Version) }
|
|
if err := n.Built(ctx, c, got, map[string]string{"program": "sha256:aa"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n.Publishing = nil; !reflect.DeepEqual(publishing, []string{"m/program@0.1.14"}) {
|
|
t.Fatalf("the publish bounds armed: %v, want m/program@0.1.14", publishing)
|
|
}
|
|
again, _ := n.Number(ctx, c)
|
|
if again.Versions["program"] != "0.1.14" || len(asked) != 3 {
|
|
t.Fatalf("again: %+v, asked %v; want the number kept, the floor not asked", again, asked)
|
|
}
|
|
if _, ok, why, _ := n.Versions.Recorded(ctx, "m/program", "0.1.14", "sha256:bb"); ok {
|
|
t.Fatalf("another digest recorded: %s", why)
|
|
}
|
|
// DLV-B4: the seat's events move the entry; a published wakes the names waiting on that seat.
|
|
if wake, err := n.Heard(ctx, SeatArtifacts, HeardPublished, "e1", Outcome{Name: "m/other", Digest: "sha256:cc"}); err != nil || len(wake) != 0 {
|
|
t.Fatalf("a by-digest publish of another seat's wait: %v, %v", wake, err)
|
|
}
|
|
n.Waits.Wait("m/lib", SeatGoPackages, "", Floor{Silent: []string{SourceSeat}}, nil, time.Unix(0, 0))
|
|
if wake, _ := n.Heard(ctx, SeatGoPackages, HeardPublished, "e2", Outcome{Name: "m/x", Digest: "sha256:dd"}); !reflect.DeepEqual(wake, []string{"m/lib"}) {
|
|
t.Fatalf("a published on the Go seat woke %v, want m/lib, whose seat was silent", wake)
|
|
}
|
|
for _, h := range []struct{ event, state, reason string }{{HeardRefused, Refused, "bytes differ"}, {HeardPublished, Published, ""}, {HeardRefused, Published, ""}, {HeardRemoved, Removed, "retention"}} {
|
|
if _, err := n.Heard(ctx, SeatArtifacts, h.event, "e3", Outcome{Name: "m/program", Version: "0.1.14", Digest: "sha256:aa", Reason: h.reason}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
all, _ := n.Versions.Of(ctx, "m/program")
|
|
if all[0].State != h.state {
|
|
t.Fatalf("after %s: %+v, want %s", h.event, all[0], h.state)
|
|
}
|
|
}
|
|
if _, err := n.Heard(ctx, SeatArtifacts, HeardPublished, "e4", Outcome{Name: "m/none", Version: "0.1.0"}); err == nil {
|
|
t.Fatal("a published of a version the builder never numbered is not said")
|
|
}
|
|
// D-B5 (ADR 0666 §15, design 51 *The versions* line 287): a package's build whose published versions cannot be read
|
|
// is refused, naming the state; a bundle's is built as before (ADR 0617 §1), the error said.
|
|
st := n.Versions.Store.(*mem)
|
|
st.down = errors.New("nats: no responders available for request")
|
|
if got, err := n.Number(ctx, pkg); err != nil || !strings.Contains(got.Refused, "state versions (bucket mesh-delivery-builder_versions) cannot be read") || len(got.Versions) != 0 {
|
|
t.Errorf("a package with its versions unread = %+v, %v; want refused naming the state", got, err)
|
|
}
|
|
if got, err := n.Number(ctx, c); err == nil || got.Refused != "" {
|
|
t.Errorf("a bundle with its versions unread = %+v, %v; want the error, no refusal", got, err)
|
|
}
|
|
st.down = nil
|
|
})
|
|
t.Run("a package is numbered under its name in its ecosystem", func(t *testing.T) {
|
|
// design 32: the indexer's packages state names, in each series key, the package's name and the <module>/<artifact>
|
|
// it is built from; the builder numbers, asks the floor of and records the package under that name.
|
|
ctx := context.Background()
|
|
state := packagesMem{
|
|
"git=2enovox=2ebe/novox/mesh-sdk/go.0.1": []byte(`{"v":1,"name":"git.novox.be/novox/mesh-sdk/go","package":"mesh-sdk/go","series":"0.1"}`),
|
|
"@novox/mesh-sdk.0.1": []byte(`{"v":1,"name":"@novox/mesh-sdk","package":"mesh-sdk/ts","series":"0.1"}`),
|
|
"git=2enovox=2ebe/novox/mesh-sdk/go": []byte(`{"v":1,"module":"git.novox.be/novox/mesh-sdk/go"}`),
|
|
"broken.0.1": []byte(`not json`),
|
|
}
|
|
var floorOf []string
|
|
src := FloorSources{
|
|
Seat: func(_ context.Context, _, name string) ([]string, error) {
|
|
floorOf = append(floorOf, name)
|
|
return []string{"0.1.3"}, nil
|
|
},
|
|
Tags: func(context.Context, string, string) ([]string, error) { return nil, nil },
|
|
Requirements: func(_ context.Context, name string) ([]string, error) {
|
|
floorOf = append(floorOf, name)
|
|
return nil, nil
|
|
},
|
|
}
|
|
n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Names: NamesIn(state)}
|
|
c := BuildClaim{Module: "mesh-sdk", Path: "go", Repository: "novox/mesh-sdk", Fingerprint: "fp1", Commit: "c1", Revision: "3", Merge: 3,
|
|
Artifacts: []ClaimArtifact{{Artifact: "go", Kind: KindPackage, Language: "go"}, {Artifact: "ts", Kind: KindPackage, Language: "typescript"},
|
|
{Artifact: "new", Kind: KindPackage, Language: "go"}, {Artifact: "program", Kind: KindBundle, Language: "go"}}}
|
|
got, err := n.Number(ctx, c)
|
|
if err != nil || got.Versions["go"] != "0.1.4" || got.Versions["ts"] != "0.1.4" || !reflect.DeepEqual(got.Unnumbered, []string{"new"}) {
|
|
t.Fatalf("Number = %+v, %v; want go and ts numbered, new unnumbered (no name yet)", got, err)
|
|
}
|
|
if got.Names["go"] != "git.novox.be/novox/mesh-sdk/go" || got.Names["ts"] != "@novox/mesh-sdk" || got.Names["program"] != "mesh-sdk/program" {
|
|
t.Errorf("names = %v", got.Names)
|
|
}
|
|
if !slices.Contains(floorOf, "git.novox.be/novox/mesh-sdk/go") || slices.Contains(floorOf, "mesh-sdk/go") {
|
|
t.Errorf("the floor was asked of %v, want the Go module path", floorOf)
|
|
}
|
|
if es, _ := n.Versions.Of(ctx, "git.novox.be/novox/mesh-sdk/go"); len(es) != 1 || es[0].Version != "0.1.4" {
|
|
t.Errorf("versions of the Go module path = %+v", es)
|
|
}
|
|
if es, _ := n.Versions.Of(ctx, "mesh-sdk/go"); len(es) != 0 {
|
|
t.Errorf("numbered under the builder's name too: %+v", es)
|
|
}
|
|
if err := n.Built(ctx, c, got, map[string]string{"go": "sha256:aa"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if es, _ := n.Versions.Of(ctx, "git.novox.be/novox/mesh-sdk/go"); len(es) != 1 || es[0].Digest != "sha256:aa" {
|
|
t.Errorf("the built digest is not recorded under the name: %+v", es)
|
|
}
|
|
// The indexer writes the name of `new` later: its series key names the module, and the look at the done build
|
|
// numbers it with the done key's digest; the rest are kept.
|
|
state["new.0.1"] = []byte(`{"v":1,"name":"new","package":"mesh-sdk/new","series":"0.1"}`)
|
|
for key, want := range map[string]string{"new.0.1": "mesh-sdk", "git=2enovox=2ebe/novox/mesh-sdk/go": "", "broken.0.1": ""} {
|
|
if module, name, ok := NamedModule(key, state[key]); module != want || ok != (want != "") || (ok && name != "new") {
|
|
t.Errorf("NamedModule(%s) = %q, %q, %v, want %q", key, module, name, ok, want)
|
|
}
|
|
}
|
|
done := c
|
|
done.Done = map[string]string{"go": "sha256:aa", "new": "sha256:bb"}
|
|
late, err := n.Number(ctx, done)
|
|
if err != nil || late.Versions["new"] != "0.1.4" || late.Versions["go"] != "0.1.4" || len(late.Unnumbered) != 0 {
|
|
t.Fatalf("a look at the done build = %+v, %v; want new numbered and go kept", late, err)
|
|
}
|
|
if es, _ := n.Versions.Of(ctx, "new"); len(es) != 1 || es[0].Digest != "sha256:bb" {
|
|
t.Errorf("versions of new = %+v, want its done digest recorded", es)
|
|
}
|
|
if _, _, err := NamesIn(packagesMem(nil).failing())(ctx, "m", "a"); err == nil {
|
|
t.Error("a packages state that cannot be read is no error")
|
|
}
|
|
// A packages state not on the bus yet leaves the package unnumbered, said, and numbers the rest of the claim.
|
|
n.Names = NamesIn(packagesMem(nil).failing())
|
|
bare, err := n.Number(ctx, c)
|
|
if err != nil || !reflect.DeepEqual(bare.Unnumbered, []string{"go", "new", "ts"}) || bare.Versions["program"] == "" ||
|
|
!strings.Contains(bare.Said[0].Line, "cannot be read") {
|
|
t.Errorf("an unreadable packages state: %+v, %v", bare, err)
|
|
}
|
|
})
|
|
t.Run("fill", func(t *testing.T) {
|
|
if _, full := Fill(Cap*79/100, Cap); full {
|
|
t.Error("79 % is full")
|
|
}
|
|
if _, full := Fill((Cap*80+99)/100, Cap); !full {
|
|
t.Error("80 % is not full")
|
|
}
|
|
})
|
|
t.Run("publish again", func(t *testing.T) {
|
|
num := Entry{Name: "m/p", Kind: KindBundle, Version: "0.1.3", Fingerprint: "f1", Digest: "sha256:aa", State: Numbered}
|
|
ref := num
|
|
ref.State, ref.RefusedDigest = Refused, "sha256:aa"
|
|
other := ref
|
|
other.RefusedDigest = "sha256:bb"
|
|
pub := num
|
|
pub.State = Published
|
|
gone := pub
|
|
gone.State = Removed
|
|
tagged := num
|
|
tagged.Tag, tagged.Version = "feat-x", "0.1.3-feat-x.1"
|
|
for _, c := range []struct {
|
|
name string
|
|
entries []Entry
|
|
fp string
|
|
built bool
|
|
do string
|
|
}{
|
|
{"a held build is said numbered again", []Entry{num}, "f1", true, AgainPublish},
|
|
{"refused for its own bytes is published again", []Entry{ref}, "f1", true, AgainPublish},
|
|
{"a gone file is built again under the same number", []Entry{ref}, "f1", false, AgainBuild},
|
|
{"after a published it takes a new number", []Entry{pub}, "f1", true, AgainRenumber},
|
|
{"after a removal it takes a new number", []Entry{gone}, "f1", false, AgainRenumber},
|
|
{"refused for other bytes is refused", []Entry{other}, "f1", true, ""},
|
|
{"a fingerprint not numbered is refused", []Entry{num}, "f2", true, ""},
|
|
{"a tagged version is not a trunk fingerprint's", []Entry{tagged}, "f1", true, ""},
|
|
} {
|
|
a, err := PublishAgain(c.entries, c.fp, c.built)
|
|
if (err != nil) != (c.do == "") || a.Do != c.do {
|
|
t.Errorf("%s: PublishAgain = %+v, %v; want %q", c.name, a, err, c.do)
|
|
}
|
|
}
|
|
ctx := context.Background()
|
|
// A fresh claim of a published fingerprint takes the next number; the old one stays.
|
|
st := newMem()
|
|
v := Versions{Store: st}
|
|
raw, _ := Encode(Entry{Name: "m/p", Kind: KindBundle, Series: "0.1", Version: "0.1.3", Fingerprint: "f1", State: Published, At: time.Unix(1, 0)})
|
|
_, _ = st.Create(ctx, Key("m/p", "0.1.3"), raw)
|
|
if d, _, err := v.Number(ctx, Claim{Name: "m/p", Kind: KindBundle, Fingerprint: "f1"}); err != nil || d.Version != "0.1.3" || !d.Keep {
|
|
t.Fatalf("a published fingerprint keeps its number: %+v %v", d, err)
|
|
}
|
|
if d, e, err := v.Number(ctx, Claim{Name: "m/p", Kind: KindBundle, Fingerprint: "f1", Fresh: true}); err != nil || d.Version != "0.1.4" || d.Keep || e.Fingerprint != "f1" {
|
|
t.Fatalf("a fresh claim takes the next number: %+v %+v %v", d, e, err)
|
|
}
|
|
// A refusal records the digest refused; Built replaces a digest no published was said for, never a published one.
|
|
n := &Numberer{Versions: v}
|
|
_, _ = n.Heard(ctx, SeatArtifacts, HeardRefused, "r1", Outcome{Name: "m/p", Version: "0.1.4", Digest: "sha256:old", Reason: "x"})
|
|
if es, _ := v.Of(ctx, "m/p"); es[1].RefusedDigest != "sha256:old" {
|
|
t.Errorf("refused records no digest: %+v", es[1])
|
|
}
|
|
_, _ = v.Mark(ctx, "m/p", "0.1.4", func(e *Entry) { e.Digest = "sha256:old" })
|
|
bc := BuildClaim{Module: "m"}
|
|
_ = n.Built(ctx, bc, Numbering{Versions: map[string]string{"p": "0.1.4"}}, map[string]string{"p": "sha256:new"})
|
|
_, _ = v.Mark(ctx, "m/p", "0.1.3", func(e *Entry) { e.Digest = "sha256:held" })
|
|
_ = n.Built(ctx, bc, Numbering{Versions: map[string]string{"p": "0.1.3"}}, map[string]string{"p": "sha256:other"})
|
|
es, _ := v.Of(ctx, "m/p")
|
|
if es[0].Digest != "sha256:held" || es[1].Digest != "sha256:new" {
|
|
t.Errorf("Built: published %s (want sha256:held), refused %s (want sha256:new)", es[0].Digest, es[1].Digest)
|
|
}
|
|
if all, err := v.All(ctx); err != nil || len(all) != 2 {
|
|
t.Errorf("All = %d entries, %v", len(all), err)
|
|
}
|
|
// The hand acts: a held build is said numbered once more under a fresh dedup id; a gone one is built again; a
|
|
// published one takes a new number; a name with versions asks no floor.
|
|
if _, _, err := (Acts{Numberer: &Numberer{Versions: v}, Done: func(context.Context, string, string) (bool, error) { return true, nil }}).PublishAgain(ctx, "m/p", "f1", "w", "", time.Unix(4, 0)); err == nil {
|
|
t.Error("a version refused for other bytes is published again")
|
|
}
|
|
_, _ = v.Mark(ctx, "m/p", "0.1.4", func(e *Entry) { e.State = Published })
|
|
var rebuilt []string
|
|
done := true
|
|
acts := Acts{Numberer: &Numberer{Versions: v, Waits: &FloorWaits{}},
|
|
Done: func(context.Context, string, string) (bool, error) { return done, nil },
|
|
BuildAgain: func(_ context.Context, m, fp, _, _ string) (string, bool) {
|
|
rebuilt = append(rebuilt, m+" "+fp)
|
|
return "built again", false
|
|
},
|
|
Reconsider: func(context.Context) {}}
|
|
// Nothing says a version numbered (design 47 G §349): a held build is refused before any number is taken.
|
|
if _, _, err := acts.PublishAgain(ctx, "m/p", "f1", "w", "me", time.Unix(5, 0)); err == nil || len(rebuilt) != 0 {
|
|
t.Errorf("publish again of a published, held build answered %v, rebuilt %v; want a refusal", err, rebuilt)
|
|
}
|
|
if es, _ := v.Of(ctx, "m/p"); len(es) != 2 {
|
|
t.Errorf("the refused publish again took a number: %+v", es)
|
|
}
|
|
done = false
|
|
if ag, _, err := acts.PublishAgain(ctx, "m/p", "f1", "w", "me", time.Unix(6, 0)); err != nil || ag.Do != AgainRenumber || ag.Entry.Version != "0.1.5" || len(rebuilt) != 1 {
|
|
t.Errorf("publish again of a gone build: %+v %v rebuilt %v", ag, err, rebuilt)
|
|
}
|
|
if _, err := acts.FloorAgain(ctx, "m/p"); err == nil {
|
|
t.Error("a name with versions asks its floor again")
|
|
}
|
|
if ans, err := acts.FloorAgain(ctx, "m/new"); err != nil || ans == "" {
|
|
t.Errorf("floor again of a new name: %q %v", ans, err)
|
|
}
|
|
})
|
|
}
|