One module answers 'did my change go out' for a commit and orders a cross-repository change: one compiled state table, its state on the bus, every transition said, noted on the commit and shown on the pull request. The forge's holder gains the note, view and status tools it asks with, and says closed pull requests and a merge's head and statuses.
375 lines
9.1 KiB
Go
375 lines
9.1 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
|
|
}
|
|
|
|
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")
|
|
}
|
|
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")
|
|
}
|
|
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
|
|
}
|
|
|
|
func newFakeForge() *fakeForge {
|
|
return &fakeForge{notes: map[string][]string{}, views: map[string]string{}, statuses: map[string]string{}}
|
|
}
|
|
|
|
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()
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|