use rustledger_core::Directive;
use rustledger_parser::{ParseError, ParseErrorKind, ParseResult, parse, parse_directives};
fn parse_ok(source: &str) -> ParseResult {
let result = parse(source);
assert!(
result.errors.is_empty(),
"expected no errors, got: {:?}",
result.errors
);
result
}
fn count_directive_type(result: &ParseResult, type_name: &str) -> usize {
result
.directives
.iter()
.filter(|d| match &d.value {
Directive::Open(_) => type_name == "open",
Directive::Close(_) => type_name == "close",
Directive::Transaction(_) => type_name == "transaction",
Directive::Balance(_) => type_name == "balance",
Directive::Pad(_) => type_name == "pad",
Directive::Price(_) => type_name == "price",
Directive::Event(_) => type_name == "event",
Directive::Note(_) => type_name == "note",
Directive::Document(_) => type_name == "document",
Directive::Commodity(_) => type_name == "commodity",
Directive::Query(_) => type_name == "query",
Directive::Custom(_) => type_name == "custom",
})
.count()
}
#[test]
fn test_parse_open_directive() {
let source = r"2024-01-01 open Assets:Bank:Checking USD, EUR";
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "open"), 1);
if let Directive::Open(open) = &result.directives[0].value {
assert_eq!(open.account, "Assets:Bank:Checking");
assert_eq!(open.currencies, vec!["USD", "EUR"]);
} else {
panic!("expected open directive");
}
}
#[test]
fn test_parse_close_directive() {
let source = r"2024-12-31 close Assets:Bank:OldAccount";
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "close"), 1);
if let Directive::Close(close) = &result.directives[0].value {
assert_eq!(close.account, "Assets:Bank:OldAccount");
} else {
panic!("expected close directive");
}
}
#[test]
fn test_parse_simple_transaction() {
let source = r#"
2024-01-15 * "Coffee Shop" "Morning coffee"
Expenses:Food:Coffee 5.00 USD
Assets:Cash
"#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "transaction"), 1);
if let Directive::Transaction(txn) = &result.directives[0].value {
assert_eq!(txn.payee.as_deref(), Some("Coffee Shop"));
assert_eq!(txn.narration.as_str(), "Morning coffee");
assert_eq!(txn.postings.len(), 2);
} else {
panic!("expected transaction");
}
}
#[test]
fn test_parse_transaction_with_tags_and_links() {
let source = r#"
2024-01-15 * "Dinner" #food #restaurant ^receipt-123
Expenses:Food 45.00 USD
Assets:Cash
"#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "transaction"), 1);
if let Directive::Transaction(txn) = &result.directives[0].value {
assert!(txn.tags.iter().any(|t| t.as_str() == "food"));
assert!(txn.tags.iter().any(|t| t.as_str() == "restaurant"));
assert!(txn.links.iter().any(|l| l.as_str() == "receipt-123"));
} else {
panic!("expected transaction");
}
}
#[test]
fn test_parse_balance_directive() {
let source = r"2024-01-31 balance Assets:Bank:Checking 1000.00 USD";
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "balance"), 1);
if let Directive::Balance(bal) = &result.directives[0].value {
assert_eq!(bal.account, "Assets:Bank:Checking");
assert_eq!(bal.amount.number.to_string(), "1000.00");
assert_eq!(bal.amount.currency, "USD");
} else {
panic!("expected balance");
}
}
#[test]
fn test_parse_pad_directive() {
let source = r"2024-01-01 pad Assets:Bank:Checking Equity:Opening-Balances";
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "pad"), 1);
if let Directive::Pad(pad) = &result.directives[0].value {
assert_eq!(pad.account, "Assets:Bank:Checking");
assert_eq!(pad.source_account, "Equity:Opening-Balances");
} else {
panic!("expected pad");
}
}
#[test]
fn test_parse_price_directive() {
let source = r"2024-01-15 price AAPL 185.50 USD";
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "price"), 1);
if let Directive::Price(price) = &result.directives[0].value {
assert_eq!(price.currency, "AAPL");
assert_eq!(price.amount.number.to_string(), "185.50");
assert_eq!(price.amount.currency, "USD");
} else {
panic!("expected price");
}
}
#[test]
fn test_parse_event_directive() {
let source = r#"2024-01-01 event "location" "New York""#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "event"), 1);
if let Directive::Event(event) = &result.directives[0].value {
assert_eq!(event.event_type, "location");
assert_eq!(event.value, "New York");
} else {
panic!("expected event");
}
}
#[test]
fn test_parse_note_directive() {
let source = r#"2024-01-15 note Assets:Bank:Checking "Account review completed""#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "note"), 1);
if let Directive::Note(note) = &result.directives[0].value {
assert_eq!(note.account, "Assets:Bank:Checking");
assert_eq!(note.comment, "Account review completed");
} else {
panic!("expected note");
}
}
#[test]
fn test_parse_document_directive() {
let source = r#"2024-01-15 document Assets:Bank:Checking "/path/to/statement.pdf""#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "document"), 1);
if let Directive::Document(doc) = &result.directives[0].value {
assert_eq!(doc.account, "Assets:Bank:Checking");
assert_eq!(doc.path, "/path/to/statement.pdf");
} else {
panic!("expected document");
}
}
#[test]
fn test_parse_commodity_directive() {
let source = r#"2024-01-01 commodity USD
name: "US Dollar""#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "commodity"), 1);
if let Directive::Commodity(comm) = &result.directives[0].value {
assert_eq!(comm.currency, "USD");
} else {
panic!("expected commodity");
}
}
#[test]
fn test_commodity_precision_metadata_round_trips() {
use rust_decimal_macros::dec;
use rustledger_core::{FormatConfig, MetaValue, format_directive};
let source = "2024-01-01 commodity USD\n precision: 2\n";
let parsed = parse_ok(source);
let Directive::Commodity(comm) = &parsed.directives[0].value else {
panic!("expected commodity");
};
assert_eq!(
comm.meta.get("precision"),
Some(&MetaValue::Number(dec!(2))),
"parser must produce Number(2) for unquoted integer metadata"
);
let formatted = format_directive(&parsed.directives[0].value, &FormatConfig::default());
let reparsed = parse_ok(&formatted);
let Directive::Commodity(comm2) = &reparsed.directives[0].value else {
panic!("expected commodity after re-parse");
};
assert_eq!(
comm2.meta.get("precision"),
Some(&MetaValue::Number(dec!(2))),
"round-tripped precision must remain Number(2); got formatted: {formatted:?}"
);
}
#[test]
fn test_parse_query_directive() {
let source = r#"2024-01-01 query "expenses" "SELECT account, SUM(position)""#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "query"), 1);
if let Directive::Query(q) = &result.directives[0].value {
assert_eq!(q.name, "expenses");
assert!(q.query.contains("SELECT"));
} else {
panic!("expected query");
}
}
#[test]
fn test_parse_custom_directive() {
let source = r#"2024-01-01 custom "budget" Expenses:Food 500.00 USD"#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "custom"), 1);
}
#[test]
fn test_parse_options() {
let source = r#"
option "title" "My Ledger"
option "operating_currency" "USD"
option "operating_currency" "EUR"
"#;
let result = parse_ok(source);
assert_eq!(result.options.len(), 3);
assert_eq!(result.options[0].0, "title");
assert_eq!(result.options[0].1, "My Ledger");
}
#[test]
fn test_parse_includes() {
let source = r#"
include "accounts.beancount"
include "transactions/2024.beancount"
"#;
let result = parse_ok(source);
assert_eq!(result.includes.len(), 2);
assert_eq!(result.includes[0].0, "accounts.beancount");
assert_eq!(result.includes[1].0, "transactions/2024.beancount");
}
#[test]
fn test_parse_plugins() {
let source = r#"
plugin "beancount.plugins.leafonly"
plugin "beancount.plugins.check_commodity" "config_string"
"#;
let result = parse_ok(source);
assert_eq!(result.plugins.len(), 2);
assert_eq!(result.plugins[0].0, "beancount.plugins.leafonly");
assert!(result.plugins[0].1.is_none());
assert_eq!(result.plugins[1].0, "beancount.plugins.check_commodity");
assert_eq!(result.plugins[1].1, Some("config_string".to_string()));
}
#[test]
fn test_parse_transaction_with_cost() {
let source = r#"
2024-01-15 * "Buy stock"
Assets:Brokerage 10 AAPL {185.50 USD}
Assets:Cash -1855.00 USD
"#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "transaction"), 1);
if let Directive::Transaction(txn) = &result.directives[0].value {
let posting = &txn.postings[0];
assert!(posting.cost.is_some());
let cost = posting.cost.as_ref().unwrap();
assert_eq!(cost.number_per.unwrap().to_string(), "185.50");
assert_eq!(cost.currency.as_deref(), Some("USD"));
} else {
panic!("expected transaction");
}
}
#[test]
fn test_parse_transaction_with_price() {
let source = r#"
2024-01-15 * "Currency exchange"
Assets:USD 100.00 USD @ 0.85 EUR
Assets:EUR -85.00 EUR
"#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "transaction"), 1);
if let Directive::Transaction(txn) = &result.directives[0].value {
let posting = &txn.postings[0];
assert!(posting.price.is_some());
} else {
panic!("expected transaction");
}
}
#[test]
fn test_parse_transaction_with_total_cost() {
let source = r#"
2024-01-15 * "Buy stock with fees"
Assets:Brokerage 10 AAPL {{1860.00 USD}}
Assets:Cash -1860.00 USD
"#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "transaction"), 1);
if let Directive::Transaction(txn) = &result.directives[0].value {
let posting = &txn.postings[0];
assert!(posting.cost.is_some());
} else {
panic!("expected transaction");
}
}
#[test]
fn test_parse_transaction_with_metadata() {
let source = r#"
2024-01-15 * "Purchase"
receipt: "scan-001.pdf"
category: "office"
Expenses:Office 100.00 USD
item: "Printer paper"
Assets:Cash
"#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "transaction"), 1);
if let Directive::Transaction(txn) = &result.directives[0].value {
assert!(txn.meta.contains_key("receipt"));
assert!(txn.meta.contains_key("category"));
assert!(txn.postings[0].meta.contains_key("item"));
} else {
panic!("expected transaction");
}
}
#[test]
fn test_parse_boolean_metadata() {
let source = r#"
2024-01-15 * "Test"
recurring: TRUE
active: FALSE
enabled: True
disabled: False
Expenses:Test 100.00 USD
Assets:Cash
"#;
let result = parse_ok(source);
if let Directive::Transaction(txn) = &result.directives[0].value {
use rustledger_core::MetaValue;
assert_eq!(txn.meta.get("recurring"), Some(&MetaValue::Bool(true)));
assert_eq!(txn.meta.get("active"), Some(&MetaValue::Bool(false)));
assert_eq!(txn.meta.get("enabled"), Some(&MetaValue::Bool(true)));
assert_eq!(txn.meta.get("disabled"), Some(&MetaValue::Bool(false)));
} else {
panic!("expected transaction");
}
}
#[test]
fn test_parse_extended_transaction_flags() {
for (flag, expected) in [
("P", 'P'), ("S", 'S'), ("T", 'T'), ("C", 'C'), ("U", 'U'), ("R", 'R'), ("M", 'M'), ("#", '#'), ("?", '?'), ] {
let source = format!(
r#"
2024-01-15 {flag} "Test transaction"
Expenses:Test 100 USD
Assets:Cash
"#
);
let result = parse_ok(&source);
if let Directive::Transaction(txn) = &result.directives[0].value {
assert_eq!(
txn.flag, expected,
"Flag {flag} should parse as '{expected}'"
);
} else {
panic!("expected transaction for flag {flag}");
}
}
}
#[test]
fn test_error_recovery_continues_parsing() {
let source = r"
2024-01-01 open Assets:Bank
; This line has an error
2024-01-15 invalid directive here
2024-01-31 close Assets:Bank
";
let result = parse(source);
assert!(!result.errors.is_empty(), "expected parse errors");
assert!(
count_directive_type(&result, "open") >= 1,
"should have parsed open directive"
);
}
#[test]
fn test_error_on_invalid_date() {
let source = r"2024-13-45 open Assets:Bank";
let result = parse(source);
assert!(!result.errors.is_empty(), "expected error for invalid date");
}
#[test]
fn test_parse_single_digit_month() {
let source = "2024-1-15 open Assets:Checking\n";
let result = parse(source);
assert!(
result.errors.is_empty(),
"unexpected errors for single-digit month: {:?}",
result.errors
);
assert_eq!(count_directive_type(&result, "open"), 1);
if let Directive::Open(open) = &result.directives[0].value {
assert_eq!(open.date, rustledger_core::naive_date(2024, 1, 15).unwrap());
} else {
panic!("expected open directive");
}
}
#[test]
fn test_parse_single_digit_day() {
let source = "2024-01-5 open Assets:Cash USD\n";
let result = parse(source);
assert!(
result.errors.is_empty(),
"unexpected errors for single-digit day: {:?}",
result.errors
);
assert_eq!(count_directive_type(&result, "open"), 1);
}
#[test]
fn test_parse_single_digit_month_and_day() {
let source = "2024-1-1 open Assets:Cash USD\n";
let result = parse(source);
assert!(
result.errors.is_empty(),
"unexpected errors for single-digit month/day: {:?}",
result.errors
);
assert_eq!(count_directive_type(&result, "open"), 1);
if let Directive::Open(open) = &result.directives[0].value {
assert_eq!(open.date, rustledger_core::naive_date(2024, 1, 1).unwrap());
} else {
panic!("expected open directive");
}
}
#[test]
fn test_error_invalid_leap_year_date() {
let source = "2023-02-29 open Assets:Cash USD\n";
let result = parse(source);
assert!(
!result.errors.is_empty(),
"expected error for invalid leap-year date"
);
let err = &result.errors[0];
assert!(
matches!(err.kind, ParseErrorKind::InvalidDateValue(_)),
"expected InvalidDateValue error kind, got: {:?}",
err.kind
);
let msg = err.message();
assert!(
msg.contains("day") && msg.contains("out of range"),
"expected error mentioning 'day' and 'out of range', got: '{msg}'"
);
assert!(
msg.contains("2023-02"),
"expected error mentioning '2023-02', got: '{msg}'"
);
}
#[test]
fn test_error_invalid_date_month_out_of_range() {
let source = "2024-13-01 open Assets:Cash USD\n";
let result = parse(source);
assert!(
!result.errors.is_empty(),
"expected error for month out of range"
);
let err = &result.errors[0];
assert!(
matches!(err.kind, ParseErrorKind::InvalidDateValue(_)),
"expected InvalidDateValue error kind, got: {:?}",
err.kind
);
let msg = err.message();
assert!(
msg.contains("month") && msg.contains("out of range"),
"expected error mentioning 'month' and 'out of range', got: '{msg}'"
);
}
#[test]
fn test_error_on_invalid_account() {
let source = r"2024-01-01 open lowercase:invalid";
let result = parse(source);
assert!(
!result.errors.is_empty(),
"expected error for invalid account"
);
}
#[test]
fn test_parse_empty_input() {
let result = parse("");
assert!(result.errors.is_empty());
assert!(result.directives.is_empty());
}
#[test]
fn test_parse_only_comments() {
let source = r"
; This is a comment
; Another comment
";
let result = parse_ok(source);
assert!(result.directives.is_empty());
assert_eq!(result.comments.len(), 2);
assert!(result.comments[0].value.contains("This is a comment"));
assert!(result.comments[1].value.contains("Another comment"));
}
#[test]
fn test_parse_comments_with_directives() {
let source = r#"
; Header comment
option "operating_currency" "USD"
; Section comment
2024-01-01 open Assets:Bank USD
description: "Main account"
; Footer comment
"#;
let result = parse_ok(source);
assert_eq!(result.directives.len(), 1);
assert_eq!(result.options.len(), 1);
assert_eq!(result.comments.len(), 3);
assert!(result.comments[0].value.contains("Header comment"));
assert!(result.comments[1].value.contains("Section comment"));
assert!(result.comments[2].value.contains("Footer comment"));
}
#[test]
fn test_parse_unicode_in_narration() {
let source = r#"2024-01-15 * "Café ☕" "Latte mit Milch"
Expenses:Food 5.00 EUR
Assets:Cash"#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "transaction"), 1);
if let Directive::Transaction(txn) = &result.directives[0].value {
assert_eq!(txn.payee.as_deref(), Some("Café ☕"));
assert_eq!(txn.narration.as_str(), "Latte mit Milch");
} else {
panic!("expected transaction");
}
}
#[test]
fn test_parse_negative_amounts() {
let source = r#"
2024-01-15 * "Refund"
Assets:Bank -50.00 USD
Expenses:Food
"#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "transaction"), 1);
}
#[test]
fn test_parse_large_numbers() {
let source = r"2024-01-15 price BTC 15000.00 USD";
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "price"), 1);
}
#[test]
fn test_parse_booking_method() {
let source = r#"2024-01-01 open Assets:Stock "FIFO""#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "open"), 1);
if let Directive::Open(open) = &result.directives[0].value {
assert_eq!(open.booking, Some("FIFO".to_string()));
} else {
panic!("expected open");
}
}
#[test]
fn test_parse_complete_ledger() {
let source = r#"
; Main ledger file
option "title" "Personal Finance"
option "operating_currency" "USD"
plugin "beancount.plugins.auto_accounts"
2024-01-01 open Assets:Bank:Checking USD
2024-01-01 open Assets:Bank:Savings USD
2024-01-01 open Expenses:Food
2024-01-01 open Expenses:Transport
2024-01-01 open Income:Salary
2024-01-01 pad Assets:Bank:Checking Equity:Opening-Balances
2024-01-15 * "Employer" "Monthly salary"
Income:Salary -5000.00 USD
Assets:Bank:Checking 5000.00 USD
2024-01-16 * "Grocery Store" "Weekly groceries" #food
Expenses:Food 150.00 USD
Assets:Bank:Checking
2024-01-17 * "Gas Station" "Fill up"
Expenses:Transport 45.00 USD
Assets:Bank:Checking
2024-01-31 balance Assets:Bank:Checking 4805.00 USD
2024-01-31 note Assets:Bank:Checking "Reconciled with bank statement"
"#;
let result = parse_ok(source);
assert_eq!(result.options.len(), 2);
assert_eq!(result.plugins.len(), 1);
assert_eq!(count_directive_type(&result, "open"), 5);
assert_eq!(count_directive_type(&result, "pad"), 1);
assert_eq!(count_directive_type(&result, "transaction"), 3);
assert_eq!(count_directive_type(&result, "balance"), 1);
assert_eq!(count_directive_type(&result, "note"), 1);
}
#[test]
fn test_parse_investment_ledger() {
let source = r#"
2024-01-01 open Assets:Brokerage AAPL, GOOG, USD
2024-01-01 open Income:Dividends
2024-01-01 open Income:Capital-Gains
2024-01-01 commodity AAPL
name: "Apple Inc."
2024-01-15 * "Buy Apple stock"
Assets:Brokerage 10 AAPL {185.00 USD, 2024-01-15}
Assets:Brokerage -1850.00 USD
2024-02-15 * "Receive dividend"
Assets:Brokerage 5.00 USD
Income:Dividends -5.00 USD
2024-03-15 price AAPL 190.00 USD
2024-04-15 * "Sell Apple stock"
Assets:Brokerage -5 AAPL {185.00 USD, 2024-01-15}
Assets:Brokerage 950.00 USD
Income:Capital-Gains -25.00 USD
"#;
let result = parse_ok(source);
assert_eq!(count_directive_type(&result, "open"), 3);
assert_eq!(count_directive_type(&result, "commodity"), 1);
assert_eq!(count_directive_type(&result, "transaction"), 3);
assert_eq!(count_directive_type(&result, "price"), 1);
}
#[test]
fn test_parse_directives_simple() {
let source = r#"
option "title" "Test"
2024-01-01 open Assets:Bank
"#;
let (directives, errors) = parse_directives(source);
assert!(errors.is_empty());
assert_eq!(directives.len(), 1);
}
#[test]
fn test_reject_leading_decimal() {
let source = "2024-01-15 * \"Test\"\n Expenses:Food .50 USD\n Assets:Checking\n";
let result = parse(source);
assert!(
!result.errors.is_empty(),
"expected parse error for leading decimal amount '.50 USD'"
);
}
#[test]
fn test_accept_decimal_with_integer_part() {
let source = "2024-01-15 * \"Test\"\n Expenses:Food 0.50 USD\n Assets:Checking\n";
let result = parse(source);
assert!(
result.errors.is_empty(),
"valid amount '0.50 USD' should parse without errors, errors: {:?}",
result.errors
);
}
#[test]
fn test_reject_lowercase_booking_method() {
let source = "2024-01-01 open Assets:Stock AAPL \"fifo\"\n";
let result = parse(source);
assert!(
!result.errors.is_empty(),
"expected parse error for lowercase booking method 'fifo'"
);
}
#[test]
fn test_accept_uppercase_booking_method() {
let source = "2024-01-01 open Assets:Stock AAPL \"FIFO\"\n";
let result = parse(source);
assert!(
result.errors.is_empty(),
"uppercase booking method 'FIFO' should be valid, errors: {:?}",
result.errors
);
}
#[test]
fn test_accept_strict_with_size_booking_method() {
let source = "2024-01-01 open Assets:Stock AAPL \"STRICT_WITH_SIZE\"\n";
let result = parse(source);
assert!(
result.errors.is_empty(),
"booking method 'STRICT_WITH_SIZE' should be valid, errors: {:?}",
result.errors
);
if let Directive::Open(open) = &result.directives[0].value {
assert_eq!(open.booking, Some("STRICT_WITH_SIZE".to_string()));
} else {
panic!("expected open directive");
}
}
#[test]
fn test_invalid_booking_method_error_includes_strict_with_size() {
let source = "2024-01-01 open Assets:Stock AAPL \"invalid_method\"\n";
let result = parse(source);
assert!(
!result.errors.is_empty(),
"expected parse error for invalid booking method"
);
let error_msg = result.errors[0].message();
assert!(
error_msg.contains("STRICT_WITH_SIZE"),
"error message should list STRICT_WITH_SIZE as a valid method, got: {error_msg}"
);
}
#[test]
fn test_reject_uppercase_metadata_key() {
let source =
"2024-01-15 * \"Test\"\n Category: \"test\"\n Expenses:Food 50 USD\n Assets:Checking\n";
let result = parse(source);
assert!(
!result.errors.is_empty(),
"expected parse error for metadata key starting with uppercase 'Category:'"
);
}
#[test]
fn test_reject_balance_without_amount() {
let source = "2024-01-15 balance Assets:Checking\n";
let result = parse(source);
assert!(
!result.errors.is_empty(),
"expected parse error for balance directive without amount"
);
}
#[test]
fn test_reject_pad_without_source_account() {
let source = "2024-01-15 pad Assets:Checking\n";
let result = parse(source);
assert!(
!result.errors.is_empty(),
"expected parse error for pad directive without source account"
);
}
#[test]
fn test_accept_unicode_account_name() {
let source = "2024-01-01 open Assets:銀行口座\n";
let result = parse(source);
assert!(
result.errors.is_empty(),
"Unicode account names should parse successfully, got: {:?}",
result
.errors
.iter()
.map(rustledger_parser::ParseError::message)
.collect::<Vec<_>>()
);
}
#[test]
fn test_reject_unclosed_cost_brace() {
let source = "\
2024-01-01 open Assets:Stock
2024-01-01 open Assets:Cash USD
2024-01-15 *
Assets:Stock 10 AAPL {150 USD
Assets:Cash -1500 USD
";
let result = parse(source);
assert!(
result
.errors
.iter()
.any(|e| e.message().contains("unclosed cost")),
"expected 'unclosed cost' parse error, got: {:?}",
result
.errors
.iter()
.map(ParseError::message)
.collect::<Vec<_>>()
);
}
#[test]
fn test_reject_incomplete_final_directive_at_eof() {
let source = "2024-01-01 open";
let result = parse(source);
assert!(
!result.errors.is_empty(),
"expected parse error for incomplete open directive at EOF, got: {:?}",
result
.errors
.iter()
.map(ParseError::message)
.collect::<Vec<_>>()
);
}
#[test]
fn test_reject_unclosed_cost_brace_at_eof() {
let source = "\
2024-01-01 open Assets:Stock
2024-01-01 open Assets:Cash USD
2024-01-15 *
Assets:Stock 10 AAPL {150 USD";
let result = parse(source);
assert!(
result
.errors
.iter()
.any(|e| e.message().contains("unclosed cost")),
"expected 'unclosed cost' parse error at EOF, got: {:?}",
result
.errors
.iter()
.map(ParseError::message)
.collect::<Vec<_>>()
);
}