mesh/merge-gate head e4acab32d6a2 merged into trunk 60cc9ec363df — pass: builds mesh-controller → novox; no bus step; every machine composes with the chang…
mesh/repo-check head e4acab32d6a2 merged into trunk 60cc9ec363df — pass: its merge-check.sh passed
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792009731456564734 (novox/mesh-controller at 60cc9ec3 into ma…
A check judges one head; once the pull request has another, its verdict answers nobody and its run holds a build machine the new head waits behind. Asking a check of R#N at H now drops every waiting check of R#N at another head and kills every one in flight, saying each in the log. Announcements are unordered and a recheck asks a recorded head, so this happens only when the forge confirms H is the pull request's current head; otherwise nothing ends and that is said. A one-shot recheck ends them within the call. Tries, walk builds, group checks, other pull requests' checks and rechecks of the same head are left alone. novox/hq issue 726
193 lines
7.5 KiB
Go
193 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A new push to a pull request ends the check of its old head (novox/hq issue 726): the waiting one is dropped from
|
|
// the queue, the one in flight is killed on its machine, and each is said. A try, a walk's build, another pull
|
|
// request's check, a group's check and a recheck of the new head are left alone. On the bus alone: no store.
|
|
func TestANewHeadEndsTheChecksOfTheOldOnes(t *testing.T) {
|
|
js, _ := aBuildQueue(t)
|
|
ctx := t.Context()
|
|
const (
|
|
oldHead = "1111111111111111111111111111111111111111"
|
|
olderHead = "2222222222222222222222222222222222222222"
|
|
newHead = "3333333333333333333333333333333333333333"
|
|
)
|
|
publish := func(r link.BuildRequest) {
|
|
t.Helper()
|
|
body, _ := json.Marshal(r)
|
|
if _, err := js.Context().Publish(link.BuildWork(), body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
check := func(id string, number int, head string) link.BuildRequest {
|
|
return link.BuildRequest{ID: id, Repository: "https://forge.example/novox/a.git", Ref: head,
|
|
Check: &link.CheckRequest{Owner: "novox", Repo: "a", Number: number}}
|
|
}
|
|
// In flight first: taken by a holder, started on ace.
|
|
publish(check("check-running", 7, olderHead))
|
|
worker, _ := broker.HolderConsumerFor("", "", broker.DeclaredSeat{Name: link.TheBuildMachine, Accepts: []string{"build"}})
|
|
sub, err := js.Context().PullSubscribe(worker.Filters[0], worker.Name, nats.Bind(worker.Stream, worker.Name), nats.ManualAck())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = sub.Unsubscribe() }()
|
|
if _, err := sub.Fetch(1, nats.MaxWait(3*time.Second)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
started, _ := json.Marshal(link.BuildStart{ID: "check-running", On: "ace", At: time.Now().UTC().Format(time.RFC3339Nano)})
|
|
if _, err := js.Context().Publish(link.BuildStarted(), started); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
publish(check("check-old", 7, oldHead))
|
|
publish(check("check-other-pull", 8, oldHead))
|
|
publish(check("check-recheck", 7, newHead))
|
|
publish(check("check-new", 7, newHead))
|
|
try := check("try-old", 7, oldHead)
|
|
try.Check.Number, try.Check.Try = 0, true
|
|
publish(try)
|
|
group := check("group-old", 7, oldHead)
|
|
group.Check.Group = "g1"
|
|
publish(group)
|
|
publish(link.BuildRequest{ID: "walk-build", Repository: "https://forge.example/novox/a.git", Ref: oldHead})
|
|
|
|
var mu sync.Mutex
|
|
var killed []string
|
|
stop, err := link.OverNATS{Conn: js.Conn()}.ServeNodeSeatTools(link.TheBuildMachine, "ace", map[string]link.ToolHandler{
|
|
"kill": func(_ context.Context, raw json.RawMessage) (any, error) {
|
|
var args struct{ ID string }
|
|
_ = json.Unmarshal(raw, &args)
|
|
mu.Lock()
|
|
killed = append(killed, args.ID)
|
|
mu.Unlock()
|
|
return map[string]any{"said": "killed " + args.ID}, nil
|
|
}}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer stop()
|
|
|
|
forgeSays(t, newHead, nil)
|
|
said := endSupersededChecks(ctx, js, link.TheBuildMachine,
|
|
link.PullUpdated{Owner: "novox", Repo: "a", Number: 7, Commit: newHead}, "check-new")
|
|
|
|
mu.Lock()
|
|
if len(killed) != 1 || killed[0] != "check-running" {
|
|
t.Errorf("the holder was asked to kill %v, want only the in-flight check of the older head", killed)
|
|
}
|
|
mu.Unlock()
|
|
q, err := link.ReadQueue(ctx, js, link.TheBuildMachine)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, found := q.Find("check-old"); found {
|
|
t.Error("the waiting check of the old head is still in the queue")
|
|
}
|
|
for _, id := range []string{"check-other-pull", "check-recheck", "check-new", "try-old", "group-old", "walk-build"} {
|
|
if _, found := q.Find(id); !found {
|
|
t.Errorf("%s was ended; only checks of the same pull request at another head end", id)
|
|
}
|
|
}
|
|
text := strings.Join(said, "\n")
|
|
for _, want := range []string{
|
|
"ended: check of novox/a#7 at " + oldHead[:12] + " — superseded by " + newHead[:12],
|
|
"ended: check of novox/a#7 at " + olderHead[:12] + " — superseded by " + newHead[:12],
|
|
} {
|
|
if !strings.Contains(text, want) {
|
|
t.Errorf("the log does not say %q:\n%s", want, text)
|
|
}
|
|
}
|
|
if len(said) != 2 {
|
|
t.Errorf("said %d lines, want 2:\n%s", len(said), text)
|
|
}
|
|
}
|
|
|
|
// Which asks a check supersedes, read from the queue alone.
|
|
func TestSupersededChecksAreOnlyThePullRequestsOtherHeads(t *testing.T) {
|
|
ask := func(id, state, ref string, c *link.CheckRequest) link.QueuedAsk {
|
|
return link.QueuedAsk{ID: id, State: state, Ref: ref, Request: link.BuildRequest{ID: id, Ref: ref, Check: c}}
|
|
}
|
|
pr := func(n int) *link.CheckRequest { return &link.CheckRequest{Owner: "Novox", Repo: "A", Number: n} }
|
|
head := "abcdefabcdefabcdef0123456789abcdef012345"
|
|
q := link.Queue{Asks: []link.QueuedAsk{
|
|
ask("old", link.AskWaiting, "0123456789ab0123456789ab0123456789ab0123", pr(7)),
|
|
ask("short-same", link.AskWaiting, head[:12], pr(7)),
|
|
ask("dead", link.AskDead, "0123456789ab0123456789ab0123456789ab0123", pr(7)),
|
|
ask("other", link.AskWaiting, "0123456789ab0123456789ab0123456789ab0123", pr(8)),
|
|
ask("build", link.AskWaiting, "0123456789ab0123456789ab0123456789ab0123", nil),
|
|
ask("keep", link.AskWaiting, "0123456789ab0123456789ab0123456789ab0123", pr(7)),
|
|
}}
|
|
got := supersededChecks(q, "novox", "a", 7, head, "keep")
|
|
if len(got) != 1 || got[0].ID != "old" {
|
|
t.Fatalf("superseded %+v, want only old", got)
|
|
}
|
|
if got := supersededChecks(q, "novox", "a", 0, head, ""); got != nil {
|
|
t.Errorf("a check of no pull request superseded %+v", got)
|
|
}
|
|
}
|
|
|
|
// forgeSays makes the forge name head as every pull request's current head, or fail with err.
|
|
func forgeSays(t *testing.T, head string, err error) {
|
|
t.Helper()
|
|
was := forgePullHead
|
|
forgePullHead = func(context.Context, *broker.JetStream, string, string, int) (string, error) { return head, err }
|
|
t.Cleanup(func() { forgePullHead = was })
|
|
}
|
|
|
|
// An older head heard after a newer one — an announcement out of order, one heard again after a restart, a recheck
|
|
// of a recorded head — ends nothing: the forge says the pull request's head is the newer one. Nor does a check whose
|
|
// pull request's head the forge cannot say.
|
|
func TestAnOlderHeadHeardLateEndsNothing(t *testing.T) {
|
|
js, _ := aBuildQueue(t)
|
|
ctx := t.Context()
|
|
const (
|
|
oldHead = "1111111111111111111111111111111111111111"
|
|
newHead = "3333333333333333333333333333333333333333"
|
|
)
|
|
for _, id := range []string{"check-new", "check-old-late"} {
|
|
head := newHead
|
|
if id == "check-old-late" {
|
|
head = oldHead
|
|
}
|
|
body, _ := json.Marshal(link.BuildRequest{ID: id, Repository: "https://forge.example/novox/a.git", Ref: head,
|
|
Check: &link.CheckRequest{Owner: "novox", Repo: "a", Number: 7}})
|
|
if _, err := js.Context().Publish(link.BuildWork(), body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
late := link.PullUpdated{Owner: "novox", Repo: "a", Number: 7, Commit: oldHead}
|
|
forgeSays(t, newHead, nil)
|
|
said := endSupersededChecks(ctx, js, link.TheBuildMachine, late, "check-old-late")
|
|
if len(said) != 1 || !strings.Contains(said[0], "nothing ended") {
|
|
t.Errorf("an older head heard late said %q", said)
|
|
}
|
|
forgeSays(t, "", errors.New("nothing serves gitea.gitea_api"))
|
|
said = endSupersededChecks(ctx, js, link.TheBuildMachine,
|
|
link.PullUpdated{Owner: "novox", Repo: "a", Number: 7, Commit: newHead}, "check-new")
|
|
if len(said) != 1 || !strings.Contains(said[0], "nothing ended") {
|
|
t.Errorf("a head the forge could not confirm said %q", said)
|
|
}
|
|
q, err := link.ReadQueue(ctx, js, link.TheBuildMachine)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, id := range []string{"check-new", "check-old-late"} {
|
|
if _, found := q.Find(id); !found {
|
|
t.Errorf("%s was ended", id)
|
|
}
|
|
}
|
|
}
|