Test that every step and the probe run Go's own release and the probe fills the module cache (issue 1560, ADR 0476)

This commit is contained in:
2026-10-21 00:51:40 +00:00
parent 3f60d1ff31
commit c5f8f81a07
3 changed files with 240 additions and 0 deletions
@@ -0,0 +1,64 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// 1560 (ADR 0476 §1): every environment a step gives its go commands names the recipe's release as the toolchain
// and never GOTOOLCHAIN=local, which would run the distribution's build.
func TestEveryStepEnvironmentCarriesTheRecipesRelease(t *testing.T) {
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),
}
for name, env := range envs {
joined := "\n" + strings.Join(env, "\n") + "\n"
if !strings.Contains(joined, "\nGOTOOLCHAIN="+RecipeGo+"\n") {
t.Errorf("%s lacks GOTOOLCHAIN=%s:\n%s", name, RecipeGo, joined)
}
if strings.Contains(joined, "GOTOOLCHAIN=local") {
t.Errorf("%s still says GOTOOLCHAIN=local:\n%s", name, joined)
}
if !strings.Contains(joined, "\nGOMODCACHE="+filepath.Join(m.Cache, "gomod")+"\n") {
t.Errorf("%s lacks the module cache:\n%s", name, joined)
}
}
}
// 1560 (ADR 0476 §3): the fetch step downloads the change's locked dependencies and nothing else: no `go version`
// and no toolchain download among its commands, and each go command carries the recipe's release.
func TestTheFetchStepsCommandsFetchNoToolchain(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.Contains(c.line, "version") || strings.Contains(c.line, "toolchain") {
t.Errorf("the fetch step ran %q", c.line)
}
if strings.HasPrefix(c.line, "go ") {
goCalls++
if env := strings.Join(c.env, " "); !strings.Contains(env, "GOTOOLCHAIN="+RecipeGo) || strings.Contains(env, "GOTOOLCHAIN=local") {
t.Errorf("%s: environment %s", c.line, env)
}
}
}
if goCalls == 0 {
t.Error("no go command ran")
}
}
@@ -0,0 +1,52 @@
//go:build integration
package main
import (
"context"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
)
// 1560 (ADR 0476 §3): the probe, with a fresh module cache and network, lets the installed Go put
// golang.org/toolchain@v0.0.1-<RecipeGo>.<os>-<arch> into the cache and reads the release's version; then a step
// with the cache read-only and no network (GOPROXY=off) reports the recipe's version from it.
func TestTheProbePutsTheReleaseIntoTheCacheAndAStepReadsItOffline(t *testing.T) {
home, cache := t.TempDir(), t.TempDir()
t.Cleanup(func() { // the Go module cache is read-only by Go's own doing
filepath.WalkDir(cache, func(p string, d fs.DirEntry, err error) error {
if err == nil && d.IsDir() {
os.Chmod(p, 0o755)
}
return nil
})
})
v, err := ProbeGo(home, cache)(context.Background())
if err != nil || v != RecipeGo {
t.Fatalf("probe = %q, %v; want %s", v, err, RecipeGo)
}
dirs, _ := filepath.Glob(filepath.Join(cache, "gomod", "golang.org", "toolchain@v0.0.1-"+RecipeGo+".*"))
if len(dirs) != 1 {
t.Fatalf("the cache holds %v, want golang.org/toolchain@v0.0.1-%s.*", dirs, RecipeGo)
}
filepath.WalkDir(cache, func(p string, d fs.DirEntry, err error) error {
if err == nil {
if info, e := d.Info(); e == nil {
os.Chmod(p, info.Mode()&^0o222)
}
}
return nil
})
r := StepRunner(home)(context.Background(), []string{
"GOTOOLCHAIN=" + RecipeGo, "GOMODCACHE=" + filepath.Join(cache, "gomod"), "GOCACHE=" + t.TempDir(), "GOPROXY=off", "GOFLAGS=-mod=mod",
}, "go", "version")
if r.Status != 0 || r.Err != "" {
t.Fatalf("go version offline: %s %s", r.Err, r.Stderr)
}
if got, err := parseGoVersion(r.Stdout); err != nil || got != RecipeGo || !strings.Contains(r.Stdout, RecipeGo) {
t.Errorf("the step reports %q, %v; want %s", r.Stdout, err, RecipeGo)
}
}
@@ -0,0 +1,124 @@
package main
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/forgekey"
"git.novox.be/novox/mesh-sdk/go/health"
)
// probeFake is a runner that records the command and answers by rule.
type probeFake struct {
env []string
line string
ran forgekey.Ran
}
func (p *probeFake) run(_ context.Context, env []string, name string, args ...string) forgekey.Ran {
p.env, p.line = env, name+" "+strings.Join(args, " ")
return p.ran
}
// 1560 (ADR 0476 §2, §3): the probe runs `go version` with the recipe's release as the toolchain and the module's
// cache as GOMODCACHE, in an environment built from scratch (nothing of the process's, no GOTOOLCHAIN=local).
func TestTheProbeRunsGoVersionWithTheRecipesReleaseAndTheModuleCache(t *testing.T) {
p := &probeFake{ran: forgekey.Ran{Stdout: "go version go1.27.1 linux/amd64\n"}}
cache := t.TempDir()
v, err := probeGo(context.Background(), p.run, "/home/x", cache)
if err != nil || v != "go1.27.1" {
t.Fatalf("= %q, %v", v, err)
}
env := "\n" + strings.Join(p.env, "\n") + "\n"
for _, want := range []string{"\nGOTOOLCHAIN=" + RecipeGo + "\n", "\nGOMODCACHE=" + filepath.Join(cache, "gomod") + "\n", "\nHOME=/home/x\n"} {
if !strings.Contains(env, want) {
t.Errorf("the probe's environment lacks %q:\n%s", want, env)
}
}
if strings.Contains(env, "GOTOOLCHAIN=local") || p.line != "go version" {
t.Errorf("command %q, environment:\n%s", p.line, env)
}
}
// 1560: the release's own output parses to its version; the distribution's build parses to a different one.
func TestParseGoVersionOfTheReleaseAndOfTheDistributionsBuild(t *testing.T) {
if v, err := parseGoVersion("go version go1.27.1 linux/amd64\n"); err != nil || v != RecipeGo {
t.Errorf("release: %q, %v", v, err)
}
if v, _ := parseGoVersion("go version go1.27.1-X:nodwarf5 linux/amd64\n"); v == RecipeGo {
t.Errorf("the distribution's build parsed as the release: %q", v)
}
}
// 1560 (ADR 0476 §2): the toolchains key carries the version the probe read from the release.
func TestTheToolchainsKeyIsTheReleasesVersion(t *testing.T) {
states := newStates()
m := moduleWith(states, "")
p := &probeFake{ran: forgekey.Ran{Stdout: "go version go1.27.1 linux/amd64\n"}}
m.Probe = func(ctx context.Context) (string, error) { return probeGo(ctx, p.run, "/home/x", t.TempDir()) }
if err := m.Check(context.Background()); err != nil {
t.Fatal(err)
}
tc, ok := states.toolchain("ace.go")
if !ok || tc.Version != RecipeGo {
t.Fatalf("ace.go = %+v, %v; want version %s", tc, ok, RecipeGo)
}
}
// 1560 (ADR 0476 §2): a probe that fails (the release cannot be had) or reads another version makes the module
// refuse, and its health is unhealthy naming the recipe's version and what the probe got.
func TestAFailedFetchOrAnotherVersionMakesHealthUnhealthyNamingBoth(t *testing.T) {
cases := []struct {
name string
ran forgekey.Ran
want []string
}{
{"fetch fails", forgekey.Ran{Status: 1, Stderr: "go: download go1.27.1 for linux/amd64: dial tcp: no route to host\n"},
[]string{"go1.27.1", "no route to host"}},
{"other version", forgekey.Ran{Stdout: "go version go1.27.1-X:nodwarf5 linux/amd64\n"},
[]string{"go1.27.1", "go1.27.1-X:nodwarf5"}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
m := moduleWith(newStates(), "")
p := &probeFake{ran: c.ran}
m.Probe = func(ctx context.Context) (string, error) { return probeGo(ctx, p.run, "/home/x", t.TempDir()) }
a := m.Health(context.Background())
if a.Healthy || a.State != health.StateUnhealthy {
t.Fatalf("health = %+v, want unhealthy", a)
}
for _, w := range c.want {
if !strings.Contains(a.Why, w) {
t.Errorf("health does not name %q: %q", w, a.Why)
}
}
if !strings.Contains(a.Why, "recipe") && !strings.Contains(a.Why, RecipeGo) {
t.Errorf("health does not name the recipe's version: %q", a.Why)
}
})
}
}
// 1560: the probe is bound long enough to download while the release is missing, and short once it is cached.
func TestTheProbeIsBoundLongOnlyWhileTheReleaseIsMissing(t *testing.T) {
var got time.Duration
run := func(ctx context.Context, _ []string, _ string, _ ...string) forgekey.Ran {
d, _ := ctx.Deadline()
got = time.Until(d)
return forgekey.Ran{Stdout: "go version go1.27.1 linux/amd64\n"}
}
cache := t.TempDir()
probeGo(context.Background(), run, "/h", cache)
if got < time.Minute {
t.Errorf("missing release: bound %v", got)
}
os.MkdirAll(filepath.Join(cache, "gomod", "golang.org", "toolchain@v0.0.1-"+RecipeGo+".linux-amd64"), 0o755)
probeGo(context.Background(), run, "/h", cache)
if got > ProbeTimeout {
t.Errorf("cached release: bound %v", got)
}
}