The window opens while a counted node-engine reads, of some kind, none of the newest form sdk/forms knows, and closes when none does: said once as version.old-form-closed, and a node holding it seven days is old-form-held about it. It steps on a statement's change, the placement, a hand drop and each build key written, never on a timer. A retired node dropped by hand leaves the count of the forms a build key keeps (design 51). internal/versions' own Forms goes: which forms a key keeps is sdk/forms'. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
615 lines
29 KiB
Go
615 lines
29 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())
|
|
}
|
|
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"}}
|
|
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)
|
|
}
|
|
})
|
|
}
|