One commit, one plan: a commit off the trunk — a pull request's head, a branch built by hand, a rebuild or replay of one — is for checking. The build seat reads from its clone which branches hold the commit, and the controller records and never registers a build whose commit is not on the branch the module follows (the repository's default for a new one), so nothing off the trunk can be sent. A pull request's check now carries its change plan, computed by the planner: what a merge would build in which order, what each machine would receive, and what is not an ordinary send — the bus step, a module waiting for a person, a provider's consumers.
213 lines
12 KiB
Go
213 lines
12 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 with its validator", pull("novox", "mesh-host", "main", []string{"validate/v.go"}),
|
|
"mesh-host", "", "module.json", true, "validator"},
|
|
{"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)
|
|
}
|
|
}
|