Files
mesh-delivery/modules/mesh-delivery-builder/internal/versions/versions_test.go
T
jschoubben 5fbd84091e DLV-B6: a check's build of a head a kept tag ask names numbers its packages tagged at its claim
The builder reads tag-asks for the candidate's repository and head (versions.TagFor), numbers only its packages under
the tag below the next version, keeps each with its repository for the branch's end, and says numbered with the head;
the same fingerprint claimed again keeps its number (design 51 *Tagged versions*).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:00:17 +01:00

647 lines
31 KiB
Go

package versions
import (
"context"
"encoding/json"
"errors"
"reflect"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/sdk/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()
}
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 }
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("merge order", func(t *testing.T) {
es := []Entry{{Fingerprint: "f1", Version: "0.1.0", Series: "0.1"}}
for _, c := range []struct {
name string
merge uint64
held map[string]uint64
want uint64
}{
{"an earlier merge numbered waits on nothing", 5, map[string]uint64{"f1": 3}, 0},
{"an earlier merge not numbered is waited on", 5, map[string]uint64{"f1": 3, "f2": 4}, 4},
{"a replaced fingerprint the indexer no longer holds is skipped", 5, map[string]uint64{}, 0},
{"a later merge is not waited on", 5, map[string]uint64{"f3": 7}, 0},
} {
if got := WaitsOn(es, c.merge, c.held); got != c.want {
t.Errorf("%s: WaitsOn = %d, want %d", c.name, got, c.want)
}
}
})
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")
}
})
t.Run("old form", func(t *testing.T) {
// One kind, a first form and a newer one (the builder's own forms are all first today, so no window opens).
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)
}
}
if got := Holding(forms.Current, map[string][]string{"a": nil}, nil); got != nil {
t.Errorf("forms.Current, a node stating nothing: Holding = %v, want none", got)
}
t0 := time.Unix(0, 0)
w, closed, overdue := OldFormWindow{}.Step([]string{"c"}, t0)
if closed || len(overdue) != 0 || !w.Open {
t.Fatalf("Step = %+v %v %v", w, closed, overdue)
}
w, _, overdue = w.Step([]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")
}
w, closed, _ = w.Step(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(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 []NumberedBody
fw := &FloorWaits{}
n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Waits: fw, Said: func(b NumberedBody) { 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)
if err := n.Built(ctx, c, got, map[string]string{"program": "sha256:aa"}); err != nil {
t.Fatal(err)
}
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("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 []NumberedBody
var rebuilt []string
done := true
acts := Acts{Numberer: &Numberer{Versions: v, Said: func(b NumberedBody) { 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)
}
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)
}
})
}