233 files changed,
2458 insertions(+),
1366 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-07-28 15:22:06 +0300
Authored at:
2026-07-28 13:17:02 +0300
Change ID:
kpxnwmwtzmrrrrotouznnlywuvozrrws
Parent:
8575d55
jump to
M
internal/linter/linter_test.go
··· 31 31 func TestLinter(t *testing.T) { 32 32 for tname, trules := range tests { 33 33 t.Run(tname, func(t *testing.T) { 34 - inp := golden.Load(t, tname) 35 - pf, err := journal.NewLoader().LoadBytes(tname+".journal", inp) 34 + a := golden.Read(t, tname) 35 + fsys, err := a.FS() 36 36 if err != nil { 37 - t.Fatalf("failed to load test journal: %v\n", err) 37 + t.Fatal(err) 38 + } 39 + 40 + l := journal.NewLoader() 41 + if _, err := l.LoadFS(fsys, "in.journal"); err != nil { 42 + t.Fatalf("failed to load test journal: %v", err) 38 43 } 39 44 40 - ctx := semantic.Build([]*journal.ParsedFile{pf}) 45 + ctx := semantic.Build(l.Ordered()) 41 46 finds := NewLinter(trules).Run(ctx) 42 47 43 48 var b strings.Builder 44 49 Fprint(&b, PathBasename, finds) 45 - golden.Assert(t, tname, b.String()) 50 + golden.Assert(t, a, b.String()) 46 51 }) 47 52 } 48 53 }
D
internal/linter/testdata/account-depth.golden
··· 1 -account-depth.journal:2:9: account-depth: account "Expenses:Food:Restaurants:Pizza:Hawaiian" depth (5) exceeds max allowed depth (3) 2 -account-depth.journal:3:9: account-depth: account "Expenses:Tax:Income:More:Deep:Too" depth (6) exceeds max allowed depth (3) 3 -account-depth.journal:5:19: account-depth: account "Expenses:Food:Restaurants:Pizza:Hawaiian" depth (5) exceeds max allowed depth (3) 4 -account-depth.journal:12:5: account-depth: account "Income:Job:Salary:Monthly:Base" depth (5) exceeds max allowed depth (3) 5 -account-depth.journal:13:5: account-depth: account "Assets:Checking:Deep:Sub" depth (4) exceeds max allowed depth (3)
D
internal/linter/testdata/account-depth.input
··· 1 -account Expenses:Food:Restaurants 2 -account Expenses:Food:Restaurants:Pizza:Hawaiian 3 -account Expenses:Tax:Income:More:Deep:Too 4 - 5 -alias Fast:Food = Expenses:Food:Restaurants:Pizza:Hawaiian 6 - 7 -2024-01-01 groceries 8 - Expenses:Food $10.00 9 - Assets:Checking 10 - 11 -2024-01-02 salary 12 - Income:Job:Salary:Monthly:Base $1000 13 - Assets:Checking:Deep:Sub
A
internal/linter/testdata/account-depth.txtar
··· 1 +-- in.journal -- 2 +account Expenses:Food:Restaurants 3 +account Expenses:Food:Restaurants:Pizza:Hawaiian 4 +account Expenses:Tax:Income:More:Deep:Too 5 + 6 +alias Fast:Food = Expenses:Food:Restaurants:Pizza:Hawaiian 7 + 8 +2024-01-01 groceries 9 + Expenses:Food $10.00 10 + Assets:Checking 11 + 12 +2024-01-02 salary 13 + Income:Job:Salary:Monthly:Base $1000 14 + Assets:Checking:Deep:Sub 15 + 16 +-- expect -- 17 +in.journal:2:9: account-depth: account "Expenses:Food:Restaurants:Pizza:Hawaiian" depth (5) exceeds max allowed depth (3) 18 +in.journal:3:9: account-depth: account "Expenses:Tax:Income:More:Deep:Too" depth (6) exceeds max allowed depth (3) 19 +in.journal:5:19: account-depth: account "Expenses:Food:Restaurants:Pizza:Hawaiian" depth (5) exceeds max allowed depth (3) 20 +in.journal:12:5: account-depth: account "Income:Job:Salary:Monthly:Base" depth (5) exceeds max allowed depth (3) 21 +in.journal:13:5: account-depth: account "Assets:Checking:Deep:Sub" depth (4) exceeds max allowed depth (3)
D
internal/linter/testdata/duplicated-account.golden
··· 1 -duplicated-account.journal:1:9: duplicated-account: duplicated account declaration: Expenses:Food 2 -duplicated-account.journal:2:9: duplicated-account: duplicated account declaration: Expenses:Transport 3 -duplicated-account.journal:4:9: duplicated-account: duplicated account declaration: Expenses:Food 4 -duplicated-account.journal:6:9: duplicated-account: duplicated account declaration: Expenses:Transport
A
internal/linter/testdata/duplicated-account.txtar
··· 1 +-- in.journal -- 2 +account Expenses:Food 3 +account Expenses:Transport 4 +account Income:Salary 5 +account Expenses:Food 6 +account Assets:Cash 7 +account Expenses:Transport 8 + 9 +-- expect -- 10 +in.journal:1:9: duplicated-account: duplicated account declaration: Expenses:Food 11 +in.journal:2:9: duplicated-account: duplicated account declaration: Expenses:Transport 12 +in.journal:4:9: duplicated-account: duplicated account declaration: Expenses:Food 13 +in.journal:6:9: duplicated-account: duplicated account declaration: Expenses:Transport
D
internal/linter/testdata/duplicated-commodity.golden
··· 1 -duplicated-commodity.journal:1:1: duplicated-commodity: duplicated commodity declaration: USD 2 -duplicated-commodity.journal:2:1: duplicated-commodity: duplicated commodity declaration: EUR 3 -duplicated-commodity.journal:4:1: duplicated-commodity: duplicated commodity declaration: USD 4 -duplicated-commodity.journal:6:1: duplicated-commodity: duplicated commodity declaration: EUR
A
internal/linter/testdata/duplicated-commodity.txtar
··· 1 +-- in.journal -- 2 +commodity USD 3 +commodity EUR 4 +commodity GBP 5 +commodity USD 6 +commodity JPY 7 +commodity EUR 8 + 9 +-- expect -- 10 +in.journal:1:1: duplicated-commodity: duplicated commodity declaration: USD 11 +in.journal:2:1: duplicated-commodity: duplicated commodity declaration: EUR 12 +in.journal:4:1: duplicated-commodity: duplicated commodity declaration: USD 13 +in.journal:6:1: duplicated-commodity: duplicated commodity declaration: EUR
D
internal/linter/testdata/empty-postings.golden
··· 1 -empty-postings.journal:1:1: empty-postings: transaction has no postings
D
internal/linter/testdata/empty-postings.input
··· 1 -2024-05-01 empty txn 2 - 3 -2024-05-01 "not empty txn" 4 - my:account
D
internal/linter/testdata/missing-commodity.golden
··· 1 -missing-commodity.journal:2:21: missing-commodity: amount missing commodity 2 -missing-commodity.journal:10:21: missing-commodity: amount missing commodity 3 -missing-commodity.journal:11:20: missing-commodity: amount missing commodity 4 -missing-commodity.journal:11:27: missing-commodity: amount missing commodity 5 -missing-commodity.journal:15:18: missing-commodity: amount missing commodity 6 -missing-commodity.journal:19:3: missing-commodity: amount missing commodity 7 -missing-commodity.journal:21:3: missing-commodity: amount missing commodity 8 -missing-commodity.journal:26:20: missing-commodity: amount missing commodity 9 -missing-commodity.journal:30:20: missing-commodity: amount missing commodity
D
internal/linter/testdata/missing-commodity.input
··· 1 -2024-06-01 "no commodity" 2 - expenses:food 50.00 3 - assets:cash 4 - 5 -2024-06-02 "with commodity" 6 - expenses:food 50.00 USD 7 - assets:cash 8 - 9 -2024-06-03 "another missing" 10 - expenses:food 100 @ 1$ 11 - expenses:rent 1000 @ 1 12 - expenses:rent2 1000 USD @ 1 UAH 13 - assets:cash 14 - 15 -P 2026-06-20 USD 2 16 -P 2026-06-20 USD 2 UAH 17 - 18 -D 100 USD 19 -D 100 20 - 21 -C 1 = 40 UAH 22 -C 1 USD = 40 UAH 23 - 24 -~ monthly 25 - expenses:rent $2000 26 - expenses:food 2000 27 - assets:bank 28 - 29 -= /restaurant/ 30 - expenses:food 20.00 31 - assets:checking
A
internal/linter/testdata/missing-commodity.txtar
··· 1 +-- in.journal -- 2 +2024-06-01 "no commodity" 3 + expenses:food 50.00 4 + assets:cash 5 + 6 +2024-06-02 "with commodity" 7 + expenses:food 50.00 USD 8 + assets:cash 9 + 10 +2024-06-03 "another missing" 11 + expenses:food 100 @ 1$ 12 + expenses:rent 1000 @ 1 13 + expenses:rent2 1000 USD @ 1 UAH 14 + assets:cash 15 + 16 +P 2026-06-20 USD 2 17 +P 2026-06-20 USD 2 UAH 18 + 19 +D 100 USD 20 +D 100 21 + 22 +C 1 = 40 UAH 23 +C 1 USD = 40 UAH 24 + 25 +~ monthly 26 + expenses:rent $2000 27 + expenses:food 2000 28 + assets:bank 29 + 30 += /restaurant/ 31 + expenses:food 20.00 32 + assets:checking 33 + 34 +-- expect -- 35 +in.journal:2:21: missing-commodity: amount missing commodity 36 +in.journal:10:21: missing-commodity: amount missing commodity 37 +in.journal:11:20: missing-commodity: amount missing commodity 38 +in.journal:11:27: missing-commodity: amount missing commodity 39 +in.journal:15:18: missing-commodity: amount missing commodity 40 +in.journal:19:3: missing-commodity: amount missing commodity 41 +in.journal:21:3: missing-commodity: amount missing commodity 42 +in.journal:26:20: missing-commodity: amount missing commodity 43 +in.journal:30:20: missing-commodity: amount missing commodity
D
internal/linter/testdata/missing-payee.golden
··· 1 -missing-payee.journal:5:1: missing-payee: transaction has no payee
M
internal/linter/testdata/missing-payee.input
→ internal/linter/testdata/missing-payee.txtar
··· 1 +-- in.journal -- 1 2 2024-05-01 "groceries" 2 3 expenses:food 50.00 USD 3 4 assets:cash ··· 9 10 2024-05-03 * "groceries" 10 11 expenses:food 50.00 USD 11 12 assets:cash 13 + 14 +-- expect -- 15 +in.journal:5:1: missing-payee: transaction has no payee
D
internal/linter/testdata/missing-status.golden
··· 1 -missing-status.journal:1:12: missing-status: transaction has no status
D
internal/linter/testdata/missing-status.input
··· 1 -2024-05-01 "groceries" 2 - expenses:food 50.00 USD 3 - assets:cash
D
internal/linter/testdata/multiple-omitted-amounts.golden
··· 1 -multiple-omitted-amounts.journal:2:1: multiple-omitted-amounts: more than one posting has omitted amount 2 -multiple-omitted-amounts.journal:3:1: multiple-omitted-amounts: more than one posting has omitted amount 3 -multiple-omitted-amounts.journal:7:1: multiple-omitted-amounts: more than one posting has omitted amount 4 -multiple-omitted-amounts.journal:8:1: multiple-omitted-amounts: more than one posting has omitted amount 5 -multiple-omitted-amounts.journal:9:1: multiple-omitted-amounts: more than one posting has omitted amount 6 -multiple-omitted-amounts.journal:11:1: multiple-omitted-amounts: more than one posting has omitted amount
D
internal/linter/testdata/multiple-omitted-amounts.input
··· 1 -2025-01-01 "two omitted amounts" 2 - Expenses:Food 3 - Expenses:Fun 4 - Assets:Cash 100 USD 5 - 6 -2025-01-02 "three omitted amounts" 7 - Expenses:Food 8 - Expenses:Fun 9 - Expenses:Play 10 - Assets:Cash 100 USD 11 - Equity:Opening-Balances 12 - 13 -2025-01-03 "one omitted amount (ok)" 14 - Expenses:Food 50 USD 15 - Assets:Cash 16 - 17 -2025-01-04 "all have amounts (ok)" 18 - Expenses:Food 50 USD 19 - Assets:Cash 50 USD 20 - 21 -2025-01-05 * reconcile or some s..stuff 22 - Assets:Bank = 1.69 UAH 23 - Assets:Cash = 2.00 UAH
A
internal/linter/testdata/multiple-omitted-amounts.txtar
··· 1 +-- in.journal -- 2 +2025-01-01 "two omitted amounts" 3 + Expenses:Food 4 + Expenses:Fun 5 + Assets:Cash 100 USD 6 + 7 +2025-01-02 "three omitted amounts" 8 + Expenses:Food 9 + Expenses:Fun 10 + Expenses:Play 11 + Assets:Cash 100 USD 12 + Equity:Opening-Balances 13 + 14 +2025-01-03 "one omitted amount (ok)" 15 + Expenses:Food 50 USD 16 + Assets:Cash 17 + 18 +2025-01-04 "all have amounts (ok)" 19 + Expenses:Food 50 USD 20 + Assets:Cash 50 USD 21 + 22 +2025-01-05 * reconcile or some s..stuff 23 + Assets:Bank = 1.69 UAH 24 + Assets:Cash = 2.00 UAH 25 + 26 +-- expect -- 27 +in.journal:2:1: multiple-omitted-amounts: more than one posting has omitted amount 28 +in.journal:3:1: multiple-omitted-amounts: more than one posting has omitted amount 29 +in.journal:7:1: multiple-omitted-amounts: more than one posting has omitted amount 30 +in.journal:8:1: multiple-omitted-amounts: more than one posting has omitted amount 31 +in.journal:9:1: multiple-omitted-amounts: more than one posting has omitted amount 32 +in.journal:11:1: multiple-omitted-amounts: more than one posting has omitted amount
D
internal/linter/testdata/omitted-precision.golden
··· 1 -omitted-precision.journal:2:9: omitted-precision: amount has insufficient precision 2 -omitted-precision.journal:4:9: omitted-precision: amount has insufficient precision
D
internal/linter/testdata/omitted-precision.input
··· 1 -2026-06-10 * asdf 2 - qwer 1 UIO 3 - asdf 1.00 ASD 4 - zxcv 2 UIO
A
internal/linter/testdata/omitted-precision.txtar
··· 1 +-- in.journal -- 2 +2026-06-10 * asdf 3 + qwer 1 UIO 4 + asdf 1.00 ASD 5 + zxcv 2 UIO 6 + 7 +-- expect -- 8 +in.journal:2:9: omitted-precision: amount has insufficient precision 9 +in.journal:4:9: omitted-precision: amount has insufficient precision
D
internal/linter/testdata/orderdate.golden
··· 1 -orderdate.journal:5:1: orderdate: transaction is out of chronological order (date 2026-01-10 before 2026-01-15) 2 -orderdate.journal:9:1: orderdate: transaction is out of chronological order (date 2026-01-12 before 2026-01-15) 3 -orderdate.journal:17:1: orderdate: transaction is out of chronological order (date 2026-01-20 before 2026-02-01) 4 -orderdate.journal:25:1: orderdate: transaction is out of chronological order (date 2025-12-01 before 2026-03-01)
D
internal/linter/testdata/orderdate.input
··· 1 -2026-01-15 * "Transaction 1" 2 - Expenses:Food $10 3 - Assets:Cash 4 - 5 -2026-01-10 * "Transaction 2 - out of order (before Txn 1)" 6 - Expenses:Food $20 7 - Assets:Cash 8 - 9 -2026-01-12 * "Transaction 3 - would slip through old prev logic (12 < 15 but 12 > 10)" 10 - Expenses:Food $8 11 - Assets:Cash 12 - 13 -2026-02-01 * "Transaction 4" 14 - Expenses:Food $15 15 - Assets:Cash 16 - 17 -2026-01-20 * "Transaction 5 - out of order (before Txn 4)" 18 - Expenses:Food $5 19 - Assets:Cash 20 - 21 -2026-03-01 * "Transaction 6" 22 - Expenses:Food $25 23 - Assets:Cash 24 - 25 -2025-12-01 * "Transaction 7 - entirely before everything" 26 - Expenses:Food $30 27 - Assets:Cash
A
internal/linter/testdata/orderdate.txtar
··· 1 +-- in.journal -- 2 +2026-01-15 * "Transaction 1" 3 + Expenses:Food $10 4 + Assets:Cash 5 + 6 +2026-01-10 * "Transaction 2 - out of order (before Txn 1)" 7 + Expenses:Food $20 8 + Assets:Cash 9 + 10 +2026-01-12 * "Transaction 3 - would slip through old prev logic (12 < 15 but 12 > 10)" 11 + Expenses:Food $8 12 + Assets:Cash 13 + 14 +2026-02-01 * "Transaction 4" 15 + Expenses:Food $15 16 + Assets:Cash 17 + 18 +2026-01-20 * "Transaction 5 - out of order (before Txn 4)" 19 + Expenses:Food $5 20 + Assets:Cash 21 + 22 +2026-03-01 * "Transaction 6" 23 + Expenses:Food $25 24 + Assets:Cash 25 + 26 +2025-12-01 * "Transaction 7 - entirely before everything" 27 + Expenses:Food $30 28 + Assets:Cash 29 + 30 +-- expect -- 31 +in.journal:5:1: orderdate: transaction is out of chronological order (date 2026-01-10 before 2026-01-15) 32 +in.journal:9:1: orderdate: transaction is out of chronological order (date 2026-01-12 before 2026-01-15) 33 +in.journal:17:1: orderdate: transaction is out of chronological order (date 2026-01-20 before 2026-02-01) 34 +in.journal:25:1: orderdate: transaction is out of chronological order (date 2025-12-01 before 2026-03-01)
D
internal/linter/testdata/parse-error.golden
··· 1 -parse-error.journal:1:1: parse-error: unexpected token AT
D
internal/linter/testdata/unbalanced-transaction.golden
··· 1 -unbalanced-transaction.journal:14:1: unbalanced-transaction: transaction is unbalanced; USD balance is 5 2 -unbalanced-transaction.journal:18:1: unbalanced-transaction: transaction is unbalanced; EUR balance is 5 3 -unbalanced-transaction.journal:18:1: unbalanced-transaction: transaction is unbalanced; USD balance is 5 4 -unbalanced-transaction.journal:32:1: unbalanced-transaction: transaction is unbalanced; USD balance is 10
M
internal/linter/testdata/unbalanced-transaction.input
→ internal/linter/testdata/unbalanced-transaction.txtar
··· 1 +-- in.journal -- 1 2 2024-01-01 "auto-balance" 2 3 expenses:food 10.00 USD 3 4 assets:cash ··· 36 37 2024-01-01 "all-virtual" 37 38 (Budget:fun) 10.00 USD 38 39 (Budget:fun) -10.00 USD 40 + 41 +-- expect -- 42 +in.journal:14:1: unbalanced-transaction: transaction is unbalanced; USD balance is 5 43 +in.journal:18:1: unbalanced-transaction: transaction is unbalanced; EUR balance is 5 44 +in.journal:18:1: unbalanced-transaction: transaction is unbalanced; USD balance is 5 45 +in.journal:32:1: unbalanced-transaction: transaction is unbalanced; USD balance is 10
D
internal/linter/testdata/undeclared-account.golden
··· 1 -undeclared-account.journal:5:3: undeclared-account: undeclared account: Assets:Cash 2 -undeclared-account.journal:9:3: undeclared-account: undeclared account: Assets:Cash
A
internal/linter/testdata/undeclared-account.txtar
··· 1 +-- in.journal -- 2 +account Expenses:Food 3 + 4 +2026-01-15 * "Lunch" 5 + Expenses:Food $10 6 + Assets:Cash 7 + 8 +2026-01-16 * "Dinner" 9 + Expenses:Food $15 10 + Assets:Cash 11 + 12 +-- expect -- 13 +in.journal:5:3: undeclared-account: undeclared account: Assets:Cash 14 +in.journal:9:3: undeclared-account: undeclared account: Assets:Cash
D
internal/linter/testdata/undeclared-commodity.golden
··· 1 -undeclared-commodity.journal:13:22: undeclared-commodity: undeclared commodity: GBP
M
internal/linter/testdata/undeclared-commodity.input
→ internal/linter/testdata/undeclared-commodity.txtar
··· 1 +-- in.journal -- 1 2 commodity USD 2 3 commodity EUR 3 4 ··· 12 13 2026-01-17 * "Transfer" 13 14 Assets:Cash 100 GBP 14 15 Assets:Bank 16 + 17 +-- expect -- 18 +in.journal:13:22: undeclared-commodity: undeclared commodity: GBP
D
internal/linter/testdata/unused-account.golden
··· 1 -unused-account.journal:2:9: unused-account: unused account: equity:opening
A
internal/linter/testdata/unused-account.txtar
··· 1 +-- in.journal -- 2 +account assets:cash 3 +account equity:opening 4 +account assets:cash 5 + 6 +2024-05-02 * "groceries" 7 + expenses:food 40.00 USD 8 + assets:cash 9 + 10 +-- expect -- 11 +in.journal:2:9: unused-account: unused account: equity:opening
M
internal/testutil/golden/golden.go
··· 2 2 3 3 import ( 4 4 "flag" 5 + "io/fs" 5 6 "os" 6 7 "path/filepath" 7 8 "strings" 8 9 "testing" 10 + 11 + "olexsmir.xyz/clerk/internal/testutil/txtar" 9 12 ) 10 13 11 14 var update = flag.Bool("golden.update", false, "update golden files") 12 15 13 -// Load reads testdata/<path>.input. Fails the test if file is not found. 14 -func Load(t testing.TB, path string) []byte { 16 +var normalizer = strings.NewReplacer("/", "__", " ", "_") 17 + 18 +func txtarPath(name string) string { 19 + return filepath.Join("testdata", normalizer.Replace(name)+".txtar") 20 +} 21 + 22 +// Archive wraps a txtar.Archive with its on-disk path for efficient updates. 23 +type Archive struct { 24 + *txtar.Archive 25 + path string 26 +} 27 + 28 +// Read parses testdata/<name>.txtar and returns the archive. 29 +// Fails the test if the file doesn't exist or the archive is empty. 30 +func Read(t testing.TB, name string) *Archive { 15 31 t.Helper() 16 - data, err := os.ReadFile(filepath.Join("testdata", path+".input")) 32 + path := txtarPath(name) 33 + data, err := os.ReadFile(path) 17 34 if err != nil { 18 - t.Fatalf("loading %s: %v", path, err) 35 + t.Fatalf("loading %s: %v", name+".txtar", err) 19 36 } 20 - return data 37 + a := txtar.Parse(data) 38 + if len(a.Files) == 0 { 39 + t.Fatalf("txtar %s: no files", name) 40 + } 41 + return &Archive{Archive: a, path: path} 21 42 } 22 43 23 -// Assert compares got against testdata/<name>.golden. 24 -func Assert(t testing.TB, name, got string) { 25 - t.Helper() 44 +func (a *Archive) FS() (fs.FS, error) { return txtar.FS(a.Archive) } 26 45 27 - normalized := strings.NewReplacer("/", "__", " ", "_").Replace(name) 28 - path := filepath.Join("testdata", normalized+".golden") 46 +// Assert compares got against the "expect" file in a txtar archive. 47 +// With -golden.update, rewrites the expect section in-place, preserving other files. 48 +func Assert(t testing.TB, a *Archive, got string) { 49 + t.Helper() 29 50 30 51 if *update { 31 - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { 32 - t.Fatalf("creating testdata dir: %v", err) 52 + for i, f := range a.Files { 53 + if f.Name == "expect" { 54 + a.Files[i].Data = []byte(got) 55 + goto write 56 + } 33 57 } 34 - if err := os.WriteFile(path, []byte(got), 0o644); err != nil { 35 - t.Fatalf("writing golden file: %v", err) 58 + a.Files = append(a.Files, txtar.File{Name: "expect", Data: []byte(got)}) 59 + write: 60 + if err := os.WriteFile(a.path, txtar.Format(a.Archive), 0o644); err != nil { 61 + t.Fatalf("writing updated txtar: %v", err) 36 62 } 37 63 return 38 64 } 39 65 40 - golden, err := os.ReadFile(path) 41 - if os.IsNotExist(err) { 42 - t.Fatalf("no golden file %s, run with -golden.update:\n%s", path, got) 43 - } 44 - if err != nil { 45 - t.Fatalf("reading golden file: %v", err) 46 - } 47 - 48 - if string(golden) != got { 49 - t.Fatalf("golden mismatch for %s\nwant:\n%s\ngot:\n%s", path, string(golden), got) 66 + for _, f := range a.Files { 67 + if f.Name == "expect" { 68 + if string(f.Data) != got { 69 + t.Fatalf("golden mismatch for %s\nwant:\n%s\ngot:\n%s", a.path, string(f.Data), got) 70 + } 71 + return 72 + } 50 73 } 74 + t.Fatalf("txtar %s: no 'expect' file", a.path) 51 75 }
A
internal/testutil/txtar/archive.go
··· 1 +// Copyright 2018 The Go Authors. All rights reserved. 2 +// Use of this source code is governed by a BSD-style 3 +// license that can be found in the LICENSE file. 4 + 5 +// Package txtar implements a trivial text-based file archive format. 6 +// 7 +// The goals for the format are: 8 +// 9 +// - be trivial enough to create and edit by hand. 10 +// - be able to store trees of text files describing go command test cases. 11 +// - diff nicely in git history and code reviews. 12 +// 13 +// Non-goals include being a completely general archive format, 14 +// storing binary data, storing file modes, storing special files like 15 +// symbolic links, and so on. 16 +// 17 +// # Txtar format 18 +// 19 +// A txtar archive is zero or more comment lines and then a sequence of file entries. 20 +// Each file entry begins with a file marker line of the form "-- FILENAME --" 21 +// and is followed by zero or more file content lines making up the file data. 22 +// The comment or file content ends at the next file marker line. 23 +// The file marker line must begin with the three-byte sequence "-- " 24 +// and end with the three-byte sequence " --", but the enclosed 25 +// file name can be surrounding by additional white space, 26 +// all of which is stripped. 27 +// 28 +// If the txtar file is missing a trailing newline on the final line, 29 +// parsers should consider a final newline to be present anyway. 30 +// 31 +// There are no possible syntax errors in a txtar archive. 32 +package txtar 33 + 34 +import ( 35 + "bytes" 36 + "fmt" 37 + "os" 38 + "strings" 39 +) 40 + 41 +// An Archive is a collection of files. 42 +type Archive struct { 43 + Comment []byte 44 + Files []File 45 +} 46 + 47 +// A File is a single file in an archive. 48 +type File struct { 49 + Name string // name of file ("foo/bar.txt") 50 + Data []byte // text content of file 51 +} 52 + 53 +// Get returns the data of the first file with the given name, or nil if not found. 54 +func (a *Archive) Get(name string) []byte { 55 + for _, f := range a.Files { 56 + if f.Name == name { 57 + return f.Data 58 + } 59 + } 60 + return nil 61 +} 62 + 63 +// Format returns the serialized form of an Archive. 64 +// It is assumed that the Archive data structure is well-formed: 65 +// a.Comment and all a.File[i].Data contain no file marker lines, 66 +// and all a.File[i].Name is non-empty. 67 +func Format(a *Archive) []byte { 68 + var buf bytes.Buffer 69 + buf.Write(fixNL(a.Comment)) 70 + for _, f := range a.Files { 71 + fmt.Fprintf(&buf, "-- %s --\n", f.Name) 72 + buf.Write(fixNL(f.Data)) 73 + } 74 + return buf.Bytes() 75 +} 76 + 77 +// ParseFile parses the named file as an archive. 78 +func ParseFile(file string) (*Archive, error) { 79 + data, err := os.ReadFile(file) 80 + if err != nil { 81 + return nil, err 82 + } 83 + return Parse(data), nil 84 +} 85 + 86 +// Parse parses the serialized form of an Archive. 87 +// The returned Archive holds slices of data. 88 +func Parse(data []byte) *Archive { 89 + a := new(Archive) 90 + var name string 91 + a.Comment, name, data = findFileMarker(data) 92 + for name != "" { 93 + f := File{name, nil} 94 + f.Data, name, data = findFileMarker(data) 95 + a.Files = append(a.Files, f) 96 + } 97 + return a 98 +} 99 + 100 +var ( 101 + newlineMarker = []byte("\n-- ") 102 + marker = []byte("-- ") 103 + markerEnd = []byte(" --") 104 +) 105 + 106 +// findFileMarker finds the next file marker in data, 107 +// extracts the file name, and returns the data before the marker, 108 +// the file name, and the data after the marker. 109 +// If there is no next marker, findFileMarker returns before = fixNL(data), name = "", after = nil. 110 +func findFileMarker(data []byte) (before []byte, name string, after []byte) { 111 + var i int 112 + for { 113 + if name, after = isMarker(data[i:]); name != "" { 114 + return data[:i], name, after 115 + } 116 + j := bytes.Index(data[i:], newlineMarker) 117 + if j < 0 { 118 + return fixNL(data), "", nil 119 + } 120 + i += j + 1 // positioned at start of new possible marker 121 + } 122 +} 123 + 124 +// isMarker checks whether data begins with a file marker line. 125 +// If so, it returns the name from the line and the data after the line. 126 +// Otherwise it returns name == "" with an unspecified after. 127 +func isMarker(data []byte) (name string, after []byte) { 128 + if !bytes.HasPrefix(data, marker) { 129 + return "", nil 130 + } 131 + if i := bytes.IndexByte(data, '\n'); i >= 0 { 132 + data, after = data[:i], data[i+1:] 133 + if data[i-1] == '\r' { // handle \r\n line ending 134 + data = data[:i-1] 135 + } 136 + } 137 + if !bytes.HasSuffix(data, markerEnd) || len(data) < len(marker)+len(markerEnd) { 138 + return "", nil 139 + } 140 + return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after 141 +} 142 + 143 +// If data is empty or ends in \n, fixNL returns data. 144 +// Otherwise fixNL returns a new slice consisting of data with a final \n added. 145 +func fixNL(data []byte) []byte { 146 + if len(data) == 0 || data[len(data)-1] == '\n' { 147 + return data 148 + } 149 + d := make([]byte, len(data)+1) 150 + copy(d, data) 151 + d[len(data)] = '\n' 152 + return d 153 +}
A
internal/testutil/txtar/archive_test.go
··· 1 +// Copyright 2018 The Go Authors. All rights reserved. 2 +// Use of this source code is governed by a BSD-style 3 +// license that can be found in the LICENSE file. 4 + 5 +package txtar 6 + 7 +import ( 8 + "bytes" 9 + "fmt" 10 + "reflect" 11 + "testing" 12 +) 13 + 14 +func TestParse(t *testing.T) { 15 + tests := []struct { 16 + name string 17 + text string 18 + parsed *Archive 19 + }{ 20 + { 21 + name: "basic", 22 + text: `comment1 23 +comment2 24 +-- file1 -- 25 +File 1 text. 26 +-- foo --- 27 +More file 1 text. 28 +-- file 2 -- 29 +File 2 text. 30 +-- empty -- 31 +-- noNL -- 32 +hello world 33 +-- empty filename line -- 34 +some content 35 +-- --`, 36 + parsed: &Archive{ 37 + Comment: []byte("comment1\ncomment2\n"), 38 + Files: []File{ 39 + {"file1", []byte("File 1 text.\n-- foo ---\nMore file 1 text.\n")}, 40 + {"file 2", []byte("File 2 text.\n")}, 41 + {"empty", []byte{}}, 42 + {"noNL", []byte("hello world\n")}, 43 + {"empty filename line", []byte("some content\n-- --\n")}, 44 + }, 45 + }, 46 + }, 47 + { 48 + name: "crlf", 49 + text: "comment\r\n-- file --\r\ndata\r\n", 50 + parsed: &Archive{ 51 + Comment: []byte("comment\r\n"), 52 + Files: []File{{"file", []byte("data\r\n")}}, 53 + }, 54 + }, 55 + { 56 + name: "utf8 and comment", 57 + text: `# This is a test comment 58 +-- hello.txt -- 59 +Hello 60 +-- unicode.txt -- 61 +Go语言 62 +`, 63 + parsed: &Archive{ 64 + Comment: []byte("# This is a test comment\n"), 65 + Files: []File{ 66 + {"hello.txt", []byte("Hello\n")}, 67 + {"unicode.txt", []byte("Go语言\n")}, 68 + }, 69 + }, 70 + }, 71 + } 72 + for _, tt := range tests { 73 + t.Run(tt.name, func(t *testing.T) { 74 + a := Parse([]byte(tt.text)) 75 + if !reflect.DeepEqual(a, tt.parsed) { 76 + t.Fatalf("Parse: wrong output:\nhave:\n%s\nwant:\n%s", shortArchive(a), shortArchive(tt.parsed)) 77 + } 78 + text := Format(a) 79 + a = Parse(text) 80 + if !reflect.DeepEqual(a, tt.parsed) { 81 + t.Fatalf("Parse after Format: wrong output:\nhave:\n%s\nwant:\n%s", shortArchive(a), shortArchive(tt.parsed)) 82 + } 83 + }) 84 + } 85 +} 86 + 87 +func TestFormat(t *testing.T) { 88 + tests := []struct { 89 + name string 90 + input *Archive 91 + wanted string 92 + }{ 93 + { 94 + name: "basic", 95 + input: &Archive{ 96 + Comment: []byte("comment1\ncomment2\n"), 97 + Files: []File{ 98 + {"file1", []byte("File 1 text.\n-- foo ---\nMore file 1 text.\n")}, 99 + {"file 2", []byte("File 2 text.\n")}, 100 + {"empty", []byte{}}, 101 + {"noNL", []byte("hello world")}, 102 + }, 103 + }, 104 + wanted: `comment1 105 +comment2 106 +-- file1 -- 107 +File 1 text. 108 +-- foo --- 109 +More file 1 text. 110 +-- file 2 -- 111 +File 2 text. 112 +-- empty -- 113 +-- noNL -- 114 +hello world 115 +`, 116 + }, 117 + } 118 + for _, tt := range tests { 119 + t.Run(tt.name, func(t *testing.T) { 120 + result := Format(tt.input) 121 + if string(result) != tt.wanted { 122 + t.Errorf("Wrong output. \nGot:\n%s\nWant:\n%s\n", string(result), tt.wanted) 123 + } 124 + }) 125 + } 126 +} 127 + 128 +func shortArchive(a *Archive) string { 129 + var buf bytes.Buffer 130 + fmt.Fprintf(&buf, "comment: %q\n", a.Comment) 131 + for _, f := range a.Files { 132 + fmt.Fprintf(&buf, "file %q: %q\n", f.Name, f.Data) 133 + } 134 + return buf.String() 135 +}
A
internal/testutil/txtar/fs.go
··· 1 +// Copyright 2024 The Go Authors. All rights reserved. 2 +// Use of this source code is governed by a BSD-style 3 +// license that can be found in the LICENSE file. 4 + 5 +package txtar 6 + 7 +import ( 8 + "errors" 9 + "fmt" 10 + "io" 11 + "io/fs" 12 + "path" 13 + "slices" 14 + "time" 15 +) 16 + 17 +// FS returns the file system form of an Archive. 18 +// It returns an error if any of the file names in the archive 19 +// are not valid file system names. 20 +// The archive must not be modified while the FS is in use. 21 +// 22 +// If the file system detects that it has been modified, calls to the 23 +// file system return an ErrModified error. 24 +func FS(a *Archive) (fs.FS, error) { 25 + // Create a filesystem with a root directory. 26 + root := &node{fileinfo: fileinfo{path: ".", mode: readOnlyDir}} 27 + fsys := &filesystem{a, map[string]*node{root.path: root}} 28 + 29 + if err := initFiles(fsys); err != nil { 30 + return nil, fmt.Errorf("cannot create fs.FS from txtar.Archive: %s", err) 31 + } 32 + return fsys, nil 33 +} 34 + 35 +const ( 36 + readOnly fs.FileMode = 0o444 // read only mode 37 + readOnlyDir = readOnly | fs.ModeDir 38 +) 39 + 40 +// ErrModified indicates that file system returned by FS 41 +// noticed that the underlying archive has been modified 42 +// since the call to FS. Detection of modification is best effort, 43 +// to help diagnose misuse of the API, and is not guaranteed. 44 +var ErrModified error = errors.New("txtar.Archive has been modified during txtar.FS") 45 + 46 +// A filesystem is a simple in-memory file system for txtar archives, 47 +// represented as a map from valid path names to information about the 48 +// files or directories they represent. 49 +// 50 +// File system operations are read only. Modifications to the underlying 51 +// *Archive may race. To help prevent this, the filesystem tries 52 +// to detect modification during Open and return ErrModified if it 53 +// is able to detect a modification. 54 +type filesystem struct { 55 + ar *Archive 56 + nodes map[string]*node 57 +} 58 + 59 +// node is a file or directory in the tree of a filesystem. 60 +type node struct { 61 + fileinfo // fs.FileInfo and fs.DirEntry implementation 62 + idx int // index into ar.Files (for files) 63 + entries []fs.DirEntry // subdirectories and files (for directories) 64 +} 65 + 66 +var ( 67 + _ fs.FS = (*filesystem)(nil) 68 + _ fs.DirEntry = (*node)(nil) 69 +) 70 + 71 +// initFiles initializes fsys from fsys.ar.Files. Returns an error if there are any 72 +// invalid file names or collisions between file or directories. 73 +func initFiles(fsys *filesystem) error { 74 + for idx, file := range fsys.ar.Files { 75 + name := file.Name 76 + if !fs.ValidPath(name) { 77 + return fmt.Errorf("file %q is an invalid path", name) 78 + } 79 + 80 + n := &node{idx: idx, fileinfo: fileinfo{path: name, size: len(file.Data), mode: readOnly}} 81 + if err := insert(fsys, n); err != nil { 82 + return err 83 + } 84 + } 85 + return nil 86 +} 87 + 88 +// insert adds node n as an entry to its parent directory within the filesystem. 89 +func insert(fsys *filesystem, n *node) error { 90 + if m := fsys.nodes[n.path]; m != nil { 91 + return fmt.Errorf("duplicate path %q", n.path) 92 + } 93 + fsys.nodes[n.path] = n 94 + 95 + // fsys.nodes contains "." to prevent infinite loops. 96 + parent, err := directory(fsys, path.Dir(n.path)) 97 + if err != nil { 98 + return err 99 + } 100 + parent.entries = append(parent.entries, n) 101 + return nil 102 +} 103 + 104 +// directory returns the directory node with the path dir and lazily-creates it 105 +// if it does not exist. 106 +func directory(fsys *filesystem, dir string) (*node, error) { 107 + if m := fsys.nodes[dir]; m != nil && m.IsDir() { 108 + return m, nil // pre-existing directory 109 + } 110 + 111 + n := &node{fileinfo: fileinfo{path: dir, mode: readOnlyDir}} 112 + if err := insert(fsys, n); err != nil { 113 + return nil, err 114 + } 115 + return n, nil 116 +} 117 + 118 +// dataOf returns the data associated with the file t. 119 +// May return ErrModified if fsys.ar has been modified. 120 +func dataOf(fsys *filesystem, n *node) ([]byte, error) { 121 + if n.idx >= len(fsys.ar.Files) { 122 + return nil, ErrModified 123 + } 124 + 125 + f := fsys.ar.Files[n.idx] 126 + if f.Name != n.path || len(f.Data) != n.size { 127 + return nil, ErrModified 128 + } 129 + return f.Data, nil 130 +} 131 + 132 +func (fsys *filesystem) Open(name string) (fs.File, error) { 133 + if !fs.ValidPath(name) { 134 + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} 135 + } 136 + 137 + n := fsys.nodes[name] 138 + switch { 139 + case n == nil: 140 + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} 141 + case n.IsDir(): 142 + return &openDir{fileinfo: n.fileinfo, entries: n.entries}, nil 143 + default: 144 + data, err := dataOf(fsys, n) 145 + if err != nil { 146 + return nil, err 147 + } 148 + return &openFile{fileinfo: n.fileinfo, data: data}, nil 149 + } 150 +} 151 + 152 +func (fsys *filesystem) ReadFile(name string) ([]byte, error) { 153 + file, err := fsys.Open(name) 154 + if err != nil { 155 + return nil, err 156 + } 157 + if file, ok := file.(*openFile); ok { 158 + return slices.Clone(file.data), nil 159 + } 160 + return nil, &fs.PathError{Op: "read", Path: name, Err: fs.ErrInvalid} 161 +} 162 + 163 +// A fileinfo implements fs.FileInfo and fs.DirEntry for a given archive file. 164 +type fileinfo struct { 165 + path string // unique path to the file or directory within a filesystem 166 + size int 167 + mode fs.FileMode 168 +} 169 + 170 +var ( 171 + _ fs.FileInfo = (*fileinfo)(nil) 172 + _ fs.DirEntry = (*fileinfo)(nil) 173 +) 174 + 175 +func (i *fileinfo) Name() string { return path.Base(i.path) } 176 +func (i *fileinfo) Size() int64 { return int64(i.size) } 177 +func (i *fileinfo) Mode() fs.FileMode { return i.mode } 178 +func (i *fileinfo) Type() fs.FileMode { return i.mode.Type() } 179 +func (i *fileinfo) ModTime() time.Time { return time.Time{} } 180 +func (i *fileinfo) IsDir() bool { return i.mode&fs.ModeDir != 0 } 181 +func (i *fileinfo) Sys() any { return nil } 182 +func (i *fileinfo) Info() (fs.FileInfo, error) { return i, nil } 183 + 184 +// An openFile is a regular (non-directory) fs.File open for reading. 185 +type openFile struct { 186 + fileinfo 187 + data []byte 188 + offset int64 189 +} 190 + 191 +var _ fs.File = (*openFile)(nil) 192 + 193 +func (f *openFile) Stat() (fs.FileInfo, error) { return &f.fileinfo, nil } 194 +func (f *openFile) Close() error { return nil } 195 +func (f *openFile) Read(b []byte) (int, error) { 196 + if f.offset >= int64(len(f.data)) { 197 + return 0, io.EOF 198 + } 199 + if f.offset < 0 { 200 + return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid} 201 + } 202 + n := copy(b, f.data[f.offset:]) 203 + f.offset += int64(n) 204 + return n, nil 205 +} 206 + 207 +func (f *openFile) Seek(offset int64, whence int) (int64, error) { 208 + switch whence { 209 + case 0: 210 + // offset += 0 211 + case 1: 212 + offset += f.offset 213 + case 2: 214 + offset += int64(len(f.data)) 215 + } 216 + if offset < 0 || offset > int64(len(f.data)) { 217 + return 0, &fs.PathError{Op: "seek", Path: f.path, Err: fs.ErrInvalid} 218 + } 219 + f.offset = offset 220 + return offset, nil 221 +} 222 + 223 +func (f *openFile) ReadAt(b []byte, offset int64) (int, error) { 224 + if offset < 0 || offset > int64(len(f.data)) { 225 + return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid} 226 + } 227 + n := copy(b, f.data[offset:]) 228 + if n < len(b) { 229 + return n, io.EOF 230 + } 231 + return n, nil 232 +} 233 + 234 +// A openDir is a directory fs.File (so also an fs.ReadDirFile) open for reading. 235 +type openDir struct { 236 + fileinfo 237 + entries []fs.DirEntry 238 + offset int 239 +} 240 + 241 +var _ fs.ReadDirFile = (*openDir)(nil) 242 + 243 +func (d *openDir) Stat() (fs.FileInfo, error) { return &d.fileinfo, nil } 244 +func (d *openDir) Close() error { return nil } 245 +func (d *openDir) Read(b []byte) (int, error) { 246 + return 0, &fs.PathError{Op: "read", Path: d.path, Err: fs.ErrInvalid} 247 +} 248 + 249 +func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) { 250 + n := len(d.entries) - d.offset 251 + if n == 0 && count > 0 { 252 + return nil, io.EOF 253 + } 254 + if count > 0 && n > count { 255 + n = count 256 + } 257 + list := make([]fs.DirEntry, n) 258 + copy(list, d.entries[d.offset:d.offset+n]) 259 + d.offset += n 260 + return list, nil 261 +}
A
internal/testutil/txtar/fs_test.go
··· 1 +// Copyright 2024 The Go Authors. All rights reserved. 2 +// Use of this source code is governed by a BSD-style 3 +// license that can be found in the LICENSE file. 4 + 5 +package txtar_test 6 + 7 +import ( 8 + "io/fs" 9 + "strings" 10 + "testing" 11 + "testing/fstest" 12 + 13 + "olexsmir.xyz/clerk/internal/testutil/txtar" 14 +) 15 + 16 +func TestFS(t *testing.T) { 17 + fstestcases := []struct{ name, input, files string }{ 18 + { 19 + name: "empty", 20 + input: ``, 21 + files: "", 22 + }, 23 + { 24 + name: "one", 25 + input: ` 26 +-- one.txt -- 27 +one 28 +`, 29 + files: "one.txt", 30 + }, 31 + { 32 + name: "two", 33 + input: ` 34 +-- one.txt -- 35 +one 36 +-- two.txt -- 37 +two 38 +`, 39 + files: "one.txt two.txt", 40 + }, 41 + { 42 + name: "subdirectories", 43 + input: ` 44 +-- one.txt -- 45 +one 46 +-- 2/two.txt -- 47 +two 48 +-- 2/3/three.txt -- 49 +three 50 +-- 4/four.txt -- 51 +four 52 +`, 53 + files: "one.txt 2/two.txt 2/3/three.txt 4/four.txt", 54 + }, 55 + } 56 + 57 + for _, tc := range fstestcases { 58 + t.Run(tc.name, func(t *testing.T) { 59 + a := txtar.Parse([]byte(tc.input)) 60 + fsys, err := txtar.FS(a) 61 + if err != nil { 62 + t.Fatal(err) 63 + } 64 + 65 + files := strings.Fields(tc.files) 66 + if err := fstest.TestFS(fsys, files...); err != nil { 67 + t.Fatal(err) 68 + } 69 + 70 + for _, f := range a.Files { 71 + b, err := fs.ReadFile(fsys, f.Name) 72 + if err != nil { 73 + t.Errorf("ReadFile(%q) failed with error: %v", f.Name, err) 74 + } 75 + if got, want := string(b), string(f.Data); got != want { 76 + t.Errorf("ReadFile(%q) = %q; want %q", f.Name, got, want) 77 + } 78 + } 79 + }) 80 + } 81 +} 82 + 83 +func TestInvalid(t *testing.T) { 84 + invalidtestcases := []struct{ name, want, input string }{ 85 + {"unclean file names", "invalid path", ` 86 +-- 1/../one.txt -- 87 +one 88 +-- 2/sub/../two.txt -- 89 +two 90 +`}, 91 + {"duplicate name", `cannot create fs.FS from txtar.Archive: duplicate path "1/2/one.txt"`, ` 92 +-- 1/2/one.txt -- 93 +one 94 +-- 1/2/one.txt -- 95 +two 96 +`}, 97 + {"file conflicts with directory", `duplicate path "1/2"`, ` 98 +-- 1/2 -- 99 +one 100 +-- 1/2/one.txt -- 101 +two 102 +`}, 103 + } 104 + 105 + for _, tc := range invalidtestcases { 106 + t.Run(tc.name, func(t *testing.T) { 107 + a := txtar.Parse([]byte(tc.input)) 108 + _, err := txtar.FS(a) 109 + if err == nil { 110 + t.Fatal("txtar.FS(...) succeeded; expected an error") 111 + } 112 + if got := err.Error(); !strings.Contains(got, tc.want) || tc.want == "" { 113 + t.Errorf("txtar.FS(...) got error %q; want %q", got, tc.want) 114 + } 115 + }) 116 + } 117 +} 118 + 119 +func TestModified(t *testing.T) { 120 + const input = ` 121 +-- one.txt -- 122 +one 123 +` 124 + for _, mod := range []func(a *txtar.Archive){ 125 + func(a *txtar.Archive) { a.Files[0].Data = []byte("other") }, 126 + func(a *txtar.Archive) { a.Files[0].Name = "other" }, 127 + func(a *txtar.Archive) { a.Files = nil }, 128 + } { 129 + a := txtar.Parse([]byte(input)) 130 + if n := len(a.Files); n != 1 { 131 + t.Fatalf("txtar.Parse(%q) got %d files; expected 1", input, n) 132 + } 133 + 134 + fsys, err := txtar.FS(a) 135 + if err != nil { 136 + t.Fatal(err) 137 + } 138 + 139 + // Confirm we can open "one.txt". 140 + _, err = fsys.Open("one.txt") 141 + if err != nil { 142 + t.Fatal(err) 143 + } 144 + // Modify a to get ErrModified when opening "one.txt". 145 + mod(a) 146 + 147 + _, err = fsys.Open("one.txt") 148 + if err != txtar.ErrModified { 149 + t.Errorf("Open(%q) got error %s; want ErrModified", "one.txt", err) 150 + } 151 + } 152 +} 153 + 154 +func TestReadFile(t *testing.T) { 155 + const input = ` 156 +-- 1/one.txt -- 157 +one 158 +` 159 + a := txtar.Parse([]byte(input)) 160 + fsys, err := txtar.FS(a) 161 + if err != nil { 162 + t.Fatal(err) 163 + } 164 + readfs := fsys.(fs.ReadFileFS) 165 + _, err = readfs.ReadFile("1") 166 + if err == nil { 167 + t.Errorf("ReadFile(%q) succeeded; expected an error when reading a directory", "1") 168 + } 169 + 170 + content, err := readfs.ReadFile("1/one.txt") 171 + if err != nil { 172 + t.Fatal(err) 173 + } 174 + want := "one\n" 175 + if got := string(content); want != got { 176 + t.Errorf("ReadFile(%q) = %q; want %q", "1/one.txt", got, want) 177 + } 178 +}
M
journal/lexer/lexer_test.go
··· 8 8 ) 9 9 10 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 -2024/01/02 (ABS) groceries 39 -`}, 40 - {"transaction with virtual accounts", `2024/01/01 * groceries 41 - (virtual:account) 1 PESO 42 - [some thing:else] 5 PESO 43 -`}, 44 - {"transaction with unicode commodity symbols", `2024/01/01 groceries 45 - expenses:food €10.00 46 - expenses:food £5.00 47 - expenses:food ₹700.00 48 - expenses:food 40.00 гривні 49 - assets:cash 50 -`}, 51 - {"special chars in description", ` 52 -2024/01/01 * groceries + water| 1 + 2 53 -2024/01/01 groceries * water | 1 * 2 54 -2024/01/01 groceries ! water | 1 ! 2 55 -2024/01/01 groceries # water | 1 # 2 56 -2024/01/01 groceries % water | 1 % 2 57 -2024/01/01 groceries ^ water | 1 ^ 2 58 -2024/01/01 groceries & water | 1 & 2 59 -2024/01/01 groceries ( water | 1 ( 2 60 -2024/01/01 groceries ) water | 1 ) 2 61 -2024/01/01 groceries [ water | 1 [ 2 62 -2024/01/01 groceries ] water | 1 ] 2 63 -2024/01/01 groceries { water | 1 { 2 64 -2024/01/01 groceries } water | 1 } 2 65 -2026-06-07 * payee !one | something *important* 66 - expenses:food 40.00 67 - assets:cash 68 -`}, 69 - {"date with secondary", `2024/01/01=2024/01/02 groceries`}, 70 - {"better date", `2024-01-02`}, 71 - {"comment line", `; this is a comment`}, 72 - {"star comment", `* this is a comment`}, 73 - {"hash comment", `# this is a comment`}, 74 - {"account directive", `account expenses:food`}, 75 - {"commodity directive", `commodity 1,000.00 UAH`}, 76 - {"market price directive", "P 2024-01-01 USD 40.50 UAH\n"}, 77 - {"market price directive with time", "P 2024-01-01 12:00:00 USD 40.50 UAH\n"}, 78 - {"inline comment", `2024/01/01 groceries ; a note`}, 79 - {"empty", ``}, 80 - {"blank lines", "\n\n\n"}, 81 - {"comment block directive", "comment\ncontent\nend\n"}, 82 - {"comment block directive without end", "comment\ncontent\n"}, 11 + tests := []string{ 12 + "account directive", 13 + "automated transaction", 14 + "better date", 15 + "blank lines", 16 + "cleared transaction", 17 + "comment block directive without end", 18 + "comment block directive", 19 + "comment line", 20 + "commodity directive", 21 + "date with secondary", 22 + "empty", 23 + "hash comment", 24 + "inline comment", 25 + "market price directive with time", 26 + "market price directive", 27 + "simple transaction", 28 + "special chars in description", 29 + "star comment", 30 + "transaction with code", 31 + "transaction with unicode commodity symbols", 32 + "transaction with virtual accounts", 33 + "transaction, accounts with uppercase latters", 83 34 } 84 35 for _, tt := range tests { 85 - t.Run(tt.name, func(t *testing.T) { 86 - l := New("j", []byte(tt.input)) 87 - golden.Assert(t, tt.name, l.Dump()) 36 + t.Run(tt, func(t *testing.T) { 37 + a := golden.Read(t, tt) 38 + golden.Assert(t, a, New("j", a.Get("input")).Dump()) 88 39 }) 89 40 } 90 41 } 91 42 92 -// token category bounds, ensures fuzzer never sees out-of-range token types. 93 -const maxKnownTokenType = token.N 43 +func FuzzLexer(f *testing.F) { 44 + const maxKnownTokenType = token.C // ensures fuzzer never sees out-of-range token types 94 45 95 -func FuzzLexer(f *testing.F) { 96 46 f.Add([]byte("2024/01/01 groceries\n expenses:food $10.00\n assets:checking\n")) 97 47 f.Add([]byte("2024/01/01 * groceries\n expenses:food $10.00\n assets:checking\n")) 98 48 f.Add([]byte("2024/01/01 ! groceries\n expenses:food $10.00\n assets:checking\n"))
M
journal/lexer/testdata/automated_transaction.golden
→ journal/lexer/testdata/automated_transaction.txtar
··· 1 +-- input -- 2 += ^income 3 + (liabilities:tax) *.33 4 + 5 += expenses:gifts 6 + budget:gifts (amount * -1) 7 +-- expect -- 1 8 EQ "=" 1:1-1:2 2 9 WHITESPACE " " 1:2-1:3 3 10 TEXT "^income" 1:3-2:0
D
journal/lexer/testdata/blank_lines.golden
··· 1 -NEWLINE "\n" 2:0-3:0 2 -NEWLINE "\n" 3:0-4:0 3 -NEWLINE "\n" 4:0-4:1 4 -EOF "" 4:1-4:1
D
journal/lexer/testdata/comment_line.golden
··· 1 -SEMICOLON ";" 1:1-1:2 2 -TEXT "this is a comment" 1:3-1:20 3 -NEWLINE "\n" 1:20-1:21 4 -EOF "" 1:21-1:21
A
journal/lexer/testdata/commodity_directive.txtar
··· 1 +-- input -- 2 +commodity 1,000.00 UAH 3 +-- expect -- 4 +COMMODITY "commodity" 1:1-1:10 5 +WHITESPACE " " 1:10-1:11 6 +DECIMAL "1,000.00" 1:11-1:19 7 +WHITESPACE " " 1:19-1:20 8 +COMMODITYMARK "UAH" 1:20-2:0 9 +NEWLINE "\n" 2:0-2:1 10 +EOF "" 2:1-2:1
A
journal/lexer/testdata/date_with_secondary.txtar
··· 1 +-- input -- 2 +2024/01/01=2024/01/02 groceries 3 +-- expect -- 4 +DATE "2024/01/01" 1:1-1:11 5 +EQ "=" 1:11-1:12 6 +DATE "2024/01/02" 1:12-1:22 7 +WHITESPACE " " 1:22-1:23 8 +TEXT "groceries" 1:23-2:0 9 +NEWLINE "\n" 2:0-2:1 10 +EOF "" 2:1-2:1
M
journal/lexer/testdata/empty.golden
→ journal/lexer/testdata/empty.txtar
··· 1 +-- input -- 2 +-- expect -- 1 3 EOF "" 1:1-1:1
D
journal/lexer/testdata/hash_comment.golden
··· 1 -SEMICOLON "#" 1:1-1:2 2 -TEXT "this is a comment" 1:3-1:20 3 -NEWLINE "\n" 1:20-1:21 4 -EOF "" 1:21-1:21
A
journal/lexer/testdata/inline_comment.txtar
··· 1 +-- input -- 2 +2024/01/01 groceries ; a note 3 +-- expect -- 4 +DATE "2024/01/01" 1:1-1:11 5 +WHITESPACE " " 1:11-1:12 6 +TEXT "groceries" 1:12-1:21 7 +WHITESPACE " " 1:21-1:22 8 +SEMICOLON ";" 1:22-1:23 9 +TEXT "a note" 1:24-2:0 10 +NEWLINE "\n" 2:0-2:1 11 +EOF "" 2:1-2:1
M
journal/lexer/testdata/special_chars_in_description.golden
→ journal/lexer/testdata/special_chars_in_description.txtar
··· 1 +-- input -- 2 + 3 +2024/01/01 * groceries + water| 1 + 2 4 +2024/01/01 groceries * water | 1 * 2 5 +2024/01/01 groceries ! water | 1 ! 2 6 +2024/01/01 groceries # water | 1 # 2 7 +2024/01/01 groceries % water | 1 % 2 8 +2024/01/01 groceries ^ water | 1 ^ 2 9 +2024/01/01 groceries & water | 1 & 2 10 +2024/01/01 groceries ( water | 1 ( 2 11 +2024/01/01 groceries ) water | 1 ) 2 12 +2024/01/01 groceries [ water | 1 [ 2 13 +2024/01/01 groceries ] water | 1 ] 2 14 +2024/01/01 groceries { water | 1 { 2 15 +2024/01/01 groceries } water | 1 } 2 16 +2026-06-07 * payee !one | something *important* 17 + expenses:food 40.00 18 + assets:cash 19 +-- expect -- 1 20 NEWLINE "\n" 2:0-2:1 2 21 DATE "2024/01/01" 2:1-2:11 3 22 WHITESPACE " " 2:11-2:12
D
journal/lexer/testdata/star_comment.golden
··· 1 -STAR "*" 1:1-1:2 2 -TEXT "this is a comment" 1:3-1:20 3 -NEWLINE "\n" 1:20-1:21 4 -EOF "" 1:21-1:21
M
journal/lexer/testdata/transaction,_accounts_with_uppercase_latters.golden
→ journal/lexer/testdata/transaction,_accounts_with_uppercase_latters.txtar
··· 1 +-- input -- 2 + 3 +2011/01/27 Book Store 4 + Expenses:Books $20.00 5 + Liabilities:MasterCard 6 +-- expect -- 1 7 NEWLINE "\n" 2:0-2:1 2 8 DATE "2011/01/27" 2:1-2:11 3 9 WHITESPACE " " 2:11-2:12
M
journal/lexer/testdata/transaction_with_code.golden
→ journal/lexer/testdata/transaction_with_code.txtar
··· 1 +-- input -- 2 +2024/01/01 (123) groceries 3 + expenses:food $10.00 4 + assets:checking 5 + 6 +2024/01/02 (ABS) groceries 7 +-- expect -- 1 8 DATE "2024/01/01" 1:1-1:11 2 9 WHITESPACE " " 1:11-1:12 3 10 TEXT "(123)" 1:12-1:17
M
journal/lexer/testdata/transaction_with_unicode_commodity_symbols.golden
→ journal/lexer/testdata/transaction_with_unicode_commodity_symbols.txtar
··· 1 +-- input -- 2 +2024/01/01 groceries 3 + expenses:food €10.00 4 + expenses:food £5.00 5 + expenses:food ₹700.00 6 + expenses:food 40.00 гривні 7 + assets:cash 8 +-- expect -- 1 9 DATE "2024/01/01" 1:1-1:11 2 10 WHITESPACE " " 1:11-1:12 3 11 TEXT "groceries" 1:12-2:0
M
journal/lexer/testdata/transaction_with_virtual_accounts.golden
→ journal/lexer/testdata/transaction_with_virtual_accounts.txtar
··· 1 +-- input -- 2 +2024/01/01 * groceries 3 + (virtual:account) 1 PESO 4 + [some thing:else] 5 PESO 5 +-- expect -- 1 6 DATE "2024/01/01" 1:1-1:11 2 7 WHITESPACE " " 1:11-1:12 3 8 STAR "*" 1:12-1:13
M
journal/loader.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 + "io/fs" 5 6 "os" 7 + "path" 6 8 "path/filepath" 7 9 "slices" 8 10 "strings" ··· 21 23 FileErrors []*ast.FileError 22 24 } 23 25 26 +// CollectFiles returns root and all its transitive includes. 27 +func CollectFiles(root *ParsedFile) []*ParsedFile { 28 + var files []*ParsedFile 29 + visited := make(map[*ParsedFile]bool) 30 + var walk func(*ParsedFile) 31 + walk = func(pf *ParsedFile) { 32 + if visited[pf] { 33 + return 34 + } 35 + visited[pf] = true 36 + files = append(files, pf) 37 + for _, inc := range pf.Includes { 38 + walk(inc) 39 + } 40 + } 41 + walk(root) 42 + return files 43 +} 44 + 24 45 type Loader struct { 25 - files map[string]*ParsedFile // key is absolute path 46 + files map[string]*ParsedFile // key is absolute path (OS) or FS-relative path 47 + fsys fs.FS // set during LoadFS 26 48 } 27 49 28 50 func NewLoader() *Loader { 29 - return &Loader{make(map[string]*ParsedFile)} 51 + return &Loader{files: make(map[string]*ParsedFile)} 30 52 } 31 53 32 54 func (l *Loader) Load(fpath string) (*ParsedFile, error) { 33 55 return l.loadFile(fpath, nil) 34 56 } 35 57 58 +// LoadFS loads a journal from an [fs.FS], resolving includes through the same filesystem. 59 +func (l *Loader) LoadFS(fsys fs.FS, fpath string) (*ParsedFile, error) { 60 + l.fsys = fsys 61 + defer func() { l.fsys = nil }() 62 + return l.loadFile(fpath, nil) 63 +} 64 + 36 65 func (l *Loader) LoadBytes(path string, src []byte) (*ParsedFile, error) { 37 66 return l.loadBytes(path, src, nil) 38 67 } ··· 63 92 return roots 64 93 } 65 94 66 -// CollectFiles returns root and all its transitive includes. 67 -func CollectFiles(root *ParsedFile) []*ParsedFile { 68 - var files []*ParsedFile 69 - visited := make(map[*ParsedFile]bool) 70 - var walk func(*ParsedFile) 71 - walk = func(pf *ParsedFile) { 72 - if visited[pf] { 73 - return 74 - } 75 - visited[pf] = true 76 - files = append(files, pf) 77 - for _, inc := range pf.Includes { 78 - walk(inc) 79 - } 80 - } 81 - walk(root) 82 - return files 83 -} 84 - 85 95 // Ordered returns all files in dependency order (included before includer) 86 96 func (l *Loader) Ordered() []*ParsedFile { 87 97 visited := make(map[string]bool) ··· 104 114 } 105 115 106 116 func (l *Loader) loadFile(fpath string, stack []string) (*ParsedFile, error) { 117 + if l.fsys != nil { 118 + fpath = path.Clean(fpath) 119 + if pf, ok := l.files[fpath]; ok { 120 + return pf, nil 121 + } 122 + src, err := fs.ReadFile(l.fsys, fpath) 123 + if err != nil { 124 + return nil, err 125 + } 126 + return l.loadBytes(fpath, src, stack) 127 + } 128 + 107 129 abs, err := filepath.Abs(fpath) 108 130 if err != nil { 109 131 return nil, err 110 132 } 111 - 112 - // reuse already loaded 113 133 if pf, ok := l.files[abs]; ok { 114 134 return pf, nil 115 135 } 116 - 117 136 src, err := os.ReadFile(abs) 118 137 if err != nil { 119 138 return nil, err 120 139 } 121 - 122 140 return l.loadBytes(abs, src, stack) 123 141 } 124 142 125 -func (l *Loader) loadBytes(path string, src []byte, stack []string) (*ParsedFile, error) { 126 - abs, err := filepath.Abs(path) 127 - if err != nil { 128 - return nil, err 143 +func (l *Loader) loadBytes(pathStr string, src []byte, stack []string) (*ParsedFile, error) { 144 + var fpath string 145 + if l.fsys != nil { 146 + fpath = path.Clean(pathStr) 147 + } else { 148 + abs, err := filepath.Abs(pathStr) 149 + if err != nil { 150 + return nil, err 151 + } 152 + fpath = abs 129 153 } 130 154 131 - // cycle includes 132 - if slices.Contains(stack, abs) { 133 - return nil, fmt.Errorf("include cycle: %s", strings.Join(append(stack, abs), " → ")) 155 + if slices.Contains(stack, fpath) { 156 + return nil, fmt.Errorf("include cycle: %s", strings.Join(append(stack, fpath), " → ")) 134 157 } 135 - 136 - // reuse already loaded 137 - if pf, ok := l.files[abs]; ok { 158 + if pf, ok := l.files[fpath]; ok { 138 159 return pf, nil 139 160 } 140 161 141 - lex := lexer.New(abs, src) 162 + lex := lexer.New(fpath, src) 142 163 par := parser.New(lex) 143 164 j := par.ParseJournal() 144 165 145 166 pf := &ParsedFile{ 146 - Path: abs, 167 + Path: fpath, 147 168 Src: src, 148 169 Ast: j, 149 170 Includes: []*ParsedFile{}, 150 171 Errors: j.Errors, 151 172 } 152 - l.files[abs] = pf 173 + l.files[fpath] = pf 153 174 154 175 for _, entry := range j.Entries { 155 176 inc, ok := entry.(*ast.IncludeDirective) ··· 157 178 continue 158 179 } 159 180 160 - incPath := filepath.Join(filepath.Dir(abs), inc.Path) 181 + var incPath string 182 + var matches []string 183 + var err error 184 + if l.fsys != nil { 185 + incPath = path.Join(path.Dir(fpath), inc.Path) 186 + matches, err = fs.Glob(l.fsys, incPath) 187 + } else { 188 + incPath = filepath.Join(filepath.Dir(fpath), inc.Path) 189 + matches, err = filepath.Glob(incPath) 190 + } 161 191 162 - matches, err := filepath.Glob(incPath) 163 192 if err != nil || len(matches) == 0 { 164 193 pf.FileErrors = append(pf.FileErrors, &ast.FileError{ 165 194 Path: incPath, ··· 170 199 } 171 200 172 201 for _, match := range matches { 173 - child, err := l.loadFile(match, append(stack, abs)) 202 + child, err := l.loadFile(match, append(stack, fpath)) 174 203 if err != nil { 175 204 pf.FileErrors = append(pf.FileErrors, &ast.FileError{ 176 205 Path: match,
M
journal/parser/parser_test.go
··· 9 9 ) 10 10 11 11 func TestParser_ParseFile(t *testing.T) { 12 - tests := []struct{ name, inp string }{ 13 - {"blank line", "\n"}, 14 - {"comment semicolon", "; a comment\n"}, 15 - {"comment hash", "# a comment\n"}, 16 - {"comment percent", "% a comment\n"}, 17 - {"comment star", "* a comment\n"}, 18 - {"alias directive", "alias checking = assets:bank:checking\n"}, 19 - {"tag directive", "tag project-xyz\n"}, 20 - {"year directive", "year 1488\n"}, 21 - {"decimal-mark directive", "decimal-mark ,\n"}, 22 - {"C directive", "C 1.00s = 100c\n"}, 23 - {"D directive", `D $1.00 24 -D 10 UAH 25 -`}, 26 - {"P directive", "P 2024/01/01 USD 40.50 UAH\n"}, 27 - {"P directive with time", "P 2024-01-01 12:00:00 USD 40.50 UAH\n"}, 28 - {"N directive", "N $\n"}, 29 - {"inclue directive", "include ./path-to.journal\n"}, 30 - {"inclue directive with comment", "include ./path.journal ; something importnat\n"}, 31 - {"apply tag directive", "apply tag hashtag\n"}, 32 - {"apply fixed directive", "apply fixed CAD $0.90\n"}, 33 - {"end apply directive", "end apply tag\n"}, 34 - {"comment directive", "comment\nsome text\nend comment\n"}, 35 - {"comment directive end alone", "comment\nsome text\nend\n"}, 36 - {"comment directive with header", "comment tag:hidden\nstuff\nend\n"}, 37 - {"empty comment block", "comment\nend\n"}, 38 - {"never ending comment directive", "comment\nsome text\n"}, 39 - {"nested apply tag directives", `apply tag hashtag 40 -apply tag nestedtag: true 41 -2011/01/27 Book Store 42 - expenses:books $20.00 43 - liabilities:mastercard 44 -end apply tag 45 -end apply tag 46 -`}, 47 - {"account directive", "account expenses:food\n"}, 48 - {"account directive with comment", "account expenses:food ; my account\n"}, 49 - {"account with subdirectives", `account expenses:food 50 - note some note 51 - alias food ; this gets ignored 52 -`}, 53 - {"comodity directive", "commodity $\n"}, 54 - {"comodity directive word", "commodity UAH\n"}, 55 - {"comodity directive no space", "commodity $1000.00\n"}, 56 - {"commodity quantity first", "commodity 1,000.00 UAH\n"}, 57 - {"commodity quantity after", "commodity UAH 1,000.00\n"}, 58 - {"commodity with subdirectives", `commodity UAH 59 - format 1 000.00 UAH 60 - note Божествена Гривня ; this gets ignored 61 -`}, 62 - {"payee directive", `payee grocery store 63 -payee 'grocery store 3' 64 -payee "grocery store 2" 65 -payee grocery store 1 66 -`}, 67 - {"transaction", "2024/01/01\n"}, 68 - {"automated transaction", `= ^income 69 - (liabilities:tax) *.33 70 - 71 -= expenses:gifts 72 - budget:gifts *-1 73 - assets:budget *1 74 - 75 -= income:salary 76 - budget:savings (amount * 0.5) 77 -`}, 78 - {"transaction with payee", "2024-01-01 groceries\n"}, 79 - {"transaction with digit payees", `2002/01/01 * 1a1a6305d06ce4b284dba0d267c23f69d70c20be 80 - af0628973ff35bd62ddb048fa41dd8d83c1c46fe $474.31 81 - fc6f6f10f627ad1a5af9d488c98405a1498d019d 82 - 83 -2002/03/01 * 9861ce541c17b11f627e71c26bf350b33141f62b 84 - 0ecbb1b15e2cf3e515cc0f8533e5bb0fb2326728 $14.91 85 - fc6f6f10f627ad1a5af9d488c98405a1498d019d 86 -`}, 87 - {"account with spaces", `2026-05-11 testies 88 - expenses:account name 20.00 89 - assets:bank 90 -`}, 91 - {"transaction with multiword payee", "2024-01-01 opening balances\n"}, 92 - {"transaction pending", "2024-01-01 ! groceries\n"}, 93 - {"transaction clearerd", "2024-01-01 * groceries\n"}, 94 - {"transaction with note", "2024-01-01 groceries | eating out\n"}, 95 - {"transaction with comment", "2024-01-01 groceries ; note\n"}, 96 - {"transaction with secondary date", "2024/01/01=2024/01/02 groceries\n"}, 97 - {"transaction with costs", `2026-05-11 testies 98 - expenses:atm 20.00 UAH @ 1 USD 99 - assets:bank 100 - 101 -2026-05-11 testies2 102 - expenses:atm 20.00 UAH @@ 1 USD 103 - assets:bank 104 - 105 -2026-05-12 testies3 106 - expenses:atm 20.00 UAH @ 1 USD = 20.00 UAH @ 1 USD 107 - assets:bank 108 - 109 -2015-01-03 money exchange office 110 - assets:cash -20 EUR @ 7.53 HRK 111 - assets:cash 150.60 HRK 112 -`}, 113 - {"transaction with cost and assertion", `2026-05-11 testies 114 - expenses:atm UAH 20.00 @ 1 USD = 0 UAH 115 - assets:bank 116 -`}, 117 - {"transaction with posting", `2024-01-01 groceries 118 - expenses:food $10.00 119 - assets:checking 120 -`}, 121 - {"transaction with unicode commodity symbols", `2024-01-01 groceries 122 - expenses:food €10.00 123 - expenses:food £5.00 124 - expenses:food ₹700.00 125 - assets:checking 126 -`}, 127 - {"transaction with quoted commodity", `2025-06-16 groceries 128 - expenses:food 10.00 "EUR" 129 - assets:cash 130 -`}, 131 - {"transaction with strange commodity symbols", `2024-01-01 groceries 132 -2026-05-20 133 - asdf 123 $€£ 134 - asdf2 135 - 136 -2026-05-20 137 - asdf 123 bytes 138 - asdf2 139 -`}, 140 - {"special chars in description", ` 141 -2024/01/01 groceries | 1 + 2 142 -2024/01/01 groceries | 1 * 2 143 -2024/01/01 groceries | 1 ! 2 144 -2024/01/01 groceries | 1 # 2 145 -2024/01/01 groceries | 1 % 2 146 -2024/01/01 groceries | 1 ^ 2 147 -2024/01/01 groceries | 1 & 2 148 -2024/01/01 groceries | 1 ( 2 149 -2024/01/01 groceries | 1 ) 2 150 -2024/01/01 groceries | 1 [ 2 151 -2024/01/01 groceries | 1 ] 2 152 -2024/01/01 groceries | 1 { 2 153 -2024/01/01 groceries | 1 } 2 154 -2026-06-07 * payee !one | something *important* 155 - expenses:food 40.00 156 - assets:cash 157 -`}, 158 - {"transaction with tabs", `2024-01-01 groceries 159 - expenses:food $10.00 160 - assets:checking 161 -`}, 162 - {"transaction in ukrainian", `2024/03/02 Обід 163 - витрати:їжа 350 UAH 164 - активи:готівка 165 -`}, 166 - {"transaction with code", `2024-01-01 (123) groceries 167 - expenses:food $10.00 168 - assets:checking 169 -`}, 170 - {"transaction with posting amounts", `2024.01.01 groceries 171 - expenses:food 10.00 UAH 172 - assets:checking -10.00 UAH 173 -`}, 174 - {"transaction with spaced account name", `2022-01-01 opening balances 175 - assets 21 = 21 176 - equity:opening/closing balances 177 -`}, 178 - {"transaction with inline comment", `2024/01/01 groceries 179 - Expenses:Good $10.00 ; food 180 - Assets:Checking 181 -`}, 182 - {"transaction with header comment", `2024/01/01 groceries 183 - ; header comment 184 - expenses:food $10.00 185 - assets:checking 186 -`}, 187 - {"transaction with trailing indent", ` 188 -2013/1/1 * pay taxes 189 - expenses:personal:tax $1250 190 - assets:bank:checking $-1250 191 - 192 -`}, 193 - {"transaction with balance assertion", `2024/01/01 groceries 194 - expenses:food $10.00 = $100.00 195 - assets:checking 196 - 197 -2025/03/07 groceries2 198 - expenses:food $10.00 == $100.00 199 - assets:checking == $0.00 200 - 201 -2025/04/08 groceries3 202 - expenses:food $10.00 203 - assets:checking = $0.00 204 -`}, 205 - {"transaction with virtual accounts", `2024/01/01 groceries 206 - (virtual:account) 1 PESO 207 - [something:else] 5 PESO 208 - something:else 209 -`}, 210 - {"virtual postings with statuses", `2024/01/01 test 211 - ! (assets:cash) $10 212 - (income:gift) $-10 213 - 214 -2024/01/01 test 215 - (! assets:cash) $10 216 - (income:gift) $-10 217 - 218 -2024/01/01 test 219 - ! (! assets:cash) $10 220 - (* income:gift) $-10 221 -`}, 222 - {"period transaction expressions", ` 223 -~ monthly 224 - expenses:rent $2000 225 - assets:bank:checking 226 - 227 -~ monthly from 2023-04-15 to 2023-06-16 228 - expenses:utilities $400 229 - assets:bank:checking 230 - 231 -~ every 2 months in 2023, we will review 232 - expenses:utilities $400 233 - assets:bank:checking 234 - 235 -~ monthly Next year blah blah 236 - expenses:food $100 237 - assets:checking 238 - 239 -~ monthly from 2018/6 ;In 2019 we will change this 240 - expenses:food $100 241 - assets:checking 242 -`}, 243 - {"unexpected token", `@@@ garbage\n`}, 244 - {"recovery after bad posting", `2024-01-01 groceries 245 - @@@invalid 246 - assets:checking 247 - 248 -2024/01/02 salary 249 - income:salary $1000 250 - assets:checking 251 -`}, 252 - {"illegal only", "@@@\n"}, 253 - {"illegal at start", "@@@ garbage\n"}, 254 - {"illegal in posting", `2024/01/01 groceries 255 - @@@invalid 256 - assets:checking 257 -`}, 258 - {"illegal between transactions", `2024/01/01 groceries 259 - expenses:food $10 260 - assets:checking 261 -@@@ 262 -2024/01/02 salary 263 - income:salary $1000 264 - assets:checking 265 -`}, 266 - {"multiple bad postings", `2024/01/01 groceries 267 - expenses:food $10 268 - @@@bad1 269 - @bad2 270 - assets:checking 271 -`}, 272 - {"three bad postings", `2024/01/01 groceries 273 - expenses:food $10 274 - @@@bad1 275 - @bad2 276 - @bad3 277 - assets:checking 278 -`}, 279 - {"quoted payee names", `2024-01-01 "groceries store" 280 - expenses:food $10 281 - assets:checking 282 -`}, 283 - {"digit group marks", `2024-01-01 groceries 284 - expenses:food 1 000.00 UAH 285 - expenses:supplies 1'000.00 USD 286 - assets:checking -2_000.00 USD 287 -`}, 288 - {"directive prefixes", `!account expenses:food 289 -@commodity USD 290 -`}, 291 - {"bad between good", `2024/01/01 groceries 292 - expenses:food $10 293 - @@@bad 294 - assets:cash $5 295 -`}, 296 - {"all postings bad", `2024/01/01 groceries 297 - @@@bad1 298 - @bad2 299 - @bad3 300 -`}, 301 - {"bad then next transaction", `2024/01/01 groceries 302 - expenses:food $10 303 - @@@bad 304 - 305 -2024/01/02 salary 306 - income:salary $1000 307 - assets:checking 308 -`}, 309 - {"comment between bad postings", `2024/01/01 groceries 310 - expenses:food $10 311 - @@@bad1 312 - ; a comment 313 - @bad2 314 - assets:checking 315 -`}, 316 - {"bad posting at end", `2024/01/01 groceries 317 - expenses:food $10 318 - @@@bad 319 - 320 -2024/01/02 salary 321 - income:salary $1000 322 - assets:checking 323 -`}, 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", 324 94 } 325 95 for _, tt := range tests { 326 - t.Run(tt.name, func(t *testing.T) { 327 - l := lexer.New("j", []byte(tt.inp)) 96 + t.Run(tt, func(t *testing.T) { 97 + a := golden.Read(t, tt) 98 + l := lexer.New("j", a.Get("input")) 328 99 f := New(l).ParseJournal() 329 - golden.Assert(t, tt.name, ast.Dump(f)) 100 + golden.Assert(t, a, ast.Dump(f)) 330 101 }) 331 102 } 332 103 }
A
journal/parser/testdata/account_with_subdirectives.txtar
··· 1 +-- input -- 2 +account expenses:food 3 + note some note 4 + alias food ; this gets ignored 5 +-- expect -- 6 +Journal 7 + AccountDirective j:1:1-4:1 8 + Account j:1:9-2:0 9 + SubAccount: "expenses" j:1:9-1:17 10 + SubAccount: "food" j:1:18-2:0
D
journal/parser/testdata/alias_directive.golden
··· 1 -Journal 2 - AliasDirective j:1:1-2:1 3 - From: "checking" 4 - To: "assets:bank:checking"
D
journal/parser/testdata/apply_fixed_directive.golden
··· 1 -Journal 2 - ApplyDirective j:1:1-2:1 3 - Expr: "fixed CAD $0.90"
D
journal/parser/testdata/apply_tag_directive.golden
··· 1 -Journal 2 - ApplyDirective j:1:1-2:1 3 - Expr: "tag hashtag"
M
journal/parser/testdata/automated_transaction.golden
→ journal/parser/testdata/automated_transaction.txtar
··· 1 +-- input -- 2 += ^income 3 + (liabilities:tax) *.33 4 + 5 += expenses:gifts 6 + budget:gifts *-1 7 + assets:budget *1 8 + 9 += income:salary 10 + budget:savings (amount * 0.5) 11 +-- expect -- 1 12 Journal 2 13 AutomatedTransaction j:1:1-4:0 3 14 Expr: "^income"
M
journal/parser/testdata/bad_posting_at_end.golden
→ journal/parser/testdata/bad_posting_at_end.txtar
··· 1 +-- input -- 2 +2024/01/01 groceries 3 + expenses:food $10 4 + @@@bad 5 + 6 +2024/01/02 salary 7 + income:salary $1000 8 + assets:checking 9 +-- expect -- 1 10 Journal 2 11 Transaction j:1:1-5:0 3 12 Date: 2024/01/01
M
journal/parser/testdata/bad_then_next_transaction.golden
→ journal/parser/testdata/bad_then_next_transaction.txtar
··· 1 +-- input -- 2 +2024/01/01 groceries 3 + expenses:food $10 4 + @@@bad 5 + 6 +2024/01/02 salary 7 + income:salary $1000 8 + assets:checking 9 +-- expect -- 1 10 Journal 2 11 Transaction j:1:1-5:0 3 12 Date: 2024/01/01
M
journal/parser/testdata/comment_between_bad_postings.golden
→ journal/parser/testdata/comment_between_bad_postings.txtar
··· 1 +-- input -- 2 +2024/01/01 groceries 3 + expenses:food $10 4 + @@@bad1 5 + ; a comment 6 + @bad2 7 + assets:checking 8 +-- expect -- 1 9 Journal 2 10 Transaction j:1:1-7:1 3 11 Date: 2024/01/01
D
journal/parser/testdata/commodity_with_subdirectives.golden
··· 1 -Journal 2 - CommodityDirective j:1:1-4:1 3 - Commodity: "UAH"
D
journal/parser/testdata/decimal-mark_directive.golden
··· 1 -Journal 2 - DecimalMarkDirective j:1:1-2:1 3 - Mark: ","
M
journal/parser/testdata/digit_group_marks.golden
→ journal/parser/testdata/digit_group_marks.txtar
··· 1 +-- input -- 2 +2024-01-01 groceries 3 + expenses:food 1 000.00 UAH 4 + expenses:supplies 1'000.00 USD 5 + assets:checking -2_000.00 USD 6 +-- expect -- 1 7 Journal 2 8 Transaction j:1:1-5:1 3 9 Date: 2024-01-01
D
journal/parser/testdata/end_apply_directive.golden
··· 1 -Journal 2 - EndDirective j:1:1-2:1 3 - Expr: "apply tag"
D
journal/parser/testdata/illegal_at_start.golden
··· 1 -Journal 2 - Errors 3 - j:1:1-1:2: unexpected token AT
M
journal/parser/testdata/illegal_between_transactions.golden
→ journal/parser/testdata/illegal_between_transactions.txtar
··· 1 +-- input -- 2 +2024/01/01 groceries 3 + expenses:food $10 4 + assets:checking 5 +@@@ 6 +2024/01/02 salary 7 + income:salary $1000 8 + assets:checking 9 +-- expect -- 1 10 Journal 2 11 Transaction j:1:1-4:1 3 12 Date: 2024/01/01
D
journal/parser/testdata/illegal_only.golden
··· 1 -Journal 2 - Errors 3 - j:1:1-1:2: unexpected token AT
D
journal/parser/testdata/inclue_directive.golden
··· 1 -Journal 2 - IncludeDirective j:1:1-2:1 3 - Path: "./path-to.journal"
M
journal/parser/testdata/nested_apply_tag_directives.golden
→ journal/parser/testdata/nested_apply_tag_directives.txtar
··· 1 +-- input -- 2 +apply tag hashtag 3 +apply tag nestedtag: true 4 +2011/01/27 Book Store 5 + expenses:books $20.00 6 + liabilities:mastercard 7 +end apply tag 8 +end apply tag 9 +-- expect -- 1 10 Journal 2 11 ApplyDirective j:1:1-2:1 3 12 Expr: "tag hashtag"
M
journal/parser/testdata/payee_directive.golden
→ journal/parser/testdata/payee_directive.txtar
··· 1 +-- input -- 2 +payee grocery store 3 +payee 'grocery store 3' 4 +payee "grocery store 2" 5 +payee grocery store 1 6 +-- expect -- 1 7 Journal 2 8 PayeeDirective j:1:1-2:1 3 9 Name: "grocery store"
M
journal/parser/testdata/period_transaction_expressions.golden
→ journal/parser/testdata/period_transaction_expressions.txtar
··· 1 +-- input -- 2 + 3 +~ monthly 4 + expenses:rent $2000 5 + assets:bank:checking 6 + 7 +~ monthly from 2023-04-15 to 2023-06-16 8 + expenses:utilities $400 9 + assets:bank:checking 10 + 11 +~ every 2 months in 2023, we will review 12 + expenses:utilities $400 13 + assets:bank:checking 14 + 15 +~ monthly Next year blah blah 16 + expenses:food $100 17 + assets:checking 18 + 19 +~ monthly from 2018/6 ;In 2019 we will change this 20 + expenses:food $100 21 + assets:checking 22 +-- expect -- 1 23 Journal 2 24 BlankLine j:2:0-2:1 3 25 PeriodicTransaction j:2:1-6:0
M
journal/parser/testdata/recovery_after_bad_posting.golden
→ journal/parser/testdata/recovery_after_bad_posting.txtar
··· 1 +-- input -- 2 +2024-01-01 groceries 3 + @@@invalid 4 + assets:checking 5 + 6 +2024/01/02 salary 7 + income:salary $1000 8 + assets:checking 9 +-- expect -- 1 10 Journal 2 11 Transaction j:1:1-5:0 3 12 Date: 2024-01-01
M
journal/parser/testdata/special_chars_in_description.golden
→ journal/parser/testdata/special_chars_in_description.txtar
··· 1 +-- input -- 2 + 3 +2024/01/01 groceries | 1 + 2 4 +2024/01/01 groceries | 1 * 2 5 +2024/01/01 groceries | 1 ! 2 6 +2024/01/01 groceries | 1 # 2 7 +2024/01/01 groceries | 1 % 2 8 +2024/01/01 groceries | 1 ^ 2 9 +2024/01/01 groceries | 1 & 2 10 +2024/01/01 groceries | 1 ( 2 11 +2024/01/01 groceries | 1 ) 2 12 +2024/01/01 groceries | 1 [ 2 13 +2024/01/01 groceries | 1 ] 2 14 +2024/01/01 groceries | 1 { 2 15 +2024/01/01 groceries | 1 } 2 16 +2026-06-07 * payee !one | something *important* 17 + expenses:food 40.00 18 + assets:cash 19 +-- expect -- 1 20 Journal 2 21 BlankLine j:2:0-2:1 3 22 Transaction j:2:1-3:1
D
journal/parser/testdata/tag_directive.golden
··· 1 -Journal 2 - TagDirective j:1:1-2:1 3 - Name: "project-xyz"
D
journal/parser/testdata/transaction.golden
··· 1 -Journal 2 - Transaction j:1:1-2:1 3 - Date: 2024/01/01
M
journal/parser/testdata/transaction_with_balance_assertion.golden
→ journal/parser/testdata/transaction_with_balance_assertion.txtar
··· 1 +-- input -- 2 +2024/01/01 groceries 3 + expenses:food $10.00 = $100.00 4 + assets:checking 5 + 6 +2025/03/07 groceries2 7 + expenses:food $10.00 == $100.00 8 + assets:checking == $0.00 9 + 10 +2025/04/08 groceries3 11 + expenses:food $10.00 12 + assets:checking = $0.00 13 +-- expect -- 1 14 Journal 2 15 Transaction j:1:1-5:0 3 16 Date: 2024/01/01
M
journal/parser/testdata/transaction_with_cost_and_assertion.golden
→ journal/parser/testdata/transaction_with_cost_and_assertion.txtar
··· 1 +-- input -- 2 +2026-05-11 testies 3 + expenses:atm UAH 20.00 @ 1 USD = 0 UAH 4 + assets:bank 5 +-- expect -- 1 6 Journal 2 7 Transaction j:1:1-4:1 3 8 Date: 2026-05-11
M
journal/parser/testdata/transaction_with_costs.golden
→ journal/parser/testdata/transaction_with_costs.txtar
··· 1 +-- input -- 2 +2026-05-11 testies 3 + expenses:atm 20.00 UAH @ 1 USD 4 + assets:bank 5 + 6 +2026-05-11 testies2 7 + expenses:atm 20.00 UAH @@ 1 USD 8 + assets:bank 9 + 10 +2026-05-12 testies3 11 + expenses:atm 20.00 UAH @ 1 USD = 20.00 UAH @ 1 USD 12 + assets:bank 13 + 14 +2015-01-03 money exchange office 15 + assets:cash -20 EUR @ 7.53 HRK 16 + assets:cash 150.60 HRK 17 +-- expect -- 1 18 Journal 2 19 Transaction j:1:1-5:0 3 20 Date: 2026-05-11
M
journal/parser/testdata/transaction_with_digit_payees.golden
→ journal/parser/testdata/transaction_with_digit_payees.txtar
··· 1 +-- input -- 2 +2002/01/01 * 1a1a6305d06ce4b284dba0d267c23f69d70c20be 3 + af0628973ff35bd62ddb048fa41dd8d83c1c46fe $474.31 4 + fc6f6f10f627ad1a5af9d488c98405a1498d019d 5 + 6 +2002/03/01 * 9861ce541c17b11f627e71c26bf350b33141f62b 7 + 0ecbb1b15e2cf3e515cc0f8533e5bb0fb2326728 $14.91 8 + fc6f6f10f627ad1a5af9d488c98405a1498d019d 9 +-- expect -- 1 10 Journal 2 11 Transaction j:1:1-5:0 3 12 Date: 2002/01/01
M
journal/parser/testdata/transaction_with_header_comment.golden
→ journal/parser/testdata/transaction_with_header_comment.txtar
··· 1 +-- input -- 2 +2024/01/01 groceries 3 + ; header comment 4 + expenses:food $10.00 5 + assets:checking 6 +-- expect -- 1 7 Journal 2 8 Transaction j:1:1-5:1 3 9 Date: 2024/01/01
M
journal/parser/testdata/transaction_with_posting_amounts.golden
→ journal/parser/testdata/transaction_with_posting_amounts.txtar
··· 1 +-- input -- 2 +2024.01.01 groceries 3 + expenses:food 10.00 UAH 4 + assets:checking -10.00 UAH 5 +-- expect -- 1 6 Journal 2 7 Transaction j:1:1-4:1 3 8 Date: 2024.01.01
M
journal/parser/testdata/transaction_with_spaced_account_name.golden
→ journal/parser/testdata/transaction_with_spaced_account_name.txtar
··· 1 +-- input -- 2 +2022-01-01 opening balances 3 + assets 21 = 21 4 + equity:opening/closing balances 5 +-- expect -- 1 6 Journal 2 7 Transaction j:1:1-4:1 3 8 Date: 2022-01-01
M
journal/parser/testdata/transaction_with_strange_commodity_symbols.golden
→ journal/parser/testdata/transaction_with_strange_commodity_symbols.txtar
··· 1 +-- input -- 2 +2024-01-01 groceries 3 +2026-05-20 4 + asdf 123 $€£ 5 + asdf2 6 + 7 +2026-05-20 8 + asdf 123 bytes 9 + asdf2 10 +-- expect -- 1 11 Journal 2 12 Transaction j:1:1-2:1 3 13 Date: 2024-01-01
M
journal/parser/testdata/transaction_with_trailing_indent.golden
→ journal/parser/testdata/transaction_with_trailing_indent.txtar
··· 1 +-- input -- 2 + 3 +2013/1/1 * pay taxes 4 + expenses:personal:tax $1250 5 + assets:bank:checking $-1250 6 + 7 +-- expect -- 1 8 Journal 2 9 BlankLine j:2:0-2:1 3 10 Transaction j:2:1-6:0
M
journal/parser/testdata/transaction_with_unicode_commodity_symbols.golden
→ journal/parser/testdata/transaction_with_unicode_commodity_symbols.txtar
··· 1 +-- input -- 2 +2024-01-01 groceries 3 + expenses:food €10.00 4 + expenses:food £5.00 5 + expenses:food ₹700.00 6 + assets:checking 7 +-- expect -- 1 8 Journal 2 9 Transaction j:1:1-6:1 3 10 Date: 2024-01-01
M
journal/parser/testdata/transaction_with_virtual_accounts.golden
→ journal/parser/testdata/transaction_with_virtual_accounts.txtar
··· 1 +-- input -- 2 +2024/01/01 groceries 3 + (virtual:account) 1 PESO 4 + [something:else] 5 PESO 5 + something:else 6 +-- expect -- 1 7 Journal 2 8 Transaction j:1:1-5:1 3 9 Date: 2024/01/01
M
journal/parser/testdata/virtual_postings_with_statuses.golden
→ journal/parser/testdata/virtual_postings_with_statuses.txtar
··· 1 +-- input -- 2 +2024/01/01 test 3 + ! (assets:cash) $10 4 + (income:gift) $-10 5 + 6 +2024/01/01 test 7 + (! assets:cash) $10 8 + (income:gift) $-10 9 + 10 +2024/01/01 test 11 + ! (! assets:cash) $10 12 + (* income:gift) $-10 13 +-- expect -- 1 14 Journal 2 15 Transaction j:1:1-5:0 3 16 Date: 2024/01/01
D
journal/parser/testdata/year_directive.golden
··· 1 -Journal 2 - YearDirective j:1:1-2:1 3 - Year: 1488
M
journal/printer/printer_test.go
··· 13 13 func TestRoundTrip(t *testing.T) { 14 14 for _, tname := range tests { 15 15 t.Run(tname, func(t *testing.T) { 16 - inp := golden.Load(t, tname) 17 - 18 - pf, err := journal.NewLoader().LoadBytes(tname+".journal", inp) 16 + a := golden.Read(t, tname) 17 + pf, err := journal.NewLoader().LoadBytes(tname+".journal", a.Get("input")) 19 18 if err != nil { 20 19 t.Fatal(err) 21 20 } ··· 24 23 t.Fatal(err) 25 24 } 26 25 27 - golden.Assert(t, tname, b.String()) 26 + golden.Assert(t, a, b.String()) 28 27 }) 29 28 } 30 29 } ··· 33 32 for _, tname := range tests { 34 33 b.Run(tname, func(b *testing.B) { 35 34 b.ReportAllocs() 36 - inp := golden.Load(b, tname) 35 + inp := golden.Read(b, tname).Get("input") 37 36 pf, err := journal.NewLoader().LoadBytes(tname+".journal", inp) 38 37 if err != nil { 39 38 b.Fatal(err) ··· 61 60 func TestRoundTrip_WithConfig(t *testing.T) { 62 61 for tname, tt := range testsWithConfig { 63 62 t.Run(tname, func(t *testing.T) { 64 - inp := golden.Load(t, tname) 65 - pf, err := journal.NewLoader().LoadBytes(tname+".journal", inp) 63 + a := golden.Read(t, tname) 64 + pf, err := journal.NewLoader().LoadBytes(tname+".journal", a.Get("input")) 66 65 if err != nil { 67 66 t.Fatal(err) 68 67 } ··· 71 70 t.Fatal(err) 72 71 } 73 72 74 - golden.Assert(t, tname, b.String()) 73 + golden.Assert(t, a, b.String()) 75 74 }) 76 75 } 77 76 } 78 77 79 78 func BenchmarkPrinter_Config(b *testing.B) { 80 - inp := golden.Load(b, "entries") 79 + inp := golden.Read(b, "entries").Get("input") 81 80 pf, err := journal.NewLoader().LoadBytes("entries.journal", inp) 82 81 if err != nil { 83 82 b.Fatal(err)
M
journal/printer/testdata/align_right.golden
→ journal/printer/testdata/align_right.txtar
··· 1 +-- input -- 2 +2026-01-15 * Test restaurant 3 + Expenses:FoodAndDrinks 50.00$ 4 + Assets:Cash -50.00$ 5 + 6 +2026-01-16 another entry 7 + Expenses:Food 30.00$ 8 + Assets:Cash -30.00$ 9 + 10 +-- expect -- 1 11 2026-01-15 * Test restaurant 2 12 Expenses:FoodAndDrinks 50.00$ 3 13 Assets:Cash -50.00$
D
journal/printer/testdata/align_tab.golden
··· 1 -2026-01-15 * Test restaurant 2 - Expenses:FoodAndDrinks 50.00$ 3 - Assets:Cash -50.00$ 4 - 5 -2026-01-16 Another entry with longer account 6 - Expenses:SuperMarket 120.50$ 7 - Assets:Cash -120.50$ 8 - 9 -2026-01-17 Short accounts 10 - a 10.00$ 11 - b -10.00$
D
journal/printer/testdata/align_tab.input
··· 1 -2026-01-15 * Test restaurant 2 - Expenses:FoodAndDrinks 50.00$ 3 - Assets:Cash -50.00$ 4 - 5 -2026-01-16 Another entry with longer account 6 - Expenses:SuperMarket 120.50$ 7 - Assets:Cash -120.50$ 8 - 9 -2026-01-17 Short accounts 10 - a 10.00$ 11 - b -10.00$
A
journal/printer/testdata/align_tab.txtar
··· 1 +-- input -- 2 +2026-01-15 * Test restaurant 3 + Expenses:FoodAndDrinks 50.00$ 4 + Assets:Cash -50.00$ 5 + 6 +2026-01-16 Another entry with longer account 7 + Expenses:SuperMarket 120.50$ 8 + Assets:Cash -120.50$ 9 + 10 +2026-01-17 Short accounts 11 + a 10.00$ 12 + b -10.00$ 13 + 14 +-- expect -- 15 +2026-01-15 * Test restaurant 16 + Expenses:FoodAndDrinks 50.00$ 17 + Assets:Cash -50.00$ 18 + 19 +2026-01-16 Another entry with longer account 20 + Expenses:SuperMarket 120.50$ 21 + Assets:Cash -120.50$ 22 + 23 +2026-01-17 Short accounts 24 + a 10.00$ 25 + b -10.00$
D
journal/printer/testdata/basic_twospaces.golden
··· 1 -2026-01-15 * Test restaurant 2 - ; header comment on transaction (before payee) 3 - Expenses:Food 50.00$ 4 - Assets:Cash -50.00$ 5 - ; inline comment on posting 6 - 7 -; A simple comment line 8 - 9 -2026-01-16 another entry 10 - Expenses:Food 30.00$ 11 - Assets:Cash -30.00$
A
journal/printer/testdata/commodity_before.txtar
··· 1 +-- input -- 2 +2026-01-15 * Groceries 3 + Expenses:Food 50.00$ 4 + Assets:Cash 5 + 6 +2026-01-16 Salary 7 + Assets:Cash $2000.00 8 + Income:Salary -2000.00$ 9 + 10 +-- expect -- 11 +2026-01-15 * Groceries 12 + Expenses:Food $50.00 13 + Assets:Cash 14 + 15 +2026-01-16 Salary 16 + Assets:Cash $2000.00 17 + Income:Salary $-2000.00
D
journal/printer/testdata/directives.golden
··· 1 -; Directives 2 -account Assets:Cash ; cash account 3 -commodity $ 4 -commodity UAH 5 -commodity 1000.00 USD 6 -commodity $1000.00 7 -commodity 1,000.00 UAH 8 -include ./other.journal 9 -include ../one../another.journal 10 -alias Assets:Checking = Assets:Cash 11 -payee "Test Payee" 12 -tag important 13 -tag something-else 14 -year 2026 15 -decimal-mark , 16 - 17 -; Market price 18 -P 2026-01-15 12:00:00 GOOG 150.00$ 19 - 20 -; Conversion 21 -C 100.00$ = 85.00€ ; test comment 22 - 23 -; Default commodity 24 -D 1,000.00$ 25 - 26 -; N directive 27 -N $ 28 -; Apply/end 29 -apply account Expenses 30 -end 31 - 32 -; Comment block 33 -comment 34 -Some notes about the file 35 -end
D
journal/printer/testdata/directives.input
··· 1 -; Directives 2 -account Assets:Cash ; cash account 3 -commodity $ 4 -commodity UAH 5 -commodity 1000.00 USD 6 -commodity $1000.00 7 -commodity 1,000.00 UAH 8 -include ./other.journal 9 -include ../one../another.journal 10 -alias Assets:Checking = Assets:Cash 11 -payee "Test Payee" 12 -tag "important" 13 -tag something-else 14 -year 2026 15 -decimal-mark , 16 - 17 -; Market price 18 -P 2026/01/15 12:00:00 GOOG $150.00 19 - 20 -; Conversion 21 -C 100.00$ = 85.00€ ; test comment 22 - 23 -; Default commodity 24 -D $1,000.00 25 - 26 -; N directive 27 -N $ 28 - 29 -; Apply/end 30 -apply account Expenses 31 -end 32 - 33 -; Comment block 34 -comment 35 -Some notes about the file 36 -end comment
A
journal/printer/testdata/directives.txtar
··· 1 +-- input -- 2 +; Directives 3 +account Assets:Cash ; cash account 4 +commodity $ 5 +commodity UAH 6 +commodity 1000.00 USD 7 +commodity $1000.00 8 +commodity 1,000.00 UAH 9 +include ./other.journal 10 +include ../one../another.journal 11 +alias Assets:Checking = Assets:Cash 12 +payee "Test Payee" 13 +tag "important" 14 +tag something-else 15 +year 2026 16 +decimal-mark , 17 + 18 +; Market price 19 +P 2026/01/15 12:00:00 GOOG $150.00 20 + 21 +; Conversion 22 +C 100.00$ = 85.00€ ; test comment 23 + 24 +; Default commodity 25 +D $1,000.00 26 + 27 +; N directive 28 +N $ 29 + 30 +; Apply/end 31 +apply account Expenses 32 +end 33 + 34 +; Comment block 35 +comment 36 +Some notes about the file 37 +end comment 38 + 39 +-- expect -- 40 +; Directives 41 +account Assets:Cash ; cash account 42 +commodity $ 43 +commodity UAH 44 +commodity 1000.00 USD 45 +commodity $1000.00 46 +commodity 1,000.00 UAH 47 +include ./other.journal 48 +include ../one../another.journal 49 +alias Assets:Checking = Assets:Cash 50 +payee "Test Payee" 51 +tag important 52 +tag something-else 53 +year 2026 54 +decimal-mark , 55 + 56 +; Market price 57 +P 2026-01-15 12:00:00 GOOG 150.00$ 58 + 59 +; Conversion 60 +C 100.00$ = 85.00€ ; test comment 61 + 62 +; Default commodity 63 +D 1,000.00$ 64 + 65 +; N directive 66 +N $ 67 +; Apply/end 68 +apply account Expenses 69 +end 70 + 71 +; Comment block 72 +comment 73 +Some notes about the file 74 +end
D
journal/printer/testdata/entries.golden
··· 1 -; i am a comment 2 -;; i am a comment 3 -# i am a comment 4 -* i am a comment 5 -**** i'm a fat comment 6 -% and i am a comment 7 - 8 -2026-01-15 * Test restaurant 9 - ; header comment on transaction (before payee) 10 - Expenses:Food 50.00$ 11 - Assets:Cash -50.00$ 12 - ; inline comment on posting 13 - 14 -; A simple comment line 15 - 16 -2026-01-16 another entry 17 - Expenses:Food 30.00$ 18 - Assets:Cash -30.00$ 19 - 20 -~ monthly * 21 - Expenses:Food 100.00$ 22 - Assets:Cash -100.00$ 23 - 24 -= account:Expenses 25 - Expenses:Food 100.00$ 26 - Assets:Cash -100.00$ 27 - 28 -2026-01-15 * (CODE) Test with code and note | Some note 29 - Expenses:Food 100.00$ 30 - Assets:Cash -100.00$ 31 - 32 -2026-01-15=2026-01-20 * Virtual posting 33 - (Expenses:Food) 100.00$ 34 - [Assets:Cash] -100.00$ 35 - 36 -2026-01-15 * Cost test 37 - Expenses:Food 100.00$ @ 1.50$ 38 - Assets:Cash -100.00$ @@ 1.50$ 39 - ; continuation comment 40 - 41 -2026-02-01 * Balance assertion test 42 - Expenses:Food 50.00$ = 500.00$ 43 - Assets:Cash -50.00$ == 500.00$ 44 - Assets:Total === 0.00$ 45 - 46 -2026-03-01 Multi-posting 47 - Expenses:Food 30.00$ 48 - Expenses:Dining 20.00$ 49 - Assets:Cash -50.00$ 50 - 51 -0000-01-01 test 52 - a 123.00$ 53 - b 54 - 55 -2026-01-15 * Comments test 56 - ; header comment 57 - Expenses:Food 50.00$ 58 - Assets:Cash -50.00$ 59 - ; inline comment
D
journal/printer/testdata/entries.input
··· 1 -; i am a comment 2 -;; i am a comment 3 -# i am a comment 4 -* i am a comment 5 -**** i'm a fat comment 6 -% and i am a comment 7 - 8 -2026/01/15 * Test restaurant 9 - ; header comment on transaction (before payee) 10 - Expenses:Food $50.00 11 - Assets:Cash $-50.00 12 - ; inline comment on posting 13 - 14 -; A simple comment line 15 - 16 -2026/01/16 another entry 17 - Expenses:Food $30.00 18 - Assets:Cash $-30.00 19 - 20 -~ monthly 21 - Expenses:Food $100.00 22 - Assets:Cash $-100.00 23 - 24 -= account:Expenses 25 - Expenses:Food $100.00 26 - Assets:Cash $-100.00 27 - 28 -2026/01/15 * (CODE) Test with code and note | Some note 29 - Expenses:Food $100 30 - Assets:Cash $-100 31 - 32 -2026/01/15=2026/01/20 * Virtual posting 33 - (Expenses:Food) $100.00 34 - [Assets:Cash] $-100.00 35 - 36 -2026/01/15 * Cost test 37 - Expenses:Food $100.00 @ $1.50 38 - Assets:Cash $-100.00 @@ $1.50 39 - ; continuation comment 40 - 41 -2026/02/01 * Balance assertion test 42 - Expenses:Food $50.00 = $500.00 43 - Assets:Cash $-50.00 == $500.00 44 - Assets:Total === $0.00 45 - 46 -2026/03/01 Multi-posting 47 - Expenses:Food $30.00 48 - Expenses:Dining $20.00 49 - Assets:Cash $-50.00 50 - 51 -01-01 test 52 - a $123.00 53 - b 54 - 55 -2026/01/15 * Comments test 56 - ; header comment 57 - Expenses:Food $50.00 58 - Assets:Cash $-50.00 59 - ; inline comment
A
journal/printer/testdata/entries.txtar
··· 1 +-- input -- 2 +; i am a comment 3 +;; i am a comment 4 +# i am a comment 5 +* i am a comment 6 +**** i'm a fat comment 7 +% and i am a comment 8 + 9 +2026/01/15 * Test restaurant 10 + ; header comment on transaction (before payee) 11 + Expenses:Food $50.00 12 + Assets:Cash $-50.00 13 + ; inline comment on posting 14 + 15 +; A simple comment line 16 + 17 +2026/01/16 another entry 18 + Expenses:Food $30.00 19 + Assets:Cash $-30.00 20 + 21 +~ monthly 22 + Expenses:Food $100.00 23 + Assets:Cash $-100.00 24 + 25 += account:Expenses 26 + Expenses:Food $100.00 27 + Assets:Cash $-100.00 28 + 29 +2026/01/15 * (CODE) Test with code and note | Some note 30 + Expenses:Food $100 31 + Assets:Cash $-100 32 + 33 +2026/01/15=2026/01/20 * Virtual posting 34 + (Expenses:Food) $100.00 35 + [Assets:Cash] $-100.00 36 + 37 +2026/01/15 * Cost test 38 + Expenses:Food $100.00 @ $1.50 39 + Assets:Cash $-100.00 @@ $1.50 40 + ; continuation comment 41 + 42 +2026/02/01 * Balance assertion test 43 + Expenses:Food $50.00 = $500.00 44 + Assets:Cash $-50.00 == $500.00 45 + Assets:Total === $0.00 46 + 47 +2026/03/01 Multi-posting 48 + Expenses:Food $30.00 49 + Expenses:Dining $20.00 50 + Assets:Cash $-50.00 51 + 52 +01-01 test 53 + a $123.00 54 + b 55 + 56 +2026/01/15 * Comments test 57 + ; header comment 58 + Expenses:Food $50.00 59 + Assets:Cash $-50.00 60 + ; inline comment 61 + 62 +-- expect -- 63 +; i am a comment 64 +;; i am a comment 65 +# i am a comment 66 +* i am a comment 67 +**** i'm a fat comment 68 +% and i am a comment 69 + 70 +2026-01-15 * Test restaurant 71 + ; header comment on transaction (before payee) 72 + Expenses:Food 50.00$ 73 + Assets:Cash -50.00$ 74 + ; inline comment on posting 75 + 76 +; A simple comment line 77 + 78 +2026-01-16 another entry 79 + Expenses:Food 30.00$ 80 + Assets:Cash -30.00$ 81 + 82 +~ monthly * 83 + Expenses:Food 100.00$ 84 + Assets:Cash -100.00$ 85 + 86 += account:Expenses 87 + Expenses:Food 100.00$ 88 + Assets:Cash -100.00$ 89 + 90 +2026-01-15 * (CODE) Test with code and note | Some note 91 + Expenses:Food 100.00$ 92 + Assets:Cash -100.00$ 93 + 94 +2026-01-15=2026-01-20 * Virtual posting 95 + (Expenses:Food) 100.00$ 96 + [Assets:Cash] -100.00$ 97 + 98 +2026-01-15 * Cost test 99 + Expenses:Food 100.00$ @ 1.50$ 100 + Assets:Cash -100.00$ @@ 1.50$ 101 + ; continuation comment 102 + 103 +2026-02-01 * Balance assertion test 104 + Expenses:Food 50.00$ = 500.00$ 105 + Assets:Cash -50.00$ == 500.00$ 106 + Assets:Total === 0.00$ 107 + 108 +2026-03-01 Multi-posting 109 + Expenses:Food 30.00$ 110 + Expenses:Dining 20.00$ 111 + Assets:Cash -50.00$ 112 + 113 +0000-01-01 test 114 + a 123.00$ 115 + b 116 + 117 +2026-01-15 * Comments test 118 + ; header comment 119 + Expenses:Food 50.00$ 120 + Assets:Cash -50.00$ 121 + ; inline comment
A
journal/printer/testdata/indent_width.txtar
··· 1 +-- input -- 2 +2026-01-15 * Test restaurant 3 + Expenses:Food 50.00$ 4 + Assets:Cash -50.00$ 5 + 6 +2026-01-16 Another entry 7 + Expenses:Food 30.00$ 8 + Assets:Cash -30.00$ 9 + 10 +-- expect -- 11 +2026-01-15 * Test restaurant 12 + Expenses:Food 50.00$ 13 + Assets:Cash -50.00$ 14 + 15 +2026-01-16 Another entry 16 + Expenses:Food 30.00$ 17 + Assets:Cash -30.00$
D
journal/printer/testdata/preserve_blanks.golden
··· 1 -; accounts {{{ 2 -account assets 3 -account assets:bank 4 -account assets:cash 5 -account equity 6 -account income 7 -account income:salary 8 -account income:invenstment 9 -account expenses 10 -account expenses:education 11 -; }}} 12 -; 01 Jan {{{ 13 -2026-01-01 opening balances 14 - assets:bank 123.00 USD 15 - assets:cash 1234.12 USD 16 - equity:open 17 - 18 -2026-01-04 selary 19 - income:salary 543.22$ 20 - assets:bank 21 -; }}} 22 -; 02 Feb {{{ 23 -2026-02-01 internet 24 - expenses:utilities 350.00 UAH 25 - assets:bank 26 - 27 - 28 -2026-02-10 * something | 2 months of *income* 29 - assets:bank 1.50 USD ; jan 30 - assets:bank 1.00 USD ; feb 31 - income:invenstment
D
journal/printer/testdata/preserve_blanks.input
··· 1 -; accounts {{{ 2 -account assets 3 -account assets:bank 4 -account assets:cash 5 -account equity 6 -account income 7 -account income:salary 8 -account income:invenstment 9 -account expenses 10 -account expenses:education 11 -; }}} 12 -; 01 Jan {{{ 13 -2026-01-01 opening balances 14 - assets:bank 123.00 USD 15 - assets:cash 1234.12 USD 16 - equity:open 17 - 18 -2026-01-04 selary 19 - income:salary $543.22 20 - assets:bank 21 -; }}} 22 -; 02 Feb {{{ 23 -2026-02-01 internet 24 - expenses:utilities 350 UAH 25 - assets:bank 26 - 27 - 28 -2026-02-10 * something | 2 months of *income* 29 - assets:bank 1.50 USD ; jan 30 - assets:bank USD 1 ; feb 31 - income:invenstment
A
journal/printer/testdata/preserve_blanks.txtar
··· 1 +-- input -- 2 +; accounts {{{ 3 +account assets 4 +account assets:bank 5 +account assets:cash 6 +account equity 7 +account income 8 +account income:salary 9 +account income:invenstment 10 +account expenses 11 +account expenses:education 12 +; }}} 13 +; 01 Jan {{{ 14 +2026-01-01 opening balances 15 + assets:bank 123.00 USD 16 + assets:cash 1234.12 USD 17 + equity:open 18 + 19 +2026-01-04 selary 20 + income:salary $543.22 21 + assets:bank 22 +; }}} 23 +; 02 Feb {{{ 24 +2026-02-01 internet 25 + expenses:utilities 350 UAH 26 + assets:bank 27 + 28 + 29 +2026-02-10 * something | 2 months of *income* 30 + assets:bank 1.50 USD ; jan 31 + assets:bank USD 1 ; feb 32 + income:invenstment 33 + 34 +-- expect -- 35 +; accounts {{{ 36 +account assets 37 +account assets:bank 38 +account assets:cash 39 +account equity 40 +account income 41 +account income:salary 42 +account income:invenstment 43 +account expenses 44 +account expenses:education 45 +; }}} 46 +; 01 Jan {{{ 47 +2026-01-01 opening balances 48 + assets:bank 123.00 USD 49 + assets:cash 1234.12 USD 50 + equity:open 51 + 52 +2026-01-04 selary 53 + income:salary 543.22$ 54 + assets:bank 55 +; }}} 56 +; 02 Feb {{{ 57 +2026-02-01 internet 58 + expenses:utilities 350.00 UAH 59 + assets:bank 60 + 61 + 62 +2026-02-10 * something | 2 months of *income* 63 + assets:bank 1.50 USD ; jan 64 + assets:bank 1.00 USD ; feb 65 + income:invenstment
D
journal/printer/testdata/sample.golden
··· 1 -; accounts {{{ 2 -account assets 3 -account assets:bank 4 -account assets:cash 5 -account equity 6 -account income 7 -account income:salary 8 -account income:invenstment 9 -account expenses 10 -account expenses:education 11 -; }}} 12 -; 01 Jan {{{ 13 -2026-01-01 opening balances 14 - assets:bank 123.00 USD 15 - assets:cash 1234.12 USD 16 - equity:open 17 - 18 -2026-01-04 selary 19 - income:salary 543.22$ 20 - assets:bank 21 -; }}} 22 -; 02 Feb {{{ 23 -2026-02-01 internet 24 - expenses:utilities 350.00 UAH 25 - assets:bank 26 - 27 -2026-02-10 * something | 2 months of *income* + idk 28 - assets:bank 1.50 USD ; jan 29 - assets:bank 1.00 USD ; feb 30 - income:invenstment
D
journal/printer/testdata/sample.input
··· 1 -; accounts {{{ 2 -account assets 3 -account assets:bank 4 -account assets:cash 5 -account equity 6 -account income 7 -account income:salary 8 -account income:invenstment 9 -account expenses 10 -account expenses:education 11 -; }}} 12 -; 01 Jan {{{ 13 -2026-01-01 opening balances 14 - assets:bank 123.00 USD 15 - assets:cash 1234.12 USD 16 - equity:open 17 - 18 -2026-01-04 selary 19 - income:salary $543.22 20 - assets:bank 21 -; }}} 22 -; 02 Feb {{{ 23 -2026-02-01 internet 24 - expenses:utilities 350 UAH 25 - assets:bank 26 - 27 - 28 -2026-02-10 * something | 2 months of *income* + idk 29 - assets:bank 1.50 USD ; jan 30 - assets:bank USD 1 ; feb 31 - income:invenstment
A
journal/printer/testdata/sample.txtar
··· 1 +-- input -- 2 +; accounts {{{ 3 +account assets 4 +account assets:bank 5 +account assets:cash 6 +account equity 7 +account income 8 +account income:salary 9 +account income:invenstment 10 +account expenses 11 +account expenses:education 12 +; }}} 13 +; 01 Jan {{{ 14 +2026-01-01 opening balances 15 + assets:bank 123.00 USD 16 + assets:cash 1234.12 USD 17 + equity:open 18 + 19 +2026-01-04 selary 20 + income:salary $543.22 21 + assets:bank 22 +; }}} 23 +; 02 Feb {{{ 24 +2026-02-01 internet 25 + expenses:utilities 350 UAH 26 + assets:bank 27 + 28 + 29 +2026-02-10 * something | 2 months of *income* + idk 30 + assets:bank 1.50 USD ; jan 31 + assets:bank USD 1 ; feb 32 + income:invenstment 33 + 34 +-- expect -- 35 +; accounts {{{ 36 +account assets 37 +account assets:bank 38 +account assets:cash 39 +account equity 40 +account income 41 +account income:salary 42 +account income:invenstment 43 +account expenses 44 +account expenses:education 45 +; }}} 46 +; 01 Jan {{{ 47 +2026-01-01 opening balances 48 + assets:bank 123.00 USD 49 + assets:cash 1234.12 USD 50 + equity:open 51 + 52 +2026-01-04 selary 53 + income:salary 543.22$ 54 + assets:bank 55 +; }}} 56 +; 02 Feb {{{ 57 +2026-02-01 internet 58 + expenses:utilities 350.00 UAH 59 + assets:bank 60 + 61 +2026-02-10 * something | 2 months of *income* + idk 62 + assets:bank 1.50 USD ; jan 63 + assets:bank 1.00 USD ; feb 64 + income:invenstment
A
journal/printer/testdata/tab_indent.txtar
··· 1 +-- input -- 2 +2026-01-15 * Test restaurant 3 + Expenses:Food 50.00$ 4 + Assets:Cash -50.00$ 5 + 6 +2026-01-16 Another entry 7 + Expenses:Food 30.00$ 8 + Assets:Cash -30.00$ 9 + 10 +-- expect -- 11 +2026-01-15 * Test restaurant 12 + Expenses:Food 50.00$ 13 + Assets:Cash -50.00$ 14 + 15 +2026-01-16 Another entry 16 + Expenses:Food 30.00$ 17 + Assets:Cash -30.00$
M
journal/semantic/build_test.go
··· 9 9 ) 10 10 11 11 func TestBuild(t *testing.T) { 12 - tests := map[string][]string{ 13 - "empty": {}, 14 - "journal": {"journal"}, 15 - "include": {"include0", "include1"}, 16 - } 12 + tests := []string{"empty", "journal", "include"} 13 + for _, tt := range tests { 14 + t.Run(tt, func(t *testing.T) { 15 + a := golden.Read(t, tt) 16 + fsys, err := a.FS() 17 + if err != nil { 18 + t.Fatal(err) 19 + } 17 20 18 - for name, inputs := range tests { 19 - t.Run(name, func(t *testing.T) { 20 - var files []*journal.ParsedFile 21 - for _, in := range inputs { 22 - l := journal.NewLoader() 23 - pf, err := l.LoadBytes(in+".journal", golden.Load(t, in)) 24 - if err != nil { 25 - t.Fatalf("LoadBytes(%q): %v", in, err) 26 - } 27 - files = append(files, pf) 21 + l := journal.NewLoader() 22 + if _, err := l.LoadFS(fsys, "in.journal"); err != nil { 23 + t.Fatalf("LoadFS: %v", err) 28 24 } 29 - ctx := Build(files) 30 25 31 26 var buf bytes.Buffer 32 - fprint(&buf, ctx) 33 - golden.Assert(t, name, buf.String()) 27 + fprint(&buf, Build(l.Ordered())) 28 + golden.Assert(t, a, buf.String()) 34 29 }) 35 30 } 36 31 }
M
journal/semantic/testdata/include.golden
→ journal/semantic/testdata/include.txtar
··· 1 +-- accounts.journal -- 2 +account expenses:food 3 + 4 +-- in.journal -- 5 +include accounts.journal 6 + 7 +2024/01/01 ! test 8 + expenses:food $10 9 + assets:checking 10 + 11 +-- expect -- 1 12 files (2): 2 - 0: include0.journal 3 - 1: include1.journal 13 + 0: accounts.journal 14 + 1: in.journal 4 15 5 16 commodities (1): 6 17 $
M
journal/semantic/testdata/journal.golden
→ journal/semantic/testdata/journal.txtar
··· 1 +-- in.journal -- 2 +account expenses:food 3 +account expenses:food 4 +account expenses:transport 5 + 6 +commodity $ 7 +commodity $ 8 +commodity EUR 9 + 10 +~ monthly 11 + expenses:transport $50 12 + assets:checking 13 + 14 += expr:big 15 + expenses:food $100 16 + assets:credit 17 + 18 +2024/01/01 groceries 19 + expenses:food $10 20 + assets:checking 21 + 22 +2024/01/02 lunch 23 + expenses:food $5 24 + assets:cash 25 + 26 +2024/01/04 aws 27 + expenses:food 1000000.00 USD 28 + assets:cash 29 + 30 +-- expect -- 1 31 files (1): 2 - 0: journal.journal 32 + 0: in.journal 3 33 4 34 commodities (3): 5 35 $
D
journal/semantic/testdata/journal.input
··· 1 -account expenses:food 2 -account expenses:food 3 -account expenses:transport 4 - 5 -commodity $ 6 -commodity $ 7 -commodity EUR 8 - 9 -~ monthly 10 - expenses:transport $50 11 - assets:checking 12 - 13 -= expr:big 14 - expenses:food $100 15 - assets:credit 16 - 17 -2024/01/01 groceries 18 - expenses:food $10 19 - assets:checking 20 - 21 -2024/01/02 lunch 22 - expenses:food $5 23 - assets:cash 24 - 25 -2024/01/04 aws 26 - expenses:food 1000000.00 USD 27 - assets:cash