route-proxy leaves the controller's repository, which is archived, and nothing registered a move since the controller went: the indexer left a module named by another repository's merge as it was, forever. Now that merge waits, and the holding repository's merge that drops the module hands it over in place, so its key never goes missing between the two; merged the other way round, the key is deleted and written new. A trunk that still holds the module keeps it (ADR 0420 clause 7, ADR 0501 clause 3). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
492 lines
23 KiB
Go
492 lines
23 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))
|
|
}},
|
|
// 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 {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = "novox/mesh-controller", "novox/mesh-networking"
|
|
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"})
|
|
for _, tc := range []struct {
|
|
name string
|
|
merges []delivery.Forge
|
|
repository string // modules.a's repository at the end, "" for none
|
|
deletedKeyA bool
|
|
}{
|
|
{"named while its trunk holds it: left", []delivery.Forge{held, named}, from, false},
|
|
{"named, then dropped by its trunk: handed over in place", []delivery.Forge{held, named, dropped}, to, false},
|
|
{"dropped by its trunk, then named: deleted, then written new", []delivery.Forge{held, dropped, named}, to, true},
|
|
{"dropped by its trunk with no merge naming it: deleted", []delivery.Forge{held, dropped}, "", true},
|
|
} {
|
|
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) }
|
|
for _, f := range tc.merges {
|
|
if err := ix.Apply(context.Background(), f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
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 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 {
|
|
t.Error("modules.b, which its trunk still holds, is gone")
|
|
}
|
|
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
|
|
}
|