AtlatestRepositorysigil-ledger
sigil-ledger / tree / testtest-ledger.sgl
1
;;; Test suite for (ledger)3
(import (sigil test)4
(sigil string)5
(ledger))7
;; ========== Transaction Formatting ==========9
(test-group "format-transaction - basic"10
(test "format simple transaction"11
(let ((txn (journal-transaction12
date: "2026-03-15"13
status: "*"14
description: "Grocery store"15
postings: (list16
(journal-posting17
account: "expenses:food:groceries"18
amount: (journal-amount quantity: 47.23 commodity: "USD"))19
(journal-posting20
account: "assets:checking")))))21
(assert-equal22
"2026-03-15 * Grocery store\n expenses:food:groceries 47.23 USD\n assets:checking"23
(format-transaction txn))))25
(test "format transaction with pending status"26
(let ((txn (journal-transaction27
date: "2026-03-15"28
status: "!"29
description: "Pending purchase"30
postings: (list31
(journal-posting32
account: "expenses:food"33
amount: (journal-amount quantity: 10 commodity: "USD"))34
(journal-posting35
account: "assets:checking")))))36
(assert-equal37
"2026-03-15 ! Pending purchase\n expenses:food 10.00 USD\n assets:checking"38
(format-transaction txn))))40
(test "format transaction with no status"41
(let ((txn (journal-transaction42
date: "2026-03-15"43
description: "Some purchase"44
postings: (list45
(journal-posting46
account: "expenses:food"47
amount: (journal-amount quantity: 10 commodity: "USD"))48
(journal-posting49
account: "assets:checking")))))50
(assert-equal51
"2026-03-15 Some purchase\n expenses:food 10.00 USD\n assets:checking"52
(format-transaction txn)))))54
(test-group "format-transaction - code and description"55
(test "format transaction with code"56
(let ((txn (journal-transaction57
date: "2026-03-15"58
status: "*"59
code: "ref-001"60
description: "Whole Foods | groceries"61
postings: (list62
(journal-posting63
account: "expenses:food"64
amount: (journal-amount quantity: 47.23 commodity: "$"))65
(journal-posting66
account: "assets:checking")))))67
(assert-equal68
"2026-03-15 * (ref-001) Whole Foods | groceries\n expenses:food $47.23\n assets:checking"69
(format-transaction txn)))))71
(test-group "format-transaction - tags"72
(test "format transaction with tags"73
(let ((txn (journal-transaction74
date: "2026-03-15"75
status: "*"76
description: "Dinner"77
tags: (list (cons "trip" "hawaii"))78
postings: (list79
(journal-posting80
account: "expenses:meals"81
amount: (journal-amount quantity: 85 commodity: "$"))82
(journal-posting83
account: "assets:checking")))))84
(assert-equal85
"2026-03-15 * Dinner\n ; trip: hawaii\n expenses:meals $85.00\n assets:checking"86
(format-transaction txn))))88
(test "format posting with inline tags"89
(let ((txn (journal-transaction90
date: "2026-03-15"91
status: "*"92
description: "Test"93
postings: (list94
(journal-posting95
account: "expenses:food"96
amount: (journal-amount quantity: 50 commodity: "USD")97
tags: (list (cons "receipt" "scan-001")))98
(journal-posting99
account: "assets:checking")))))100
(assert-equal101
"2026-03-15 * Test\n expenses:food 50.00 USD ; receipt: scan-001\n assets:checking"102
(format-transaction txn)))))104
(test-group "format-transaction - cost notation"105
(test "format total cost (@@)"106
(let ((txn (journal-transaction107
date: "2026-03-15"108
status: "*"109
description: "Currency conversion"110
postings: (list111
(journal-posting112
account: "assets:wise:eur"113
amount: (journal-amount quantity: -500 commodity: "EUR"))114
(journal-posting115
account: "assets:wise:usd"116
amount: (journal-amount quantity: 540 commodity: "USD")117
cost: (journal-cost118
type: 'total119
amount: (journal-amount quantity: 500 commodity: "EUR")))))))120
(assert-equal121
"2026-03-15 * Currency conversion\n assets:wise:eur -500.00 EUR\n assets:wise:usd 540.00 USD @@ 500.00 EUR"122
(format-transaction txn))))124
(test "format per-unit cost (@)"125
(let ((txn (journal-transaction126
date: "2026-03-15"127
status: "*"128
description: "Currency conversion"129
postings: (list130
(journal-posting131
account: "assets:wise:eur"132
amount: (journal-amount quantity: 100 commodity: "EUR")133
cost: (journal-cost134
type: 'per-unit135
amount: (journal-amount quantity: 1.08 commodity: "USD")))136
(journal-posting137
account: "assets:wise:usd")))))138
(assert-equal139
"2026-03-15 * Currency conversion\n assets:wise:eur 100.00 EUR @ 1.08 USD\n assets:wise:usd"140
(format-transaction txn)))))142
(test-group "format-transaction - balance assertions"143
(test "format balance assertion"144
(let ((txn (journal-transaction145
date: "2026-03-15"146
status: "*"147
description: "Deposit"148
postings: (list149
(journal-posting150
account: "assets:checking"151
amount: (journal-amount quantity: 3000 commodity: "$")152
balance-assertion: (journal-amount quantity: 5000 commodity: "$"))153
(journal-posting154
account: "income:salary")))))155
(assert-equal156
"2026-03-15 * Deposit\n assets:checking $3000.00 = $5000.00\n income:salary"157
(format-transaction txn)))))159
(test-group "format-transaction - balance assertion types"160
(test "format inclusive balance assertion =*"161
(let ((txn (journal-transaction162
date: "2026-03-15"163
status: "*"164
description: "Verify"165
postings: (list166
(journal-posting167
account: "assets:checking"168
amount: (journal-amount quantity: 0 commodity: "$")169
balance-assertion: (journal-amount quantity: 5000 commodity: "$")170
balance-assertion-type: "=*")171
(journal-posting172
account: "income:salary")))))173
(assert-equal174
"2026-03-15 * Verify\n assets:checking $0.00 =* $5000.00\n income:salary"175
(format-transaction txn))))177
(test "format strict balance assertion =="178
(let ((txn (journal-transaction179
date: "2026-03-15"180
status: "*"181
description: "Verify"182
postings: (list183
(journal-posting184
account: "assets:checking"185
amount: (journal-amount quantity: 0 commodity: "$")186
balance-assertion: (journal-amount quantity: 5000 commodity: "$")187
balance-assertion-type: "==")188
(journal-posting189
account: "income:salary")))))190
(assert-equal191
"2026-03-15 * Verify\n assets:checking $0.00 == $5000.00\n income:salary"192
(format-transaction txn)))))194
(test-group "format-transactions"195
(test "format multiple transactions"196
(let ((txns (list197
(journal-transaction198
date: "2026-03-15"199
status: "*"200
description: "Groceries"201
postings: (list202
(journal-posting203
account: "expenses:food"204
amount: (journal-amount quantity: 47.23 commodity: "USD"))205
(journal-posting206
account: "assets:checking")))207
(journal-transaction208
date: "2026-03-16"209
status: "*"210
description: "Gas"211
postings: (list212
(journal-posting213
account: "expenses:transport"214
amount: (journal-amount quantity: 30 commodity: "USD"))215
(journal-posting216
account: "assets:checking"))))))217
(assert-equal218
"2026-03-15 * Groceries\n expenses:food 47.23 USD\n assets:checking\n\n2026-03-16 * Gas\n expenses:transport 30.00 USD\n assets:checking"219
(format-transactions txns)))))221
;; ========== Journal Parsing ==========223
(test-group "parse-journal - basic"224
(test "parse simple transaction"225
(let* ((text "2026-03-15 * Grocery store\n expenses:food $47.23\n assets:checking\n")226
(txns (parse-journal text)))227
(assert-equal 1 (length txns))228
(let ((txn (car txns)))229
(assert-equal "2026-03-15" (journal-transaction-date txn))230
(assert-equal "*" (journal-transaction-status txn))231
(assert-equal "Grocery store" (journal-transaction-description txn))232
(assert-equal 2 (length (journal-transaction-postings txn))))))234
(test "parse transaction with pending status"235
(let* ((text "2026-03-15 ! Pending purchase\n expenses:food $10.00\n assets:checking\n")236
(txns (parse-journal text)))237
(assert-equal "!" (journal-transaction-status (car txns)))))239
(test "parse transaction with no status"240
(let* ((text "2026-03-15 Some purchase\n expenses:food $10.00\n assets:checking\n")241
(txns (parse-journal text)))242
(assert-equal "" (journal-transaction-status (car txns)))243
(assert-equal "Some purchase" (journal-transaction-description (car txns)))))245
(test "parse transaction with code"246
(let* ((text "2026-03-15 * (ref-001) Whole Foods\n expenses:food $47.23\n assets:checking\n")247
(txns (parse-journal text)))248
(let ((txn (car txns)))249
(assert-equal "ref-001" (journal-transaction-code txn))250
(assert-equal "Whole Foods" (journal-transaction-description txn)))))252
(test "parse multiple transactions"253
(let* ((text (str "2026-03-15 * Groceries\n"254
" expenses:food $47.23\n"255
" assets:checking\n"256
"\n"257
"2026-03-16 * Gas\n"258
" expenses:transport $30.00\n"259
" assets:checking\n"))260
(txns (parse-journal text)))261
(assert-equal 2 (length txns))262
(assert-equal "Groceries" (journal-transaction-description (car txns)))263
(assert-equal "Gas" (journal-transaction-description (cadr txns))))))265
(test-group "parse-journal - amounts"266
(test "parse left-symbol commodity"267
(let* ((text "2026-03-15 * Test\n expenses:food $47.23\n assets:checking\n")268
(txns (parse-journal text))269
(posting (car (journal-transaction-postings (car txns)))))270
(assert-equal "$" (journal-amount-commodity (journal-posting-amount posting)))271
(assert-equal 47.23 (journal-amount-quantity (journal-posting-amount posting)))))273
(test "parse right-symbol commodity"274
(let* ((text "2026-03-15 * Test\n assets:wise:eur 100.00 EUR\n assets:checking\n")275
(txns (parse-journal text))276
(posting (car (journal-transaction-postings (car txns)))))277
(assert-equal "EUR" (journal-amount-commodity (journal-posting-amount posting)))278
(assert-equal 100.0 (journal-amount-quantity (journal-posting-amount posting)))))280
(test "parse negative amount"281
(let* ((text "2026-03-15 * Test\n assets:wise:eur -500.00 EUR\n assets:checking\n")282
(txns (parse-journal text))283
(posting (car (journal-transaction-postings (car txns)))))284
(assert-equal -500.0 (journal-amount-quantity (journal-posting-amount posting)))))286
(test "parse negative left-symbol -$50"287
(let* ((text "2026-03-15 * Test\n expenses:food -$50.00\n assets:checking\n")288
(txns (parse-journal text))289
(posting (car (journal-transaction-postings (car txns)))))290
(assert-equal -50.0 (journal-amount-quantity (journal-posting-amount posting)))291
(assert-equal "$" (journal-amount-commodity (journal-posting-amount posting)))))293
(test "parse internal negative $-50"294
(let* ((text "2026-03-15 * Test\n expenses:food $-50.00\n assets:checking\n")295
(txns (parse-journal text))296
(posting (car (journal-transaction-postings (car txns)))))297
(assert-equal -50.0 (journal-amount-quantity (journal-posting-amount posting)))298
(assert-equal "$" (journal-amount-commodity (journal-posting-amount posting)))))300
(test "parse inferred amount posting"301
(let* ((text "2026-03-15 * Test\n expenses:food $50.00\n assets:checking\n")302
(txns (parse-journal text))303
(posting (cadr (journal-transaction-postings (car txns)))))304
(assert-false (journal-posting-amount posting)))))306
(test-group "parse-journal - cost notation"307
(test "parse per-unit cost"308
(let* ((text "2026-03-15 * Test\n assets:wise:eur 100.00 EUR @ 1.08 USD\n assets:wise:usd\n")309
(txns (parse-journal text))310
(posting (car (journal-transaction-postings (car txns))))311
(cost (journal-posting-cost posting)))312
(assert-true (journal-cost? cost))313
(assert-equal 'per-unit (journal-cost-type cost))314
(assert-equal 1.08 (journal-amount-quantity (journal-cost-amount cost)))315
(assert-equal "USD" (journal-amount-commodity (journal-cost-amount cost)))))317
(test "parse total cost"318
(let* ((text "2026-03-15 * Test\n assets:wise:usd 540.00 USD @@ 500.00 EUR\n assets:wise:eur\n")319
(txns (parse-journal text))320
(posting (car (journal-transaction-postings (car txns))))321
(cost (journal-posting-cost posting)))322
(assert-true (journal-cost? cost))323
(assert-equal 'total (journal-cost-type cost))324
(assert-equal 500.0 (journal-amount-quantity (journal-cost-amount cost)))325
(assert-equal "EUR" (journal-amount-commodity (journal-cost-amount cost))))))327
(test-group "parse-journal - comments and tags"328
(test "parse transaction with tag comment"329
(let* ((text "2026-03-15 * Dinner\n ; trip: hawaii\n expenses:meals $85.00\n assets:checking\n")330
(txns (parse-journal text))331
(txn (car txns))332
(tags (journal-transaction-tags txn)))333
(assert-equal 1 (length tags))334
(assert-equal "trip" (caar tags))335
(assert-equal "hawaii" (cdar tags))))337
(test "parse posting with inline comment"338
(let* ((text "2026-03-15 * Test\n expenses:food $50.00 ; receipt: scan-001\n assets:checking\n")339
(txns (parse-journal text))340
(posting (car (journal-transaction-postings (car txns)))))341
(assert-equal 1 (length (journal-posting-tags posting)))342
(assert-equal "receipt" (caar (journal-posting-tags posting)))343
(assert-equal "scan-001" (cdar (journal-posting-tags posting))))))345
(test-group "parse-journal - balance assertions"346
(test "parse balance assertion"347
(let* ((text "2026-03-15 * Deposit\n assets:checking $3000.00 = $5000.00\n income:salary\n")348
(txns (parse-journal text))349
(posting (car (journal-transaction-postings (car txns))))350
(bal (journal-posting-balance-assertion posting)))351
(assert-true (journal-amount? bal))352
(assert-equal 5000.0 (journal-amount-quantity bal))353
(assert-equal "$" (journal-amount-commodity bal))))355
(test "parse strict balance assertion (==)"356
(let* ((text "2026-03-15 * Verify\n assets:checking $0 == $5000.00\n income:salary\n")357
(txns (parse-journal text))358
(posting (car (journal-transaction-postings (car txns))))359
(bal (journal-posting-balance-assertion posting)))360
(assert-true (journal-amount? bal))361
(assert-equal 5000.0 (journal-amount-quantity bal))362
(assert-equal "$" (journal-amount-commodity bal))363
(assert-equal "==" (journal-posting-balance-assertion-type posting))))365
(test "parse inclusive balance assertion (=*)"366
(let* ((text "2026-03-15 * Verify\n assets:checking $0 =* $5000.00\n income:salary\n")367
(txns (parse-journal text))368
(posting (car (journal-transaction-postings (car txns))))369
(bal (journal-posting-balance-assertion posting)))370
(assert-true (journal-amount? bal))371
(assert-equal 5000.0 (journal-amount-quantity bal))372
(assert-equal "$" (journal-amount-commodity bal))373
(assert-equal "=*" (journal-posting-balance-assertion-type posting))))375
(test "parse normal balance assertion type is ="376
(let* ((text "2026-03-15 * Deposit\n assets:checking $3000.00 = $5000.00\n income:salary\n")377
(txns (parse-journal text))378
(posting (car (journal-transaction-postings (car txns)))))379
(assert-equal "=" (journal-posting-balance-assertion-type posting))))381
(test "parse cost with balance assertion"382
(let* ((text "2026-03-15 * Test\n assets:wise:eur 100.00 EUR @ 1.08 USD = 200.00 EUR\n assets:wise:usd\n")383
(txns (parse-journal text))384
(posting (car (journal-transaction-postings (car txns))))385
(cost (journal-posting-cost posting))386
(bal (journal-posting-balance-assertion posting)))387
(assert-true (journal-cost? cost))388
(assert-equal 'per-unit (journal-cost-type cost))389
(assert-true (journal-amount? bal))390
(assert-equal "EUR" (journal-amount-commodity bal))391
;; Verify quantity parses correctly (use = for numeric comparison392
;; since exact/inexact integers may not be equal? to each other)393
(assert-true (= 200 (journal-amount-quantity bal))))))395
;; ========== Round-trip Tests ==========397
(test-group "round-trip"398
(test "parse and re-format simple transaction"399
(let* ((original (str "2026-03-15 * Grocery store\n"400
" expenses:food:groceries 47.23 USD\n"401
" assets:checking"))402
(txns (parse-journal (str original "\n")))403
(formatted (format-transaction (car txns))))404
(assert-equal original formatted)))406
(test "parse and re-format transaction with cost"407
(let* ((original (str "2026-03-15 * Currency conversion\n"408
" assets:wise:eur -500.00 EUR\n"409
" assets:wise:usd 540.00 USD @@ 500.00 EUR"))410
(txns (parse-journal (str original "\n")))411
(formatted (format-transaction (car txns))))412
(assert-equal original formatted)))414
(test "parse and re-format prefix negative -$50"415
(let* ((original (str "2026-03-15 * Refund\n"416
" assets:checking -$50.00\n"417
" expenses:food"))418
(txns (parse-journal (str original "\n")))419
(formatted (format-transaction (car txns))))420
(assert-equal original formatted)))422
(test "parse and re-format internal negative $-50"423
(let* ((original (str "2026-03-15 * Refund\n"424
" assets:checking $-50.00\n"425
" expenses:food"))426
(txns (parse-journal (str original "\n")))427
(formatted (format-transaction (car txns))))428
(assert-equal original formatted)))430
(test "parse and re-format balance assertion with =="431
(let* ((original (str "2026-03-15 * Verify\n"432
" assets:checking $0.00 == $5000.00\n"433
" income:salary"))434
(txns (parse-journal (str original "\n")))435
(formatted (format-transaction (car txns))))436
(assert-equal original formatted)))438
(test "parse and re-format balance assertion with =*"439
(let* ((original (str "2026-03-15 * Verify\n"440
" assets:checking $0.00 =* $5000.00\n"441
" income:salary"))442
(txns (parse-journal (str original "\n")))443
(formatted (format-transaction (car txns))))444
(assert-equal original formatted)))446
(test "parse and re-format multiple transactions"447
(let* ((original (str "2026-03-15 * Groceries\n"448
" expenses:food 47.23 USD\n"449
" assets:checking\n"450
"\n"451
"2026-03-16 * Gas\n"452
" expenses:transport 30.00 USD\n"453
" assets:checking"))454
(txns (parse-journal (str original "\n")))455
(formatted (format-transactions txns)))456
(assert-equal original formatted))))458
;; ========== Deduplication ==========460
(test-group "deduplicate-transactions"461
(test "filter out duplicate by ref tag"462
(let* ((existing (list463
(journal-transaction464
date: "2026-03-15"465
status: "*"466
description: "Existing"467
tags: (list (cons "ref" "TRF-001"))468
postings: (list469
(journal-posting account: "expenses:food"470
amount: (journal-amount quantity: 50 commodity: "USD"))471
(journal-posting account: "assets:checking")))))472
(new-txns (list473
(journal-transaction474
date: "2026-03-16"475
status: "*"476
description: "Already imported"477
tags: (list (cons "ref" "TRF-001"))478
postings: (list479
(journal-posting account: "expenses:food"480
amount: (journal-amount quantity: 50 commodity: "USD"))481
(journal-posting account: "assets:checking")))482
(journal-transaction483
date: "2026-03-17"484
status: "*"485
description: "New one"486
tags: (list (cons "ref" "TRF-002"))487
postings: (list488
(journal-posting account: "expenses:food"489
amount: (journal-amount quantity: 30 commodity: "USD"))490
(journal-posting account: "assets:checking")))))491
(result (deduplicate-transactions new-txns existing)))492
(assert-equal 1 (length result))493
(assert-equal "New one" (journal-transaction-description (car result)))))495
(test "keep transactions without ref"496
(let* ((existing '())497
(new-txns (list498
(journal-transaction499
date: "2026-03-15"500
description: "No ref"501
postings: (list502
(journal-posting account: "expenses:misc"503
amount: (journal-amount quantity: 10 commodity: "USD"))504
(journal-posting account: "assets:checking")))))505
(result (deduplicate-transactions new-txns existing)))506
(assert-equal 1 (length result)))))508
(run-tests)