clerk/journal/parser/parser_test.go (view raw)
| 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 | } |