The integration tests hear the forge's own word that a pull request merged, as the real flow does, and fail on any call of the controller.
362 lines
14 KiB
Go
362 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The holder, end to end over fakes (novox/hq ADR 0239 *How it is checked*, cut by ADR 0539 §17): a pull request's head
|
|
// proposed and ready; merged; a newer head closing the older; a restart resuming; the trunk rule; and every transition
|
|
// kept before it is said, noted on the commit and shown on the pull request.
|
|
|
|
const (
|
|
head = "aaaaaaaaaaaa1111"
|
|
merge = "mmmmmmmmmmmm9999"
|
|
)
|
|
|
|
func TestAPullRequestsHeadIsProposedAndReady(t *testing.T) {
|
|
w := newWorld(t)
|
|
p := pr("novox/app", 7, head, "feat/x", "src/a.go")
|
|
p.Body = "" // an empty description is never read again, so the review set below stays
|
|
w.h.PullUpdated(p)
|
|
id := IDOf("novox/app", head)
|
|
// This head is not a full commit id, so its description line cannot name it: its review is set as read.
|
|
w.h.deliveries[id].ReviewedBy, w.h.deliveries[id].ReviewedAt = "tester@shanks", head
|
|
if w.state(id) != Proposed {
|
|
t.Fatalf("a head is %s", w.state(id))
|
|
}
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
d := w.delivery(id)
|
|
if d.State != Ready {
|
|
t.Fatalf("a passing verdict left it %s", d.State)
|
|
}
|
|
var path []State
|
|
for _, tr := range d.Transitions {
|
|
path = append(path, tr.To)
|
|
}
|
|
if strings.Join(statesOf(path), ",") != "proposed,ready" {
|
|
t.Fatalf("its transitions are %v", path)
|
|
}
|
|
w.settleAll()
|
|
notes := w.forge.notesOn(head)
|
|
if len(notes) != 2 || !strings.Contains(notes[1], "proposed -> ready") {
|
|
t.Fatalf("the head's note is %q", notes)
|
|
}
|
|
if !strings.Contains(w.forge.statuses["novox/app@"+head+" mesh/delivery"], "success ready") {
|
|
t.Fatalf("the head's status is %q", w.forge.statuses)
|
|
}
|
|
if v := w.forge.views["novox/app#7"]; !strings.Contains(v, viewMarker) || !strings.Contains(v, "**ready**") {
|
|
t.Fatalf("the pull request's view is %q", v)
|
|
}
|
|
// A failing verdict for another head rejects it, and a newer head closes the older.
|
|
w.h.PullUpdated(pr("novox/app", 7, "bbbbbbbbbbbb2222", "feat/x"))
|
|
if w.state(id) != Closed {
|
|
t.Fatalf("a newer head left the older %s", w.state(id))
|
|
}
|
|
w.h.Checked(verdict("novox/app", 7, "bbbbbbbbbbbb2222", "fail"))
|
|
if s := w.state(IDOf("novox/app", "bbbbbbbbbbbb2222")); s != Rejected {
|
|
t.Fatalf("a failing verdict left it %s", s)
|
|
}
|
|
}
|
|
|
|
func statesOf(ss []State) []string {
|
|
var out []string
|
|
for _, s := range ss {
|
|
out = append(out, string(s))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// A merge puts the delivery on the trunk and ends it there: merged, with the merge commit's note and the pull request's
|
|
// status naming the source revision it moved. Nothing after the merge is derived (ADR 0539 §17).
|
|
func TestAMergeIsMergedAndTheGateDerivesNothingAfterIt(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
d := w.delivery(id)
|
|
if d.State != Merged || d.MergedAs != merge {
|
|
t.Fatalf("a merge left it %s as %q", d.State, d.MergedAs)
|
|
}
|
|
w.settleAll()
|
|
w.later(72 * time.Hour)
|
|
w.settleAll()
|
|
if s := w.state(id); s != Merged {
|
|
t.Fatalf("time moved a merged delivery to %s", s)
|
|
}
|
|
if st := w.forge.statuses["novox/app@"+head+" mesh/delivery"]; !strings.HasPrefix(st, "success merged: source.main moved to novox/app@"+merge[:12]) {
|
|
t.Fatalf("the pull request's mesh/delivery status is %q", st)
|
|
}
|
|
view := w.forge.views["novox/app#7"]
|
|
if !strings.Contains(view, "**Source**") || !strings.Contains(view, "source.main moved to novox/app@"+merge[:12]) ||
|
|
!strings.Contains(view, "mesh-delivery-view") || strings.Contains(view, "**Delivery plan**") || strings.Contains(view, "**Machines**") {
|
|
t.Fatalf("the pull request's view is %q", view)
|
|
}
|
|
n := 0
|
|
for _, s := range w.said {
|
|
if s["id"] == id {
|
|
n++
|
|
}
|
|
}
|
|
if n != len(d.Transitions) {
|
|
t.Fatalf("%d transitions, %d said", len(d.Transitions), n)
|
|
}
|
|
}
|
|
|
|
// The trunk rule: a ready delivery whose commit never reached the trunk is never merged.
|
|
func TestACommitOffTheTrunkIsNeverMerged(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.settleAll()
|
|
if s := w.state(id); s != Ready {
|
|
t.Fatalf("an off-trunk head is %s", s)
|
|
}
|
|
}
|
|
|
|
// A restart resumes: a new holder over the same state stands every delivery where it was.
|
|
func TestARestartResumesFromTheState(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
w.settleAll()
|
|
if s := w.state(id); s != Merged {
|
|
t.Fatalf("before the restart it is %s", s)
|
|
}
|
|
again := newWorldOver(t, w.store, w.forge, w.now.Add(time.Hour))
|
|
if s := again.state(id); s != Merged {
|
|
t.Fatalf("read back, it is %s", s)
|
|
}
|
|
}
|
|
|
|
// A delivery kept in a state an earlier owner had and this one does not is read as the state it came to and kept so.
|
|
func TestADeliveryKeptInAGoneStateIsReadAsTheMergeStateItCameTo(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
w.settleAll()
|
|
raw := w.store.deliveries[id]
|
|
w.store.deliveries[id] = []byte(strings.Replace(string(raw), `"state":"merged"`, `"state":"delivering"`, 1))
|
|
again := newWorldOver(t, w.store, w.forge, w.now.Add(time.Hour))
|
|
if s := again.state(id); s != Merged {
|
|
t.Fatalf("a delivering delivery was read as %s", s)
|
|
}
|
|
again.settleAll()
|
|
if !strings.Contains(string(w.store.deliveries[id]), `"state":"merged"`) {
|
|
t.Fatalf("the read state was not kept: %s", w.store.deliveries[id])
|
|
}
|
|
}
|
|
|
|
// Kept before it is said: with the state unreachable, nothing is emitted or noted; once it is kept, all of it.
|
|
func TestATransitionIsKeptBeforeItIsSaid(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.store.failing = true
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.settleAll()
|
|
if len(w.said) != 0 || len(w.forge.notesOn(head)) != 0 {
|
|
t.Fatalf("said before it was kept: %v %v", w.said, w.forge.notesOn(head))
|
|
}
|
|
w.store.failing = false
|
|
w.settleAll()
|
|
if len(w.said) != 1 || len(w.forge.notesOn(head)) != 1 {
|
|
t.Fatalf("once kept, said %d and noted %d", len(w.said), len(w.forge.notesOn(head)))
|
|
}
|
|
// A bucket the bus cannot take is written again; a note is appended once however often it is asked.
|
|
w.owed.down = true
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.settleAll()
|
|
if len(w.forge.notesOn(head)) != 1 {
|
|
t.Fatal("noted while the bus was away")
|
|
}
|
|
w.owed.down = false
|
|
w.later(time.Hour) // each failure put its next try later: past every one of them
|
|
w.settleAll()
|
|
w.settleAll()
|
|
if notes := w.forge.notesOn(head); len(notes) != 2 {
|
|
t.Fatalf("after the bus came back the note has %d line(s): %q", len(notes), notes)
|
|
}
|
|
if d := w.delivery(IDOf("novox/app", head)); len(d.Owed) != 0 {
|
|
t.Fatalf("still owed: %+v", d.Owed)
|
|
}
|
|
}
|
|
|
|
// A merge before the check passed is held for a person; a merge this owner never heard the head of is made
|
|
// from the forge's word and merged when the forge says its gate passed.
|
|
func TestAMergeWithoutAPassingCheckIsHeld(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "fail"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
if s := w.state(IDOf("novox/app", head)); s != Held {
|
|
t.Fatalf("a rejected head merged is %s", s)
|
|
}
|
|
// Never heard: the forge's statuses on its head decide.
|
|
m := merged("novox/lab", 3, "eeeeeeeeeeee5555", "ffffffffffff6666")
|
|
m.HeadChecks = map[string]string{"mesh/check": "success"}
|
|
m.Paths = []string{"README.md"}
|
|
w.h.PullMerged(m)
|
|
w.settleAll()
|
|
d := w.delivery(IDOf("novox/lab", "eeeeeeeeeeee5555"))
|
|
if d.State != Merged {
|
|
t.Fatalf("a merge whose head passed by the forge's word is %s", d.State)
|
|
}
|
|
}
|
|
|
|
// A group merged whole ends merged, member by member and as a group, and source.main holds its heads (ADR 0539 §17, R6-C2).
|
|
func TestAMergedGroupEndsMergedWithSourceMainMoved(t *testing.T) {
|
|
w := newWorld(t)
|
|
heads := map[string]string{"novox/a": "a1a1a1a1a1a1a1", "novox/b": "b2b2b2b2b2b2b2"}
|
|
ids := map[string]string{}
|
|
for i, repo := range []string{"novox/a", "novox/b"} {
|
|
ids[repo] = IDOf(repo, heads[repo])
|
|
w.h.PullUpdated(pr(repo, i+1, heads[repo], "feat/y", "x"))
|
|
w.h.Checked(verdict(repo, i+1, heads[repo], "pass"))
|
|
}
|
|
w.settleAll()
|
|
w.h.Checked(CheckedEvent{Group: "feat/y", ID: "check-1", Verdict: "pass", Summary: "compose"})
|
|
for i, repo := range []string{"novox/a", "novox/b"} {
|
|
w.h.PullMerged(merged(repo, i+1, heads[repo], strings.Repeat(string(rune('p'+i)), 12)))
|
|
}
|
|
w.settleAll()
|
|
for repo, id := range ids {
|
|
if s := w.state(id); s != Merged {
|
|
t.Errorf("%s is %s, wanted merged", repo, s)
|
|
}
|
|
}
|
|
g := w.h.groups["feat/y"]
|
|
if state, why := GroupState(g, w.h.membersOf(g)); state != "merged" {
|
|
t.Fatalf("the group is %s (%s), wanted merged", state, why)
|
|
}
|
|
if w.source.main == nil || w.source.main.Heads["novox/a"] != strings.Repeat("p", 12) || w.source.main.Heads["novox/b"] != strings.Repeat("q", 12) {
|
|
t.Fatalf("source.main is %+v, wanted both merges", w.source.main)
|
|
}
|
|
}
|
|
|
|
// ADR 0419 §2, issue 978: a description's `after:` line is not read, and says so once on its pull request. The
|
|
// composed check is asked as for any other group: no order is asked of the controller first, and none gates it.
|
|
func TestAnAfterLineIsNotReadAndSaidOnceAndNothingWaitsForAnOrder(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.op.auto = false
|
|
pa := pr("novox/a", 1, "a1a1a1a1a1a1a1", "feat/z")
|
|
pa.Body = "Goes after the other.\n\nafter: novox/b\n"
|
|
w.h.PullUpdated(pa)
|
|
w.h.PullUpdated(pr("novox/b", 2, "b2b2b2b2b2b2b2", "feat/z"))
|
|
w.h.Checked(verdict("novox/a", 1, "a1a1a1a1a1a1a1", "pass"))
|
|
w.h.Checked(verdict("novox/b", 2, "b2b2b2b2b2b2b2", "pass"))
|
|
w.settleAll()
|
|
w.later(time.Minute)
|
|
w.settleAll()
|
|
said := 0
|
|
for _, c := range w.forge.comments {
|
|
if strings.Contains(c, "not read") && strings.Contains(c, "novox/a#1") {
|
|
said++
|
|
}
|
|
}
|
|
if said != 1 {
|
|
t.Fatalf("the after: line is not said once on its pull request: %v", w.forge.comments)
|
|
}
|
|
// A new head of the same pull request, with the line still there, is not told again.
|
|
pa2 := pr("novox/a", 1, "a9a9a9a9a9a9a9", "feat/z")
|
|
pa2.Body = pa.Body
|
|
w.h.PullUpdated(pa2)
|
|
w.settleAll()
|
|
said = 0
|
|
for _, c := range w.forge.comments {
|
|
if strings.Contains(c, "not read") {
|
|
said++
|
|
}
|
|
}
|
|
if said != 1 {
|
|
t.Fatalf("said again for a new head: %v", w.forge.comments)
|
|
}
|
|
}
|
|
|
|
// Stalled: a delivery past its state's bound is listed with what H2 may do; close takes only that. Only a proposed
|
|
// delivery has a bound now: its check was asked and not answered.
|
|
func TestStalledAndCloseWorkFromTheTable(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
if _, err := w.h.Close(id, "too early"); err == nil {
|
|
t.Fatal("closed within its bound")
|
|
}
|
|
w.later(3 * time.Hour)
|
|
stalled := w.h.Stalled()
|
|
if len(stalled) != 1 || stalled[0].ID != id || stalled[0].State != Proposed {
|
|
t.Fatalf("stalled %+v", stalled)
|
|
}
|
|
// A merge is the gate's last word: it is never stalled, however old.
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
w.later(72 * time.Hour)
|
|
for _, s := range w.h.Stalled() {
|
|
if s.ID == id {
|
|
t.Fatalf("a merged delivery is stalled: %+v", s)
|
|
}
|
|
}
|
|
if _, err := w.h.Close(id, "x"); err == nil {
|
|
t.Fatal("H2 closed a merged delivery")
|
|
}
|
|
// A merge without a passing check is held: a person's, with no transition for H2.
|
|
w.h.PullUpdated(pr("novox/app", 8, "ffffffffffff0000", "feat/q"))
|
|
w.h.PullMerged(merged("novox/app", 8, "ffffffffffff0000", "eeeeeeeeeeee0000"))
|
|
w.later(48 * time.Hour)
|
|
if _, err := w.h.Close(IDOf("novox/app", "ffffffffffff0000"), "x"); err == nil {
|
|
t.Fatal("H2 closed a held delivery")
|
|
}
|
|
}
|
|
|
|
// novox/hq issue 867, ADR 0382 §1 (contract A): each member of a group in `groups` also says its pull
|
|
// request number, full head, base branch and whether it is open; `members` keeps its text form.
|
|
func TestGroupsNamesEachMembersPullRequestHeadBaseAndOpen(t *testing.T) {
|
|
w := newWorld(t)
|
|
headA := "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
|
|
headB := "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"
|
|
pb := pr("novox/b", 2, headB, "feat/z")
|
|
pb.Base = "release/1"
|
|
w.h.PullUpdated(pr("novox/a", 1, headA, "feat/z"))
|
|
w.h.PullUpdated(pb)
|
|
lines := w.h.Groups(false)
|
|
if len(lines) != 1 || len(lines[0].Details) != 2 || len(lines[0].Members) != 2 {
|
|
t.Fatalf("groups: %+v", lines)
|
|
}
|
|
got := map[string]MemberLine{}
|
|
for _, m := range lines[0].Details {
|
|
got[m.Repository] = m
|
|
}
|
|
a, b := got["novox/a"], got["novox/b"]
|
|
if a.Number != 1 || a.Head != headA || a.Base != "main" || !a.Open || a.ID == "" {
|
|
t.Fatalf("member a: %+v", a)
|
|
}
|
|
if b.Number != 2 || b.Head != headB || b.Base != "release/1" || !b.Open {
|
|
t.Fatalf("member b: %+v", b)
|
|
}
|
|
if !strings.Contains(lines[0].Members[0], "(") {
|
|
t.Fatalf("members lost its text form: %v", lines[0].Members)
|
|
}
|
|
// Closed unmerged: still listed while the group is, but not open.
|
|
w.h.PullClosed(pr("novox/b", 2, headB, "feat/z"))
|
|
for _, l := range w.h.Groups(true) {
|
|
for _, m := range l.Details {
|
|
if m.Repository == "novox/b" && m.Open {
|
|
t.Fatalf("a closed pull request is open: %+v", m)
|
|
}
|
|
}
|
|
}
|
|
// JSON keys are the contract's.
|
|
raw, _ := json.Marshal(a)
|
|
for _, k := range []string{`"number":1`, `"head":"` + headA + `"`, `"base":"main"`, `"open":true`} {
|
|
if !strings.Contains(string(raw), k) {
|
|
t.Fatalf("%s lacks %s", raw, k)
|
|
}
|
|
}
|
|
}
|