Files
mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go
T
jschoubben 3def293d94 Build modules: no thread limit without the composer's share; with it, the share over their own capacity (ADR 0553 round 2)
Change A rebuilds the three build modules, so their runtimes restart once; it
changes nothing else about their builds until change B gives them the share.
2026-10-26 14:41:12 +01:00

193 lines
7.1 KiB
Go

package main
import (
"context"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/buildcpu"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/forgekey"
"git.novox.be/novox/mesh-sdk/go/health"
)
// toolchainValues are the GOTOOLCHAIN settings of an environment.
func toolchainValues(env []string) []string {
var out []string
for _, e := range env {
if strings.HasPrefix(e, "GOTOOLCHAIN=") {
out = append(out, e)
}
}
return out
}
// 1572/AC1 (ADR 0477 §1): every environment a step gives its go commands says GOTOOLCHAIN=local, once, and names no
// release.
func TestEveryStepEnvironmentSaysGotoolchainLocal(t *testing.T) {
t.Setenv("MESH_BUILD_CPU_SHARE", "50")
m := &Module{Cache: t.TempDir()}
vend := t.TempDir()
os.MkdirAll(filepath.Join(vend, "vendor"), 0o755)
os.WriteFile(filepath.Join(vend, "vendor", "modules.txt"), nil, 0o644)
plain := t.TempDir()
envs := map[string][]string{
"goEnv": m.goEnv(), "goEnvFor(plain)": m.goEnvFor(plain), "goEnvFor(vendor)": m.goEnvFor(vend),
"buildEnv(plain)": m.buildEnv(plain), "buildEnv(vendor)": m.buildEnv(vend), "probeEnv": probeEnv(t.TempDir()),
}
for name, env := range envs {
if got := toolchainValues(env); len(got) != 1 || got[0] != "GOTOOLCHAIN=local" {
t.Errorf("%s: GOTOOLCHAIN settings %v, want exactly GOTOOLCHAIN=local", name, got)
}
// Issue 1806: and its threads, its part of the node's processor cap (probeEnv runs no build).
if procs := strings.Count(strings.Join(env, "\n"), "GOMAXPROCS="); name != "probeEnv" && procs != 1 {
t.Errorf("%s: GOMAXPROCS said %d times, want once: %v", name, procs, env)
}
}
// Without the composer's share no thread limit; with it, the share over its own three slots (ADR 0553 §4).
t.Setenv("MESH_BUILD_CPU_SHARE", "")
m = &Module{Cache: t.TempDir(), Node: "shanks", Capacity: func() int { return 3 }}
if l := m.cpuLimits(); l.Threads != 0 || strings.Contains(strings.Join(m.goEnv(), " "), "GOMAXPROCS=") {
t.Errorf("without the share: %+v, goEnv %v", l, m.goEnv())
}
t.Setenv("MESH_BUILD_CPU_SHARE", "50")
want := buildcpu.Threads(50, runtime.NumCPU(), 3)
if l := m.cpuLimits(); l.Slots != 3 || !strings.Contains(strings.Join(m.goEnv(), " "), "GOMAXPROCS="+strconv.Itoa(want)) {
t.Errorf("limits %+v, goEnv %v; want %d threads", l, m.goEnv(), want)
}
if st := m.cpuStatus(); st["limits"].(buildcpu.Limits) != m.cpuLimits() {
t.Errorf("the status says %v", st)
}
}
// 1572/AC1: the fetch step runs no `go version` and downloads no toolchain, and each go command says GOTOOLCHAIN=local.
func TestTheFetchStepsGoCommandsSayGotoolchainLocalAndFetchNoToolchain(t *testing.T) {
f := &fakeStep{gomods: "go.mod\n"}
m, states := fetchModule(t, f)
if _, err := fetchTool(t, m)(fetchArgs); err != nil {
t.Fatal(err)
}
if st := states.waitEnd(t); st.State != delivery.StepPassed {
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
}
goCalls := 0
for _, c := range f.calls {
if strings.HasSuffix(c.line, " version") || strings.Contains(c.line, " get toolchain") || strings.Contains(c.line, " toolchain") {
t.Errorf("the fetch step ran %q", c.line)
}
if strings.HasPrefix(c.line, "go ") {
goCalls++
if got := toolchainValues(c.env); len(got) != 1 || got[0] != "GOTOOLCHAIN=local" {
t.Errorf("%s: GOTOOLCHAIN settings %v", c.line, got)
}
}
}
if goCalls == 0 {
t.Error("no go command ran")
}
}
type probeSpy struct {
env []string
line string
ran forgekey.Ran
left int64
}
func (p *probeSpy) run(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
p.env, p.line = env, name+" "+strings.Join(args, " ")
if d, ok := ctx.Deadline(); ok {
p.left = int64(d.Sub(time.Now()))
}
return p.ran
}
// 1572/AC1 (ADR 0477 §1): the probe runs `go version` with GOTOOLCHAIN=local and a short bound, with an empty cache:
// it fetches nothing, so there is no long bound for a download.
func TestTheProbeRunsGoVersionLocalWithOneShortBound(t *testing.T) {
p := &probeSpy{ran: forgekey.Ran{Stdout: "go version go1.27.1-X:nodwarf5 linux/amd64\n"}}
v, err := probeGo(context.Background(), p.run, "/home/x", t.TempDir())
if err != nil || v != "go1.27.1-X:nodwarf5" {
t.Fatalf("= %q, %v", v, err)
}
if got := toolchainValues(p.env); len(got) != 1 || got[0] != "GOTOOLCHAIN=local" || p.line != "go version" {
t.Errorf("command %q, GOTOOLCHAIN settings %v", p.line, got)
}
if p.left > int64(ProbeTimeout) || p.left <= 0 {
t.Errorf("the probe's bound is %v, want at most %v", p.left, int64(ProbeTimeout))
}
}
// 1572/AC2 (ADR 0477 §2): neither the recipe nor the module names a Go version: the recipe has no field for one.
func TestTheRecipeHasNoFieldForAToolchainVersion(t *testing.T) {
typ := reflect.TypeOf(Recipe{})
for i := 0; i < typ.NumField(); i++ {
if n := typ.Field(i).Name; n == "Go" || strings.Contains(n, "Toolchain") {
t.Errorf("Recipe has the field %s", n)
}
}
}
// 1572/AC2 and AC4: whatever version the node has installed, the module stays healthy, names that version in its health,
// and writes it to <node>.go of the toolchains state.
func TestAnyInstalledGoIsHealthyNamedAndReported(t *testing.T) {
for _, installed := range []string{"go1.27.1", "go1.27.2", "go1.28.0", "go1.27.1-X:nodwarf5"} {
t.Run(installed, func(t *testing.T) {
states := newStates()
m := moduleWith(states, installed)
if err := m.Check(context.Background()); err != nil {
t.Fatalf("Check refused: %v", err)
}
a := m.Health(context.Background())
if !a.Healthy || a.State != health.StateHealthy || !strings.Contains(a.Why, installed) {
t.Errorf("health = %+v, want healthy naming %s", a, installed)
}
tc, ok := states.toolchain("ace.go")
if !ok || tc.Version != installed || tc.Recipe != m.Recipe.Version() {
t.Errorf("ace.go = %+v, %v; want version %s and recipe %s", tc, ok, installed, m.Recipe.Version())
}
ran := false
run := func(context.Context, map[string]any) (any, error) { ran = true; return "done", nil }
if _, err := m.Short(run)(nil); err != nil || !ran {
t.Errorf("a step on go %s: ran %v, err %v", installed, ran, err)
}
})
}
}
// 1572/AC3 (ADR 0477 §3): the recipe version is made without a toolchain version: the module's recipe is the same
// whatever go is installed, and a toolchain version set on the recipe by any name moves nothing; it moves with the
// steps' version.
func TestTheRecipeVersionIgnoresTheToolchainVersion(t *testing.T) {
base := Current()
v := base.Version()
for _, installed := range []string{"go1.27.1", "go1.28.0"} {
m := moduleWith(newStates(), installed)
m.Recipe = Current()
if err := m.Check(context.Background()); err != nil {
t.Fatal(err)
}
if got := m.Recipe.Version(); got != v {
t.Errorf("with go %s installed the recipe version is %s, want %s", installed, got, v)
}
}
r := base
if f := reflect.ValueOf(&r).Elem().FieldByName("Go"); f.IsValid() {
f.SetString("go9.9.9")
if r.Version() != v {
t.Errorf("a toolchain version on the recipe moved its version")
}
}
r = base
r.Steps += "x"
if r.Version() == v {
t.Errorf("the steps' version did not move the recipe version")
}
}