all repos

clerk @ e3c8c08aaa1afff4a8669ff5e9da4f677d8df464

missing tooling for ledger/hledger

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

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