Test the Go build module's integration level against a faked and a real store and bus (issues 1486, 1290, 1487), part 12 of 20

This commit is contained in:
2026-10-20 20:56:12 +00:00
parent 2462f96f65
commit dbc36d3d3c
2 changed files with 477 additions and 0 deletions
@@ -0,0 +1,318 @@
package main
import (
"context"
"encoding/json"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
// Issue 1486: the integration level, with the store and the bus faked at the process boundary: the commands the level
// runs are recorded, and a clock the commands advance stands in for the time they would take.
type integFake struct {
mu sync.Mutex
calls []cmdRecord
clock time.Time
postgres string // what `postgres --version` prints
captured string // the go test -json output of the run
// advance is how long a phase takes on the fake clock: compile, start (of the store), test.
advance map[string]time.Duration
busURL string
busDir string
busDown bool
runDirs []string // run directories seen when the store was started, to be gone at the end
panicOn string // a phase that panics
}
func newIntegFake(t *testing.T, captured string) *integFake {
t.Helper()
raw, err := os.ReadFile(filepath.Join("testdata", "captured", captured+".json"))
if err != nil {
t.Fatal(err)
}
return &integFake{clock: time.Date(2026, 10, 20, 12, 0, 0, 0, time.UTC), postgres: "postgres (PostgreSQL) 18.6",
captured: string(raw), advance: map[string]time.Duration{}, busURL: "nats://127.0.0.1:45222"}
}
func (f *integFake) now() time.Time { f.mu.Lock(); defer f.mu.Unlock(); return f.clock }
func (f *integFake) tick(phase string) {
f.mu.Lock()
f.clock = f.clock.Add(f.advance[phase])
f.mu.Unlock()
if f.panicOn == phase {
panic("the " + phase + " phase panicked")
}
}
func (f *integFake) run(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
line := name + " " + strings.Join(args, " ")
f.mu.Lock()
f.calls = append(f.calls, cmdRecord{env: env, line: line})
f.mu.Unlock()
switch {
case name == "postgres":
return forgekey.Ran{Stdout: f.postgres + "\n"}
case name == "git" && len(args) > 2 && args[2] == "ls-files":
return forgekey.Ran{Stdout: "go.mod\n"}
case name == "go" && strings.Contains(line, "-run ^$"):
f.tick("compile")
case name == "go":
f.tick("test")
status := 0
if strings.Contains(f.captured, `"Action":"fail"`) {
status = 1
}
return forgekey.Ran{Stdout: f.captured, Status: status}
case name == "pg_ctl" && strings.HasSuffix(line, " start"):
f.tick("start")
for i, a := range args {
if a == "-D" {
f.runDirs = append(f.runDirs, filepath.Dir(filepath.Dir(args[i+1])))
}
}
}
return forgekey.Ran{}
}
func (f *integFake) startBus(_ context.Context, dir string) (string, func(), error) {
f.mu.Lock()
f.busDir = dir
f.mu.Unlock()
return f.busURL, func() { f.mu.Lock(); f.busDown = true; f.mu.Unlock() }, nil
}
func (f *integFake) lines() string {
f.mu.Lock()
defer f.mu.Unlock()
out := []string{}
for _, c := range f.calls {
out = append(out, c.line)
}
return strings.Join(out, "\n")
}
func integModule(t *testing.T, f *integFake) (*Module, *keptStates) {
t.Helper()
states := newStates()
m := moduleWith(states, "go1.27.1")
m.Home, m.Work, m.Cache = t.TempDir(), t.TempDir(), t.TempDir()
m.Run, m.Now, m.StartBus = f.run, f.now, f.startBus
if err := os.MkdirAll(filepath.Join(m.Work, "ws1", "tree"), 0o755); err != nil {
t.Fatal(err)
}
return m, states
}
func runLevelTool(t *testing.T, m *Module, states *keptStates, level string) (TestResult, delivery.Step) {
t.Helper()
for _, tool := range m.Tools(m.StepTools()) {
if tool.Name != "build_go_test" {
continue
}
if _, err := tool.Run(map[string]any{"workspace": "ws1", "level": level}); err != nil {
t.Fatal(err)
}
}
st := states.waitEnd(t)
var res TestResult
if st.State == delivery.StepPassed {
if err := json.Unmarshal(st.Result, &res); err != nil {
t.Fatal(err)
}
}
return res, st
}
// 1486/AC2: the store's start carries listen_addresses=” and a socket directory under the run; the tests are given
// its address and the bus's, and the run's directory is in the workspace's.
func TestTheStoreListensOnASocketUnderTheRunAndNowhereElse(t *testing.T) {
f := newIntegFake(t, "slow")
m, states := integModule(t, f)
res, st := runLevelTool(t, m, states, "integration")
if st.State != delivery.StepPassed || !res.Passed || res.Level != "integration" {
t.Fatalf("the step ended %s (%s): %+v", st.State, st.Reason, res)
}
var start, initdb string
for _, c := range f.calls {
if strings.HasPrefix(c.line, "pg_ctl ") && strings.HasSuffix(c.line, " start") {
start = c.line
}
if strings.HasPrefix(c.line, "initdb ") {
initdb = c.line
}
}
run := f.runDirs[0]
if !strings.HasPrefix(run, filepath.Join(m.Work, "ws1", "run-")) {
t.Errorf("the run directory is %s, not under the workspace's", run)
}
for _, want := range []string{"listen_addresses=''", "-k " + filepath.Join(run, "pg", "socket"), "fsync=off", "-w"} {
if !strings.Contains(start, want) {
t.Errorf("the store's start %q lacks %q", start, want)
}
}
if strings.Contains(start, "listen_addresses='*'") || strings.Contains(start, "-h ") || strings.Contains(start, "port=") {
t.Errorf("the store listens on TCP: %s", start)
}
if !strings.Contains(initdb, "-D "+filepath.Join(run, "pg", "data")) || !strings.Contains(initdb, "--auth=trust") {
t.Errorf("initdb = %q", initdb)
}
var test cmdRecord
for _, c := range f.calls {
if strings.HasPrefix(c.line, "go ") && strings.Contains(c.line, "-json") {
test = c
}
}
if !strings.Contains(test.line, "-tags integration") {
t.Errorf("the run does not ask for the tag: %s", test.line)
}
env := map[string]string{}
for _, w := range test.env {
k, v, _ := strings.Cut(w, "=")
env[k] = v
}
u, err := url.Parse(env["MESH_TEST_POSTGRES"])
if err != nil || u.Scheme != "postgres" || u.Query().Get("host") != filepath.Join(run, "pg", "socket") || u.Host != "" {
t.Errorf("MESH_TEST_POSTGRES = %q", env["MESH_TEST_POSTGRES"])
}
if strings.Count(strings.TrimPrefix(env["MESH_TEST_POSTGRES"], "postgres://"), "/") != 1 {
t.Errorf("MESH_TEST_POSTGRES = %q has a slash beyond the one before the database: the controller's tests cut it at the last", env["MESH_TEST_POSTGRES"])
}
if env["MESH_TEST_NATS"] != "nats://127.0.0.1:45222" || f.busDir != filepath.Join(run, "nats") {
t.Errorf("MESH_TEST_NATS = %q, bus dir %q", env["MESH_TEST_NATS"], f.busDir)
}
if res.Store != "postgres 18" || res.Bus != "nats-server "+m.Recipe.NatsServer {
t.Errorf("the result names %q and %q", res.Store, res.Bus)
}
}
// 1486/AC3: a postgres of major 17 refuses the integration level, naming 17 and 18; the unit level still runs.
func TestAnotherPostgresMajorRefusesTheIntegrationLevelOnly(t *testing.T) {
f := newIntegFake(t, "slow")
f.postgres = "postgres (PostgreSQL) 17.2"
m, states := integModule(t, f)
_, st := runLevelTool(t, m, states, "integration")
if st.State != delivery.StepRefused || !strings.Contains(st.Reason, "17") || !strings.Contains(st.Reason, "18") {
t.Fatalf("the integration step ended %s: %q", st.State, st.Reason)
}
if strings.Contains(f.lines(), "initdb") || strings.Contains(f.lines(), "-tags") {
t.Errorf("the refused level ran something:\n%s", f.lines())
}
res, st := runLevelTool(t, m, states, "unit")
if st.State != delivery.StepPassed || !res.Passed || res.Level != "unit" {
t.Errorf("the unit level: %s %+v", st.State, res)
}
}
// 1486/AC4: a setup over thirty seconds fails the level naming the setup bound; a run over sixty names the running
// bound and the slowest tests. The clock the commands move stands in for the time they take.
func TestSetupAndRunningTimeAreJudgedApart(t *testing.T) {
f := newIntegFake(t, "slow")
f.advance["compile"], f.advance["start"] = 20*time.Second, 15*time.Second
m, states := integModule(t, f)
res, _ := runLevelTool(t, m, states, "integration")
if res.Passed || res.Setup != 35 || !strings.Contains(res.Why, "setup took 35.00 s, over its setup bound of 30 s") || strings.Contains(res.Why, "slowest tests") {
t.Errorf("a setup of 35 s: %+v", res)
}
g := newIntegFake(t, "slow")
g.advance["test"] = 61 * time.Second
m, states = integModule(t, g)
res, _ = runLevelTool(t, m, states, "integration")
if res.Passed || res.Elapsed != 61 || !strings.Contains(res.Why, "took 61.00 s, over its bound of 60 s; the slowest tests: fixture/slow TestTakesAWhile") || strings.Contains(res.Why, "setup") {
t.Errorf("a run of 61 s: %+v", res)
}
h := newIntegFake(t, "slow")
h.advance["compile"], h.advance["test"] = 29*time.Second, 59*time.Second
m, states = integModule(t, h)
if res, _ = runLevelTool(t, m, states, "integration"); !res.Passed || res.Setup != 29 || res.Elapsed != 59 {
t.Errorf("29 s of setup and 59 s of running are within both bounds: %+v", res)
}
}
// 1486/AC5: a run that fails, and a run that panics: the store is stopped, the bus is stopped, the run's directory is
// gone.
func TestAFailedOrPanickedRunStopsTheServersAndRemovesTheRunDirectory(t *testing.T) {
f := newIntegFake(t, "failing")
m, states := integModule(t, f)
res, _ := runLevelTool(t, m, states, "integration")
if res.Passed {
t.Fatalf("the failing fixture passed: %+v", res)
}
checkCleaned := func(f *integFake, m *Module) {
t.Helper()
lines := f.lines()
if !strings.Contains(lines, "pg_ctl -D "+filepath.Join(f.runDirs[0], "pg", "data")+" -m immediate -w stop") {
t.Errorf("the store was not stopped:\n%s", lines)
}
if strings.LastIndex(lines, " stop") < strings.LastIndex(lines, "-json") {
t.Errorf("the store was stopped before the tests ended:\n%s", lines)
}
if !f.busDown {
t.Errorf("the bus was not stopped")
}
if _, err := os.Stat(f.runDirs[0]); !os.IsNotExist(err) {
t.Errorf("the run directory %s is still there (%v)", f.runDirs[0], err)
}
}
checkCleaned(f, m)
g := newIntegFake(t, "failing")
g.panicOn = "test"
m, _ = integModule(t, g)
func() {
defer func() { recover() }()
m.integration(context.Background(), "ws1", filepath.Join(m.Work, "ws1", "tree"), nil)
}()
checkCleaned(g, m)
}
// A store that does not start ends the step failed, with the server stopped and the directory gone.
func TestAStoreThatDoesNotStartEndsTheStepFailedAndLeavesNothing(t *testing.T) {
f := newIntegFake(t, "slow")
m, states := integModule(t, f)
inner := m.Run
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
r := inner(ctx, env, name, args...)
if name == "pg_ctl" && strings.HasSuffix(strings.Join(args, " "), " start") {
return forgekey.Ran{Status: 1, Stderr: "pg_ctl: could not start server\n"}
}
return r
}
_, st := runLevelTool(t, m, states, "integration")
if st.State != delivery.StepFailed || !strings.Contains(st.Reason, "pg_ctl: could not start server") {
t.Errorf("the step ended %s: %q", st.State, st.Reason)
}
if !strings.Contains(f.lines(), " stop") {
t.Errorf("the half-started store was not stopped:\n%s", f.lines())
}
if f.busDir != "" {
t.Errorf("the bus was started after the store failed")
}
if entries, _ := os.ReadDir(filepath.Join(m.Work, "ws1")); len(entries) != 1 || entries[0].Name() != "tree" {
t.Errorf("the workspace holds %v, want only its tree", entries)
}
}
func TestThePostgresMajorIsReadFromTheVersionLine(t *testing.T) {
for line, want := range map[string]int{"postgres (PostgreSQL) 18.6": 18, "postgres (PostgreSQL) 9.6.24": 9, "postgres (PostgreSQL) 17.2 (Arch)": 17} {
f := &integFake{postgres: line, advance: map[string]time.Duration{}}
m := &Module{Run: f.run}
if got, err := m.postgresMajor(context.Background()); err != nil || got != want {
t.Errorf("%q = %d, %v; want %d", line, got, err, want)
}
}
if _, err := (&Module{Run: (&integFake{postgres: "no", advance: map[string]time.Duration{}}).run}).postgresMajor(context.Background()); err == nil {
t.Errorf("accepted a line that is not postgres's")
}
}
@@ -0,0 +1,159 @@
//go:build integration
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
// TestMain makes this test binary the bus when the module starts it as one (bus.go).
func TestMain(m *testing.M) {
if len(os.Args) > 1 && os.Args[1] == busArg {
os.Exit(runBus(os.Args[2:]))
}
os.Exit(m.Run())
}
// repository copies a fixture repository into a git tree, as a fetch leaves one.
func repository(t *testing.T, name string) string {
t.Helper()
tree := t.TempDir()
if out, err := exec.Command("cp", "-r", filepath.Join("testdata", "repos", name)+"/.", tree).CombinedOutput(); err != nil {
t.Fatalf("%s: %v %s", name, err, out)
}
for _, c := range [][]string{{"init", "-q"}, {"add", "-A"}, {"-c", "user.email=a@b", "-c", "user.name=x", "commit", "-qm", "x"}} {
if out, err := exec.Command("git", append([]string{"-C", tree}, c...)...).CombinedOutput(); err != nil {
t.Fatalf("%s: git %v: %v %s", name, c, err, out)
}
}
return tree
}
// Issue 1290: the unit level against the real go on the fixture repositories in testdata/repos: the tests of
// test_test.go read the output captured from them, and this one proves the capture is what the real go says today. It
// starts go test, so it carries the tag.
func TestUnitLevelRunsTheRealGoOnTheFixtureRepositories(t *testing.T) {
want := map[string]struct {
passed, noTests bool
failed string
}{
"failing": {failed: "fixture/failing/bad"},
"tagged": {passed: true, noTests: true},
"empty": {passed: true, noTests: true},
"slow": {passed: true},
"broken": {failed: "fixture/broken"},
}
for name, w := range want {
tree := repository(t, name)
home := t.TempDir()
m := &Module{Home: home, Work: t.TempDir(), Cache: t.TempDir()}
m.Run = StepRunner(home)
res, err := m.runLevel(context.Background(), tree, levelOpts{level: LevelUnit, bounds: Bounds{Run: UnitBound}})
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if res.Passed != w.passed || res.NoTests != w.noTests || (w.failed != "" && (len(res.Failed) != 1 || res.Failed[0].Package != w.failed)) {
t.Errorf("%s: result = %+v", name, res)
}
}
}
// processesNamed are the processes whose command line holds text.
func processesNamed(text string) []string {
var out []string
cmdlines, _ := filepath.Glob("/proc/[0-9]*/cmdline")
for _, f := range cmdlines {
if raw, err := os.ReadFile(f); err == nil && strings.Contains(string(raw), text) {
out = append(out, f)
}
}
return out
}
// 1486/AC1: given a fixture repository whose integration test opens MESH_TEST_POSTGRES and MESH_TEST_NATS, the
// integration level passes, and afterwards no postgres or bus process of the run is left and its directory is gone.
// It starts postgres and a bus, so it runs in the merge check and in a try, never on the way.
func TestIntegrationLevelRunsAgainstARealStoreAndBusAndLeavesNothing(t *testing.T) {
tree := repository(t, "store")
work := t.TempDir()
if err := os.MkdirAll(filepath.Join(work, "ws1"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.Rename(tree, filepath.Join(work, "ws1", "tree")); err != nil {
t.Fatal(err)
}
home := t.TempDir()
m := &Module{Recipe: Current(), Home: home, Work: work, Cache: t.TempDir()}
m.Run = StepRunner(home)
res, err := m.integration(context.Background(), "ws1", filepath.Join(work, "ws1", "tree"), nil)
if err != nil {
t.Fatalf("the integration level: %v", err)
}
if !res.Passed || res.Tests != 1 || res.Setup <= 0 || res.Setup > IntegrationSetup.Seconds() {
t.Errorf("result = %+v", res)
}
entries, _ := os.ReadDir(filepath.Join(work, "ws1"))
if len(entries) != 1 || entries[0].Name() != "tree" {
t.Errorf("the workspace holds %v, want only its tree: the run directory is gone", entries)
}
for deadline := time.Now().Add(5 * time.Second); len(processesNamed(work)) > 0 && time.Now().Before(deadline); time.Sleep(100 * time.Millisecond) {
}
if left := processesNamed(work); len(left) > 0 {
t.Errorf("processes of the run are left: %v", left)
}
}
// 1487/AC2: given a fixture Go module, when it is built twice, both digests are equal and the archive holds one
// executable named as binaryName says. It runs the real go, so it carries the tag.
func TestBuildTwiceGivesOneDigestAndOneExecutable(t *testing.T) {
work := t.TempDir()
tree := filepath.Join(work, "ws1", "tree")
if err := os.MkdirAll(tree, 0o755); err != nil {
t.Fatal(err)
}
if out, err := exec.Command("cp", "-r", filepath.Join("testdata", "repos", "build")+"/.", tree).CombinedOutput(); err != nil {
t.Fatalf("%v %s", err, out)
}
home := t.TempDir()
m := &Module{Home: home, Work: work, Cache: t.TempDir()}
m.Run = StepRunner(home)
args := map[string]any{"workspace": "ws1", "module": ".", "artifact": "tools"}
first, err := m.build(context.Background(), args)
if err != nil {
t.Fatal(err)
}
second, err := m.build(context.Background(), args)
if err != nil {
t.Fatal(err)
}
a, b := first.(BuildResult), second.(BuildResult)
if a.Digest != b.Digest || a.Binary != "prog-bin" {
t.Errorf("two builds of one tree: %+v and %+v", a, b)
}
raw, err := os.ReadFile(a.Path)
if err != nil {
t.Fatal(err)
}
zr, err := gzip.NewReader(bytes.NewReader(raw))
if err != nil {
t.Fatal(err)
}
tr := tar.NewReader(zr)
h, err := tr.Next()
if err != nil || h.Name != "prog-bin" || h.Mode&0o111 == 0 {
t.Fatalf("first entry = %+v, %v", h, err)
}
if _, err := tr.Next(); err != io.EOF {
t.Errorf("the archive holds more than one file")
}
}