Files
mesh-delivery/modules/mesh-delivery-builder/internal/versions/versions_test.go
T
jschoubben 64b918c64c DLV-B1, DLV-B2: the builder's state versions, numbering under compare-and-set per fingerprint or source revision, and builder_versions
The builder keeps one key per name, tag and version (design 51 *The versions*, ADR 0606), in its old state
section (ADR 0619), history one, 64 MiB with the 80 % mark. A package is numbered per source revision and keeps a
version held published, publishing nothing; one numbered but never stored is published again under it. An image,
bundle or archive keeps its fingerprint's number. An unknown floor builds an image, bundle or archive unnumbered and
makes a package wait (ADR 0617 §1). Merge order and tagged versions below the next version are the package's
helpers. builder_versions answers a name's versions and the holder's question (ADR 0605 §4).

Four claim tests merged into two, so the new table test adds no function (test budget).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:39:17 +01:00

237 lines
9.2 KiB
Go

package versions
import (
"context"
"encoding/json"
"strings"
"sync"
"testing"
"time"
)
// 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 strings.HasPrefix(k, prefix) && !strings.Contains(k[len(prefix):], ".") {
out = append(out, k)
}
}
return out, nil
}
func known(seat, tags, reqs []string) Floor { return Floor{Seat: seat, Tags: tags, Requirements: reqs} }
// TestTheBuildersVersions holds design 51 *The versions*, *The floor* and *Tagged versions* (DLV-B1, DLV-B2, DLV-B3):
// one table per rule, the unit cases of the builder's numbering.
func TestTheBuildersVersions(t *testing.T) {
t.Run("keys", func(t *testing.T) {
names := []string{"@novox/mesh-sdk", "git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/mesh-sdk", "a=40b", "a@b"}
seen := map[string]string{}
for _, n := range names {
k := Key(n, "0.1.14")
if strings.Count(k, ".") != 1 {
t.Errorf("Key(%q) = %q, want two tokens", n, k)
}
if o, dup := seen[k]; dup {
t.Errorf("%q and %q escape alike: %q", n, o, k)
}
seen[k] = n
if !strings.HasPrefix(k, strings.TrimSuffix(NameFilter(n), "*")) {
t.Errorf("Key(%q) = %q is not under NameFilter %q", n, k, NameFilter(n))
}
}
})
t.Run("floor", func(t *testing.T) {
for _, c := range []struct {
name string
floor Floor
want string
}{
{"none starts at 0", known(nil, nil, nil), "0.1.0"},
{"the Go tag of today gives v0.1.14", known(nil, []string{"go/v0.1.13"}, []string{"v0.1.14-0.20261103221822-85ba0a9a70d8"}), "0.1.14"},
{"npm's 0.1.17 on the seat gives 0.1.18", known([]string{"0.1.17", "0.1.3"}, nil, nil), "0.1.18"},
{"another series and a tagged version count nothing", known([]string{"0.2.40", "0.1.9-feat-x.1"}, nil, nil), "0.1.0"},
{"the highest of the three", known([]string{"0.1.4"}, []string{"v0.1.9"}, []string{"v0.1.7"}), "0.1.10"},
} {
if got := Next(nil, "0.1", c.floor); got != c.want {
t.Errorf("%s: Next = %s, want %s", c.name, got, c.want)
}
}
if got := Next([]Entry{{Version: "0.1.20", Series: "0.1"}, {Version: "0.1.21-x.1", Series: "0.1", Tag: "x"}}, "0.1", known([]string{"0.1.3"}, nil, nil)); got != "0.1.21" {
t.Errorf("Next above the state's own = %s, want 0.1.21", got)
}
})
t.Run("decide", func(t *testing.T) {
pub := Entry{Name: "n", Kind: KindPackage, Series: "0.1", Version: "0.1.5", Revision: "r1", Fingerprint: "fA", State: Published}
ref := pub
ref.State, ref.Version, ref.Revision = Refused, "0.1.6", "r2"
img := Entry{Name: "i", Kind: KindBundle, Series: "0.1", Version: "0.1.2", Fingerprint: "fB", State: Published}
silent := Floor{Silent: []string{SourceSeat}}
for _, c := range []struct {
name string
entries []Entry
kind, fp, rev string
floor Floor
version string
keep, publish, unnum bool
wait bool
}{
{"a package's revision held published keeps its version and publishes nothing", []Entry{pub}, KindPackage, "fX", "r1", silent, "0.1.5", true, false, false, false},
{"a revision numbered but refused is published by its second build under the same version", []Entry{pub, ref}, KindPackage, "fY", "r2", known(nil, nil, nil), "0.1.6", true, true, false, false},
{"a new revision takes the next", []Entry{pub, ref}, KindPackage, "fZ", "r3", known(nil, nil, nil), "0.1.7", false, true, false, false},
{"a bundle's fingerprint keeps its number", []Entry{img}, KindBundle, "fB", "", silent, "0.1.2", true, false, false, false},
{"an unknown floor builds a bundle unnumbered", nil, KindBundle, "fC", "", silent, "", false, true, true, false},
{"an unknown floor builds an image unnumbered", nil, KindImage, "fC", "", silent, "", false, true, true, false},
{"an unknown floor makes a package wait", nil, KindPackage, "fC", "r9", silent, "", false, false, false, true},
} {
d := Decide(c.entries, c.kind, "0.1", c.fp, c.rev, c.floor)
if d.Version != c.version || d.Keep != c.keep || d.Publish != c.publish || d.Unnumbered != c.unnum || d.Wait != c.wait || d.Why == "" {
t.Errorf("%s: Decide = %+v", c.name, d)
}
}
})
t.Run("two claims never one number", func(t *testing.T) {
st := newMem()
v := Versions{Store: st, Now: func() time.Time { return time.Unix(0, 0) }}
ctx := context.Background()
st.before = func() { // a second claim takes 0.1.0 between the first's read and its create
if _, _, err := (Versions{Store: st}).Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f2"}); err != nil {
t.Error(err)
}
}
d, e, err := v.Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f1"})
if err != nil || d.Version != "0.1.1" || e.State != Numbered || e.Series != DefaultSeries {
t.Fatalf("Number = %+v, %+v, %v, want 0.1.1 after the other claim's 0.1.0", d, e, err)
}
if d, _, _ := v.Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f1"}); !d.Keep || d.Version != "0.1.1" {
t.Errorf("a numbered fingerprint claimed again = %+v, want it kept", d)
}
got, err := v.Mark(ctx, "n", "0.1.1", func(e *Entry) { e.State = Published })
if err != nil || got.State != Published {
t.Errorf("Mark = %+v, %v", got, err)
}
if _, err := v.Mark(ctx, "n", "0.9.9", func(*Entry) {}); err == nil {
t.Error("marking a version never numbered is no error")
}
})
t.Run("recorded", func(t *testing.T) {
st := newMem()
v := Versions{Store: st}
ctx := context.Background()
if _, _, err := v.Number(ctx, Claim{Name: "n", Kind: KindImage, Fingerprint: "f", Digest: "sha256:aa", Floor: known(nil, nil, nil)}); err != nil {
t.Fatal(err)
}
for _, c := range []struct {
version, digest string
ok bool
}{{"0.1.0", "sha256:aa", true}, {"0.1.0", "sha256:bb", false}, {"0.1.7", "sha256:aa", false}} {
if _, ok, why, err := v.Recorded(ctx, "n", c.version, c.digest); err != nil || ok != c.ok || (!ok && why == "") {
t.Errorf("Recorded(%s, %s) = %v, %q, %v; want %v", c.version, c.digest, ok, why, err, c.ok)
}
}
// builder_versions answers the same through the tools bundle's read-only view.
r := Reader{Keys: func() ([]string, error) {
var all []string
for k := range st.vals {
all = append(all, k)
}
return all, nil
}, Get: func(k string) (json.RawMessage, bool, error) {
v, _, ok, err := st.Get(ctx, k)
return v, ok, err
}}
a, err := AnswerVersions(ctx, r, "n", "0.1.0", "sha256:bb")
if err != nil || len(a.Versions) != 1 || a.Recorded == nil || *a.Recorded || a.Why == "" {
t.Errorf("builder_versions = %+v, %v; want one version and not recorded for another digest", a, err)
}
if _, err := AnswerVersions(ctx, r, " ", "", ""); err == nil {
t.Error("builder_versions with no name answered")
}
})
t.Run("tagged", func(t *testing.T) {
es := []Entry{{Version: "0.1.14", Series: "0.1"}, {Version: "0.1.15-feat-x.1", Series: "0.1", Tag: "feat-x"}}
if got := Tagged(es, "0.1", "feat/X", known(nil, nil, nil)); got != "0.1.15-feat-x.2" {
t.Errorf("Tagged = %s, want 0.1.15-feat-x.2", got)
}
if got := Tagged(nil, "0.1", "feat-x", known(nil, nil, nil)); got != "0.1.0-feat-x.1" {
t.Errorf("a package new in a group = %s, want 0.1.0-feat-x.1", got)
}
if got := CleanTag("--Fix/ä_1965--"); got != "fix-1965" {
t.Errorf("CleanTag = %s", got)
}
})
t.Run("merge order", func(t *testing.T) {
es := []Entry{{Fingerprint: "f1", Version: "0.1.0", Series: "0.1"}}
for _, c := range []struct {
name string
merge uint64
held map[string]uint64
want uint64
}{
{"an earlier merge numbered waits on nothing", 5, map[string]uint64{"f1": 3}, 0},
{"an earlier merge not numbered is waited on", 5, map[string]uint64{"f1": 3, "f2": 4}, 4},
{"a replaced fingerprint the indexer no longer holds is skipped", 5, map[string]uint64{}, 0},
{"a later merge is not waited on", 5, map[string]uint64{"f3": 7}, 0},
} {
if got := WaitsOn(es, c.merge, c.held); got != c.want {
t.Errorf("%s: WaitsOn = %d, want %d", c.name, got, c.want)
}
}
})
t.Run("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")
}
})
}