Each run step goes through mesh-sdk's buildstep.Isolated: no network, none of the account, the workspace read-write, the toolchain read-only, the npm cache beneath a throwaway layer unless the build step is told publish (a trunk build). 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 script through the real test step.
179 lines
6.5 KiB
Go
179 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
// shutOff is one command a step ran in bubblewrap: what it bound read-write, read-only and beneath a throwaway layer,
|
|
// its bubblewrap flags, and the command.
|
|
type shutOff struct {
|
|
flags []string
|
|
binds, roBinds, overlays []string
|
|
env []string
|
|
command 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 reads a bubblewrap command back (recorded in seen) and hands its command to run with the environment
|
|
// bubblewrap sets (a name set twice keeps its last value, as bubblewrap's --setenv does); any other command 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
|
|
var names []string
|
|
values := map[string]string{}
|
|
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":
|
|
if _, set := values[args[i+1]]; !set {
|
|
names = append(names, args[i+1])
|
|
}
|
|
values[args[i+1]] = args[i+2]
|
|
}
|
|
i += n
|
|
}
|
|
if i+1 >= len(args) {
|
|
t.Errorf("bubblewrap was given no command: %v", args)
|
|
return forgekey.Ran{Status: 1, Err: "no command"}
|
|
}
|
|
for _, n := range names {
|
|
s.env = append(s.env, n+"="+values[n])
|
|
}
|
|
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, the toolchain read-only, and the
|
|
// shared cache beneath a throwaway layer unless write.
|
|
func checkShutOff(t *testing.T, m *Module, root string, 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", s.command, f)
|
|
}
|
|
}
|
|
home := m.Toolchain.Paths.Home
|
|
for _, b := range append(append([]string{}, s.binds...), s.roBinds...) {
|
|
if b == home || strings.HasPrefix(b, home+"/") || strings.HasPrefix(b, "/run") {
|
|
t.Errorf("%s was given %s: the account's home or the runtime directory", s.command, b)
|
|
}
|
|
}
|
|
for _, e := range s.env {
|
|
if e == "HOME="+home {
|
|
t.Errorf("%s was told the account's home", s.command)
|
|
}
|
|
}
|
|
if !slices.Contains(s.roBinds, m.toolchainDir()) || slices.Contains(s.binds, m.toolchainDir()) {
|
|
t.Errorf("%s was not given the toolchain read-only: %v %v", s.command, s.roBinds, s.binds)
|
|
}
|
|
c := m.npmCache()
|
|
bound, layered := slices.Contains(s.binds, c), slices.Contains(s.overlays, c)
|
|
if write != bound || write == layered {
|
|
t.Errorf("%s: the shared cache bound %v, layered %v; a trunk build (%v) alone writes it", s.command, bound, layered, write)
|
|
}
|
|
if !slices.Contains(s.binds, root) {
|
|
t.Errorf("%s was not given its workspace %s read-write: %v", s.command, root, s.binds)
|
|
}
|
|
}
|
|
|
|
// 1309-S7: the type-check and the test script run shut off, the cache beneath a throwaway layer.
|
|
func TestTheTestStepRunsItsCommandsShutOff(t *testing.T) {
|
|
m, f, work := newMachine(t)
|
|
workspaceWith(t, work, map[string]string{"index.ts": `export const a = 1;`, "package.json": `{"scripts":{"test":"x"}}`})
|
|
if _, err := m.test(context.Background(), testArgs("unit")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(f.shut) != 2 || len(f.calls) != 2 {
|
|
t.Fatalf("shut off %d of %d commands, want the type-check and the script: %v", len(f.shut), len(f.calls), f.calls)
|
|
}
|
|
for _, s := range f.shut {
|
|
checkShutOff(t, m, work+"/w1", s, false)
|
|
}
|
|
}
|
|
|
|
// 1309-S8: tsc and esbuild of a candidate's build run shut off with the cache beneath a throwaway layer; a trunk build
|
|
// (publish) writes it.
|
|
func TestTheBuildStepRunsShutOffAndOnlyATrunkBuildWritesTheCache(t *testing.T) {
|
|
for _, publish := range []bool{false, true} {
|
|
m, f, work := builtMachine(t, "")
|
|
ws := "w1"
|
|
args := buildArgs()
|
|
if publish {
|
|
args["publish"] = true
|
|
}
|
|
if _, err := m.build(context.Background(), args); err != nil {
|
|
t.Fatalf("publish %v: %v", publish, err)
|
|
}
|
|
if len(f.shut) == 0 || len(f.shut) != len(f.calls) {
|
|
t.Fatalf("publish %v: shut off %d of %d commands", publish, len(f.shut), len(f.calls))
|
|
}
|
|
for _, s := range f.shut {
|
|
checkShutOff(t, m, work+"/"+ws, s, publish)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1309-S1: a step is refused, and the module is unhealthy, while bubblewrap is missing or older than 0.10.
|
|
func TestAnOldOrMissingBubblewrapRefusesTheStepsAndIsUnhealthy(t *testing.T) {
|
|
for _, c := range []struct {
|
|
version string
|
|
err error
|
|
ok bool
|
|
}{{"0.13.0", nil, true}, {"0.10", nil, true}, {"0.9.0", nil, false}, {"", errors.New("bwrap: not found"), false}} {
|
|
m, _, _ := newMachine(t)
|
|
m.Probe = func(context.Context) (string, error) { return "v22.20.0", nil }
|
|
m.Toolchains = func(string, any) (uint64, error) { return 1, nil }
|
|
m.Bubblewrap = func(context.Context) (string, error) { return c.version, c.err }
|
|
err := m.Check(context.Background())
|
|
raw, _ := json.Marshal(m.Health(context.Background()))
|
|
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", c.version, c.err, err)
|
|
}
|
|
if !strings.Contains(string(raw), "unhealthy") || !strings.Contains(string(raw), "bubblewrap") {
|
|
t.Errorf("bubblewrap %q %v: health %s", c.version, c.err, raw)
|
|
}
|
|
}
|
|
}
|