Fill a preference's ${setting:} from its manifest default (hq ADR 0262)
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.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user