all repos

clerk @ 1f3a17349e37c8189adb11b8d956acbf3b2080d1

missing tooling for ledger/hledger

clerk/journal/loader_test.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
improve loader, 1 day ago
1
package journal
2
3
import (
4
	"fmt"
5
	"os"
6
	"path/filepath"
7
	"testing"
8
9
	"olexsmir.xyz/clerk/internal/testutil/txtar"
10
	"olexsmir.xyz/clerk/journal/ast"
11
)
12
13
func TestLoader_Resolve_basic(t *testing.T) {
14
	rj := resolveTxtar(t, "root.journal", `
15
-- root.journal --
16
2024/01/01 t
17
  a  $1
18
`)
19
	if len(rj.Occurrences) != 1 || len(rj.Items) != 1 || rj.Items[0].IsInclude {
20
		t.Fatal("basic: expected 1 file, 1 non-include item")
21
	}
22
}
23
24
func TestLoader_Resolve_withInclude(t *testing.T) {
25
	rj := resolveTxtar(t, "parent.journal", `
26
-- parent.journal --
27
include child.journal
28
2024/01/01 t
29
  a  $1
30
31
-- child.journal --
32
account expenses:food
33
2024/06/15 lunch
34
  expenses:food  $5
35
  assets:cash
36
`)
37
	// 2 files: parent (include + tx), child (directive + tx) = 4 items
38
	if len(rj.Items) != 4 {
39
		t.Fatalf("expected 4 items, got %d", len(rj.Items))
40
	}
41
	if !rj.Items[0].IsInclude {
42
		t.Fatal("items[0] should be include marker")
43
	}
44
	if len(rj.Occurrences) != 2 {
45
		t.Fatal("expected 2 occurrences")
46
	}
47
}
48
49
func TestLoader_Resolve_yearPropagation(t *testing.T) {
50
	rj := resolveTxtar(t, "parent.journal", `
51
-- parent.journal --
52
year 2025
53
include child.journal
54
12-01 dinner
55
  expenses:food  $10
56
  assets:cash
57
58
-- child.journal --
59
06-15 lunch
60
  expenses:food  $5
61
  assets:cash
62
`)
63
	dates := make([]string, 0, 2)
64
	for _, item := range rj.Items {
65
		if item.IsInclude {
66
			continue
67
		}
68
		tx, ok := item.Occurrence.Ast.Entries[item.EntryIndex].(*ast.Transaction)
69
		if !ok {
70
			continue
71
		}
72
		dates = append(dates, fmt.Sprintf("%d-%02d-%02d", tx.Date.Year, tx.Date.Month, tx.Date.Day))
73
	}
74
	if len(dates) != 2 || dates[0] != "2025-06-15" || dates[1] != "2025-12-01" {
75
		t.Fatalf("expected [2025-06-15 2025-12-01], got %v", dates)
76
	}
77
}
78
79
func TestLoader_Resolve_cycleDetection(t *testing.T) {
80
	rj := resolveTxtar(t, "a.journal", `
81
-- a.journal --
82
include b.journal
83
84
-- b.journal --
85
include a.journal
86
`)
87
	if len(rj.FileErrors()) == 0 {
88
		t.Fatal("expected cycle error")
89
	}
90
}
91
92
func TestLoader_Resolve_repeatedInclude(t *testing.T) {
93
	rj := resolveTxtar(t, "parent.journal", `
94
-- parent.journal --
95
include shared.journal
96
include shared.journal
97
98
-- shared.journal --
99
account expenses:shared
100
`)
101
	if len(rj.Occurrences) != 3 {
102
		t.Fatalf("expected 3 occurrences, got %d", len(rj.Occurrences))
103
	}
104
	if len(rj.ByPath["shared.journal"]) != 2 {
105
		t.Fatalf("expected 2 ByPath entries for shared.journal, got %d",
106
			len(rj.ByPath["shared.journal"]))
107
	}
108
}
109
110
func TestLoader_ResolveBytes_parseErrors(t *testing.T) {
111
	rj := NewLoader().ResolveBytes("bad.journal", []byte("@@@ garbage\n"))
112
	if len(rj.Occurrences[0].Errors) == 0 {
113
		t.Fatal("expected parse errors for garbage input")
114
	}
115
}
116
117
func TestLoader_ContentCache(t *testing.T) {
118
	dir := t.TempDir()
119
	fpath := filepath.Join(dir, "a.journal")
120
121
	os.WriteFile(fpath, []byte("2024/01/15 t\n  a  $1\n"), 0o644)
122
	l := NewLoader()
123
124
	// first resolve populates cache.
125
	rj1 := mustResolve(t, l, fpath)
126
	date1 := txDate(rj1)
127
128
	// modify file, resolve again - should return cached date.
129
	os.WriteFile(fpath, []byte("2024/06/01 t\n  a  $1\n"), 0o644)
130
	rj2 := mustResolve(t, l, fpath)
131
	if txDate(rj2) != date1 {
132
		t.Fatal("expected cached date before invalidation")
133
	}
134
135
	// invalidate and resolve again - should see new date.
136
	l.InvalidateFile(fpath)
137
	rj3 := mustResolve(t, l, fpath)
138
	if txDate(rj3) != "2024-06-01" {
139
		t.Fatalf("expected 2024-06-01 after invalidation, got %s", txDate(rj3))
140
	}
141
}
142
143
func TestLoader_ContentCache_LineEndings(t *testing.T) {
144
	dir := t.TempDir()
145
	fpath := filepath.Join(dir, "crlf.journal")
146
	os.WriteFile(fpath, []byte("2024/01/01 t\r\n  a  $1\r\n"), 0o644)
147
148
	rj, err := NewLoader().Resolve(fpath)
149
	if err != nil || len(rj.Occurrences[0].Errors) > 0 {
150
		t.Fatal("CRLF file should parse without errors")
151
	}
152
}
153
154
func TestLoader_ClearCache(t *testing.T) {
155
	l := NewLoader()
156
	if len(l.contentCache) != 0 {
157
		t.Fatal("expected empty cache")
158
	}
159
	l.contentCache["x"] = []byte("y")
160
	l.ClearCache()
161
	if len(l.contentCache) != 0 {
162
		t.Fatal("expected empty cache after clear")
163
	}
164
}
165
166
// helpers
167
168
func resolveTxtar(t *testing.T, rootFile, archive string) *ResolvedJournal {
169
	t.Helper()
170
	a := txtar.Parse([]byte(archive))
171
	fsys, err := txtar.FS(a)
172
	if err != nil {
173
		t.Fatal(err)
174
	}
175
	l := NewLoader()
176
	rj, err := l.ResolveFS(fsys, rootFile)
177
	if err != nil {
178
		t.Fatal(err)
179
	}
180
	return rj
181
}
182
183
func mustResolve(t *testing.T, l *Loader, fpath string) *ResolvedJournal {
184
	t.Helper()
185
	rj, err := l.Resolve(fpath)
186
	if err != nil {
187
		t.Fatal(err)
188
	}
189
	return rj
190
}
191
192
func txDate(rj *ResolvedJournal) string {
193
	tx := rj.Occurrences[0].Ast.Entries[0].(*ast.Transaction)
194
	return fmt.Sprintf("%d-%02d-%02d", tx.Date.Year, tx.Date.Month, tx.Date.Day)
195
}