Files
mesh-delivery/modules/mesh-delivery-indexer/internal/index/toolchains_test.go
T

269 lines
12 KiB
Go

package index
import (
"context"
"reflect"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The fixtures here are mesh-catalog a67f0196's manifests (testdata, captured from that commit) with the module renamed (and,
// where a test needs one, a build.on entry added as the controller's own test has it, standing_on_test.go:22): the manifest
// parser, not a hand-written manifest, reads them.
func renamed(t *testing.T, from, to string) string {
t.Helper()
raw := catalogueManifest(t, from)
out := strings.Replace(raw, `"module": "`+from+`"`, `"module": "`+to+`"`, 1)
if out == raw {
t.Fatalf("%s's manifest has no module line", from)
}
return out
}
func standingOn(t *testing.T, raw, base string) string {
t.Helper()
out := strings.Replace(raw, `"build": {`, `"build": {
"on": [{"arg": "RUNTIME_BASE", "module": "`+base+`", "artifact": "runtime"}],`, 1)
if out == raw {
t.Fatal("the manifest has no build")
}
return out
}
const buildAgentRepo = "novox/mesh-controller"
type mod struct {
name, manifest string
}
// merged applies one merge of the catalogue carrying these modules (every one a Go bundle's go.sum, a TypeScript bundle's
// package-lock.json) and returns the indexer.
func newFormIndexer(t *testing.T, mods ...mod) (*Indexer, *memStore) {
t.Helper()
ix, store, f := newFormIndexerUnmerged(t, mods...)
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
}
// build-agent's value of today's form, from a merge of the controller (modules.build-agent holds its fingerprint).
if err := ix.Apply(context.Background(), mergedMsg(buildAgentRepo, map[string]string{"modules/build-agent": "agent-tree-1"})); err != nil {
t.Fatal(err)
}
return ix, store
}
// newFormIndexerUnmerged is the indexer and the catalogue merge of these modules, not yet applied.
func newFormIndexerUnmerged(t *testing.T, mods ...mod) (*Indexer, *memStore, delivery.Forge) {
t.Helper()
var recs [][2]any
trees, manifests := map[string]string{}, map[string]string{}
locks := map[string]map[string]string{}
for i, m := range mods {
dir := "modules/" + m.name
recs = append(recs, rec(t, "build-"+m.name, m.name, catalogueRepo, dir, "main", "", "2026-10-19T09:00:00Z", aInputs))
trees[dir], manifests[dir] = "tree-"+m.name, m.manifest
locks[dir] = map[string]string{"go.sum": avahiGoSum, "package-lock.json": strings.Repeat("c", 63) + string(rune('a'+i))}
}
recs = append(recs, rec(t, "build-agent-1", "build-agent", buildAgentRepo, "modules/build-agent", "main", "", "2026-10-19T09:00:00Z", aInputs))
ix, store, _ := manifestsIndexer(t, recs...)
return ix, store, withManifests(mergedMsg(catalogueRepo, trees), manifests, locks)
}
// toolchainValue is a toolchains value as the build module's writer encodes it (mesh-sdk go/delivery EncodeToolchain).
func toolchainValue(t *testing.T, language, version, recipe string) []byte {
t.Helper()
b, err := delivery.EncodeToolchain(delivery.Toolchain{Language: language, Version: version, Recipe: recipe, At: time.Now()})
if err != nil {
t.Fatal(err)
}
return b
}
func reportToolchain(t *testing.T, ix *Indexer, module, node, language, version string) {
t.Helper()
key, err := delivery.ToolchainKey(node, language)
if err != nil {
t.Fatal(err)
}
if err := ix.ApplyToolchain(context.Background(), module, key, toolchainValue(t, language, version, "1"), false); err != nil {
t.Fatal(err)
}
}
func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 1444/AC5
ix, mods := newFormIndexer(t, mod{"avahi", catalogueManifest(t, "avahi")})
if m, _ := moduleOf(t, mods, "avahi"); m.None2 != delivery.ToolchainNoReport {
t.Fatalf("before: none2 = %q", m.None2)
}
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.27.1")
m, _ := moduleOf(t, mods, "avahi")
if m.None2 != None2Builder || m.FP2 != "" {
t.Errorf("none2 = %q fp2 = %q, want none2 %q", m.None2, m.FP2, None2Builder)
}
}
func TestNodesReportingDifferentVersionsLeaveTheReasonNamingBoth(t *testing.T) { // 1444/AC6
ix, mods := newFormIndexer(t, mod{"avahi", catalogueManifest(t, "avahi")})
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.26.0")
m, _ := moduleOf(t, mods, "avahi")
if !strings.Contains(m.None2, "builders disagree") || !strings.Contains(m.None2, "go1.27.1") || !strings.Contains(m.None2, "go1.26.0") {
t.Errorf("none2 = %q, want 'builders disagree' naming both versions", m.None2)
}
}
// builderModules are the builder, a base, a module on the base (named so that it is stored before its base) and a module
// whose base is a TypeScript module with no toolchain reported.
func builderModules(t *testing.T) []mod {
return []mod{
{"mesh-delivery-builder", renamed(t, "avahi", "mesh-delivery-builder")},
{"mesh-tools", renamed(t, "avahi", "mesh-tools")},
{"avahi", standingOn(t, catalogueManifest(t, "avahi"), "mesh-tools")},
{"other", standingOn(t, renamed(t, "avahi", "other"), "ts-base")},
{"ts-base", renamed(t, "audit-logger", "ts-base")},
}
}
func goInputs(t *testing.T, ix *Indexer, mods *memStore, module, builder string, bases ...string) delivery.SourceInputs2 {
t.Helper()
return delivery.SourceInputs2{Module: module, Tree: "tree-" + module, Builder: builder, Bases: bases,
Locks: map[string]string{"tools": avahiGoSum},
Toolchains: map[string]delivery.Toolchain2{"tools": {Language: "go", Version: "go1.27.1", Recipe: "1"}}}
}
func fp2(t *testing.T, in delivery.SourceInputs2) string {
t.Helper()
fp, none := delivery.SourceFingerprint2(in)
if none != "" {
t.Fatal(none)
}
return fp
}
func TestBasesAndTheBuilderAreComputedBeforeTheModulesTakingThem(t *testing.T) { // 1444/AC7, AC8
ix, mods := newFormIndexer(t, builderModules(t)...)
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
agent, _ := moduleOf(t, mods, "build-agent")
if agent.FP == "" {
t.Fatalf("build-agent has no fingerprint of today's form: %+v", agent)
}
builderFP := fp2(t, goInputs(t, ix, mods, "mesh-delivery-builder", agent.FP))
toolsFP := fp2(t, goInputs(t, ix, mods, "mesh-tools", builderFP))
want := map[string]string{"mesh-delivery-builder": builderFP, "mesh-tools": toolsFP, "avahi": fp2(t, goInputs(t, ix, mods, "avahi", builderFP, toolsFP))}
for name, fp := range want {
if m, _ := moduleOf(t, mods, name); m.FP2 != fp || m.None2 != "" {
t.Errorf("%s: fp2 = %q none2 = %q, want %s", name, m.FP2, m.None2, fp)
}
}
if m, _ := moduleOf(t, mods, "other"); m.FP2 != "" || !strings.Contains(m.None2, "ts-base") || !strings.Contains(m.None2, "base") {
t.Errorf("other: fp2 = %q none2 = %q, want none2 naming the base ts-base", m.FP2, m.None2)
}
// 1444/AC8: build-agent changes at a merge of the controller; the builder's value moves with it, and so do the modules built by it.
if err := ix.Apply(context.Background(), mergedMsg(buildAgentRepo, map[string]string{"modules/build-agent": "agent-tree-2"})); err != nil {
t.Fatal(err)
}
moved, _ := moduleOf(t, mods, "build-agent")
if moved.FP == agent.FP {
t.Fatal("build-agent's fingerprint did not change")
}
newBuilder := fp2(t, goInputs(t, ix, mods, "mesh-delivery-builder", moved.FP))
if m, _ := moduleOf(t, mods, "mesh-delivery-builder"); m.FP2 != newBuilder || newBuilder == builderFP {
t.Errorf("builder: fp2 = %q, want %s (was %s)", m.FP2, newBuilder, builderFP)
}
if m, _ := moduleOf(t, mods, "mesh-tools"); m.FP2 != fp2(t, goInputs(t, ix, mods, "mesh-tools", newBuilder)) {
t.Errorf("mesh-tools did not follow the builder: %+v", m)
}
}
func TestAToolchainEntryChangedToTheSameValuesWritesNothing(t *testing.T) { // 1444/AC9
ix, mods := newFormIndexer(t, builderModules(t)...)
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
before := mods.writes
time.Sleep(2 * time.Millisecond)
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1") // the same version and recipe, a later time
if mods.writes != before {
t.Errorf("modules written %d times after the same values, want none", mods.writes-before)
}
}
func TestModulesThatStandOnEachOtherAreNamedAsACycle(t *testing.T) { // 1444 (the indexer has no graph: a cycle is a reason)
ix, mods := newFormIndexer(t,
mod{"aa", standingOn(t, renamed(t, "avahi", "aa"), "bb")},
mod{"bb", standingOn(t, renamed(t, "avahi", "bb"), "aa")})
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
for _, name := range []string{"aa", "bb"} {
if m, _ := moduleOf(t, mods, name); m.FP2 != "" || !strings.Contains(m.None2, "cycle") {
t.Errorf("%s: fp2 = %q none2 = %q, want a reason naming the cycle", name, m.FP2, m.None2)
}
}
}
func TestAMergeWritesEachModuleOnceWithItsFinalValueAndAReplayWritesNothing(t *testing.T) { // 1444 (review finding 8)
ctx := context.Background()
ix, mods, f := newFormIndexerUnmerged(t, mod{"avahi", catalogueManifest(t, "avahi")})
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1") // agreed before the merge is heard
if err := ix.Apply(ctx, f); err != nil {
t.Fatal(err)
}
m, _ := moduleOf(t, mods, "avahi")
if mods.writes != 1 || m.None2 != None2Builder {
t.Errorf("writes = %d none2 = %q after one merge, want one write with %q", mods.writes, m.None2, None2Builder)
}
if err := ix.Apply(ctx, f); err != nil {
t.Fatal(err)
}
if mods.writes != 1 {
t.Errorf("writes = %d after the same merge again, want still 1", mods.writes)
}
}
func TestAMergeKeepsAFingerprintTheModuleStillHas(t *testing.T) { // 1444 (review finding 8: a merge never blanks FP2)
ctx := context.Background()
ix, mods := newFormIndexer(t, builderModules(t)...)
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
before, _ := moduleOf(t, mods, "mesh-tools")
if before.FP2 == "" {
t.Fatalf("mesh-tools has no fp2: %+v", before)
}
w := mods.writes
// the same catalogue merged again at a later commit: nothing moved
_, _, f := newFormIndexerUnmerged(t, builderModules(t)...)
if err := ix.Apply(ctx, f); err != nil {
t.Fatal(err)
}
if after, _ := moduleOf(t, mods, "mesh-tools"); after.FP2 != before.FP2 || mods.writes != w {
t.Errorf("fp2 = %q (was %q), writes %d more", after.FP2, before.FP2, mods.writes-w)
}
}
func TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith(t *testing.T) { // 1615/AC3
ix, mods := newFormIndexer(t, builderModules(t)...)
if m, _ := moduleOf(t, mods, "avahi"); m.Toolchains != nil {
t.Fatalf("before a toolchain is reported: toolchains = %+v, want none", m.Toolchains)
}
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
m, _ := moduleOf(t, mods, "avahi")
want := map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}}
if m.FP2 == "" || !reflect.DeepEqual(m.Toolchains, want) {
t.Fatalf("avahi: fp2 %q toolchains %+v, want %+v", m.FP2, m.Toolchains, want)
}
if o, _ := moduleOf(t, mods, "other"); o.FP2 != "" || o.Toolchains != nil {
t.Errorf("a module with no fingerprint carries toolchains %+v", o.Toolchains)
}
// An upgrade moves the fingerprint and the toolchains together, with no merge.
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.2")
n, _ := moduleOf(t, mods, "avahi")
if n.FP2 == m.FP2 || n.Toolchains["go"].Version != "go1.27.2" {
t.Errorf("after the upgrade: fp2 %q (was %q) toolchains %+v, want the new version with the new fingerprint", n.FP2, m.FP2, n.Toolchains)
}
// Nodes that disagree leave no fingerprint and no toolchains.
reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.26.0")
if d, _ := moduleOf(t, mods, "avahi"); d.FP2 != "" || d.Toolchains != nil {
t.Errorf("disagreeing nodes: fp2 %q toolchains %+v, want neither", d.FP2, d.Toolchains)
}
}