From 47dbf87e330aad52c43059c4e9b01efd3a956ff6 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 14:02:59 +0200 Subject: [PATCH] Fold the siblings into a package digest only when its tests reach MESH_CHECK_BESIDE, so a sibling move keeps unrelated verdicts (issue 1896) --- .../cmd/mesh-build-go/beside_digest_test.go | 69 ++++++++++++++----- .../mesh-build-go/cmd/mesh-build-go/digest.go | 31 +++++++-- .../cmd/mesh-build-go/digest_test.go | 2 +- 3 files changed, 77 insertions(+), 25 deletions(-) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/beside_digest_test.go b/modules/mesh-build-go/cmd/mesh-build-go/beside_digest_test.go index 9f14bd9c..3682a61b 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/beside_digest_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/beside_digest_test.go @@ -11,37 +11,72 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" ) -// The lead's decision L5 (the checker keeps a verdict of a core repository): the fetch writes the siblings it placed -// beside the workspace, and the digest folds each one's repository and commit into every package's digest and echoes -// them, so a verdict is kept only for the siblings it was judged with. -func TestTheSiblingsCommitsAreInEveryDigestAndEchoed(t *testing.T) { +// writeSiblings records the siblings the fetch placed, as the fetch does. +func writeSiblings(t *testing.T, m *Module, placed []Sibling) { + t.Helper() + raw, _ := json.Marshal(placed) + if err := os.WriteFile(filepath.Join(m.Work, "ws1", besideRecord), raw, 0o600); err != nil { + t.Fatal(err) + } +} + +// Issue 1896/AC1 and AC3 (ADR 0536 §1): a sibling's commit changes no digest of a package whose tests reach no file +// naming MESH_CHECK_BESIDE; the answer still echoes every placed sibling, in form 4. +func TestASiblingsCommitLeavesAPackageThatDoesNotReadTheSiblingsAsItWas(t *testing.T) { t.Parallel() m, _ := digestFixture(t) alone := digests(t, m, "unit") if len(alone.Beside) != 0 { t.Errorf("a workspace without siblings echoes %v", alone.Beside) } - write := func(placed []Sibling) { - raw, _ := json.Marshal(placed) - if err := os.WriteFile(filepath.Join(m.Work, "ws1", besideRecord), raw, 0o600); err != nil { - t.Fatal(err) - } - } other := strings.Repeat("b", 40) - write([]Sibling{{Repository: "novox/mesh-sdk", Commit: commitSHA, Dir: "mesh-sdk"}, {Repository: "novox/mesh-catalog", Commit: commitSHA, Dir: "mesh-catalog"}}) + writeSiblings(t, m, []Sibling{{Repository: "novox/mesh-sdk", Commit: commitSHA, Dir: "mesh-sdk"}, {Repository: "novox/mesh-catalog", Commit: commitSHA, Dir: "mesh-catalog"}}) with := digests(t, m, "unit") want := []string{"novox/mesh-catalog " + commitSHA, "novox/mesh-sdk " + commitSHA} if !slices.Equal(with.Beside, want) { t.Errorf("echoed %v, want %v", with.Beside, want) } - write([]Sibling{{Repository: "novox/mesh-sdk", Commit: other, Dir: "mesh-sdk"}, {Repository: "novox/mesh-catalog", Commit: commitSHA, Dir: "mesh-catalog"}}) + writeSiblings(t, m, []Sibling{{Repository: "novox/mesh-sdk", Commit: other, Dir: "mesh-sdk"}, {Repository: "novox/mesh-catalog", Commit: commitSHA, Dir: "mesh-catalog"}}) moved := digests(t, m, "unit") for p, d := range alone.Packages { - if with.Packages[p] == d { - t.Errorf("%s: the siblings left the digest as it was without them", p) + if with.Packages[p] != d || moved.Packages[p] != d { + t.Errorf("%s: a sibling left its digest changed though no test of it reads the siblings", p) } - if moved.Packages[p] == with.Packages[p] { - t.Errorf("%s: a sibling's new commit left the digest as it was", p) + } + if !slices.Equal(moved.Beside, []string{"novox/mesh-catalog " + commitSHA, "novox/mesh-sdk " + other}) { + t.Errorf("echoed %v after the move", moved.Beside) + } +} + +// Issue 1896/AC2: a package whose test imports an in-tree package naming MESH_CHECK_BESIDE is judged again when a +// sibling moves; a package beside it that does not import it is not. +func TestASiblingsCommitChangesTheDigestOfAPackageWhoseTestsReachAReaderOfTheSiblings(t *testing.T) { + t.Parallel() + m, tree := digestFixture(t) + for name, text := range map[string]string{ + "f/f.go": "package f\n\n// Beside is where the siblings lie.\nconst Beside = \"MESH_CHECK_BESIDE\"\n", + "e/e_test.go": "package e\n\nimport (\n\t\"testing\"\n\n\t\"fixture/digest/f\"\n)\n\nfunc TestE(t *testing.T) {\n\tif f.Beside == \"\" {\n\t\tt.Fatal(\"e\")\n\t}\n}\n", + "e/e.go": "package e\n", + "g/g_test.go": "package g\n\nimport \"testing\"\n\nfunc TestG(t *testing.T) {\n\t// MESH_CHECK_BESIDE named in the test's own file\n}\n", + "g/g.go": "package g\n", + } { + if err := os.MkdirAll(filepath.Dir(filepath.Join(tree, name)), 0o755); err != nil { + t.Fatal(err) + } + edit(t, tree, name, text) + } + writeSiblings(t, m, []Sibling{{Repository: "novox/mesh-sdk", Commit: commitSHA, Dir: "mesh-sdk"}}) + before := digests(t, m, "unit") + writeSiblings(t, m, []Sibling{{Repository: "novox/mesh-sdk", Commit: strings.Repeat("b", 40), Dir: "mesh-sdk"}}) + after := digests(t, m, "unit") + for _, p := range []string{"fixture/digest/e", "fixture/digest/g"} { + if len(before.Packages[p]) != 64 || before.Packages[p] == after.Packages[p] { + t.Errorf("%s reads the siblings and its digest stayed (%q, %q)", p, before.Packages[p], after.Packages[p]) + } + } + for _, p := range []string{"fixture/digest/a", "fixture/digest/c"} { + if before.Packages[p] != after.Packages[p] { + t.Errorf("%s does not read the siblings and its digest changed", p) } } } @@ -81,7 +116,7 @@ func TestTheIntegrationLevelDigestsUnderBothTags(t *testing.T) { t.Errorf("the integration digest listed %q", s.command) } } - if DigestVersion != "3" { + if DigestVersion != "4" { t.Errorf("the digest's form changed in kind and is still version %s", DigestVersion) } } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/digest.go b/modules/mesh-build-go/cmd/mesh-build-go/digest.go index 55412464..f3e2ed53 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/digest.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/digest.go @@ -5,6 +5,7 @@ package main // digest is unchanged is not run again. The tool answers in its call (it is short, ADR 0469 §2). import ( + "bytes" "context" "crypto/sha256" "encoding/hex" @@ -22,7 +23,12 @@ import ( // DigestVersion is the form of the digest's text: changed whenever what the digest hashes changes in kind. // 2: no Go version in the text; the toolchain enters a fingerprint as the agreed version (ADR 0477 §3). 3: the siblings' // commits (the lead's decision L5) and the integration level's tags integration,isolation (the checker's agreement). -const DigestVersion = "3" +// 4: the siblings' commits enter only the digest of a package whose tests reach a file naming MESH_CHECK_BESIDE (ADR 0536 §1). +const DigestVersion = "4" + +// besideVariable is the name a file bears when it reads where the siblings lie; a package whose tests reach such a file +// depends on the siblings' commits. +const besideVariable = "MESH_CHECK_BESIDE" // integrationTags are the build tags of the integration level, its run and its digest alike: integration, and isolation // for the bubblewrap proofs (mesh-sdk's and the build modules'), which a build node can run. @@ -132,8 +138,9 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) { if err != nil { return nil, err } + besideHeader := make([]string, 0, len(beside)) for _, b := range beside { - header = append(header, "beside "+b) + besideHeader = append(besideHeader, "beside "+b) } dirs, err := m.goModDirs(ctx, tree) @@ -146,7 +153,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) { if err != nil { return nil, err } - if err := digestModule(&res, pkgs, tree, header); err != nil { + if err := digestModule(&res, pkgs, tree, header, besideHeader); err != nil { return nil, err } } @@ -190,7 +197,7 @@ func parsePackages(out string) (map[string]*goPackage, error) { // digestModule adds to res the digest of each root package of one go.mod directory. A root is a package of the main // module that is listed, not only a dependency, and not a variant of another. -func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, header []string) error { +func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, header, beside []string) error { roots := []string{} for path, p := range pkgs { if p.ForTest == "" && !p.DepOnly && !p.Standard && !strings.HasSuffix(path, ".test") && p.Module != nil && p.Module.Main { @@ -208,7 +215,7 @@ func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, he if testMain == nil { return fmt.Errorf("go list gave no test binary for %s", path) } - d, err := packageDigest(root, testMain, pkgs, tree, header) + d, err := packageDigest(root, testMain, pkgs, tree, header, beside) if err != nil { return fmt.Errorf("%s: %w", path, err) } @@ -219,8 +226,9 @@ func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, he // packageDigest is the digest of one package: a hash over a sorted text of the header, the files of every package its // tests reach in the module's own tree (by their content, under their path in the tree), the version and checksum of -// every module outside it, and the package's own test data. A package of the standard library is the Go version's. -func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree string, header []string) (string, error) { +// every module outside it, and the package's own test data. The beside lines (the siblings' commits) join the header +// only when a file the tests reach names MESH_CHECK_BESIDE. A package of the standard library is the Go version's. +func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree string, header, beside []string) (string, error) { reached := map[string]*goPackage{root.ImportPath: root} for _, dep := range testMain.Deps { if p := pkgs[dep]; p != nil { @@ -228,6 +236,7 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s } } lines := []string{} + readsBeside := false for _, p := range reached { if p.Standard || p.Module == nil { continue @@ -246,6 +255,11 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s return "", err } lines = append(lines, l) + if !readsBeside { + if raw, err := os.ReadFile(filepath.Join(p.Dir, f)); err == nil && bytes.Contains(raw, []byte(besideVariable)) { + readsBeside = true + } + } } } data := filepath.Join(root.Dir, "testdata") @@ -263,6 +277,9 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s } } sort.Strings(lines) + if readsBeside { + header = append(append([]string{}, header...), beside...) + } h := sha256.New() for _, l := range append(append([]string{}, header...), lines...) { h.Write([]byte(l + "\n")) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go b/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go index ff1e8e6e..5e2d81d0 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go @@ -173,7 +173,7 @@ func TestAModuleOutsideTheTreeCountsByVersionAndChecksum(t *testing.T) { testMain := &goPackage{ImportPath: "x/r.test", Deps: []string{ext.ImportPath}} pkgs[root.ImportPath] = root digest := func() string { - d, err := packageDigest(root, testMain, pkgs, dir, []string{"digest 1"}) + d, err := packageDigest(root, testMain, pkgs, dir, []string{"digest 1"}, nil) if err != nil { t.Fatal(err) }