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
237 lines
9.2 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|