package catalogue import ( "fmt" "os" "path/filepath" "reflect" "strings" "testing" ) // The session environment is a collection of typed items (design 57 L3, issue 795): a variable is an item with // kind=variable and name=, a PATH entry one with kind=path and at=start|end, each a template whose text is the // value or the entry. Where node-env is not handed the controller renders the items as the old form was rendered; // where it is, it names the collection for the client, which fills the same files (mesh-catalog's provisioner client // holds that half against the same golden files). testdata/environment holds the environment of the catalogue's five // contributors as the old form composed it, with all five and with zsh alone. type envDecl struct{ module, item, name, value string } func environmentOfTheCatalogue() []envDecl { return []envDecl{ {"adwaita", "gtk-theme", "GTK_THEME", "Adwaita:dark"}, {"adwaita", "gtk2-rc-files", "GTK2_RC_FILES", "/usr/share/themes/Adwaita-dark/gtk-2.0/gtkrc"}, {"adwaita", "qt-qpa-platformtheme", "QT_QPA_PLATFORMTHEME", "qt6ct"}, {"adwaita", "qt-style-override", "QT_STYLE_OVERRIDE", "Fusion"}, {"adwaita", "qt-select", "QT_SELECT", "6"}, {"adwaita", "xcursor-theme", "XCURSOR_THEME", "Adwaita"}, {"adwaita", "xcursor-size", "XCURSOR_SIZE", "24"}, {"clipmenu", "cm-selections", "CM_SELECTIONS", "clipboard"}, {"clipmenu", "cm-max-clips", "CM_MAX_CLIPS", "500"}, {"clipmenu", "cm-histlength", "CM_HISTLENGTH", "15"}, {"i3", "xdg-current-desktop", "XDG_CURRENT_DESKTOP", "i3"}, {"i3", "xdg-session-desktop", "XDG_SESSION_DESKTOP", "i3"}, {"xterm", "terminal", "TERMINAL", "xterm"}, {"zsh", "editor", "EDITOR", "vim"}, {"zsh", "visual", "VISUAL", "vim"}, {"zsh", "xdg-config-home", "XDG_CONFIG_HOME", "${machine:account-home}/.config"}, {"zsh", "path-local-bin", "path:10", "${machine:account-home}/.local/bin"}, {"zsh", "path-scripts", "path:20", "${machine:account-home}/scripts"}, {"zsh", "path-scripts-bin", "path:30", "${machine:account-home}/scripts/bin"}, } } const environmentCollector = `{"module":"node-env","version":"1","moved":true, "claims":[{"name":"node-environment","scope":"node"}], "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[{"id":"posix","type":"file","path":"/home/op/.config/mesh/environment.sh","template":"files/posix"}, {"id":"systemd","type":"file","path":"/home/op/.config/environment.d/50-mesh.conf","template":"files/systemd"}]}` // nodeEnvWith is node-env, its file placing the environment as plan says, with the declared contributors' items. func nodeEnvWith(t *testing.T, decls []envDecl, plan string) Resolution { t.Helper() collector := held(t, environmentCollector, builtWith("node-env", map[string]string{ "posix": "# header\n" + plan, "systemd": "${environment:systemd}"})) mods := []Manifest{collector} order := []string{"node-env"} byModule := map[string][]envDecl{} var modules []string for _, d := range decls { if byModule[d.module] == nil { modules = append(modules, d.module) } byModule[d.module] = append(byModule[d.module], d) } for _, name := range modules { var items []string texts := map[string]string{} for _, d := range byModule[name] { attrs := fmt.Sprintf(`{"kind":"variable","name":%q}`, d.name) if order, isPath := strings.CutPrefix(d.name, "path:"); isPath { attrs = fmt.Sprintf(`{"kind":"path","at":"start","order":%q}`, order) } items = append(items, fmt.Sprintf(`{"collection":"environment","name":%q,"attributes":%s,"template":"files/env-%s"}`, d.item, attrs, d.item)) texts["env-"+d.item] = d.value + "\n" } raw := fmt.Sprintf(`{"module":%q,"version":"1","moved":true,"items":[%s], "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`, name, strings.Join(items, ",")) mods = append(mods, held(t, raw, builtWith(name, texts))) order = append(order, name) } r, err := Resolve(shelf(mods...), order, workstation(), World{}) if err != nil { t.Fatal(err) } r.Account = "op" return r } func TestTheEnvironmentOfTheCatalogueIsTheSameHandedOrNot(t *testing.T) { read := func(name string) string { b, err := os.ReadFile(filepath.Join("testdata", "environment", name)) if err != nil { t.Fatal(err) } return string(b) } for name, only := range map[string]string{"all": "", "zsh": "zsh"} { var decls []envDecl for _, d := range environmentOfTheCatalogue() { if only == "" || d.module == only { decls = append(decls, d) } } // Named as the collector names it, and as the old placeholder does. for _, plan := range []string{"${collected:environment:posix}", "${environment:posix}"} { r := nodeEnvWith(t, decls, plan) // Not handed: the controller renders each moved contributor's items as the old form was rendered. if got, want := composedFile(t, r, "node-env.posix"), "# header\n"+read(name+".posix"); got != want { t.Errorf("%s, %s, not handed, the POSIX file is\n%s\nwant\n%s", name, plan, got, want) } if got, want := composedFile(t, r, "node-env.systemd"), read(name+".systemd"); got != want { t.Errorf("%s, %s, not handed, the environment.d file is\n%s\nwant\n%s", name, plan, got, want) } // Handed: one collection, named for the client; each contributor's plan names its items. c, _ := composeWith(t, r, handingTo("store:5000")) if got := c.Plans["node-env"].Collections; !reflect.DeepEqual(got, []PlanCollection{{Name: "environment", Comment: "#"}}) { t.Errorf("%s: the plan collects %+v", name, got) } items, got := len(decls), 0 for m := range c.Plans { got += len(c.Plans[m].Items) } if got != items { t.Errorf("%s: the plans name %d items, want %d", name, got, items) } } } } func TestAnEnvironmentItemIsTypedAndNamesWhatItNeeds(t *testing.T) { item := func(attrs map[string]string) []string { return environmentItemProblems("x", Item{Collection: "environment", Attributes: attrs}) } for _, ok := range []map[string]string{ {"kind": "variable", "name": "GTK_THEME"}, {"kind": "path", "at": "end", "order": "10"}, } { if p := item(ok); len(p) != 0 { t.Errorf("%v is refused: %v", ok, p) } } for _, bad := range []map[string]string{ nil, {"kind": "other"}, {"kind": "variable"}, {"kind": "variable", "name": "PATH"}, {"kind": "path"}, {"kind": "path", "at": "middle"}, {"kind": "path", "at": "start", "name": "X"}, {"kind": "variable", "name": "A", "color": "red"}, } { if p := item(bad); len(p) == 0 { t.Errorf("%v is taken", bad) } } if !attributeFits("environment", "name", "GTK_THEME") || attributeFits("shell:zsh:normal", "name", "GTK_THEME") || attributeFits("environment", "kind", "Variable") { t.Error("an upper-case name is the environment's name alone") } // A module that sets a variable twice, in two modules, is refused as the old form refused it. a := Manifest{Module: "a", Items: []Item{{Collection: "environment", Content: "1\n", Attributes: map[string]string{"kind": "variable", "name": "X"}}}} b := Manifest{Module: "b", Items: []Item{{Collection: "environment", Content: "2\n", Attributes: map[string]string{"kind": "variable", "name": "X"}}}} if err := variablesSetOnce([]Manifest{a, b}); err == nil || !strings.Contains(err.Error(), "both set X") { t.Errorf("two modules setting X: %v", err) } }