package main import ( "os" "path/filepath" "reflect" "strings" "testing" ) func module(t *testing.T, root, name, body string, files map[string]string) { t.Helper() dir := filepath.Join(root, "modules", name) if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil { t.Fatal(err) } for path, text := range files { full := filepath.Join(dir, filepath.FromSlash(path)) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte(text), 0o644); err != nil { t.Fatal(err) } } } // Each of the settings form's names is found where it stands: the declaration, a mergeable file's keys, a // placeholder in the manifest, and a placeholder in a template file the manifest names (novox/hq ADR 0348 §1). func TestEveryOldNameIsFound(t *testing.T) { root := t.TempDir() module(t, root, "declares", `{"module":"declares","settings":{"size":{"kind":"preference","default":1,"why":"w"}},`+ `"resources":[{"id":"f","type":"file","content":"${setting:size}"}]}`, nil) module(t, root, "takes", `{"module":"takes","resources":[{"id":"f","type":"file","merge":"json",`+ `"takes-settings":["a"],"content":"{}"}]}`, nil) module(t, root, "asks", `{"module":"asks","serves":{"x":{"d":"${setting:domain}"}}}`, nil) module(t, root, "templated", `{"module":"templated","config":{"size":{"type":"number","description":"d",`+ `"default":1}},"resources":[{"id":"f","type":"file","template":"files/conf"}]}`, map[string]string{"files/conf": "size = ${setting:size}\n"}) module(t, root, "moved", `{"module":"moved","config":{"size":{"type":"number","description":"d","default":1}},`+ `"resources":[{"id":"f","type":"file","template":"files/conf"},{"id":"g","type":"file","merge":"json",`+ `"takes-config":[],"content":"{\"settings\": 1}"}]}`, map[string]string{"files/conf": "size = ${config:size}\n# change the settings, never this file\n"}) module(t, root, "none", `{"module":"none","resources":[{"id":"f","type":"file","content":"no settings here"}]}`, nil) got, err := Check(root) if err != nil { t.Fatal(err) } want := map[string][]string{ "asks": {"${setting:…}"}, "declares": {"${setting:…}", "settings"}, "takes": {"takes-settings"}, "templated": {"${setting:…} in files/conf"}, } if !reflect.DeepEqual(got, want) { t.Fatalf("found %v, want %v", got, want) } } // A module not on the list says no old name; one on the list that says none is taken off it; a line the trunk's // list does not hold is refused, so the list only shrinks until it is empty (novox/hq ADR 0348 §11). func TestTheListOfModulesNotYetMovedOnlyShrinks(t *testing.T) { found := map[string][]string{"old": {"settings"}, "forgotten": {"takes-settings"}} unmoved := map[string]bool{"old": true, "done": true, "new": true} base := map[string]bool{"old": true, "done": true, "forgotten": true} refused, stale, added := Judge(found, unmoved, base) if !reflect.DeepEqual(refused, []string{"forgotten"}) || !reflect.DeepEqual(stale, []string{"done", "new"}) || !reflect.DeepEqual(added, []string{"new"}) { t.Fatalf("refused %v, stale %v, added %v", refused, stale, added) } } func TestTheListIsReadWithoutItsComments(t *testing.T) { path := filepath.Join(t.TempDir(), "unmoved") if err := os.WriteFile(path, []byte("# why\n\nmailu\n searxng \n"), 0o644); err != nil { t.Fatal(err) } got, err := Unmoved(path) if err != nil || !reflect.DeepEqual(got, map[string]bool{"mailu": true, "searxng": true}) { t.Fatalf("%v %v", got, err) } } // The catalogue itself: every moved module says config alone, and the list names exactly the modules that // still say settings. func TestTheCatalogueSaysConfigInEveryMovedModule(t *testing.T) { found, err := Check(filepath.Join("..", "..")) if err != nil { t.Fatal(err) } unmoved, err := Unmoved("unmoved") if err != nil { t.Fatal(err) } refused, stale, added := Judge(found, unmoved, seed) for _, m := range refused { t.Errorf("%s is not on checks/config/unmoved and says %s", m, strings.Join(found[m], ", ")) } for _, m := range stale { t.Errorf("checks/config/unmoved names %s, which says no old name", m) } for _, m := range added { t.Errorf("checks/config/unmoved gains %s, which its first copy did not hold", m) } } // A mergeable file that names no keys takes every key of its module's layer (novox/hq ADR 0326 §3): the settings // form's last shape, which declares none of what it takes, so mesh-config could set none of it (ADR 0348 §10). It // is found like an old name, in a module that says no other (issue 735); one naming its keys, an empty list // included, is not. func TestAMergeableFileTakingTheWholeLayerIsFound(t *testing.T) { root := t.TempDir() module(t, root, "whole", `{"module":"whole","resources":[{"id":"runtime-config","type":"file","merge":"json",`+ `"template":"files/config.json"}]}`, map[string]string{"files/config.json": "{}\n"}) module(t, root, "names", `{"module":"names","config":{"url":{"type":"string","description":"d","default":""}},`+ `"resources":[{"id":"runtime-config","type":"file","merge":"json","takes-config":["url"],"content":"{}"}]}`, nil) module(t, root, "none", `{"module":"none","resources":[{"id":"c","type":"file","merge":"json",`+ `"takes-config":[],"content":"{}"}]}`, nil) module(t, root, "plain", `{"module":"plain","resources":[{"id":"c","type":"file","content":"{}"}]}`, nil) got, err := Check(root) if err != nil { t.Fatal(err) } want := map[string][]string{"whole": {"runtime-config taking its whole layer"}} if !reflect.DeepEqual(got, want) { t.Fatalf("found %v, want %v", got, want) } }