v0.1.0 3b1b6ef

parse_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close