Files
mesh-delivery/modules/mesh-delivery-builder/internal/versions/versions_test.go
T
jschoubben 4dbb6b2aec F-delivery: builder_bounds reads the builder's state bounds
Every state a module keeps has a read tool (the lead's ruling). Each key of bounds now holds what it bounds
and when it ends, and builder_bounds lists them with the time left.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:46:32 +01:00

787 lines
39 KiB
Go

package versions
import (
"context"
"encoding/json"
"errors"
"maps"
"reflect"
"slices"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/sdk/forms"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// 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()
}
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()
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("tagged", func(t *testing.T) {
es := []Entry{{Version: "0.1.14", Series: "0.1"}, {Version: "0.1.15-feat-x.1", Series: "0.1", Tag: "feat-x"}}
if got := Tagged(es, "0.1", "feat/X", known(nil, nil, nil)); got != "0.1.15-feat-x.2" {
t.Errorf("Tagged = %s, want 0.1.15-feat-x.2", got)
}
if got := Tagged(nil, "0.1", "feat-x", known(nil, nil, nil)); got != "0.1.0-feat-x.1" {
t.Errorf("a package new in a group = %s, want 0.1.0-feat-x.1", got)
}
if got := CleanTag("--Fix/ä_1965--"); got != "fix-1965" {
t.Errorf("CleanTag = %s", got)
}
})
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)
}
var fw FloorWaits
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 delivery.StepWindow: a read naming no node (an empty
// node_haves, the placement 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 },
}
var said []delivery.VersionNumbered
fw := &FloorWaits{}
n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Waits: fw, Said: func(b delivery.VersionNumbered) { said = append(said, b) }}
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 || len(said) != 0 {
t.Fatalf("asked %v, waits %+v, said %v", asked, fw.Waiting(), said)
}
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(said) != 1 || said[0].Version != "0.1.14" || len(fw.Waiting()) != 1 {
t.Fatalf("floor answered: %+v, %v, said %v, waits %+v; want 0.1.14 said once, lib still waiting", got, err, said, fw.Waiting())
}
// DLV-B6: a check's build of a head a tag ask names numbers only its packages, tagged below the next version, kept
// with its repository for the branch's end, said with the head; claimed again it keeps its number.
tagged := c
tagged.Tag, tagged.Head, tagged.Revision, tagged.Merge = "feat-x", "h1", "", 0
tagged.Artifacts = append(tagged.Artifacts, ClaimArtifact{Artifact: "tlib", Kind: KindPackage, Language: "go"})
saidBefore, askedBefore := said, asked
said = nil
got, err = n.Number(ctx, tagged)
if err != nil || len(got.Versions) != 1 || !strings.Contains(got.Versions["tlib"], "-feat-x.1") || len(said) != 1 || said[0].Head != "h1" || said[0].Tag != "feat-x" {
t.Fatalf("tagged claim: %+v, %v, said %+v; want tlib alone tagged, said with its head", got, err, said)
}
if es, _ := n.Versions.Of(ctx, "m/tlib"); len(es) != 1 || es[0].Repository != "novox/m" {
t.Errorf("the tagged version kept = %+v, want its repository", es)
}
if again, _ := n.Number(ctx, tagged); again.Versions["tlib"] != got.Versions["tlib"] || len(said) != 1 {
t.Errorf("claimed again: %+v, said %d; want the same number, said once", again, len(said))
}
said, asked = saidBefore, askedBefore
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(said) != 1 || len(asked) != 3 {
t.Fatalf("again: %+v, said %d, asked %v; want the number kept, nothing said, the floor not asked", again, len(said), 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")
}
})
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
},
}
var said []delivery.VersionNumbered
n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Names: NamesIn(state), Said: func(b delivery.VersionNumbered) { said = append(said, b) }}
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)
}
names := map[string]bool{}
for _, b := range said {
names[b.Name] = true
}
if !names["git.novox.be/novox/mesh-sdk/go"] || !names["@novox/mesh-sdk"] {
t.Errorf("version.numbered said %v, want the ecosystem names", names)
}
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)
}
tagged, _ := TagAsk{Head: "h", State: TagAsked}.WithNumbered("mesh-sdk", "h", map[string]string{"go": "0.1.4-x.1"}, got.Names, time.Unix(1, 0))
if tagged.Versions["git.novox.be/novox/mesh-sdk/go"] != "0.1.4-x.1" {
t.Errorf("a tag ask names %v, want the Go module path", tagged.Versions)
}
// 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, said in version.numbered for the late publish; 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)
}
}
said = nil
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 len(said) != 1 || said[0].Name != "new" || said[0].Digest != "sha256:bb" {
t.Errorf("version.numbered said %+v, want new once with the done build's digest", said)
}
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 said []delivery.VersionNumbered
var rebuilt []string
done := true
acts := Acts{Numberer: &Numberer{Versions: v, Said: func(b delivery.VersionNumbered) { said = append(said, b) }, 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) {}}
if ag, _, err := acts.PublishAgain(ctx, "m/p", "f1", "w", "me", time.Unix(5, 0)); err != nil || ag.Do != AgainRenumber || ag.Entry.Version != "0.1.5" ||
len(said) != 1 || said[0].Version != "0.1.5" || said[0].Again == "" || len(rebuilt) != 0 {
t.Errorf("publish again of a published, held build: %+v %v said %+v rebuilt %v", ag, err, said, rebuilt)
}
done = false
if ag, _, err := acts.PublishAgain(ctx, "m/p", "f1", "w", "me", time.Unix(6, 0)); err != nil || ag.Do != AgainBuild || ag.Entry.Version != "0.1.5" || len(rebuilt) != 1 || len(said) != 1 {
t.Errorf("publish again of a gone build: %+v %v said %d rebuilt %v", ag, err, len(said), 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)
}
})
t.Run("tag asks", func(t *testing.T) {
at := time.Unix(100, 0)
k, raw, err := NewTagAsk("novox/mesh-sdk", "feat/X y", "abc", "a check", "me@shanks", at)
if err != nil || k != TagAskKey("novox/mesh-sdk", "feat/X y") || strings.Count(k, ".") != 1 {
t.Fatalf("NewTagAsk = %q, %v", k, err)
}
a, err := DecodeTagAsk(raw)
if err != nil || a.Tag != "feat-x-y" || a.State != TagAsked || a.Untag {
t.Fatalf("DecodeTagAsk = %+v, %v", a, err)
}
for _, bad := range [][4]string{{"mesh-sdk", "b", "h", "w"}, {"o/r", "", "h", "w"}, {"o/r", "b", "", "w"}, {"o/r", "b", "h", ""}} {
if _, _, err := NewTagAsk(bad[0], bad[1], bad[2], bad[3], "", at); err == nil {
t.Errorf("NewTagAsk%v is not refused", bad)
}
}
_, uraw, err := NewUntag("o/r", "feat/x", "branch gone by hand", "", at.Add(time.Second))
if u, _ := DecodeTagAsk(uraw); err != nil || !u.Untag {
t.Fatalf("NewUntag = %+v, %v", u, err)
}
vals := map[string][]byte{"a": uraw, "b": raw}
r := Reader{Keys: func() ([]string, error) { return []string{"a", "b"}, nil },
Get: func(k string) (json.RawMessage, bool, error) { return vals[k], true, nil }}
if got, err := AnswerTagAsks(r, ""); err != nil || len(got.Asks) != 2 || got.Asks[0].Repository != "novox/mesh-sdk" {
t.Errorf("AnswerTagAsks = %+v, %v", got, err)
}
if got, _ := AnswerTagAsks(r, "o/r"); len(got.Asks) != 1 || !got.Asks[0].Untag {
t.Errorf("AnswerTagAsks(o/r) = %+v", got)
}
es := []Entry{{Name: "n", Version: "0.1.2-feat-x.1", Tag: "feat-x", Series: "0.1", Repository: "o/r"}, {Name: "n", Version: "0.1.2-feat-x.2", Tag: "feat-x", Series: "0.1", State: Removed, Repository: "o/r"},
{Name: "n2", Version: "0.1.2-feat-x.1", Tag: "feat-x", Series: "0.1", Repository: "o/other"},
{Name: "n", Version: "0.1.1", Series: "0.1"}, {Name: "n", Version: "0.1.2-feat-y.1", Tag: "feat-y", Series: "0.1"}}
for _, c := range []struct {
asks []TagAsk
repo, head string
want string
}{
{[]TagAsk{a}, "novox/mesh-sdk", "abc", "feat-x-y"},
{[]TagAsk{a}, "novox/mesh-sdk", "other", ""},
{[]TagAsk{{Repository: "novox/mesh-sdk", Head: "abc", Tag: "t", State: TagRemoved}}, "novox/mesh-sdk", "abc", ""},
} {
if got, _ := TagFor(c.asks, c.repo, c.head); got != c.want {
t.Errorf("TagFor(%s, %s) = %q, want %q", c.repo, c.head, got, c.want)
}
}
if got := Untagged(es, "o/r", "feat/x"); len(got) != 1 || got[0].Version != "0.1.2-feat-x.1" {
t.Errorf("Untagged = %+v", got)
}
st := newMem()
for _, e := range append(es, Entry{Name: "n3", Version: "0.1.0-feat-x.1", Tag: "feat-x", Series: "0.1", Repository: "o/r", Seat: SeatNPM}) {
e.Kind, e.Module, e.Fingerprint, e.At = KindPackage, "m", "f", at
if e.State == "" {
e.State = Numbered
}
raw, err := Encode(e)
if err != nil {
t.Fatal(err)
}
_, _ = st.Create(context.Background(), Key(e.Name, e.Version), raw)
}
var removed []string
acts := Acts{Numberer: &Numberer{Versions: Versions{Store: st}}, Remove: func(_ context.Context, seat string, e Entry) error {
removed = append(removed, e.Name+"@"+e.Version)
if seat == SeatNPM {
return errors.New("down")
}
return nil
}}
u, _ := DecodeTagAsk(uraw)
got := acts.TagAsked(context.Background(), u, at)
if got.State != TagRemoving || len(removed) != 2 || len(got.Left) != 2 || !strings.Contains(got.Left["n3@0.1.0-feat-x.1"], "down") || got.Answered.IsZero() {
t.Errorf("untag: %+v removed %v", got, removed)
}
if k := acts.TagAsked(context.Background(), a, at); k.State != TagAsked || k.Answer == "" {
t.Errorf("a tag ask is not kept: %+v", k)
}
// DLV-B6: a branch's end asks remove of every tagged version not removed, and is done once each seat said removed.
removed = nil
end, err := acts.BranchEnded(context.Background(), "o/r", "feat/x")
if err != nil || end.Done || len(removed) != 2 || !reflect.DeepEqual(end.Seats(), []string{"", SeatNPM}) || !strings.Contains(end.Finding("o/r", "feat/x", at), "n3@0.1.0-feat-x.1 on npm-package-registry: down") {
t.Fatalf("BranchEnded = %+v %v, removed %v", end, err, removed)
}
for _, v := range [][2]string{{"n", "0.1.2-feat-x.1"}, {"n3", "0.1.0-feat-x.1"}} {
if _, err := acts.Numberer.Heard(context.Background(), SeatNPM, HeardRemoved, "rm", Outcome{Name: v[0], Version: v[1], Reason: "tag-gone"}); err != nil {
t.Fatal(err)
}
}
if end, _ := acts.BranchEnded(context.Background(), "o/r", "feat/x"); !end.Done {
t.Errorf("every seat said removed, and the branch's end is not done: %+v", end)
}
// B6: a check's build of the ask's head marks it numbered with its versions; another head, or an untag, is left.
if !a.Unanswered() {
t.Errorf("a kept ask with no version is answered: %+v", a)
}
if _, changed := a.WithNumbered("mesh-sdk", "other", map[string]string{"go": "0.1.15-feat-x-y.1"}, nil, at); changed {
t.Error("an ask is marked numbered by a build of another head")
}
if _, changed := u.WithNumbered("o", "", map[string]string{"go": "1"}, nil, at); changed {
t.Error("an untag is marked numbered")
}
n, changed := a.WithNumbered("mesh-sdk", "abc", map[string]string{"go": "0.1.15-feat-x-y.1"}, nil, at.Add(time.Minute))
if !changed || n.State != TagNumbered || n.Versions["mesh-sdk/go"] != "0.1.15-feat-x-y.1" || n.Unanswered() || !strings.Contains(n.Answer, "mesh-sdk/go 0.1.15-feat-x-y.1") {
t.Errorf("WithNumbered = %+v, %v", n, changed)
}
if _, again := n.WithNumbered("mesh-sdk", "abc", map[string]string{"go": "0.1.15-feat-x-y.1"}, nil, at); again {
t.Error("the same versions mark the ask again")
}
said := NarrateUnanswered(a, []FloorWait{{Name: "mesh-sdk/go", Silent: []string{"bridge_tags"}}}, at.Add(6*time.Minute))
for _, w := range []string{"novox/mesh-sdk feat/X y", "after 6m0s", "not claimed yet", "mesh-sdk/go waits on its floor (bridge_tags silent)"} {
if !strings.Contains(said, w) {
t.Errorf("NarrateUnanswered = %q, want %q in it", said, w)
}
}
if k, v, err := TagWantedAsk(TagWanted{Repository: "o/r", Branch: "feat/x", Head: "abc"}, "mesh-delivery-checker", at); err != nil || k != TagAskKey("o/r", "feat/x") || !strings.Contains(string(v), `"by":"mesh-delivery-checker"`) {
t.Errorf("TagWantedAsk = %q %s %v", k, v, err)
}
})
}