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
647 lines
31 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|