Merge order on a protected trunk was the whole proof; the review asked for ancestry. The gitea module gains gitea_contains, a read-only compare of two commits (total_commits 0 is contained), and mesh-delivery asks it once per pair, off the lock like its plan ask, keeping the answer on the delivery. A forge that cannot say leaves the merge held, not covered. Merge order stays the cheap first filter. A later delivery now carries a module only when its own check passed (so a merge held without a passing check is closed only by one that passed), its walk is of its own merge commit on the same trunk, and its steps say the module reached every machine running it, not one. The words say what is known: a later merge that contains it was delivered.
486 lines
13 KiB
Go
486 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// What a test stands the holder on: a store in memory that can be made to fail, a controller that records
|
|
// what it was asked and answers what it is told, a forge that records every note, view and status.
|
|
|
|
type memStore struct {
|
|
mu sync.Mutex
|
|
deliveries map[string][]byte
|
|
groups map[string][]byte
|
|
failing bool
|
|
}
|
|
|
|
func newMemStore() *memStore {
|
|
return &memStore{deliveries: map[string][]byte{}, groups: map[string][]byte{}}
|
|
}
|
|
|
|
func (s *memStore) PutDelivery(d *Delivery) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.failing {
|
|
return errors.New("the bus is away")
|
|
}
|
|
s.deliveries[d.ID] = mustJSON(d)
|
|
return nil
|
|
}
|
|
func (s *memStore) DeleteDelivery(id string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
delete(s.deliveries, id)
|
|
return nil
|
|
}
|
|
func (s *memStore) PutGroup(g *Group) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.failing {
|
|
return errors.New("the bus is away")
|
|
}
|
|
s.groups[g.ID] = mustJSON(g)
|
|
return nil
|
|
}
|
|
func (s *memStore) DeleteGroup(id string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
delete(s.groups, id)
|
|
return nil
|
|
}
|
|
func (s *memStore) Deliveries() ([]*Delivery, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
var out []*Delivery
|
|
for _, raw := range s.deliveries {
|
|
var d Delivery
|
|
if err := json.Unmarshal(raw, &d); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, &d)
|
|
}
|
|
return out, nil
|
|
}
|
|
func (s *memStore) Groups() ([]*Group, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
var out []*Group
|
|
for _, raw := range s.groups {
|
|
var g Group
|
|
if err := json.Unmarshal(raw, &g); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, &g)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type fakeController struct {
|
|
mu sync.Mutex
|
|
walks map[string]Walk
|
|
held bool
|
|
order func([]Member) Order
|
|
checks []string
|
|
asked int
|
|
delivers []string
|
|
stops []string
|
|
plans int
|
|
down bool
|
|
// onCheck runs as a check is asked, before it is answered: what the forge held at that moment.
|
|
onCheck func()
|
|
// planned is what Plan answers instead of "builds app", when set.
|
|
planned *DeliveryPlan
|
|
}
|
|
|
|
func newFakeController() *fakeController {
|
|
return &fakeController{walks: map[string]Walk{}, held: true}
|
|
}
|
|
|
|
func (c *fakeController) Plan(repository, base, head string, paths, dirs, removed []string) (*DeliveryPlan, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.plans++
|
|
if c.down {
|
|
return nil, errors.New("down")
|
|
}
|
|
if c.planned != nil {
|
|
p := *c.planned
|
|
return &p, nil
|
|
}
|
|
return &DeliveryPlan{Repository: repository, Base: base, Head: head, Moved: []string{"app"}, Summary: "builds app"}, nil
|
|
}
|
|
func (c *fakeController) Order(ms []Member) (Order, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.down {
|
|
return Order{}, errors.New("down")
|
|
}
|
|
if c.order != nil {
|
|
return c.order(ms), nil
|
|
}
|
|
var ids []string
|
|
for _, m := range ms {
|
|
ids = append(ids, m.ID)
|
|
}
|
|
sort.Strings(ids)
|
|
return Order{Order: ids}, nil
|
|
}
|
|
func (c *fakeController) Check(group string, ms []Member) (string, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.down {
|
|
return "", errors.New("down")
|
|
}
|
|
if c.onCheck != nil {
|
|
c.onCheck()
|
|
}
|
|
c.asked++
|
|
id := fmt.Sprintf("check-%d", c.asked)
|
|
var heads []string
|
|
for _, m := range ms {
|
|
heads = append(heads, m.ID)
|
|
}
|
|
c.checks = append(c.checks, group+":"+strings.Join(heads, ","))
|
|
return id, nil
|
|
}
|
|
func (c *fakeController) Deliver(walk, why string) error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.down {
|
|
return errors.New("down")
|
|
}
|
|
w, known := c.walks[walk]
|
|
if !known || w.Delivery == nil || w.Delivery.Go != nil {
|
|
return fmt.Errorf("%s waits for nobody", walk)
|
|
}
|
|
now := time.Now()
|
|
w.Delivery.Go, w.Delivery.By, w.Delivery.Why = &now, byOwner, why
|
|
w.Revision++
|
|
c.walks[walk] = w
|
|
c.delivers = append(c.delivers, walk)
|
|
return nil
|
|
}
|
|
func (c *fakeController) Stop(walk, why, by string) error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.down {
|
|
return errors.New("down")
|
|
}
|
|
c.stops = append(c.stops, walk)
|
|
if w, known := c.walks[walk]; known {
|
|
w.State = walkFailed
|
|
if w.Delivery == nil {
|
|
w.Delivery = &WalkDelivery{}
|
|
}
|
|
w.Delivery.Stopped, w.Delivery.StoppedWhy = by, why
|
|
w.Revision++
|
|
c.walks[walk] = w
|
|
}
|
|
return nil
|
|
}
|
|
func (c *fakeController) Walks(walk string) (bool, []Walk, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.down {
|
|
return false, nil, errors.New("down")
|
|
}
|
|
var out []Walk
|
|
for id, w := range c.walks {
|
|
if walk == "" || walk == id {
|
|
out = append(out, w)
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
|
|
return c.held, out, nil
|
|
}
|
|
|
|
// put is the controller keeping a walk; it answers the walk as `plan-moved` would say it.
|
|
func (c *fakeController) put(w Walk) Walk {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if old, known := c.walks[w.ID]; known {
|
|
w.Revision = old.Revision + 1
|
|
} else if w.Revision == 0 {
|
|
w.Revision = 1
|
|
}
|
|
c.walks[w.ID] = w
|
|
return w
|
|
}
|
|
|
|
func (c *fakeController) walk(id string) Walk {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.walks[id]
|
|
}
|
|
|
|
type fakeForge struct {
|
|
mu sync.Mutex
|
|
notes map[string][]string
|
|
views map[string]string
|
|
statuses map[string]string
|
|
down bool
|
|
// viewErr is what View answers instead, when set: a pull request the forge no longer holds.
|
|
viewErr error
|
|
viewCalls int
|
|
// What Statuses reads: the pull requests by owner/repo#number, the base's required statuses, whether the
|
|
// protection cannot be read, and what it was asked.
|
|
pulls map[string]ForgePull
|
|
required []string
|
|
protectionDown bool
|
|
asked []string
|
|
// contains answers gitea_contains by "<commit> in <in>"; a pair it does not hold is a forge that cannot say.
|
|
contains map[string]bool
|
|
containsDown bool
|
|
containsAsks []string
|
|
}
|
|
|
|
func newFakeForge() *fakeForge {
|
|
return &fakeForge{notes: map[string][]string{}, views: map[string]string{}, statuses: map[string]string{},
|
|
pulls: map[string]ForgePull{}, contains: map[string]bool{}}
|
|
}
|
|
|
|
func (f *fakeForge) Note(owner, repo, commit, line string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.down {
|
|
return errors.New("the forge is away")
|
|
}
|
|
key := owner + "/" + repo + "@" + commit
|
|
for _, l := range f.notes[key] {
|
|
if l == line {
|
|
return nil // appended once
|
|
}
|
|
}
|
|
f.notes[key] = append(f.notes[key], line)
|
|
return nil
|
|
}
|
|
func (f *fakeForge) View(owner, repo string, number int, body string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.viewCalls++
|
|
if f.viewErr != nil {
|
|
return f.viewErr
|
|
}
|
|
if f.down {
|
|
return errors.New("the forge is away")
|
|
}
|
|
f.views[fmt.Sprintf("%s/%s#%d", owner, repo, number)] = body
|
|
return nil
|
|
}
|
|
func (f *fakeForge) Status(owner, repo, commit, context, state, description, target string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.down {
|
|
return errors.New("the forge is away")
|
|
}
|
|
f.statuses[owner+"/"+repo+"@"+commit+" "+context] = state + " " + description + " → " + target
|
|
return nil
|
|
}
|
|
|
|
// Statuses answers what the fake was told: a pull request's head from pulls, the statuses set on the commit
|
|
// (those Status set too, as the forge would keep them), and the base's required statuses from required.
|
|
func (f *fakeForge) Statuses(owner, repo string, number int, commit, base string) (*ForgeChecks, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.down {
|
|
return nil, errors.New("the forge is away")
|
|
}
|
|
f.asked = append(f.asked, fmt.Sprintf("%s/%s#%d@%s base=%s", owner, repo, number, commit, base))
|
|
out := &ForgeChecks{}
|
|
if number > 0 {
|
|
p, ok := f.pulls[fmt.Sprintf("%s/%s#%d", owner, repo, number)]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no pull request %d", number)
|
|
}
|
|
out.Pull = &p
|
|
if commit == "" {
|
|
commit = p.HeadSHA
|
|
}
|
|
if base == "" {
|
|
base = p.Base
|
|
}
|
|
}
|
|
for key, said := range f.statuses {
|
|
at, context, _ := strings.Cut(key, " ")
|
|
if !strings.HasPrefix(at, owner+"/"+repo+"@") || !sameCommit(strings.TrimPrefix(at, owner+"/"+repo+"@"), commit) {
|
|
continue
|
|
}
|
|
if len(strings.TrimPrefix(at, owner+"/"+repo+"@")) > len(commit) {
|
|
commit = strings.TrimPrefix(at, owner+"/"+repo+"@")
|
|
}
|
|
state, rest, _ := strings.Cut(said, " ")
|
|
description, link, _ := strings.Cut(rest, " → ")
|
|
out.Statuses = append(out.Statuses, ForgeStatus{Context: context, State: state, Description: description,
|
|
TargetURL: link, UpdatedAt: "2026-10-06T12:00:00Z", Creator: "mesh-admin"})
|
|
}
|
|
sort.Slice(out.Statuses, func(i, j int) bool { return out.Statuses[i].Context < out.Statuses[j].Context })
|
|
out.Commit = commit
|
|
if base == "" {
|
|
base = "main"
|
|
}
|
|
out.Merge = ForgeMerge{Branch: base, Protected: f.required != nil, Required: f.required}
|
|
if f.protectionDown {
|
|
out.Merge.Says = "the protection of " + base + " could not be read"
|
|
return out, nil
|
|
}
|
|
ok := true
|
|
for _, r := range f.required {
|
|
state := "missing"
|
|
for _, st := range out.Statuses {
|
|
if st.Context == r {
|
|
state = st.State
|
|
}
|
|
}
|
|
if state != "success" {
|
|
ok = false
|
|
out.Merge.Blocking = append(out.Merge.Blocking, struct {
|
|
Context string `json:"context"`
|
|
State string `json:"state"`
|
|
}{r, state})
|
|
}
|
|
}
|
|
out.Merge.Mergeable = &ok
|
|
out.Merge.Says = fmt.Sprintf("%s requires %v", base, f.required)
|
|
return out, nil
|
|
}
|
|
|
|
// Contains answers what the fake was told of two commits.
|
|
func (f *fakeForge) Contains(owner, repo, commit, in string) (bool, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.containsAsks = append(f.containsAsks, commit+" in "+in)
|
|
if f.containsDown || f.down {
|
|
return false, errors.New("the forge is away")
|
|
}
|
|
yes, said := f.contains[commit+" in "+in]
|
|
if !said {
|
|
return false, fmt.Errorf("the forge has no commit %s or %s", commit, in)
|
|
}
|
|
return yes, nil
|
|
}
|
|
|
|
func (f *fakeForge) notesOn(commit string) []string {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
for key, lines := range f.notes {
|
|
if strings.HasSuffix(key, "@"+commit) {
|
|
return append([]string(nil), lines...)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// world is a holder over fakes, with a clock a test moves.
|
|
type world struct {
|
|
t *testing.T
|
|
h *Holder
|
|
store *memStore
|
|
ctl *fakeController
|
|
forge *fakeForge
|
|
now time.Time
|
|
said []map[string]any
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func newWorld(t *testing.T) *world {
|
|
return newWorldOver(t, newMemStore(), newFakeController(), newFakeForge(), time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC))
|
|
}
|
|
|
|
func newWorldOver(t *testing.T, store *memStore, ctl *fakeController, forge *fakeForge, now time.Time) *world {
|
|
w := &world{t: t, store: store, ctl: ctl, forge: forge, now: now}
|
|
w.h = &Holder{Store: store, Controller: ctl, Forge: forge,
|
|
Now: func() time.Time { w.mu.Lock(); defer w.mu.Unlock(); return w.now },
|
|
Logf: func(format string, a ...any) { t.Logf(format, a...) },
|
|
Emit: func(event string, body any) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
var m map[string]any
|
|
_ = json.Unmarshal(mustJSON(body), &m)
|
|
m["event-kind"] = event
|
|
w.said = append(w.said, m)
|
|
return nil
|
|
}}
|
|
w.h.init()
|
|
if err := w.h.Load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return w
|
|
}
|
|
|
|
func (w *world) later(d time.Duration) {
|
|
w.mu.Lock()
|
|
w.now = w.now.Add(d)
|
|
w.mu.Unlock()
|
|
}
|
|
|
|
// settleAll ticks and flushes until nothing changes, a few times.
|
|
func (w *world) settleAll() {
|
|
for range 4 {
|
|
w.h.Tick()
|
|
w.h.Flush()
|
|
}
|
|
}
|
|
|
|
func (w *world) delivery(id string) *Delivery {
|
|
w.t.Helper()
|
|
w.h.mu.Lock()
|
|
defer w.h.mu.Unlock()
|
|
d := w.h.deliveries[id]
|
|
if d == nil {
|
|
w.t.Fatalf("no delivery %s; there are %v", id, w.ids())
|
|
}
|
|
c := *d
|
|
return &c
|
|
}
|
|
|
|
func (w *world) ids() []string {
|
|
var out []string
|
|
for id := range w.h.deliveries {
|
|
out = append(out, id)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func (w *world) state(id string) State { return w.delivery(id).State }
|
|
|
|
// pr is a pull request's head as the forge announces it.
|
|
func pr(repo string, number int, sha, branch string, paths ...string) PullEvent {
|
|
owner, name, _ := strings.Cut(repo, "/")
|
|
return PullEvent{Owner: owner, Repo: name, Number: number, Base: "main", Head: branch, HeadSHA: sha,
|
|
HTMLURL: fmt.Sprintf("https://forge.invalid/%s/pulls/%d", repo, number), Paths: paths}
|
|
}
|
|
|
|
// verdict is the controller's `checked` for a head.
|
|
func verdict(repo string, number int, sha, gate string) CheckedEvent {
|
|
owner, name, _ := strings.Cut(repo, "/")
|
|
c := CheckedEvent{Owner: owner, Repo: name, Number: number, Commit: sha, Verdict: gate, Summary: "the gate said " + gate,
|
|
ID: "check-of-" + sha, Plan: &DeliveryPlan{Repository: repo, Moved: []string{"app"}, Summary: "builds app → anchor"}}
|
|
return c
|
|
}
|
|
|
|
// merged is the forge's `pull.merged`.
|
|
func merged(repo string, number int, head, merge string) PullEvent {
|
|
p := pr(repo, number, head, "feature")
|
|
p.MergeCommit = merge
|
|
p.MergedAt = "2026-10-06T12:30:00Z"
|
|
return p
|
|
}
|
|
|
|
// aWalk is the controller's walk of a merge commit, waiting for its delivery's word.
|
|
func aWalk(id, repo, commit string, waits bool) Walk {
|
|
w := Walk{ID: id, Repository: repo, Branch: "main", Commit: commit, Created: time.Date(2026, 10, 6, 12, 31, 0, 0, time.UTC),
|
|
State: walkBuilding, Tiers: [][]string{{"app"}}, Modules: map[string]*WalkModule{"app": {}}}
|
|
if waits {
|
|
w.Delivery = &WalkDelivery{Awaits: "mesh-delivery"}
|
|
}
|
|
return w
|
|
}
|