Files
mesh-delivery/modules/mesh-delivery-indexer/internal/index/modules_test.go
T
jschoubben e1d0c9fe03 A merge that leaves a repository no module never deletes a module another trunk holds, and indexer_take_in takes a repository in (issue 1367)
dropRemoved hands the module over from the other trunk, read from the forge at the head the source names, so no wait in memory is needed. indexer_take_in with a repository takes in a head with no module.json as a merge that leaves none, and answers by name for a head with modules.
2026-11-03 16:45:06 +01:00

566 lines
27 KiB
Go

package index
import (
"context"
"errors"
"fmt"
"path"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
const merge = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
// logged is one entry of the builder's log (mesh-sdk go/delivery BuildLogEntry, as the builder appends it): a build of
// module from repo's directory path at ref ("" for the trunk), its outcome ("" done, "failed", or "asked" for a build only
// started), at the time at; id makes its fingerprint.
func logged(t *testing.T, id, module, repo, path, ref, outcome, at string) []byte {
t.Helper()
when, err := time.Parse(time.RFC3339, at)
if err != nil {
t.Fatal(err)
}
if path == "" {
path = delivery.RootPath
}
e := delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: module, FP: "src2:" + id, Repository: repo, Directory: path,
Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Ref: ref, Node: "n1", At: when, Outcome: delivery.BuildDone}
switch outcome {
case "":
case "failed":
e.Outcome = delivery.BuildFailed
default:
e.Event, e.Outcome = delivery.BuildStartedEvent, ""
}
raw, err := delivery.EncodeBuildLogEntry(e)
if err != nil {
t.Fatal(err)
}
return raw
}
// record is logged under the module's log key, as the tests' tables carry it.
func record(t *testing.T, id, module, repo, path, ref, state, at, _ string) (string, []byte) {
t.Helper()
return module, logged(t, id, module, repo, path, ref, state, at)
}
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 {
if err := ix.Builds.Put(r[1].([]byte)); err != nil {
t.Fatal(err)
}
}
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
// err says then fails (the message is left for redelivery); said is a line the indexer's log (or that error) must hold.
err bool
said string
// contains, when set, is the forge's order, and asks how often then asks it (one question per merged message).
contains func(context.Context, string, string, string) (bool, error)
asks int
}{
{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,
said: "novox/mesh-delivery at cccccccccccc (#12): merged message applied; modules written: a",
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))
if err := ix.ApplySource(context.Background(), 8, sourceOf(repo, next)); err != nil {
return err
}
return ix.FillTaken(context.Background())
}},
// Issue 1126 (mesh-catalog#535): the forge bridge never wrote the merged message of 9fd9569, so the forge stream was
// drained with the merge not applied; the source head moved the key all the same, and the view said the merge was
// on the machines. A key moves only by a merge applied (ADR 0556 clause 1), never by the source's word.
{name: "a source head of a drained forge stream whose merge was never heard leaves the key", 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 == merge && in == next, 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 no missing key", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: "",
then: func(ix *Indexer) error {
delete(ix.Taken.(*memStore).vals, takenKey(repo))
if err := ix.ApplySource(context.Background(), 8, sourceOf(repo, next)); err != nil {
return err
}
return ix.FillTaken(context.Background())
}},
{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)
}},
// Issue 1126 note 76: mesh-catalog's merge 9cdac09a was written back over the later 69056948, rolling kernel and
// mesh-build-image back on the nodes. A merge never moves a module backwards: the held commit must be in the incoming one.
{name: "an earlier merge heard again after a later one moves no module back", writes: 1, commit: map[string]string{"a": next, "b": merge, "c": merge}, taken: next,
said: "the merged message is older than taken.novox/mesh-delivery at cccccccccccc",
then: func(ix *Indexer) error {
ix.Contains = func(_ context.Context, _, commit, in string) (bool, error) {
return commit == in || commit == merge && in == next, nil
}
later := mergedMsg(repo, map[string]string{"modules/a": "treeA2", "modules/b": "treeB", "modules/c": "treeC"})
later.Merge = next
if err := ix.Apply(context.Background(), later); err != nil {
return err
}
return ix.Apply(context.Background(), mergedMsg(repo, three))
}},
{name: "an earlier merge that lacks a later merge's module deletes nothing", writes: 1, commit: map[string]string{"a": merge, "d": next}, taken: next,
said: "the merged message is older than taken.novox/mesh-delivery at cccccccccccc",
then: func(ix *Indexer) error {
ix.Contains = func(_ context.Context, _, commit, in string) (bool, error) {
return commit == in || commit == merge && in == next, nil
}
later := mergedMsg(repo, map[string]string{"modules/a": "treeA", "modules/b": "treeB", "modules/c": "treeC", "modules/d": "treeD"})
later.Merge = next
if err := ix.Apply(context.Background(), later); err != nil {
return err
}
return ix.Apply(context.Background(), mergedMsg(repo, three))
}},
// Issue 1126 note 94: a merge that leaves no module.json anywhere (hq, or a repository whose last module was removed) is a
// merged message with no trees; it drops the repository's modules and moves its taken key, and an older one, heard late,
// goes through the same order check and drops nothing.
{name: "a merge that leaves no module.json drops every module and is taken in", writes: 3, commit: map[string]string{"a": "", "b": "", "c": ""}, taken: next,
said: "modules written: none",
then: func(ix *Indexer) error {
m := mergedMsg(repo, nil)
m.Merge = next
return ix.Apply(context.Background(), m)
}},
{name: "an earlier merge with no module.json heard after a later one drops nothing", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: next,
said: "the merged message is older than taken.novox/mesh-delivery at cccccccccccc",
then: func(ix *Indexer) error {
const earlier = "dddddddddddddddddddddddddddddddddddddddd"
ix.Contains = func(_ context.Context, _, commit, in string) (bool, error) {
return commit == in || commit == earlier && in == next, nil
}
later := mergedMsg(repo, three)
later.Merge = next
if err := ix.Apply(context.Background(), later); err != nil {
return err
}
gone := mergedMsg(repo, nil)
gone.Merge = earlier
return ix.Apply(context.Background(), gone)
}},
// Review round 2: a 502 whose words carry "404" inside a sha is no not-found; the late message is left for redelivery.
{name: "a late message whose order the forge fails with a 502 writes nothing and is left for redelivery", writes: 1, commit: map[string]string{"a": next, "b": merge, "c": merge}, taken: next, err: true,
said: "502 Bad Gateway",
then: func(ix *Indexer) error {
ix.Contains = func(context.Context, string, string, string) (bool, error) { return false, nil }
later := mergedMsg(repo, map[string]string{"modules/a": "treeA2", "modules/b": "treeB", "modules/c": "treeC"})
later.Merge = next
if err := ix.Apply(context.Background(), later); err != nil {
return err
}
ix.Contains = func(context.Context, string, string, string) (bool, error) {
return false, errors.New("Gitea API /repos/x/y/compare/aaa404aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: 502 Bad Gateway")
}
return ix.Apply(context.Background(), mergedMsg(repo, three))
}},
{name: "a forge that cannot say the order writes nothing and leaves the message for redelivery", writes: 0, commit: map[string]string{"a": merge}, taken: merge, err: true,
said: "cannot be read",
then: func(ix *Indexer) error {
ix.Contains = func(context.Context, string, string, string) (bool, error) {
return false, errors.New("the forge did not answer")
}
later := mergedMsg(repo, map[string]string{"modules/a": "treeA2", "modules/b": "treeB", "modules/c": "treeC"})
later.Merge = next
return ix.Apply(context.Background(), later)
}},
// Review of 77d738d: only a merge older than the one taken in is kept back. A diverged or rewritten history, or a
// taken merge the forge no longer has, is not older, and one question per message is asked.
{name: "a merge of a diverged history is written, asking the forge once", writes: 1, commit: map[string]string{"a": next, "b": merge, "c": merge}, taken: next,
contains: func(context.Context, string, string, string) (bool, error) { return false, nil }, asks: 1,
then: func(ix *Indexer) error {
later := mergedMsg(repo, map[string]string{"modules/a": "treeA2", "modules/b": "treeB", "modules/c": "treeC"})
later.Merge = next
return ix.Apply(context.Background(), later)
}},
{name: "a taken merge the forge no longer has stops no later merge", writes: 1, commit: map[string]string{"a": next, "b": merge, "c": merge}, taken: next,
said: "is not known to be older",
then: func(ix *Indexer) error {
ix.Contains = func(context.Context, string, string, string) (bool, error) {
return false, errors.New("gitea_contains: the forge answered: 404 Not Found")
}
later := mergedMsg(repo, map[string]string{"modules/a": "treeA2", "modules/b": "treeB", "modules/c": "treeC"})
later.Merge = next
return ix.Apply(context.Background(), later)
}},
{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
asks := 0
if tc.contains != nil {
ix.Contains = func(ctx context.Context, repo, commit, in string) (bool, error) {
asks++
return tc.contains(ctx, repo, commit, in)
}
}
var said strings.Builder
ix.Logf = func(format string, args ...any) { fmt.Fprintf(&said, format+"\n", args...) }
err := tc.then(ix)
if tc.contains != nil && asks != tc.asks {
t.Errorf("the forge was asked %d times, want %d", asks, tc.asks)
}
if (err != nil) != tc.err {
t.Fatalf("then = %v, want an error %v", err, tc.err)
}
if err != nil {
said.WriteString(err.Error()) // what handle logs as left for redelivery
}
if !strings.Contains(said.String(), tc.said) {
t.Errorf("the log %q does not say %q", said.String(), tc.said)
}
if got := mods.writes - writes; got != tc.writes {
t.Errorf("%d values written, want %d", got, tc.writes)
}
for module, want := range tc.commit { // "" is a module the merge deleted
if m, ok := moduleOf(t, mods, module); ok != (want != "") || 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)
}
}
}
// deletesSeen is a modules store that remembers each key deleted.
type deletesSeen struct {
*memStore
deleted []string
}
func (d *deletesSeen) Delete(ctx context.Context, key string, rev uint64) error {
d.deleted = append(d.deleted, key)
return d.memStore.Delete(ctx, key, rev)
}
// I1: a move between repositories is the merges of the group that moves it (ADR 0420 clause 7; issue 1126, route-proxy
// leaving the controller's repository). A merge naming a module another repository's trunk still holds leaves it, said; the
// holding repository's merge that drops it hands it over in place, never deleting its key; merged the other way round,
// the module is deleted and then written new.
func TestAModuleMovesBetweenRepositoriesOnlyWhenItsTrunkDropsIt(t *testing.T) {
const from, to, toHead = "novox/mesh-controller", "novox/mesh-networking", "bbbbbbbbbbbb"
held := mergedMsg(from, map[string]string{"modules/a": "t", "modules/b": "t"})
named := mergedMsg(to, map[string]string{"modules/a": "t2"})
dropped := mergedMsg(from, map[string]string{"modules/b": "t"})
gone := mergedMsg(from, nil) // issue 1367: a merge that leaves no module.json
gone.Trees, gone.Manifests = nil, nil
for _, tc := range []struct {
name string
merges []delivery.Forge
repository string // modules.a's repository at the end, "" for none
deletedKeyA bool
// Issue 1367: a fresh indexer, no wait in memory, the source naming heads; trunk is the module directories the forge
// reads at the head of `to`; takeIn is a repository taken in after the merges (indexer_take_in), whose answer must
// hold answer and be stored or not.
heads bool
trunk []string
takeIn string
answer string
stored bool
bGoneAtEnd bool
}{
{name: "named while its trunk holds it: left", merges: []delivery.Forge{held, named}, repository: from},
{name: "named, then dropped by its trunk: handed over in place", merges: []delivery.Forge{held, named, dropped}, repository: to},
{name: "dropped by its trunk, then named: deleted, then written new", merges: []delivery.Forge{held, dropped, named}, repository: to, deletedKeyA: true},
{name: "dropped by its trunk with no merge naming it: deleted", merges: []delivery.Forge{held, dropped}, deletedKeyA: true},
{name: "no module.json left, another trunk holds it, nothing waits in memory: written from that trunk, never deleted",
merges: []delivery.Forge{held, gone}, heads: true, trunk: []string{"modules/a", "modules/other"}, repository: to, bGoneAtEnd: true},
{name: "no module.json left, no other trunk holds it: deleted",
merges: []delivery.Forge{held, gone}, heads: true, deletedKeyA: true, bGoneAtEnd: true},
{name: "taken in by repository, a head with no module.json: handed over from the other trunk",
merges: []delivery.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: from, answer: "handed over", stored: true, repository: to, bGoneAtEnd: true},
{name: "taken in by repository, a head with modules: answered by name",
merges: []delivery.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: to, answer: "by name", repository: from},
{name: "taken in by repository, no head named: answered so",
merges: []delivery.Forge{held}, heads: true, takeIn: "novox/nobody", answer: "names no head", repository: from},
} {
t.Run(tc.name, func(t *testing.T) {
mods := &deletesSeen{memStore: newMem()}
ix := &Indexer{Store: newMem(), Modules: mods, Builds: NewBuilds()}
var logs []string
ix.Logf = func(format string, args ...any) { logs = append(logs, format) }
if tc.heads {
ix.source = sourceNow{rev: 7, heads: map[string]string{from: merge, to: toHead}}
ix.Trunk = func(_ context.Context, repository, sha string) ([]string, error) {
if repository == to && sha == toHead {
return tc.trunk, nil
}
if repository == from && tc.takeIn == from {
return nil, nil
}
return nil, nil
}
ix.Read = func(_ context.Context, repository, sha, dir string) (RepoRead, error) {
if repository != to || sha != toHead || dir != "modules/a" {
t.Errorf("read %s at %s %q, want only %s at %s modules/a", repository, sha, dir, to, toHead)
}
return RepoRead{Root: "r2", Trees: map[string]string{dir: "t2"}, Manifests: map[string]string{dir: `{"module":"a","version":"1"}`}}, nil
}
}
for _, f := range tc.merges {
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
}
}
if tc.takeIn != "" {
got := ix.TakeInRepository(context.Background(), tc.takeIn)
if got.Stored != tc.stored || !strings.Contains(got.Answer, tc.answer) {
t.Errorf("take-in answered %+v, want stored %v and %q", got, tc.stored, tc.answer)
}
}
m, ok := moduleOf(t, mods.memStore, "a")
if got := map[bool]string{true: m.Repository}[ok]; got != tc.repository {
t.Errorf("modules.a of %q, want %q (logs %v)", got, tc.repository, logs)
}
if ok && m.Path != "modules/a" {
t.Errorf("modules.a at %q, want modules/a", m.Path)
}
if ok && m.Repository == to && tc.heads && m.Commit != toHead {
t.Errorf("modules.a at %s, want the other trunk's head %s", m.Commit, toHead)
}
if deleted := slices.Contains(mods.deleted, "a"); deleted != tc.deletedKeyA {
t.Errorf("modules.a deleted %v, want %v", deleted, tc.deletedKeyA)
}
if _, ok := mods.vals["b"]; ok == tc.bGoneAtEnd {
t.Errorf("modules.b held %v, want %v", ok, !tc.bGoneAtEnd)
}
if len(logs) == 0 {
t.Error("nothing was said")
}
})
}
}
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
}