The judge refuses bus state in requires that breaks a template's bounds, naming the entry and the field (issue 1966 D1)
This commit is contained in:
@@ -23,3 +23,5 @@ require (
|
||||
)
|
||||
|
||||
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun => ../mesh-delivery-builder/buildrun
|
||||
|
||||
replace git.novox.be/novox/mesh-sdk/go => ../../../mesh-sdk/go
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
package judge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// The rules for bus state asked for in `requires` (issue 1966 D1; novox/hq ADR 0595 §1 to §3, ADR 0596 §2, ADR 0597 §2,
|
||||
// design 25 § 12.1). mesh-sdk keeps only the entry's shape (ADR 0578 §7); every bound below is this judge's. An entry is
|
||||
// judged once, here, and a refusal names the module, the entry and the field.
|
||||
|
||||
// optionKind is what an option's value is.
|
||||
type optionKind int
|
||||
|
||||
const (
|
||||
optionInt optionKind = iota
|
||||
optionFlag
|
||||
optionText
|
||||
optionEvents
|
||||
)
|
||||
|
||||
// optionRule is one option a template takes: its kind, its bound where it is a number, and whether it must be given.
|
||||
type optionRule struct {
|
||||
kind optionKind
|
||||
least, most int
|
||||
required bool
|
||||
}
|
||||
|
||||
// busTemplate is a template: the storage type it is on, the options it takes, and what it fixes in plain words.
|
||||
type busTemplate struct {
|
||||
storage string
|
||||
options map[string]optionRule
|
||||
fixed string
|
||||
}
|
||||
|
||||
// busTemplates are the six templates of design 25 § 12.1 and the bounds of its table.
|
||||
var busTemplates = map[string]busTemplate{
|
||||
catalogue.TemplateState: {
|
||||
storage: catalogue.StorageKeyValue,
|
||||
fixed: "what happens when full, the size of a value and of the bucket, and the copies",
|
||||
options: map[string]optionRule{
|
||||
catalogue.OptionPastValues: {kind: optionInt, least: 1, most: catalogue.StateMostHistory},
|
||||
catalogue.OptionTimeToLive: {kind: optionInt, least: 1, most: 315360000},
|
||||
catalogue.OptionLifetimePerKey: {kind: optionFlag},
|
||||
catalogue.OptionPerMachine: {kind: optionFlag},
|
||||
},
|
||||
},
|
||||
catalogue.TemplateEvents: {
|
||||
storage: catalogue.StorageStream,
|
||||
fixed: "what happens when full (the oldest is dropped) and the module's own subjects",
|
||||
options: map[string]optionRule{
|
||||
"events": {kind: optionEvents, required: true},
|
||||
catalogue.OptionAgeDays: {kind: optionInt, least: catalogue.StreamLeastDays, most: catalogue.StreamMostDays},
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
|
||||
catalogue.OptionGrant: {kind: optionText},
|
||||
},
|
||||
},
|
||||
catalogue.TemplateWorkQueue: {
|
||||
storage: catalogue.StorageStream,
|
||||
fixed: "what happens when full (a new message is refused), one taker, and no grant",
|
||||
options: map[string]optionRule{
|
||||
"events": {kind: optionEvents, required: true},
|
||||
catalogue.OptionDeliveries: {kind: optionInt, least: catalogue.StreamLeastDeliveries, most: catalogue.StreamMostDeliveries},
|
||||
catalogue.OptionDuplicates: {kind: optionInt, least: 1, most: catalogue.StreamMostDays * 86400},
|
||||
catalogue.OptionAgeDays: {kind: optionInt, least: catalogue.StreamLeastDays, most: catalogue.StreamMostDays},
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
|
||||
},
|
||||
},
|
||||
catalogue.TemplateLog: {
|
||||
storage: catalogue.StorageStream,
|
||||
fixed: "what happens when full (a new entry is refused), that an entry is never rewritten, and its size",
|
||||
options: map[string]optionRule{
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
|
||||
catalogue.OptionAgeDays: {kind: optionInt, least: 1, most: 3650},
|
||||
},
|
||||
},
|
||||
catalogue.TemplateObjects: {
|
||||
storage: catalogue.StorageObjectStore,
|
||||
fixed: "what happens when full (a new object is refused) and that objects are kept for good",
|
||||
options: map[string]optionRule{
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.ObjectsLeastMiB, most: catalogue.ObjectsMostMiB, required: true},
|
||||
catalogue.OptionGrant: {kind: optionText},
|
||||
},
|
||||
},
|
||||
catalogue.TemplateLedger: {
|
||||
storage: catalogue.StorageLedger,
|
||||
fixed: "what happens when full (a new entry is refused) and its state and index buckets",
|
||||
options: map[string]optionRule{
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// bareStream is a stream of a module's own configuration, where no template fits: the three settings the judge
|
||||
// refuses one for missing (design 25 § 12.1), and those it may also give.
|
||||
var bareStream = map[string]optionRule{
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB, required: true},
|
||||
catalogue.OptionAgeDays: {kind: optionInt, least: 1, most: 3650, required: true},
|
||||
catalogue.OptionGrant: {kind: optionText},
|
||||
"events": {kind: optionEvents},
|
||||
}
|
||||
|
||||
// ledgerConfiguration is what a ledger's own configuration may give: how long its history is kept (design 25 § 12.1).
|
||||
var ledgerConfiguration = map[string]optionRule{
|
||||
catalogue.OptionAgeDays: {kind: optionInt, least: 1, most: 3650},
|
||||
}
|
||||
|
||||
// noAge is how a bare stream says it keeps its messages for good.
|
||||
const noAge = "none"
|
||||
|
||||
// ledgerEffects are the four effects an operation may have (ADR 0597 §2).
|
||||
var ledgerEffects = []string{catalogue.EffectSetFields, catalogue.EffectSetEntry, catalogue.EffectAppend, catalogue.EffectRemove}
|
||||
|
||||
// BusStateProblems is what is wrong with one manifest's bus state, judged on the manifest alone (issue 1966 D1).
|
||||
func BusStateProblems(m catalogue.Manifest) []string {
|
||||
if len(m.BusState) == 0 {
|
||||
return nil
|
||||
}
|
||||
var problems []string
|
||||
if !stateName.MatchString(m.Module) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s keeps bus state, and a module's name is part of its names on the bus, which take one plain name — "+
|
||||
"no dot (novox/hq ADR 0201, ADR 0595 §1)", m.Module))
|
||||
}
|
||||
storageOf := map[string]string{}
|
||||
terminal := catalogue.TerminalKeys(m)
|
||||
for _, e := range m.BusState {
|
||||
at := fmt.Sprintf("%s's bus state %q", m.Module, e.Name)
|
||||
switch {
|
||||
case strings.Contains(e.Name, "_"):
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: its name holds an underscore, which separates the module from the local name on the bus; use a "+
|
||||
"hyphen (novox/hq design 25 § 12.1)", at))
|
||||
case !stateName.MatchString(e.Name):
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: its name is a local name — lower-case letters, digits and hyphens, no dot and no underscore; the "+
|
||||
"mesh derives the name on the bus (novox/hq ADR 0201)", at))
|
||||
}
|
||||
if first, taken := storageOf[e.Name]; taken {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: the name is used twice (as %s and as %s); each name is one, whatever its storage type (novox/hq "+
|
||||
"ADR 0595 §1)", at, first, e.Storage))
|
||||
}
|
||||
storageOf[e.Name] = e.Storage
|
||||
problems = append(problems, busEntryProblems(at, e, terminal)...)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// busEntryProblems judges one entry's storage type, template, options and configuration.
|
||||
func busEntryProblems(at string, e catalogue.BusStateEntry, terminal []string) []string {
|
||||
var problems []string
|
||||
switch e.Storage {
|
||||
case catalogue.StorageStream, catalogue.StorageKeyValue, catalogue.StorageObjectStore, catalogue.StorageLedger:
|
||||
default:
|
||||
return []string{fmt.Sprintf("%s: the storage type %q is unknown; it is %s", at, e.Storage,
|
||||
strings.Join([]string{catalogue.StorageStream, catalogue.StorageKeyValue, catalogue.StorageObjectStore, catalogue.StorageLedger}, ", "))}
|
||||
}
|
||||
if e.Storage != catalogue.StorageLedger && len(e.Types) > 0 {
|
||||
problems = append(problems, fmt.Sprintf("%s: types are a ledger's; storage type %s has none", at, e.Storage))
|
||||
}
|
||||
if e.Template == "" {
|
||||
if e.Storage != catalogue.StorageStream {
|
||||
return append(problems, fmt.Sprintf(
|
||||
"%s: the storage type %s is asked for by its template; only a stream is asked for bare, with a "+
|
||||
"configuration of its own (novox/hq design 25 § 12.1)", at, e.Storage))
|
||||
}
|
||||
if len(e.Options) > 0 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: a bare stream gives its settings in its configuration, not in options", at))
|
||||
}
|
||||
return append(problems, settingsProblems(at, "configuration", e.Configuration, bareStream, "a bare stream", terminal, true)...)
|
||||
}
|
||||
tpl, known := busTemplates[e.Template]
|
||||
switch {
|
||||
case !known:
|
||||
names := make([]string, 0, len(busTemplates))
|
||||
for n := range busTemplates {
|
||||
names = append(names, n)
|
||||
}
|
||||
slices.Sort(names)
|
||||
return append(problems, fmt.Sprintf("%s: the template %q is unknown; it is %s", at, e.Template, strings.Join(names, ", ")))
|
||||
case tpl.storage != e.Storage:
|
||||
return append(problems, fmt.Sprintf(
|
||||
"%s: the template %s is on the storage type %s, not %s", at, e.Template, tpl.storage, e.Storage))
|
||||
}
|
||||
problems = append(problems, settingsProblems(at, "options", e.Options, tpl.options, "the template "+e.Template+" (it fixes "+tpl.fixed+")", terminal, false)...)
|
||||
if e.Template == catalogue.TemplateLedger {
|
||||
problems = append(problems, settingsProblems(at, "configuration", e.Configuration, ledgerConfiguration, "a ledger's own configuration", terminal, false)...)
|
||||
problems = append(problems, ledgerTypeProblems(at, e)...)
|
||||
} else if len(e.Configuration) > 0 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: the template %s is given a configuration; a configuration of its own is for a bare stream or a "+
|
||||
"ledger whose history expires, and a template's settings are its options (novox/hq ADR 0595 §2)", at, e.Template))
|
||||
}
|
||||
if e.Template == catalogue.TemplateWorkQueue {
|
||||
problems = append(problems, workQueueProblems(at, e)...)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// settingsProblems judges the given settings against the rules: an unknown one is refused (a fixed setting among them),
|
||||
// a number outside its bound, a kind mismatch, and a required one missing. A bare stream's when-full and age are read
|
||||
// here too.
|
||||
func settingsProblems(at, field string, given map[string]json.RawMessage, rules map[string]optionRule, of string, terminal []string, bare bool) []string {
|
||||
var problems []string
|
||||
for _, k := range sortedKeys(given) {
|
||||
raw := given[k]
|
||||
if bare && k == catalogue.ConfigWhenFull {
|
||||
var v string
|
||||
if json.Unmarshal(raw, &v) != nil || (v != catalogue.WhenFullRefuse && v != catalogue.WhenFullDropOldest) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: %s %q is %s; a bare stream says %q or %q", at, field, k, string(raw), catalogue.WhenFullRefuse, catalogue.WhenFullDropOldest))
|
||||
}
|
||||
continue
|
||||
}
|
||||
rule, ok := rules[k]
|
||||
if !ok {
|
||||
if k == catalogue.ConfigWhenFull {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: %s %q is given to %s; what happens when full is fixed and is never an option (novox/hq ADR 0595 §3)", at, field, k, of))
|
||||
continue
|
||||
}
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: %s %q is not a setting of %s (novox/hq ADR 0595 §3)", at, field, k, of))
|
||||
continue
|
||||
}
|
||||
problems = append(problems, valueProblems(at, field, k, raw, rule, terminal, bare)...)
|
||||
}
|
||||
for _, k := range sortedKeys(rules) {
|
||||
if _, given := given[k]; !given && rules[k].required {
|
||||
problems = append(problems, fmt.Sprintf("%s: %s %q is missing; %s gives it", at, field, k, of))
|
||||
}
|
||||
}
|
||||
if bare {
|
||||
if _, given := given[catalogue.ConfigWhenFull]; !given {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: %s %q is missing; a bare stream says %q or %q", at, field, catalogue.ConfigWhenFull, catalogue.WhenFullRefuse, catalogue.WhenFullDropOldest))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// valueProblems judges one setting's value against its rule.
|
||||
func valueProblems(at, field, key string, raw json.RawMessage, rule optionRule, terminal []string, bare bool) []string {
|
||||
switch rule.kind {
|
||||
case optionInt:
|
||||
if bare && key == catalogue.OptionAgeDays {
|
||||
var word string
|
||||
if json.Unmarshal(raw, &word) == nil && word == noAge {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
n, ok := wholeNumber(raw)
|
||||
if !ok {
|
||||
return []string{fmt.Sprintf("%s: %s %q is %s, which is not a whole number", at, field, key, string(raw))}
|
||||
}
|
||||
if n < rule.least || n > rule.most {
|
||||
extra := ""
|
||||
if key == catalogue.OptionPastValues && n == 0 {
|
||||
extra = "; 0 is refused here"
|
||||
}
|
||||
return []string{fmt.Sprintf("%s: %s %q is %d, outside its bound of %d to %d%s", at, field, key, n, rule.least, rule.most, extra)}
|
||||
}
|
||||
case optionFlag:
|
||||
var b bool
|
||||
if json.Unmarshal(raw, &b) != nil {
|
||||
return []string{fmt.Sprintf("%s: %s %q is %s, which is not true or false", at, field, key, string(raw))}
|
||||
}
|
||||
case optionText:
|
||||
var config string
|
||||
if json.Unmarshal(raw, &config) != nil || !slices.Contains(terminal, config) {
|
||||
return []string{fmt.Sprintf(
|
||||
"%s: %s %q names %s, which is not a trusted config value of the module, set only at the controller's "+
|
||||
"terminal or on the operator's Approve (novox/hq ADR 0348 §8)", at, field, key, string(raw))}
|
||||
}
|
||||
case optionEvents:
|
||||
var events []string
|
||||
if json.Unmarshal(raw, &events) != nil || len(events) == 0 {
|
||||
return []string{fmt.Sprintf("%s: %s %q is %s; it is a list of at least one event", at, field, key, string(raw))}
|
||||
}
|
||||
var out []string
|
||||
for _, ev := range events {
|
||||
if err := eventPattern(ev, false); err != nil {
|
||||
out = append(out, fmt.Sprintf("%s: %s %q carries %q: %v", at, field, key, ev, err))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wholeNumber reads a JSON number that is a whole number.
|
||||
func wholeNumber(raw json.RawMessage) (int, bool) {
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
var n json.Number
|
||||
if dec.Decode(&n) != nil {
|
||||
return 0, false
|
||||
}
|
||||
i, err := n.Int64()
|
||||
return int(i), err == nil
|
||||
}
|
||||
|
||||
// workQueueProblems holds a work queue's duplicate window to its age (design 25 § 12.1: up to its age).
|
||||
func workQueueProblems(at string, e catalogue.BusStateEntry) []string {
|
||||
raw, given := e.Options[catalogue.OptionDuplicates]
|
||||
if !given {
|
||||
return nil
|
||||
}
|
||||
window, ok := wholeNumber(raw)
|
||||
if !ok {
|
||||
return nil // valueProblems said so
|
||||
}
|
||||
days := catalogue.StreamDefaultDays
|
||||
if age, ok := wholeNumber(e.Options[catalogue.OptionAgeDays]); ok && age > 0 {
|
||||
days = age
|
||||
}
|
||||
if window > days*86400 {
|
||||
return []string{fmt.Sprintf(
|
||||
"%s: options %q is %d seconds, longer than the queue's age of %d days (novox/hq design 25 § 12.1)",
|
||||
at, catalogue.OptionDuplicates, window, days)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ledgerTypeProblems judges a ledger's types: named locally and once, and every operation's effect one of the four.
|
||||
func ledgerTypeProblems(at string, e catalogue.BusStateEntry) []string {
|
||||
var problems []string
|
||||
if len(e.Types) == 0 {
|
||||
problems = append(problems, fmt.Sprintf("%s: a ledger names at least one type (novox/hq ADR 0597 §1)", at))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, t := range e.Types {
|
||||
where := fmt.Sprintf("%s: type %q", at, t.Name)
|
||||
switch {
|
||||
case !stateName.MatchString(t.Name):
|
||||
problems = append(problems, fmt.Sprintf("%s: its name is lower-case letters, digits and hyphens", where))
|
||||
case seen[t.Name]:
|
||||
problems = append(problems, fmt.Sprintf("%s: the type is named twice", where))
|
||||
}
|
||||
seen[t.Name] = true
|
||||
operations := map[string]bool{}
|
||||
for _, op := range t.Operations {
|
||||
if operations[op.Name] {
|
||||
problems = append(problems, fmt.Sprintf("%s: the operation %q is named twice", where, op.Name))
|
||||
}
|
||||
operations[op.Name] = true
|
||||
if !slices.Contains(ledgerEffects, op.Effect) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: the operation %q has the effect %q, outside the four (%s) (novox/hq ADR 0597 §2)",
|
||||
where, op.Name, op.Effect, strings.Join(ledgerEffects, ", ")))
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// BusStateCatalogueProblems are the rules of bus state no single manifest can be judged against: two modules' names on
|
||||
// the bus alike, and a `reads` naming bus state in the new form, which has no readers (novox/hq ADR 0596 §2).
|
||||
func BusStateCatalogueProblems(shelf catalogue.Shelf) []string {
|
||||
var problems []string
|
||||
owner := map[string]string{} // a name on the bus, upper-cased, to the module asking for it
|
||||
for _, module := range shelfOrder(shelf) {
|
||||
for _, o := range shelf[module].Objects {
|
||||
owner[strings.ToUpper(o.Bucket(module))] = module
|
||||
}
|
||||
}
|
||||
for _, module := range shelfOrder(shelf) {
|
||||
for _, e := range shelf[module].BusState {
|
||||
name := catalogue.ObjectsBucket(module, e.Name)
|
||||
if first, taken := owner[strings.ToUpper(name)]; taken && first != module {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s's bus state %q has the name %s on the bus, which %s's already has or is alike; a name on the bus "+
|
||||
"has one owner (novox/hq ADR 0201 §4)", module, e.Name, name, first))
|
||||
continue
|
||||
}
|
||||
owner[strings.ToUpper(name)] = module
|
||||
}
|
||||
}
|
||||
for _, module := range shelfOrder(shelf) {
|
||||
for _, r := range shelf[module].Reads {
|
||||
from, local, err := catalogue.ReadState(r)
|
||||
if err != nil || from == module {
|
||||
continue // StateProblems says why
|
||||
}
|
||||
for _, e := range shelf[from].BusState {
|
||||
if e.Name == local {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s reads %q, which %s has moved to bus state in `requires`; the new form has no readers: ask %s's "+
|
||||
"tools, or hear its events (novox/hq ADR 0596 §2)", module, r, from, from))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package judge
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// A manifest's bus state, asked for in `requires` by template, is judged once and each refusal names the module, the entry
|
||||
// and the field (issue 1966 D1, D2; novox/hq design 25 § 12.1, ADR 0595, ADR 0596 §2, ADR 0597 §2).
|
||||
func TestBusStateIsJudgedOnce(t *testing.T) {
|
||||
const ledger = `{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":512},"types":[` +
|
||||
`{"name":"decision","schema":{"title":{"type":"text"}},"operations":[{"name":"propose","effect":"set-fields","fields":["title"]}]}]}`
|
||||
manifest := func(entries string) string {
|
||||
return `{"module":"a","version":"1","requires":[` + entries + `]}`
|
||||
}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
raw string
|
||||
refused []string // words the first problem holds, in order: absent is accepted
|
||||
}{
|
||||
// 1966/AC8: a state, a log, objects and a ledger within bounds
|
||||
{"within bounds", manifest(`{"name":"cache","storage":"key-value","template":"state","options":{"history":8,"key-ttl":true}},` +
|
||||
`{"name":"audit","storage":"stream","template":"log","options":{"max-mib":64,"max-age-days":365}},` +
|
||||
`{"name":"files","storage":"object-store","template":"objects","options":{"max-mib":2048}},` + ledger), nil},
|
||||
// 1966/AC7: each refused case, naming the module, the entry and the field
|
||||
{"unknown storage type", manifest(`{"name":"x","storage":"tape"}`), []string{"a's bus state \"x\"", "storage type \"tape\""}},
|
||||
{"unknown template", manifest(`{"name":"x","storage":"stream","template":"pile"}`), []string{"\"x\"", "template \"pile\""}},
|
||||
{"template on another storage type", manifest(`{"name":"x","storage":"stream","template":"state"}`), []string{"\"x\"", "template state is on the storage type key-value"}},
|
||||
{"past values 0", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"history":0}}`), []string{"\"x\"", "options \"history\" is 0", "0 is refused"}},
|
||||
{"past values 65", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"history":65}}`), []string{"\"x\"", "\"history\" is 65", "1 to 64"}},
|
||||
{"a lifetime that is no number", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"ttl-seconds":"soon"}}`), []string{"\"x\"", "\"ttl-seconds\""}},
|
||||
{"events age over 30 days", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"max-age-days":31}}`), []string{"\"x\"", "\"max-age-days\" is 31", "1 to 30"}},
|
||||
{"events cap over 8192", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"max-mib":8193}}`), []string{"\"x\"", "\"max-mib\" is 8193"}},
|
||||
{"events naming none", manifest(`{"name":"x","storage":"stream","template":"events","options":{}}`), []string{"\"x\"", "\"events\" is missing"}},
|
||||
{"a grant that is no trusted value", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"grant":"nobody"}}`), []string{"\"x\"", "\"grant\"", "trusted config value"}},
|
||||
{"deliveries 11", manifest(`{"name":"x","storage":"stream","template":"work-queue","options":{"events":["job"],"max-deliveries":11}}`), []string{"\"x\"", "\"max-deliveries\" is 11", "1 to 10"}},
|
||||
{"duplicates past the age", manifest(`{"name":"x","storage":"stream","template":"work-queue","options":{"events":["job"],"max-age-days":1,"duplicates":86401}}`), []string{"\"x\"", "\"duplicates\"", "longer than the queue's age of 1 days"}},
|
||||
{"log age 3651", manifest(`{"name":"x","storage":"stream","template":"log","options":{"max-age-days":3651}}`), []string{"\"x\"", "\"max-age-days\" is 3651"}},
|
||||
{"objects without a cap", manifest(`{"name":"x","storage":"object-store","template":"objects"}`), []string{"\"x\"", "\"max-mib\" is missing"}},
|
||||
{"when full given as an option", manifest(`{"name":"x","storage":"stream","template":"log","options":{"when-full":"drop-oldest"}}`), []string{"\"x\"", "\"when-full\"", "fixed"}},
|
||||
{"another fixed setting given", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"max-value-kib":1024}}`), []string{"\"x\"", "\"max-value-kib\" is not a setting of the template state"}},
|
||||
{"a configuration beside a template", manifest(`{"name":"x","storage":"key-value","template":"state","configuration":{"max-mib":1}}`), []string{"\"x\"", "configuration"}},
|
||||
{"a name used twice across storage types", manifest(
|
||||
`{"name":"x","storage":"key-value","template":"state"},{"name":"x","storage":"stream","template":"log"}`), []string{"\"x\"", "used twice (as key-value and as stream)"}},
|
||||
{"a local name holding an underscore", manifest(`{"name":"my_x","storage":"key-value","template":"state"}`), []string{"\"my_x\"", "underscore"}},
|
||||
{"a bare stream without when full", manifest(`{"name":"x","storage":"stream","configuration":{"max-mib":10,"max-age-days":"none"}}`), []string{"\"x\"", "\"when-full\" is missing"}},
|
||||
{"a bare stream without a cap", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"refuse","max-age-days":3}}`), []string{"\"x\"", "\"max-mib\" is missing"}},
|
||||
{"a bare stream without an age", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"refuse","max-mib":10}}`), []string{"\"x\"", "\"max-age-days\" is missing"}},
|
||||
{"a bare stream, complete", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"drop-oldest","max-mib":10,"max-age-days":"none"}}`), nil},
|
||||
{"a bare stream with a wrong when full", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"crash","max-mib":10,"max-age-days":9}}`), []string{"\"x\"", "\"when-full\"", "\"refuse\" or \"drop-oldest\""}},
|
||||
{"a ledger effect outside the four", manifest(strings.Replace(ledger, `"set-fields"`, `"upsert"`, 1)), []string{"\"records\"", "effect \"upsert\"", "outside the four"}},
|
||||
{"a ledger whose history expires", manifest(strings.Replace(ledger, `"options"`, `"configuration":{"max-age-days":30},"options"`, 1)), nil},
|
||||
} {
|
||||
_, problems := Manifest([]byte(c.raw), nil)
|
||||
switch {
|
||||
case c.refused == nil && len(problems) > 0:
|
||||
t.Errorf("%s: refused: %v", c.name, problems)
|
||||
case c.refused != nil && len(problems) == 0:
|
||||
t.Errorf("%s: accepted", c.name)
|
||||
case c.refused != nil:
|
||||
text := strings.Join(problems, "\n")
|
||||
at := 0
|
||||
for _, word := range c.refused {
|
||||
i := strings.Index(text[at:], word)
|
||||
if i < 0 {
|
||||
t.Errorf("%s: problems %q lack %q", c.name, text, word)
|
||||
break
|
||||
}
|
||||
at += i
|
||||
}
|
||||
}
|
||||
}
|
||||
// Between the manifests: two modules' names on the bus alike, and a read of bus state that moved (ADR 0596 §2).
|
||||
entry := func(name string) []catalogue.BusStateEntry {
|
||||
return []catalogue.BusStateEntry{{Name: name, Storage: catalogue.StorageKeyValue, Template: catalogue.TemplateState}}
|
||||
}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
shelf catalogue.Shelf
|
||||
want string
|
||||
}{
|
||||
{"names alike", catalogue.Shelf{"a_b": {Module: "a_b", BusState: entry("c")}, "a": {Module: "a", BusState: entry("b_c")}}, "has one owner"},
|
||||
{"a read of moved state", catalogue.Shelf{"owner": {Module: "owner", BusState: entry("cache")},
|
||||
"reader": {Module: "reader", Reads: []string{"owner.cache"}}}, "reader reads \"owner.cache\", which owner has moved"},
|
||||
{"a read of state still kept", catalogue.Shelf{"owner": {Module: "owner", State: []catalogue.StateDeclaration{{Name: "cache"}}},
|
||||
"reader": {Module: "reader", Reads: []string{"owner.cache"}}}, ""},
|
||||
} {
|
||||
got := strings.Join(BusStateCatalogueProblems(c.shelf), "\n")
|
||||
if (c.want == "") != (got == "") || !strings.Contains(got, c.want) {
|
||||
t.Errorf("%s: between the manifests: %q, want %q", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2626,6 +2626,8 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M
|
||||
// And the object buckets it owns (novox/hq ADR 0592 §5, §9).
|
||||
problems = append(problems, ObjectsProblems(m)...)
|
||||
problems = append(problems, WorkQueueStreamProblems(m)...)
|
||||
// And the bus state it asks for in `requires`, by template (busstate.go, novox/hq ADR 0595; issue 1966 D1).
|
||||
problems = append(problems, BusStateProblems(m)...)
|
||||
// And the defaults it gives its settings (setting_defaults.go, novox/hq ADR 0262), or, written in the
|
||||
// config form, the whole declaration of each value (config.go, novox/hq ADR 0348).
|
||||
if m.DeclaresConfig() {
|
||||
@@ -4554,6 +4556,8 @@ func CatalogueProblems(shelf catalogue.Shelf) []string {
|
||||
problems = append(problems, StreamNameProblems(shelf)...)
|
||||
// An object bucket's name has one owner, and another module reads it only through its owner's grant (ADR 0592 §5, §9).
|
||||
problems = append(problems, ObjectsCatalogueProblems(shelf)...)
|
||||
// Bus state in `requires`: a name on the bus has one owner, and the new form has no readers (ADR 0595, ADR 0596 §2).
|
||||
problems = append(problems, BusStateCatalogueProblems(shelf)...)
|
||||
// A module that hears a restricted stream runs as an account of its own (novox/hq ADR 0376 §2).
|
||||
problems = append(problems, RestrictedHearingProblems(shelf)...)
|
||||
// A work-queue stream is never asked for by another module's consumes (novox/hq ADR 0436 §6).
|
||||
|
||||
@@ -54,12 +54,6 @@ func TestABinaryMustSayWhichSystemItIsFor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestABinaryThatNamesASystemIsAccepted(t *testing.T) {
|
||||
if got := buildProblemsOf(t, bundleFor("go", "arch")); got != "" {
|
||||
t.Fatalf("a compiled bundle naming a system was refused:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABlockOutsideTheShapeIsRefused(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
`{"seat":"node-bar","kind":"block","content":"[[block]]"}`: "has no data",
|
||||
@@ -2356,18 +2350,19 @@ func TestAPlaceholderOutsideTheHolderIsRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// **And a language that runs anywhere must not name one.** A system that decides nothing reads as
|
||||
// though it did, which is the same fault as a restriction that restricts nothing (novox/hq ADR 0045).
|
||||
func TestAPortableBundleMayNotNameASystem(t *testing.T) {
|
||||
got := buildProblemsOf(t, bundleFor("typescript", "arch"))
|
||||
if !strings.Contains(got, "runs anywhere") {
|
||||
t.Fatalf("a portable bundle was allowed to name a system:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPortableBundleNamingNoSystemIsAccepted(t *testing.T) {
|
||||
if got := buildProblemsOf(t, bundleFor("typescript", "")); got != "" {
|
||||
t.Fatalf("an ordinary bundle was refused:\n%s", got)
|
||||
// **A language that runs anywhere must not name a system.** A system that decides nothing reads as
|
||||
// though it did, which is the same fault as a restriction that restricts nothing (novox/hq ADR 0045); a compiled
|
||||
// bundle names one, and a portable bundle naming none is an ordinary one.
|
||||
func TestABundleNamesASystemOnlyWhereItCompiles(t *testing.T) {
|
||||
for _, c := range []struct{ language, system, refused string }{
|
||||
{"go", "arch", ""},
|
||||
{"typescript", "", ""},
|
||||
{"typescript", "arch", "runs anywhere"},
|
||||
} {
|
||||
got := buildProblemsOf(t, bundleFor(c.language, c.system))
|
||||
if (c.refused == "") != (got == "") || !strings.Contains(got, c.refused) {
|
||||
t.Errorf("a %s bundle naming %q: %q, want %q", c.language, c.system, got, c.refused)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user