package main import ( "os" "path/filepath" "reflect" "strings" "testing" ) // The running dunst reads its file again when the file changes (novox/hq ADR 0262: a setting the // operator changes shows at the next notification, not at the next login). The node-engine reloads // the account's dunst.service, whose ExecReload is `dunstctl reload`; dunst_reload does the same now. // writeConfig puts a dunstrc and drop-ins under a fake home's ~/.config/dunst. func writeConfig(t *testing.T, home string, files map[string]string) { t.Helper() for name, body := range files { path := filepath.Join(home, ".config", "dunst", name) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } } } func TestTheGlobalsAreTheLastFileThatSetsThem(t *testing.T) { home := t.TempDir() writeConfig(t, home, map[string]string{ "dunstrc": "[global]\n width = 500\n height = (0, 300)\n font = Inter 13\n" + "[urgency_low]\n font = Mono 99\n", // A rule's font is the rule's, not the global one; a later [global] in a drop-in outranks dunstrc. "dunstrc.d/10-slack.conf": "[slack]\n appname = Slack\n font = \"Comic 40\"\n", "dunstrc.d/90-big.conf": "# a comment\n[global]\n font = \"Inter 16\"\n", }) got := globalsOf(configFiles(filepath.Join(home, ".config", "dunst"))) if got.Font != "Inter 16" || got.FontSize != 16 || got.Width != "500" { t.Fatalf("%+v", got) } if !strings.HasSuffix(got.FontFrom, "dunstrc.d/90-big.conf") || !strings.HasSuffix(got.WidthFrom, "/dunstrc") { t.Fatalf("from: %+v", got) } } func TestReloadAsksTheRunningDunstToReadItsFilesAndSaysWhatTheySet(t *testing.T) { root := withBus(t) writeConfig(t, filepath.Join(root, "home"), map[string]string{ "dunstrc": "[global]\n width = 500\n font = Inter 13\n", }) bin := fakeBinaries(t, map[string]string{"dunstctl": `echo "$*" >> "$LOG" case "$*" in count*) echo 0 ;; is-paused) echo false ;; esac`}) t.Setenv("LOG", filepath.Join(bin, "log")) got, err := Reload() if err != nil { t.Fatal(err) } if !got.Reloaded || got.Font != "Inter 13" || got.FontSize != 13 || got.Width != "500" || len(got.Files) != 1 { t.Fatalf("%+v", got) } if !strings.Contains(got.Note, "does not answer") { t.Fatalf("the note does not say where the values come from: %q", got.Note) } log, _ := os.ReadFile(filepath.Join(bin, "log")) asked := strings.Split(strings.TrimSpace(string(log)), "\n") // reload first, with no files named (dunst reads its own search path, drop-ins included), and then // asked something to show it still answers. if asked[0] != "reload" || len(asked) < 2 { t.Fatalf("dunstctl was asked: %q", asked) } } func TestAReloadDunstRefusesIsAFailure(t *testing.T) { withBus(t) fakeBinaries(t, map[string]string{"dunstctl": `echo "Failed to communicate with dunst" >&2; exit 1`}) if _, err := Reload(); err == nil || !strings.Contains(err.Error(), "Failed to communicate") { t.Fatalf("%v", err) } } func TestTheRunningDunstIsReloadedWhenItsFileChanges(t *testing.T) { m := readManifest(t) s := m.resource(t, "notifier") want := map[string]any{ "id": "notifier", "type": "service", "unit": "dunst.service", "scope": "user", "user": "${machine:account}", "reload-on": []any{"configuration"}, } if !reflect.DeepEqual(s, want) { t.Fatalf("notifier: %v\nwant %v", s, want) } }