mesh/merge-gate head 3ce5a0239bb1 merged into trunk 2d18f438c516 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 3ce5a0239bb1 merged into trunk 2d18f438c516 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
Review of #314: a try's start named its commit, which the forge's holder matches to an open pull request's head and marks pending. Its start now names no repository or commit. One try per commit and four in all, a try never ages ahead of deliveries or pull request checks, and its id is checked before it is a bus subject.
195 lines
8.8 KiB
Go
195 lines
8.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A branch is tried before its pull request (novox/hq issue 686): the repository's own check asked for a pushed
|
|
// branch or commit, with no pull request.
|
|
|
|
// What a try asks is a check — after deliveries in the build queue, bounded as one — of the repository's own
|
|
// script alone: no module of the graph, so no gate; nothing built for a merge, so no reusable image.
|
|
func TestATryAsksACheckOfTheRepositorysOwnScriptAlone(t *testing.T) {
|
|
request := link.BuildRequest{ID: "build-1", Ref: "c0ffee", Check: &link.CheckRequest{Owner: "novox", Repo: "a",
|
|
Number: 7, Base: "main", Modules: []string{"a"}, New: []string{"b"}, Manifests: []string{"module.json"},
|
|
Dependents: []string{"c"}, Builds: []link.CheckBuild{{Module: "a"}}, Judge: link.JudgeSelf,
|
|
Plan: &link.ChangePlan{}}}
|
|
tried := asATry(request, "^TestStore", true)
|
|
c := tried.Check
|
|
if !c.Try || c.Number != 0 || c.Tests != "^TestStore" || !c.StoreOnly {
|
|
t.Fatalf("not marked a try, or it kept a pull request's number: %+v", c)
|
|
}
|
|
if len(c.Modules)+len(c.New)+len(c.Manifests)+len(c.Dependents)+len(c.Builds) != 0 || c.Judge != "" || c.Plan != nil {
|
|
t.Fatalf("a try runs the gate or builds for a merge: %+v", c)
|
|
}
|
|
if c.Base != "main" || c.Owner != "novox" || tried.Ref != "c0ffee" {
|
|
t.Fatalf("a try lost what it merges into what: %+v", tried)
|
|
}
|
|
if link.KindOf(tried) != link.KindCheck {
|
|
t.Fatal("a try does not count as a check in the build queue")
|
|
}
|
|
}
|
|
|
|
// A try's outcome is heard and left there: no `checked` is said, so the forge's holder sets no status, and
|
|
// nothing is recorded or registered (the handler holds no store here, and would fail reaching for one).
|
|
func TestATrysOutcomeSetsNoStatusAndRegistersNothing(t *testing.T) {
|
|
said := &saidOnTheBus{}
|
|
checkEvents = said
|
|
defer func() { checkEvents = nil }()
|
|
for name, result := range map[string]link.BuildResult{
|
|
"a try": {ID: "build-1", Ref: "c0ffee", Checked: &link.CheckRequest{Owner: "novox", Repo: "a", Try: true},
|
|
Check: &link.CheckOutcome{Verdict: "pass", RepoCheck: &link.CheckLayer{Verdict: "fail", Summary: "failed"}}},
|
|
"a try that could not run": {ID: "build-2", Checked: &link.CheckRequest{Owner: "novox", Repo: "a", Try: true},
|
|
Failed: "cannot clone"},
|
|
// A build seat from before the mark echoes the ask without it: a check of no pull request and no
|
|
// group is still nobody's status.
|
|
"a try echoed by an older build seat": {ID: "build-3", Checked: &link.CheckRequest{Owner: "novox", Repo: "a"},
|
|
Check: &link.CheckOutcome{Verdict: "pass"}},
|
|
} {
|
|
if err := (builds{}).Built(context.Background(), result); err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
}
|
|
if len(said.events) != 0 {
|
|
t.Fatalf("a try's outcome was said for the forge: %v", said.events)
|
|
}
|
|
// A pull request's check is still said.
|
|
_ = (builds{}).Built(context.Background(), link.BuildResult{ID: "build-4", Checked: &link.CheckRequest{
|
|
Owner: "novox", Repo: "a", Number: 7}, Check: &link.CheckOutcome{Verdict: "pass"}})
|
|
if len(said.events) != 1 {
|
|
t.Fatalf("a pull request's check was said %d time(s)", len(said.events))
|
|
}
|
|
}
|
|
|
|
// saidOnTheBus keeps the seat events said, and nothing else.
|
|
type saidOnTheBus struct {
|
|
link.Bus
|
|
events []string
|
|
}
|
|
|
|
func (s *saidOnTheBus) PublishSeatEvent(_ context.Context, seat, event string, _ []byte) error {
|
|
s.events = append(s.events, seat+" "+event)
|
|
return nil
|
|
}
|
|
|
|
// A ref the forge does not hold is refused before anything is asked, naming it.
|
|
func TestATryOfAnUnknownRefIsRefusedLoudly(t *testing.T) {
|
|
was := forgeCommit
|
|
defer func() { forgeCommit = was }()
|
|
forgeCommit = func(_ context.Context, owner, repo, ref string) (string, string, error) {
|
|
return "", "", errors.New("404 object does not exist [id: " + ref + "]")
|
|
}
|
|
err := tryCommand(context.Background(), []string{"novox/mesh-controller", "fix/no-such-branch"})
|
|
if err == nil {
|
|
t.Fatal("a try of a ref the forge does not hold was asked")
|
|
}
|
|
for _, want := range []string{"fix/no-such-branch", "novox/mesh-controller", "nothing was asked", "404"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the refusal does not say %q: %v", want, err)
|
|
}
|
|
}
|
|
for _, args := range [][]string{{"mesh-controller", "main"}, {"novox/mesh-controller"}, {"novox/a", "--upload-pack=x"},
|
|
{"novox/a", "main", "--tests", "x; rm -rf /"}, {"novox/a", "main", "--id", "build-1"}} {
|
|
if err := tryCommand(context.Background(), args); err == nil {
|
|
t.Errorf("try %v was taken", args)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The answer of a try carries the lines that name what failed (novox/hq issue 460's shape) and how long it took.
|
|
func TestATrysAnswerCarriesTheFailingLines(t *testing.T) {
|
|
tried, _ := json.Marshal(link.Tried{Verdict: "fail", Summary: "its merge-check.sh failed: TestX", Took: "5m3s",
|
|
Head: "c0ffee", Trunk: "ca3f43c", Failed: []string{"--- FAIL: TestX (0.01s)", " x_test.go:12: got 1, want 2",
|
|
"FAIL\tgithub.com/novox/a/cmd\t3.1s"}})
|
|
lines := []link.BuildLine{{Seq: 1, Step: "check", Message: "novox/a#0 at c0ffee"},
|
|
{Seq: 2, Step: "output", Message: "ok github.com/novox/a/internal 1.2s"},
|
|
{Seq: 3, Step: link.StepTried, Message: string(tried)}, {Seq: 4, Step: "checked", Message: "pass: x"}}
|
|
a := tryAnswerOf("build-1", lines)
|
|
if a.State != "fail" || a.Took != "5m3s" || len(a.Failed) != 3 || a.Failed[0] != "--- FAIL: TestX (0.01s)" ||
|
|
a.Trunk != "ca3f43c" || a.ID != "build-1" {
|
|
t.Fatalf("the answer lost the verdict, the time or the failing lines: %+v", a)
|
|
}
|
|
// Not answered yet: said as running, with how far it is — never as a pass.
|
|
if a := tryAnswerOf("build-1", lines[:2]); a.State != "running" || a.Last == "" {
|
|
t.Fatalf("a try still running: %+v", a)
|
|
}
|
|
if a := tryAnswerOf("build-1", nil); a.State != "waiting" {
|
|
t.Fatalf("a try nothing was said of: %+v", a)
|
|
}
|
|
// One that could not run is an error with the build seat's word, never a pass.
|
|
if a := tryAnswerOf("build-1", append(lines[:1:1], link.BuildLine{Seq: 2, Step: "failed",
|
|
Message: "cannot clone"})); a.State != "error" || !strings.Contains(a.Said, "cannot clone") {
|
|
t.Fatalf("a try that could not run: %+v", a)
|
|
}
|
|
}
|
|
|
|
// One try of a commit at a time, and four in all (novox/hq issue 686): a second ask is answered the first's id,
|
|
// a fifth is refused; a try that ended, and a pull request's check, count for nothing.
|
|
func TestTriesAreBoundedInNumber(t *testing.T) {
|
|
ask := func(id, repo, ref, state string, try bool) link.QueuedAsk {
|
|
return link.QueuedAsk{ID: id, State: state, Request: link.BuildRequest{ID: id, Ref: ref,
|
|
Check: &link.CheckRequest{Owner: "novox", Repo: repo, Try: try}}}
|
|
}
|
|
q := link.Queue{Asks: []link.QueuedAsk{
|
|
ask("t1", "a", "c1", link.AskInFlight, true), ask("t2", "a", "c2", link.AskWaiting, true),
|
|
ask("t3", "b", "c1", link.AskDead, true), ask("p1", "a", "c9", link.AskWaiting, false)}}
|
|
if same, room := triesIn(q, "novox", "a", "c2"); same != "t2" || !room {
|
|
t.Fatalf("a second try of a commit: %q %v", same, room)
|
|
}
|
|
if same, room := triesIn(q, "novox", "b", "c1"); same != "" || !room {
|
|
t.Fatalf("a dead try, or another repository's, was taken for this one: %q %v", same, room)
|
|
}
|
|
if same, _ := triesIn(q, "novox", "a", "c9"); same != "" {
|
|
t.Fatalf("a pull request's check was taken for a try: %q", same)
|
|
}
|
|
q.Asks = append(q.Asks, ask("t4", "c", "c3", link.AskWaiting, true), ask("t5", "d", "c4", link.AskWaiting, true))
|
|
if _, room := triesIn(q, "novox", "e", "c5"); room {
|
|
t.Fatal("a fifth try was let in")
|
|
}
|
|
// An id is checked before it is a token of a bus subject.
|
|
for _, id := range []string{"x.>", "*", "a b"} {
|
|
if err := tryCommand(context.Background(), []string{"--id", id}); err == nil ||
|
|
!strings.Contains(err.Error(), "not a try's id") {
|
|
t.Errorf("try --id %q: %v", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The verb runs its command, and is declared with the shell habit it replaces (novox/hq ADR 0245).
|
|
func TestTheTryVerbRunsItsCommand(t *testing.T) {
|
|
for _, c := range []struct {
|
|
args map[string]any
|
|
want []string
|
|
}{
|
|
{map[string]any{"repository": "novox/a", "ref": "fix/x"}, []string{"try", "novox/a", "fix/x"}},
|
|
{map[string]any{"repository": "novox/a", "ref": "fix/x", "base": "main", "tests": "^TestX$", "store-only": "true"},
|
|
[]string{"try", "novox/a", "fix/x", "--base", "main", "--tests", "^TestX$", "--store-only"}},
|
|
{map[string]any{"id": "build-1"}, []string{"try", "--id", "build-1"}},
|
|
} {
|
|
got, err := argvFor("try", c.args)
|
|
if err != nil || !reflect.DeepEqual(got, c.want) {
|
|
t.Errorf("try %v: %v %v, want %v", c.args, got, err, c.want)
|
|
}
|
|
}
|
|
if _, err := argvFor("try", map[string]any{"repository": "novox/a"}); err == nil {
|
|
t.Error("a try of no ref was taken")
|
|
}
|
|
for _, v := range catalogue.ControllerVerbs {
|
|
if v.Name == "try" {
|
|
if len(v.Replaces) == 0 {
|
|
t.Error("try names no shell habit it replaces")
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("try is not a verb of the controller seat")
|
|
}
|