Check a delivery group's member beside its siblings' merges, judged by the group's controller (issue 867, ADR 0382)
mesh/merge-gate head 37fd73f3781e merged into trunk 4446027108fd — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 37fd73f3781e merged into trunk 4446027108fd — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/867-group-siblings delivered: every member is delivered
mesh/merge-gate head 37fd73f3781e merged into trunk 4446027108fd — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 37fd73f3781e merged into trunk 4446027108fd — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/867-group-siblings delivered: every member is delivered
A mesh-host change and a mesh-controller change that need each other could not both pass their own checks: each read the other at its base. The check now asks mesh-delivery for the group's open siblings into a base of the same name, reads each at its head merged onto its base, builds the judge from a controller sibling's merge in the isolated judge environment keyed on its inputs, and names the siblings in the verdict. With no answer from mesh-delivery the verdict says it could not learn them; a pull request with no sibling is checked as before. TestReplay867 replays the failure. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+187
-5
@@ -4,6 +4,8 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -122,6 +124,42 @@ type CheckSpec struct {
|
||||
// request (novox/hq issue 686): handed to the script as EnvTests and EnvStoreOnly, which it may read.
|
||||
Tests string
|
||||
StoreOnly bool
|
||||
// Siblings are the other open pull requests of this one's delivery group, each read beside the check at its head
|
||||
// merged onto its base head in place of the commit the mesh runs (novox/hq ADR 0382 §2). SiblingsUnknown says the
|
||||
// controller could not learn them, and the verdict says so.
|
||||
Siblings []Sibling
|
||||
SiblingsUnknown string
|
||||
}
|
||||
|
||||
// Sibling is one other open pull request of a delivery group (novox/hq ADR 0382): the repository it is in as
|
||||
// owner/repo, its number, its whole head, the branch it merges into, and Dir, the directory the check finds that
|
||||
// repository under beside it (an entry of Beside).
|
||||
type Sibling struct {
|
||||
Repository string
|
||||
Number int
|
||||
Head string
|
||||
Base string
|
||||
Dir string
|
||||
}
|
||||
|
||||
// JudgedSibling is a sibling as the check read it (ADR 0382 §4): its head, and BaseCommit, the base's head the head was
|
||||
// merged onto — both whole.
|
||||
type JudgedSibling struct {
|
||||
Repository string
|
||||
Number int
|
||||
Head string
|
||||
Base string
|
||||
BaseCommit string
|
||||
}
|
||||
|
||||
// siblingIn is the sibling read under a directory, nil when none is.
|
||||
func siblingIn(spec CheckSpec, dir string) *Sibling {
|
||||
for i := range spec.Siblings {
|
||||
if spec.Siblings[i].Dir == dir {
|
||||
return &spec.Siblings[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// selection is what a check hands the repository's script to narrow what it runs (novox/hq issue 686): nothing
|
||||
@@ -206,6 +244,12 @@ type CheckVerdict struct {
|
||||
// mesh-host's of the release branch with its commit, the one the release branch's controller vendors, the
|
||||
// change's own, or the one the change's own controller vendors. Empty for a check of any other base.
|
||||
Validator string
|
||||
// Siblings are the pull requests of the delivery group the check read beside it, each merged onto its base commit
|
||||
// (novox/hq ADR 0382 §4); SiblingsUnknown is why it could not learn them; JudgeFrom says where the judge came from
|
||||
// when the group made it (§3), the validator's source being in Validator.
|
||||
Siblings []JudgedSibling
|
||||
SiblingsUnknown string
|
||||
JudgeFrom string
|
||||
}
|
||||
|
||||
// Layer is one layer of a check, judged.
|
||||
@@ -412,6 +456,7 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
named := func(v CheckVerdict) CheckVerdict {
|
||||
v.Head, v.Trunk, v.Merge, v.Beside, v.Members = judged.Head, judged.Trunk, judged.Merge, judged.Beside, judged.Members
|
||||
v.Release, v.JudgedBy, v.BesideAt, v.Validator = spec.Release, judged.JudgedBy, judged.BesideAt, judged.Validator
|
||||
v.Siblings, v.SiblingsUnknown, v.JudgeFrom = judged.Siblings, spec.SiblingsUnknown, judged.JudgeFrom
|
||||
return v
|
||||
}
|
||||
// **What the merge would move is built from a checkout nothing of the check touches** (novox/hq issue 703): the
|
||||
@@ -467,7 +512,27 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
}
|
||||
b.Else = ""
|
||||
}
|
||||
if err := cloneAt(b.Repository, b.Ref, b.Else, dir); err != nil {
|
||||
if sibling := siblingIn(spec, dir); sibling != nil && dir != "mesh-lab" {
|
||||
// **A sibling's repository is read at the sibling's merge** (novox/hq ADR 0382 §2): its head merged onto its
|
||||
// base's head, as its own check merges it — in place of the commit the mesh runs.
|
||||
if err := cloneAt(b.Repository, "", "", dir); err != nil {
|
||||
return CheckVerdict{}, fmt.Errorf("beside it, %w", err)
|
||||
}
|
||||
m, err := mergeIntoTrunk(ctx, run, filepath.Join(root, dir), sibling.Base, sibling.Head)
|
||||
if err != nil {
|
||||
return CheckVerdict{}, fmt.Errorf("beside it, the sibling %s#%d: %w", sibling.Repository, sibling.Number, err)
|
||||
}
|
||||
judged.Siblings = append(judged.Siblings, JudgedSibling{Repository: sibling.Repository, Number: sibling.Number,
|
||||
Head: m.Head, Base: sibling.Base, BaseCommit: m.Trunk})
|
||||
if len(m.Conflicts) > 0 {
|
||||
said := fmt.Sprintf("the sibling %s#%d (head %s) does not merge cleanly into its base %s %s: in conflict %s",
|
||||
sibling.Repository, sibling.Number, twelve(m.Head), sibling.Base, twelve(m.Trunk), strings.Join(m.Conflicts, ", "))
|
||||
say("check", "%s", said)
|
||||
return named(CheckVerdict{Verdict: "fail", Summary: said, Report: said, Took: time.Since(began),
|
||||
Gate: &Layer{Verdict: "fail", Summary: said}, Repo: &Layer{Verdict: "fail", Summary: said}}), nil
|
||||
}
|
||||
readAt[dir] = sibling.Base
|
||||
} else if err := cloneAt(b.Repository, b.Ref, b.Else, dir); err != nil {
|
||||
return CheckVerdict{}, fmt.Errorf("beside it, %w", err)
|
||||
}
|
||||
if dir == "mesh-lab" {
|
||||
@@ -491,7 +556,12 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
}
|
||||
judged.Beside[dir] = at
|
||||
}
|
||||
say("check", "beside it %s at %s", dir, refWords(readAt[dir]))
|
||||
if sibling := siblingIn(spec, dir); sibling != nil && dir != "mesh-lab" {
|
||||
say("check", "beside it %s at the merge of %s#%d (%s) onto %s", dir, sibling.Repository, sibling.Number,
|
||||
short(sibling.Head), sibling.Base)
|
||||
} else {
|
||||
say("check", "beside it %s at %s", dir, refWords(readAt[dir]))
|
||||
}
|
||||
}
|
||||
|
||||
// A delivery group's other heads (novox/hq ADR 0239), each at its head, for the gate to compose with this.
|
||||
@@ -658,7 +728,7 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
}
|
||||
defer os.RemoveAll(judgeHome)
|
||||
inJudgeBuild := func(dir string, _ []string, command ...string) []string {
|
||||
args := in(spec.Toolchain, dir, JudgeBuildEnv(judgeHome, judgeGoCache), command...)
|
||||
args := in(spec.Toolchain, dir, judgeBuildEnv(judgeHome, judgeGoCache), command...)
|
||||
if spec.JudgeCache == "" {
|
||||
return args // under the check's root, in the workspace mounted already
|
||||
}
|
||||
@@ -685,10 +755,15 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
|
||||
// The judge. Its failing to build is the change's fault when the change is the judge or its validator,
|
||||
// and the check's when it is the controller the mesh runs.
|
||||
gate, judgeFault, err := judgeFor(ctx, labelled, run, spec, root, tree, judged.BesideAt, inJudgeBuild, say)
|
||||
var siblingJudge siblingJudgement
|
||||
gate, judgeFault, err := judgeFor(ctx, labelled, run, spec, root, tree, judged.BesideAt, judged, &siblingJudge, inJudgeBuild, say)
|
||||
if err != nil {
|
||||
return CheckVerdict{}, err
|
||||
}
|
||||
judged.JudgeFrom = siblingJudge.From
|
||||
if siblingJudge.Validator != "" {
|
||||
judged.Validator = siblingJudge.Validator
|
||||
}
|
||||
if spec.Release != "" {
|
||||
// **A release-branch judgement says so** (novox/hq ADR 0358 §4): the branch, the controller's commit that
|
||||
// judged, and each repository read beside it at its branch and commit.
|
||||
@@ -697,7 +772,9 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
judgedBy = tree
|
||||
}
|
||||
judged.JudgedBy = headAt(ctx, run, judgedBy)
|
||||
judged.Validator = releaseValidator(spec, judged.BesideAt, judged.Beside)
|
||||
if siblingJudge.Validator == "" {
|
||||
judged.Validator = releaseValidator(spec, judged.BesideAt, judged.Beside)
|
||||
}
|
||||
said := releaseWords(spec.Release, judged.JudgedBy, judged.Validator, judged.BesideAt, judged.Beside)
|
||||
fmt.Fprintf(&out, "--- %s\n", said)
|
||||
say("check", "%s", said)
|
||||
@@ -1160,8 +1237,12 @@ func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFil
|
||||
// has that branch too, or with the change's own validator for a change to the node-engine — never the controller the
|
||||
// mesh runs. besideAt is the branch each repository beside it was read at.
|
||||
func judgeFor(ctx context.Context, labelled, run Runner, spec CheckSpec, root, tree string, besideAt map[string]string,
|
||||
judged CheckVerdict, siblings *siblingJudgement,
|
||||
inToolchain func(string, []string, ...string) []string, say func(step, format string, args ...any)) (string, string, error) {
|
||||
gated := spec.Gated()
|
||||
if bin, fault, done, err := judgeFromTheGroup(ctx, labelled, run, spec, root, tree, judged, siblings, inToolchain, say); done {
|
||||
return bin, fault, err
|
||||
}
|
||||
switch spec.Judge {
|
||||
case "self":
|
||||
bin := filepath.Join(root, "bin", "judge-of-itself")
|
||||
@@ -1263,6 +1344,10 @@ func releaseJudge(ctx context.Context, run, plain Runner, spec CheckSpec, root s
|
||||
return bin, dir, nil
|
||||
}
|
||||
|
||||
// judgeBuildEnv is JudgeBuildEnv, a seam only a test replaces: to show what the plain environment would let a planted
|
||||
// workspace do to a judge's build.
|
||||
var judgeBuildEnv = JudgeBuildEnv
|
||||
|
||||
// JudgeBuildEnv is the environment every judge is built in (novox/hq ADR 0358 §6, issue 482): home, made fresh for the
|
||||
// build, as HOME and configuration; goCache, the judge cache's Go cache that no check's container mounts; no global
|
||||
// or system git configuration; no Go environment file; and no version stamped from git, so no git runs at all.
|
||||
@@ -1859,3 +1944,100 @@ func lastLine(s string) string {
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
return strings.TrimSpace(lines[len(lines)-1])
|
||||
}
|
||||
|
||||
// siblingJudgement says, besides the judge's path, where a judge the group made came from (novox/hq ADR 0382 §3): From
|
||||
// the controller it was built from and Validator the node-engine validator in it. Both empty for any other judge.
|
||||
type siblingJudgement struct {
|
||||
From, Validator string
|
||||
}
|
||||
|
||||
// judgeFromTheGroup builds the judge of a member that has siblings (novox/hq ADR 0382 §3); done says it did (or failed
|
||||
// to), and false leaves the judge to the rules before it.
|
||||
//
|
||||
// - A member with a mesh-controller sibling is judged by the controller built from that sibling's merge (read beside
|
||||
// the check), with the node-engine's validator from the member's own change when it is mesh-host's, else from a
|
||||
// mesh-host sibling's merge, else the one that controller vendors.
|
||||
// - A mesh-controller member is judged by itself, with a mesh-host sibling's validator in place of the one it vendors.
|
||||
//
|
||||
// **The judge is unreviewed code** and is built where nothing the change's script does can reach it: from a copy of the
|
||||
// merge made apart from the workspace's tree, with the judge-build environment (JudgeBuildEnv) — a home and Go cache of
|
||||
// its own, no global or system git configuration, no version stamping from git — and kept in the judge cache, which no
|
||||
// check's container mounts, only for the same inputs: the controller merge and the base commits it was merged onto, the
|
||||
// validator's source and commit.
|
||||
func judgeFromTheGroup(ctx context.Context, labelled, run Runner, spec CheckSpec, root, tree string, judged CheckVerdict,
|
||||
said *siblingJudgement, inToolchain func(string, []string, ...string) []string,
|
||||
say func(step, format string, args ...any)) (string, string, bool, error) {
|
||||
controller, host := siblingIn(spec, "mesh-controller"), siblingIn(spec, "mesh-host")
|
||||
var source, validatorDir, from, validator string
|
||||
var inputs []string
|
||||
merge := func(dir string, s *Sibling) string {
|
||||
return fmt.Sprintf("sibling %s#%d merge %s", s.Repository, s.Number, judged.Beside[dir])
|
||||
}
|
||||
switch {
|
||||
case spec.Judge == "self" && host != nil:
|
||||
source, validatorDir = tree, filepath.Join(root, "mesh-host")
|
||||
from = "this change's own merge " + judged.Head
|
||||
validator = merge("mesh-host", host) + "'s validator"
|
||||
case spec.Judge != "self" && controller != nil:
|
||||
source = filepath.Join(root, "mesh-controller")
|
||||
from = merge("mesh-controller", controller)
|
||||
switch {
|
||||
case spec.Judge == "validator":
|
||||
validatorDir, validator = tree, "the change's own validator"
|
||||
case host != nil:
|
||||
validatorDir, validator = filepath.Join(root, "mesh-host"), merge("mesh-host", host)+"'s validator"
|
||||
default:
|
||||
validator = "the validator " + merge("mesh-controller", controller) + " vendors"
|
||||
}
|
||||
default:
|
||||
return "", "", false, nil
|
||||
}
|
||||
for _, sib := range judged.Siblings {
|
||||
inputs = append(inputs, "base "+sib.Repository+" "+sib.BaseCommit+" head "+sib.Head)
|
||||
}
|
||||
// A merge made for a check is a new commit each time (its time is in it); what it holds is the same for the same
|
||||
// heads and bases, so the key names those and the trees the judge is built from.
|
||||
inputs = append(inputs, "member "+judged.Head+" onto "+judged.Trunk, "controller tree "+treeAt(ctx, run, source))
|
||||
if validatorDir != "" {
|
||||
inputs = append(inputs, "validator tree "+treeAt(ctx, run, validatorDir))
|
||||
}
|
||||
sum := sha256.Sum256([]byte(strings.Join(inputs, "\n")))
|
||||
key := "siblings-" + hex.EncodeToString(sum[:16])
|
||||
bin := filepath.Join(root, "bin", "judge-of-the-group")
|
||||
said.From, said.Validator = from, validator
|
||||
if judgeKept(keptJudges(spec.JudgeCache), key, bin) {
|
||||
say("check", "judged by %s, with %s, built for an earlier check of the same inputs", from, validator)
|
||||
return bin, "", true, nil
|
||||
}
|
||||
src := filepath.Join(root, "judge-src")
|
||||
defer os.RemoveAll(src)
|
||||
if err := copyCheckout(ctx, run, source, src); err != nil {
|
||||
return "", "", true, fmt.Errorf("the controller that judges (%s) could not be copied: %w", from, err)
|
||||
}
|
||||
if validatorDir != "" {
|
||||
vendored := filepath.Join(src, "vendor", "github.com", "novox", "mesh-host")
|
||||
for _, pkg := range []string{"validate", filepath.Join("internal", "declaration")} {
|
||||
if err := replaceGoFiles(filepath.Join(validatorDir, pkg), filepath.Join(vendored, pkg)); err != nil {
|
||||
return "", "the validator (" + validator + ") could not be put in the judge: " + err.Error(), true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := labelled(ctx, root, "docker", inToolchain(src, nil,
|
||||
"go", "build", "-o", bin, "./cmd/mesh-controller")...); err != nil {
|
||||
return "", "the controller that judges (" + from + ") does not build with " + validator + ": " + firstLine(err.Error()),
|
||||
true, nil
|
||||
}
|
||||
keepJudge(keptJudges(spec.JudgeCache), key, bin, say)
|
||||
say("check", "judged by %s, with %s", from, validator)
|
||||
return bin, "", true, nil
|
||||
}
|
||||
|
||||
// treeAt is the tree object a checkout's commit holds: the content a merge made for a check has, whatever time it was
|
||||
// made at.
|
||||
func treeAt(ctx context.Context, run Runner, dir string) string {
|
||||
out, err := run(ctx, dir, "git", "rev-parse", "--verify", "--quiet", "HEAD^{tree}")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(out)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// **A delivery group's member is checked beside the other members' heads** (novox/hq ADR 0382, issue 867): a
|
||||
// mesh-host change and a mesh-controller change that need each other. The sibling's repository is read beside the
|
||||
// check at the sibling's head merged onto its base head, in place of the commit the mesh runs.
|
||||
|
||||
// aSiblingWorld is the mesh's two core repositories as the trunk holds them, and a pull request into each that the
|
||||
// other needs: the controller's adds a lock line the node-engine's own check reads, and builds only with the
|
||||
// node-engine's new validator word.
|
||||
type aSiblingWorld struct {
|
||||
controller, host string // repositories
|
||||
controllerHead, hostHead string // the pull requests' whole heads
|
||||
controllerMain, hostMain string // the trunks' heads
|
||||
}
|
||||
|
||||
func theSiblingWorld(t *testing.T) aSiblingWorld {
|
||||
t.Helper()
|
||||
var w aSiblingWorld
|
||||
// The controller's trunk: a judge saying who it is, its lock without the new line, and the check that judges it.
|
||||
controllerFiles := aJudgeOf("the trunk's controller")
|
||||
controllerFiles["lock.txt"] = "foundation: old\n"
|
||||
controllerFiles["module.json"] = `{"module":"mesh-controller"}`
|
||||
controllerFiles[CheckScript] = "true\n"
|
||||
var controllerRelease string
|
||||
w.controller, w.controllerMain, controllerRelease = aRepositoryWithARelease(t, controllerFiles, map[string]string{"r": "r"})
|
||||
_ = controllerRelease
|
||||
gitInRepo(t, w.controller, "checkout", "--quiet", "-b", "change", "main")
|
||||
change := aJudgeOf("the sibling controller")
|
||||
change["lock.txt"] = "foundation: new\n"
|
||||
w.controllerHead = commitFiles(t, w.controller, change)
|
||||
gitInRepo(t, w.controller, "checkout", "--quiet", "main")
|
||||
// The node-engine's trunk: a validator, and the check that reads the controller's lock beside it.
|
||||
hostFiles := aHostSaying("the trunk's validator")
|
||||
hostFiles["module.json"] = `{"module":"mesh-host"}`
|
||||
hostFiles[CheckScript] = "set -e\ngrep -q 'foundation: new' \"$MESH_CHECK_BESIDE/mesh-controller/lock.txt\"\n"
|
||||
w.host, w.hostMain, _ = aRepositoryWithARelease(t, hostFiles, map[string]string{"r": "r"})
|
||||
gitInRepo(t, w.host, "checkout", "--quiet", "-b", "change", "main")
|
||||
w.hostHead = commitFiles(t, w.host, aHostSaying("the sibling's validator"))
|
||||
gitInRepo(t, w.host, "checkout", "--quiet", "main")
|
||||
return w
|
||||
}
|
||||
|
||||
func (w aSiblingWorld) beside() map[string]Beside {
|
||||
return map[string]Beside{"mesh-controller": {Repository: w.controller, Ref: w.controllerMain},
|
||||
"mesh-host": {Repository: w.host, Ref: w.hostMain}}
|
||||
}
|
||||
|
||||
// hostCheck is the node-engine's pull request checked, with or without its sibling.
|
||||
func (w aSiblingWorld) hostCheck(t *testing.T, id, workspace, cache string, siblings ...Sibling) CheckVerdict {
|
||||
t.Helper()
|
||||
if workspace == "" {
|
||||
workspace = t.TempDir()
|
||||
}
|
||||
beside := w.beside()
|
||||
delete(beside, "mesh-host")
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: w.host, Ref: w.hostHead, Owner: "novox",
|
||||
Repo: "mesh-host", Number: 114, Base: "main", Judge: "validator", JudgeCache: cache,
|
||||
Paths: []string{"validate/v.go"}, Modules: []string{"mesh-host"}, Beside: beside, Siblings: siblings,
|
||||
Toolchain: "golang"}, workspace, aFactsRegistry(t), GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// controllerCheck is the controller's pull request checked, with or without its sibling.
|
||||
func (w aSiblingWorld) controllerCheck(t *testing.T, id, cache string, siblings ...Sibling) CheckVerdict {
|
||||
t.Helper()
|
||||
beside := w.beside()
|
||||
delete(beside, "mesh-controller")
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: w.controller, Ref: w.controllerHead, Owner: "novox",
|
||||
Repo: "mesh-controller", Number: 346, Base: "main", Judge: "self", JudgeCache: cache,
|
||||
Paths: []string{"lock.txt"}, Modules: []string{"mesh-controller"}, Beside: beside, Siblings: siblings,
|
||||
Toolchain: "golang"}, t.TempDir(), aFactsRegistry(t), GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (w aSiblingWorld) controllerSibling() Sibling {
|
||||
return Sibling{Repository: "novox/mesh-controller", Number: 346, Head: w.controllerHead, Base: "main", Dir: "mesh-controller"}
|
||||
}
|
||||
|
||||
func (w aSiblingWorld) hostSibling() Sibling {
|
||||
return Sibling{Repository: "novox/mesh-host", Number: 114, Head: w.hostHead, Base: "main", Dir: "mesh-host"}
|
||||
}
|
||||
|
||||
// The node-engine's change fails its own check without its sibling (the controller's lock beside it is the trunk's),
|
||||
// and passes with it read at its merge: the check reads the lock the sibling's change brings, the judge is the
|
||||
// controller built from the sibling's merge with the node-engine change's own validator, and the verdict names both.
|
||||
func TestAHostMemberIsCheckedBesideItsControllerSibling(t *testing.T) {
|
||||
aRuntimeThatBuildsTheJudge(t)
|
||||
w := theSiblingWorld(t)
|
||||
|
||||
alone := w.hostCheck(t, "check-867-host-alone", "", "")
|
||||
if alone.Repo == nil || alone.Repo.Verdict != "fail" || len(alone.Siblings) != 0 {
|
||||
t.Fatalf("with no sibling the node-engine change should fail its own check (the lock beside it is the trunk's): %+v",
|
||||
alone.Repo)
|
||||
}
|
||||
|
||||
v := w.hostCheck(t, "check-867-host", "", "", w.controllerSibling())
|
||||
if v.Repo == nil || v.Repo.Verdict != "pass" {
|
||||
t.Fatalf("beside its sibling the node-engine change should pass its own check: %+v\n%s", v.Repo, v.Report)
|
||||
}
|
||||
if v.Gate == nil || v.Gate.Verdict != "pass" || !strings.Contains(v.Gate.Summary, "the sibling controller with the sibling's validator") {
|
||||
t.Fatalf("the judge was not the controller built from the sibling's merge with the change's own validator: %+v\n%s",
|
||||
v.Gate, v.Report)
|
||||
}
|
||||
baseCommit := w.controllerMain
|
||||
if len(v.Siblings) != 1 || v.Siblings[0] != (JudgedSibling{Repository: "novox/mesh-controller", Number: 346,
|
||||
Head: w.controllerHead, Base: "main", BaseCommit: baseCommit}) {
|
||||
t.Fatalf("the verdict names siblings %+v, not mesh-controller#346 at %s onto %s", v.Siblings, w.controllerHead, baseCommit)
|
||||
}
|
||||
if v.Beside["mesh-controller"] == baseCommit[:12] || v.Beside["mesh-controller"] == w.controllerHead[:12] {
|
||||
t.Fatalf("the controller was read at %s, which is the trunk's or the head and not their merge", v.Beside["mesh-controller"])
|
||||
}
|
||||
if want := "sibling novox/mesh-controller#346 merge " + v.Beside["mesh-controller"]; v.JudgeFrom != want {
|
||||
t.Fatalf("the judge came from %q, not %q", v.JudgeFrom, want)
|
||||
}
|
||||
if v.Validator != "the change's own validator" {
|
||||
t.Fatalf("the validator was %q", v.Validator)
|
||||
}
|
||||
// The statuses' "beside siblings" part is the forge module's to write, from the verdict's siblings.
|
||||
if strings.Contains(v.Gate.Summary, "beside siblings") || strings.Contains(v.Repo.Summary, "beside siblings") {
|
||||
t.Fatalf("the check writes the siblings mark itself:\n%s\n%s", v.Gate.Summary, v.Repo.Summary)
|
||||
}
|
||||
// The base of the sibling is the trunk of the sibling's repository, as the trunk stood when the check cloned it.
|
||||
if v.Trunk != w.hostMain[:12] {
|
||||
t.Fatalf("the member's own trunk commit is %s, not %s", v.Trunk, w.hostMain[:12])
|
||||
}
|
||||
}
|
||||
|
||||
// The controller's change fails with no sibling (it builds only with the node-engine change's validator), and passes with
|
||||
// it: judged by itself, with the sibling node-engine's validator in place of the one it vendors.
|
||||
func TestAControllerMemberIsJudgedByItselfWithItsHostSiblingsValidator(t *testing.T) {
|
||||
aRuntimeThatBuildsTheJudge(t)
|
||||
w := theSiblingWorld(t)
|
||||
// The controller change calls a function only the node-engine change brings.
|
||||
gitInRepo(t, w.controller, "checkout", "--quiet", "change")
|
||||
main := aJudgeOf("the sibling controller")["cmd/mesh-controller/main.go"]
|
||||
w.controllerHead = commitFiles(t, w.controller, map[string]string{
|
||||
"cmd/mesh-controller/main.go": strings.Replace(main, "validate.Word)", "validate.Word+validate.More)", 1)})
|
||||
gitInRepo(t, w.controller, "checkout", "--quiet", "main")
|
||||
gitInRepo(t, w.host, "checkout", "--quiet", "change")
|
||||
w.hostHead = commitFiles(t, w.host, map[string]string{
|
||||
"validate/more.go": "package validate\n\nconst More = \" and more\"\n"})
|
||||
gitInRepo(t, w.host, "checkout", "--quiet", "main")
|
||||
|
||||
alone := w.controllerCheck(t, "check-867-controller-alone", "")
|
||||
if alone.Gate == nil || alone.Gate.Verdict != "fail" || !strings.Contains(alone.Gate.Summary, "does not build") {
|
||||
t.Fatalf("with no sibling the controller change should not build (the validator it vendors lacks More): %+v", alone.Gate)
|
||||
}
|
||||
|
||||
v := w.controllerCheck(t, "check-867-controller", "", w.hostSibling())
|
||||
if v.Gate == nil || v.Gate.Verdict != "pass" || !strings.Contains(v.Gate.Summary, "the sibling controller with the sibling's validator and more") {
|
||||
t.Fatalf("the controller change was not judged by itself with the sibling's validator: %+v\n%s", v.Gate, v.Report)
|
||||
}
|
||||
if len(v.Siblings) != 1 || v.Siblings[0].Repository != "novox/mesh-host" || v.Siblings[0].BaseCommit != w.hostMain {
|
||||
t.Fatalf("the verdict names siblings %+v", v.Siblings)
|
||||
}
|
||||
if want := "sibling novox/mesh-host#114 merge " + v.Beside["mesh-host"] + "'s validator"; v.Validator != want {
|
||||
t.Fatalf("the validator was %q, not %q", v.Validator, want)
|
||||
}
|
||||
if !strings.HasPrefix(v.JudgeFrom, "this change's own merge ") {
|
||||
t.Fatalf("the judge came from %q", v.JudgeFrom)
|
||||
}
|
||||
// The member's own tree keeps the validator it vendors: its own script reads the vendored files as they are.
|
||||
if body := gitInRepo(t, w.controller, "show", "main:vendor/github.com/novox/mesh-host/validate/v.go"); strings.Contains(body, "sibling") {
|
||||
t.Fatalf("the trunk's vendored validator was touched: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// A sibling head that does not merge cleanly onto its base fails the check, naming the sibling and the files.
|
||||
func TestASiblingThatDoesNotMergeCleanlyFailsTheCheckNamingIt(t *testing.T) {
|
||||
aRuntimeThatBuildsTheJudge(t)
|
||||
w := theSiblingWorld(t)
|
||||
// The base moves under the sibling's head on the same line of the same file.
|
||||
gitInRepo(t, w.controller, "checkout", "--quiet", "main")
|
||||
commitFiles(t, w.controller, map[string]string{"lock.txt": "foundation: elsewhere\n"})
|
||||
v := w.hostCheck(t, "check-867-conflict", "", "", w.controllerSibling())
|
||||
if v.Verdict != "fail" || v.Gate == nil || v.Gate.Verdict != "fail" || v.Repo == nil || v.Repo.Verdict != "fail" {
|
||||
t.Fatalf("a sibling that does not merge was answered %+v / %+v", v.Gate, v.Repo)
|
||||
}
|
||||
for _, want := range []string{"novox/mesh-controller#346", "does not merge cleanly", "lock.txt", w.controllerHead[:12]} {
|
||||
if !strings.Contains(v.Summary, want) {
|
||||
t.Fatalf("the failure does not say %q: %s", want, v.Summary)
|
||||
}
|
||||
}
|
||||
if len(v.Siblings) != 1 || v.Siblings[0].Number != 346 {
|
||||
t.Fatalf("the verdict does not name the sibling: %+v", v.Siblings)
|
||||
}
|
||||
}
|
||||
|
||||
// A check that could not learn its siblings reads none and says so; one with none to read is judged as before.
|
||||
func TestACheckThatCouldNotLearnItsSiblingsSaysSo(t *testing.T) {
|
||||
aRuntimeThatBuildsTheJudge(t)
|
||||
w := theSiblingWorld(t)
|
||||
beside := w.beside()
|
||||
delete(beside, "mesh-host")
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: "check-867-unknown", Repository: w.host, Ref: w.hostHead,
|
||||
Owner: "novox", Repo: "mesh-host", Number: 114, Base: "main", Judge: "validator", Paths: []string{"validate/v.go"},
|
||||
Modules: []string{"mesh-host"}, Beside: beside, SiblingsUnknown: "mesh-delivery did not answer",
|
||||
Toolchain: "golang"}, t.TempDir(), aFactsRegistry(t), GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.SiblingsUnknown != "mesh-delivery did not answer" || len(v.Siblings) != 0 || v.JudgeFrom != "" {
|
||||
t.Fatalf("the verdict says %q, siblings %+v, judge from %q", v.SiblingsUnknown, v.Siblings, v.JudgeFrom)
|
||||
}
|
||||
if !strings.Contains(v.Gate.Summary, "the trunk's controller with the sibling's validator") {
|
||||
t.Fatalf("a check without siblings was not judged as before: %+v", v.Gate)
|
||||
}
|
||||
if strings.Contains(v.Gate.Summary, "[beside siblings") {
|
||||
t.Fatalf("the statuses name siblings that were not read: %s", v.Gate.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
// The judge a group made is kept only for the same inputs: the controller's merge and the base commits it was merged
|
||||
// onto, the validator's source and commit. The same inputs take the judge kept; any other builds it again.
|
||||
func TestTheGroupsJudgeIsReusedOnlyForTheSameInputs(t *testing.T) {
|
||||
built := aRuntimeThatBuildsTheJudge(t)
|
||||
w := theSiblingWorld(t)
|
||||
cache := t.TempDir()
|
||||
w.hostCheck(t, "check-867-cache-1", "", cache, w.controllerSibling())
|
||||
if n := builds(t, built); n != 1 {
|
||||
t.Fatalf("the first check built %d judge(s)", n)
|
||||
}
|
||||
v := w.hostCheck(t, "check-867-cache-2", "", cache, w.controllerSibling())
|
||||
if n := builds(t, built); n != 1 || v.Gate.Verdict != "pass" {
|
||||
t.Fatalf("the same inputs built the judge again (%d builds) or did not pass: %+v", n, v.Gate)
|
||||
}
|
||||
// The controller sibling's head moves: another merge, another judge.
|
||||
gitInRepo(t, w.controller, "checkout", "--quiet", "change")
|
||||
w.controllerHead = commitFiles(t, w.controller, map[string]string{"extra.txt": "x"})
|
||||
gitInRepo(t, w.controller, "checkout", "--quiet", "main")
|
||||
w.hostCheck(t, "check-867-cache-3", "", cache, w.controllerSibling())
|
||||
if n := builds(t, built); n != 2 {
|
||||
t.Fatalf("a moved sibling head did not build the judge again: %d builds", n)
|
||||
}
|
||||
// The controller's base moves: the merge differs again.
|
||||
commitFiles(t, w.controller, map[string]string{"base.txt": "moved"})
|
||||
w.hostCheck(t, "check-867-cache-4", "", cache, w.controllerSibling())
|
||||
if n := builds(t, built); n != 3 {
|
||||
t.Fatalf("a moved base did not build the judge again: %d builds", n)
|
||||
}
|
||||
// The validator's source moves: the member's own change differs.
|
||||
gitInRepo(t, w.host, "checkout", "--quiet", "change")
|
||||
w.hostHead = commitFiles(t, w.host, aHostSaying("another validator"))
|
||||
gitInRepo(t, w.host, "checkout", "--quiet", "main")
|
||||
w.hostCheck(t, "check-867-cache-5", "", cache, w.controllerSibling())
|
||||
if n := builds(t, built); n != 4 {
|
||||
t.Fatalf("a moved validator did not build the judge again: %d builds", n)
|
||||
}
|
||||
}
|
||||
|
||||
// The contrast the isolation test stands on: built the old way, in the workspace with its HOME and no judge-build
|
||||
// environment, the same planted Go environment file takes effect in the group's judge build (its toolexec hook runs in
|
||||
// the judge's sources). Without this, the test below could not fail.
|
||||
func TestWithThePlainEnvironmentThePlantedWorkspaceReachesTheGroupsJudgeBuild(t *testing.T) {
|
||||
aRuntimeThatBuildsTheJudge(t)
|
||||
was := judgeBuildEnv
|
||||
judgeBuildEnv = func(string, string) []string { return nil }
|
||||
t.Cleanup(func() { judgeBuildEnv = was })
|
||||
w := theSiblingWorld(t)
|
||||
workspace, ran := aPlantedWorkspace(t)
|
||||
w.hostCheck(t, "check-867-plain", workspace, "", w.controllerSibling())
|
||||
if body, _ := readFile(ran); !strings.Contains(body, "/judge-src") {
|
||||
t.Fatalf("with the plain environment the planted hook did not run in the judge's build, so the isolation test proves nothing: %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
// **The group's judge is unreviewed code, built where nothing the change's script does can reach it**: a git hook, a
|
||||
// Go environment file and a Go cache planted in the workspace, and a script writing into its home, change nothing in the
|
||||
// judge; the judge cache is mounted into judge builds alone.
|
||||
func TestTheGroupsJudgeIsBuiltWhereNothingTheChangeDoesReachesIt(t *testing.T) {
|
||||
built := aRuntimeThatBuildsTheJudge(t)
|
||||
homes := filepath.Join(t.TempDir(), "homes")
|
||||
t.Setenv("JUDGE_HOMES", homes)
|
||||
runs := filepath.Join(t.TempDir(), "runs")
|
||||
t.Setenv("RUNS", runs)
|
||||
w := theSiblingWorld(t)
|
||||
// The member's own script (the base branch's) touches git in its tree and writes into its home.
|
||||
gitInRepo(t, w.host, "checkout", "--quiet", "main")
|
||||
commitFiles(t, w.host, map[string]string{CheckScript: "set -e\ngit status >/dev/null 2>&1 || true\n" +
|
||||
"echo planted > \"$HOME/.gitconfig\"\ngrep -q 'foundation: new' \"$MESH_CHECK_BESIDE/mesh-controller/lock.txt\"\n"})
|
||||
w.hostMain = gitInRepo(t, w.host, "rev-parse", "main")
|
||||
gitInRepo(t, w.host, "checkout", "--quiet", "change")
|
||||
gitInRepo(t, w.host, "rebase", "--quiet", "main")
|
||||
w.hostHead = gitInRepo(t, w.host, "rev-parse", "HEAD")
|
||||
gitInRepo(t, w.host, "checkout", "--quiet", "main")
|
||||
workspace, ran := aPlantedWorkspace(t)
|
||||
cache := t.TempDir()
|
||||
v := w.hostCheck(t, "check-867-planted", workspace, cache, w.controllerSibling())
|
||||
if v.Gate == nil || v.Gate.Verdict != "pass" || v.Repo == nil || v.Repo.Verdict != "pass" {
|
||||
t.Fatalf("the check did not pass: %+v %+v\n%s", v.Gate, v.Repo, v.Report)
|
||||
}
|
||||
if body, _ := readFile(ran); !strings.Contains(body, "/check/mesh-host") {
|
||||
t.Fatalf("the planted hook never ran in the member's tree, so this test proves nothing: %q", body)
|
||||
}
|
||||
if body, _ := readFile(ran); strings.Contains(body, "/judge-src") || judgeRanTheHook(t, ran) != "" {
|
||||
t.Fatalf("the hook planted in the workspace ran during the group's judge:\n%s", body)
|
||||
}
|
||||
if body, _ := readFile(homes); len(body) == 0 || strings.Contains(body, workspace) {
|
||||
t.Fatalf("the judge was built with a HOME in the workspace: %q", body)
|
||||
}
|
||||
said, _ := readFile(built)
|
||||
if !strings.Contains(said, "GOCACHE="+filepath.Join(cache, "go-cache")+" GOENV=off go build") {
|
||||
t.Fatalf("the judge was not built with the judge cache's Go cache and no Go environment file:\n%s", said)
|
||||
}
|
||||
// Only a judge build mounts the judge cache.
|
||||
body, _ := readFile(runs)
|
||||
for _, line := range strings.Split(strings.TrimSpace(body), "\n") {
|
||||
if !strings.Contains(line, " go build ") && strings.Contains(line, cache) {
|
||||
t.Fatalf("a container the check runs for the pull request names the judge cache: %s", line)
|
||||
}
|
||||
}
|
||||
// The sources the judge was built from are not left where the member's script could have written them.
|
||||
if _, err := os.Stat(filepath.Join(workspace, "check", "judge-src")); err == nil {
|
||||
t.Fatal("the judge's sources were left in the workspace")
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(path string) (string, error) {
|
||||
body, err := os.ReadFile(path)
|
||||
return string(body), err
|
||||
}
|
||||
Reference in New Issue
Block a user