From 9109524502f9a469d41e0426cc637f402912b971 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 27 Oct 2026 21:36:28 +0100 Subject: [PATCH] A missing bubblewrap fails the sandbox proofs on a build node instead of skipping them The levels a build module judges hand the tests MESH_BUILD_NODE=1; the tests that run a step in bubblewrap fail on it when bwrap is missing. Issue 1309 --- .../cmd/mesh-build-go/integration.go | 2 +- .../mesh-build-go/cmd/mesh-build-go/level.go | 2 +- .../cmd/mesh-build-go/needtools_test.go | 44 +++++++++++++++++ .../mesh-build-go/cmd/mesh-build-go/test.go | 11 ++++- .../test_real_integration_test.go | 3 ++ .../mesh-build-node/build_integration_test.go | 3 +- .../isolation_integration_test.go | 5 +- .../cmd/mesh-build-node/needtools_test.go | 49 +++++++++++++++++++ .../package_integration_test.go | 3 +- .../publish_integration_test.go | 3 +- .../mesh-build-node/store_integration_test.go | 5 +- 11 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 modules/mesh-build-go/cmd/mesh-build-go/needtools_test.go create mode 100644 modules/mesh-build-node/cmd/mesh-build-node/needtools_test.go diff --git a/modules/mesh-build-go/cmd/mesh-build-go/integration.go b/modules/mesh-build-go/cmd/mesh-build-go/integration.go index 231714a0..704dab50 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/integration.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/integration.go @@ -72,7 +72,7 @@ func (m *Module) integrationHere(ctx context.Context, workspace, tree string, pa return nil, fmt.Errorf("the integration level's bus: %w", err) } defer stopBus() - env := append([]string{"MESH_TEST_POSTGRES=" + postgres, "MESH_TEST_NATS=" + nats}, m.besideEnv(workspace)...) + env := append([]string{"MESH_TEST_POSTGRES=" + postgres, "MESH_TEST_NATS=" + nats}, m.checkEnv(workspace)...) // Compiling counts as setup: the tests are built once, here, with the addresses a TestMain may read, and the run // below finds them built. Whether they compile is for the run to say; its failure is not read here. // Only the directories the selected packages lie in compile (ADR 0536 ยง3); no selection is every directory. diff --git a/modules/mesh-build-go/cmd/mesh-build-go/level.go b/modules/mesh-build-go/cmd/mesh-build-go/level.go index 0b867566..2b6ee2b8 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/level.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/level.go @@ -228,7 +228,7 @@ func (m *Module) answer(ctx context.Context, spec levelSpec) levelAnswer { return levelAnswer{Digest: &d} case stepUnit: res, err := m.runLevel(ctx, tree, levelOpts{workspace: spec.Workspace, level: LevelUnit, packages: spec.Packages, run: spec.Run, - bounds: Bounds{Run: m.unitBound()}, env: m.besideEnv(spec.Workspace)}) + bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(spec.Workspace)}) if err != nil { return levelAnswer{Error: err.Error()} } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/needtools_test.go b/modules/mesh-build-go/cmd/mesh-build-go/needtools_test.go new file mode 100644 index 00000000..25a50c9d --- /dev/null +++ b/modules/mesh-build-go/cmd/mesh-build-go/needtools_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "slices" + "testing" +) + +// Issue 1309: a test that runs a step in bubblewrap is proof only where it runs. Where a build module judges (it hands +// the tests MESH_BUILD_NODE=1) a missing bubblewrap is a failure, since every build node declares and has it; a missing +// bubblewrap anywhere else, or any other missing tool, is a skip that says NOT RUN. +func TestAMissingToolSkipsExceptBubblewrapOnABuildNode(t *testing.T) { + for _, c := range []struct { + name string + tool string + onBuildNode bool + want string + }{ + {"bubblewrap on a desk", "bwrap", false, "skip"}, + {"bubblewrap on a build node", "bwrap", true, "fail"}, + {"another tool on a build node", "initdb", true, "skip"}, + {"another tool on a desk", "initdb", false, "skip"}, + } { + if got := missingToolAction(c.tool, c.onBuildNode); got != c.want { + t.Errorf("%s: %q, want %q", c.name, got, c.want) + } + } +} + +// The levels a build module judges hand the tests the build node mark, with the siblings they read. +func TestEveryLevelHandsTheTestsTheBuildNodeMark(t *testing.T) { + m := &Module{Work: t.TempDir()} + if env := m.checkEnv("ws1"); !slices.Contains(env, BuildNodeEnv+"=1") { + t.Errorf("env %v lacks %s=1", env, BuildNodeEnv) + } +} + +// missingToolAction is what a test does when tool is not installed: "fail" for bubblewrap where the build module that +// judges says so (BuildNodeEnv), otherwise "skip". +func missingToolAction(tool string, onBuildNode bool) string { + if tool == "bwrap" && onBuildNode { + return "fail" + } + return "skip" +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/test.go b/modules/mesh-build-go/cmd/mesh-build-go/test.go index d571634d..6491c900 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/test.go @@ -83,7 +83,7 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) { case runner == RunnerWorkspace && (level == LevelUnit || level == LevelIntegration): return m.testAsCandidate(ctx, workspace, level, packages, pattern) case level == LevelUnit: - res, err = m.runLevel(ctx, tree, levelOpts{workspace: workspace, level: LevelUnit, packages: packages, run: pattern, bounds: Bounds{Run: m.unitBound()}, env: m.besideEnv(workspace)}) + res, err = m.runLevel(ctx, tree, levelOpts{workspace: workspace, level: LevelUnit, packages: packages, run: pattern, bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(workspace)}) case level == LevelIntegration: res, err = m.integration(ctx, workspace, tree, packages, pattern) default: @@ -128,6 +128,15 @@ func (m *Module) besideEnv(workspace string) []string { return []string{"MESH_CHECK_BESIDE=" + dir} } +// BuildNodeEnv is the mark a build module hands the tests it runs: this is a build node, where bubblewrap is declared +// and present, so a test that needs it fails where it is missing instead of skipping (issue 1309). +const BuildNodeEnv = "MESH_BUILD_NODE" + +// checkEnv is what every level hands the tests it runs: the build node mark and the siblings (besideEnv). +func (m *Module) checkEnv(workspace string) []string { + return append([]string{BuildNodeEnv + "=1"}, m.besideEnv(workspace)...) +} + // packagesArg reads the packages argument: a list of package patterns, as []any when it came over the bus as JSON and // as []string when an ask handed it in process (issue 1925). func packagesArg(v any) ([]string, error) { diff --git a/modules/mesh-build-go/cmd/mesh-build-go/test_real_integration_test.go b/modules/mesh-build-go/cmd/mesh-build-go/test_real_integration_test.go index 78a5a8f6..5603f45a 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/test_real_integration_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/test_real_integration_test.go @@ -33,6 +33,9 @@ func needTools(t *testing.T, what string, tools ...string) { t.Helper() for _, tool := range tools { if _, err := exec.LookPath(tool); err != nil { + if missingToolAction(tool, os.Getenv(BuildNodeEnv) == "1") == "fail" { + t.Fatalf("no %s installed on a build node (%s): every build node declares it, so the proof is not skipped here", tool, what) + } t.Skipf("NOT RUN: no %s installed where this check runs (%s); proven live on a build node", tool, what) } } diff --git a/modules/mesh-build-node/cmd/mesh-build-node/build_integration_test.go b/modules/mesh-build-node/cmd/mesh-build-node/build_integration_test.go index c113308f..901fc642 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/build_integration_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/build_integration_test.go @@ -24,7 +24,8 @@ import ( // name: an install from the public registry inside a shut-off level only fails, after 70 s of npm's retries. func realMachine(t *testing.T) (*Module, string) { t.Helper() - for _, p := range []string{"node", "npm", "bwrap"} { // bwrap: tsc, esbuild and the scripts run shut off (1309) + needBubblewrap(t) // tsc, esbuild and the scripts run shut off (1309) + for _, p := range []string{"node", "npm"} { if _, err := exec.LookPath(p); err != nil { t.Skipf("NOT RUN: no %s installed where this check runs (the mesh's Go toolchain image holds none); proven live on a build node", p) } diff --git a/modules/mesh-build-node/cmd/mesh-build-node/isolation_integration_test.go b/modules/mesh-build-node/cmd/mesh-build-node/isolation_integration_test.go index 095f16cf..19f933d6 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/isolation_integration_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/isolation_integration_test.go @@ -7,7 +7,6 @@ import ( "encoding/json" "net" "os" - "os/exec" "path/filepath" "strconv" "testing" @@ -34,9 +33,7 @@ fs.writeFileSync("seen.json", JSON.stringify(seen)); // resolves no name, reaches no socket of the machine (a TCP port on its loopback, a unix socket), and leaves the // shared cache unchanged. func TestARunStepReachesNoKeyLoginNameOrSocketAndLeavesTheCacheUnchanged(t *testing.T) { - if _, err := exec.LookPath("bwrap"); err != nil { - t.Skip("NOT RUN: no bubblewrap installed where this check runs; every build node has it (the module declares it)") - } + needBubblewrap(t) m, work := realMachine(t) home := m.Toolchain.Paths.Home key, login := filepath.Join(home, ".ssh", "mesh_ed25519"), filepath.Join(home, "broker") diff --git a/modules/mesh-build-node/cmd/mesh-build-node/needtools_test.go b/modules/mesh-build-node/cmd/mesh-build-node/needtools_test.go new file mode 100644 index 00000000..6c02049b --- /dev/null +++ b/modules/mesh-build-node/cmd/mesh-build-node/needtools_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "os" + "os/exec" + "testing" +) + +// bubblewrapAction is what a test that runs a step in bubblewrap does: "" when bubblewrap is there, "fail" where a +// build module that judges says this is a build node (MESH_BUILD_NODE=1, which mesh-build-go hands the tests), since +// every build node declares and has it, and "skip" anywhere else (issue 1309). +func bubblewrapAction(found bool, buildNodeMark string) string { + switch { + case found: + return "" + case buildNodeMark == "1": + return "fail" + } + return "skip" +} + +// needBubblewrap stops a test that runs a step in bubblewrap where there is none. +func needBubblewrap(t *testing.T) { + t.Helper() + _, err := exec.LookPath("bwrap") + switch bubblewrapAction(err == nil, os.Getenv("MESH_BUILD_NODE")) { + case "fail": + t.Fatal("no bubblewrap installed on a build node: every build node declares it, so the proof is not skipped here") + case "skip": + t.Skip("NOT RUN: no bubblewrap installed where this check runs; every build node has it (the module declares it)") + } +} + +func TestAMissingBubblewrapFailsOnABuildNodeAndSkipsElsewhere(t *testing.T) { + for _, c := range []struct { + name string + found bool + mark string + want string + }{ + {"there", true, "1", ""}, + {"missing on a desk", false, "", "skip"}, + {"missing on a build node", false, "1", "fail"}, + } { + if got := bubblewrapAction(c.found, c.mark); got != c.want { + t.Errorf("%s: %q, want %q", c.name, got, c.want) + } + } +} diff --git a/modules/mesh-build-node/cmd/mesh-build-node/package_integration_test.go b/modules/mesh-build-node/cmd/mesh-build-node/package_integration_test.go index 9c7a9c14..2801a94a 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/package_integration_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/package_integration_test.go @@ -45,7 +45,8 @@ func forgeRepo(t *testing.T, root, name string, files map[string]string) string // (its build script and npm pack shut off, nothing published), and a member whose lock names that version with that // integrity gets it in its node_modules; a member whose lock names it with another integrity fails naming both. func TestTheFetchPacksTheGroupsPackageAndServesItToAMember(t *testing.T) { - for _, tool := range []string{"node", "npm", "git", "bwrap"} { + needBubblewrap(t) + for _, tool := range []string{"node", "npm", "git"} { if _, err := exec.LookPath(tool); err != nil { t.Skipf("NOT RUN: no %s where this check runs; every build node has it", tool) } diff --git a/modules/mesh-build-node/cmd/mesh-build-node/publish_integration_test.go b/modules/mesh-build-node/cmd/mesh-build-node/publish_integration_test.go index 50882d7e..c8420b88 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/publish_integration_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/publish_integration_test.go @@ -81,7 +81,8 @@ func (r *npmRegistry) serve(t *testing.T) *httptest.Server { // the module's credential, once: a second trunk build of the same package finds its file there and publishes nothing; // a candidate publishes nothing at all. func TestARealTrunkPackageIsPublishedOnce(t *testing.T) { - for _, tool := range []string{"node", "npm", "bwrap"} { + needBubblewrap(t) + for _, tool := range []string{"node", "npm"} { if _, err := exec.LookPath(tool); err != nil { t.Skipf("NOT RUN: no %s where this check runs; every build node has it", tool) } diff --git a/modules/mesh-build-node/cmd/mesh-build-node/store_integration_test.go b/modules/mesh-build-node/cmd/mesh-build-node/store_integration_test.go index 372330bb..1ad3d262 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/store_integration_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/store_integration_test.go @@ -10,7 +10,6 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "path/filepath" "strings" "testing" @@ -45,9 +44,7 @@ func realStore(t *testing.T, m *Module) string { // 1587-S10 (ADR 0498's check): a done bundle names a reference whose tail is its digest, and that reference resolves in // a real artifact store to bytes of that digest. The build is the real tsc's and esbuild's, shut off. func TestADoneBundleResolvesInARealArtifactStore(t *testing.T) { - if _, err := exec.LookPath("bwrap"); err != nil { - t.Skip("NOT RUN: no bubblewrap installed where this check runs; every build node has it (the module declares it)") - } + needBubblewrap(t) m, _ := realMachine(t) addr := realStore(t, m) out, err := m.build(context.Background(), map[string]any{"workspace": "w1", "module": "modules/m", "artifact": "tools"}) -- 2.54.0