|
|
|
@@ -0,0 +1,162 @@
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
|
}
|