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). // 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") }