519 lines
25 KiB
Go
519 lines
25 KiB
Go
package index
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
|
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-checker/checkermsg"
|
|
"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 := ix.ApplySource(context.Background(), 5, meshdelivery.SourceByMainValue{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})
|
|
// 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 := ix.ApplySource(context.Background(), 6, meshdelivery.SourceByMainValue{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 whole group try has no pull request, yet the indexer writes its candidate from the checker's try events and
|
|
// the previews its fetch wrote (number 0, the heads hash of its members), keeps it apart from the pull requests' candidates,
|
|
// and deletes it when the try ends. Its manifests are judged with the seat definitions its members carry, in one locked
|
|
// pass, whether the contracts module's manifest changed or only its seat.json did (the wait is bounded: a lock held twice
|
|
// would show as a failure here, not as a stuck run).
|
|
for _, row := range []struct {
|
|
name string
|
|
unchanged bool // the contracts module's module.json is the trunk's: only its definitions changed
|
|
}{
|
|
{name: "a changed manifest that carries definitions"},
|
|
{name: "an unchanged manifest that carries definitions", unchanged: true},
|
|
} {
|
|
t.Run("a group try's candidate: "+row.name, func(t *testing.T) {
|
|
ix, mods, cands := previewBed(t, true)
|
|
ctx := context.Background()
|
|
ix.Verdicts = newMem()
|
|
var changed map[string]any
|
|
if err := json.Unmarshal([]byte(catalogueManifest(t, "avahi")), &changed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
changed["build"] = map[string]any{"artifacts": []any{map[string]any{"name": "go-seats", "kind": "package", "language": "go", "paths": []any{"go"}, "generated-from": "defs"}}}
|
|
raw, _ := json.Marshal(changed)
|
|
pv := avahiPreview(t, trunkNow, h1)
|
|
pm := meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{Tree: "tree-avahi", Manifest: meshcatalogclient.ManifestDigest(raw), Locks: map[string]string{"go.sum": avahiGoSum}}
|
|
if row.unchanged {
|
|
m, found := moduleOf(t, mods, "avahi")
|
|
_ = found
|
|
pm.Manifest = m.Manifest
|
|
key, _ := meshcatalogclient.ManifestsKey(m.Manifest)
|
|
data, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "avahi", Manifest: string(raw)})
|
|
if _, _, _, err := ix.Manifests.Get(ctx, key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = ix.Manifests.(*memStore).Update(ctx, key, data, ix.Manifests.(*memStore).revs[key])
|
|
}
|
|
pv.Modules["modules/avahi"] = pm
|
|
ix.Read = func(_ context.Context, _, _, dir string) (RepoRead, error) {
|
|
return RepoRead{Manifests: map[string]string{dir: string(raw)}}, nil
|
|
}
|
|
listed := 0
|
|
ix.Files = func(_ context.Context, _, commit string, dirs []string) (map[string]map[string]string, error) {
|
|
listed++
|
|
if commit != h1 || !slices.Equal(dirs, []string{"modules/avahi/defs/seats"}) {
|
|
t.Errorf("listed %v at %s, want the member's head", dirs, commit)
|
|
}
|
|
return map[string]map[string]string{dirs[0]: {"node-engine/seat.json": "1"}}, nil
|
|
}
|
|
ix.ReadFile = func(_ context.Context, _, _, p string) ([]byte, error) {
|
|
switch p {
|
|
case "modules/avahi/defs/seats/node-engine/seat.json":
|
|
return []byte(`{"format":1,"seat":"node-engine","scope":"node","grain":"seat-list","decision":"d","domain":"Core","verbs":[{"name":"try-verb"}]}`), nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
started := `{"id":"try-7"}`
|
|
_ = started
|
|
members := []TryMember{{Repository: catalogueRepo, Head: h1, Trunk: trunkNow}}
|
|
held := func() []string { k, _ := cands.Keys(ctx); return k }
|
|
if err := ix.StartTry(ctx, "try-7", members); err != nil || len(held()) != 0 {
|
|
t.Fatalf("a try before its preview: %v, candidates %v", err, held())
|
|
}
|
|
done := make(chan struct{})
|
|
go func() { defer close(done); applyPreview(t, ix, pv) }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(10 * time.Second):
|
|
panic("writing the try's candidate hangs: the seat lock is held twice (a locked pass cannot be left by t.Fatal: the lock stays held)")
|
|
}
|
|
if listed == 0 {
|
|
t.Error("the member's definitions were not read")
|
|
}
|
|
key, _ := meshcatalogclient.CandidatesKey("try-7")
|
|
rawc, _, found, _ := cands.Get(ctx, key)
|
|
if !found {
|
|
t.Fatalf("no candidate for the try after its preview: %v", held())
|
|
}
|
|
c, err := DecodeCandidate(rawc)
|
|
pull := meshcatalog.CandidatesByGroupValuePullsItem{}
|
|
if len(c.Pulls) == 1 {
|
|
pull = c.Pulls[0]
|
|
}
|
|
if row.unchanged {
|
|
if err != nil || c.Group != "try-7" || len(pull.Modules) != 0 {
|
|
t.Fatalf("candidate = %+v, %v, want the try's group with no module touched", c, err)
|
|
}
|
|
} else if err != nil || c.Group != "try-7" || pull.Number != 0 || pull.Head != h1 || pull.Fps["avahi"].Manifest == "" { // no fingerprint asserted: this mesh-sdk pin does not read generated-from yet
|
|
t.Fatalf("candidate = %+v, %v, want one pull of number 0 with avahi's manifest", c, err)
|
|
}
|
|
if err := ix.Apply(ctx, forgemsg.Forge{Kind: forgemsg.ForgeOpen}); err != nil || len(held()) != 1 {
|
|
t.Errorf("the open message left %v (%v), want the try's candidate alone", held(), err)
|
|
}
|
|
// The tools: one reads the candidates held for tries, one drops by an ask the indexer answers.
|
|
asks := func(key string, value []byte) (TakeInAsk, error) {
|
|
a, err := DecodeTakeIn(value)
|
|
r := ix.DropTryCandidate(ctx, a.TryCandidate, a.By, a.Why)
|
|
a.Result = &r
|
|
return a, err
|
|
}
|
|
recovery := newMem()
|
|
ix.TryRecovery = recovery
|
|
tools := TryCandidateTools(cands, recovery, asks, time.Now)
|
|
if out, err := tools[0].Run(map[string]any{}); err != nil || len(out.(map[string]any)["tries"].([]meshcatalog.CandidatesByGroupValue)) != 1 {
|
|
t.Errorf("%s = %v, %v, want the try's candidate", tools[0].Name, out, err)
|
|
}
|
|
if _, err := tools[2].Run(map[string]any{"try": "feat/x", "why": "w"}); err == nil {
|
|
t.Error("a branch was dropped as a try")
|
|
}
|
|
if out, err := tools[2].Run(map[string]any{"try": "try-7", "why": "by hand"}); err != nil || !out.(*TakenIn).Stored || len(held()) != 0 {
|
|
t.Errorf("drop = %v, %v, candidates %v", out, err, held())
|
|
}
|
|
// The start recovered from the checker's tool: an open whole try is written again, one the checker no longer
|
|
// lists is deleted, and a try narrowed to a level is none.
|
|
ix.OpenTries = func(context.Context) ([]checkermsg.Open, error) {
|
|
return []checkermsg.Open{{ID: "try-7", Whole: true, Members: []checkermsg.Member{{Repository: catalogueRepo, Head: h1, Trunk: trunkNow}}},
|
|
{ID: "try-8", Whole: false, Members: []checkermsg.Member{{Repository: catalogueRepo, Head: h2, Trunk: trunkNow}}}}, nil
|
|
}
|
|
if err := ix.RecoverTries(ctx); err != nil || len(held()) != 1 {
|
|
t.Fatalf("recovered: %v, candidates %v, want try-7's alone", err, held())
|
|
}
|
|
ix.OpenTries = func(context.Context) ([]checkermsg.Open, error) { return nil, nil }
|
|
if err := ix.RecoverTries(ctx); err != nil || len(held()) != 0 {
|
|
t.Errorf("recovered with no open try: %v, candidates %v, want none", err, held())
|
|
}
|
|
// A recovery whose call fails is loud and deletes nothing: the candidate stays, indexer_try_candidates says recovery not
|
|
// done since when and why, and the next try event runs the recovery again, which clears it; the tool runs it by hand.
|
|
if err := ix.StartTry(ctx, "try-7", members); err != nil || len(held()) != 1 {
|
|
t.Fatalf("started: %v, candidates %v", err, held())
|
|
}
|
|
ix.OpenTries = func(context.Context) ([]checkermsg.Open, error) {
|
|
return nil, errors.New("checker_tries does not answer")
|
|
}
|
|
if err := ix.RecoverTries(ctx); err == nil || len(held()) != 1 {
|
|
t.Fatalf("a failed call: %v, candidates %v, want the error and the candidate kept", err, held())
|
|
}
|
|
if out, err := tools[0].Run(map[string]any{}); err != nil || !strings.Contains(out.(map[string]any)["recovery"].(map[string]any)["says"].(string), "recovery not done, since") ||
|
|
!strings.Contains(out.(map[string]any)["recovery"].(map[string]any)["says"].(string), "checker_tries does not answer") {
|
|
t.Errorf("%s = %v, %v, want recovery not done with since and why", tools[0].Name, out, err)
|
|
}
|
|
ix.OpenTries = func(context.Context) ([]checkermsg.Open, error) { return nil, nil }
|
|
if err := ix.StartTry(ctx, "try-11", members); err != nil || len(held()) != 1 || held()[0] != "try-11" {
|
|
t.Fatalf("a try event after a failed recovery: %v, candidates %v, want the recovery run (try-7 not listed, gone) and try-11 written", err, held())
|
|
}
|
|
if out, _ := tools[0].Run(map[string]any{}); out.(map[string]any)["recovery"].(map[string]any)["done"] != true {
|
|
t.Errorf("recovery still said not done after a success: %v", out)
|
|
}
|
|
ix.OpenTries = func(context.Context) ([]checkermsg.Open, error) { return nil, errors.New("down") }
|
|
recoverAsk := func(key string, value []byte) (TakeInAsk, error) {
|
|
a, err := DecodeTakeIn(value)
|
|
r := ix.RecoverTriesByHand(ctx, a.By, a.Why)
|
|
a.Result = &r
|
|
return a, err
|
|
}
|
|
byHand := TryCandidateTools(cands, recovery, recoverAsk, time.Now)
|
|
if out, err := byHand[1].Run(map[string]any{"why": "the checker was replaced"}); err != nil || out.(*TakenIn).Stored || !strings.Contains(out.(*TakenIn).Answer, "recovery not done") {
|
|
t.Errorf("%s with the checker down = %v, %v", byHand[1].Name, out, err)
|
|
}
|
|
ix.OpenTries = func(context.Context) ([]checkermsg.Open, error) { return nil, nil }
|
|
if out, err := byHand[1].Run(map[string]any{"why": "the checker is back"}); err != nil || !out.(*TakenIn).Stored || len(held()) != 0 {
|
|
t.Errorf("%s = %v, %v, candidates %v, want it recovered (try-11 not listed)", byHand[1].Name, out, err, held())
|
|
}
|
|
if err := ix.StartTry(ctx, "try-7", members); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// ADR 0638 §3, §4: the consumer exists before the call, so what happens between the call and the hearing is heard after
|
|
// it. A try the call listed and whose start is then heard gets one candidate; a try started after the list gets one; a
|
|
// try that ended during the call, listed as open, leaves none once its end is heard.
|
|
ix.OpenTries = func(context.Context) ([]checkermsg.Open, error) {
|
|
return []checkermsg.Open{{ID: "try-7", Whole: true, Members: []checkermsg.Member{{Repository: catalogueRepo, Head: h1, Trunk: trunkNow}}},
|
|
{ID: "try-9", Whole: true, Members: []checkermsg.Member{{Repository: catalogueRepo, Head: h1, Trunk: trunkNow}}}}, nil
|
|
}
|
|
if err := ix.RecoverTries(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, step := range []struct {
|
|
what string
|
|
do func() error
|
|
want int
|
|
}{
|
|
{"the start heard after the call, of a try the call listed", func() error { return ix.StartTry(ctx, "try-7", members) }, 2},
|
|
{"a start after the list", func() error { return ix.StartTry(ctx, "try-10", members) }, 3},
|
|
{"the end heard of a try ended during the call", func() error { return ix.EndTry(ctx, "try-9") }, 2},
|
|
} {
|
|
if err := step.do(); err != nil || len(held()) != step.want {
|
|
t.Fatalf("%s: %v, candidates %v, want %d", step.what, err, held(), step.want)
|
|
}
|
|
}
|
|
for _, id := range []string{"try-10"} {
|
|
if err := ix.EndTry(ctx, id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := ix.StartTry(ctx, "try-7", members); err != nil || len(held()) != 1 {
|
|
t.Fatalf("started again: %v, candidates %v", err, held())
|
|
}
|
|
if err := ix.EndTry(ctx, "try-7"); err != nil || len(held()) != 0 {
|
|
t.Errorf("an ended try: %v, candidates %v, want none", err, held())
|
|
}
|
|
// The hand recovery never overlaps a try event: a start heard while the call to the checker is out writes its candidate
|
|
// before or after the sweep, never between the call and the sweep (which would delete it, the try not being listed).
|
|
startDone := make(chan error, 1)
|
|
ix.OpenTries = func(context.Context) ([]checkermsg.Open, error) {
|
|
go func() { startDone <- ix.StartTry(ctx, "try-12", members) }()
|
|
for i := 0; i < 2000 && len(held()) == 0; i++ {
|
|
runtime.Gosched() // lets the start write if nothing holds it back
|
|
}
|
|
return nil, nil
|
|
}
|
|
if r := ix.RecoverTriesByHand(ctx, "lead", "a test"); !r.Stored {
|
|
t.Fatalf("the hand recovery = %+v", r)
|
|
}
|
|
if err := <-startDone; err != nil || len(held()) != 1 || held()[0] != "try-12" {
|
|
t.Errorf("a start heard during the hand recovery: %v, candidates %v, want try-12 kept", err, held())
|
|
}
|
|
})
|
|
}
|
|
// 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)
|
|
}
|
|
}
|