200 lines
8.7 KiB
Go
200 lines
8.7 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// **A pull request marked ready is merged by this owner** (novox/hq issue 722): before, a reviewed pull request with
|
|
// green checks waited until a session called the merge tool, and again after every check the tool asked because the
|
|
// trunk had moved. Each test below fails on a holder without the ready mark: nothing merged it.
|
|
|
|
const (
|
|
readyHead = "1111111111111111111111111111111111111111"
|
|
pushed = "2222222222222222222222222222222222222222"
|
|
)
|
|
|
|
// markedPull is an open pull request as gitea_open_pulls reads it, labelled mesh/ready at the head named.
|
|
func markedPull(repo string, number int, head, markedAt string) OpenPull {
|
|
owner, name, _ := strings.Cut(repo, "/")
|
|
return OpenPull{Owner: owner, Repo: name, Number: number, Base: "main", HeadSHA: head, Labels: []string{ReadyLabel},
|
|
Ready: &ReadyMark{Head: markedAt, By: "reviewer", At: "2026-10-14T19:00:00Z", LabelID: 31}}
|
|
}
|
|
|
|
func judgedAt(repo string, number int, sha, gate, trunk string) CheckedEvent {
|
|
c := verdict(repo, number, sha, gate)
|
|
c.Trunk = trunk
|
|
return c
|
|
}
|
|
|
|
func TestAReadyPullRequestMarkedAtItsHeadIsMergedThroughTheMergeTool(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", readyHead)
|
|
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
|
|
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 0 {
|
|
t.Fatalf("an unmarked pull request was merged: %v", w.forge.merges)
|
|
}
|
|
w.forge.open = []OpenPull{markedPull("novox/app", 7, readyHead, readyHead)}
|
|
w.h.SeeOpenPulls()
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 1 || w.forge.merges[0] != "novox/app#7@"+readyHead {
|
|
t.Fatalf("a ready, marked pull request was merged %v; wanted once, at the marked head", w.forge.merges)
|
|
}
|
|
d := w.delivery(id)
|
|
if d.Ready == nil || d.Ready.Merged == nil || d.Ready.By != "reviewer" || d.Ready.Tries != 1 {
|
|
t.Fatalf("what was done with the mark is not kept: %+v", d.Ready)
|
|
}
|
|
// Its merge's transition names who merged it.
|
|
w.h.PullMerged(merged("novox/app", 7, readyHead, "m7m7m7m7m7m7"))
|
|
w.h.WalkMoved(w.ctl.put(aWalk("plan-7", "novox/app", "m7m7m7m7m7m7", true)))
|
|
w.settleAll()
|
|
d = w.delivery(id)
|
|
var last Transition
|
|
for _, tr := range d.Transitions {
|
|
if tr.Event == EvMerged {
|
|
last = tr
|
|
}
|
|
}
|
|
if !strings.Contains(last.By, "ready mark at "+readyHead[:12]+" by reviewer") {
|
|
t.Fatalf("the merge's transition does not name the mark: %+v", last)
|
|
}
|
|
if len(w.forge.merges) != 1 {
|
|
t.Fatalf("merged again: %v", w.forge.merges)
|
|
}
|
|
}
|
|
|
|
func TestAPushAfterTheMarkClearsItAndNothingIsMerged(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
|
|
w.h.PullUpdated(pr("novox/app", 7, pushed, "feat/x", "src/a.go"))
|
|
w.h.Checked(judgedAt("novox/app", 7, pushed, "pass", "t1t1t1t1t1t1"))
|
|
w.forge.open = []OpenPull{markedPull("novox/app", 7, pushed, readyHead)}
|
|
w.h.SeeOpenPulls()
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 0 {
|
|
t.Fatalf("a head pushed after the mark was merged: %v", w.forge.merges)
|
|
}
|
|
if len(w.forge.unlabels) != 1 || w.forge.unlabels[0] != "novox/app#7-31" {
|
|
t.Fatalf("the stale mark's label was not taken off: %v", w.forge.unlabels)
|
|
}
|
|
if len(w.forge.comments) != 1 || !strings.Contains(w.forge.comments[0], "cleared") ||
|
|
!strings.Contains(w.forge.comments[0], pushed) {
|
|
t.Fatalf("the cleared mark is not said on the pull request: %v", w.forge.comments)
|
|
}
|
|
}
|
|
|
|
func TestAMovedTrunkIsJudgedAgainByTheMergeToolAndMergedOnItsVerdict(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", readyHead)
|
|
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
|
|
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
|
|
// Another merge moves the trunk: its verdict is out of date, and it goes back to proposed.
|
|
w.h.PullMerged(merged("novox/app", 9, "9999999999999999", "t2t2t2t2t2t2"))
|
|
if w.state(id) != Proposed {
|
|
t.Fatalf("out of date, it is %s", w.state(id))
|
|
}
|
|
// The merge tool asks the check against the trunk's head and refuses: nothing merged.
|
|
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: The check of head merged into trunk was asked")
|
|
w.forge.open = []OpenPull{markedPull("novox/app", 7, readyHead, readyHead)}
|
|
w.h.SeeOpenPulls()
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 1 {
|
|
t.Fatalf("the merge tool was not asked once to judge it again: %v", w.forge.merges)
|
|
}
|
|
// The tool's ask is the head announced again; it waits for that verdict, asking nothing.
|
|
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
|
|
w.later(5 * time.Minute)
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 1 || !strings.Contains(w.delivery(id).Ready.Waits, "asked") {
|
|
t.Fatalf("asked again while its check was on its way: %v (%+v)", w.forge.merges, w.delivery(id).Ready)
|
|
}
|
|
// It passes against the trunk's head: merged, without anyone asking.
|
|
w.forge.refuse = nil
|
|
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t2t2t2t2t2t2"))
|
|
w.settleAll()
|
|
if w.state(id) != Ready || len(w.forge.merges) != 2 || w.delivery(id).Ready.Merged == nil {
|
|
t.Fatalf("on its passing verdict it was not merged: %s %v %+v", w.state(id), w.forge.merges, w.delivery(id).Ready)
|
|
}
|
|
}
|
|
|
|
func TestAMarkedHeadWhoseCheckFailedIsNotMergedAndIsSaid(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", readyHead)
|
|
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
|
|
w.h.Checked(judgedAt("novox/app", 7, readyHead, "fail", "t1t1t1t1t1t1"))
|
|
w.forge.open = []OpenPull{markedPull("novox/app", 7, readyHead, readyHead)}
|
|
w.h.SeeOpenPulls()
|
|
w.settleAll()
|
|
w.later(10 * time.Minute)
|
|
w.h.SeeOpenPulls()
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 0 {
|
|
t.Fatalf("a head whose check failed was merged: %v", w.forge.merges)
|
|
}
|
|
if len(w.forge.comments) != 1 || !strings.Contains(w.forge.comments[0], "not merged") {
|
|
t.Fatalf("the failure is not said once on the pull request: %v", w.forge.comments)
|
|
}
|
|
lines := w.h.Stalled()
|
|
if len(lines) != 1 || lines[0].ID != id || lines[0].State != Rejected || !strings.Contains(lines[0].Says, "marked ready") {
|
|
t.Fatalf("the failure is not a stalled line, which the controller raises as a condition: %+v", lines)
|
|
}
|
|
}
|
|
|
|
func TestAMarkRefusedForAnHourIsSaid(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
|
|
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
|
|
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: mesh/repo-check is pending")
|
|
w.forge.open = []OpenPull{markedPull("novox/app", 7, readyHead, readyHead)}
|
|
w.h.SeeOpenPulls()
|
|
w.settleAll()
|
|
w.later(30 * time.Second)
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 1 {
|
|
t.Fatalf("asked again within the minute: %v", w.forge.merges)
|
|
}
|
|
w.later(readyRefusedFor)
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 2 {
|
|
t.Fatalf("not asked again after a minute: %v", w.forge.merges)
|
|
}
|
|
if lines := w.h.Stalled(); len(lines) != 1 || !strings.Contains(lines[0].Says, "refused it since") {
|
|
t.Fatalf("a mark refused for an hour is not said: %+v", lines)
|
|
}
|
|
}
|
|
|
|
func TestAGroupMergesItsMarkedMembersInOrderOnlyOnceComposed(t *testing.T) {
|
|
w := newWorld(t)
|
|
const ctlHead, catHead = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
|
|
ctl, cat := IDOf("novox/mesh-controller", ctlHead), IDOf("novox/mesh-catalog", catHead)
|
|
w.ctl.order = func(ms []Member) Order { return Order{Order: []string{ctl, cat}} }
|
|
w.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
|
|
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
|
|
w.h.Checked(judgedAt("novox/mesh-controller", 1, ctlHead, "pass", "t1t1t1t1t1t1"))
|
|
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catHead, "pass", "t3t3t3t3t3t3"))
|
|
w.forge.open = []OpenPull{markedPull("novox/mesh-controller", 1, ctlHead, ctlHead),
|
|
markedPull("novox/mesh-catalog", 2, catHead, catHead)}
|
|
w.h.SeeOpenPulls()
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 0 {
|
|
t.Fatalf("a member merged before its group's composed check passed: %v", w.forge.merges)
|
|
}
|
|
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 1 || w.forge.merges[0] != "novox/mesh-controller#1@"+ctlHead {
|
|
t.Fatalf("composed, the first member in the group's order was not merged alone: %v", w.forge.merges)
|
|
}
|
|
if waits := w.delivery(cat).Ready.Waits; !strings.Contains(waits, "merges "+ctl+" first") {
|
|
t.Fatalf("the second member does not say it waits its turn: %q", waits)
|
|
}
|
|
w.h.PullMerged(merged("novox/mesh-controller", 1, ctlHead, "m1m1m1m1m1m1"))
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 2 || w.forge.merges[1] != "novox/mesh-catalog#2@"+catHead {
|
|
t.Fatalf("after the first member merged, the next was not: %v", w.forge.merges)
|
|
}
|
|
}
|