all repos

clerk @ 68a2727

missing tooling for ledger/hledger

clerk/journal/parser/parser_test.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
parser: support quote commodities in N and P directives, 1 month ago
1
package parser
2
3
import (
4
	"testing"
5
6
	"olexsmir.xyz/clerk/internal/testutil/golden"
7
	"olexsmir.xyz/clerk/journal/ast"
8
	"olexsmir.xyz/clerk/journal/lexer"
9
)
10
11
func TestParser_ParseFile(t *testing.T) {
12
	tests := []string{
13
		"blank line",
14
		"comment semicolon",
15
		"comment hash",
16
		"comment percent",
17
		"comment star",
18
		"comment with tags",
19
		"transaction with tag comments",
20
		"alias directive",
21
		"tag directive",
22
		"year directive",
23
		"decimal-mark directive",
24
		"C directive",
25
		"D directive",
26
		"P directive",
27
		"P directive with time",
28
		"P directive quoted commodity",
29
		"N directive",
30
		"N directive quoted",
31
		"inclue directive",
32
		"inclue directive with comment",
33
		"inclue directive digit path",
34
		"apply tag directive",
35
		"apply fixed directive",
36
		"end apply directive",
37
		"comment directive",
38
		"comment directive end alone",
39
		"comment directive with header",
40
		"empty comment block",
41
		"never ending comment directive",
42
		"nested apply tag directives",
43
		"account directive",
44
		"account directive with comment",
45
		"account with subdirectives",
46
		"account subdirectives types",
47
		"comodity directive",
48
		"comodity directive word",
49
		"comodity directive no space",
50
		"commodity quantity first",
51
		"commodity quantity after",
52
		"commodity with subdirectives",
53
		"commodity format errors",
54
		"payee directive",
55
		"transaction",
56
		"automated transaction",
57
		"transaction with payee",
58
		"transaction with digit payees",
59
		"account with spaces",
60
		"transaction with multiword payee",
61
		"transaction pending",
62
		"transaction clearerd",
63
		"transaction with note",
64
		"transaction with comment",
65
		"transaction with secondary date",
66
		"transaction with costs",
67
		"transaction with cost and assertion",
68
		"transaction with posting",
69
		"transaction with unicode commodity symbols",
70
		"transaction with quoted commodity",
71
		"transaction with strange commodity symbols",
72
		"special chars in description",
73
		"transaction with tabs",
74
		"transaction in ukrainian",
75
		"transaction with code",
76
		"transaction with posting amounts",
77
		"transaction with spaced account name",
78
		"transaction with inline comment",
79
		"transaction with header comment",
80
		"transaction with trailing indent",
81
		"transaction with balance assertion",
82
		"transaction with inclusive balance assertion",
83
		"transaction with balance assignment",
84
		"transaction with scientific amounts",
85
		"transaction with virtual accounts",
86
		"virtual postings with statuses",
87
		"period transaction expressions",
88
		"unexpected token",
89
		"recovery after bad posting",
90
		"illegal only",
91
		"illegal at start",
92
		"illegal in posting",
93
		"illegal between transactions",
94
		"multiple bad postings",
95
		"three bad postings",
96
		"quoted payee names",
97
		"digit group marks",
98
		"directive prefixes",
99
		"bad between good",
100
		"all postings bad",
101
		"bad then next transaction",
102
		"comment between bad postings",
103
		"bad posting at end",
104
	}
105
	for _, tt := range tests {
106
		t.Run(tt, func(t *testing.T) {
107
			a := golden.Read(t, tt)
108
			l := lexer.New("j", a.Get("input"))
109
			f := New(l).ParseJournal()
110
			golden.Assert(t, a, ast.Dump(f))
111
		})
112
	}
113
}
114
115
func FuzzParser(f *testing.F) {
116
	f.Add([]byte(""))
117
	f.Add([]byte("account expenses:food\n"))
118
	f.Add([]byte("account a\n  ; subdirective\n"))
119
	f.Add([]byte("commodity 1,000.00 UAH\n"))
120
	f.Add([]byte("include other.journal\n"))
121
	f.Add([]byte("alias checking = assets:bank:checking\n"))
122
	f.Add([]byte("2024/01/01 * groceries\n    expenses:food  $10.00\n    assets:checking\n"))
123
	f.Add([]byte("2024/01/01=2024/01/02 groceries\n"))
124
	f.Add([]byte("2024/01/01 groceries\n  expenses:food  $10.00\n  assets:checking\n"))
125
	f.Add([]byte("2008/06/03 * eat & shop\n    expenses:food      $1\n    expenses:supplies  $1\n    assets:cash\n"))
126
	f.Add([]byte("2015-01-03 * Money exchange office\n    Assets:Cash  -20 EUR @ 7.53 HRK\n    Assets:Cash  150.60 HRK\n"))
127
	f.Add([]byte("2024/01/01 t ; inline comment\n  a  $10\n"))
128
	f.Add([]byte("2024/01/01 t\n  (a)  10 @@ $20\n  [b]  30\n"))
129
	f.Add([]byte("2024/01/01 ß\n  (ß)  10 ß\n"))
130
	f.Add([]byte("2024/01/01 t\n  (! a)  10\n"))
131
	f.Add([]byte("2024/01/01 t\n  a  $10 == $10\n"))
132
	f.Add([]byte("  2024/01/01 t\n  a  $10\n"))
133
	f.Add([]byte("P 2024/01/01 USD 41.50 UAH\n"))
134
	f.Add([]byte("P 2024-01-01 12:00:00 USD 41.50 UAH\n"))
135
	f.Add([]byte("P 2024-01-01 12:00 USD 41.50 UAH\n"))
136
	f.Add([]byte("~ monthly\n    expenses:food  $100\n    assets:checking\n"))
137
	f.Add([]byte("= /^Income/\n  expenses:food  $10\n"))
138
	f.Add([]byte("; a comment\n"))
139
	f.Add([]byte("; tag1:val1, tag2:val2\n"))
140
	f.Add([]byte("comment\nbody\nend\n"))
141
	f.Add([]byte("\n\n\n"))
142
	f.Add([]byte("перевірка\n"))
143
	f.Add([]byte("N $\n"))
144
	f.Add([]byte("apply tag foo\nend\n"))
145
	f.Add([]byte("@@@\n"))
146
	f.Add([]byte("   \n"))
147
	f.Add([]byte("0\n"))
148
	f.Add([]byte{0xff, 0xfe, 0x00})
149
150
	f.Fuzz(func(t *testing.T, data []byte) {
151
		l := lexer.New("j", data)
152
		j := New(l).ParseJournal()
153
		if j == nil {
154
			t.Fatalf("nil journal for input %q", string(data))
155
		}
156
157
		// error spans must be in bounds (allow sentinel extension past input)
158
		dataLen := len(data)
159
		for _, e := range j.Errors {
160
			if e.Span.Start.Offset < 0 {
161
				t.Fatal("error span start is negative")
162
			}
163
			if e.Span.End.Offset > dataLen+1 {
164
				t.Fatalf("error span end out of bounds: [%d,%d] len=%d", e.Span.Start.Offset, e.Span.End.Offset, dataLen)
165
			}
166
			if len(e.Message) == 0 {
167
				t.Fatal("empty error message")
168
			}
169
		}
170
171
		// dump must not panic and must be deterministic
172
		dump1, dump2 := ast.Dump(j), ast.Dump(j)
173
		if dump1 != dump2 {
174
			t.Fatal("non-deterministic dump")
175
		}
176
	})
177
}