Without a default, a running module could never gain a setting: the file asking for it failed to compose until set, and the key was refused as stray until a file asked for it. Defaults sit under the mesh's and the node's settings, never merge into a JSON file, are refused for the operator's own values, and settings shows each value's source.
196 lines
7.8 KiB
Go
196 lines
7.8 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A preference has a default in the definition; the mesh's layer, then the node's, override it; a key
|
|
// with a default is not stray; and a value that is the operator's still has none (novox/hq ADR 0262).
|
|
|
|
func notifier() Manifest {
|
|
return Manifest{Module: "notifier",
|
|
Settings: map[string]SettingDeclaration{
|
|
"font-size": {Kind: KindPreference, Default: float64(10), Why: "readable at a scale of one"},
|
|
"width": {Kind: KindPreference, Default: float64(250), Why: "fits a title of forty characters"},
|
|
},
|
|
Resources: []map[string]any{{"id": "configuration", "type": "file", "path": "/x/notifierrc",
|
|
"content": "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"}},
|
|
}
|
|
}
|
|
|
|
func parsed(t *testing.T, m Manifest) error {
|
|
t.Helper()
|
|
raw, err := json.Marshal(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = ParseManifest(raw)
|
|
return err
|
|
}
|
|
|
|
func TestAnUnsetPreferenceTakesItsDefault(t *testing.T) {
|
|
m := notifier()
|
|
for _, layers := range [][]Layer{nil, {{From: MeshWideLayer, Values: map[string]any{}}}} {
|
|
file := map[string]any{}
|
|
for k, v := range m.Resources[0] {
|
|
file[k] = v
|
|
}
|
|
if err := settingInto(file, WithDefaults(m, layers), m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if file["content"] != "font = Inter 10\nwidth = 250\n" {
|
|
t.Fatalf("filled as %q", file["content"])
|
|
}
|
|
}
|
|
if err := JudgeSettings(m, nil, false); err != nil {
|
|
t.Fatalf("a module whose every key has a default does not compose with no layer: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheNodeOverTheMeshOverTheDefault(t *testing.T) {
|
|
m := notifier()
|
|
layers := []Layer{
|
|
{From: MeshWideLayer, Values: map[string]any{"width": float64(300)}},
|
|
{From: "laptop", Values: map[string]any{"font-size": float64(13), "width": float64(340)}},
|
|
}
|
|
file := map[string]any{"type": "file", "content": m.Resources[0]["content"]}
|
|
if err := settingInto(file, WithDefaults(m, layers), m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if file["content"] != "font = Inter 13\nwidth = 340\n" {
|
|
t.Fatalf("filled as %q", file["content"])
|
|
}
|
|
file = map[string]any{"type": "file", "content": m.Resources[0]["content"]}
|
|
if err := settingInto(file, WithDefaults(m, layers[:1]), m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if file["content"] != "font = Inter 10\nwidth = 300\n" {
|
|
t.Fatalf("the mesh's layer over the default filled as %q", file["content"])
|
|
}
|
|
}
|
|
|
|
// Composed for a machine, the way a push writes it: the default reaches the file, and the node's
|
|
// layer overrides it.
|
|
func TestAComposedMachineGetsTheDefaultAndTheNodesValue(t *testing.T) {
|
|
r := anAdoptedAnchor()
|
|
r.Modules = append(r.Modules, notifier())
|
|
with := anchorRendering(false)
|
|
composed, err := r.Compose(with)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if why, left := composed.LeftOut["notifier"]; left {
|
|
t.Fatalf("the notifier was left out: %s", why)
|
|
}
|
|
if c := byID(composed.Resources)["notifier.configuration"]["content"]; c != "font = Inter 10\nwidth = 250\n" {
|
|
t.Fatalf("composed with no layer as %q", c)
|
|
}
|
|
with.Settings["notifier"] = []Layer{{From: "anchor", Values: map[string]any{"font-size": float64(13)}}}
|
|
if composed, err = r.Compose(with); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c := byID(composed.Resources)["notifier.configuration"]["content"]; c != "font = Inter 13\nwidth = 250\n" {
|
|
t.Fatalf("composed with the node's font size as %q", c)
|
|
}
|
|
}
|
|
|
|
// Setting a key the module gives a default is not refused as reaching nothing: it overrides the
|
|
// default, which is how a running module gains a setting with no gap between.
|
|
func TestAKeyWithADefaultIsNotStray(t *testing.T) {
|
|
m := notifier()
|
|
m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"
|
|
stray := UnusedSettings(m, []Layer{{From: "laptop", Values: map[string]any{"font-size": float64(13), "colour": "red"}}})
|
|
joined := strings.Join(stray, "; ")
|
|
if strings.Contains(joined, `"font-size"`) || !strings.Contains(joined, `"colour"`) {
|
|
t.Fatalf("stray: %s", joined)
|
|
}
|
|
}
|
|
|
|
// A default fills ${setting:…} only: it never becomes a key of a mergeable file (issue 168).
|
|
func TestADefaultIsNotMergedIntoAJSONFile(t *testing.T) {
|
|
m := notifier()
|
|
m.Resources = append(m.Resources, map[string]any{"id": "other", "type": "file", "path": "/x/other.json",
|
|
"merge": MergeJSON, "content": `{"keep": 1}`})
|
|
out, err := ApplySettings(m.Resources[1], WithDefaults(m, nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(out["content"].(string), "font-size") {
|
|
t.Fatalf("a default reached a mergeable file: %s", out["content"])
|
|
}
|
|
}
|
|
|
|
// A value that is the operator's has no default: no layer setting it is refused by name, as before.
|
|
func TestAnOperatorsValueWithoutADefaultIsStillRefused(t *testing.T) {
|
|
m := notifier()
|
|
m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\nfrom = ${setting:domain}\n"
|
|
err := JudgeSettings(m, nil, false)
|
|
if err == nil || !strings.Contains(err.Error(), "${setting:domain}") {
|
|
t.Fatalf("judged %v", err)
|
|
}
|
|
if strings.Contains(err.Error(), "font-size") {
|
|
t.Fatalf("a default was named as set today: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheParserTakesAPreferenceAndRefusesTheRest(t *testing.T) {
|
|
if err := parsed(t, notifier()); err != nil {
|
|
t.Fatalf("a preference with a default was refused: %v", err)
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
key string
|
|
d SettingDeclaration
|
|
refuse string
|
|
}{
|
|
{"no kind", "font-size", SettingDeclaration{Default: float64(10), Why: "x"}, `only a "preference" has a default`},
|
|
{"another kind", "font-size", SettingDeclaration{Kind: "operator", Default: float64(10), Why: "x"}, `only a "preference"`},
|
|
{"no default", "font-size", SettingDeclaration{Kind: KindPreference, Why: "x"}, "no default"},
|
|
{"an empty default", "font-size", SettingDeclaration{Kind: KindPreference, Default: " ", Why: "x"}, "an empty default"},
|
|
{"an object", "font-size", SettingDeclaration{Kind: KindPreference, Default: map[string]any{"a": 1.0}, Why: "x"}, "a string, a number or a boolean"},
|
|
{"no why", "font-size", SettingDeclaration{Kind: KindPreference, Default: float64(10)}, "no why"},
|
|
{"the operator's", "mail-domain", SettingDeclaration{Kind: KindPreference, Default: "example.tld", Why: "x"}, "is the operator's value"},
|
|
{"a secret", "api-token", SettingDeclaration{Kind: KindPreference, Default: "x", Why: "x"}, "is the operator's value"},
|
|
{"the mesh's word", PortsSetting, SettingDeclaration{Kind: KindPreference, Default: float64(1), Why: "x"}, "the mesh reads"},
|
|
{"read by nothing", "height", SettingDeclaration{Kind: KindPreference, Default: float64(300), Why: "x"}, "reaches nothing"},
|
|
} {
|
|
m := notifier()
|
|
m.Resources[0]["content"] = m.Resources[0]["content"].(string) + "x = ${setting:" + c.key + "}\n"
|
|
if c.name == "read by nothing" {
|
|
m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"
|
|
}
|
|
m.Settings[c.key] = c.d
|
|
err := parsed(t, m)
|
|
if err == nil || !strings.Contains(err.Error(), c.refuse) {
|
|
t.Errorf("%s: parsed %v, want %q", c.name, err, c.refuse)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEveryValueSaysWhereItCameFrom(t *testing.T) {
|
|
m := notifier()
|
|
m.Resources[0]["content"] = m.Resources[0]["content"].(string) + "x = ${setting:position}\n"
|
|
got := Effective(m, []Layer{
|
|
{From: MeshWideLayer, Values: map[string]any{"width": float64(300), "position": "top-right"}},
|
|
{From: "laptop", Values: map[string]any{"font-size": float64(13)}},
|
|
})
|
|
want := map[string]string{"font-size": "laptop", "position": MeshWideLayer, "width": MeshWideLayer}
|
|
if len(got) != 3 {
|
|
t.Fatalf("effective: %+v", got)
|
|
}
|
|
for _, s := range got {
|
|
if s.From != want[s.Key] {
|
|
t.Errorf("%s from %q, want %q", s.Key, s.From, want[s.Key])
|
|
}
|
|
}
|
|
if got[0].Key != "font-size" || got[0].Value != float64(13) || got[0].Default != float64(10) {
|
|
t.Fatalf("font-size: %+v", got[0])
|
|
}
|
|
only := Effective(m, nil)
|
|
if only[0].From != DefaultLayer || only[0].Value != float64(10) {
|
|
t.Fatalf("with no layer: %+v", only)
|
|
}
|
|
}
|