all repos

clerk @ 6fdb9097048e212574439fb0da84d0c94aa7e01b

missing tooling for ledger/hledger

clerk/journal/loader_test.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lexer & parser & ast..., 14 days ago
1
package journal
2
3
import (
4
	"os"
5
	"path/filepath"
6
	"testing"
7
)
8
9
func TestLoader_LoadBytes(t *testing.T) {
10
	t.Run("empty", func(t *testing.T) {
11
		l := NewLoader()
12
		pf, err := l.LoadBytes("empty.journal", []byte{})
13
		if err != nil {
14
			t.Fatalf("unexpected error: %v", err)
15
		}
16
		if pf.Ast == nil {
17
			t.Fatal("expected non-nil AST")
18
		}
19
	})
20
21
	t.Run("transaction", func(t *testing.T) {
22
		l := NewLoader()
23
		src := []byte("2024/01/01 groceries\n    expenses:food  $10\n    assets:checking\n")
24
		pf, err := l.LoadBytes("t.journal", src)
25
		if err != nil {
26
			t.Fatalf("unexpected error: %v", err)
27
		}
28
		if len(pf.Errors) > 0 {
29
			t.Fatalf("unexpected parse errors: %v", pf.Errors)
30
		}
31
		if len(pf.FileErrors) > 0 {
32
			t.Fatalf("unexpected file errors: %v", pf.FileErrors)
33
		}
34
	})
35
36
	t.Run("parse errors", func(t *testing.T) {
37
		l := NewLoader()
38
		src := []byte("@@@ garbage\n")
39
		pf, err := l.LoadBytes("bad.journal", src)
40
		if err != nil {
41
			t.Fatalf("unexpected error: %v", err)
42
		}
43
		if len(pf.Errors) == 0 {
44
			t.Fatal("expected parse errors")
45
		}
46
	})
47
48
	t.Run("include not found", func(t *testing.T) {
49
		l := NewLoader()
50
		src := []byte("include nonexistent.journal\n")
51
		pf, err := l.LoadBytes("parent.journal", src)
52
		if err != nil {
53
			t.Fatalf("unexpected error: %v", err)
54
		}
55
		if len(pf.FileErrors) == 0 {
56
			t.Fatal("expected file errors for missing include")
57
		}
58
		if len(pf.Errors) > 0 {
59
			t.Fatalf("expected no parse errors, got %d", len(pf.Errors))
60
		}
61
		for _, fe := range pf.FileErrors {
62
			if fe.Path == "" {
63
				t.Error("expected non-empty path in FileError")
64
			}
65
			if fe.Message == "" {
66
				t.Error("expected non-empty message in FileError")
67
			}
68
		}
69
	})
70
71
	t.Run("self include dedup", func(t *testing.T) {
72
		l := NewLoader()
73
		src := []byte("include self.journal\n2024/01/01 t\n  a  $1\n")
74
		pf, err := l.LoadBytes("self.journal", src)
75
		if err != nil {
76
			t.Fatalf("unexpected error: %v", err)
77
		}
78
		// self-include is deduped (file already stored before includes resolved)
79
		if len(pf.Includes) != 0 {
80
			t.Fatalf("expected 0 includes (deduped), got %d", len(pf.Includes))
81
		}
82
	})
83
84
	t.Run("dedup", func(t *testing.T) {
85
		l := NewLoader()
86
		src := []byte("2024/01/01 t\n  a  $1\n")
87
		pf1, err := l.LoadBytes("same.journal", src)
88
		if err != nil {
89
			t.Fatalf("unexpected error: %v", err)
90
		}
91
		pf2, err := l.LoadBytes("same.journal", src)
92
		if err != nil {
93
			t.Fatalf("unexpected error: %v", err)
94
		}
95
		if pf1 != pf2 {
96
			t.Fatal("expected same pointer for deduplicated load")
97
		}
98
		// different path, same content — should NOT dedup
99
		pf3, err := l.LoadBytes("other.journal", src)
100
		if err != nil {
101
			t.Fatalf("unexpected error: %v", err)
102
		}
103
		if pf1 == pf3 {
104
			t.Fatal("expected different pointers for different paths")
105
		}
106
	})
107
}
108
109
func TestLoader_Load(t *testing.T) {
110
	dir := t.TempDir()
111
	mainPath := filepath.Join(dir, "main.journal")
112
	if err := os.WriteFile(mainPath, []byte("2024/01/01 t\n  a  $1\n"), 0o644); err != nil {
113
		t.Fatalf("writing temp file: %v", err)
114
	}
115
116
	l := NewLoader()
117
	pf, err := l.Load(mainPath)
118
	if err != nil {
119
		t.Fatalf("unexpected error: %v", err)
120
	}
121
	if pf.Path != mainPath {
122
		t.Fatalf("expected path %q, got %q", mainPath, pf.Path)
123
	}
124
	if len(pf.Errors) > 0 {
125
		t.Fatalf("unexpected errors: %v", pf.Errors)
126
	}
127
128
	t.Run("reuse on repeated load", func(t *testing.T) {
129
		pf2, err := l.Load(mainPath)
130
		if err != nil {
131
			t.Fatalf("unexpected error: %v", err)
132
		}
133
		if pf != pf2 {
134
			t.Fatal("expected same pointer on repeated load")
135
		}
136
	})
137
138
	t.Run("file not found", func(t *testing.T) {
139
		_, err := l.Load(filepath.Join(dir, "nonexistent.journal"))
140
		if err == nil {
141
			t.Fatal("expected error for nonexistent file")
142
		}
143
	})
144
}
145
146
func TestLoader_Load_withInclude(t *testing.T) {
147
	dir := t.TempDir()
148
149
	child := []byte("account expenses:food\n")
150
	if err := os.WriteFile(filepath.Join(dir, "child.journal"), child, 0o644); err != nil {
151
		t.Fatalf("writing child: %v", err)
152
	}
153
154
	parent := []byte("include child.journal\n")
155
	if err := os.WriteFile(filepath.Join(dir, "parent.journal"), parent, 0o644); err != nil {
156
		t.Fatalf("writing parent: %v", err)
157
	}
158
159
	l := NewLoader()
160
	pf, err := l.Load(filepath.Join(dir, "parent.journal"))
161
	if err != nil {
162
		t.Fatalf("unexpected error: %v", err)
163
	}
164
	if len(pf.FileErrors) > 0 {
165
		t.Fatalf("unexpected file errors: %v", pf.FileErrors)
166
	}
167
	if len(pf.Includes) != 1 {
168
		t.Fatalf("expected 1 include, got %d", len(pf.Includes))
169
	}
170
	included := pf.Includes[0]
171
	if included.Path != filepath.Join(dir, "child.journal") {
172
		t.Fatalf("expected child path, got %q", included.Path)
173
	}
174
}
175
176
func TestLoader_Load_withGlobInclude(t *testing.T) {
177
	dir := t.TempDir()
178
	sub := filepath.Join(dir, "data")
179
	os.MkdirAll(sub, 0o755)
180
181
	for _, name := range []string{"a.journal", "b.journal", "c.journal"} {
182
		if err := os.WriteFile(filepath.Join(sub, name), []byte("account expenses:"+name[:1]+"\n"), 0o644); err != nil {
183
			t.Fatalf("writing %s: %v", name, err)
184
		}
185
	}
186
187
	parent := []byte("include data/*.journal\n")
188
	if err := os.WriteFile(filepath.Join(dir, "parent.journal"), parent, 0o644); err != nil {
189
		t.Fatalf("writing parent: %v", err)
190
	}
191
192
	l := NewLoader()
193
	pf, err := l.Load(filepath.Join(dir, "parent.journal"))
194
	if err != nil {
195
		t.Fatalf("unexpected error: %v", err)
196
	}
197
	if len(pf.FileErrors) > 0 {
198
		t.Fatalf("unexpected file errors: %v", pf.FileErrors)
199
	}
200
	if len(pf.Includes) != 3 {
201
		t.Fatalf("expected 3 includes, got %d", len(pf.Includes))
202
	}
203
}
204
205
func TestLoader_Load_includeNotFound(t *testing.T) {
206
	dir := t.TempDir()
207
208
	parent := []byte("include data/*.journal\n")
209
	if err := os.WriteFile(filepath.Join(dir, "parent.journal"), parent, 0o644); err != nil {
210
		t.Fatalf("writing parent: %v", err)
211
	}
212
213
	l := NewLoader()
214
	pf, err := l.Load(filepath.Join(dir, "parent.journal"))
215
	if err != nil {
216
		t.Fatalf("unexpected error: %v", err)
217
	}
218
	if len(pf.FileErrors) != 1 {
219
		t.Fatalf("expected 1 file error, got %d", len(pf.FileErrors))
220
	}
221
	if len(pf.Errors) != 0 {
222
		t.Fatalf("expected 0 parse errors, got %d", len(pf.Errors))
223
	}
224
}
225
226
func TestLoader_Load_cycleDedup(t *testing.T) {
227
	dir := t.TempDir()
228
229
	aPath := filepath.Join(dir, "a.journal")
230
	bPath := filepath.Join(dir, "b.journal")
231
232
	if err := os.WriteFile(aPath, []byte("include b.journal\n"), 0o644); err != nil {
233
		t.Fatalf("writing a: %v", err)
234
	}
235
	if err := os.WriteFile(bPath, []byte("include a.journal\n"), 0o644); err != nil {
236
		t.Fatalf("writing b: %v", err)
237
	}
238
239
	l := NewLoader()
240
	pf, err := l.Load(aPath)
241
	if err != nil {
242
		t.Fatalf("unexpected error: %v", err)
243
	}
244
	// circular A→B→A: A is deduped (already stored before includes resolved)
245
	if len(pf.Includes) != 1 {
246
		t.Fatalf("expected 1 include, got %d", len(pf.Includes))
247
	}
248
	if pf.Includes[0].Path != bPath {
249
		t.Fatalf("expected include path %q, got %q", bPath, pf.Includes[0].Path)
250
	}
251
	// B's include of A should resolve to the same pf (dedup)
252
	if pf.Includes[0].Includes[0] != pf {
253
		t.Fatal("expected circular dedup: B's include of A should point to original A")
254
	}
255
}
256
257
func TestLoader_Ordered(t *testing.T) {
258
	dir := t.TempDir()
259
260
	leaf := []byte("2024/01/01 t\n  a  $1\n")
261
	if err := os.WriteFile(filepath.Join(dir, "leaf.journal"), leaf, 0o644); err != nil {
262
		t.Fatalf("writing leaf: %v", err)
263
	}
264
265
	middle := []byte("include leaf.journal\n")
266
	if err := os.WriteFile(filepath.Join(dir, "middle.journal"), middle, 0o644); err != nil {
267
		t.Fatalf("writing middle: %v", err)
268
	}
269
270
	root := []byte("include middle.journal\n")
271
	if err := os.WriteFile(filepath.Join(dir, "root.journal"), root, 0o644); err != nil {
272
		t.Fatalf("writing root: %v", err)
273
	}
274
275
	l := NewLoader()
276
	pf, err := l.Load(filepath.Join(dir, "root.journal"))
277
	if err != nil {
278
		t.Fatalf("unexpected error: %v", err)
279
	}
280
	if len(pf.FileErrors) > 0 {
281
		t.Fatalf("unexpected file errors: %v", pf.FileErrors)
282
	}
283
284
	ordered := l.Ordered()
285
	names := make([]string, len(ordered))
286
	for i, f := range ordered {
287
		names[i] = filepath.Base(f.Path)
288
	}
289
290
	// leaf before middle before root — includes before includer
291
	if len(names) != 3 {
292
		t.Fatalf("expected 3 ordered files, got %d: %v", len(names), names)
293
	}
294
	if names[0] != "leaf.journal" {
295
		t.Fatalf("expected leaf first, got %q", names[0])
296
	}
297
	if names[1] != "middle.journal" {
298
		t.Fatalf("expected middle second, got %q", names[1])
299
	}
300
	if names[2] != "root.journal" {
301
		t.Fatalf("expected root last, got %q", names[2])
302
	}
303
}
304
305
func TestLoader_Load_fileErrorsSeparateFromParseErrors(t *testing.T) {
306
	l := NewLoader()
307
	src := []byte("@@@ garbage\ninclude nonexistent.journal\n")
308
	pf, err := l.LoadBytes("mixed.journal", src)
309
	if err != nil {
310
		t.Fatalf("unexpected error: %v", err)
311
	}
312
	if len(pf.Errors) == 0 {
313
		t.Fatal("expected parse errors")
314
	}
315
	if len(pf.FileErrors) == 0 {
316
		t.Fatal("expected file errors")
317
	}
318
}