Prove against the bus that a mesh-sdk merge moves only importers of a changed package
The indexer whole: two catalogue modules report their imports at their builds' end, mesh-sdk merges a change of buildstep alone, the packages key names the merge with a digest per package, the buildstep importer moves and the other keeps its fingerprint though the version line would have moved it (ADR 0593 §2-§4; D11). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -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() })
|
||||
}
|
||||
Reference in New Issue
Block a user