pullScope no longer gives a mesh-host change a Judge, and link.JudgeValidator is gone: the validator is mesh-sdk's, at the controller's pin (ADR 0446 clause 5). An older request that says validator is read as the controller the mesh runs.
294 lines
17 KiB
Go
294 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// **The mesh's module graph decides what a pull request's check runs**, not the repository (novox/hq
|
|
// ADR 0237 as amended): a change is mapped onto the graph by the rule a merge is (issue 278), the gate
|
|
// runs when it touches a module — a new one included — and a repository that is the mesh's and touches
|
|
// none still has its own merge-check.sh run.
|
|
func TestThePullRequestIsMappedOntoTheModuleGraph(t *testing.T) {
|
|
const catalogue = "http://forge.internal:20000/novox/mesh-catalog.git"
|
|
const controller = "http://forge.internal:20000/novox/mesh-controller.git"
|
|
const host = "http://forge.internal:20000/novox/mesh-host.git"
|
|
photos := fromRepo("photos", "http://forge.internal:20000/novox/photos.git", "")
|
|
photos.Source.Ref = "nox-mesh"
|
|
snake := fromRepo("snake", "jschoubben/snake", "")
|
|
snake.Source.Seat = "git"
|
|
entries := []inventory.Entry{
|
|
fromRepo("gitea", catalogue, "modules/gitea"),
|
|
fromRepo("keycloak", catalogue, "modules/keycloak"),
|
|
fromRepo("nats", catalogue, "modules/nats"),
|
|
fromRepo("mesh-controller", controller, ""),
|
|
fromRepo("mesh-host", host, ""),
|
|
photos, snake,
|
|
}
|
|
pull := func(owner, repo, base string, paths []string, dirs ...string) link.PullUpdated {
|
|
return link.PullUpdated{Owner: owner, Repo: repo, Base: base, Commit: "abc", Paths: paths,
|
|
ModuleDirs: dirs, ModuleDirsSaid: true}
|
|
}
|
|
for _, c := range []struct {
|
|
what string
|
|
p link.PullUpdated
|
|
modules, new, manifest string
|
|
mesh bool
|
|
judge string
|
|
}{
|
|
{"one module's own files", pull("novox", "mesh-catalog", "main", []string{"modules/gitea/index.ts"}, "modules/gitea"),
|
|
"gitea", "", "modules/gitea/module.json", true, ""},
|
|
{"a file no module's build reads touches no module (issue 280)",
|
|
pull("novox", "mesh-catalog", "main", []string{"merge-check.sh", "README.md"}), "", "", "", true, ""},
|
|
{"files the announcer could not list: everything built from it",
|
|
link.PullUpdated{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "abc", PathsTruncated: true,
|
|
Paths: []string{"README.md"}}, "gitea,keycloak,nats", "",
|
|
"modules/gitea/module.json,modules/keycloak/module.json,modules/nats/module.json", true, ""},
|
|
{"a new module, said by the head", pull("novox", "mesh-catalog", "main",
|
|
[]string{"modules/newmod/index.ts", "modules/newmod/module.json"}, "modules/newmod"),
|
|
"", "modules/newmod", "modules/newmod/module.json", true, ""},
|
|
{"the controller judges itself", pull("novox", "mesh-controller", "main", []string{"cmd/x.go"}),
|
|
"mesh-controller", "", "module.json", true, "self"},
|
|
{"the node-engine is judged by the controller the mesh runs, as any module is (issue 1391)",
|
|
pull("novox", "mesh-host", "main", []string{"validate/v.go"}), "mesh-host", "", "module.json", true, ""},
|
|
{"the core's owner, no module: the mesh's, its own check alone", pull("novox", "hq", "main", []string{"README.md"}),
|
|
"", "", "", true, ""},
|
|
{"a branch nothing is built from: the mesh's repository, no module", pull("novox", "photos", "master",
|
|
[]string{"server/x.js"}), "", "", "", true, ""},
|
|
{"the branch a module is built from", pull("novox", "photos", "nox-mesh", []string{"server/x.js"}),
|
|
"photos", "", "module.json", true, ""},
|
|
{"a repository on the forge's seat", pull("jschoubben", "snake", "main", []string{"index.html"}),
|
|
"snake", "", "module.json", true, ""},
|
|
{"a repository of nobody's, touching nothing", pull("someone", "dotfiles", "main", []string{"x"}),
|
|
"", "", "", false, ""},
|
|
{"a repository adding a module at its root", pull("someone", "newapp", "main", []string{"module.json", "x.js"}),
|
|
"", ".", "module.json", true, ""},
|
|
} {
|
|
s := pullScope(c.p, entries, nil, nil)
|
|
got := []string{strings.Join(s.Modules, ","), strings.Join(s.New, ","), strings.Join(s.Manifests, ",")}
|
|
want := []string{c.modules, c.new, c.manifest}
|
|
for i, what := range []string{"modules", "new", "manifests"} {
|
|
if got[i] != want[i] {
|
|
t.Errorf("%s: %s %q, wanted %q", c.what, what, got[i], want[i])
|
|
}
|
|
}
|
|
if s.Mesh != c.mesh || s.Judge != c.judge {
|
|
t.Errorf("%s: the mesh's %v judged by %q, wanted %v by %q", c.what, s.Mesh, s.Judge, c.mesh, c.judge)
|
|
}
|
|
if s.gated() != (c.modules != "" || c.new != "") {
|
|
t.Errorf("%s: gated %v", c.what, s.gated())
|
|
}
|
|
}
|
|
}
|
|
|
|
// A module whose build packages another repository's source is touched by a change to it.
|
|
func TestAPullRequestTouchesWhatPackagesItsRepository(t *testing.T) {
|
|
entries := []inventory.Entry{fromRepo("node-tools", "http://forge.internal:20000/novox/mesh-tools.git", "node-tools")}
|
|
read := map[string][]inventory.ReadRepository{"node-tools": {{Repository: "http://forge.internal:20000/novox/mesh-sdk.git"}}}
|
|
s := pullScope(link.PullUpdated{Owner: "novox", Repo: "mesh-sdk", Base: "main", Commit: "abc", Paths: []string{"go/x.go"}}, entries, read, nil)
|
|
if strings.Join(s.Modules, ",") != "node-tools" || len(s.Manifests) != 0 {
|
|
t.Fatalf("a change to what node-tools packages touched %v (manifests %v)", s.Modules, s.Manifests)
|
|
}
|
|
}
|
|
|
|
// Each layer is said; a check that could not run is an error on both, never a pass; a build seat from
|
|
// before the layers is read as the gate.
|
|
func TestAChecksLayersAreEachSaidAndAnErrorIsNeverAPass(t *testing.T) {
|
|
asked := &link.CheckRequest{Owner: "novox", Repo: "mesh-catalog", Number: 3, Modules: []string{"gitea"}}
|
|
c := checkedOf(link.BuildResult{ID: "b", Ref: "abc", Checked: asked, Failed: "the facts snapshot cannot be read"})
|
|
if c.Verdict != "error" || c.Gate == nil || c.Gate.Verdict != "error" || c.RepoCheck == nil || c.RepoCheck.Verdict != "error" {
|
|
t.Fatalf("a check that could not run said %+v", c)
|
|
}
|
|
if strings.Join(c.Gate.Modules, ",") != "gitea" {
|
|
t.Errorf("the gate names %v", c.Gate.Modules)
|
|
}
|
|
c = checkedOf(link.BuildResult{ID: "b", Ref: "abc", Checked: asked, Check: &link.CheckOutcome{Verdict: "warning", Summary: "wide"}})
|
|
if c.Gate == nil || c.Gate.Verdict != "warning" || c.RepoCheck != nil {
|
|
t.Fatalf("an outcome without layers said %+v", c)
|
|
}
|
|
c = checkedOf(link.BuildResult{ID: "b", Ref: "abc", Checked: asked, Check: &link.CheckOutcome{Verdict: "pass",
|
|
Gate: &link.CheckLayer{Verdict: "pass"}, RepoCheck: &link.CheckLayer{Verdict: "fail", Summary: "its merge-check.sh failed"}}})
|
|
if c.RepoCheck.Verdict != "fail" || c.Gate.Verdict != "pass" {
|
|
t.Fatalf("the layers said %+v / %+v", c.Gate, c.RepoCheck)
|
|
}
|
|
}
|
|
|
|
// **The check asks the planner, never a mapping of its own** (novox/hq ADR 0238): what a pull request
|
|
// reaches is reachOfMerge's answer — the same that plans a merge — so a file at the root touches no
|
|
// module in both, and what stands on a touched module is named as its dependents in both.
|
|
func TestAPullRequestReachesWhatAMergeOfItWouldPlan(t *testing.T) {
|
|
const catalogue = "http://forge.internal:20000/novox/mesh-catalog.git"
|
|
nats := fromRepo("nats", catalogue, "modules/nats")
|
|
nats.Source.BuiltFrom = "old"
|
|
gitea := fromRepo("gitea", catalogue, "modules/gitea")
|
|
gitea.Source.BuiltFrom = "old"
|
|
tools := fromRepo("node-tools", "http://forge.internal:20000/novox/mesh-tools.git", "node-tools")
|
|
entries := []inventory.Entry{nats, gitea, tools}
|
|
// node-tools stands on the bus's image; a code edge, so a plan takes it along.
|
|
edges := []inventory.Edge{{From: "node-tools", To: "nats", Kind: inventory.EdgeStandsOn}}
|
|
read := map[string][]inventory.ReadRepository{}
|
|
|
|
for _, c := range []struct {
|
|
what string
|
|
paths []string
|
|
moved, dependents string
|
|
width int
|
|
unread string
|
|
}{
|
|
{"a file at the root, read by no build", []string{"merge-check.sh"}, "", "", 0, "merge-check.sh"},
|
|
{"the bus's own directory", []string{"modules/nats/Dockerfile"}, "nats", "node-tools", 2, ""},
|
|
{"both, and a README", []string{"modules/gitea/index.ts", "modules/nats/x", "README.md"}, "gitea,nats", "node-tools", 3, "README.md"},
|
|
} {
|
|
p := link.PullUpdated{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "head", Paths: c.paths}
|
|
s := pullScope(p, entries, read, edges)
|
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "head", Paths: c.paths}
|
|
r := reachOfMerge(m, entries, read, edges)
|
|
if got := strings.Join(s.Modules, ","); got != c.moved || got != strings.Join(r.Moved(), ",") {
|
|
t.Errorf("%s: the check reaches %q, the planner %v, wanted %q", c.what, got, r.Moved(), c.moved)
|
|
}
|
|
if got := strings.Join(s.Dependents, ","); got != c.dependents {
|
|
t.Errorf("%s: dependents %q, wanted %q", c.what, got, c.dependents)
|
|
}
|
|
if s.Width != c.width || s.Width != len(r.Plan.Modules) {
|
|
t.Errorf("%s: a merge would build %d, the planner says %d, wanted %d", c.what, s.Width, len(r.Plan.Modules), c.width)
|
|
}
|
|
if got := strings.Join(s.Unread, ","); got != c.unread {
|
|
t.Errorf("%s: unread %q, wanted %q", c.what, got, c.unread)
|
|
}
|
|
}
|
|
|
|
// A repository whose build another module's recipe packages: that module is reached through the
|
|
// planner's `read`, and what stands on it after it.
|
|
read["gitea"] = []inventory.ReadRepository{{Repository: "http://forge.internal:20000/novox/mesh-sdk.git"}}
|
|
s := pullScope(link.PullUpdated{Owner: "novox", Repo: "mesh-sdk", Base: "main", Commit: "head",
|
|
Paths: []string{"src/index.ts"}}, entries, read, edges)
|
|
if strings.Join(s.Modules, ",") != "gitea" || len(s.Manifests) != 0 {
|
|
t.Fatalf("a change to the source gitea packages reaches %v (manifests %v)", s.Modules, s.Manifests)
|
|
}
|
|
}
|
|
|
|
// **Only a commit on the trunk is published** (novox/hq ADR 0238): a build of a commit off its repository's
|
|
// default branch — a pull request's head, a branch built by hand, a rebuild or replay of one — is recorded
|
|
// and never registered, so nothing can send it; a check or a dry run never is either.
|
|
func TestABuildOffTheTrunkIsNeverPublished(t *testing.T) {
|
|
on := link.BuildResult{ID: "b", Commit: "abc", Trunk: "main", OnTrunk: true}
|
|
if err := publishable(on, ""); err != nil {
|
|
t.Errorf("a build on the trunk was refused: %v", err)
|
|
}
|
|
off := link.BuildResult{ID: "b", Commit: "abc", Trunk: "main"}
|
|
if err := publishable(off, ""); !errors.Is(err, errOffTheTrunk) || !strings.Contains(err.Error(), "main") {
|
|
t.Errorf("a build off the trunk was let through: %v", err)
|
|
}
|
|
for _, r := range []link.BuildResult{
|
|
{ID: "c", Trunk: "main", OnTrunk: true, Check: &link.CheckOutcome{Verdict: "pass"}},
|
|
{ID: "d", Trunk: "main", OnTrunk: true, Checked: &link.CheckRequest{}},
|
|
{ID: "e", Trunk: "main", OnTrunk: true, DryRun: true},
|
|
} {
|
|
if publishable(r, "") == nil {
|
|
t.Errorf("%s, a check or a dry run, was publishable", r.ID)
|
|
}
|
|
}
|
|
// A module that follows a branch other than the default: that branch is its trunk, and the default
|
|
// is not.
|
|
follows := link.BuildResult{ID: "g", Commit: "abc", Trunk: "master", Branches: []string{"nox-mesh"}}
|
|
if err := publishable(follows, "nox-mesh"); err != nil {
|
|
t.Errorf("a commit on the branch a module follows was refused: %v", err)
|
|
}
|
|
if err := publishable(link.BuildResult{ID: "h", Commit: "abc", Trunk: "master", OnTrunk: true,
|
|
Branches: []string{"master"}}, "nox-mesh"); err == nil {
|
|
t.Error("a commit on the default but not on the branch the module follows was published")
|
|
}
|
|
if err := publishable(link.BuildResult{ID: "i", Commit: "abc", Trunk: "main", Branches: []string{"feat/x"}}, ""); err == nil {
|
|
t.Error("a feature branch's commit was published")
|
|
}
|
|
// A build seat older than the rule says nothing: let through and said, so the rule can reach the mesh.
|
|
if err := publishable(link.BuildResult{ID: "f", Commit: "abc"}, ""); err != nil {
|
|
t.Errorf("a build seat that said nothing was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// **A verdict names the trunk commit it judged** (novox/hq ADR 0323 §3, issue 341): the controller's `checked`
|
|
// carries the head and the trunk commit the build seat merged it into, the commits of the repositories read beside
|
|
// it, how long it took, and the kept delivery plan records the trunk commit with its other inputs. A group's
|
|
// composed check names, per member, the trunk commit that head was merged into (§6).
|
|
func TestAVerdictNamesTheTrunkCommitItJudged(t *testing.T) {
|
|
plan := &link.ChangePlan{Repository: "novox/mesh-host", Base: "main", Head: "dfb1415aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
Summary: "builds mesh-host"}
|
|
asked := &link.CheckRequest{Owner: "novox", Repo: "mesh-host", Number: 66, Base: "main", Plan: plan}
|
|
c := checkedOf(link.BuildResult{ID: "b", Ref: "dfb1415aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Checked: asked,
|
|
Check: &link.CheckOutcome{Verdict: "pass", Took: "4m12s", Head: "dfb1415aaaaa", Trunk: "d28ca95bbbbb",
|
|
Gate: &link.CheckLayer{Verdict: "pass"}, RepoCheck: &link.CheckLayer{Verdict: "pass"},
|
|
Beside: map[string]string{"mesh-catalog": "0123456789ab", "mesh-lab": "ba9876543210"}}})
|
|
if c.Trunk != "d28ca95bbbbb" || c.Commit != "dfb1415aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" {
|
|
t.Fatalf("the verdict names head %s and trunk %q", c.Commit, c.Trunk)
|
|
}
|
|
if c.Beside["mesh-catalog"] != "0123456789ab" || c.Beside["mesh-lab"] != "ba9876543210" {
|
|
t.Fatalf("the verdict names the repositories read beside it as %v", c.Beside)
|
|
}
|
|
if c.Took != "4m12s" {
|
|
t.Fatalf("the verdict says it took %q", c.Took)
|
|
}
|
|
if c.Plan == nil || c.Plan.Trunk != "d28ca95bbbbb" || c.Plan.Head != plan.Head {
|
|
t.Fatalf("the kept plan records %+v", c.Plan)
|
|
}
|
|
if plan.Trunk != "" {
|
|
t.Fatal("the plan the controller asked with was changed: the verdict's is a copy")
|
|
}
|
|
|
|
group := &link.CheckRequest{Owner: "novox", Repo: "mesh-controller", Number: 7, Base: "main", Group: "feat/x",
|
|
Members: []link.GroupMember{{Owner: "novox", Repo: "mesh-host", Number: 8, Ref: "cccccccccccc0000", Base: "main"}}}
|
|
c = checkedOf(link.BuildResult{ID: "g", Ref: "aaaaaaaaaaaa0000", Checked: group, Check: &link.CheckOutcome{
|
|
Verdict: "pass", Head: "aaaaaaaaaaaa", Trunk: "111111111111", Gate: &link.CheckLayer{Verdict: "pass"},
|
|
Members: []link.CheckedMember{{Owner: "novox", Repo: "mesh-host", Commit: "cccccccccccc", Trunk: "222222222222"}}}})
|
|
if len(c.Members) != 2 || c.Members[0].Trunk != "111111111111" || c.Members[1].Trunk != "222222222222" ||
|
|
c.Members[1].Commit != "cccccccccccc0000" {
|
|
t.Fatalf("the composed check names its members %+v", c.Members)
|
|
}
|
|
}
|
|
|
|
// **The merge made for a check is never registered** (novox/hq ADR 0323 §2): a build of it — a commit on no branch
|
|
// of the forge — is refused, naming the trunk. The rule of ADR 0238 §1 stands unchanged; this holds it for the
|
|
// merge commit a check makes.
|
|
func TestABuildOfACheckMergeIsRefusedNamingTheTrunk(t *testing.T) {
|
|
merge := link.BuildResult{ID: "b", Commit: "4b5c6d7e8f90aabbccddeeff0011223344556677", Trunk: "main"}
|
|
err := publishable(merge, "")
|
|
if !errors.Is(err, errOffTheTrunk) || !strings.Contains(err.Error(), "not on main") {
|
|
t.Fatalf("a build of a check's merge was answered %v", err)
|
|
}
|
|
}
|
|
|
|
// **A verdict no build judges names its trunk commit too** (novox/hq ADR 0323 §3): a change touching no module of a
|
|
// repository with no merge-check.sh (novox/mesh-tools-go) is answered by the controller itself. Naming no trunk, the
|
|
// merge tool refused it, asked again, got the same verdict, and refused for ever.
|
|
func TestADirectVerdictNamesTheTrunkTheForgeAnnounced(t *testing.T) {
|
|
p := link.PullUpdated{Owner: "novox", Repo: "mesh-tools-go", Number: 3, Base: "main", Commit: "1a2b3c4d5e6f7a8b9c0d",
|
|
BaseSHA: "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b"}
|
|
c := directVerdict(p, link.ChangePlan{Repository: "novox/mesh-tools-go", Base: "main", Head: p.Commit})
|
|
if c.Trunk != "7a8b9c0d1e2f" || c.Plan == nil || c.Plan.Trunk != "7a8b9c0d1e2f" {
|
|
t.Fatalf("the direct verdict names trunk %q, its plan %+v", c.Trunk, c.Plan)
|
|
}
|
|
if c.Verdict != "pass" || c.Commit != p.Commit {
|
|
t.Fatalf("the direct verdict is %+v", c)
|
|
}
|
|
if c := directVerdict(link.PullUpdated{Owner: "novox", Repo: "x", Commit: "abc"}, link.ChangePlan{}); c.Trunk != "" {
|
|
t.Fatalf("a forge that said no base head gave the trunk %q", c.Trunk)
|
|
}
|
|
}
|
|
|
|
// A head checked again through mesh-delivery (`delivery check --members`, a recheck) reaches the same direct verdict:
|
|
// it names the base head the forge last announced, kept on the member, or the merge tool would refuse it for ever.
|
|
func TestARecheckedHeadsDirectVerdictNamesItsTrunk(t *testing.T) {
|
|
m := orderMember{ID: "novox/mesh-tools-go@1a2b3c4d5e6f", Repository: "novox/mesh-tools-go", Base: "main",
|
|
Head: "1a2b3c4d5e6f7a8b9c0d", Number: 3, BaseSHA: "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b"}
|
|
if c := directVerdict(m.pull(), link.ChangePlan{}); c.Trunk != "7a8b9c0d1e2f" {
|
|
t.Fatalf("a rechecked head's direct verdict names trunk %q", c.Trunk)
|
|
}
|
|
}
|
|
|
|
// A check stopped by its pull request's key moving on says no verdict (issue 974, review 2): an error said for it would name the
|
|
// live head, and the newer request is the one that judges. A check that failed for any other reason still says its error.
|