The runner, the toolchain guard, the fetch, the parsers, buildcpu, the conformance kit and mesh-pack move from mesh-sdk's buildstep into modules/mesh-build-go/buildstep (own go.mod). mesh-build-go, mesh-build-node and mesh-build-image require it by a same-repository replace; what the delivery parts read (the failure contract, the fetch request, the stale sweep, the module tree shape, NewID, CapacitySetting) stays imported from mesh-sdk as sdkstep. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
193 lines
7.1 KiB
Go
193 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/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")
|
|
}
|
|
}
|