The operator, 2026-10-25: only if a module has a new version in commit x is a new value written naming commit x. A merge wrote every module of the repository at the new commit, fingerprint unchanged (about 186 keys per mesh-catalog merge), and the controller's take-in took each in again: twice tonight it flooded. Now a merge that gives a module the value it holds but for the commit writes nothing, and the value keeps the commit its fingerprint first appeared at. The kind: a write that differs only in where it was computed. The other instance, a source revision re-stamping Src on every module at its repository's head after any merge anywhere, now stamps Src once. The trunk head stays the gate's source.main.
255 lines
11 KiB
Go
255 lines
11 KiB
Go
package index
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"path"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
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
|
|
}{
|
|
{name: "the same merge again", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": 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},
|
|
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 later source revision of another repository's merge", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": 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)
|
|
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|