175 lines
6.2 KiB
Go
175 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// **The pull request's key** (novox/hq ADR 0406 §4, issue 975): one key per pull request on the bus, holding its head,
|
|
// the trunk commit its current check is asked against and its verdict, written by compare-and-set. A head the forge
|
|
// announces writes head and trunk; a verdict for that head and trunk writes the verdict; nothing else writes.
|
|
|
|
type fakePulls struct {
|
|
mu sync.Mutex
|
|
keys map[string]PullKey
|
|
writes int
|
|
// lose makes the first n writes lose to another writer, which sets rival.
|
|
lose int
|
|
rival PullKey
|
|
}
|
|
|
|
func newFakePulls() *fakePulls { return &fakePulls{keys: map[string]PullKey{}} }
|
|
|
|
func (f *fakePulls) Change(key string, apply func(PullKey) (PullKey, bool)) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
for tries := 0; tries < 5; tries++ {
|
|
next, write := apply(f.keys[key])
|
|
if !write {
|
|
return nil
|
|
}
|
|
if f.lose > 0 { // another writer was first: read again and apply again
|
|
f.lose--
|
|
f.keys[key] = f.rival
|
|
continue
|
|
}
|
|
f.keys[key] = next
|
|
f.writes++
|
|
return nil
|
|
}
|
|
return errors.New("lost the race too often")
|
|
}
|
|
|
|
func (f *fakePulls) get(key string) PullKey {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.keys[key]
|
|
}
|
|
|
|
const appKey = "novox.app.7"
|
|
|
|
func announcedAt(head, trunk string) PullEvent {
|
|
p := pr("novox/app", 7, head, "feat/x", "src/a.go")
|
|
p.BaseSHA = trunk
|
|
return p
|
|
}
|
|
|
|
func TestAnAnnouncedHeadWritesTheKeyAndTheSameAnnouncementAgainWritesNothing(t *testing.T) {
|
|
w := newWorld(t)
|
|
pulls := newFakePulls()
|
|
w.h.Pulls = pulls
|
|
p := announcedAt(readyHead, "t1t1t1t1t1t1")
|
|
w.h.PullUpdated(p)
|
|
w.h.KeepPull(p)
|
|
if got := pulls.get(appKey); got.Head != readyHead || got.Trunk != "t1t1t1t1t1t1" || got.Verdict != "" {
|
|
t.Fatalf("the key holds %+v", got)
|
|
}
|
|
w.h.KeepPull(p)
|
|
if pulls.writes != 1 {
|
|
t.Fatalf("the same announcement wrote %d times", pulls.writes)
|
|
}
|
|
}
|
|
|
|
func TestAVerdictForTheKeysHeadAndTrunkIsWrittenAndOneForAnotherIsNot(t *testing.T) {
|
|
w := newWorld(t)
|
|
pulls := newFakePulls()
|
|
w.h.Pulls = pulls
|
|
w.h.KeepPull(announcedAt(readyHead, "t1t1t1t1t1t1"))
|
|
old := judgedAt("novox/app", 7, pushed, "pass", "t1t1t1t1t1t1")
|
|
w.h.KeepVerdict(old)
|
|
if pulls.get(appKey).Verdict != "" {
|
|
t.Fatal("a verdict for another head was written")
|
|
}
|
|
other := judgedAt("novox/app", 7, readyHead, "pass", "t9t9t9t9t9t9")
|
|
w.h.KeepVerdict(other)
|
|
if pulls.get(appKey).Verdict != "" {
|
|
t.Fatal("a verdict for another trunk commit was written")
|
|
}
|
|
w.h.KeepVerdict(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
|
|
if got := pulls.get(appKey); got.Verdict != "pass" {
|
|
t.Fatalf("the key holds %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestANewTrunkCommitClearsTheVerdictAndANewHeadReplacesTheKey(t *testing.T) {
|
|
w := newWorld(t)
|
|
pulls := newFakePulls()
|
|
w.h.Pulls = pulls
|
|
w.h.KeepPull(announcedAt(readyHead, "t1t1t1t1t1t1"))
|
|
w.h.KeepVerdict(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
|
|
w.h.KeepPull(announcedAt(readyHead, "t2t2t2t2t2t2"))
|
|
if got := pulls.get(appKey); got.Trunk != "t2t2t2t2t2t2" || got.Verdict != "" {
|
|
t.Fatalf("the check asked again against a new trunk left %+v", got)
|
|
}
|
|
w.h.KeepPull(announcedAt(pushed, "t2t2t2t2t2t2"))
|
|
if got := pulls.get(appKey); got.Head != pushed {
|
|
t.Fatalf("a new head left %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestALostRaceReadsTheKeyAgainAndAppliesTheChangeToWhatItFound(t *testing.T) {
|
|
w := newWorld(t)
|
|
pulls := newFakePulls()
|
|
pulls.lose, pulls.rival = 1, PullKey{Head: readyHead, Trunk: "t1t1t1t1t1t1", Verdict: "pass"}
|
|
w.h.Pulls = pulls
|
|
// The rival writer already holds this very head and trunk with a verdict: the change finds nothing to write.
|
|
w.h.KeepPull(announcedAt(readyHead, "t1t1t1t1t1t1"))
|
|
if got := pulls.get(appKey); got.Verdict != "pass" || pulls.writes != 0 {
|
|
t.Fatalf("after the lost race the key holds %+v after %d writes", got, pulls.writes)
|
|
}
|
|
}
|
|
|
|
// A check asked for a head whose Approve is asked is on the ready stream (ADR 0406 §3, ADR 0421 §10); one for a head
|
|
// with no Approve is asked as before.
|
|
func TestAPullRequestWithItsApproveAskedHasItsCheckAskedOnTheReadyStream(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.op.auto = false
|
|
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()
|
|
w.h.PullMerged(merged("novox/app", 9, "9999999999999999", "t2t2t2t2t2t2"))
|
|
w.settleAll()
|
|
if len(w.ctl.checkReady) == 0 || !w.ctl.checkReady[0] {
|
|
t.Fatalf("the check of a pull request whose Approve is asked was asked with ready %v", w.ctl.checkReady)
|
|
}
|
|
// One with no Approve asked is asked as before.
|
|
w2 := newWorld(t)
|
|
w2.h.PullUpdated(pr("novox/app", 8, pushed, "feat/y", "src/a.go"))
|
|
w2.settleAll()
|
|
if len(w2.ctl.checkReady) == 0 || w2.ctl.checkReady[0] {
|
|
t.Fatalf("the check of a pull request with no Approve asked was asked with ready %v", w2.ctl.checkReady)
|
|
}
|
|
}
|
|
|
|
func TestATrunkLessVerdictWritesNothingToTheKey(t *testing.T) {
|
|
w := newWorld(t)
|
|
pulls := newFakePulls()
|
|
w.h.Pulls = pulls
|
|
w.h.KeepPull(announcedAt(readyHead, "t1t1t1t1t1t1"))
|
|
w.h.KeepVerdict(judgedAt("novox/app", 7, readyHead, "pass", ""))
|
|
if got := pulls.get(appKey); got.Verdict != "" {
|
|
t.Fatalf("a verdict naming no trunk commit was written: %+v", got)
|
|
}
|
|
}
|
|
|
|
// The forge's catch-up after a restart can announce a merge again past the bus's de-duplication window (issue 975):
|
|
// heard again, it changes nothing, and moves no trunk back.
|
|
func TestTheSameMergeHeardAgainAfterALaterMergeMovesNoTrunkBack(t *testing.T) {
|
|
w := newWorld(t)
|
|
a, b := "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb"
|
|
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
|
|
w.h.PullUpdated(pr("novox/app", 8, a, "feat/y", "src/a.go"))
|
|
w.h.PullUpdated(pr("novox/app", 9, b, "feat/z", "src/a.go"))
|
|
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "m8m8m8m8m8m8"))
|
|
w.h.PullMerged(merged("novox/app", 8, a, "m8m8m8m8m8m8"))
|
|
w.h.PullMerged(merged("novox/app", 9, b, "m9m9m9m9m9m9"))
|
|
id := IDOf("novox/app", readyHead)
|
|
before := w.delivery(id)
|
|
w.h.PullMerged(merged("novox/app", 8, a, "m8m8m8m8m8m8")) // announced again, past the window
|
|
after := w.delivery(id)
|
|
if after.TrunkNow != before.TrunkNow || after.TrunkNow != "m9m9m9m9m9m9" {
|
|
t.Fatalf("the trunk moved from %q to %q on a merge heard again; wanted it to stay m9m9m9m9m9m9", before.TrunkNow, after.TrunkNow)
|
|
}
|
|
}
|