137 lines
7.0 KiB
Go
137 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A delivery group's gate is the composed check's, and its members' own checks run their repository's check alone
|
|
// (novox/hq ADR 0403 clauses 1 and 7). Pure: no store, no bus.
|
|
|
|
// A member of a group whose line says composed_gate is a composed member; one whose group says nothing of it (an older
|
|
// mesh-delivery), one of a group of a single pull request and a lone pull request are not.
|
|
func TestAMemberOfAComposedGroupIsKnownByTheGroupsLine(t *testing.T) {
|
|
two := func(composed bool) groupsLine {
|
|
l := group("feat/x", member("novox/mesh-host", 114, headHost, "main", true), member("novox/mesh-controller", 346, headController, "main", true))
|
|
l.ComposedGate = composed
|
|
return l
|
|
}
|
|
p := link.PullUpdated{Owner: "novox", Repo: "mesh-host", Number: 114, Base: "main", Head: "feat/x", Commit: headHost}
|
|
recheck := link.PullUpdated{Owner: "novox", Repo: "mesh-host", Number: 114, Base: "main", Commit: headHost}
|
|
for name, c := range map[string]struct {
|
|
p link.PullUpdated
|
|
lines []groupsLine
|
|
want bool
|
|
}{
|
|
"composed": {p, []groupsLine{two(true)}, true},
|
|
"a recheck, by its number": {recheck, []groupsLine{two(true)}, true},
|
|
"an older mesh-delivery": {p, []groupsLine{two(false)}, false},
|
|
"another group": {p, []groupsLine{group("feat/y", member("novox/mesh-host", 1, headHost, "main", true))}, false},
|
|
"no group": {p, nil, false},
|
|
} {
|
|
if got := composedMember(c.p, c.lines); got != c.want {
|
|
t.Errorf("%s: composed %v, want %v", name, got, c.want)
|
|
}
|
|
}
|
|
alone := group("feat/x", member("novox/mesh-host", 114, headHost, "main", true))
|
|
alone.ComposedGate = true
|
|
if composedMember(p, []groupsLine{alone}) {
|
|
t.Error("a group of a single pull request is composed")
|
|
}
|
|
}
|
|
|
|
// A composed member's verdict never says its gate passed: the gate layer, and the verdict that is the gate's, read
|
|
// "composed"; a failure or an error stays what it was.
|
|
func TestAComposedMembersVerdictHasNoPassingGate(t *testing.T) {
|
|
asked := &link.CheckRequest{Owner: "novox", Repo: "mesh-host", Number: 114, Composed: true}
|
|
pass := link.BuildResult{ID: "b", Ref: headHost, Checked: asked, Check: &link.CheckOutcome{Verdict: "pass", Summary: "no module touched",
|
|
Gate: &link.CheckLayer{Verdict: "pass", Summary: "no module touched"}, RepoCheck: &link.CheckLayer{Verdict: "pass", Summary: "ok"}}}
|
|
c := checkedOf(pass)
|
|
if c.Gate == nil || c.Gate.Verdict != "composed" || c.Verdict != "composed" || c.RepoCheck == nil || c.RepoCheck.Verdict != "pass" {
|
|
t.Fatalf("the member's verdict says %+v gate %+v", c, c.Gate)
|
|
}
|
|
asked.Composed = false
|
|
if c := checkedOf(pass); c.Gate.Verdict != "pass" || c.Verdict != "pass" {
|
|
t.Fatalf("a member of no composed group has its own gate: %+v", c.Gate)
|
|
}
|
|
asked.Composed = true
|
|
failing := pass
|
|
failing.Check = &link.CheckOutcome{Verdict: "fail", Summary: "conflict", Gate: &link.CheckLayer{Verdict: "fail", Summary: "conflict"},
|
|
RepoCheck: &link.CheckLayer{Verdict: "fail", Summary: "conflict"}}
|
|
if c := checkedOf(failing); c.Gate.Verdict != "fail" || c.Verdict != "fail" {
|
|
t.Fatalf("a failure was read as composed: %+v", c.Gate)
|
|
}
|
|
broken := link.BuildResult{ID: "b", Ref: headHost, Checked: asked, Failed: "the facts snapshot cannot be read"}
|
|
if c := checkedOf(broken); c.Verdict != "error" || c.Gate.Verdict != "error" {
|
|
t.Fatalf("an error was read as composed: %+v", c)
|
|
}
|
|
}
|
|
|
|
// The composed verdict on the bus carries every member, each with its trunk commit, for the forge's holder to set each
|
|
// member's mesh/merge-gate from it.
|
|
func TestTheComposedVerdictCarriesEveryMember(t *testing.T) {
|
|
asked := &link.CheckRequest{Owner: "novox", Repo: "mesh-host", Number: 114, Group: "feat/x",
|
|
Members: []link.GroupMember{{Owner: "novox", Repo: "mesh-controller", Number: 346, Ref: headController}}}
|
|
c := checkedOf(link.BuildResult{ID: "g", Ref: headHost, Checked: asked, Check: &link.CheckOutcome{Verdict: "pass", Summary: "ok",
|
|
Trunk: "aaaaaaaaaaaa", Members: []link.CheckedMember{{Owner: "novox", Repo: "mesh-controller", Commit: headController[:12], Trunk: "bbbbbbbbbbbb"}}}})
|
|
body, _ := json.Marshal(c)
|
|
for _, want := range []string{`"group":"feat/x"`, `"members":[{"owner":"novox","repo":"mesh-host","number":114,"commit":"` + headHost + `","trunk":"aaaaaaaaaaaa"}`,
|
|
`{"owner":"novox","repo":"mesh-controller","number":346,"commit":"` + headController + `","trunk":"bbbbbbbbbbbb"}`} {
|
|
if !strings.Contains(string(body), want) {
|
|
t.Fatalf("the composed verdict lacks %s:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Members whose manifests order their modules in a cycle fail the composed check, naming every module and edge, and no
|
|
// ask is made of the build seat.
|
|
func TestAGroupInACycleFailsItsComposedCheckWithoutABuild(t *testing.T) {
|
|
members := []orderMember{
|
|
{ID: "novox/x@" + headHost[:12], Repository: "novox/x", Base: "main", Head: headHost, Number: 7, Paths: []string{"module.json"}},
|
|
{ID: "novox/y@" + headController[:12], Repository: "novox/y", Base: "main", Head: headController, Number: 9, Paths: []string{"module.json"}},
|
|
}
|
|
reach := map[string]orderReach{members[0].ID: {Moved: []string{"mx"}}, members[1].ID: {Moved: []string{"my"}}}
|
|
was, wasParent := manifestAt, parentOf
|
|
t.Cleanup(func() { manifestAt, parentOf = was, wasParent })
|
|
parentOf = func(context.Context, string, string, string) (string, error) { return "", nil }
|
|
manifestAt = func(_ context.Context, _, repo, _, ref string) (catalogue.Manifest, bool, error) {
|
|
if ref == "main" {
|
|
return catalogue.Manifest{}, false, nil // what the pull requests merge into holds no such manifest
|
|
}
|
|
if repo == "x" {
|
|
return catalogue.Manifest{Module: "mx", DefinesSeats: []catalogue.SeatDeclaration{{Name: "s", Serves: []catalogue.Verb{{Name: "a"}}}},
|
|
Invokes: []string{"seat:s.b"}}, true, nil
|
|
}
|
|
return catalogue.Manifest{Module: "my", DefinesSeats: []catalogue.SeatDeclaration{{Name: "s", Serves: []catalogue.Verb{{Name: "b"}}}},
|
|
Invokes: []string{"seat:s.a"}}, true, nil
|
|
}
|
|
c, cyclic, err := cycleVerdict(context.Background(), "feat/cycle", members, reach, []inventory.Edge(nil), nil)
|
|
if err != nil || !cyclic {
|
|
t.Fatalf("no cycle found: %v", err)
|
|
}
|
|
if c.Verdict != "fail" || c.Group != "feat/cycle" || len(c.Members) != 2 || c.Gate == nil || c.Gate.Verdict != "fail" {
|
|
t.Fatalf("the verdict is %+v", c)
|
|
}
|
|
for _, w := range []string{"mx", "my", "s.a", "s.b"} {
|
|
if !strings.Contains(c.Summary, w) {
|
|
t.Errorf("the cycle does not name %s: %s", w, c.Summary)
|
|
}
|
|
}
|
|
// Members with no edge between them are no cycle.
|
|
manifestsReadMu.Lock()
|
|
clear(manifestsRead)
|
|
manifestsReadMu.Unlock()
|
|
manifestAt = func(context.Context, string, string, string, string) (catalogue.Manifest, bool, error) {
|
|
return catalogue.Manifest{}, false, nil
|
|
}
|
|
if _, cyclic, err := cycleVerdict(context.Background(), "feat/cycle", members, reach, nil, nil); err != nil || cyclic {
|
|
t.Fatalf("a cycle where there is none: %v %v", cyclic, err)
|
|
}
|
|
}
|