338 lines
15 KiB
Go
338 lines
15 KiB
Go
package index
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"path"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/process"
|
|
)
|
|
|
|
const merge = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
|
|
// record is a build record as the controller writes it (mesh-sdk go/delivery/testdata/build-record-build-agent.json is
|
|
// the captured shape): SourceInputs under its Go field names, no json keys. inputs is that object's JSON.
|
|
func record(t *testing.T, id, module, repo, path, ref, state, at, inputs string) (string, []byte) {
|
|
t.Helper()
|
|
// an outcome has no state key (mesh-controller internal/chain/records.go:57-59); state asked is a request
|
|
raw := `{"id":"` + id + `",`
|
|
switch state {
|
|
case "":
|
|
case "failed": // a failure is an outcome with failed set and no state (mesh-controller internal/chain/records.go:109)
|
|
raw += `"failed":"the build failed",`
|
|
default:
|
|
raw += `"state":"` + state + `",`
|
|
}
|
|
raw += `"repository":"` + repo + `","ref":"` + ref + `","module":"` + module + `","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"` + path + `","at":"` + at + `"`
|
|
if inputs != "" {
|
|
raw += `,"source-fingerprint":"src1:ignored","source-inputs":` + inputs
|
|
}
|
|
raw += `}`
|
|
var check delivery.BuildRecord
|
|
if err := json.Unmarshal([]byte(raw), &check); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return delivery.BuildKey(module, id), []byte(raw)
|
|
}
|
|
|
|
func modulesIndexer(t *testing.T, recs ...[2]any) (*Indexer, *memStore) {
|
|
t.Helper()
|
|
mods := newMem()
|
|
ix := &Indexer{Store: newMem(), Modules: mods, Builds: NewBuilds()}
|
|
for _, r := range recs {
|
|
ix.Builds.Put(r[0].(string), r[1].([]byte))
|
|
}
|
|
return ix, mods
|
|
}
|
|
|
|
func rec(t *testing.T, id, module, repo, path, ref, state, at, inputs string) [2]any {
|
|
k, v := record(t, id, module, repo, path, ref, state, at, inputs)
|
|
return [2]any{k, v}
|
|
}
|
|
|
|
// mergedMsg is a merged message of the repository with these module directories' trees, each with a manifest naming its
|
|
// module, the directory's last element ("r" for the root), as the forge bridge sends it (one manifest per directory that
|
|
// holds a module.json).
|
|
func mergedMsg(repo string, trees map[string]string) delivery.Forge {
|
|
manifests := map[string]string{}
|
|
for dir := range trees {
|
|
manifests[dir] = `{"module":"` + nameOfDir(dir) + `","version":"1"}`
|
|
}
|
|
return delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: repo, Branch: "feat/x", Number: 12, Head: h1, Merge: merge, Root: "r0", Trees: trees, Manifests: manifests}
|
|
}
|
|
|
|
func nameOfDir(dir string) string {
|
|
if dir == "" {
|
|
return "r"
|
|
}
|
|
return path.Base(dir)
|
|
}
|
|
|
|
func moduleOf(t *testing.T, st *memStore, key string) (delivery.Module, bool) {
|
|
t.Helper()
|
|
v, ok := st.vals[key]
|
|
if !ok {
|
|
return delivery.Module{}, false
|
|
}
|
|
m, err := delivery.DecodeModule(v)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m, true
|
|
}
|
|
|
|
const aInputs = `{"Module":"a","Tree":"oldtree","Bases":["registry.example/base@sha256:aaa"],"Contexts":null,"Toolchains":{"code":"go sha256:ccc 0123456789abcdef"},"Unpinned":""}`
|
|
|
|
func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value writes nothing); the operator 2026-10-25 (a module's value moves only with the module)
|
|
const repo, next = "novox/mesh-delivery", "cccccccccccccccccccccccccccccccccccccccc"
|
|
three := map[string]string{"modules/a": "treeA", "modules/b": "treeB", "modules/c": "treeC"}
|
|
for _, tc := range []struct {
|
|
name string
|
|
then func(ix *Indexer) error // after the first merge of a, b and c at merge, stamped by source revision 7
|
|
// writes is the values written by then; commit is each module's commit after.
|
|
writes int
|
|
commit map[string]string
|
|
// taken is the merge the repository's taken key names after then (issue 1280, ADR 0556 clause 1).
|
|
taken string
|
|
}{
|
|
{name: "the same merge again", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: merge,
|
|
then: func(ix *Indexer) error { return ix.Apply(context.Background(), mergedMsg(repo, three)) }},
|
|
{name: "a merge touching one of three modules", writes: 1, commit: map[string]string{"a": next, "b": merge, "c": merge}, taken: next,
|
|
then: func(ix *Indexer) error {
|
|
m := mergedMsg(repo, map[string]string{"modules/a": "treeA2", "modules/b": "treeB", "modules/c": "treeC"})
|
|
m.Merge = next
|
|
return ix.Apply(context.Background(), m)
|
|
}},
|
|
{name: "a merge touching none of them is still taken in", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: next,
|
|
then: func(ix *Indexer) error {
|
|
m := mergedMsg(repo, three)
|
|
m.Merge = next
|
|
return ix.Apply(context.Background(), m)
|
|
}},
|
|
{name: "a merge while the taken state is not on the bus yet is still taken in", writes: 1, commit: map[string]string{"a": next, "b": merge, "c": merge}, taken: merge,
|
|
then: func(ix *Indexer) error { // issue 1790: the state is bound when it appears, so the indexer runs before it does
|
|
ix.Taken = &memStore{unbound: process.ErrNotYet}
|
|
m := mergedMsg(repo, map[string]string{"modules/a": "treeA2", "modules/b": "treeB", "modules/c": "treeC"})
|
|
m.Merge = next
|
|
return ix.Apply(context.Background(), m)
|
|
}},
|
|
{name: "a merge heard while the taken state was missing is filled when the state appears", writes: 2, commit: map[string]string{"a": next, "b": merge, "c": merge}, taken: next,
|
|
then: func(ix *Indexer) error { // issue 1280: the key was missing, the merge was taken in without it
|
|
on := ix.Taken.(*memStore)
|
|
key, _ := delivery.RepositoryKey(repo)
|
|
delete(on.vals, key)
|
|
ix.Taken = &memStore{unbound: process.ErrNotYet}
|
|
m := mergedMsg(repo, map[string]string{"modules/a": "treeA2", "modules/b": "treeB", "modules/c": "treeC"})
|
|
m.Merge = next
|
|
if err := ix.Apply(context.Background(), m); err != nil {
|
|
return err
|
|
}
|
|
if err := ix.ApplySource(context.Background(), 8, sourceOf(repo, next)); err != nil {
|
|
return err
|
|
}
|
|
ix.Taken = on
|
|
return ix.FillTaken(context.Background()) // no Drained: only the merge applied is written, not the source's word
|
|
}},
|
|
{name: "a source head whose merge was not applied is not a taken key", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: "",
|
|
then: func(ix *Indexer) error { // the forge stream still holds the merge: the consumer is not drained
|
|
delete(ix.Taken.(*memStore).vals, takenKey(repo))
|
|
ix.Drained = func(context.Context) (bool, error) { return false, nil }
|
|
if err := ix.ApplySource(context.Background(), 8, sourceOf(repo, next)); err != nil {
|
|
return err
|
|
}
|
|
return ix.FillTaken(context.Background())
|
|
}},
|
|
{name: "a source head of a drained forge stream fills a missing key", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: next,
|
|
then: func(ix *Indexer) error {
|
|
delete(ix.Taken.(*memStore).vals, takenKey(repo))
|
|
ix.Drained = func(context.Context) (bool, error) { return true, nil }
|
|
if err := ix.ApplySource(context.Background(), 8, sourceOf(repo, next)); err != nil {
|
|
return err
|
|
}
|
|
return ix.FillTaken(context.Background())
|
|
}},
|
|
{name: "filling from an older source head leaves a later key", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: next,
|
|
then: func(ix *Indexer) error {
|
|
ix.Contains = func(_ context.Context, _, commit, in string) (bool, error) { return commit == merge && in == next, nil }
|
|
m := mergedMsg(repo, three)
|
|
m.Merge = next
|
|
if err := ix.Apply(context.Background(), m); err != nil {
|
|
return err
|
|
}
|
|
ix.Drained = func(context.Context) (bool, error) { return true, nil }
|
|
return ix.FillTaken(context.Background()) // the source still names merge
|
|
}},
|
|
{name: "an earlier merge heard again after a later one leaves taken", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: merge,
|
|
then: func(ix *Indexer) error {
|
|
ix.Contains = func(_ context.Context, _, commit, in string) (bool, error) { return commit == next && in == merge, nil }
|
|
m := mergedMsg(repo, three)
|
|
m.Merge = next
|
|
return ix.Apply(context.Background(), m)
|
|
}},
|
|
{name: "a later source revision of another repository's merge", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: merge,
|
|
then: func(ix *Indexer) error {
|
|
return ix.ApplySource(context.Background(), 9, delivery.Source{Group: "g", Heads: map[string]string{repo: merge, "novox/mesh-catalog": otherMerge}})
|
|
}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ix, mods := modulesIndexer(t)
|
|
taken := newMem()
|
|
ix.Taken = taken
|
|
ctx := context.Background()
|
|
if err := ix.Apply(ctx, mergedMsg(repo, three)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ix.ApplySource(ctx, 7, sourceOf(repo, merge)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writes := mods.writes
|
|
if err := tc.then(ix); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := mods.writes - writes; got != tc.writes {
|
|
t.Errorf("%d values written, want %d", got, tc.writes)
|
|
}
|
|
for module, want := range tc.commit {
|
|
if m, ok := moduleOf(t, mods, module); !ok || m.Commit != want {
|
|
t.Errorf("%s = %+v (%v), want commit %.8s", module, m, ok, want)
|
|
}
|
|
}
|
|
key, _ := delivery.RepositoryKey(repo)
|
|
if _, held := taken.vals[key]; !held && tc.taken == "" {
|
|
return
|
|
}
|
|
if got, err := delivery.DecodeTaken(taken.vals[key]); err != nil || got.Merge != tc.taken || got.Repository != repo {
|
|
t.Errorf("taken.%s = %+v (%v), want merge %.8s", key, got, err, tc.taken)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMergedMessageRemovesThePullFromItsGroupToo(t *testing.T) { // 1238/AC5 with 1239 (both effects of one message)
|
|
ix, _ := modulesIndexer(t, rec(t, "build-1", "a", "novox/mesh-delivery", "modules/a", "main", "", "2026-10-19T09:00:00Z", aInputs))
|
|
cands := ix.Store.(*memStore)
|
|
_ = ix.Apply(context.Background(), pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a"))
|
|
_ = ix.Apply(context.Background(), mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "newtree"}))
|
|
if _, ok := cands.vals["feat/x"]; ok {
|
|
t.Error("the merged pull request is still a candidate")
|
|
}
|
|
}
|
|
|
|
func TestAMergeWritesTheModulesDirectoryAndKeepsItThroughSourceAndRecompute(t *testing.T) { // 1606/AC1, AC2
|
|
ix, mods := modulesIndexer(t,
|
|
rec(t, "build-1", "a", "novox/mesh-delivery", "modules/a", "main", "", "2026-10-19T09:00:00Z", aInputs),
|
|
rec(t, "build-2", "r", "novox/mesh-delivery", "", "main", "", "2026-10-19T09:00:00Z", strings.Replace(aInputs, `"Module":"a"`, `"Module":"r"`, 1)))
|
|
ctx := context.Background()
|
|
if err := ix.Apply(ctx, mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "newtree", "": "roottree"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for module, want := range map[string]string{"a": "modules/a", "r": delivery.RootPath} {
|
|
if m, ok := moduleOf(t, mods, module); !ok || m.Path != want {
|
|
t.Errorf("after the merge %s = %+v (%v), want path %q", module, m, ok, want)
|
|
}
|
|
}
|
|
if err := ix.ApplySource(ctx, 7, sourceOf("novox/mesh-delivery", merge)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ix.Recompute(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m, _ := moduleOf(t, mods, "a"); m.Path != "modules/a" || m.Src != 7 {
|
|
t.Errorf("after the source and a recompute a = %+v, want path modules/a kept and Src 7", m)
|
|
}
|
|
raw := string(mods.vals["a"])
|
|
if !strings.Contains(raw, `"path":"modules/a"`) {
|
|
t.Errorf("the stored value %s has no path", raw)
|
|
}
|
|
}
|
|
|
|
func TestARootModuleHasTheDirectoryDot(t *testing.T) { // 1615/AC3: empty means not known, a root module is "."
|
|
ix, mods := modulesIndexer(t, rec(t, "build-1", "r", "novox/mesh-sdk", "", "main", "", "2026-10-19T09:00:00Z", strings.Replace(aInputs, `"Module":"a"`, `"Module":"r"`, 1)))
|
|
if err := ix.Apply(context.Background(), mergedMsg("novox/mesh-sdk", map[string]string{"": "roottree"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m, ok := moduleOf(t, mods, "r"); !ok || m.Path != delivery.RootPath {
|
|
t.Errorf("module = %+v (%v), want path %q", m, ok, delivery.RootPath)
|
|
}
|
|
}
|
|
|
|
func TestAMergeAddingAModuleDirectoryWritesItsValueWithNoBuildRecordAnywhere(t *testing.T) { // I1: issue 1851, ADR 0501 clause 6
|
|
ix, mods, _ := manifestsIndexer(t) // no build record of any module
|
|
avahi := catalogueManifest(t, "avahi")
|
|
f := withManifests(mergedMsg(catalogueRepo, map[string]string{"modules/avahi": "newtree"}), map[string]string{"modules/avahi": avahi},
|
|
map[string]map[string]string{"modules/avahi": {"go.sum": avahiGoSum}})
|
|
if err := ix.Apply(context.Background(), f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, ok := moduleOf(t, mods, "avahi")
|
|
if !ok || m.Path != "modules/avahi" || m.Repository != catalogueRepo || m.Commit != merge || m.Manifest != delivery.ManifestDigest([]byte(avahi)) || m.Inputs2 == nil || m.Inputs2.Tree != "newtree" {
|
|
t.Fatalf("module = %+v (%v), want its directory, merge, manifest and inputs from the message alone", m, ok)
|
|
}
|
|
if m.FP != "" || m.None != "" {
|
|
t.Errorf("module = %+v, want no value of today's form (I4: it came from the build records)", m)
|
|
}
|
|
if m.FP2 == "" && m.None2 == "" {
|
|
t.Errorf("module = %+v, want a new-form fingerprint or its reason", m)
|
|
}
|
|
}
|
|
|
|
func TestAMergeWritesOnlyTheModulesItsManifestsName(t *testing.T) { // I1
|
|
ix, mods := modulesIndexer(t, rec(t, "build-2", "c", "novox/mesh-catalog", "modules/c", "main", "", "2026-10-19T09:00:00Z", aInputs))
|
|
f := mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "t", "modules/b": "t"})
|
|
f.Trees["modules/c"] = "t" // a directory whose manifest the message does not carry
|
|
if err := ix.Apply(context.Background(), f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, want := range map[string]bool{"a": true, "b": true, "c": false} {
|
|
if _, ok := mods.vals[name]; ok != want {
|
|
t.Errorf("modules.%s written %v, want %v", name, ok, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAModuleHeldOfAnotherRepositoryIsLeftAndSaid(t *testing.T) { // I1: a move between repositories is the operator's (ADR 0420 clause 7)
|
|
ix, mods := modulesIndexer(t)
|
|
var logs []string
|
|
ix.Logf = func(format string, args ...any) { logs = append(logs, format) }
|
|
ctx := context.Background()
|
|
if err := ix.Apply(ctx, mergedMsg("novox/mesh-catalog", map[string]string{"modules/a": "t"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := string(mods.vals["a"])
|
|
if err := ix.Apply(ctx, mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "t2"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(mods.vals["a"]) != before || len(logs) == 0 {
|
|
t.Errorf("modules.a = %s (was %s), logs %v: want it left, and said", mods.vals["a"], before, logs)
|
|
}
|
|
}
|
|
|
|
func TestTwoDirectoriesNamingOneModuleWriteOneAndSayTheOther(t *testing.T) { // I1
|
|
ix, mods := modulesIndexer(t)
|
|
var logs []string
|
|
ix.Logf = func(format string, args ...any) { logs = append(logs, format) }
|
|
f := mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "t1", "other/a": "t2"})
|
|
if err := ix.Apply(context.Background(), f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m, ok := moduleOf(t, mods, "a"); !ok || m.Path != "modules/a" || len(logs) == 0 {
|
|
t.Errorf("module = %+v (%v), logs %v: want the first directory by name, and the other said", m, ok, logs)
|
|
}
|
|
// Once held, the directory the bucket holds stays the module's.
|
|
f = mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "t1", "aaa/a": "t3"})
|
|
if err := ix.Apply(context.Background(), f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m, _ := moduleOf(t, mods, "a"); m.Path != "modules/a" {
|
|
t.Errorf("module = %+v, want the held directory kept", m)
|
|
}
|
|
}
|
|
|
|
func takenKey(repo string) string {
|
|
key, _ := delivery.RepositoryKey(repo)
|
|
return key
|
|
}
|