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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user