1package config
2
3import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/bnema/vev/internal/domain"
13 "github.com/stretchr/testify/require"
14)
15
16func TestParseThemeAccent(t *testing.T) {
17 t.Parallel()
18
19 tests := []struct {
20 name string
21 input string
22 want domain.ThemeAccent
23 warnings []domain.Warning
24 }{
25 {
26 name: "auto",
27 input: "theme.accent = auto\n",
28 want: domain.ThemeAccent{Mode: domain.ThemeAccentAuto},
29 },
30 {
31 name: "slot zero is valid but warns",
32 input: "theme.accent = 0\n",
33 want: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 0},
34 warnings: []domain.Warning{
35 {Line: 1, Msg: "theme.accent slot 0 is conventionally neutral"},
36 },
37 },
38 {
39 name: "slot fifteen is valid but warns",
40 input: "theme.accent = 15\n",
41 want: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 15},
42 warnings: []domain.Warning{
43 {Line: 1, Msg: "theme.accent slot 15 is conventionally neutral"},
44 },
45 },
46 {
47 name: "chromatic slot",
48 input: "theme.accent = 2\n",
49 want: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2},
50 },
51 {
52 name: "negative slot falls back to auto",
53 input: "theme.accent = -1\n",
54 want: domain.ThemeAccent{Mode: domain.ThemeAccentAuto},
55 warnings: []domain.Warning{
56 {Line: 1, Msg: "invalid theme.accent \"-1\""},
57 },
58 },
59 {
60 name: "out of range slot falls back to auto",
61 input: "theme.accent = 16\n",
62 want: domain.ThemeAccent{Mode: domain.ThemeAccentAuto},
63 warnings: []domain.Warning{
64 {Line: 1, Msg: "invalid theme.accent \"16\""},
65 },
66 },
67 {
68 name: "non numeric value falls back to auto",
69 input: "theme.accent = teal\n",
70 want: domain.ThemeAccent{Mode: domain.ThemeAccentAuto},
71 warnings: []domain.Warning{
72 {Line: 1, Msg: "invalid theme.accent \"teal\""},
73 },
74 },
75 {
76 name: "non canonical slot falls back to auto",
77 input: "theme.accent = 02\n",
78 want: domain.ThemeAccent{Mode: domain.ThemeAccentAuto},
79 warnings: []domain.Warning{
80 {Line: 1, Msg: "invalid theme.accent \"02\""},
81 },
82 },
83 {
84 name: "case sensitive auto falls back to auto",
85 input: "theme.accent = AUTO\n",
86 want: domain.ThemeAccent{Mode: domain.ThemeAccentAuto},
87 warnings: []domain.Warning{
88 {Line: 1, Msg: "invalid theme.accent \"AUTO\""},
89 },
90 },
91 }
92
93 for _, tt := range tests {
94 t.Run(tt.name, func(t *testing.T) {
95 t.Parallel()
96
97 cfg, warnings, err := Parse(strings.NewReader(tt.input))
98 require.NoError(t, err)
99 require.Equal(t, tt.want, cfg.ThemeAccent)
100 require.Equal(t, tt.warnings, warnings)
101 })
102 }
103}
104
105func TestDefaultsThemeAccent(t *testing.T) {
106 t.Parallel()
107
108 require.Equal(t, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, domain.Defaults().ThemeAccent)
109}
110
111func TestWatchThemeAccentInvalidReloadFallsBackToAuto(t *testing.T) {
112 t.Parallel()
113
114 path := filepath.Join(t.TempDir(), "config")
115 require.NoError(t, os.WriteFile(path, []byte("theme.accent = 2\n"), 0o600))
116 initial, warnings, err := Load(path)
117 require.NoError(t, err)
118 require.Empty(t, warnings)
119 require.Equal(t, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}, initial.ThemeAccent)
120
121 clk := newWatchClockMock(t)
122 ctx, cancel := context.WithCancel(context.Background())
123 defer cancel()
124 type change struct {
125 cfg domain.Config
126 warnings []domain.Warning
127 }
128 changes := make(chan change, 1)
129 done := make(chan error, 1)
130 go func() {
131 done <- Watch(ctx, clk.clock, path, func(cfg domain.Config, warnings []domain.Warning) {
132 changes <- change{cfg: cfg, warnings: warnings}
133 })
134 }()
135
136 clk.waitForTimer()
137 require.NoError(t, os.WriteFile(path, []byte("theme.accent = invalid\n"), 0o600))
138 clk.fire()
139
140 select {
141 case got := <-changes:
142 require.Equal(t, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, got.cfg.ThemeAccent)
143 require.Equal(t, []domain.Warning{{Line: 1, Msg: "invalid theme.accent \"invalid\""}}, got.warnings)
144 case <-time.After(2 * time.Second):
145 t.Fatal("timed out waiting for config reload")
146 }
147
148 cancel()
149 select {
150 case err := <-done:
151 require.True(t, errors.Is(err, context.Canceled))
152 case <-time.After(2 * time.Second):
153 t.Fatal("timed out waiting for config watcher shutdown")
154 }
155}