package versions import ( "context" "encoding/json" "errors" "maps" "reflect" "slices" "strings" "sync" "testing" "time" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/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() down error // answered by KeysOf: a state that cannot be read } 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() if m.down != nil { return nil, m.down } prefix := strings.TrimSuffix(filter, "*") var out []string for k := range m.vals { if filter == AllFilter { out = append(out, k) continue } if strings.Count(filter, ".") == 2 { // a three-token filter, `a.*.*` as the bus reads it ft, kt := strings.Split(filter, "."), strings.Split(k, ".") if len(kt) == 3 && kt[0] == ft[0] { 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("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 git.tags silent", f, why) } holders := map[string]string{SourceSeats[SourceTags]: "mesh-delivery-forge-bridge", SourceSeats[SourceRequirements]: "mesh-delivery-indexer"} fw := FloorWaits{Holder: func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok }} w := fw.Wait("m/program", "oci-artifact-registry", "distribution", f, why, time.Unix(0, 0)) if !strings.Contains(w.Finding(), "git.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}, {WakeRevision, "", "", "", []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(WakeRevision, "", "", ""); !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 window.StepWindow: a read naming no node (an empty // node_haves, the assignments 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 }, } fw := &FloorWaits{} n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Waits: fw} 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 { t.Fatalf("asked %v, waits %+v", asked, fw.Waiting()) } 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(fw.Waiting()) != 1 { t.Fatalf("floor answered: %+v, %v, waits %+v; want 0.1.14, lib still waiting", got, err, fw.Waiting()) } 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(asked) != 3 { t.Fatalf("again: %+v, asked %v; want the number kept, the floor not asked", again, 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") } // D-B5 (ADR 0666 §15, design 51 *The versions* line 287): a package's build whose published versions cannot be read // is refused, naming the state; a bundle's is built as before (ADR 0617 §1), the error said. st := n.Versions.Store.(*mem) st.down = errors.New("nats: no responders available for request") if got, err := n.Number(ctx, pkg); err != nil || !strings.Contains(got.Refused, "state versions (bucket mesh-delivery-builder_versions) cannot be read") || len(got.Versions) != 0 { t.Errorf("a package with its versions unread = %+v, %v; want refused naming the state", got, err) } if got, err := n.Number(ctx, c); err == nil || got.Refused != "" { t.Errorf("a bundle with its versions unread = %+v, %v; want the error, no refusal", got, err) } st.down = nil }) 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 }, } n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Names: NamesIn(state)} 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) } 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) } // 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; 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) } } 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 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("fresh claims and the floor", func(t *testing.T) { 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) } _, _ = v.Mark(ctx, "m/p", "0.1.4", func(e *Entry) { e.State = Published }) acts := Acts{Numberer: &Numberer{Versions: v, Waits: &FloorWaits{}}, Reconsider: func(context.Context) {}} 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) } }) }