I1: a merge writes each module its manifests name, at its directory. I2: a pull request's changed directory, a preview's touched module and a source revision's modules come from the modules bucket (a module a pull request adds is found in the preview by its manifest). I4: the value of today's form (fp, none) is no longer written, since it was computed from the build records; mesh-sdk keeps it readable until R4g. One commit: the three share applyMerged and its tests. Pins mesh-sdk feat/r4e-sdk 9cb4d59.
218 lines
9.3 KiB
Go
218 lines
9.3 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)
|
|
ix, mods := modulesIndexer(t, rec(t, "build-1", "a", "novox/mesh-delivery", "modules/a", "main", "", "2026-10-19T09:00:00Z", aInputs))
|
|
m := mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "newtree"})
|
|
_ = ix.Apply(context.Background(), m)
|
|
rev, writes := mods.revs["a"], mods.writes
|
|
_ = ix.Apply(context.Background(), m)
|
|
if mods.revs["a"] != rev || mods.writes != writes {
|
|
t.Errorf("the same merge wrote again (%d writes, was %d)", mods.writes, writes)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|