1package kvconf
2
3import (
4 "errors"
5 "reflect"
6 "strings"
7 "testing"
8)
9
10func TestParse(t *testing.T) {
11 tests := []struct {
12 name string
13 in string
14 opts Options
15 want []Entry
16 warnings []string
17 err string
18 }{
19 {name: "empty", in: "", want: []Entry{}},
20 {
21 name: "basic",
22 in: "# comment\n\n a = 1\nb=two words \n",
23 want: []Entry{{"a", "1", 3}, {"b", "two words", 4}},
24 },
25 {
26 name: "CRLF and BOM",
27 in: "\ufeffa = 1\r\nb = 2\r\n",
28 want: []Entry{{"a", "1", 1}, {"b", "2", 2}},
29 },
30 {
31 name: "empty value",
32 in: "a =\n",
33 want: []Entry{{"a", "", 1}},
34 },
35 {
36 name: "value with equals",
37 in: "cmd = env A=1 foo\n",
38 want: []Entry{{"cmd", "env A=1 foo", 1}},
39 },
40 {
41 name: "spaced comments keep colors",
42 in: "color = #ff8800 # orange\nx = a#b\ny = a #b\n",
43 want: []Entry{{"color", "#ff8800", 1}, {"x", "a#b", 2}, {"y", "a", 3}},
44 },
45 {
46 name: "tab before comment",
47 in: "a = 1\t# note\n",
48 want: []Entry{{"a", "1", 1}},
49 },
50 {
51 name: "line comments only",
52 in: "a = 1 # kept\n# skipped = 1\n",
53 opts: Options{Comments: CommentLine},
54 want: []Entry{{"a", "1 # kept", 1}},
55 },
56 {
57 name: "any hash is a comment",
58 in: "a = x#y\n",
59 opts: Options{Comments: CommentAny},
60 want: []Entry{{"a", "x", 1}},
61 },
62 {
63 name: "quotes",
64 in: `a = "x # y" # c
65b = 'it''s'
66c = 'lit\n'
67d = "tab\t"
68e = "a" "b"
69f = "unterminated
70g = spawn "foo bar"
71`,
72 want: []Entry{
73 {"a", "x # y", 1},
74 {"b", "'it''s'", 2},
75 {"c", `lit\n`, 3},
76 {"d", "tab\t", 4},
77 {"e", `"a" "b"`, 5},
78 {"f", `"unterminated`, 6},
79 {"g", `spawn "foo bar"`, 7},
80 },
81 },
82 {
83 name: "apostrophe inside a word keeps the comment",
84 in: "title = it's fine # a note\nb = say \"x # y\" # c\n",
85 want: []Entry{{"title", "it's fine", 1}, {"b", `say "x # y"`, 2}},
86 },
87 {
88 name: "raw values",
89 in: `a = "x"` + "\n",
90 opts: Options{RawValues: true},
91 want: []Entry{{"a", `"x"`, 1}},
92 },
93 {
94 name: "duplicate warns, both kept",
95 in: "a = 1\na = 2\n",
96 want: []Entry{{"a", "1", 1}, {"a", "2", 2}},
97 warnings: []string{"line 2: a: overrides line 1"},
98 },
99 {
100 name: "duplicates allowed",
101 in: "a = 1\na = 2\n",
102 opts: Options{AllowDuplicates: true},
103 want: []Entry{{"a", "1", 1}, {"a", "2", 2}},
104 },
105 {
106 name: "bad lines are skipped",
107 in: "nope\n= 1\nok = 1\n",
108 want: []Entry{{"ok", "1", 3}},
109 warnings: []string{"line 1: expected key = value", "line 2: missing key"},
110 },
111 {
112 name: "key with spaces",
113 in: "layout.LG Electronics 27GR95UM.overflow = scroll\n",
114 want: []Entry{{"layout.LG Electronics 27GR95UM.overflow", "scroll", 1}},
115 },
116 {
117 name: "strict stops at first problem",
118 in: "ok = 1\nnope\nbad\n",
119 opts: Options{Strict: true},
120 warnings: []string{"line 2: expected key = value"},
121 err: "line 2: expected key = value",
122 },
123 {
124 name: "strict duplicate",
125 in: "a = 1\na = 2\n",
126 opts: Options{Strict: true},
127 err: "line 2: a: overrides line 1",
128 warnings: []string{"line 2: a: overrides line 1"},
129 },
130 {
131 name: "long line",
132 in: "a = " + strings.Repeat("x", 20) + "\nb = 1\n",
133 opts: Options{MaxLine: 10},
134 want: []Entry{{"b", "1", 2}},
135 warnings: []string{"line 1: line longer than 10 bytes"},
136 },
137 {
138 name: "too large",
139 in: "a = 1234567890\n",
140 opts: Options{MaxBytes: 8},
141 err: ErrTooLarge.Error(),
142 },
143 {
144 name: "no size limit",
145 in: "a = 1234567890\n",
146 opts: Options{MaxBytes: -1, MaxLine: -1},
147 want: []Entry{{"a", "1234567890", 1}},
148 },
149 {
150 name: "invalid UTF-8",
151 in: "a = \xff\n",
152 err: ErrNotUTF8.Error(),
153 },
154 }
155 for _, tt := range tests {
156 t.Run(tt.name, func(t *testing.T) {
157 got, warnings, err := Parse(strings.NewReader(tt.in), tt.opts)
158 if tt.err != "" {
159 if err == nil || err.Error() != tt.err {
160 t.Fatalf("err = %v, want %q", err, tt.err)
161 }
162 } else if err != nil {
163 t.Fatalf("err = %v", err)
164 } else if !reflect.DeepEqual(got, tt.want) {
165 t.Errorf("entries = %#v\nwant %#v", got, tt.want)
166 }
167 if msgs := warningStrings(warnings); !reflect.DeepEqual(msgs, tt.warnings) {
168 t.Errorf("warnings = %q, want %q", msgs, tt.warnings)
169 }
170 })
171 }
172}
173
174func warningStrings(ws []Warning) []string {
175 var out []string
176 for _, w := range ws {
177 out = append(out, w.Error())
178 }
179 return out
180}
181
182func TestStrictErrorIsWarning(t *testing.T) {
183 _, _, err := Parse(strings.NewReader("bad\n"), Options{Strict: true})
184 var w Warning
185 if !errors.As(err, &w) || w.Line != 1 {
186 t.Fatalf("err = %#v, want a Warning on line 1", err)
187 }
188}
189
190func TestChanged(t *testing.T) {
191 tests := []struct {
192 name string
193 old, cur map[string]string
194 want []string
195 }{
196 {name: "same", old: map[string]string{"a": "1"}, cur: map[string]string{"a": "1"}},
197 {name: "added", old: map[string]string{}, cur: map[string]string{"b": "1", "a": "1"}, want: []string{"a", "b"}},
198 {name: "removed", old: map[string]string{"a": "1"}, cur: nil, want: []string{"a"}},
199 {name: "changed", old: map[string]string{"a": "1", "b": "1"}, cur: map[string]string{"a": "2", "b": "1"}, want: []string{"a"}},
200 }
201 for _, tt := range tests {
202 t.Run(tt.name, func(t *testing.T) {
203 if got := Changed(tt.old, tt.cur); !reflect.DeepEqual(got, tt.want) {
204 t.Errorf("Changed = %q, want %q", got, tt.want)
205 }
206 })
207 }
208}
209
210func FuzzParse(f *testing.F) {
211 for _, s := range []string{"a = 1\n", "a = \"x # y\" # c\n", "\ufeffa=b\r\n", "x\n=\n'"} {
212 f.Add(s)
213 }
214 f.Fuzz(func(t *testing.T, in string) {
215 entries, _, err := Parse(strings.NewReader(in), Options{})
216 if err != nil {
217 return
218 }
219 // Whatever was read can be written and read back the same.
220 var data []byte
221 for _, e := range entries {
222 data = append(data, e.Key+" = "+Quote(e.Value)+"\n"...)
223 }
224 again, _, err := ParseBytes(data, Options{AllowDuplicates: true})
225 if err != nil {
226 t.Fatalf("re-parse: %v\n%s", err, data)
227 }
228 if !sameEntries(entries, again) {
229 t.Fatalf("round trip:\n%#v\n%#v", entries, again)
230 }
231 })
232}