317 lines
15 KiB
Go
317 lines
15 KiB
Go
package index
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
func TestARegisteredModuleIsIndexedFromItsRepository(t *testing.T) { // the grants part's gaps: modules registered from their own repositories had no manifest here
|
|
const forge = "http://novox.internal:20000/jschoubben/snake.git"
|
|
snake := `{"module":"snake","version":"1"}`
|
|
for _, tc := range []struct {
|
|
name string
|
|
path string
|
|
read RepoRead
|
|
err error
|
|
ref string // the build record's ref
|
|
heads map[string]string // the gate source's heads
|
|
wantRead bool // the forge is read
|
|
held bool // modules.snake exists already
|
|
other bool // modules.other of the same repository exists
|
|
want bool // modules.snake is written
|
|
}{
|
|
{wantRead: true, name: "a module at the root of its repository", path: "", want: true,
|
|
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}},
|
|
{wantRead: true, name: "a root spelled as a dot", path: ".", want: true,
|
|
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}},
|
|
{wantRead: true, name: "a module in a directory", path: "modules/snake", want: true,
|
|
read: RepoRead{Root: "r0", Trees: map[string]string{"modules/snake": "t1"}, Manifests: map[string]string{"modules/snake": snake}}},
|
|
{wantRead: true, name: "another module of the repository is neither written nor dropped", path: "", want: true, other: true,
|
|
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}},
|
|
{name: "a module the bucket holds is left", path: "", held: true,
|
|
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}},
|
|
{wantRead: true, name: "no manifest at the path", path: "modules/x",
|
|
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}},
|
|
{wantRead: true, name: "a manifest of another module", path: "",
|
|
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": `{"module":"other","version":"1"}`}}},
|
|
{wantRead: true, name: "a repository whose default branch is master", path: "", ref: "master", want: true,
|
|
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}},
|
|
{wantRead: true, name: "a repository built from a branch of its own (nox-mesh)", path: "", ref: "nox-mesh", want: true,
|
|
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}},
|
|
{wantRead: true, name: "a repository the source names, the module unheld and present at the head", path: "", want: true, heads: map[string]string{"jschoubben/snake": h2},
|
|
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}},
|
|
{wantRead: true, name: "a forge that cannot say", path: "", err: errors.New("down")},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ix, mods := modulesIndexer(t, rec(t, "build-1", "snake", forge, tc.path, tc.ref, "", "2026-10-19T09:00:00Z", ""))
|
|
if tc.held {
|
|
if err := mods.Create(context.Background(), "snake", mustModule(t, delivery.Module{Repository: "jschoubben/snake", Commit: h1, Path: ".", None2: "x"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if tc.other {
|
|
if err := mods.Create(context.Background(), "other", mustModule(t, delivery.Module{Repository: "jschoubben/snake", Commit: h1, Path: "modules/other", None2: "x"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
ix.Manifests = newMem()
|
|
var asked struct {
|
|
repo, sha, dir string
|
|
n int
|
|
}
|
|
ix.Read = func(_ context.Context, repo, sha, dir string) (RepoRead, error) {
|
|
asked.repo, asked.sha, asked.dir = repo, sha, dir
|
|
asked.n++
|
|
return tc.read, tc.err
|
|
}
|
|
if tc.heads != nil {
|
|
ix.source = sourceNow{rev: 1, heads: tc.heads}
|
|
}
|
|
err := ix.IndexRegisteredAll(context.Background())
|
|
if (err != nil) != (tc.err != nil) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
wantCommit := "7a74cee83f88e6585a5511c3cc53bdf0f5303d20" // the record's
|
|
if tc.heads != nil {
|
|
wantCommit = h2 // the source's head
|
|
}
|
|
if tc.wantRead && asked.sha != wantCommit {
|
|
t.Errorf("the forge was asked at %s, want %s", asked.sha, wantCommit)
|
|
}
|
|
wantReads := 1
|
|
if !tc.wantRead {
|
|
wantReads = 0
|
|
}
|
|
if tc.wantRead && asked.dir != dirOf(tc.path) {
|
|
t.Errorf("the forge was asked for directory %q", asked.dir)
|
|
}
|
|
if asked.n != wantReads {
|
|
t.Errorf("forge reads = %d, want %d", asked.n, wantReads)
|
|
}
|
|
m, ok := moduleOf(t, mods, "snake")
|
|
if tc.want {
|
|
if !ok || m.Manifest == "" || m.Repository != "jschoubben/snake" || m.Commit != wantCommit || m.Path != pathOf(dirOf(tc.path)) {
|
|
t.Fatalf("modules.snake = %+v, found %v", m, ok)
|
|
}
|
|
if len(ix.Manifests.(*memStore).vals) != 1 {
|
|
t.Errorf("manifests written = %d, want 1", len(ix.Manifests.(*memStore).vals))
|
|
}
|
|
if asked.repo != "jschoubben/snake" {
|
|
t.Errorf("the forge was asked for %q", asked.repo)
|
|
}
|
|
} else if !tc.held && ok {
|
|
t.Fatalf("modules.snake written: %+v", m)
|
|
}
|
|
if _, ok := moduleOf(t, mods, "other"); ok != tc.other {
|
|
t.Errorf("modules.other held = %v, want %v", ok, tc.other)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Issue 1126: mesh-catalog#535 changed mesh-vault, but its merged message never reached the indexer, so the record
|
|
// stood at the merge before. A take-in of the held module reads it again at the source's head, only when the record is
|
|
// behind that head: a head older than the record (a merge applied before the source moved), or one whose order the forge
|
|
// cannot say, is never written back.
|
|
for _, tc := range []struct {
|
|
name string
|
|
contains func(context.Context, string, string, string) (bool, error)
|
|
moved bool
|
|
}{
|
|
{name: "a held module behind the source's head is read again", moved: true,
|
|
contains: func(_ context.Context, _, commit, in string) (bool, error) { return commit == merge && in == h2, nil }},
|
|
{name: "a source head older than the record is not read", contains: func(_ context.Context, _, commit, in string) (bool, error) { return commit == h2 && in == merge, nil }},
|
|
// Review of 77d738d: a diverged or rewritten history, or a record whose commit the forge no longer has, is not older.
|
|
{name: "a source head of a diverged history is read again", moved: true,
|
|
contains: func(context.Context, string, string, string) (bool, error) { return false, nil }},
|
|
{name: "a record whose commit the forge no longer has is read again", moved: true, contains: func(context.Context, string, string, string) (bool, error) {
|
|
return false, errors.New("gitea_contains: 404 Not Found")
|
|
}},
|
|
{name: "a forge that cannot say the order reads nothing", contains: func(context.Context, string, string, string) (bool, error) {
|
|
return false, errors.New("timeout")
|
|
}},
|
|
{name: "no forge to ask reads nothing"},
|
|
} {
|
|
t.Run("take-in: "+tc.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
ix, mods := modulesIndexer(t)
|
|
ix.Manifests = newMem()
|
|
if err := ix.Apply(ctx, mergedMsg("novox/mesh-catalog", map[string]string{"modules/a": "treeA"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ix.source = sourceNow{rev: 2, heads: map[string]string{"novox/mesh-catalog": h2}}
|
|
ix.Contains = tc.contains
|
|
var asked []string
|
|
ix.Read = func(_ context.Context, _, sha, dir string) (RepoRead, error) {
|
|
asked = append(asked, sha+":"+dir)
|
|
return RepoRead{Root: "r1", Trees: map[string]string{"modules/a": "treeA2"}, Manifests: map[string]string{"modules/a": `{"module":"a","version":"2"}`}}, nil
|
|
}
|
|
got := ix.TakeIn(ctx, "a")
|
|
m, ok := moduleOf(t, mods, "a")
|
|
if !tc.moved {
|
|
if got.Stored || !got.Held || len(asked) != 0 || !strings.Contains(got.Answer, "another head") || !ok || m.Commit != merge {
|
|
t.Errorf("take-in answered %+v, asked %v, modules.a at %.8s", got, asked, m.Commit)
|
|
}
|
|
return
|
|
}
|
|
if !got.Stored || !got.Held || got.Commit != h2 || got.Path != "modules/a" || !strings.Contains(got.Answer, "written") {
|
|
t.Errorf("take-in answered %+v", got)
|
|
}
|
|
if !ok || m.Commit != h2 || m.Inputs2 == nil || m.Inputs2.Tree != "treeA2" {
|
|
t.Errorf("modules.a = %+v (%v), want it at %.8s with tree treeA2 (asked %v)", m, ok, h2, asked)
|
|
}
|
|
n := len(asked) // at the head now: a take-in answers held and reads nothing
|
|
if again := ix.TakeIn(ctx, "a"); again.Stored || !again.Held || len(asked) != n {
|
|
t.Errorf("a take-in of a module at the source's head answered %+v and asked %v", again, asked)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func mustModule(t *testing.T, m delivery.Module) []byte {
|
|
t.Helper()
|
|
b, err := delivery.EncodeModule(withV(m))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func dirOf(p string) string {
|
|
if p == "." {
|
|
return ""
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestARegistrationOfAnotherRepositoryDoesNotHideTheRealOne(t *testing.T) { // photos: its newest trunk record was a directory of the catalogue's, then novox/photos at nox-mesh
|
|
const catalogue, own = "http://novox.internal:20000/novox/mesh-catalog.git", "http://novox.internal:20000/novox/Link2Pay.git"
|
|
ix, mods := modulesIndexer(t,
|
|
rec(t, "build-1", "link2pay", own, "", "nox-mesh", "", "2026-10-06T23:02:00Z", ""),
|
|
rec(t, "build-2", "link2pay", catalogue, "modules/link2pay", "main", "", "2026-10-04T16:02:00Z", ""),
|
|
rec(t, "build-3", "link2pay", catalogue, "modules/link2pay", "main", "", "2026-10-03T16:02:00Z", ""))
|
|
ix.Manifests = newMem()
|
|
var asked []string
|
|
ix.Read = func(_ context.Context, repo, _, dir string) (RepoRead, error) {
|
|
asked = append(asked, repo+":"+dir)
|
|
if repo == "novox/Link2Pay" {
|
|
return RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": `{"module":"link2pay","version":"1"}`}}, nil
|
|
}
|
|
return RepoRead{Root: "r0"}, nil
|
|
}
|
|
if err := ix.IndexRegisteredAll(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, ok := moduleOf(t, mods, "link2pay")
|
|
if !ok || m.Repository != "novox/Link2Pay" {
|
|
t.Fatalf("modules.link2pay = %+v, found %v", m, ok)
|
|
}
|
|
if len(asked) != 2 || asked[0] != "novox/mesh-catalog:modules/link2pay" || asked[1] != "novox/Link2Pay:." && asked[1] != "novox/Link2Pay:" {
|
|
t.Errorf("the forge was asked %v: want the catalogue's trunk record first, once, then the repository's own", asked)
|
|
}
|
|
}
|
|
|
|
func TestAForgeThatCannotSayLeavesTheModuleUnwrittenAndAsksAgain(t *testing.T) { // a timeout or a 404 at the head: no read at an older commit
|
|
// Issue 1945: car-hunter and snake answered 404 until the forge's account could read them, and were read again only when
|
|
// the indexer started again. Each row's trigger must now take the module in once the forge reads it.
|
|
const snake = `{"module":"snake","version":"1"}`
|
|
ctx := context.Background()
|
|
for _, tc := range []struct{ failure, trigger string }{
|
|
{"timeout", "the indexer starts again"},
|
|
{"Gitea API /repos/x/git/trees/y: 404 not found", "the indexer starts again"},
|
|
{"Gitea API /repos/x/git/trees/y: 404 not found", "a new source revision"},
|
|
{"Gitea API /repos/x/git/trees/y: 404 not found", "a merged message of another repository"},
|
|
{"Gitea API /repos/x/git/trees/y: 404 not found", "a person's take-in"},
|
|
{"timeout", "a person's take-in"},
|
|
} {
|
|
t.Run(tc.failure+", then "+tc.trigger, func(t *testing.T) {
|
|
ix, mods := modulesIndexer(t, rec(t, "build-1", "snake", "http://novox.internal:20000/jschoubben/snake.git", "", "master", "", "2026-10-19T09:00:00Z", ""))
|
|
ix.Manifests = newMem()
|
|
ix.source = sourceNow{rev: 1, heads: map[string]string{"jschoubben/snake": h2}}
|
|
var asked []string
|
|
readable := false
|
|
ix.Read = func(_ context.Context, _, sha, _ string) (RepoRead, error) {
|
|
asked = append(asked, sha)
|
|
if readable {
|
|
return RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}, nil
|
|
}
|
|
return RepoRead{}, errors.New(tc.failure)
|
|
}
|
|
if err := ix.IndexRegisteredAll(ctx); err == nil {
|
|
t.Errorf("no error")
|
|
}
|
|
if _, ok := moduleOf(t, mods, "snake"); ok || len(asked) != 1 || asked[0] != h2 {
|
|
t.Errorf("written %v, asked %v", ok, asked)
|
|
}
|
|
if err := ix.IndexRegisteredAll(ctx); err == nil || len(asked) != 2 {
|
|
t.Errorf("not asked again (asked %v)", asked)
|
|
}
|
|
readable = true // the forge's account reads the repository now
|
|
var got TakenIn
|
|
switch tc.trigger {
|
|
case "the indexer starts again":
|
|
if err := ix.IndexRegisteredAll(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "a new source revision":
|
|
if err := ix.ApplySource(ctx, 2, delivery.Source{Heads: map[string]string{"jschoubben/snake": h2, "novox/other": h1}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ix.retryWG.Wait() // read again in the background
|
|
case "a merged message of another repository":
|
|
if err := ix.Apply(ctx, mergedMsg("novox/other", map[string]string{"": "t9"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ix.retryWG.Wait() // read again in the background
|
|
case "a person's take-in":
|
|
got = ix.TakeIn(ctx, "snake")
|
|
if !got.Stored || got.Repository != "jschoubben/snake" || got.Commit != h2 || got.Path != "." || got.Answer == "" {
|
|
t.Errorf("take-in answered %+v", got)
|
|
}
|
|
}
|
|
if m, ok := moduleOf(t, mods, "snake"); !ok || m.Manifest == "" || m.Commit != h2 {
|
|
t.Fatalf("after %s the readable module is not taken in: %+v, found %v (asked %v)", tc.trigger, m, ok, asked)
|
|
}
|
|
// Taken in, it is read no more: a further event asks the forge nothing.
|
|
n := len(asked)
|
|
if err := ix.ApplySource(ctx, 3, delivery.Source{Heads: map[string]string{"jschoubben/snake": h2}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ix.retryWG.Wait()
|
|
if len(asked) != n {
|
|
t.Errorf("the forge was asked again after the module was taken in: %v", asked)
|
|
}
|
|
if again := ix.TakeIn(ctx, "snake"); again.Stored || !again.Held || len(asked) != n {
|
|
t.Errorf("a take-in of a held module answered %+v and asked %v", again, asked)
|
|
}
|
|
if none := ix.TakeIn(ctx, "nobody"); none.Stored || none.Held || !strings.Contains(none.Answer, "no registration") {
|
|
t.Errorf("a take-in of an unregistered module answered %+v", none)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestARecordOfAPullRequestDoesNotReplaceTheRegisteredRef(t *testing.T) { // a newer feat/x record of the same place
|
|
const repo = "http://novox.internal:20000/novox/photos.git"
|
|
b := NewBuilds()
|
|
for _, r := range [][2]any{
|
|
rec(t, "b1", "photos", repo, "", "nox-mesh", "", "2026-10-01T09:00:00Z", ""),
|
|
rec(t, "b2", "photos", repo, "", "nox-mesh", "", "2026-10-02T09:00:00Z", ""),
|
|
rec(t, "b3", "photos", repo, "", "feat/x", "", "2026-10-09T09:00:00Z", ""),
|
|
} {
|
|
_ = b.Put(r[1].([]byte))
|
|
}
|
|
got := b.RegistrationsOf("photos")
|
|
if len(got) != 1 || got[0].Ref != "nox-mesh" {
|
|
t.Fatalf("registrations = %+v, want the ref most records name, not the newer feat/x", got)
|
|
}
|
|
k, v := record(t, "b4", "photos", repo, "", "main", "", "2026-10-03T09:00:00Z", "")
|
|
_, _ = k, b.Put(v) // a trunk's ref beats a branch of its own
|
|
if got := b.RegistrationsOf("photos"); len(got) != 1 || got[0].Ref != "main" {
|
|
t.Fatalf("registrations = %+v", got)
|
|
}
|
|
}
|