Each run step goes through mesh-sdk's buildstep.Isolated: no network, none of the account, the workspace read-write, the Go caches beneath a throwaway layer unless the build step is told publish (a trunk build). The integration level runs its store, its bus and its tests in one shut-off process (this program again, level <spec>), so they reach each other and nothing else. build_go_test takes a -run pattern (a try's tests). The module declares the bubblewrap package and refuses a step, unhealthy, while bwrap is missing or older than 0.10. The proof test runs a probe test through the real unit level.
296 lines
11 KiB
Go
296 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
// shutOff is one command a step ran in bubblewrap: the bubblewrap words before "--", what it bound read-write, read-only
|
|
// and beneath a throwaway layer, and the command with its environment.
|
|
type shutOff struct {
|
|
flags []string
|
|
binds, roBinds, overlays []string
|
|
env []string
|
|
command string
|
|
chdir string
|
|
}
|
|
|
|
// bwrapArity is how many words each bubblewrap option the SDK writes takes.
|
|
var bwrapArity = map[string]int{"--unshare-all": 0, "--die-with-parent": 0, "--new-session": 0, "--clearenv": 0,
|
|
"--proc": 1, "--dev": 1, "--tmpfs": 1, "--dir": 1, "--chdir": 1, "--overlay-src": 1, "--tmp-overlay": 1,
|
|
"--ro-bind": 2, "--ro-bind-try": 2, "--bind": 2, "--symlink": 2, "--setenv": 2}
|
|
|
|
// throughBubblewrap is the fake runner of a step's tests: a bubblewrap command is read back (recorded in seen) and its
|
|
// command handed to run with the environment bubblewrap was told to set, the way it runs in the step; any other command
|
|
// (the fetch's git, postgres --version) goes to run as it is.
|
|
func throughBubblewrap(t *testing.T, run forgekey.Runner, seen *[]shutOff) forgekey.Runner {
|
|
t.Helper()
|
|
var mu sync.Mutex
|
|
return func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
|
if name != buildstep.Bubblewrap {
|
|
return run(ctx, env, name, args...)
|
|
}
|
|
var s shutOff
|
|
i := 0
|
|
for ; i < len(args) && args[i] != "--"; i++ {
|
|
n, ok := bwrapArity[args[i]]
|
|
if !ok || i+n >= len(args) {
|
|
t.Errorf("bubblewrap was given %q, which the test does not know: %v", args[i], args)
|
|
return forgekey.Ran{Status: 1, Err: "unknown bubblewrap option"}
|
|
}
|
|
s.flags = append(s.flags, args[i])
|
|
switch args[i] {
|
|
case "--bind":
|
|
s.binds = append(s.binds, args[i+1])
|
|
case "--ro-bind", "--ro-bind-try":
|
|
s.roBinds = append(s.roBinds, args[i+1])
|
|
case "--overlay-src":
|
|
s.overlays = append(s.overlays, args[i+1])
|
|
case "--setenv":
|
|
s.env = append(s.env, args[i+1]+"="+args[i+2])
|
|
case "--chdir":
|
|
s.chdir = args[i+1]
|
|
}
|
|
i += n
|
|
}
|
|
if i+1 >= len(args) {
|
|
t.Errorf("bubblewrap was given no command: %v", args)
|
|
return forgekey.Ran{Status: 1, Err: "no command"}
|
|
}
|
|
s.command = args[i+1] + " " + strings.Join(args[i+2:], " ")
|
|
mu.Lock()
|
|
*seen = append(*seen, s)
|
|
mu.Unlock()
|
|
return run(ctx, s.env, args[i+1], args[i+2:]...)
|
|
}
|
|
}
|
|
|
|
// checkShutOff fails the test unless s ran with no network and none of the account: every namespace unshared, the
|
|
// environment cleared, nothing of the home or the state bound, the caches beneath a throwaway layer unless write.
|
|
func checkShutOff(t *testing.T, m *Module, s shutOff, write bool) {
|
|
t.Helper()
|
|
for _, f := range []string{"--unshare-all", "--clearenv", "--die-with-parent"} {
|
|
if !slices.Contains(s.flags, f) {
|
|
t.Errorf("%s ran without %s: %v", s.command, f, s.flags)
|
|
}
|
|
}
|
|
for _, b := range append(append([]string{}, s.binds...), s.roBinds...) {
|
|
if m.Home != "" && (b == m.Home || strings.HasPrefix(b, m.Home+"/")) {
|
|
t.Errorf("%s was given the account's home: %s", s.command, b)
|
|
}
|
|
if strings.HasPrefix(b, "/run") {
|
|
t.Errorf("%s was given the runtime directory: %s", s.command, b)
|
|
}
|
|
}
|
|
for _, e := range s.env {
|
|
if strings.HasPrefix(e, "HOME="+m.Home) && m.Home != "" {
|
|
t.Errorf("%s was told the account's home: %s", s.command, e)
|
|
}
|
|
}
|
|
caches := m.caches()
|
|
for _, c := range caches {
|
|
bound, layered := slices.Contains(s.binds, c), slices.Contains(s.overlays, c)
|
|
switch {
|
|
case write && (!bound || layered):
|
|
t.Errorf("a trunk build's %s does not write the shared cache %s (bound %v, layered %v)", s.command, c, bound, layered)
|
|
case !write && (bound || !layered):
|
|
t.Errorf("%s may write the shared cache %s (bound %v, layered %v): only a trunk build does", s.command, c, bound, layered)
|
|
}
|
|
}
|
|
if want := filepath.Join(m.Work, "ws1"); !slices.Contains(s.binds, want) {
|
|
t.Errorf("%s was not given its workspace %s read-write: %v", s.command, want, s.binds)
|
|
}
|
|
}
|
|
|
|
// 1309-S2: every go test of the unit level runs shut off in bubblewrap: no network, none of the account, the caches
|
|
// beneath a throwaway layer.
|
|
func TestTheUnitLevelRunsEveryGoTestShutOff(t *testing.T) {
|
|
m, states, ws, calls := workspaceFor(t, "failing")
|
|
var seen []shutOff
|
|
m.Run = throughBubblewrap(t, m.Run, &seen)
|
|
if _, st := runUnit(t, m, states, ws); st.State != delivery.StepPassed {
|
|
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
|
}
|
|
ran := 0
|
|
for _, c := range *calls {
|
|
if strings.HasPrefix(c.line, "go ") {
|
|
ran++
|
|
}
|
|
}
|
|
if ran == 0 || len(seen) != ran {
|
|
t.Fatalf("%d go commands ran, %d of them shut off: %v", ran, len(seen), *calls)
|
|
}
|
|
for _, s := range seen {
|
|
checkShutOff(t, m, s, false)
|
|
}
|
|
}
|
|
|
|
// 1309-S2: a try's tests: build_go_test's run pattern reaches go test as one -run word; a pattern that is a flag is refused.
|
|
func TestTheRunPatternReachesGoTestAndAFlagIsRefused(t *testing.T) {
|
|
m, states, ws, calls := workspaceFor(t, "failing")
|
|
var seen []shutOff
|
|
m.Run = throughBubblewrap(t, m.Run, &seen)
|
|
run := stepTool(t, m, "build_go_test")
|
|
if _, err := run(map[string]any{"workspace": ws, "level": "unit", "run": "TestA|TestB"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
states.waitEnd(t)
|
|
found := false
|
|
for _, c := range *calls {
|
|
found = found || strings.Contains(c.line, " -run TestA|TestB ")
|
|
}
|
|
if !found {
|
|
t.Errorf("no go test was given -run TestA|TestB: %v", *calls)
|
|
}
|
|
for _, bad := range []string{"-exec=/bin/sh", "a b", "(unclosed"} {
|
|
if runPatternOK(bad) {
|
|
t.Errorf("the run pattern %q was taken", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1309-S3 and S4: a candidate's go build runs shut off with the caches beneath a throwaway layer; a trunk build
|
|
// (publish) writes them.
|
|
func TestABuildRunsShutOffAndOnlyATrunkBuildWritesTheCaches(t *testing.T) {
|
|
for _, publish := range []bool{false, true} {
|
|
m, states, _ := buildModule(t, manifestWith(goBundle), "binary")
|
|
var seen []shutOff
|
|
m.Run = throughBubblewrap(t, m.Run, &seen)
|
|
run := stepTool(t, m, "build_go_build")
|
|
args := map[string]any{"workspace": "ws1", "module": "modules/demo", "artifact": "tools"}
|
|
if publish {
|
|
args["publish"] = true
|
|
}
|
|
if _, err := run(args); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st := states.waitEnd(t); st.State != delivery.StepPassed {
|
|
t.Fatalf("publish %v: the step ended %s: %s", publish, st.State, st.Reason)
|
|
}
|
|
if len(seen) != 1 || !strings.HasPrefix(seen[0].command, "go ") || !strings.Contains(seen[0].command, " build ") {
|
|
t.Fatalf("publish %v: shut off %v, want the one go build", publish, seen)
|
|
}
|
|
checkShutOff(t, m, seen[0], publish)
|
|
}
|
|
}
|
|
|
|
// 1309-S3: the digest's go list runs shut off.
|
|
func TestTheDigestListsPackagesShutOff(t *testing.T) {
|
|
m, _ := digestFixture(t)
|
|
var seen []shutOff
|
|
m.Run = throughBubblewrap(t, m.Run, &seen)
|
|
digests(t, m, LevelUnit)
|
|
if len(seen) == 0 {
|
|
t.Fatal("the digest ran no command shut off")
|
|
}
|
|
for _, s := range seen {
|
|
if !strings.HasPrefix(s.command, "go ") || !strings.Contains(s.command, " list ") {
|
|
t.Errorf("the digest shut off %q, want go list", s.command)
|
|
}
|
|
checkShutOff(t, m, s, false)
|
|
}
|
|
}
|
|
|
|
// 1309-S5: the integration level runs its store, its bus and its tests in ONE shut-off process (this program again,
|
|
// `level <spec>`), so they reach each other and nothing else; its answer is the level's result. Nothing of the level
|
|
// runs outside but reading postgres's version.
|
|
func TestTheIntegrationLevelRunsInOneShutOffProcess(t *testing.T) {
|
|
m, states, ws, _ := workspaceFor(t, "failing")
|
|
m.Executable = "/opt/bundle/mesh-build-go"
|
|
want := TestResult{Level: LevelIntegration, Passed: true, Tests: 3, Packages: []PackageResult{}, Store: "postgres 18"}
|
|
var outside []string
|
|
var seen []shutOff
|
|
m.Run = throughBubblewrap(t, func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
|
switch {
|
|
case name == "postgres" && len(args) == 1 && args[0] == "--version":
|
|
return forgekey.Ran{Stdout: "postgres (PostgreSQL) 18.6\n"}
|
|
case name == m.Executable && len(args) == 2 && args[0] == levelArg:
|
|
var spec levelSpec
|
|
if err := json.Unmarshal([]byte(args[1]), &spec); err != nil || spec.Workspace != ws || spec.Work != m.Work {
|
|
t.Errorf("the level's spec %s: %v", args[1], err)
|
|
}
|
|
raw, _ := json.Marshal(levelAnswer{Result: &want})
|
|
return forgekey.Ran{Stdout: string(raw) + "\n"}
|
|
}
|
|
outside = append(outside, name+" "+strings.Join(args, " "))
|
|
return forgekey.Ran{Status: 1, Err: "not expected here"}
|
|
}, &seen)
|
|
run := stepTool(t, m, "build_go_test")
|
|
if _, err := run(map[string]any{"workspace": ws, "level": LevelIntegration}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st := states.waitEnd(t)
|
|
if st.State != delivery.StepPassed {
|
|
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
|
}
|
|
var got TestResult
|
|
if err := json.Unmarshal(st.Result, &got); err != nil || got.Tests != 3 || !got.Passed || got.Store != "postgres 18" {
|
|
t.Errorf("the result %s (%v), want the shut-off process's", st.Result, err)
|
|
}
|
|
if len(outside) > 0 {
|
|
t.Errorf("ran outside the shut-off process: %v", outside)
|
|
}
|
|
if len(seen) != 1 {
|
|
t.Fatalf("shut off %d processes, want one: %v", len(seen), seen)
|
|
}
|
|
checkShutOff(t, m, seen[0], false)
|
|
if !slices.Contains(seen[0].roBinds, m.Executable) || slices.Contains(seen[0].roBinds, filepath.Dir(m.Executable)) {
|
|
t.Errorf("the program is bound %v; want the program alone, read-only", seen[0].roBinds)
|
|
}
|
|
}
|
|
|
|
// 1309-S1: a step is refused, and the module is unhealthy, while bubblewrap is missing or older than 0.10, naming it.
|
|
func TestAnOldOrMissingBubblewrapRefusesTheStepsAndIsUnhealthy(t *testing.T) {
|
|
for _, c := range []struct {
|
|
version string
|
|
err error
|
|
ok bool
|
|
}{{"0.13.0", nil, true}, {"0.10.0", nil, true}, {"0.9.0", nil, false}, {"", errors.New("bwrap: not found"), false}} {
|
|
m := moduleWith(newStates(), "go1.27.1")
|
|
m.Bubblewrap = func(context.Context) (string, error) { return c.version, c.err }
|
|
err := m.Check(context.Background())
|
|
h := m.Health(context.Background())
|
|
raw, _ := json.Marshal(h)
|
|
if c.ok {
|
|
if err != nil || strings.Contains(string(raw), "unhealthy") {
|
|
t.Errorf("bubblewrap %q: refused %v, health %s", c.version, err, raw)
|
|
}
|
|
continue
|
|
}
|
|
var ref *buildstep.Refusal
|
|
if !errors.As(err, &ref) || !strings.Contains(err.Error(), "bubblewrap") {
|
|
t.Errorf("bubblewrap %q %v: Check answered %v, want a refusal naming bubblewrap", c.version, c.err, err)
|
|
}
|
|
if !strings.Contains(string(raw), "unhealthy") || !strings.Contains(string(raw), "bubblewrap") {
|
|
t.Errorf("bubblewrap %q %v: health %s, want unhealthy naming bubblewrap", c.version, c.err, raw)
|
|
}
|
|
}
|
|
for out, want := range map[string]string{"bubblewrap 0.13.0\n": "0.13.0", "bubblewrap 0.10": "0.10"} {
|
|
if got, err := parseBubblewrapVersion(out); err != nil || got != want {
|
|
t.Errorf("%q read as %q, %v", out, got, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// stepTool is the Run of the named tool.
|
|
func stepTool(t *testing.T, m *Module, name string) func(map[string]any) (any, error) {
|
|
t.Helper()
|
|
for _, tool := range m.Tools(m.StepTools()) {
|
|
if tool.Name == name {
|
|
return tool.Run
|
|
}
|
|
}
|
|
t.Fatalf("no tool %s", name)
|
|
return nil
|
|
}
|