197 lines
7.5 KiB
Go
197 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// **The gate writes nothing to the forge: its statuses, views, notes and comments are owed keys, and its plan is read from
|
|
// `plans`** (novox/hq ADR 0437 §2, §8, ADR 0450 §2, §4, §7, issue 1349; the empty check answer is the mesh-delivery half of
|
|
// issue 1311). Fixture sources: the key and the value of an owed act are mesh-sdk go/delivery's (OwedKey, Owed); the arguments
|
|
// of `plans` are mesh-controller cmd/mesh-controller/seatverbs.go's (repository, paths, module-dirs, head, base, removed, data)
|
|
// and its answer the document of deliveryPlanDocument (plan, reach, mesh, gated).
|
|
|
|
// AC1: a transition that today sets a status, a view, a note or a comment writes the matching owed key.
|
|
func TestATransitionWritesItsStatusViewAndNoteAsOwedKeys(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.settleAll()
|
|
page := "https://forge.invalid/novox/app/pulls/7"
|
|
statusKey, _ := sdk.OwedKey("novox/app", 7, head, "status=mesh/delivery")
|
|
viewKey, _ := sdk.OwedKey("novox/app", 7, head, "view")
|
|
noteKey, _ := sdk.OwedKey("novox/app", 7, head, "note")
|
|
for _, key := range []string{statusKey, viewKey, noteKey} {
|
|
if _, there := w.owed.keys[key]; !there {
|
|
t.Fatalf("the key %s was not written; the keys are %v", key, w.owed.writes)
|
|
}
|
|
}
|
|
if s := w.owed.keys[statusKey]; s.Act != sdk.OwedStatus || s.Context != "mesh/delivery" || s.State != "pending" || s.Target != page || s.Text == "" {
|
|
t.Fatalf("the owed status is %+v; wanted mesh/delivery pending, pointing at %s", s, page)
|
|
}
|
|
if v := w.owed.keys[viewKey]; v.Act != sdk.OwedView || !strings.Contains(v.Text, id) {
|
|
t.Fatalf("the owed view is %+v", v)
|
|
}
|
|
if n := w.owed.keys[noteKey]; n.Act != sdk.OwedNote || n.Ref != "mesh-plan" || !strings.Contains(n.Text, "proposed") {
|
|
t.Fatalf("the owed note is %+v", n)
|
|
}
|
|
// What the bridge would perform is what the forge holds.
|
|
if len(w.forge.notesOn(head)) == 0 || !strings.HasPrefix(statusOn(w, "novox/app", head), "pending") {
|
|
t.Fatalf("the fake bridge did not perform what was owed: %v", w.forge.notesOn(head))
|
|
}
|
|
}
|
|
|
|
// AC1: a note on the merge commit, which no pull request names, is keyed with the number 0.
|
|
func TestANoteOnTheMergeCommitIsOwedWithNumberZero(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, "pass"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
w.h.WalkMoved(w.ctl.put(aWalk("plan-1", "novox/app", merge, true)))
|
|
w.settleAll()
|
|
key, _ := sdk.OwedKey("novox/app", 0, merge, "note")
|
|
if n, there := w.owed.keys[key]; !there || n.Act != sdk.OwedNote {
|
|
t.Fatalf("no note was owed on the merge commit under %s; the keys are %v", key, w.owed.writes)
|
|
}
|
|
}
|
|
|
|
// AC1: two comments on one head are two acts: the sequence number is in the act.
|
|
func TestTwoCommentsOnOneHeadAreTwoOwedKeys(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.mu.Lock()
|
|
d := w.h.deliveries[id]
|
|
w.h.sayOn(d, "the first thing said")
|
|
w.h.sayOn(d, "the second thing said")
|
|
w.h.mu.Unlock()
|
|
w.settleAll()
|
|
one, _ := sdk.OwedKey("novox/app", 7, head, "comment.1")
|
|
two, _ := sdk.OwedKey("novox/app", 7, head, "comment.2")
|
|
if w.owed.keys[one].Text != "the first thing said" || w.owed.keys[two].Text != "the second thing said" {
|
|
t.Fatalf("the comments are %+v and %+v", w.owed.keys[one], w.owed.keys[two])
|
|
}
|
|
if len(w.forge.comments) != 2 {
|
|
t.Fatalf("the bridge would say %d comments: %v", len(w.forge.comments), w.forge.comments)
|
|
}
|
|
}
|
|
|
|
// AC2: the gate's code calls no gitea write tool and no `delivery-plan`, and no controller verb beyond the ones its manifest
|
|
// names (ADR 0450 §7). Tests are left out: they name the words to prove them gone.
|
|
func TestTheGatesCodeCallsNoForgeWriteToolAndNoDeliveryPlan(t *testing.T) {
|
|
files, err := filepath.Glob("*.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
forbidden := []string{"delivery-plan", "gitea_commit_status\"", "gitea_delivery_view", "gitea_note_append", "gitea_add_comment",
|
|
"gitea_merge_pull_request\"", "gitea_open_pulls_write"}
|
|
// The controller's verbs the gate calls: ADR 0450 §7, less the checks and the queue, which the controller no longer
|
|
// keeps (issue 1318, ADR 0524 §5).
|
|
allowed := []string{"deliver", "delivery-stop", "delivery-backups-again", "delivery-walks", "plans"}
|
|
called := map[string]bool{}
|
|
fset := token.NewFileSet()
|
|
for _, f := range files {
|
|
if strings.HasSuffix(f, "_test.go") {
|
|
continue
|
|
}
|
|
file, err := parser.ParseFile(fset, f, nil, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
switch x := n.(type) {
|
|
case *ast.BasicLit:
|
|
if x.Kind == token.STRING {
|
|
for _, word := range forbidden {
|
|
if strings.Contains(x.Value, word) {
|
|
t.Errorf("%s holds the string %s", f, x.Value)
|
|
}
|
|
}
|
|
}
|
|
case *ast.CallExpr:
|
|
if sel, ok := x.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "verb" && len(x.Args) > 0 {
|
|
if lit, ok := x.Args[0].(*ast.BasicLit); ok {
|
|
verb, _ := strconv.Unquote(lit.Value)
|
|
called[verb] = true
|
|
if !slices.Contains(allowed, verb) {
|
|
t.Errorf("%s calls the controller's verb %q", f, verb)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
for _, verb := range []string{"plans"} {
|
|
if !called[verb] {
|
|
t.Errorf("the gate calls no %s: the test reads nothing", verb)
|
|
}
|
|
}
|
|
}
|
|
|
|
// stubController is a controller that answers every verb with one document, and keeps what it was asked.
|
|
type stubController struct {
|
|
mu sync.Mutex
|
|
keys []string
|
|
bodies []map[string]any
|
|
answer string
|
|
}
|
|
|
|
func (s *stubController) ask(key string, body any) (json.RawMessage, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.keys = append(s.keys, key)
|
|
b, _ := body.(map[string]any)
|
|
s.bodies = append(s.bodies, b)
|
|
return json.RawMessage(s.answer), nil
|
|
}
|
|
|
|
func (s *stubController) called(key string) []map[string]any {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
var out []map[string]any
|
|
for i, k := range s.keys {
|
|
if k == key {
|
|
out = append(out, s.bodies[i])
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// AC3: a delivery with no plan gets it from `plans`, asked as data, with the change's diffset.
|
|
func TestADeliveryWithNoPlanGetsItFromPlansAsData(t *testing.T) {
|
|
w := newWorld(t)
|
|
stub := &stubController{answer: `{"ok":true,"output":"","answer":{"asked":"check-1","held":true,"walks":[],
|
|
"plan":{"repository":"novox/app","base":"main","head":"` + head + `","moved":["app"],"summary":"builds app, sent to the node"}}}`}
|
|
w.h.Controller = seatController{ask: stub.ask}
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
c := verdict("novox/app", 7, head, "pass")
|
|
c.Plan = nil
|
|
w.h.Checked(c)
|
|
w.settleAll()
|
|
asked := stub.called("seat:mesh-controller.plans")
|
|
if len(asked) == 0 {
|
|
t.Fatalf("`plans` was not asked; the controller was asked %v", stub.keys)
|
|
}
|
|
if len(stub.called("seat:mesh-controller.delivery-plan")) != 0 {
|
|
t.Fatal("delivery-plan was asked")
|
|
}
|
|
got := asked[0]
|
|
if got["repository"] != "novox/app" || got["paths"] != "src/a.go" || got["data"] != "true" || got["head"] != head || got["base"] != "main" {
|
|
t.Fatalf("plans was asked %v; wanted the repository, the paths, the head, the base and data", got)
|
|
}
|
|
if d := w.delivery(IDOf("novox/app", head)); d.Plan == nil || d.Plan.Summary != "builds app, sent to the node" {
|
|
t.Fatalf("the delivery's plan is %+v", d.Plan)
|
|
}
|
|
}
|