all repos

clerk @ e2e9e2b

missing tooling for ledger/hledger

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

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