Files
mesh-delivery/modules/mesh-delivery-indexer/internal/index/previews_test.go
T
jschoubben 0772e6dc23 IX14: a run on request's candidate from the runner's run-started, dropped once every member's pipeline ended
The indexer hears the runner's run-started and pipeline-ended on their own streams and writes candidates.run-<id>:
one pull per member, its repository and directory from its registration, its manifest and fingerprint from a take-in
of its commit read through the seat git, over its branch's revision view when its registration watches the branch,
else the trunk's. The state runs keeps each run's members and ended modules, so a restart between two ends still
drops the candidate. The try candidates, the checker's checks consumer, checker_tries and the three try tools go;
IsTryGroup takes run-<id> beside try-<digits>.

Builds: design 47 G §64, §190, §321, §66, A-160, A-174
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:50:15 +01:00

336 lines
14 KiB
Go

package index
import (
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"strings"
"testing"
sdkstep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"mesh-delivery-indexer/internal/fingerprint"
)
const (
trunkNow = "5555555555555555555555555555555555555555"
trunkOld = "4444444444444444444444444444444444444444"
mergeSha = "6666666666666666666666666666666666666666"
)
// A trunk of the catalogue with avahi (and the builder, a base and others) merged, the toolchain agreed, and the gate's source
// naming the catalogue's trunk head: avahi has a fingerprint of the new form at tree-avahi.
func previewBed(t *testing.T, report bool) (*Indexer, *memStore, *memStore) {
t.Helper()
ix, mods := newFormIndexer(t, builderModules(t)...)
if report {
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
}
if err := applyS(ix, context.Background(), 5, srcT{Group: "g", Heads: map[string]string{catalogueRepo: trunkNow}}); err != nil {
t.Fatal(err)
}
return ix, mods, ix.Store.(*memStore)
}
// avahiOnTools is avahi's manifest as builderModules has it: standing on mesh-tools.
func avahiOnTools(t *testing.T) string {
return standingOn(t, catalogueManifest(t, "avahi"), "mesh-tools")
}
// avahiPull is the pull message of the catalogue's pull request 301 touching avahi, with its manifest.
func avahiPull(t *testing.T, head string) forgemsg.Forge {
t.Helper()
f := manifestPull(t, 301, head, avahiOnTools(t))
f.Manifests = map[string]string{"modules/avahi": avahiOnTools(t)}
return f
}
func avahiPreview(t *testing.T, trunk, head string) meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue {
return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: catalogueRepo, Head: head, Trunk: trunk, Merge: mergeSha, Modules: map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{
"modules/avahi": {Tree: "tree-avahi", Manifest: meshcatalogclient.ManifestDigest([]byte(avahiOnTools(t))), Locks: map[string]string{"go.sum": avahiGoSum}}}}
}
func applyPreview(t *testing.T, ix *Indexer, pv meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue) {
t.Helper()
key, err := PreviewKey(pv.Repository, pv.Head, pv.Trunk)
if err != nil {
t.Fatal(err)
}
data, err := EncodePreview(pv)
if err != nil {
t.Fatal(err)
}
if err := ix.ApplyPreview(context.Background(), key, data, false); err != nil {
t.Fatal(err)
}
}
func fpOf(t *testing.T, st *memStore) meshcatalog.CandidatesByGroupValuePullsItemFpsValue {
t.Helper()
c := candidateOf(t, st)
if len(c.Pulls) != 1 {
t.Fatalf("candidate = %+v", c)
}
return c.Pulls[0].Fps["avahi"]
}
func TestACandidatesModuleGetsTheFingerprintATrunkModuleOfTheSameTreeHas(t *testing.T) { // 1445/AC2
ix, mods, cands := previewBed(t, true)
trunk, _ := moduleOf(t, mods, "avahi")
if trunk.Fp2 == "" {
t.Fatalf("the trunk's avahi has no fingerprint of the new form: %+v", trunk)
}
if err := ix.Apply(context.Background(), avahiPull(t, h1)); err != nil {
t.Fatal(err)
}
if got := fpOf(t, cands); got.None2 != meshcatalogclient.NoneNoPreview {
t.Fatalf("before the preview: %+v", got)
}
applyPreview(t, ix, avahiPreview(t, trunkNow, h1))
got := fpOf(t, cands)
if got.Fp2 != trunk.Fp2 || got.None2 != "" || got.Manifest != meshcatalogclient.ManifestDigest([]byte(avahiOnTools(t))) {
t.Fatalf("candidate avahi = %+v, want the trunk's fingerprint %s", got, trunk.Fp2)
}
c := candidateOf(t, cands)
if c.Fpset != meshcatalogclient.FingerprintSet(c.Pulls) || c.Fpset == "" {
t.Errorf("fpset = %q", c.Fpset)
}
// The candidate names the source revision the preview was read onto (5, whose catalogue head is the preview's trunk):
// the builder merges the preview onto that head, never onto the module value's commit.
if c.Base != 5 {
t.Errorf("candidate base = %d, want the source revision 5 its preview was read onto", c.Base)
}
written(t, writtenCandidate{Source: sourceRev{Rev: 5, Heads: map[string]string{catalogueRepo: trunkNow}}, Candidate: c})
// Issue 1921: a candidate's build is asked every member of the group at its head, also one that publishes no package yet
// and one the module names no package of; the fingerprint stays the trunk's, so the build is the one the merge keeps.
for _, tc := range []struct {
name string
members map[string]string // the other pull requests of the group: repository, head
want sdkstep.PackageVersions
}{
{"a group of one names nothing", nil, nil},
{"a member that publishes no package yet", map[string]string{"novox/mesh-contracts": h2},
sdkstep.PackageVersions{"git.novox.be/novox/mesh-contracts": h2}},
{"mesh-sdk by its go module", map[string]string{"novox/mesh-sdk": h2, "novox/mesh-contracts": h3},
sdkstep.PackageVersions{"git.novox.be/novox/mesh-sdk/go": h2, "git.novox.be/novox/mesh-contracts": h3}},
} {
ix, mods, cands := previewBed(t, true)
trunk, _ := moduleOf(t, mods, "avahi")
if err := ix.Apply(context.Background(), avahiPull(t, h1)); err != nil {
t.Fatal(err)
}
for repository, head := range tc.members {
other := forgemsg.Forge{V: forgemsg.Version, Kind: forgemsg.ForgePull, Repository: repository, Branch: "feat/x", Number: 7, Head: head,
Action: forgemsg.PullUpdated}
if err := ix.Apply(context.Background(), other); err != nil {
t.Fatal(err)
}
}
applyPreview(t, ix, avahiPreview(t, trunkNow, h1))
var got meshcatalog.CandidatesByGroupValuePullsItemFpsValue
for _, p := range candidateOf(t, cands).Pulls {
if p.Repository == catalogueRepo {
got = p.Fps["avahi"]
}
}
if got.Fp2 != trunk.Fp2 || !maps.Equal(got.Packages, tc.want) {
t.Errorf("%s: candidate avahi = %+v, want the trunk's fingerprint %s and the packages %v", tc.name, got, trunk.Fp2, tc.want)
}
}
// Another tree gives another fingerprint.
pv := avahiPreview(t, trunkNow, h1)
m := pv.Modules["modules/avahi"]
m.Tree = "tree-changed"
pv.Modules["modules/avahi"] = m
pv.Merge = "7777777777777777777777777777777777777777"
applyPreview(t, ix, pv)
if other := fpOf(t, cands); other.Fp2 == "" || other.Fp2 == trunk.Fp2 {
t.Errorf("a changed tree: %+v", other)
}
// Issue 1585 (ADR 0573 §2, §4): a candidate's module is built against the head of the candidate's pull request that
// publishes a package it uses, else against the package's latest published version; a preview naming a package of
// mesh-sdk that nothing published gives no fingerprint, and one of its own repository's comes with its tree.
const sdk, own = "git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/mesh-catalog/contract"
held := map[string]meshcatalog.ModulesByModuleValue{"mesh-sdk": {Repository: "novox/mesh-sdk", Path: meshcatalogclient.RootPath, Commit: trunkOld}}
if got := candidatePackages([]string{sdk}, held, map[string]string{catalogueRepo: h1}); got[sdk] != trunkOld {
t.Errorf("outside a group: %v, want mesh-sdk at its trunk %s", got, trunkOld)
}
if got := candidatePackages([]string{sdk}, held, map[string]string{catalogueRepo: h1, "novox/mesh-sdk": mergeSha}); got[sdk] != mergeSha {
t.Errorf("in a group with mesh-sdk: %v, want its member's head %s", got, mergeSha)
}
m.Packages = []string{own, sdk}
pv.Modules["modules/avahi"] = m
pv.Merge = "8888888888888888888888888888888888888888"
applyPreview(t, ix, pv)
if got := fpOf(t, cands); got.Fp2 != "" || got.None2 != fingerprint.NoPackageVersion+sdk {
t.Errorf("a package nothing published: %+v", got)
}
}
func TestAnUnknownToolchainLeavesTheCandidatesModuleNamingIt(t *testing.T) { // 1445/AC2
ix, _, cands := previewBed(t, false)
if err := ix.Apply(context.Background(), avahiPull(t, h1)); err != nil {
t.Fatal(err)
}
applyPreview(t, ix, avahiPreview(t, trunkNow, h1))
if got := fpOf(t, cands); got.Fp2 != "" || !strings.Contains(got.None2, sdkstep.ToolchainNoReport) {
t.Errorf("candidate avahi = %+v, want none2 naming %q", got, sdkstep.ToolchainNoReport)
}
}
func TestAPreviewMadeOntoAnOldTrunkIsOutOfDate(t *testing.T) { // 1445/AC3
ix, _, cands := previewBed(t, true)
if err := ix.Apply(context.Background(), avahiPull(t, h1)); err != nil {
t.Fatal(err)
}
applyPreview(t, ix, avahiPreview(t, trunkOld, h1))
if got := fpOf(t, cands); got.Fp2 != "" || got.None2 != NonePreviewStale {
t.Errorf("candidate avahi = %+v, want it out of date", got)
}
// A preview onto the trunk head now makes it good again, and the trunk moving again makes it stale.
applyPreview(t, ix, avahiPreview(t, trunkNow, h1))
if got := fpOf(t, cands); got.Fp2 == "" {
t.Fatalf("after the right preview: %+v", got)
}
if err := applyS(ix, context.Background(), 6, srcT{Group: "g", Heads: map[string]string{catalogueRepo: "8888888888888888888888888888888888888888"}}); err != nil {
t.Fatal(err)
}
if got := fpOf(t, cands); got.None2 != NonePreviewStale {
t.Errorf("after the trunk moved: %+v", got)
}
}
func TestAPreviewHeardBeforeItsPullRequestGivesTheFingerprintWhenTheCandidateIsWritten(t *testing.T) { // 1445/AC2
ix, mods, cands := previewBed(t, true)
applyPreview(t, ix, avahiPreview(t, trunkNow, h1))
if err := ix.Apply(context.Background(), avahiPull(t, h1)); err != nil {
t.Fatal(err)
}
trunk, _ := moduleOf(t, mods, "avahi")
if got := fpOf(t, cands); got.Fp2 != trunk.Fp2 {
t.Errorf("candidate avahi = %+v, want %s", got, trunk.Fp2)
}
// The same pull request at a new head has no preview of it yet.
if err := ix.Apply(context.Background(), avahiPull(t, h2)); err != nil {
t.Fatal(err)
}
if got := fpOf(t, cands); got.Fp2 != "" || got.None2 != meshcatalogclient.NoneNoPreview {
t.Errorf("a new head: %+v, want no preview", got)
}
// Issue 1921: a head whose tree lacks the directory asked is asked once, however many passes read it, until the head changes;
// any other failure (the forge does not answer) is asked again at every pass and said once per head.
for _, row := range []struct {
name string
err error
askedAfterTwo int // the same head read twice
askedAfterNew int // then another head
linesAfterNew int
}{
{"the directory is not in the tree", fmt.Errorf("listing: %w", ErrNotInTree), 1, 2, 2},
{"the forge does not answer", errors.New("the forge does not answer"), 2, 3, 2},
} {
t.Run("a failing head: "+row.name, func(t *testing.T) {
ix, _, _ := previewBed(t, false)
var logs strings.Builder
ix.Logf = func(format string, args ...any) { logs.WriteString(fmt.Sprintf(format, args...) + "\n") }
asked := 0
ix.Files = func(context.Context, string, string, []string) (map[string]map[string]string, error) {
asked++
return nil, row.err
}
held := map[string]meshcatalog.ModulesByModuleValue{"x": {Inputs2: &meshcatalog.ModulesByModuleValueInputs2{Packages: []string{"git.novox.be/novox/mesh-sdk/go"}}}}
for pass, head := range []string{h1, h1, h2} {
ix.candidatePublished(context.Background(), map[string]string{"novox/mesh-sdk": head}, held)
if pass == 1 && (asked != row.askedAfterTwo || strings.Count(logs.String(), "its packages are not read") != 1) {
t.Fatalf("after two passes over %s: asked %d (want %d), journal %q: want one line", head, asked, row.askedAfterTwo, logs.String())
}
}
if asked != row.askedAfterNew || strings.Count(logs.String(), "its packages are not read") != row.linesAfterNew {
t.Errorf("asked %d (want %d), journal %q (want %d lines)", asked, row.askedAfterNew, logs.String(), row.linesAfterNew)
}
})
}
}
func TestAPreviewNamingNoManifestOfTheModulesDirectoryGivesAReason(t *testing.T) {
t.Run("the same preview again", testTheSamePreviewAppliedAgainWritesNothing)
ix, _, cands := previewBed(t, true)
if err := ix.Apply(context.Background(), avahiPull(t, h1)); err != nil {
t.Fatal(err)
}
pv := avahiPreview(t, trunkNow, h1)
pv.Modules = map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{"modules/other": pv.Modules["modules/avahi"]}
applyPreview(t, ix, pv)
if got := fpOf(t, cands); got.None2 != NonePreviewNoModule || !strings.Contains(NonePreviewNoModule, "manifest") {
t.Errorf("candidate avahi = %+v", got)
}
}
func testTheSamePreviewAppliedAgainWritesNothing(t *testing.T) {
ix, _, cands := previewBed(t, true)
if err := ix.Apply(context.Background(), avahiPull(t, h1)); err != nil {
t.Fatal(err)
}
applyPreview(t, ix, avahiPreview(t, trunkNow, h1))
w := cands.writes
applyPreview(t, ix, avahiPreview(t, trunkNow, h1))
if cands.writes != w {
t.Errorf("candidates written %d times for the same preview, want none", cands.writes-w)
}
}
func TestAPreviewOfConflictsGivesTheCandidatesModuleTheReasonItDoesNotMerge(t *testing.T) { // 1445/AC5
ix, _, cands := previewBed(t, true)
if err := ix.Apply(context.Background(), avahiPull(t, h1)); err != nil {
t.Fatal(err)
}
applyPreview(t, ix, meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: catalogueRepo, Head: h1, Trunk: trunkNow, Conflicts: []string{"modules/avahi/main.go", "go.mod"}})
got := fpOf(t, cands)
if got.Fp2 != "" || !strings.HasPrefix(got.None2, "does not merge") || !strings.Contains(got.None2, "go.mod, modules/avahi/main.go") && !strings.Contains(got.None2, "modules/avahi/main.go, go.mod") {
t.Errorf("candidate avahi = %+v, want it not merging and naming the files", got)
}
}
// writtenCandidate is what the builder's tests read of the indexer (mesh-delivery-builder internal/build): a candidate as the
// indexer writes it and the gate's source revision it names, kept in testdata/candidate-written.json so the builder is tested
// on what the indexer writes, never on a candidate made by hand. MESH_WRITE_TESTDATA=1 writes it again.
type writtenCandidate struct {
Source sourceRev `json:"source"`
Candidate meshcatalog.CandidatesByGroupValue `json:"candidate"`
}
type sourceRev struct {
Rev uint64 `json:"rev"`
Heads map[string]string `json:"heads"`
}
func written(t *testing.T, w writtenCandidate) {
t.Helper()
data, err := json.MarshalIndent(w, "", " ")
if err != nil {
t.Fatal(err)
}
data = append(data, '\n')
const file = "testdata/candidate-written.json"
if os.Getenv("MESH_WRITE_TESTDATA") == "1" {
if err := os.WriteFile(file, data, 0o644); err != nil {
t.Fatal(err)
}
}
kept, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
if string(kept) != string(data) {
t.Errorf("%s is not what the indexer writes now (MESH_WRITE_TESTDATA=1 writes it again):\n%s", file, data)
}
}