Fold the siblings into a package digest only when its tests reach MESH_CHECK_BESIDE, so a sibling move keeps unrelated verdicts (issue 1896)

This commit is contained in:
2026-10-23 14:02:59 +02:00
parent 15378cfba7
commit 47dbf87e33
3 changed files with 77 additions and 25 deletions
@@ -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)
}
}
@@ -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"))
@@ -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)
}