A read naming no node (an empty node_haves, the placement not read yet) closed the window for good (security review F3, second of a kind). The window embeds delivery.WindowHeld and steps by the shared rule, named being the placement's nodes before any is dropped. The builder's expiring keys share one state, bounds (the publish bound of F-delivery 5 renamed), for the old-form bound next. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
777 lines
38 KiB
Go
777 lines
38 KiB
Go
package versions
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"maps"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-delivery/sdk/forms"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// mem is a Store in memory; before runs once before the next Create, to stage a race.
|
|
type mem struct {
|
|
mu sync.Mutex
|
|
vals map[string][]byte
|
|
revs map[string]uint64
|
|
rev uint64
|
|
before func()
|
|
}
|
|
|
|
func newMem() *mem { return &mem{vals: map[string][]byte{}, revs: map[string]uint64{}} }
|
|
|
|
func (m *mem) Get(_ context.Context, key string) ([]byte, uint64, bool, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
v, ok := m.vals[key]
|
|
return v, m.revs[key], ok, nil
|
|
}
|
|
|
|
func (m *mem) Create(ctx context.Context, key string, value []byte) (uint64, error) {
|
|
if f := m.before; f != nil {
|
|
m.before = nil
|
|
f()
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if _, ok := m.vals[key]; ok {
|
|
return 0, ErrConflict
|
|
}
|
|
m.rev++
|
|
m.vals[key], m.revs[key] = value, m.rev
|
|
return m.rev, nil
|
|
}
|
|
|
|
func (m *mem) Update(_ context.Context, key string, value []byte, revision uint64) (uint64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.revs[key] != revision {
|
|
return 0, ErrConflict
|
|
}
|
|
m.rev++
|
|
m.vals[key], m.revs[key] = value, m.rev
|
|
return m.rev, nil
|
|
}
|
|
|
|
func (m *mem) KeysOf(_ context.Context, filter string) ([]string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
prefix := strings.TrimSuffix(filter, "*")
|
|
var out []string
|
|
for k := range m.vals {
|
|
if filter == AllFilter {
|
|
out = append(out, k)
|
|
continue
|
|
}
|
|
if strings.HasPrefix(k, prefix) && !strings.Contains(k[len(prefix):], ".") {
|
|
out = append(out, k)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// keeperMem is the state floor-waits in memory.
|
|
type keeperMem map[string][]byte
|
|
|
|
func (k keeperMem) Put(key string, v []byte) error { k[key] = v; return nil }
|
|
func (k keeperMem) Delete(key string) error { delete(k, key); return nil }
|
|
func (k keeperMem) keys() ([]string, error) {
|
|
var out []string
|
|
for key := range k {
|
|
out = append(out, key)
|
|
}
|
|
return out, nil
|
|
}
|
|
func (k keeperMem) get(key string) (json.RawMessage, bool, error) { v, ok := k[key]; return v, ok, nil }
|
|
|
|
// packagesMem is the indexer's packages state as the builder reads it, in memory.
|
|
type packagesMem map[string][]byte
|
|
|
|
func (p packagesMem) Keys(context.Context) ([]string, error) {
|
|
if p == nil {
|
|
return nil, errors.New("not readable")
|
|
}
|
|
return slices.Collect(maps.Keys(p)), nil
|
|
}
|
|
|
|
func (p packagesMem) Get(_ context.Context, key string) ([]byte, bool, error) {
|
|
v, ok := p[key]
|
|
return v, ok, nil
|
|
}
|
|
|
|
func (p packagesMem) failing() packagesMem { return nil }
|
|
|
|
func known(seat, tags, reqs []string) Floor { return Floor{Seat: seat, Tags: tags, Requirements: reqs} }
|
|
|
|
// TestTheBuildersVersions holds design 51 *The versions*, *The floor* and *Tagged versions* (DLV-B1, DLV-B2, DLV-B3):
|
|
// one table per rule, the unit cases of the builder's numbering.
|
|
func TestTheBuildersVersions(t *testing.T) {
|
|
t.Run("keys", func(t *testing.T) {
|
|
names := []string{"@novox/mesh-sdk", "git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/mesh-sdk", "a=40b", "a@b"}
|
|
seen := map[string]string{}
|
|
for _, n := range names {
|
|
k := Key(n, "0.1.14")
|
|
if strings.Count(k, ".") != 1 {
|
|
t.Errorf("Key(%q) = %q, want two tokens", n, k)
|
|
}
|
|
if o, dup := seen[k]; dup {
|
|
t.Errorf("%q and %q escape alike: %q", n, o, k)
|
|
}
|
|
seen[k] = n
|
|
if !strings.HasPrefix(k, strings.TrimSuffix(NameFilter(n), "*")) {
|
|
t.Errorf("Key(%q) = %q is not under NameFilter %q", n, k, NameFilter(n))
|
|
}
|
|
}
|
|
})
|
|
t.Run("floor", func(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name string
|
|
floor Floor
|
|
want string
|
|
}{
|
|
{"none starts at 0", known(nil, nil, nil), "0.1.0"},
|
|
{"the Go tag of today gives v0.1.14", known(nil, []string{"go/v0.1.13"}, []string{"v0.1.14-0.20261103221822-85ba0a9a70d8"}), "0.1.14"},
|
|
{"npm's 0.1.17 on the seat gives 0.1.18", known([]string{"0.1.17", "0.1.3"}, nil, nil), "0.1.18"},
|
|
{"another series and a tagged version count nothing", known([]string{"0.2.40", "0.1.9-feat-x.1"}, nil, nil), "0.1.0"},
|
|
{"the highest of the three", known([]string{"0.1.4"}, []string{"v0.1.9"}, []string{"v0.1.7"}), "0.1.10"},
|
|
} {
|
|
if got := Next(nil, "0.1", c.floor); got != c.want {
|
|
t.Errorf("%s: Next = %s, want %s", c.name, got, c.want)
|
|
}
|
|
}
|
|
if got := Next([]Entry{{Version: "0.1.20", Series: "0.1"}, {Version: "0.1.21-x.1", Series: "0.1", Tag: "x"}}, "0.1", known([]string{"0.1.3"}, nil, nil)); got != "0.1.21" {
|
|
t.Errorf("Next above the state's own = %s, want 0.1.21", got)
|
|
}
|
|
})
|
|
t.Run("decide", func(t *testing.T) {
|
|
pub := Entry{Name: "n", Kind: KindPackage, Series: "0.1", Version: "0.1.5", Revision: "r1", Fingerprint: "fA", State: Published}
|
|
ref := pub
|
|
ref.State, ref.Version, ref.Revision = Refused, "0.1.6", "r2"
|
|
img := Entry{Name: "i", Kind: KindBundle, Series: "0.1", Version: "0.1.2", Fingerprint: "fB", State: Published}
|
|
silent := Floor{Silent: []string{SourceSeat}}
|
|
for _, c := range []struct {
|
|
name string
|
|
entries []Entry
|
|
kind, fp, rev string
|
|
floor Floor
|
|
version string
|
|
keep, publish, unnum bool
|
|
wait bool
|
|
}{
|
|
{"a package's revision held published keeps its version and publishes nothing", []Entry{pub}, KindPackage, "fX", "r1", silent, "0.1.5", true, false, false, false},
|
|
{"a revision numbered but refused is published by its second build under the same version", []Entry{pub, ref}, KindPackage, "fY", "r2", known(nil, nil, nil), "0.1.6", true, true, false, false},
|
|
{"a new revision takes the next", []Entry{pub, ref}, KindPackage, "fZ", "r3", known(nil, nil, nil), "0.1.7", false, true, false, false},
|
|
{"a bundle's fingerprint keeps its number", []Entry{img}, KindBundle, "fB", "", silent, "0.1.2", true, false, false, false},
|
|
{"an unknown floor builds a bundle unnumbered", nil, KindBundle, "fC", "", silent, "", false, true, true, false},
|
|
{"an unknown floor builds an image unnumbered", nil, KindImage, "fC", "", silent, "", false, true, true, false},
|
|
{"an unknown floor makes a package wait", nil, KindPackage, "fC", "r9", silent, "", false, false, false, true},
|
|
} {
|
|
d := Decide(c.entries, c.kind, "0.1", c.fp, c.rev, c.floor)
|
|
if d.Version != c.version || d.Keep != c.keep || d.Publish != c.publish || d.Unnumbered != c.unnum || d.Wait != c.wait || d.Why == "" {
|
|
t.Errorf("%s: Decide = %+v", c.name, d)
|
|
}
|
|
}
|
|
})
|
|
t.Run("two claims never one number", func(t *testing.T) {
|
|
st := newMem()
|
|
v := Versions{Store: st, Now: func() time.Time { return time.Unix(0, 0) }}
|
|
ctx := context.Background()
|
|
st.before = func() { // a second claim takes 0.1.0 between the first's read and its create
|
|
if _, _, err := (Versions{Store: st}).Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f2"}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
d, e, err := v.Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f1"})
|
|
if err != nil || d.Version != "0.1.1" || e.State != Numbered || e.Series != DefaultSeries {
|
|
t.Fatalf("Number = %+v, %+v, %v, want 0.1.1 after the other claim's 0.1.0", d, e, err)
|
|
}
|
|
if d, _, _ := v.Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f1"}); !d.Keep || d.Version != "0.1.1" {
|
|
t.Errorf("a numbered fingerprint claimed again = %+v, want it kept", d)
|
|
}
|
|
got, err := v.Mark(ctx, "n", "0.1.1", func(e *Entry) { e.State = Published })
|
|
if err != nil || got.State != Published {
|
|
t.Errorf("Mark = %+v, %v", got, err)
|
|
}
|
|
if _, err := v.Mark(ctx, "n", "0.9.9", func(*Entry) {}); err == nil {
|
|
t.Error("marking a version never numbered is no error")
|
|
}
|
|
})
|
|
t.Run("recorded", func(t *testing.T) {
|
|
st := newMem()
|
|
v := Versions{Store: st}
|
|
ctx := context.Background()
|
|
if _, _, err := v.Number(ctx, Claim{Name: "n", Kind: KindImage, Fingerprint: "f", Digest: "sha256:aa", Floor: known(nil, nil, nil)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range []struct {
|
|
version, digest string
|
|
ok bool
|
|
}{{"0.1.0", "sha256:aa", true}, {"0.1.0", "sha256:bb", false}, {"0.1.7", "sha256:aa", false}} {
|
|
if _, ok, why, err := v.Recorded(ctx, "n", c.version, c.digest); err != nil || ok != c.ok || (!ok && why == "") {
|
|
t.Errorf("Recorded(%s, %s) = %v, %q, %v; want %v", c.version, c.digest, ok, why, err, c.ok)
|
|
}
|
|
}
|
|
// builder_versions answers the same through the tools bundle's read-only view.
|
|
r := Reader{Keys: func() ([]string, error) {
|
|
var all []string
|
|
for k := range st.vals {
|
|
all = append(all, k)
|
|
}
|
|
return all, nil
|
|
}, Get: func(k string) (json.RawMessage, bool, error) {
|
|
v, _, ok, err := st.Get(ctx, k)
|
|
return v, ok, err
|
|
}}
|
|
a, err := AnswerVersions(ctx, r, "n", "0.1.0", "sha256:bb")
|
|
if err != nil || len(a.Versions) != 1 || a.Recorded == nil || *a.Recorded || a.Why == "" {
|
|
t.Errorf("builder_versions = %+v, %v; want one version and not recorded for another digest", a, err)
|
|
}
|
|
if _, err := AnswerVersions(ctx, r, " ", "", ""); err == nil {
|
|
t.Error("builder_versions with no name answered")
|
|
}
|
|
})
|
|
t.Run("tagged", func(t *testing.T) {
|
|
es := []Entry{{Version: "0.1.14", Series: "0.1"}, {Version: "0.1.15-feat-x.1", Series: "0.1", Tag: "feat-x"}}
|
|
if got := Tagged(es, "0.1", "feat/X", known(nil, nil, nil)); got != "0.1.15-feat-x.2" {
|
|
t.Errorf("Tagged = %s, want 0.1.15-feat-x.2", got)
|
|
}
|
|
if got := Tagged(nil, "0.1", "feat-x", known(nil, nil, nil)); got != "0.1.0-feat-x.1" {
|
|
t.Errorf("a package new in a group = %s, want 0.1.0-feat-x.1", got)
|
|
}
|
|
if got := CleanTag("--Fix/ä_1965--"); got != "fix-1965" {
|
|
t.Errorf("CleanTag = %s", got)
|
|
}
|
|
})
|
|
t.Run("the floor wakes on its silent source, never a timer", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
src := FloorSources{
|
|
Seat: func(context.Context, string, string) ([]string, error) { return []string{"0.1.3"}, nil },
|
|
Tags: func(context.Context, string, string) ([]string, error) { return nil, errors.New("no answer") },
|
|
Requirements: func(context.Context, string) ([]string, error) { return nil, nil },
|
|
}
|
|
f, why := src.Ask(ctx, FloorOf{Seat: "oci-artifact-registry", Name: "m/program"})
|
|
if f.Known() || !reflect.DeepEqual(f.Silent, []string{SourceTags}) || why[SourceTags] != "no answer" {
|
|
t.Fatalf("Ask = %+v, %v; want bridge_tags silent", f, why)
|
|
}
|
|
var fw FloorWaits
|
|
w := fw.Wait("m/program", "oci-artifact-registry", "distribution", f, why, time.Unix(0, 0))
|
|
if !strings.Contains(w.Finding(), "bridge_tags (no answer)") {
|
|
t.Errorf("finding = %q", w.Finding())
|
|
}
|
|
for _, c := range []struct {
|
|
kind, seat, module, name string
|
|
want []string
|
|
}{
|
|
{WakePublished, "oci-artifact-registry", "", "", nil}, // the seat answered already: a publish wakes nothing
|
|
{WakeAccepted, "", "", "", nil},
|
|
{WakeForge, "", "", "", []string{"m/program"}},
|
|
{WakeCleared, "", "mesh-delivery-forge-bridge", "", []string{"m/program"}}, // recovered with nothing to say
|
|
{WakeCleared, "", "mesh-delivery-indexer", "", nil},
|
|
{WakeByHand, "", "", "m/program", []string{"m/program"}},
|
|
{WakeByHand, "", "", "other", nil},
|
|
} {
|
|
if got := fw.Wake(c.kind, c.seat, c.module, c.name); !reflect.DeepEqual(got, c.want) {
|
|
t.Errorf("Wake(%s %s %s %s) = %v, want %v", c.kind, c.seat, c.module, c.name, got, c.want)
|
|
}
|
|
}
|
|
seatDown := Floor{Silent: []string{SourceSeat}}
|
|
fw.Wait("distribution/program", "container-image-registry", "distribution", seatDown, nil, time.Unix(0, 0))
|
|
if got := fw.Wake(WakePublished, "container-image-registry", "", ""); !reflect.DeepEqual(got, []string{"distribution/program"}) {
|
|
t.Errorf("the holder's published woke %v, want its own image's name", got)
|
|
}
|
|
if got := fw.Wake(WakeCleared, "", "distribution", ""); !reflect.DeepEqual(got, []string{"distribution/program"}) {
|
|
t.Errorf("the holder's clear woke %v", got)
|
|
}
|
|
if !fw.Answered("m/program") || fw.Answered("m/program") || len(fw.Waiting()) != 1 {
|
|
t.Error("Answered drops the wait once")
|
|
}
|
|
// DLV-B4: the waits are kept in floor-waits; a restarted builder takes them in, and its sources' events still wake them.
|
|
kept := keeperMem{}
|
|
restarted := &FloorWaits{}
|
|
restarted.Keep(kept, []FloorWait{{Name: "m/held", Seat: SeatImages, Silent: []string{SourceTags}, Since: time.Unix(5, 0)}})
|
|
if got := restarted.Wake(WakeForge, "", "", ""); !reflect.DeepEqual(got, []string{"m/held"}) {
|
|
t.Errorf("a wait held in floor-waits woke %v after a restart, want m/held", got)
|
|
}
|
|
restarted.Wait("m/new", SeatNPM, "", Floor{Silent: []string{SourceSeat}}, nil, time.Unix(9, 0))
|
|
ans, err := AnswerFloorWaits(Reader{Keys: kept.keys, Get: kept.get})
|
|
if err != nil || len(ans.Waits) != 2 || ans.Waits[0].Name != "m/held" || ans.Waits[1].Name != "m/new" || !strings.Contains(ans.Waits[1].Finding, "seat did not answer") {
|
|
t.Fatalf("builder_floor_waits = %+v, %v", ans, err)
|
|
}
|
|
restarted.Answered("m/held")
|
|
if _, ok := kept[FloorWaitKey("m/held")]; ok {
|
|
t.Error("an answered floor stays in floor-waits")
|
|
}
|
|
})
|
|
t.Run("old form", func(t *testing.T) {
|
|
// One kind, a first form and a newer one.
|
|
fs := forms.ArtifactForms{{Name: "registry", First: true, Kinds: []string{"image"}}, {Name: "oci", Kinds: []string{"image"}}}
|
|
both := []string{forms.FormNeed("oci")}
|
|
for _, c := range []struct {
|
|
name string
|
|
counted map[string][]string
|
|
dropped map[string]bool
|
|
want []string
|
|
}{
|
|
{"one node lacking the new form holds the old", map[string][]string{"a": both, "b": nil}, nil, []string{"b"}},
|
|
{"an away node counts, stating its last", map[string][]string{"a": both, "c": {"bus-state"}}, nil, []string{"c"}},
|
|
{"every counted node reads the newest: closed", map[string][]string{"a": both, "c": both}, nil, nil},
|
|
{"a retired node dropped by hand counts no more", map[string][]string{"a": both, "c": nil}, map[string]bool{"c": true}, nil},
|
|
{"the builder's own forms: no window", nil, nil, nil},
|
|
} {
|
|
if got := Holding(fs, c.counted, c.dropped); !reflect.DeepEqual(got, c.want) {
|
|
t.Errorf("%s: Holding = %v, want %v", c.name, got, c.want)
|
|
}
|
|
}
|
|
// The builder's own forms: the registry seat's form of a bundle is not a first form, so a node stating nothing
|
|
// holds the window and one stating form:seat-reference does not.
|
|
if got := Holding(forms.Current, map[string][]string{"a": nil, "b": {forms.FormNeed(forms.FormSeatReference)}}, nil); !reflect.DeepEqual(got, []string{"a"}) {
|
|
t.Errorf("forms.Current, a node stating nothing and one stating form:seat-reference: Holding = %v, want a", got)
|
|
}
|
|
t0 := time.Unix(0, 0)
|
|
named := []string{"c", "d"}
|
|
w, closed, overdue := OldFormWindow{}.Step(named, []string{"c"}, t0)
|
|
if closed || len(overdue) != 0 || !w.Open {
|
|
t.Fatalf("Step = %+v %v %v", w, closed, overdue)
|
|
}
|
|
w, _, overdue = w.Step(named, []string{"c"}, t0.Add(FindingAfter))
|
|
if !reflect.DeepEqual(overdue, []string{"c"}) {
|
|
t.Errorf("seven days held: overdue %v, want c", overdue)
|
|
}
|
|
w, err := w.Drop("d", "op@shanks", "retired", t0)
|
|
if err != nil || !w.DroppedSet()["d"] {
|
|
t.Fatalf("Drop = %+v, %v", w, err)
|
|
}
|
|
if again, _ := w.Drop("d", "op@shanks", "again", t0); len(again.Dropped) != 1 {
|
|
t.Errorf("a node dropped twice is kept twice: %+v", again.Dropped)
|
|
}
|
|
if _, err := w.Drop("e", "op@shanks", "", t0); err == nil {
|
|
t.Error("a drop with no why is taken")
|
|
}
|
|
// Security-upgrade F3 (review round 1), by mesh-sdk's delivery.StepWindow: a read naming no node (an empty
|
|
// node_haves, the placement not read yet) never closes the window, and its holder's seven days still count.
|
|
if held, shut, late := w.Step(nil, nil, t0.Add(FindingAfter+time.Minute)); shut || !held.Open || !reflect.DeepEqual(late, []string{"c"}) {
|
|
t.Fatalf("a read naming no node: %+v closed=%v overdue %v, want the window open and c overdue", held, shut, late)
|
|
}
|
|
w, closed, _ = w.Step(named, nil, t0.Add(FindingAfter+time.Hour))
|
|
if !closed || w.Open || !reflect.DeepEqual(w.LastHolding, []string{"c"}) || w.Closing().Dropped[0].Node != "d" {
|
|
t.Errorf("closing: %+v closed=%v", w, closed)
|
|
}
|
|
raw, _ := json.Marshal(w)
|
|
if back, err := DecodeOldFormWindow(raw); err != nil || !back.Closed.Equal(w.Closed) {
|
|
t.Errorf("the window read back = %+v, %v", back, err)
|
|
}
|
|
if _, again, _ := w.Step(named, nil, t0.Add(FindingAfter+2*time.Hour)); again {
|
|
t.Error("a closed window closes again: old-form-closed would be said twice")
|
|
}
|
|
})
|
|
t.Run("at the claim", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
tagsUp := false
|
|
var asked []string
|
|
src := FloorSources{
|
|
Seat: func(context.Context, string, string) ([]string, error) { return []string{"0.1.3"}, nil },
|
|
Tags: func(_ context.Context, repo, prefix string) ([]string, error) {
|
|
asked = append(asked, repo+" "+prefix)
|
|
if !tagsUp {
|
|
return nil, errors.New("no answer")
|
|
}
|
|
return []string{"go/v0.1.13"}, nil
|
|
},
|
|
Requirements: func(context.Context, string) ([]string, error) { return nil, nil },
|
|
}
|
|
var said []delivery.VersionNumbered
|
|
fw := &FloorWaits{}
|
|
n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Waits: fw, Said: func(b delivery.VersionNumbered) { said = append(said, b) }}
|
|
c := BuildClaim{Module: "m", Path: "go", Repository: "novox/m", Fingerprint: "fp1", Commit: "c1", Revision: "7", Merge: 7,
|
|
Artifacts: []ClaimArtifact{{Artifact: "program", Kind: KindBundle, Language: "go"}, {Artifact: "base", Kind: "upstream"}}}
|
|
got, err := n.Number(ctx, c)
|
|
if err != nil || len(got.Versions) != 0 || !reflect.DeepEqual(got.Unnumbered, []string{"program"}) || got.Wait != "" {
|
|
t.Fatalf("silent floor: %+v, %v; want program unnumbered, the upstream copy left out", got, err)
|
|
}
|
|
if !reflect.DeepEqual(asked, []string{"novox/m go/v"}) || len(fw.Waiting()) != 1 || len(said) != 0 {
|
|
t.Fatalf("asked %v, waits %+v, said %v", asked, fw.Waiting(), said)
|
|
}
|
|
pkg := c
|
|
pkg.Artifacts = []ClaimArtifact{{Artifact: "lib", Kind: KindPackage, Language: "go"}}
|
|
if got, _ := n.Number(ctx, pkg); got.Wait == "" || len(got.Versions) != 0 {
|
|
t.Fatalf("a package with no floor = %+v; want it to wait", got)
|
|
}
|
|
tagsUp = true
|
|
got, err = n.Number(ctx, c)
|
|
if err != nil || got.Versions["program"] != "0.1.14" || len(said) != 1 || said[0].Version != "0.1.14" || len(fw.Waiting()) != 1 {
|
|
t.Fatalf("floor answered: %+v, %v, said %v, waits %+v; want 0.1.14 said once, lib still waiting", got, err, said, fw.Waiting())
|
|
}
|
|
// DLV-B6: a check's build of a head a tag ask names numbers only its packages, tagged below the next version, kept
|
|
// with its repository for the branch's end, said with the head; claimed again it keeps its number.
|
|
tagged := c
|
|
tagged.Tag, tagged.Head, tagged.Revision, tagged.Merge = "feat-x", "h1", "", 0
|
|
tagged.Artifacts = append(tagged.Artifacts, ClaimArtifact{Artifact: "tlib", Kind: KindPackage, Language: "go"})
|
|
saidBefore, askedBefore := said, asked
|
|
said = nil
|
|
got, err = n.Number(ctx, tagged)
|
|
if err != nil || len(got.Versions) != 1 || !strings.Contains(got.Versions["tlib"], "-feat-x.1") || len(said) != 1 || said[0].Head != "h1" || said[0].Tag != "feat-x" {
|
|
t.Fatalf("tagged claim: %+v, %v, said %+v; want tlib alone tagged, said with its head", got, err, said)
|
|
}
|
|
if es, _ := n.Versions.Of(ctx, "m/tlib"); len(es) != 1 || es[0].Repository != "novox/m" {
|
|
t.Errorf("the tagged version kept = %+v, want its repository", es)
|
|
}
|
|
if again, _ := n.Number(ctx, tagged); again.Versions["tlib"] != got.Versions["tlib"] || len(said) != 1 {
|
|
t.Errorf("claimed again: %+v, said %d; want the same number, said once", again, len(said))
|
|
}
|
|
said, asked = saidBefore, askedBefore
|
|
got, _ = n.Number(ctx, c)
|
|
var publishing []string // review round 1: a version built and still numbered arms its publish bound
|
|
n.Publishing = func(_ context.Context, e Entry) { publishing = append(publishing, e.Name+"@"+e.Version) }
|
|
if err := n.Built(ctx, c, got, map[string]string{"program": "sha256:aa"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n.Publishing = nil; !reflect.DeepEqual(publishing, []string{"m/program@0.1.14"}) {
|
|
t.Fatalf("the publish bounds armed: %v, want m/program@0.1.14", publishing)
|
|
}
|
|
again, _ := n.Number(ctx, c)
|
|
if again.Versions["program"] != "0.1.14" || len(said) != 1 || len(asked) != 3 {
|
|
t.Fatalf("again: %+v, said %d, asked %v; want the number kept, nothing said, the floor not asked", again, len(said), asked)
|
|
}
|
|
if _, ok, why, _ := n.Versions.Recorded(ctx, "m/program", "0.1.14", "sha256:bb"); ok {
|
|
t.Fatalf("another digest recorded: %s", why)
|
|
}
|
|
// DLV-B4: the seat's events move the entry; a published wakes the names waiting on that seat.
|
|
if wake, err := n.Heard(ctx, SeatArtifacts, HeardPublished, "e1", Outcome{Name: "m/other", Digest: "sha256:cc"}); err != nil || len(wake) != 0 {
|
|
t.Fatalf("a by-digest publish of another seat's wait: %v, %v", wake, err)
|
|
}
|
|
n.Waits.Wait("m/lib", SeatGoPackages, "", Floor{Silent: []string{SourceSeat}}, nil, time.Unix(0, 0))
|
|
if wake, _ := n.Heard(ctx, SeatGoPackages, HeardPublished, "e2", Outcome{Name: "m/x", Digest: "sha256:dd"}); !reflect.DeepEqual(wake, []string{"m/lib"}) {
|
|
t.Fatalf("a published on the Go seat woke %v, want m/lib, whose seat was silent", wake)
|
|
}
|
|
for _, h := range []struct{ event, state, reason string }{{HeardRefused, Refused, "bytes differ"}, {HeardPublished, Published, ""}, {HeardRefused, Published, ""}, {HeardRemoved, Removed, "retention"}} {
|
|
if _, err := n.Heard(ctx, SeatArtifacts, h.event, "e3", Outcome{Name: "m/program", Version: "0.1.14", Digest: "sha256:aa", Reason: h.reason}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
all, _ := n.Versions.Of(ctx, "m/program")
|
|
if all[0].State != h.state {
|
|
t.Fatalf("after %s: %+v, want %s", h.event, all[0], h.state)
|
|
}
|
|
}
|
|
if _, err := n.Heard(ctx, SeatArtifacts, HeardPublished, "e4", Outcome{Name: "m/none", Version: "0.1.0"}); err == nil {
|
|
t.Fatal("a published of a version the builder never numbered is not said")
|
|
}
|
|
})
|
|
t.Run("a package is numbered under its name in its ecosystem", func(t *testing.T) {
|
|
// design 32: the indexer's packages state names, in each series key, the package's name and the <module>/<artifact>
|
|
// it is built from; the builder numbers, asks the floor of and records the package under that name.
|
|
ctx := context.Background()
|
|
state := packagesMem{
|
|
"git=2enovox=2ebe/novox/mesh-sdk/go.0.1": []byte(`{"v":1,"name":"git.novox.be/novox/mesh-sdk/go","package":"mesh-sdk/go","series":"0.1"}`),
|
|
"@novox/mesh-sdk.0.1": []byte(`{"v":1,"name":"@novox/mesh-sdk","package":"mesh-sdk/ts","series":"0.1"}`),
|
|
"git=2enovox=2ebe/novox/mesh-sdk/go": []byte(`{"v":1,"module":"git.novox.be/novox/mesh-sdk/go"}`),
|
|
"broken.0.1": []byte(`not json`),
|
|
}
|
|
var floorOf []string
|
|
src := FloorSources{
|
|
Seat: func(_ context.Context, _, name string) ([]string, error) {
|
|
floorOf = append(floorOf, name)
|
|
return []string{"0.1.3"}, nil
|
|
},
|
|
Tags: func(context.Context, string, string) ([]string, error) { return nil, nil },
|
|
Requirements: func(_ context.Context, name string) ([]string, error) {
|
|
floorOf = append(floorOf, name)
|
|
return nil, nil
|
|
},
|
|
}
|
|
var said []delivery.VersionNumbered
|
|
n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Names: NamesIn(state), Said: func(b delivery.VersionNumbered) { said = append(said, b) }}
|
|
c := BuildClaim{Module: "mesh-sdk", Path: "go", Repository: "novox/mesh-sdk", Fingerprint: "fp1", Commit: "c1", Revision: "3", Merge: 3,
|
|
Artifacts: []ClaimArtifact{{Artifact: "go", Kind: KindPackage, Language: "go"}, {Artifact: "ts", Kind: KindPackage, Language: "typescript"},
|
|
{Artifact: "new", Kind: KindPackage, Language: "go"}, {Artifact: "program", Kind: KindBundle, Language: "go"}}}
|
|
got, err := n.Number(ctx, c)
|
|
if err != nil || got.Versions["go"] != "0.1.4" || got.Versions["ts"] != "0.1.4" || !reflect.DeepEqual(got.Unnumbered, []string{"new"}) {
|
|
t.Fatalf("Number = %+v, %v; want go and ts numbered, new unnumbered (no name yet)", got, err)
|
|
}
|
|
if got.Names["go"] != "git.novox.be/novox/mesh-sdk/go" || got.Names["ts"] != "@novox/mesh-sdk" || got.Names["program"] != "mesh-sdk/program" {
|
|
t.Errorf("names = %v", got.Names)
|
|
}
|
|
if !slices.Contains(floorOf, "git.novox.be/novox/mesh-sdk/go") || slices.Contains(floorOf, "mesh-sdk/go") {
|
|
t.Errorf("the floor was asked of %v, want the Go module path", floorOf)
|
|
}
|
|
if es, _ := n.Versions.Of(ctx, "git.novox.be/novox/mesh-sdk/go"); len(es) != 1 || es[0].Version != "0.1.4" {
|
|
t.Errorf("versions of the Go module path = %+v", es)
|
|
}
|
|
if es, _ := n.Versions.Of(ctx, "mesh-sdk/go"); len(es) != 0 {
|
|
t.Errorf("numbered under the builder's name too: %+v", es)
|
|
}
|
|
names := map[string]bool{}
|
|
for _, b := range said {
|
|
names[b.Name] = true
|
|
}
|
|
if !names["git.novox.be/novox/mesh-sdk/go"] || !names["@novox/mesh-sdk"] {
|
|
t.Errorf("version.numbered said %v, want the ecosystem names", names)
|
|
}
|
|
if err := n.Built(ctx, c, got, map[string]string{"go": "sha256:aa"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if es, _ := n.Versions.Of(ctx, "git.novox.be/novox/mesh-sdk/go"); len(es) != 1 || es[0].Digest != "sha256:aa" {
|
|
t.Errorf("the built digest is not recorded under the name: %+v", es)
|
|
}
|
|
tagged, _ := TagAsk{Head: "h", State: TagAsked}.WithNumbered("mesh-sdk", "h", map[string]string{"go": "0.1.4-x.1"}, got.Names, time.Unix(1, 0))
|
|
if tagged.Versions["git.novox.be/novox/mesh-sdk/go"] != "0.1.4-x.1" {
|
|
t.Errorf("a tag ask names %v, want the Go module path", tagged.Versions)
|
|
}
|
|
// The indexer writes the name of `new` later: its series key names the module, and the look at the done build
|
|
// numbers it with the done key's digest, said in version.numbered for the late publish; the rest are kept.
|
|
state["new.0.1"] = []byte(`{"v":1,"name":"new","package":"mesh-sdk/new","series":"0.1"}`)
|
|
for key, want := range map[string]string{"new.0.1": "mesh-sdk", "git=2enovox=2ebe/novox/mesh-sdk/go": "", "broken.0.1": ""} {
|
|
if module, name, ok := NamedModule(key, state[key]); module != want || ok != (want != "") || (ok && name != "new") {
|
|
t.Errorf("NamedModule(%s) = %q, %q, %v, want %q", key, module, name, ok, want)
|
|
}
|
|
}
|
|
said = nil
|
|
done := c
|
|
done.Done = map[string]string{"go": "sha256:aa", "new": "sha256:bb"}
|
|
late, err := n.Number(ctx, done)
|
|
if err != nil || late.Versions["new"] != "0.1.4" || late.Versions["go"] != "0.1.4" || len(late.Unnumbered) != 0 {
|
|
t.Fatalf("a look at the done build = %+v, %v; want new numbered and go kept", late, err)
|
|
}
|
|
if len(said) != 1 || said[0].Name != "new" || said[0].Digest != "sha256:bb" {
|
|
t.Errorf("version.numbered said %+v, want new once with the done build's digest", said)
|
|
}
|
|
if es, _ := n.Versions.Of(ctx, "new"); len(es) != 1 || es[0].Digest != "sha256:bb" {
|
|
t.Errorf("versions of new = %+v, want its done digest recorded", es)
|
|
}
|
|
if _, _, err := NamesIn(packagesMem(nil).failing())(ctx, "m", "a"); err == nil {
|
|
t.Error("a packages state that cannot be read is no error")
|
|
}
|
|
// A packages state not on the bus yet leaves the package unnumbered, said, and numbers the rest of the claim.
|
|
n.Names = NamesIn(packagesMem(nil).failing())
|
|
bare, err := n.Number(ctx, c)
|
|
if err != nil || !reflect.DeepEqual(bare.Unnumbered, []string{"go", "new", "ts"}) || bare.Versions["program"] == "" ||
|
|
!strings.Contains(bare.Said[0].Line, "cannot be read") {
|
|
t.Errorf("an unreadable packages state: %+v, %v", bare, err)
|
|
}
|
|
})
|
|
t.Run("fill", func(t *testing.T) {
|
|
if _, full := Fill(Cap*79/100, Cap); full {
|
|
t.Error("79 % is full")
|
|
}
|
|
if _, full := Fill((Cap*80+99)/100, Cap); !full {
|
|
t.Error("80 % is not full")
|
|
}
|
|
})
|
|
t.Run("publish again", func(t *testing.T) {
|
|
num := Entry{Name: "m/p", Kind: KindBundle, Version: "0.1.3", Fingerprint: "f1", Digest: "sha256:aa", State: Numbered}
|
|
ref := num
|
|
ref.State, ref.RefusedDigest = Refused, "sha256:aa"
|
|
other := ref
|
|
other.RefusedDigest = "sha256:bb"
|
|
pub := num
|
|
pub.State = Published
|
|
gone := pub
|
|
gone.State = Removed
|
|
tagged := num
|
|
tagged.Tag, tagged.Version = "feat-x", "0.1.3-feat-x.1"
|
|
for _, c := range []struct {
|
|
name string
|
|
entries []Entry
|
|
fp string
|
|
built bool
|
|
do string
|
|
}{
|
|
{"a held build is said numbered again", []Entry{num}, "f1", true, AgainPublish},
|
|
{"refused for its own bytes is published again", []Entry{ref}, "f1", true, AgainPublish},
|
|
{"a gone file is built again under the same number", []Entry{ref}, "f1", false, AgainBuild},
|
|
{"after a published it takes a new number", []Entry{pub}, "f1", true, AgainRenumber},
|
|
{"after a removal it takes a new number", []Entry{gone}, "f1", false, AgainRenumber},
|
|
{"refused for other bytes is refused", []Entry{other}, "f1", true, ""},
|
|
{"a fingerprint not numbered is refused", []Entry{num}, "f2", true, ""},
|
|
{"a tagged version is not a trunk fingerprint's", []Entry{tagged}, "f1", true, ""},
|
|
} {
|
|
a, err := PublishAgain(c.entries, c.fp, c.built)
|
|
if (err != nil) != (c.do == "") || a.Do != c.do {
|
|
t.Errorf("%s: PublishAgain = %+v, %v; want %q", c.name, a, err, c.do)
|
|
}
|
|
}
|
|
ctx := context.Background()
|
|
// A fresh claim of a published fingerprint takes the next number; the old one stays.
|
|
st := newMem()
|
|
v := Versions{Store: st}
|
|
raw, _ := Encode(Entry{Name: "m/p", Kind: KindBundle, Series: "0.1", Version: "0.1.3", Fingerprint: "f1", State: Published, At: time.Unix(1, 0)})
|
|
_, _ = st.Create(ctx, Key("m/p", "0.1.3"), raw)
|
|
if d, _, err := v.Number(ctx, Claim{Name: "m/p", Kind: KindBundle, Fingerprint: "f1"}); err != nil || d.Version != "0.1.3" || !d.Keep {
|
|
t.Fatalf("a published fingerprint keeps its number: %+v %v", d, err)
|
|
}
|
|
if d, e, err := v.Number(ctx, Claim{Name: "m/p", Kind: KindBundle, Fingerprint: "f1", Fresh: true}); err != nil || d.Version != "0.1.4" || d.Keep || e.Fingerprint != "f1" {
|
|
t.Fatalf("a fresh claim takes the next number: %+v %+v %v", d, e, err)
|
|
}
|
|
// A refusal records the digest refused; Built replaces a digest no published was said for, never a published one.
|
|
n := &Numberer{Versions: v}
|
|
_, _ = n.Heard(ctx, SeatArtifacts, HeardRefused, "r1", Outcome{Name: "m/p", Version: "0.1.4", Digest: "sha256:old", Reason: "x"})
|
|
if es, _ := v.Of(ctx, "m/p"); es[1].RefusedDigest != "sha256:old" {
|
|
t.Errorf("refused records no digest: %+v", es[1])
|
|
}
|
|
_, _ = v.Mark(ctx, "m/p", "0.1.4", func(e *Entry) { e.Digest = "sha256:old" })
|
|
bc := BuildClaim{Module: "m"}
|
|
_ = n.Built(ctx, bc, Numbering{Versions: map[string]string{"p": "0.1.4"}}, map[string]string{"p": "sha256:new"})
|
|
_, _ = v.Mark(ctx, "m/p", "0.1.3", func(e *Entry) { e.Digest = "sha256:held" })
|
|
_ = n.Built(ctx, bc, Numbering{Versions: map[string]string{"p": "0.1.3"}}, map[string]string{"p": "sha256:other"})
|
|
es, _ := v.Of(ctx, "m/p")
|
|
if es[0].Digest != "sha256:held" || es[1].Digest != "sha256:new" {
|
|
t.Errorf("Built: published %s (want sha256:held), refused %s (want sha256:new)", es[0].Digest, es[1].Digest)
|
|
}
|
|
if all, err := v.All(ctx); err != nil || len(all) != 2 {
|
|
t.Errorf("All = %d entries, %v", len(all), err)
|
|
}
|
|
// The hand acts: a held build is said numbered once more under a fresh dedup id; a gone one is built again; a
|
|
// published one takes a new number; a name with versions asks no floor.
|
|
if _, _, err := (Acts{Numberer: &Numberer{Versions: v}, Done: func(context.Context, string, string) (bool, error) { return true, nil }}).PublishAgain(ctx, "m/p", "f1", "w", "", time.Unix(4, 0)); err == nil {
|
|
t.Error("a version refused for other bytes is published again")
|
|
}
|
|
_, _ = v.Mark(ctx, "m/p", "0.1.4", func(e *Entry) { e.State = Published })
|
|
var said []delivery.VersionNumbered
|
|
var rebuilt []string
|
|
done := true
|
|
acts := Acts{Numberer: &Numberer{Versions: v, Said: func(b delivery.VersionNumbered) { said = append(said, b) }, Waits: &FloorWaits{}},
|
|
Done: func(context.Context, string, string) (bool, error) { return done, nil },
|
|
BuildAgain: func(_ context.Context, m, fp, _, _ string) (string, bool) {
|
|
rebuilt = append(rebuilt, m+" "+fp)
|
|
return "built again", false
|
|
},
|
|
Reconsider: func(context.Context) {}}
|
|
if ag, _, err := acts.PublishAgain(ctx, "m/p", "f1", "w", "me", time.Unix(5, 0)); err != nil || ag.Do != AgainRenumber || ag.Entry.Version != "0.1.5" ||
|
|
len(said) != 1 || said[0].Version != "0.1.5" || said[0].Again == "" || len(rebuilt) != 0 {
|
|
t.Errorf("publish again of a published, held build: %+v %v said %+v rebuilt %v", ag, err, said, rebuilt)
|
|
}
|
|
done = false
|
|
if ag, _, err := acts.PublishAgain(ctx, "m/p", "f1", "w", "me", time.Unix(6, 0)); err != nil || ag.Do != AgainBuild || ag.Entry.Version != "0.1.5" || len(rebuilt) != 1 || len(said) != 1 {
|
|
t.Errorf("publish again of a gone build: %+v %v said %d rebuilt %v", ag, err, len(said), rebuilt)
|
|
}
|
|
if _, err := acts.FloorAgain(ctx, "m/p"); err == nil {
|
|
t.Error("a name with versions asks its floor again")
|
|
}
|
|
if ans, err := acts.FloorAgain(ctx, "m/new"); err != nil || ans == "" {
|
|
t.Errorf("floor again of a new name: %q %v", ans, err)
|
|
}
|
|
})
|
|
t.Run("tag asks", func(t *testing.T) {
|
|
at := time.Unix(100, 0)
|
|
k, raw, err := NewTagAsk("novox/mesh-sdk", "feat/X y", "abc", "a check", "me@shanks", at)
|
|
if err != nil || k != TagAskKey("novox/mesh-sdk", "feat/X y") || strings.Count(k, ".") != 1 {
|
|
t.Fatalf("NewTagAsk = %q, %v", k, err)
|
|
}
|
|
a, err := DecodeTagAsk(raw)
|
|
if err != nil || a.Tag != "feat-x-y" || a.State != TagAsked || a.Untag {
|
|
t.Fatalf("DecodeTagAsk = %+v, %v", a, err)
|
|
}
|
|
for _, bad := range [][4]string{{"mesh-sdk", "b", "h", "w"}, {"o/r", "", "h", "w"}, {"o/r", "b", "", "w"}, {"o/r", "b", "h", ""}} {
|
|
if _, _, err := NewTagAsk(bad[0], bad[1], bad[2], bad[3], "", at); err == nil {
|
|
t.Errorf("NewTagAsk%v is not refused", bad)
|
|
}
|
|
}
|
|
_, uraw, err := NewUntag("o/r", "feat/x", "branch gone by hand", "", at.Add(time.Second))
|
|
if u, _ := DecodeTagAsk(uraw); err != nil || !u.Untag {
|
|
t.Fatalf("NewUntag = %+v, %v", u, err)
|
|
}
|
|
vals := map[string][]byte{"a": uraw, "b": raw}
|
|
r := Reader{Keys: func() ([]string, error) { return []string{"a", "b"}, nil },
|
|
Get: func(k string) (json.RawMessage, bool, error) { return vals[k], true, nil }}
|
|
if got, err := AnswerTagAsks(r, ""); err != nil || len(got.Asks) != 2 || got.Asks[0].Repository != "novox/mesh-sdk" {
|
|
t.Errorf("AnswerTagAsks = %+v, %v", got, err)
|
|
}
|
|
if got, _ := AnswerTagAsks(r, "o/r"); len(got.Asks) != 1 || !got.Asks[0].Untag {
|
|
t.Errorf("AnswerTagAsks(o/r) = %+v", got)
|
|
}
|
|
es := []Entry{{Name: "n", Version: "0.1.2-feat-x.1", Tag: "feat-x", Series: "0.1", Repository: "o/r"}, {Name: "n", Version: "0.1.2-feat-x.2", Tag: "feat-x", Series: "0.1", State: Removed, Repository: "o/r"},
|
|
{Name: "n2", Version: "0.1.2-feat-x.1", Tag: "feat-x", Series: "0.1", Repository: "o/other"},
|
|
{Name: "n", Version: "0.1.1", Series: "0.1"}, {Name: "n", Version: "0.1.2-feat-y.1", Tag: "feat-y", Series: "0.1"}}
|
|
for _, c := range []struct {
|
|
asks []TagAsk
|
|
repo, head string
|
|
want string
|
|
}{
|
|
{[]TagAsk{a}, "novox/mesh-sdk", "abc", "feat-x-y"},
|
|
{[]TagAsk{a}, "novox/mesh-sdk", "other", ""},
|
|
{[]TagAsk{{Repository: "novox/mesh-sdk", Head: "abc", Tag: "t", State: TagRemoved}}, "novox/mesh-sdk", "abc", ""},
|
|
} {
|
|
if got, _ := TagFor(c.asks, c.repo, c.head); got != c.want {
|
|
t.Errorf("TagFor(%s, %s) = %q, want %q", c.repo, c.head, got, c.want)
|
|
}
|
|
}
|
|
if got := Untagged(es, "o/r", "feat/x"); len(got) != 1 || got[0].Version != "0.1.2-feat-x.1" {
|
|
t.Errorf("Untagged = %+v", got)
|
|
}
|
|
st := newMem()
|
|
for _, e := range append(es, Entry{Name: "n3", Version: "0.1.0-feat-x.1", Tag: "feat-x", Series: "0.1", Repository: "o/r", Seat: SeatNPM}) {
|
|
e.Kind, e.Module, e.Fingerprint, e.At = KindPackage, "m", "f", at
|
|
if e.State == "" {
|
|
e.State = Numbered
|
|
}
|
|
raw, err := Encode(e)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _ = st.Create(context.Background(), Key(e.Name, e.Version), raw)
|
|
}
|
|
var removed []string
|
|
acts := Acts{Numberer: &Numberer{Versions: Versions{Store: st}}, Remove: func(_ context.Context, seat string, e Entry) error {
|
|
removed = append(removed, e.Name+"@"+e.Version)
|
|
if seat == SeatNPM {
|
|
return errors.New("down")
|
|
}
|
|
return nil
|
|
}}
|
|
u, _ := DecodeTagAsk(uraw)
|
|
got := acts.TagAsked(context.Background(), u, at)
|
|
if got.State != TagRemoving || len(removed) != 2 || len(got.Left) != 2 || !strings.Contains(got.Left["n3@0.1.0-feat-x.1"], "down") || got.Answered.IsZero() {
|
|
t.Errorf("untag: %+v removed %v", got, removed)
|
|
}
|
|
if k := acts.TagAsked(context.Background(), a, at); k.State != TagAsked || k.Answer == "" {
|
|
t.Errorf("a tag ask is not kept: %+v", k)
|
|
}
|
|
// DLV-B6: a branch's end asks remove of every tagged version not removed, and is done once each seat said removed.
|
|
removed = nil
|
|
end, err := acts.BranchEnded(context.Background(), "o/r", "feat/x")
|
|
if err != nil || end.Done || len(removed) != 2 || !reflect.DeepEqual(end.Seats(), []string{"", SeatNPM}) || !strings.Contains(end.Finding("o/r", "feat/x", at), "n3@0.1.0-feat-x.1 on npm-package-registry: down") {
|
|
t.Fatalf("BranchEnded = %+v %v, removed %v", end, err, removed)
|
|
}
|
|
for _, v := range [][2]string{{"n", "0.1.2-feat-x.1"}, {"n3", "0.1.0-feat-x.1"}} {
|
|
if _, err := acts.Numberer.Heard(context.Background(), SeatNPM, HeardRemoved, "rm", Outcome{Name: v[0], Version: v[1], Reason: "tag-gone"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if end, _ := acts.BranchEnded(context.Background(), "o/r", "feat/x"); !end.Done {
|
|
t.Errorf("every seat said removed, and the branch's end is not done: %+v", end)
|
|
}
|
|
// B6: a check's build of the ask's head marks it numbered with its versions; another head, or an untag, is left.
|
|
if !a.Unanswered() {
|
|
t.Errorf("a kept ask with no version is answered: %+v", a)
|
|
}
|
|
if _, changed := a.WithNumbered("mesh-sdk", "other", map[string]string{"go": "0.1.15-feat-x-y.1"}, nil, at); changed {
|
|
t.Error("an ask is marked numbered by a build of another head")
|
|
}
|
|
if _, changed := u.WithNumbered("o", "", map[string]string{"go": "1"}, nil, at); changed {
|
|
t.Error("an untag is marked numbered")
|
|
}
|
|
n, changed := a.WithNumbered("mesh-sdk", "abc", map[string]string{"go": "0.1.15-feat-x-y.1"}, nil, at.Add(time.Minute))
|
|
if !changed || n.State != TagNumbered || n.Versions["mesh-sdk/go"] != "0.1.15-feat-x-y.1" || n.Unanswered() || !strings.Contains(n.Answer, "mesh-sdk/go 0.1.15-feat-x-y.1") {
|
|
t.Errorf("WithNumbered = %+v, %v", n, changed)
|
|
}
|
|
if _, again := n.WithNumbered("mesh-sdk", "abc", map[string]string{"go": "0.1.15-feat-x-y.1"}, nil, at); again {
|
|
t.Error("the same versions mark the ask again")
|
|
}
|
|
said := NarrateUnanswered(a, []FloorWait{{Name: "mesh-sdk/go", Silent: []string{"bridge_tags"}}}, at.Add(6*time.Minute))
|
|
for _, w := range []string{"novox/mesh-sdk feat/X y", "after 6m0s", "not claimed yet", "mesh-sdk/go waits on its floor (bridge_tags silent)"} {
|
|
if !strings.Contains(said, w) {
|
|
t.Errorf("NarrateUnanswered = %q, want %q in it", said, w)
|
|
}
|
|
}
|
|
if k, v, err := TagWantedAsk(TagWanted{Repository: "o/r", Branch: "feat/x", Head: "abc"}, "mesh-delivery-checker", at); err != nil || k != TagAskKey("o/r", "feat/x") || !strings.Contains(string(v), `"by":"mesh-delivery-checker"`) {
|
|
t.Errorf("TagWantedAsk = %q %s %v", k, v, err)
|
|
}
|
|
})
|
|
}
|