textmachine/platform/internal/gates/toolchain_test.go

97 lines
3.7 KiB
Go

// Package gates holds the tests of the zone's BUILD gates — the checks that run before any code
// does. They live in their own package because their subject is the Makefile and go.mod rather than
// any package's behaviour, and a gate nobody tests is a gate that quietly stops gating: this file
// exists because the toolchain floor was raised in three documents and one echo, while the check
// itself went on accepting the version it was raised against (re-check of the P5 dofix, FP5-9).
package gates
import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
)
// zoneRoot is where the Makefile and go.mod live, two levels up from this package.
const zoneRoot = "../.."
// The gate is exercised with versions THIS HOST does not have. That is the whole point of asserting
// on the comparison rather than on a run: a gate tested only against the toolchain that happens to be
// installed proves nothing about the one it is meant to refuse.
func TestTheToolchainGateComparesVersionsRatherThanMatchingThem(t *testing.T) {
min := minVersion(t)
if min != "1.26.6" {
// Not a failure — the floor is allowed to move — but the table below is written against it.
t.Logf("GO_MIN_VERSION is %s; the cases below assume the 1.26.x floor", min)
}
for _, c := range []struct {
version string
accept bool
}{
{"go1.26.6", true}, // the floor itself
{"go1.26.7", true}, // a later patch
{"go1.26.10", true}, // double digits: the regex this replaced ordered 1.26.10 BELOW 1.26.9
{"go1.27.0", true}, // a later minor
{"go2.0.0", true},
{"go1.26.5", false}, // the release the floor was raised against — the case that used to pass
{"go1.26.4", false},
{"go1.25.9", false},
{"go1.9.9", false}, // one-digit minor must not sort above 1.26
{"go1.26.6rc1", false}, // a prerelease does not carry the fixes its number promises
{"devel go1.27-abcdef", false},
} {
err := makeTarget(t, "version-check", "GO_VERSION="+c.version)
if c.accept && err != nil {
t.Errorf("the gate refused %s, which is at or above the floor %s: %v", c.version, min, err)
}
if !c.accept && err == nil {
t.Errorf("the gate ACCEPTED %s, below the floor %s", c.version, min)
}
}
}
// The other half of the same floor: the Makefile is a convenience, go.mod is what every build reads —
// including one that never calls make. The two must name the same version, or a build that skips the
// battery gets a toolchain the battery would have refused.
func TestGoModPinsTheSameToolchainTheBatteryDemands(t *testing.T) {
src, err := os.ReadFile(filepath.Join(zoneRoot, "go.mod"))
if err != nil {
t.Fatal(err)
}
m := regexp.MustCompile(`(?m)^toolchain go(\S+)$`).FindSubmatch(src)
if m == nil {
t.Fatal("go.mod carries no toolchain directive: the floor then lives only in the Makefile, and a build that does not call make ignores it")
}
if got, want := string(m[1]), minVersion(t); got != want {
t.Errorf("go.mod pins the toolchain at %s and the battery demands %s", got, want)
}
}
func minVersion(t *testing.T) string {
t.Helper()
src, err := os.ReadFile(filepath.Join(zoneRoot, "Makefile"))
if err != nil {
t.Fatal(err)
}
m := regexp.MustCompile(`(?m)^GO_MIN_VERSION := (\S+)$`).FindSubmatch(src)
if m == nil {
t.Fatal("the Makefile no longer declares GO_MIN_VERSION")
}
return string(m[1])
}
func makeTarget(t *testing.T, args ...string) error {
t.Helper()
if _, err := exec.LookPath("make"); err != nil {
t.Skip("make is not on this host: the build gates cannot be exercised")
}
cmd := exec.CommandContext(t.Context(), "make", args...)
cmd.Dir = zoneRoot
out, err := cmd.CombinedOutput()
if err != nil && !strings.Contains(string(out), "required") {
t.Fatalf("make %v failed for a reason other than the gate: %s", args, out)
}
return err
}