diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go new file mode 100644 index 0000000..2829604 --- /dev/null +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go @@ -0,0 +1,212 @@ +//go:build integration + +package main + +import ( + "context" + "encoding/json" + "os" + "strings" + "testing" + "time" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-sdk/go/buscompose" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage (ADR 0593 §2-§4; D11): the indexer whole against the +// bus. Two Go modules of the catalogue whose go.sum names mesh-sdk; their builds end reporting their imports (importer: +// buildstep; plain: announce); then mesh-sdk merges a change of buildstep alone, which moves mesh-sdk's version. Within +// 5 s the packages key of mesh-sdk/go names the merge with a digest per package, importer gets a new fingerprint, and +// plain keeps the one it had, though the version line would have moved it. The forge's tools answer on the bus as the +// gitea module's do. +func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T) { + const ( + sdkGo = "git.novox.be/novox/mesh-sdk/go" + m1, m2 = "1111111111111111111111111111111111111111", "2222222222222222222222222222222222222222" + catalogMerge = "3333333333333333333333333333333333333333" + goSumContent = "git.novox.be/novox/mesh-sdk/go v0.1.14 h1:x=\ngit.novox.be/novox/mesh-sdk/go v0.1.14/go.mod h1:y=\n" + goSumHash = "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381" + sdkRoot1, sdk2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ) + avahi, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json") + if err != nil { + t.Fatal(err) + } + sdkManifest, err := os.ReadFile("../../internal/index/testdata/sdk-9cb4d59-mesh-sdk.module.json") + if err != nil { + t.Fatal(err) + } + named := func(module string) string { + return strings.Replace(string(avahi), `"module": "avahi"`, `"module": "`+module+`"`, 1) + } + // mesh-sdk's go directory at each merge: m2 changes buildstep alone. + listing := map[string]map[string]string{ + m1: {"go.mod": "f1", "go.sum": "f2", "stdio.go": "f3", "announce/announce.go": "f4", "buildstep/step.go": "f5"}, + m2: {"go.mod": "f1", "go.sum": "f2", "stdio.go": "f3", "announce/announce.go": "f4", "buildstep/step.go": "f6"}, + } + var packages jetstream.KeyValue + js, ctx, _, modules := startIndexer(t, func(context.Context, buildLog) {}, func(ctx context.Context, js jetstream.JetStream) { + var err error + if packages, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.PackagesBucket, History: delivery.PackagesHistory}); err != nil { + t.Fatal(err) + } + if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.TakenBucket, History: 1}); err != nil { + t.Fatal(err) + } + // Both languages' toolchains, so mesh-sdk (a TypeScript package) gets a fingerprint that moves with its source. + for module, language := range map[string]string{"mesh-build-go": "go", "mesh-build-node": "typescript"} { + tc, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ToolchainsBucket(module), History: 1}) + if err != nil { + t.Fatal(err) + } + key, _ := delivery.ToolchainKey("n1", language) + value, _ := delivery.EncodeToolchain(delivery.Toolchain{Language: language, Version: "v1", Recipe: "1", At: time.Now()}) + if _, err := tc.Put(ctx, key, value); err != nil { + t.Fatal(err) + } + } + serveForge(t, js.Conn(), listing, goSumContent) + }) + merged := func(repo string, number int, merge string, f delivery.Forge) { + t.Helper() + f.Kind, f.Repository, f.Branch, f.Number, f.Head, f.Merge = delivery.ForgeMerged, repo, "feat/x", number, "0123456789abcdef0123456789abcdef01234567", merge + data, err := delivery.EncodeForge(f) + if err != nil { + t.Fatal(err) + } + if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge."+strings.ReplaceAll(repo, "/", ".")+".merged", data); err != nil { + t.Fatal(err) + } + } + module := func(name string, ok func(delivery.Module) bool) delivery.Module { + t.Helper() + var m delivery.Module + for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { + if e, err := modules.Get(ctx, name); err == nil { + if m, err = delivery.DecodeModule(e.Value()); err == nil && ok(m) { + return m + } + } + } + t.Fatalf("modules.%s = %+v within 5 s", name, m) + return m + } + sdkObjects := func(src string) map[string]map[string]string { + return map[string]map[string]string{"": {"src": src, "test": "2222222222222222222222222222222222222222", + "package.json": "3333333333333333333333333333333333333333", "package-lock.json": "4444444444444444444444444444444444444444", + "tsconfig.json": "5555555555555555555555555555555555555555"}} + } + // mesh-sdk at m1, then the catalogue's two modules: their go.sum names mesh-sdk, read by the indexer itself. + merged("novox/mesh-sdk", 70, m1, delivery.Forge{Root: sdkRoot1, Trees: map[string]string{"": sdkRoot1}, + Manifests: map[string]string{"": string(sdkManifest)}, Paths: sdkObjects("6666666666666666666666666666666666666666")}) + module("mesh-sdk", func(m delivery.Module) bool { return m.Commit == m1 }) + merged("novox/mesh-catalog", 301, catalogMerge, delivery.Forge{Root: "r0", + Trees: map[string]string{"modules/importer": "t1", "modules/plain": "t2"}, + Manifests: map[string]string{"modules/importer": named("importer"), "modules/plain": named("plain")}, + Locks: map[string]map[string]string{"modules/importer": {"go.sum": goSumHash}, "modules/plain": {"go.sum": goSumHash}}}) + // The toolchain is reported, and each module gets its fingerprint of the version line. + for _, l := range []string{"go", "node"} { + if _, err := js.Publish(ctx, "mesh.stream.mesh-build-"+l+".event.toolchain.reported", []byte(`{"node":"n1","language":"`+l+`"}`)); err != nil { + t.Fatal(err) + } + } + hasFP := func(m delivery.Module) bool { return m.FP2 != "" && m.Packages[sdkGo] == m1 } + importer, plain := module("importer", hasFP), module("plain", hasFP) + + // Each build ends reporting its imports: kept, no fingerprint moving. + for _, b := range []struct { + m delivery.Module + name string + imports []string + }{{importer, "importer", []string{sdkGo, sdkGo + "/buildstep"}}, {plain, "plain", []string{sdkGo, sdkGo + "/announce"}}} { + raw, err := delivery.EncodeBuildLogEntry(delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: b.name, FP: b.m.FP2, + Repository: "novox/mesh-catalog", Directory: "modules/" + b.name, Commit: catalogMerge, Ref: "main", Node: "n1", + At: time.Now().UTC(), Outcome: delivery.BuildDone, Imports: b.imports}) + if err != nil { + t.Fatal(err) + } + if _, err := js.Publish(ctx, buscompose.LogSubject(delivery.BuilderModule, delivery.BuilderLog)+"."+b.name, raw); err != nil { + t.Fatal(err) + } + fp := b.m.FP2 + module(b.name, func(m delivery.Module) bool { return m.Inputs2 != nil && m.Inputs2.ImportForm != "" && m.FP2 == fp }) + } + + // mesh-sdk merges a change of buildstep alone (its version moves: src changed too). + merged("novox/mesh-sdk", 71, m2, delivery.Forge{Root: sdk2, Trees: map[string]string{"": sdk2}, + Manifests: map[string]string{"": string(sdkManifest)}, Paths: sdkObjects("7777777777777777777777777777777777777777")}) + module("mesh-sdk", func(m delivery.Module) bool { return m.Commit == m2 }) + key, _ := delivery.PackagesKey(sdkGo) + var pub delivery.PublishedPackages + for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { + if e, err := packages.Get(ctx, key); err == nil { + if pub, err = delivery.DecodePublishedPackages(e.Value()); err == nil && pub.Commit == m2 { + break + } + } + } + if pub.Commit != m2 || len(pub.Packages) != 3 { + t.Fatalf("packages.%s = %+v within 5 s, want the merge %s with a digest of go, announce and buildstep", key, pub, m2) + } + moved := module("importer", func(m delivery.Module) bool { return m.FP2 != importer.FP2 }) + if moved.Packages[sdkGo] != m2 { + t.Errorf("importer moved to %+v, want it built against %s", moved.Packages, m2) + } + // plain imports nothing that changed: its value keeps its fingerprint (one recompute has run: importer moved in it). + if now := module("plain", func(delivery.Module) bool { return true }); now.FP2 != plain.FP2 { + t.Errorf("plain moved from %s to %s, though it imports nothing the merge changed", plain.FP2, now.FP2) + } +} + +// serveForge answers the forge's tools on the bus as the gitea module does: gitea_commit_trees (the trees of a commit, +// or with files the blobs under each directory of mesh-sdk's listing at that commit), gitea_get_file (every go.sum +// holds goSum) and gitea_contains. +func serveForge(t *testing.T, nc *nats.Conn, listing map[string]map[string]string, goSum string) { + t.Helper() + reply := func(m *nats.Msg, result any) { + raw, _ := json.Marshal(map[string]any{"result": result, "node": "n0"}) + _ = m.Respond(raw) + } + trees, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_commit_trees", func(m *nats.Msg) { + var a struct { + Repo string `json:"repo"` + Sha string `json:"sha"` + Files []string `json:"files"` + } + _ = json.Unmarshal(m.Data, &a) + out := map[string]any{"root": "r0", "trees": map[string]string{}} + if len(a.Files) > 0 { + files := map[string]map[string]string{} + for _, d := range a.Files { + if a.Repo == "mesh-sdk" && d == "go" && listing[a.Sha] != nil { + files[d] = listing[a.Sha] + } + } + out["files"] = files + } + reply(m, out) + }) + if err != nil { + t.Fatal(err) + } + file, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_get_file", func(m *nats.Msg) { + reply(m, map[string]any{"file": map[string]any{"content": goSum}}) + }) + if err != nil { + t.Fatal(err) + } + // Each merge here is newer than the one before: a commit is in another only when it is that commit. + contains, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_contains", func(m *nats.Msg) { + var a struct{ Commit, In string } + _ = json.Unmarshal(m.Data, &a) + reply(m, map[string]any{"contains": a.Commit == a.In}) + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = trees.Unsubscribe(); _ = file.Unsubscribe(); _ = contains.Unsubscribe() }) +}