package versions import ( "context" "encoding/json" "errors" "maps" "reflect" "slices" "strings" "sync" "testing" "time" "git.novox.be/novox/mesh-delivery/sdk/forms" "git.novox.be/novox/mesh-sdk/go/delivery" ) // mem is a Store in memory; before runs once before the next Create, to stage a race. type mem struct { mu sync.Mutex vals map[string][]byte revs map[string]uint64 rev uint64 before func() } func newMem() *mem { return &mem{vals: map[string][]byte{}, revs: map[string]uint64{}} } func (m *mem) Get(_ context.Context, key string) ([]byte, uint64, bool, error) { m.mu.Lock() defer m.mu.Unlock() v, ok := m.vals[key] return v, m.revs[key], ok, nil } func (m *mem) Create(ctx context.Context, key string, value []byte) (uint64, error) { if f := m.before; f != nil { m.before = nil f() } m.mu.Lock() defer m.mu.Unlock() if _, ok := m.vals[key]; ok { return 0, ErrConflict } m.rev++ m.vals[key], m.revs[key] = value, m.rev return m.rev, nil } func (m *mem) Update(_ context.Context, key string, value []byte, revision uint64) (uint64, error) { m.mu.Lock() defer m.mu.Unlock() if m.revs[key] != revision { return 0, ErrConflict } m.rev++ m.vals[key], m.revs[key] = value, m.rev return m.rev, nil } func (m *mem) KeysOf(_ context.Context, filter string) ([]string, error) { m.mu.Lock() defer m.mu.Unlock() prefix := strings.TrimSuffix(filter, "*") var out []string for k := range m.vals { if filter == AllFilter { out = append(out, k) continue } if strings.HasPrefix(k, prefix) && !strings.Contains(k[len(prefix):], ".") { out = append(out, k) } } return out, nil } // keeperMem is the state floor-waits in memory. type keeperMem map[string][]byte func (k keeperMem) Put(key string, v []byte) error { k[key] = v; return nil } func (k keeperMem) Delete(key string) error { delete(k, key); return nil } func (k keeperMem) keys() ([]string, error) { var out []string for key := range k { out = append(out, key) } return out, nil } func (k keeperMem) get(key string) (json.RawMessage, bool, error) { v, ok := k[key]; return v, ok, nil } // packagesMem is the indexer's packages state as the builder reads it, in memory. type packagesMem map[string][]byte func (p packagesMem) Keys(context.Context) ([]string, error) { if p == nil { return nil, errors.New("not readable") } return slices.Collect(maps.Keys(p)), nil } func (p packagesMem) Get(_ context.Context, key string) ([]byte, bool, error) { v, ok := p[key] return v, ok, nil } func (p packagesMem) failing() packagesMem { return nil } func known(seat, tags, reqs []string) Floor { return Floor{Seat: seat, Tags: tags, Requirements: reqs} } // TestTheBuildersVersions holds design 51 *The versions*, *The floor* and *Tagged versions* (DLV-B1, DLV-B2, DLV-B3): // one table per rule, the unit cases of the builder's numbering. func TestTheBuildersVersions(t *testing.T) { t.Run("keys", func(t *testing.T) { names := []string{"@novox/mesh-sdk", "git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/mesh-sdk", "a=40b", "a@b"} seen := map[string]string{} for _, n := range names { k := Key(n, "0.1.14") if strings.Count(k, ".") != 1 { t.Errorf("Key(%q) = %q, want two tokens", n, k) } if o, dup := seen[k]; dup { t.Errorf("%q and %q escape alike: %q", n, o, k) } seen[k] = n if !strings.HasPrefix(k, strings.TrimSuffix(NameFilter(n), "*")) { t.Errorf("Key(%q) = %q is not under NameFilter %q", n, k, NameFilter(n)) } } }) t.Run("floor", func(t *testing.T) { for _, c := range []struct { name string floor Floor want string }{ {"none starts at 0", known(nil, nil, nil), "0.1.0"}, {"the Go tag of today gives v0.1.14", known(nil, []string{"go/v0.1.13"}, []string{"v0.1.14-0.20261103221822-85ba0a9a70d8"}), "0.1.14"}, {"npm's 0.1.17 on the seat gives 0.1.18", known([]string{"0.1.17", "0.1.3"}, nil, nil), "0.1.18"}, {"another series and a tagged version count nothing", known([]string{"0.2.40", "0.1.9-feat-x.1"}, nil, nil), "0.1.0"}, {"the highest of the three", known([]string{"0.1.4"}, []string{"v0.1.9"}, []string{"v0.1.7"}), "0.1.10"}, } { if got := Next(nil, "0.1", c.floor); got != c.want { t.Errorf("%s: Next = %s, want %s", c.name, got, c.want) } } if got := Next([]Entry{{Version: "0.1.20", Series: "0.1"}, {Version: "0.1.21-x.1", Series: "0.1", Tag: "x"}}, "0.1", known([]string{"0.1.3"}, nil, nil)); got != "0.1.21" { t.Errorf("Next above the state's own = %s, want 0.1.21", got) } }) t.Run("decide", func(t *testing.T) { pub := Entry{Name: "n", Kind: KindPackage, Series: "0.1", Version: "0.1.5", Revision: "r1", Fingerprint: "fA", State: Published} ref := pub ref.State, ref.Version, ref.Revision = Refused, "0.1.6", "r2" img := Entry{Name: "i", Kind: KindBundle, Series: "0.1", Version: "0.1.2", Fingerprint: "fB", State: Published} silent := Floor{Silent: []string{SourceSeat}} for _, c := range []struct { name string entries []Entry kind, fp, rev string floor Floor version string keep, publish, unnum bool wait bool }{ {"a package's revision held published keeps its version and publishes nothing", []Entry{pub}, KindPackage, "fX", "r1", silent, "0.1.5", true, false, false, false}, {"a revision numbered but refused is published by its second build under the same version", []Entry{pub, ref}, KindPackage, "fY", "r2", known(nil, nil, nil), "0.1.6", true, true, false, false}, {"a new revision takes the next", []Entry{pub, ref}, KindPackage, "fZ", "r3", known(nil, nil, nil), "0.1.7", false, true, false, false}, {"a bundle's fingerprint keeps its number", []Entry{img}, KindBundle, "fB", "", silent, "0.1.2", true, false, false, false}, {"an unknown floor builds a bundle unnumbered", nil, KindBundle, "fC", "", silent, "", false, true, true, false}, {"an unknown floor builds an image unnumbered", nil, KindImage, "fC", "", silent, "", false, true, true, false}, {"an unknown floor makes a package wait", nil, KindPackage, "fC", "r9", silent, "", false, false, false, true}, } { d := Decide(c.entries, c.kind, "0.1", c.fp, c.rev, c.floor) if d.Version != c.version || d.Keep != c.keep || d.Publish != c.publish || d.Unnumbered != c.unnum || d.Wait != c.wait || d.Why == "" { t.Errorf("%s: Decide = %+v", c.name, d) } } }) t.Run("two claims never one number", func(t *testing.T) { st := newMem() v := Versions{Store: st, Now: func() time.Time { return time.Unix(0, 0) }} ctx := context.Background() st.before = func() { // a second claim takes 0.1.0 between the first's read and its create if _, _, err := (Versions{Store: st}).Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f2"}); err != nil { t.Error(err) } } d, e, err := v.Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f1"}) if err != nil || d.Version != "0.1.1" || e.State != Numbered || e.Series != DefaultSeries { t.Fatalf("Number = %+v, %+v, %v, want 0.1.1 after the other claim's 0.1.0", d, e, err) } if d, _, _ := v.Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f1"}); !d.Keep || d.Version != "0.1.1" { t.Errorf("a numbered fingerprint claimed again = %+v, want it kept", d) } got, err := v.Mark(ctx, "n", "0.1.1", func(e *Entry) { e.State = Published }) if err != nil || got.State != Published { t.Errorf("Mark = %+v, %v", got, err) } if _, err := v.Mark(ctx, "n", "0.9.9", func(*Entry) {}); err == nil { t.Error("marking a version never numbered is no error") } }) t.Run("recorded", func(t *testing.T) { st := newMem() v := Versions{Store: st} ctx := context.Background() if _, _, err := v.Number(ctx, Claim{Name: "n", Kind: KindImage, Fingerprint: "f", Digest: "sha256:aa", Floor: known(nil, nil, nil)}); err != nil { t.Fatal(err) } for _, c := range []struct { version, digest string ok bool }{{"0.1.0", "sha256:aa", true}, {"0.1.0", "sha256:bb", false}, {"0.1.7", "sha256:aa", false}} { if _, ok, why, err := v.Recorded(ctx, "n", c.version, c.digest); err != nil || ok != c.ok || (!ok && why == "") { t.Errorf("Recorded(%s, %s) = %v, %q, %v; want %v", c.version, c.digest, ok, why, err, c.ok) } } // builder_versions answers the same through the tools bundle's read-only view. r := Reader{Keys: func() ([]string, error) { var all []string for k := range st.vals { all = append(all, k) } return all, nil }, Get: func(k string) (json.RawMessage, bool, error) { v, _, ok, err := st.Get(ctx, k) return v, ok, err }} a, err := AnswerVersions(ctx, r, "n", "0.1.0", "sha256:bb") if err != nil || len(a.Versions) != 1 || a.Recorded == nil || *a.Recorded || a.Why == "" { t.Errorf("builder_versions = %+v, %v; want one version and not recorded for another digest", a, err) } if _, err := AnswerVersions(ctx, r, " ", "", ""); err == nil { t.Error("builder_versions with no name answered") } }) t.Run("tagged", func(t *testing.T) { es := []Entry{{Version: "0.1.14", Series: "0.1"}, {Version: "0.1.15-feat-x.1", Series: "0.1", Tag: "feat-x"}} if got := Tagged(es, "0.1", "feat/X", known(nil, nil, nil)); got != "0.1.15-feat-x.2" { t.Errorf("Tagged = %s, want 0.1.15-feat-x.2", got) } if got := Tagged(nil, "0.1", "feat-x", known(nil, nil, nil)); got != "0.1.0-feat-x.1" { t.Errorf("a package new in a group = %s, want 0.1.0-feat-x.1", got) } if got := CleanTag("--Fix/รค_1965--"); got != "fix-1965" { t.Errorf("CleanTag = %s", got) } }) t.Run("the floor wakes on its silent source, never a timer", func(t *testing.T) { ctx := context.Background() src := FloorSources{ Seat: func(context.Context, string, string) ([]string, error) { return []string{"0.1.3"}, nil }, Tags: func(context.Context, string, string) ([]string, error) { return nil, errors.New("no answer") }, Requirements: func(context.Context, string) ([]string, error) { return nil, nil }, } f, why := src.Ask(ctx, FloorOf{Seat: "oci-artifact-registry", Name: "m/program"}) if f.Known() || !reflect.DeepEqual(f.Silent, []string{SourceTags}) || why[SourceTags] != "no answer" { t.Fatalf("Ask = %+v, %v; want bridge_tags silent", f, why) } var fw FloorWaits w := fw.Wait("m/program", "oci-artifact-registry", "distribution", f, why, time.Unix(0, 0)) if !strings.Contains(w.Finding(), "bridge_tags (no answer)") { t.Errorf("finding = %q", w.Finding()) } for _, c := range []struct { kind, seat, module, name string want []string }{ {WakePublished, "oci-artifact-registry", "", "", nil}, // the seat answered already: a publish wakes nothing {WakeAccepted, "", "", "", nil}, {WakeForge, "", "", "", []string{"m/program"}}, {WakeCleared, "", "mesh-delivery-forge-bridge", "", []string{"m/program"}}, // recovered with nothing to say {WakeCleared, "", "mesh-delivery-indexer", "", nil}, {WakeByHand, "", "", "m/program", []string{"m/program"}}, {WakeByHand, "", "", "other", nil}, } { if got := fw.Wake(c.kind, c.seat, c.module, c.name); !reflect.DeepEqual(got, c.want) { t.Errorf("Wake(%s %s %s %s) = %v, want %v", c.kind, c.seat, c.module, c.name, got, c.want) } } seatDown := Floor{Silent: []string{SourceSeat}} fw.Wait("distribution/program", "container-image-registry", "distribution", seatDown, nil, time.Unix(0, 0)) if got := fw.Wake(WakePublished, "container-image-registry", "", ""); !reflect.DeepEqual(got, []string{"distribution/program"}) { t.Errorf("the holder's published woke %v, want its own image's name", got) } if got := fw.Wake(WakeCleared, "", "distribution", ""); !reflect.DeepEqual(got, []string{"distribution/program"}) { t.Errorf("the holder's clear woke %v", got) } if !fw.Answered("m/program") || fw.Answered("m/program") || len(fw.Waiting()) != 1 { t.Error("Answered drops the wait once") } // DLV-B4: the waits are kept in floor-waits; a restarted builder takes them in, and its sources' events still wake them. kept := keeperMem{} restarted := &FloorWaits{} restarted.Keep(kept, []FloorWait{{Name: "m/held", Seat: SeatImages, Silent: []string{SourceTags}, Since: time.Unix(5, 0)}}) if got := restarted.Wake(WakeForge, "", "", ""); !reflect.DeepEqual(got, []string{"m/held"}) { t.Errorf("a wait held in floor-waits woke %v after a restart, want m/held", got) } restarted.Wait("m/new", SeatNPM, "", Floor{Silent: []string{SourceSeat}}, nil, time.Unix(9, 0)) ans, err := AnswerFloorWaits(Reader{Keys: kept.keys, Get: kept.get}) if err != nil || len(ans.Waits) != 2 || ans.Waits[0].Name != "m/held" || ans.Waits[1].Name != "m/new" || !strings.Contains(ans.Waits[1].Finding, "seat did not answer") { t.Fatalf("builder_floor_waits = %+v, %v", ans, err) } restarted.Answered("m/held") if _, ok := kept[FloorWaitKey("m/held")]; ok { t.Error("an answered floor stays in floor-waits") } // The lead's ruling (review round 1): builder_bounds lists each key of the state bounds with what it bounds and // when it ends. at := time.Date(2026, 11, 4, 17, 0, 0, 0, time.UTC) bounds := keeperMem{PublishBoundKey("m/program", "0.1.4"): EncodeBound("the publish of m/program@0.1.4 to oci-artifact-registry", at.Add(PublishBound)), OldFormBoundKey("g14"): EncodeBound("the seven days g14 holds the old form's window", at.Add(FindingAfter))} rows, err := AnswerBounds(Reader{Keys: bounds.keys, Get: bounds.get}, at) if err != nil || len(rows) != 2 || rows[0].Key != OldFormBoundKey("g14") || rows[1].Left != "10m0s" || !strings.Contains(rows[1].Bounds, "m/program@0.1.4") || !rows[0].Ends.Equal(at.Add(FindingAfter)) { t.Fatalf("builder_bounds = %+v, %v", rows, err) } }) t.Run("old form", func(t *testing.T) { // One kind, a first form and a newer one. fs := forms.ArtifactForms{{Name: "registry", First: true, Kinds: []string{"image"}}, {Name: "oci", Kinds: []string{"image"}}} both := []string{forms.FormNeed("oci")} for _, c := range []struct { name string counted map[string][]string dropped map[string]bool want []string }{ {"one node lacking the new form holds the old", map[string][]string{"a": both, "b": nil}, nil, []string{"b"}}, {"an away node counts, stating its last", map[string][]string{"a": both, "c": {"bus-state"}}, nil, []string{"c"}}, {"every counted node reads the newest: closed", map[string][]string{"a": both, "c": both}, nil, nil}, {"a retired node dropped by hand counts no more", map[string][]string{"a": both, "c": nil}, map[string]bool{"c": true}, nil}, {"the builder's own forms: no window", nil, nil, nil}, } { if got := Holding(fs, c.counted, c.dropped); !reflect.DeepEqual(got, c.want) { t.Errorf("%s: Holding = %v, want %v", c.name, got, c.want) } } // The builder's own forms: the registry seat's form of a bundle is not a first form, so a node stating nothing // holds the window and one stating form:seat-reference does not. if got := Holding(forms.Current, map[string][]string{"a": nil, "b": {forms.FormNeed(forms.FormSeatReference)}}, nil); !reflect.DeepEqual(got, []string{"a"}) { t.Errorf("forms.Current, a node stating nothing and one stating form:seat-reference: Holding = %v, want a", got) } t0 := time.Unix(0, 0) named := []string{"c", "d"} w, closed, overdue := OldFormWindow{}.Step(named, []string{"c"}, t0) if closed || len(overdue) != 0 || !w.Open { t.Fatalf("Step = %+v %v %v", w, closed, overdue) } w, _, overdue = w.Step(named, []string{"c"}, t0.Add(FindingAfter)) if !reflect.DeepEqual(overdue, []string{"c"}) { t.Errorf("seven days held: overdue %v, want c", overdue) } w, err := w.Drop("d", "op@shanks", "retired", t0) if err != nil || !w.DroppedSet()["d"] { t.Fatalf("Drop = %+v, %v", w, err) } if again, _ := w.Drop("d", "op@shanks", "again", t0); len(again.Dropped) != 1 { t.Errorf("a node dropped twice is kept twice: %+v", again.Dropped) } if _, err := w.Drop("e", "op@shanks", "", t0); err == nil { t.Error("a drop with no why is taken") } // Security-upgrade F3 (review round 1), by mesh-sdk's delivery.StepWindow: a read naming no node (an empty // node_haves, the placement not read yet) never closes the window, and its holder's seven days still count. if held, shut, late := w.Step(nil, nil, t0.Add(FindingAfter+time.Minute)); shut || !held.Open || !reflect.DeepEqual(late, []string{"c"}) { t.Fatalf("a read naming no node: %+v closed=%v overdue %v, want the window open and c overdue", held, shut, late) } w, closed, _ = w.Step(named, nil, t0.Add(FindingAfter+time.Hour)) if !closed || w.Open || !reflect.DeepEqual(w.LastHolding, []string{"c"}) || w.Closing().Dropped[0].Node != "d" { t.Errorf("closing: %+v closed=%v", w, closed) } raw, _ := json.Marshal(w) if back, err := DecodeOldFormWindow(raw); err != nil || !back.Closed.Equal(w.Closed) { t.Errorf("the window read back = %+v, %v", back, err) } if _, again, _ := w.Step(named, nil, t0.Add(FindingAfter+2*time.Hour)); again { t.Error("a closed window closes again: old-form-closed would be said twice") } }) t.Run("at the claim", func(t *testing.T) { ctx := context.Background() tagsUp := false var asked []string src := FloorSources{ Seat: func(context.Context, string, string) ([]string, error) { return []string{"0.1.3"}, nil }, Tags: func(_ context.Context, repo, prefix string) ([]string, error) { asked = append(asked, repo+" "+prefix) if !tagsUp { return nil, errors.New("no answer") } return []string{"go/v0.1.13"}, nil }, Requirements: func(context.Context, string) ([]string, error) { return nil, nil }, } var said []delivery.VersionNumbered fw := &FloorWaits{} n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Waits: fw, Said: func(b delivery.VersionNumbered) { said = append(said, b) }} c := BuildClaim{Module: "m", Path: "go", Repository: "novox/m", Fingerprint: "fp1", Commit: "c1", Revision: "7", Merge: 7, Artifacts: []ClaimArtifact{{Artifact: "program", Kind: KindBundle, Language: "go"}, {Artifact: "base", Kind: "upstream"}}} got, err := n.Number(ctx, c) if err != nil || len(got.Versions) != 0 || !reflect.DeepEqual(got.Unnumbered, []string{"program"}) || got.Wait != "" { t.Fatalf("silent floor: %+v, %v; want program unnumbered, the upstream copy left out", got, err) } if !reflect.DeepEqual(asked, []string{"novox/m go/v"}) || len(fw.Waiting()) != 1 || len(said) != 0 { t.Fatalf("asked %v, waits %+v, said %v", asked, fw.Waiting(), said) } pkg := c pkg.Artifacts = []ClaimArtifact{{Artifact: "lib", Kind: KindPackage, Language: "go"}} if got, _ := n.Number(ctx, pkg); got.Wait == "" || len(got.Versions) != 0 { t.Fatalf("a package with no floor = %+v; want it to wait", got) } tagsUp = true got, err = n.Number(ctx, c) if err != nil || got.Versions["program"] != "0.1.14" || len(said) != 1 || said[0].Version != "0.1.14" || len(fw.Waiting()) != 1 { t.Fatalf("floor answered: %+v, %v, said %v, waits %+v; want 0.1.14 said once, lib still waiting", got, err, said, fw.Waiting()) } // DLV-B6: a check's build of a head a tag ask names numbers only its packages, tagged below the next version, kept // with its repository for the branch's end, said with the head; claimed again it keeps its number. tagged := c tagged.Tag, tagged.Head, tagged.Revision, tagged.Merge = "feat-x", "h1", "", 0 tagged.Artifacts = append(tagged.Artifacts, ClaimArtifact{Artifact: "tlib", Kind: KindPackage, Language: "go"}) saidBefore, askedBefore := said, asked said = nil got, err = n.Number(ctx, tagged) if err != nil || len(got.Versions) != 1 || !strings.Contains(got.Versions["tlib"], "-feat-x.1") || len(said) != 1 || said[0].Head != "h1" || said[0].Tag != "feat-x" { t.Fatalf("tagged claim: %+v, %v, said %+v; want tlib alone tagged, said with its head", got, err, said) } if es, _ := n.Versions.Of(ctx, "m/tlib"); len(es) != 1 || es[0].Repository != "novox/m" { t.Errorf("the tagged version kept = %+v, want its repository", es) } if again, _ := n.Number(ctx, tagged); again.Versions["tlib"] != got.Versions["tlib"] || len(said) != 1 { t.Errorf("claimed again: %+v, said %d; want the same number, said once", again, len(said)) } said, asked = saidBefore, askedBefore got, _ = n.Number(ctx, c) var publishing []string // review round 1: a version built and still numbered arms its publish bound n.Publishing = func(_ context.Context, e Entry) { publishing = append(publishing, e.Name+"@"+e.Version) } if err := n.Built(ctx, c, got, map[string]string{"program": "sha256:aa"}); err != nil { t.Fatal(err) } if n.Publishing = nil; !reflect.DeepEqual(publishing, []string{"m/program@0.1.14"}) { t.Fatalf("the publish bounds armed: %v, want m/program@0.1.14", publishing) } again, _ := n.Number(ctx, c) if again.Versions["program"] != "0.1.14" || len(said) != 1 || len(asked) != 3 { t.Fatalf("again: %+v, said %d, asked %v; want the number kept, nothing said, the floor not asked", again, len(said), asked) } if _, ok, why, _ := n.Versions.Recorded(ctx, "m/program", "0.1.14", "sha256:bb"); ok { t.Fatalf("another digest recorded: %s", why) } // DLV-B4: the seat's events move the entry; a published wakes the names waiting on that seat. if wake, err := n.Heard(ctx, SeatArtifacts, HeardPublished, "e1", Outcome{Name: "m/other", Digest: "sha256:cc"}); err != nil || len(wake) != 0 { t.Fatalf("a by-digest publish of another seat's wait: %v, %v", wake, err) } n.Waits.Wait("m/lib", SeatGoPackages, "", Floor{Silent: []string{SourceSeat}}, nil, time.Unix(0, 0)) if wake, _ := n.Heard(ctx, SeatGoPackages, HeardPublished, "e2", Outcome{Name: "m/x", Digest: "sha256:dd"}); !reflect.DeepEqual(wake, []string{"m/lib"}) { t.Fatalf("a published on the Go seat woke %v, want m/lib, whose seat was silent", wake) } for _, h := range []struct{ event, state, reason string }{{HeardRefused, Refused, "bytes differ"}, {HeardPublished, Published, ""}, {HeardRefused, Published, ""}, {HeardRemoved, Removed, "retention"}} { if _, err := n.Heard(ctx, SeatArtifacts, h.event, "e3", Outcome{Name: "m/program", Version: "0.1.14", Digest: "sha256:aa", Reason: h.reason}); err != nil { t.Fatal(err) } all, _ := n.Versions.Of(ctx, "m/program") if all[0].State != h.state { t.Fatalf("after %s: %+v, want %s", h.event, all[0], h.state) } } if _, err := n.Heard(ctx, SeatArtifacts, HeardPublished, "e4", Outcome{Name: "m/none", Version: "0.1.0"}); err == nil { t.Fatal("a published of a version the builder never numbered is not said") } }) t.Run("a package is numbered under its name in its ecosystem", func(t *testing.T) { // design 32: the indexer's packages state names, in each series key, the package's name and the / // 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 var kept map[string]bool // review round 2: the names whose seat keeps them whole (a shared digest) acts := Acts{Numberer: &Numberer{Versions: Versions{Store: st}}, Remove: func(_ context.Context, seat string, e Entry) (RemoveOutcome, error) { removed = append(removed, e.Name+"@"+e.Version) if kept[e.Name] { return RemoveOutcome{Kept: true, Reason: "shared-digest", Tags: []string{"0.1.2"}}, nil } if seat == SeatNPM { return RemoveOutcome{}, errors.New("down") } return RemoveOutcome{}, 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) } // Review round 2: a remove its seat answers kept (another tag names the digest) ends there, with no retry: the // version is recorded kept on the store with the reason and the other tags, which builder_versions shows. raw, _ = Encode(Entry{Name: "n4", Kind: KindPackage, Series: "0.1", Version: "0.1.2-feat-x.1", Tag: "feat-x", Repository: "o/r", Seat: SeatNPM, State: Numbered}) _, _ = st.Create(context.Background(), Key("n4", "0.1.2-feat-x.1"), raw) kept, removed = map[string]bool{"n4": true}, nil if end, err := acts.BranchEnded(context.Background(), "o/r", "feat/x"); err != nil || !end.Done || len(removed) != 1 { t.Fatalf("a kept remove: %+v %v, removed %v; want done at once", end, err, removed) } if es, _ := acts.Numberer.Versions.Of(context.Background(), "n4"); len(es) != 1 || es[0].State != Kept || es[0].Reason != "shared-digest" || !reflect.DeepEqual(es[0].KeptWith, []string{"0.1.2"}) { t.Errorf("the kept version = %+v, want kept, shared-digest, with 0.1.2", es) } if removed = nil; func() bool { end, _ := acts.BranchEnded(context.Background(), "o/r", "feat/x"); return !end.Done || len(removed) != 0 }() { t.Errorf("a kept version is asked again: removed %v", removed) } if _, err := acts.Numberer.Heard(context.Background(), SeatNPM, HeardKept, "k", Outcome{Name: "n", Version: "0.1.2-feat-x.1", Tags: []string{"0.1.2"}}); err != nil { t.Fatal(err) } if es, _ := acts.Numberer.Versions.Of(context.Background(), "n"); slices.ContainsFunc(es, func(e Entry) bool { return e.Version == "0.1.2-feat-x.1" && e.State == Kept }) { t.Error("a seat's kept turned a removed version back to kept") } // 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) } }) }