all repos

clerk @ 7b1aef3

missing tooling for ledger/hledger

clerk/journal/lexer/lexer_test.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
formatter..., 1 month ago
1
package lexer
2
3
import (
4
	"testing"
5
6
	"olexsmir.xyz/clerk/internal/testutil/golden"
7
	"olexsmir.xyz/clerk/journal/token"
8
)
9
10
func TestLexer(t *testing.T) {
11
	tests := []struct {
12
		name  string
13
		input string
14
	}{
15
		{"simple transaction", `2024/01/01 groceries
16
    expenses:food  $10.00
17
    assets:checking
18
`},
19
		{"transaction, accounts with uppercase latters", `
20
2011/01/27 Book Store
21
    Expenses:Books                       $20.00
22
    Liabilities:MasterCard
23
`},
24
		{"cleared transaction", `2024/01/01 * groceries
25
    expenses:food  $10.00
26
    assets:checking
27
`},
28
		{"automated transaction", `= ^income
29
    (liabilities:tax)  *.33
30
31
= expenses:gifts
32
    budget:gifts  (amount * -1)
33
`},
34
		{"transaction with code", `2024/01/01 (123) groceries
35
    expenses:food  $10.00
36
    assets:checking
37
`},
38
		{"transaction with virtual accounts", `2024/01/01 * groceries
39
	(virtual:account)  1 PESO
40
	[something:else]   5 PESO
41
`},
42
		{"transaction with unicode commodity symbols", `2024/01/01 groceries
43
    expenses:food  €10.00
44
    expenses:food  £5.00
45
    expenses:food  ₹700.00
46
    expenses:food  40.00 гривні
47
    assets:cash
48
`},
49
{"bangs and stars in transaction description", `2026-06-07 * payee !one | something *important*
50
    expenses:food  40.00
51
    assets:cash
52
`},
53
		{"date with secondary", `2024/01/01=2024/01/02 groceries`},
54
		{"better date", `2024-01-02`},
55
		{"comment line", `; this is a comment`},
56
		{"star comment", `* this is a comment`},
57
		{"hash comment", `# this is a comment`},
58
		{"account directive", `account expenses:food`},
59
		{"commodity directive", `commodity 1,000.00 UAH`},
60
		{"market price directive", "P 2024-01-01 USD 40.50 UAH\n"},
61
		{"market price directive with time", "P 2024-01-01 12:00:00 USD 40.50 UAH\n"},
62
		{"inline comment", `2024/01/01 groceries ; a note`},
63
		{"empty", ``},
64
		{"blank lines", "\n\n\n"},
65
		{"comment block directive", "comment\ncontent\nend\n"},
66
		{"comment block directive without end", "comment\ncontent\n"},
67
	}
68
	for _, tt := range tests {
69
		t.Run(tt.name, func(t *testing.T) {
70
			l := New("j", []byte(tt.input))
71
			golden.Assert(t, l.Dump())
72
		})
73
	}
74
}
75
76
// token category bounds, ensures fuzzer never sees out-of-range token types.
77
const maxKnownTokenType = token.N
78
79
func FuzzLexer(f *testing.F) {
80
	f.Add([]byte("2024/01/01 groceries\n  expenses:food  $10.00\n  assets:checking\n"))
81
	f.Add([]byte("2024/01/01 * groceries\n  expenses:food  $10.00\n  assets:checking\n"))
82
	f.Add([]byte("2024/01/01 ! groceries\n  expenses:food  $10.00\n  assets:checking\n"))
83
	f.Add([]byte("2024/01/01 t ; inline comment\n  a  $10\n"))
84
	f.Add([]byte("2024/01/01 t\n  (a)  10 @@ $20\n  [b]  30\n"))
85
	f.Add([]byte("2008/06/03 * eat & shop\n    expenses:food      $1\n    expenses:supplies  $1\n    assets:cash\n"))
86
	f.Add([]byte("2015-01-03 * Money exchange office\n    Assets:Cash  -20 EUR @ 7.53 HRK\n    Assets:Cash  150.60 HRK\n"))
87
	f.Add([]byte("2024/01/01 ß\n  (ß)  10 ß\n"))
88
	f.Add([]byte("2024/01/01 t\n  (! a)  10\n"))
89
	f.Add([]byte("comment\nbody\nend\n"))
90
	f.Add([]byte("apply tag foo\nend\n"))
91
	f.Add([]byte("; a comment\n"))
92
	f.Add([]byte("# a comment\n"))
93
	f.Add([]byte("* a comment\n"))
94
	f.Add([]byte("account expenses:food\n"))
95
	f.Add([]byte("commodity 1,000.00 UAH\n"))
96
	f.Add([]byte("N $\n"))
97
	f.Add([]byte("P 2024-01-01 USD 41.50 UAH\n"))
98
	f.Add([]byte("P 2024-01-01 12:00:00 USD 41.50 UAH\n"))
99
	f.Add([]byte("P 2024-01-01 12:00 USD 41.50 UAH\n"))
100
	f.Add([]byte("~ monthly\n  a  $10\n  b\n"))
101
	f.Add([]byte("= /^Income/\n  expenses:food  $10\n"))
102
	f.Add([]byte("перевірка\n"))
103
	f.Add([]byte(""))
104
	f.Add([]byte("\n\n\n"))
105
	f.Add([]byte("@@@\n"))
106
	f.Add([]byte("   \n"))
107
	f.Add([]byte("0\n"))
108
	f.Add([]byte{0xff, 0xfe, 0x00})
109
110
	f.Fuzz(func(t *testing.T, data []byte) {
111
		// Pass 1: lex and validate token stream
112
		l := New("j", data)
113
		var tokens []token.Token
114
		maxTokens := max(len(data)*2, 16)
115
		prevEnd := -1
116
		for range maxTokens {
117
			tok := l.Next()
118
119
			// Monotonic span
120
			if tok.Span.Start.Offset < prevEnd {
121
				t.Fatalf("non-monotonic span: prevEnd=%d current=%s %d",
122
					prevEnd, tok.Type, tok.Span.Start.Offset)
123
			}
124
125
			// Token type in range (no garbage from memory corruption)
126
			if tok.Type < 0 || tok.Type > maxKnownTokenType {
127
				t.Fatalf("token type out of range: %d", tok.Type)
128
			}
129
130
			// Span in bounds (EOF/NEWLINE sentinels may extend one past input)
131
			maxEnd := len(data)
132
			if tok.Type == token.NEWLINE || tok.Type == token.EOF {
133
				maxEnd = len(data) + 1
134
			}
135
			if tok.Span.Start.Offset < 0 || tok.Span.End.Offset > maxEnd ||
136
				tok.Span.Start.Offset > tok.Span.End.Offset {
137
				t.Fatalf("span out of bounds: [%d,%d] for len=%d type=%s",
138
					tok.Span.Start.Offset, tok.Span.End.Offset, len(data), tok.Type)
139
			}
140
141
			if tok.Type == token.EOF {
142
				break
143
			}
144
145
			// Non-zero-length for non-EOF tokens (NEWLINE sentinel is exempt)
146
			if tok.Type != token.NEWLINE && tok.Span.End.Offset <= tok.Span.Start.Offset {
147
				t.Fatalf("non-progressing token: %s %q at %d:%d-%d:%d",
148
					tok.Type, tok.Literal,
149
					tok.Span.Start.Line, tok.Span.Start.Col,
150
					tok.Span.End.Line, tok.Span.End.Col)
151
			}
152
153
			tokens = append(tokens, tok)
154
			prevEnd = tok.Span.End.Offset
155
		}
156
157
		if prevEnd > len(data)+1 {
158
			t.Fatalf("token consumed beyond input: end=%d len=%d", prevEnd, len(data))
159
		}
160
161
		// Pass 2: re-lex the same input — token stream must be identical
162
		l2 := New("j", data)
163
		for _, expected := range tokens {
164
			tok := l2.Next()
165
			if tok.Type != expected.Type || tok.Literal != expected.Literal {
166
				t.Fatalf("re-lex mismatch at offset %d: expected (%s %q), got (%s %q)",
167
					expected.Span.Start.Offset, expected.Type, expected.Literal, tok.Type, tok.Literal)
168
			}
169
		}
170
	})
171
}