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) } } }