mesh-grants moves to the same mesh-sdk (3203358c), so its shadow user list grants the checker and the builder the DEAD_LETTERS reads the controller's list will, and the take-over is not blocked by a difference. The checker and the builder hear DEAD_LETTERS through buildrun.DeadLetterHearing: in the background, again at every reconnect and every ask waited for while deaf, and keep whether they hear it in their new state hearing, which checker_status and builder_stuck say as dead_letters. checker_try says that a try ended by the not-taken bound leaves its ask in the stream. Not done as asked: a module health tool. The catalogue refuses a tool check on a module with no runtime of its own and no other check beside it (ADR 0227 rule 8, ADR 0366 §1), so the deaf state is said in the status tools and the log.
204 lines
8.0 KiB
Go
204 lines
8.0 KiB
Go
package manifest
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The controller's manifest reader is mesh-controller internal/catalogue (Manifest, manifest.go: Consumes :521,
|
|
// Invokes :590, Streams :613, Reads :659; StreamDeclaration, streams.go), an internal package of another repository.
|
|
// These tests decode the manifest strictly into the fields those types name (DisallowUnknownFields) and hold the
|
|
// rules of issue 1607 (ADR 0453 §4, ADR 0478). The merge gate's module check proves the controller takes the manifest.
|
|
|
|
type state struct {
|
|
Name string `json:"name"`
|
|
History int `json:"history,omitempty"`
|
|
}
|
|
|
|
type artifact struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
Language string `json:"language"`
|
|
System string `json:"system"`
|
|
From string `json:"from"`
|
|
Binary string `json:"binary"`
|
|
Loads []string `json:"loads"`
|
|
}
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Version string `json:"version"`
|
|
Slug string `json:"slug"`
|
|
Tools []string `json:"tools"`
|
|
Emits []string `json:"emits"`
|
|
Consumes []string `json:"consumes"`
|
|
State []state `json:"state"`
|
|
Reads []string `json:"reads"`
|
|
Uses []string `json:"uses"`
|
|
Invokes []string `json:"invokes"`
|
|
Config map[string]struct {
|
|
Type string `json:"type"`
|
|
Description string `json:"description"`
|
|
Pattern string `json:"pattern"`
|
|
Default string `json:"default"`
|
|
} `json:"config"`
|
|
OwnSecrets map[string]string `json:"own-secrets"`
|
|
SecretsOwner string `json:"secrets-owner"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []artifact `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
func load(t *testing.T) (manifest, map[string]json.RawMessage) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile("module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&m); err != nil {
|
|
t.Fatalf("the controller's reader refuses a key this manifest has: %v", err)
|
|
}
|
|
var top map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &top); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m, top
|
|
}
|
|
|
|
func sorted(s []string) []string {
|
|
s = slices.Clone(s)
|
|
slices.Sort(s)
|
|
return s
|
|
}
|
|
|
|
func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
|
m, top := load(t)
|
|
if m.Module != "mesh-delivery-builder" {
|
|
t.Errorf("module = %q", m.Module)
|
|
}
|
|
if want := []state{{Name: "builds", History: 5}, {Name: "hearing", History: 1}, {Name: "instances", History: 1}, {Name: "queues", History: 1}, {Name: "reasks", History: 1}}; !reflect.DeepEqual(m.State, want) { // delivery.BuilderBuildsHistory
|
|
t.Errorf("state = %+v, want %+v", m.State, want)
|
|
}
|
|
// 1887/A4c: the builder chooses no node, so it reads neither presence nor have, invokes no build module's tool and
|
|
// holds no capacity; it puts asks on the three build seats and reads their steps (ADR 0525 §1, §4, §5).
|
|
if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-image.steps", "mesh-build-node.steps", "mesh-build-node.toolchains", "mesh-controller.assignments",
|
|
"mesh-delivery-gate.source", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
|
t.Errorf("reads = %v, want %v", m.Reads, want)
|
|
}
|
|
if want := []string{"node-build-go", "node-build-image", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
|
|
t.Errorf("uses = %v, want the three build seats", m.Uses)
|
|
}
|
|
// 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no
|
|
// other tool; it holds no capacity.
|
|
if want := []string{"seat:mesh-controller.held"}; !reflect.DeepEqual(m.Invokes, want) || len(m.Config) != 0 {
|
|
t.Errorf("invokes = %v, config = %v, want %v and no config", m.Invokes, m.Config, want)
|
|
}
|
|
// The builder is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it
|
|
// starts; and by the controller's artifacts-gone, a take-in refused for a gone blob (1937/AC4).
|
|
if want := []string{"mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-controller.artifacts-gone"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
|
|
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
|
}
|
|
if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" {
|
|
t.Errorf("own-secrets = %v, want exactly broker", m.OwnSecrets)
|
|
}
|
|
if m.SecretsOwner != "mesh-delivery-builder" {
|
|
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
|
}
|
|
// 1945: the builder's tools (builder_seat_queue: note 62), served by its tools bundle, and the event it says when it asks an ask again.
|
|
if want := []string{"builder_ask_again", "builder_seat_queue", "builder_stuck"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
|
t.Errorf("tools = %v, want %v", m.Tools, want)
|
|
}
|
|
if want := []string{"build.asked-again"}; !reflect.DeepEqual(m.Emits, want) {
|
|
t.Errorf("emits = %v, want %v", m.Emits, want)
|
|
}
|
|
for _, k := range []string{"streams", "logs", "claims", "provides", "requires", "defines-seats", "seat", "seats", "bus", "config"} {
|
|
if _, ok := top[k]; ok {
|
|
t.Errorf("the manifest declares %q: the builder has none", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestManifestProcess(t *testing.T) { // 1607/AC1
|
|
m, _ := load(t)
|
|
var procs []map[string]any
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "process" {
|
|
procs = append(procs, r)
|
|
}
|
|
if r["type"] == "tool" || r["type"] == "seat" {
|
|
t.Errorf("resource %v: the builder's tools are its bundle's, and it holds no seat", r)
|
|
}
|
|
}
|
|
if len(procs) != 1 {
|
|
t.Fatalf("%d process resources, want one", len(procs))
|
|
}
|
|
p := procs[0]
|
|
if p["artifact"] != "program" || !reflect.DeepEqual(p["run"], []any{"./mesh-delivery-builder"}) {
|
|
t.Errorf("process = %v", p)
|
|
}
|
|
env, _ := p["env"].(map[string]any)
|
|
if len(env) != 1 || env["MESH_BROKER_FILE"] != "${dir:state}/broker" {
|
|
t.Errorf("env = %v, want the broker file alone", p["env"])
|
|
}
|
|
for _, r := range m.Resources {
|
|
if r["id"] == "settings" {
|
|
t.Errorf("a settings resource %v: the builder holds no setting since the capacity is the build modules' (ADR 0525 §5)", r)
|
|
}
|
|
}
|
|
if _, bus := p["bus"]; bus {
|
|
t.Errorf("the process declares a bus: it runs on its own broker credential")
|
|
}
|
|
if len(m.Build.Artifacts) != 2 {
|
|
t.Fatalf("artifacts = %+v, want the program and the tools", m.Build.Artifacts)
|
|
}
|
|
if a := m.Build.Artifacts[1]; a.Name != "tools" || a.Kind != "bundle" || a.Binary != "mesh-delivery-builder-tools" || !reflect.DeepEqual(a.Loads, []string{"mesh-delivery-builder-tools"}) {
|
|
t.Errorf("tools artifact = %+v", a) // 1945
|
|
} else if st, err := os.Stat(a.From); err != nil || !st.IsDir() {
|
|
t.Errorf("the tools' source %s: %v", a.From, err)
|
|
}
|
|
a := m.Build.Artifacts[0]
|
|
if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != "mesh-delivery-builder" || !strings.HasSuffix(a.From, a.Binary) || a.Kind != "bundle" || a.Language != "go" {
|
|
t.Errorf("artifact = %+v", a)
|
|
}
|
|
}
|
|
|
|
func TestServesNoRequestSubjectAndHoldsNoTicker(t *testing.T) { // 1607/AC6 (ADR 0437 §8: no timer, a wait is a context's bound)
|
|
forbidden := []string{"Subscribe(", "QueueSubscribe", "ChanSubscribe", "time.NewTicker", "time.Tick", "time.NewTimer", "time.AfterFunc", "time.After(", "time.Sleep"}
|
|
var files int
|
|
for _, root := range []string{"cmd", "internal"} {
|
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return err
|
|
}
|
|
files++
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, f := range forbidden {
|
|
if strings.Contains(string(src), f) {
|
|
t.Errorf("%s holds %s: the builder serves no request subject and holds no timer", path, f)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if files < 1 {
|
|
t.Fatalf("read %d source files, want the program's", files)
|
|
}
|
|
}
|