use rust_decimal_macros::dec;
use rustledger_core::{
Amount, Balance, Close, Directive, NaiveDate, Open, Pad, Posting, PriceAnnotation, Transaction,
};
use rustledger_parser::{Span, Spanned};
use rustledger_validate::{ErrorCode, ValidationOptions};
mod common;
use common::{validate, validate_spanned_with_options};
#[allow(clippy::missing_const_for_fn)]
fn date(year: i32, month: u32, day: u32) -> NaiveDate {
rustledger_core::naive_date(year, month, day).unwrap()
}
fn validate_directives(directives: &[Directive]) -> Vec<ErrorCode> {
let errors = validate(directives);
errors.iter().map(|e| e.code).collect()
}
#[allow(clippy::missing_const_for_fn)]
fn spanned_directive(
directive: Directive,
start: usize,
end: usize,
file_id: u16,
) -> Spanned<Directive> {
Spanned {
value: directive,
span: Span::new(start, end),
file_id,
}
}
#[test]
fn test_e1001_account_not_open() {
let directives = vec![
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Test")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(100), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Cash",
Amount::new(dec!(-100), "USD"),
)),
),
];
let errors = validate_directives(&directives);
assert!(
errors.contains(&ErrorCode::AccountNotOpen),
"expected E1001 AccountNotOpen error"
);
}
#[test]
fn test_e1002_account_already_open() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Open(Open::new(date(2024, 1, 2), "Assets:Bank")), ];
let errors = validate_directives(&directives);
assert!(
errors.contains(&ErrorCode::AccountAlreadyOpen),
"expected E1002 AccountAlreadyOpen error"
);
}
#[test]
fn test_e1003_account_closed() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Open(Open::new(date(2024, 1, 1), "Income:Salary")),
Directive::Close(Close::new(date(2024, 6, 1), "Assets:Bank")),
Directive::Transaction(
Transaction::new(date(2024, 7, 1), "After close")
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(100), "USD"),
))
.with_synthesized_posting(Posting::new(
"Income:Salary",
Amount::new(dec!(-100), "USD"),
)),
),
];
let errors = validate_directives(&directives);
assert!(
errors.contains(&ErrorCode::AccountClosed),
"expected E1003 AccountClosed error"
);
}
#[test]
fn test_valid_account_lifecycle() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Food")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Purchase")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(50), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(-50), "USD"),
)),
),
Directive::Close(Close::new(date(2024, 12, 31), "Expenses:Food")),
];
let errors = validate_directives(&directives);
let account_errors: Vec<_> = errors
.iter()
.filter(|e| {
matches!(
e,
ErrorCode::AccountNotOpen
| ErrorCode::AccountAlreadyOpen
| ErrorCode::AccountClosed
)
})
.collect();
assert!(
account_errors.is_empty(),
"expected no account lifecycle errors, got {account_errors:?}"
);
}
#[test]
fn test_e2001_balance_assertion_failed() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Food")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Groceries")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(50), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(-50), "USD"),
)),
),
Directive::Balance(Balance::new(
date(2024, 1, 31),
"Assets:Bank",
Amount::new(dec!(1000), "USD"), )),
];
let errors = validate_directives(&directives);
assert!(
errors.contains(&ErrorCode::BalanceAssertionFailed),
"expected E2001 BalanceAssertionFailed error"
);
}
#[test]
fn test_valid_balance_assertion() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Food")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Groceries")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(50), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(-50), "USD"),
)),
),
Directive::Balance(Balance::new(
date(2024, 1, 31),
"Assets:Bank",
Amount::new(dec!(-50), "USD"),
)),
];
let errors = validate_directives(&directives);
assert!(
!errors.contains(&ErrorCode::BalanceAssertionFailed),
"expected no BalanceAssertionFailed error"
);
}
#[test]
fn test_e3001_transaction_unbalanced() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Food")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Unbalanced")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(100), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(-50), "USD"),
)), ),
];
let errors = validate_directives(&directives);
assert!(
errors.contains(&ErrorCode::TransactionUnbalanced),
"expected E3001 TransactionUnbalanced error"
);
}
#[test]
fn test_e3003_no_postings_allowed() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Transaction(Transaction::new(date(2024, 1, 15), "Empty transaction")),
];
let errors = validate_directives(&directives);
assert!(
!errors.contains(&ErrorCode::NoPostings),
"should NOT report E3003 NoPostings error (Python allows empty transactions)"
);
}
#[test]
fn test_e3004_single_posting() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Single posting").with_synthesized_posting(
Posting::new("Assets:Bank", Amount::new(dec!(100), "USD")),
),
),
];
let errors = validate_directives(&directives);
assert!(
errors.contains(&ErrorCode::SinglePosting),
"expected E3004 SinglePosting warning"
);
}
#[test]
fn test_e4005_negative_cost_per_unit() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Stock")),
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Checking")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Buy with negative cost")
.with_synthesized_posting(
Posting::new("Assets:Stock", Amount::new(dec!(10), "AAPL")).with_cost(
rustledger_core::CostSpec::empty()
.with_number(rustledger_core::CostNumber::PerUnit { value: dec!(-150) })
.with_currency("USD"),
),
)
.with_synthesized_posting(Posting::auto("Assets:Checking")),
),
];
let errors = validate_directives(&directives);
assert!(
errors.contains(&ErrorCode::NegativeCost),
"expected E4005 NegativeCost error for negative per-unit cost"
);
}
#[test]
fn test_e4005_negative_total_cost() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Stock")),
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Checking")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Buy with negative total cost")
.with_synthesized_posting(
Posting::new("Assets:Stock", Amount::new(dec!(10), "AAPL")).with_cost(
rustledger_core::CostSpec::empty()
.with_number(rustledger_core::CostNumber::Total { value: dec!(-1500) })
.with_currency("USD"),
),
)
.with_synthesized_posting(Posting::auto("Assets:Checking")),
),
];
let errors = validate_directives(&directives);
assert!(
errors.contains(&ErrorCode::NegativeCost),
"expected E4005 NegativeCost error for negative total cost"
);
}
#[test]
fn test_e4005_positive_cost_ok() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Stock")),
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Checking")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Buy with positive cost")
.with_synthesized_posting(
Posting::new("Assets:Stock", Amount::new(dec!(10), "AAPL")).with_cost(
rustledger_core::CostSpec::empty()
.with_number(rustledger_core::CostNumber::PerUnit { value: dec!(150) })
.with_currency("USD"),
),
)
.with_synthesized_posting(Posting::new(
"Assets:Checking",
Amount::new(dec!(-1500), "USD"),
)),
),
];
let errors = validate_directives(&directives);
assert!(
!errors.contains(&ErrorCode::NegativeCost),
"expected no NegativeCost error for positive cost"
);
}
#[test]
fn test_valid_balanced_transaction() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Food")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Balanced")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(100), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(-100), "USD"),
)),
),
];
let errors = validate_directives(&directives);
assert!(
!errors.contains(&ErrorCode::TransactionUnbalanced),
"expected no TransactionUnbalanced error"
);
}
#[test]
fn test_valid_pad_with_balance() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Open(Open::new(date(2024, 1, 1), "Equity:Opening")),
Directive::Pad(Pad::new(date(2024, 1, 1), "Assets:Bank", "Equity:Opening")),
Directive::Balance(Balance::new(
date(2024, 1, 2),
"Assets:Bank",
Amount::new(dec!(1000), "USD"),
)),
];
let errors = validate_directives(&directives);
assert!(
!errors.contains(&ErrorCode::BalanceAssertionFailed),
"expected pad to satisfy balance assertion"
);
}
#[test]
fn test_valid_multi_currency_with_price() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:USD")),
Directive::Open(Open::new(date(2024, 1, 1), "Assets:EUR")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Currency exchange")
.with_synthesized_posting(
Posting::new("Assets:USD", Amount::new(dec!(100), "USD"))
.with_price(PriceAnnotation::unit(Amount::new(dec!(0.85), "EUR"))),
)
.with_synthesized_posting(Posting::new(
"Assets:EUR",
Amount::new(dec!(-85), "EUR"),
)),
),
];
let errors = validate_directives(&directives);
assert!(
!errors.contains(&ErrorCode::TransactionUnbalanced),
"expected multi-currency transaction with price to balance"
);
}
#[test]
fn test_complete_ledger_validation() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank:Checking")),
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank:Savings")),
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Food")),
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Transport")),
Directive::Open(Open::new(date(2024, 1, 1), "Income:Salary")),
Directive::Open(Open::new(date(2024, 1, 1), "Equity:Opening")),
Directive::Pad(Pad::new(
date(2024, 1, 1),
"Assets:Bank:Checking",
"Equity:Opening",
)),
Directive::Balance(Balance::new(
date(2024, 1, 2),
"Assets:Bank:Checking",
Amount::new(dec!(5000), "USD"),
)),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Monthly salary")
.with_payee("Employer")
.with_synthesized_posting(Posting::new(
"Income:Salary",
Amount::new(dec!(-3000), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank:Checking",
Amount::new(dec!(3000), "USD"),
)),
),
Directive::Transaction(
Transaction::new(date(2024, 1, 20), "Groceries")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(150), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank:Checking",
Amount::new(dec!(-150), "USD"),
)),
),
Directive::Transaction(
Transaction::new(date(2024, 1, 22), "Gas")
.with_synthesized_posting(Posting::new(
"Expenses:Transport",
Amount::new(dec!(45), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank:Checking",
Amount::new(dec!(-45), "USD"),
)),
),
Directive::Transaction(
Transaction::new(date(2024, 1, 25), "Transfer to savings")
.with_synthesized_posting(Posting::new(
"Assets:Bank:Savings",
Amount::new(dec!(1000), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank:Checking",
Amount::new(dec!(-1000), "USD"),
)),
),
Directive::Balance(Balance::new(
date(2024, 1, 31),
"Assets:Bank:Checking",
Amount::new(dec!(6805), "USD"), )),
];
let errors = validate_directives(&directives);
let critical_errors: Vec<_> = errors
.iter()
.filter(|e| {
matches!(
e,
ErrorCode::AccountNotOpen
| ErrorCode::TransactionUnbalanced
| ErrorCode::BalanceAssertionFailed
)
})
.collect();
assert!(
critical_errors.is_empty(),
"expected no critical validation errors, got {critical_errors:?}"
);
}
#[test]
fn test_basic_validation() {
let directives = vec![
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Food")),
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Test")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(100), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(-100), "USD"),
)),
),
];
let errors = validate(&directives);
assert!(
!errors
.iter()
.any(|e| e.code == ErrorCode::TransactionUnbalanced)
);
}
#[test]
fn test_spanned_validation_preserves_location_for_account_not_open() {
let directives = vec![
spanned_directive(
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Test")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(100), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Cash",
Amount::new(dec!(-100), "USD"),
)),
),
100,
200, 1, ),
];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let account_error = errors
.iter()
.find(|e| e.code == ErrorCode::AccountNotOpen)
.expect("expected E1001 AccountNotOpen error");
assert_eq!(account_error.span, Some(Span::new(100, 200)));
assert_eq!(account_error.file_id, Some(1));
}
#[test]
fn test_spanned_validation_preserves_location_for_unbalanced_transaction() {
let directives = vec![
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
0,
50,
0,
),
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Food")),
50,
100,
0,
),
spanned_directive(
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Unbalanced")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(100), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(-50), "USD"),
)), ),
100,
250, 2, ),
];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let unbalanced_error = errors
.iter()
.find(|e| e.code == ErrorCode::TransactionUnbalanced)
.expect("expected E3001 TransactionUnbalanced error");
assert_eq!(unbalanced_error.span, Some(Span::new(100, 250)));
assert_eq!(unbalanced_error.file_id, Some(2));
}
#[test]
fn test_spanned_validation_preserves_location_for_balance_error() {
let directives = vec![
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
0,
50,
0,
),
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Food")),
50,
100,
0,
),
spanned_directive(
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Purchase")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(50), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(-50), "USD"),
)),
),
100,
200,
0,
),
spanned_directive(
Directive::Balance(Balance::new(
date(2024, 1, 31),
"Assets:Bank",
Amount::new(dec!(1000), "USD"), )),
200,
280, 3, ),
];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let balance_error = errors
.iter()
.find(|e| e.code == ErrorCode::BalanceAssertionFailed)
.expect("expected E2001 BalanceAssertionFailed error");
assert_eq!(balance_error.span, Some(Span::new(200, 280)));
assert_eq!(balance_error.file_id, Some(3));
}
#[test]
fn test_spanned_validation_preserves_location_for_duplicate_open() {
let directives = vec![
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
0,
50,
0,
),
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 2), "Assets:Bank")),
50,
100, 1, ),
];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let dup_error = errors
.iter()
.find(|e| e.code == ErrorCode::AccountAlreadyOpen)
.expect("expected E1002 AccountAlreadyOpen error");
assert_eq!(dup_error.span, Some(Span::new(50, 100)));
assert_eq!(dup_error.file_id, Some(1));
}
#[test]
fn test_spanned_validation_preserves_location_for_account_closed() {
let directives = vec![
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
0,
50,
0,
),
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Income:Salary")),
50,
100,
0,
),
spanned_directive(
Directive::Close(Close::new(date(2024, 6, 1), "Assets:Bank")),
100,
150,
0,
),
spanned_directive(
Directive::Transaction(
Transaction::new(date(2024, 7, 1), "After close")
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(100), "USD"),
))
.with_synthesized_posting(Posting::new(
"Income:Salary",
Amount::new(dec!(-100), "USD"),
)),
),
150,
300, 4, ),
];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let closed_error = errors
.iter()
.find(|e| e.code == ErrorCode::AccountClosed)
.expect("expected E1003 AccountClosed error");
assert_eq!(closed_error.span, Some(Span::new(150, 300)));
assert_eq!(closed_error.file_id, Some(4));
}
#[test]
fn test_spanned_validation_single_posting_warning_has_location() {
let directives = vec![
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
0,
50,
0,
),
spanned_directive(
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Single posting").with_synthesized_posting(
Posting::new("Assets:Bank", Amount::new(dec!(100), "USD")),
),
),
50,
150, 5, ),
];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let single_post_error = errors
.iter()
.find(|e| e.code == ErrorCode::SinglePosting)
.expect("expected E3004 SinglePosting warning");
assert_eq!(single_post_error.span, Some(Span::new(50, 150)));
assert_eq!(single_post_error.file_id, Some(5));
}
#[test]
fn test_spanned_validation_multiple_errors_have_correct_locations() {
let directives = vec![
spanned_directive(
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "First error")
.with_synthesized_posting(Posting::new(
"Expenses:A",
Amount::new(dec!(100), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:A",
Amount::new(dec!(-100), "USD"),
)),
),
0,
100, 0,
),
spanned_directive(
Directive::Transaction(
Transaction::new(date(2024, 1, 16), "Second error")
.with_synthesized_posting(Posting::new(
"Expenses:B",
Amount::new(dec!(50), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:B",
Amount::new(dec!(-50), "USD"),
)),
),
100,
200, 0,
),
];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let account_errors: Vec<_> = errors
.iter()
.filter(|e| e.code == ErrorCode::AccountNotOpen)
.collect();
assert!(
account_errors.len() >= 2,
"expected at least 2 AccountNotOpen errors"
);
let spans: std::collections::HashSet<_> =
account_errors.iter().filter_map(|e| e.span).collect();
assert!(spans.len() >= 2, "expected errors to have different spans");
}
#[test]
fn test_spanned_validation_valid_ledger_no_errors() {
let directives = vec![
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
0,
50,
0,
),
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Expenses:Food")),
50,
100,
0,
),
spanned_directive(
Directive::Transaction(
Transaction::new(date(2024, 1, 15), "Valid")
.with_synthesized_posting(Posting::new(
"Expenses:Food",
Amount::new(dec!(100), "USD"),
))
.with_synthesized_posting(Posting::new(
"Assets:Bank",
Amount::new(dec!(-100), "USD"),
)),
),
100,
250,
0,
),
];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let critical_errors: Vec<_> = errors
.iter()
.filter(|e| !matches!(e.code, ErrorCode::DateOutOfOrder | ErrorCode::FutureDate))
.collect();
assert!(
critical_errors.is_empty(),
"expected no validation errors, got {critical_errors:?}"
);
}
#[test]
fn test_spanned_validation_out_of_order_dates_have_location() {
let directives = vec![
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
0,
50,
0,
),
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 10), "Expenses:Food")),
50,
100,
0,
),
spanned_directive(
Directive::Open(Open::new(date(2024, 1, 5), "Expenses:Transport")),
100,
150, 6, ),
];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let date_error = errors.iter().find(|e| e.code == ErrorCode::DateOutOfOrder);
if let Some(error) = date_error {
assert!(
error.span.is_some(),
"DateOutOfOrder error should have span"
);
assert!(
error.file_id.is_some(),
"DateOutOfOrder error should have file_id"
);
}
}
#[test]
fn test_plugin_synthesized_directive_gets_attribution_note() {
let directives = vec![spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Equity:Currency:FSS_AUSFIX")),
0,
0,
rustledger_parser::SYNTHESIZED_FILE_ID,
)];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let account_error = errors
.iter()
.find(|e| e.code == ErrorCode::InvalidAccountName)
.expect("expected E1005 for the '_'-containing account name");
let note = account_error
.note
.as_deref()
.expect("plugin-synthesized directive must have an attribution note");
assert!(
note.contains("synthesized by a plugin"),
"note should clearly identify the directive as plugin-generated, got: {note}",
);
assert!(
note.contains("plugin"),
"note should point users at their `plugin` declarations, got: {note}",
);
}
#[test]
fn test_source_directive_does_not_get_attribution_note() {
let directives = vec![spanned_directive(
Directive::Open(Open::new(date(2024, 1, 1), "Invalid:Root")),
10,
40,
0, )];
let errors = validate_spanned_with_options(&directives, ValidationOptions::default());
let account_error = errors
.iter()
.find(|e| e.code == ErrorCode::InvalidAccountName)
.expect("expected E1005 for bad root account type");
assert!(
account_error.note.is_none(),
"source-parsed directive must not be tagged as plugin-synthesized, got note: {:?}",
account_error.note,
);
}