258 lines
11 KiB
Go
258 lines
11 KiB
Go
package manifest
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
|
|
|
"mesh-delivery-indexer/internal/index"
|
|
)
|
|
|
|
// 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 1238 (ADR 0437 §4). 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 streamDecl struct {
|
|
Name string `json:"name"`
|
|
Events []string `json:"events"`
|
|
MaxAgeDays int `json:"max-age-days,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"`
|
|
Claims []struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Serves []string `json:"serves"`
|
|
} `json:"claims"`
|
|
Tools []string `json:"tools"`
|
|
Emits []string `json:"emits"`
|
|
Consumes []string `json:"consumes"`
|
|
State []state `json:"state"`
|
|
Streams []streamDecl `json:"streams"`
|
|
Reads []string `json:"reads"`
|
|
Invokes []string `json:"invokes"`
|
|
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) { // 1238/AC1, 1444 (reads the build modules' toolchains)
|
|
m, top := load(t)
|
|
if m.Module != "mesh-delivery-indexer" {
|
|
t.Errorf("module = %q", m.Module)
|
|
}
|
|
// 1126: placement is the seat mesh-placement's (ADR 0567): the indexer holds no placement state. ADR 0578 §1: it says
|
|
// a refused manifest as manifest-refused, and (issue 1966 D3, 1966/AC10) a newly accepted one asking for bus state as
|
|
// manifest-accepted.
|
|
if !reflect.DeepEqual(m.Emits, []string{"manifest-refused", "manifest-accepted", takein.TakeInPendingEvent, takein.TakeInTakenEvent}) {
|
|
t.Errorf("emits = %v, want manifest-refused, manifest-accepted and the two take-in events", m.Emits)
|
|
}
|
|
// 1126 note 76: heard keeps the last 64 FORGE messages the indexer handled, for indexer_heard.
|
|
if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: "oldsections", History: 1}, {Name: delivery.PackagesState, History: delivery.PackagesHistory},
|
|
{Name: graph.State, History: graph.History}, {Name: index.PendingState, History: 1}}; !reflect.DeepEqual(m.State, want) {
|
|
t.Errorf("state = %+v, want %+v", m.State, want)
|
|
}
|
|
if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-assigner.placement", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
|
t.Errorf("reads = %v, want %v", m.Reads, want)
|
|
}
|
|
// "Reads FORGE" is a consumes of the indexer's stream: the broker grants only buckets by reads (mesh-control
|
|
// internal/broker/state.go:62-93).
|
|
if want := []string{"mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-delivery-checker.preview.written", "mesh-delivery-forge-bridge.forge.**"}; !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-indexer" {
|
|
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
|
}
|
|
// Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths:
|
|
// ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a
|
|
// commit is in another (issue 1900).
|
|
if want := []string{"gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition"}; !reflect.DeepEqual(m.Invokes, want) {
|
|
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
|
}
|
|
// 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle
|
|
// reads the verdicts state (indexer_verdicts) and asks the indexer to judge again through the takeins state
|
|
// (indexer_judge; issue 774, clause 3). 1966/AC11: indexer_old_sections reads the count of the old sections (state oldsections).
|
|
// 1585/T12, T13 (ADR 0633 §5): indexer_pending reads the pending state; indexer_take_in_pending and indexer_drop_pending
|
|
// ask through takeins; indexer_take_in_merge takes one merge in, its message asked of the forge bridge (issue 1585).
|
|
if want := []string{"indexer_take_in", "indexer_old_sections", "indexer_verdicts", "indexer_judge", index.PendingTool, index.TakeInPendingTool, index.DropPendingTool, index.TakeInMergeTool}; !reflect.DeepEqual(m.Tools, want) {
|
|
t.Errorf("tools = %v, want %v", m.Tools, want)
|
|
}
|
|
// ADR 0604 §2: it holds the seat mesh-catalog, serving its four verbs from its tools bundle (which stays without a
|
|
// request subject).
|
|
if len(m.Claims) != 1 || m.Claims[0].Name != catalogue.CatalogSeat || m.Claims[0].Scope != catalogue.ScopeMesh ||
|
|
!slices.Equal(m.Claims[0].Serves, []string{"modules", "module", "provides", "dependents"}) {
|
|
t.Errorf("claims = %+v, want mesh-catalog with its four verbs", m.Claims)
|
|
}
|
|
// 1585/T11 (ADR 0633 §1, §5): the take-in events are its ordinary events, with no stream of its own: a stream is
|
|
// outside the bus's predefined forms, and the manifest validator refuses it.
|
|
if len(m.Streams) != 0 || !slices.Contains(m.Emits, takein.TakeInPendingEvent) || !slices.Contains(m.Emits, takein.TakeInTakenEvent) {
|
|
t.Errorf("streams = %+v, emits = %v: want no stream, and both take-in events emitted", m.Streams, m.Emits)
|
|
}
|
|
// No other bus section, no other seat (ADR 0437 §4).
|
|
for _, k := range []string{"logs", "provides", "requires", "uses", "defines-seats", "seat", "seats", "bus"} {
|
|
if _, ok := top[k]; ok {
|
|
t.Errorf("the manifest declares %q: the indexer has none", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestManifestProcess(t *testing.T) { // 1238/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 indexer serves no tool and 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-indexer"}) {
|
|
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", p["env"])
|
|
}
|
|
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-indexer-tools" || !reflect.DeepEqual(a.Loads, []string{"mesh-delivery-indexer-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-indexer" || !strings.HasSuffix(a.From, a.Binary) {
|
|
t.Errorf("artifact = %+v", a)
|
|
}
|
|
}
|
|
|
|
func TestServesNoRequestSubjectAndHoldsNoTicker(t *testing.T) { // 1238/AC9 (ADR 0437 §8)
|
|
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 indexer serves no request subject and holds no timer", path, f)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if files < 2 {
|
|
t.Fatalf("read %d source files, want the program's", files)
|
|
}
|
|
}
|
|
|
|
// Every forge tool the program calls is granted in invokes, or the bus refuses the call (issue 1900).
|
|
func TestEveryGiteaToolTheCodeCallsIsInInvokes(t *testing.T) {
|
|
m, _ := load(t)
|
|
files, err := filepath.Glob("cmd/mesh-delivery-indexer/*.go")
|
|
if err != nil || len(files) == 0 {
|
|
t.Fatalf("no program files: %v", err)
|
|
}
|
|
re := regexp.MustCompile(`gitea\.gitea_[a-z_]+`)
|
|
seen := 0
|
|
for _, f := range files {
|
|
if strings.HasSuffix(f, "_test.go") {
|
|
continue
|
|
}
|
|
src, err := os.ReadFile(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, tool := range re.FindAllString(string(src), -1) {
|
|
seen++
|
|
if !slices.Contains(m.Invokes, tool) {
|
|
t.Errorf("%s calls %s, which invokes does not grant", f, tool)
|
|
}
|
|
}
|
|
}
|
|
if seen < 2 {
|
|
t.Errorf("found %d tool names in the program, want at least the two it calls", seen)
|
|
}
|
|
}
|