use rustledger_parser::parse;
fn header_errors(src: &str) -> Vec<String> {
parse(src)
.errors
.iter()
.map(std::string::ToString::to_string)
.filter(|m| {
m.contains("transaction description")
|| m.contains("after a tag or link")
|| m.contains("in transaction header")
})
.collect()
}
#[test]
fn string_after_tag_is_rejected() {
let src = "2014-04-20 * #trip \"Money from CC\" ^610fa7f17e7a\n\
\x20 Expenses:Restaurant 100 USD\n\
\x20 Assets:US:Cash -100 USD\n";
let errs = header_errors(src);
assert_eq!(
errs.len(),
1,
"expected exactly one header error, got {errs:?}"
);
assert!(
errs[0].contains("after a tag or link"),
"wrong diagnostic: {}",
errs[0]
);
assert!(
errs[0].contains("Money from CC"),
"diagnostic does not name the offending string: {}",
errs[0]
);
}
#[test]
fn junk_after_the_narration_is_rejected() {
let src = "2013-05-01 open Assets:US:Cash\n\
\n\
2013-05-02 * \"Dinner\" A:*:B\n\
\x20 Expenses:Restaurant 100 USD\n\
\x20 Assets:US:Cash -100 USD\n";
let errs = header_errors(src);
assert_eq!(
errs.len(),
1,
"expected exactly one header error, got {errs:?}"
);
assert!(
errs[0].contains("in transaction header"),
"wrong diagnostic: {}",
errs[0]
);
}
#[test]
fn three_header_strings_are_rejected() {
for src in [
"2013-05-18 * \"Mermaid Inn\" \"Nice dinner\" \"With Caroline\"\n\
\x20 Expenses:Restaurant 100 USD\n\
\x20 Assets:US:Cash -100 USD\n",
"2013-05-18 * \"A\" \"B\" \"C\"\n\
\x20 Expenses:Restaurant 100 USD\n\
\x20 Assets:US:Cash -100 USD\n",
] {
let errs = header_errors(src);
assert_eq!(errs.len(), 1, "expected one header error, got {errs:?}");
assert!(
errs[0].contains("found 3"),
"diagnostic should report the count: {}",
errs[0]
);
}
}
#[test]
fn valid_headers_are_untouched() {
let bodies = "\x20 Expenses:R 100 USD\n\x20 Assets:C -100 USD\n";
for header in [
"2014-04-20 * \"payee\" \"narration\"",
"2014-04-20 * \"narration only\"",
"2014-04-20 *",
"2014-04-20 txn \"n\"",
"2014-04-20 ! \"pending\"",
"2014-04-20 # \"hash flag\"",
"2014-04-20 \"implied flag\"",
"2014-04-20 * \"p\" \"n\" #tag1 #tag2 ^link-a ^link-b",
"2014-04-20 * #tag-only",
"2014-04-20 * \"p\" \"n\" ; trailing comment",
"2012-12-17 P \"Payee\" \"Narration\"",
"2014-04-20 * \"é payee\" \"münts narration\"",
"2014-04-20 * \"two\" \"strings\"",
] {
let src = format!("{header}\n{bodies}");
assert!(
header_errors(&src).is_empty(),
"valid header wrongly rejected: {header:?} -> {:?}",
header_errors(&src)
);
}
}
#[test]
fn deprecated_pipe_is_not_reported_twice() {
let src = "2014-04-20 * \"payee\" | \"narration\"\n\
\x20 Expenses:R 100 USD\n\
\x20 Assets:C -100 USD\n";
assert!(
header_errors(src).is_empty(),
"pipe must keep its DeprecatedPipeSymbol diagnostic, not gain a header one"
);
let all: Vec<String> = parse(src)
.errors
.iter()
.map(std::string::ToString::to_string)
.collect();
assert!(
all.iter()
.any(|m| m.contains("pipe") || m.contains("Pipe") || m.contains('|')),
"expected the deprecated-pipe diagnostic, got {all:?}"
);
}
#[test]
fn one_defect_per_header() {
let src = "2013-05-02 * \"Dinner\" A:*:B\n\x20 Expenses:R 100 USD\n\x20 Assets:C -100 USD\n";
assert_eq!(header_errors(src).len(), 1);
}
fn cost_errors(src: &str) -> Vec<String> {
parse(src)
.errors
.iter()
.map(std::string::ToString::to_string)
.filter(|m| m.contains("cost spec") || m.contains("cost-spec component"))
.collect()
}
#[test]
fn empty_cost_component_is_rejected() {
let src = "2014-01-01 *\n\
\x20 Assets:Invest:AAPL 10 AAPL {, 100.0 USD, , }\n\
\x20 Assets:Invest:Cash -19.90 USD\n";
let errs = cost_errors(src);
assert_eq!(errs.len(), 1, "expected one cost-spec error, got {errs:?}");
assert!(errs[0].contains("empty cost-spec component"), "{}", errs[0]);
}
#[test]
fn junk_after_a_complete_cost_component_is_rejected() {
let src = "2014-01-01 *\n\
\x20 Assets:Invest:AAPL 1.1 AAPL {45.23 USD / 2015-07-16 / \"blabla\"}\n\
\x20 Assets:Invest:Cash -45.23 USD\n";
let errs = cost_errors(src);
assert_eq!(errs.len(), 1, "expected one cost-spec error, got {errs:?}");
assert!(errs[0].contains("already complete"), "{}", errs[0]);
}
#[test]
fn valid_cost_specs_are_untouched() {
for spec in [
"{}",
"{100.00 USD}",
"{100.00 USD, 2015-01-01}",
"{100.00 USD, \"lot-label\"}",
"{100.00 USD, 2015-01-01, \"lot\"}",
"{{500.00 USD}}",
"{{500.00 USD, 2015-01-01}}",
"{# 500.00 USD}",
"{10.00 * 3 USD}",
"{30.00 / 3 USD}",
"{10.00 + 2 USD}",
"{*}",
"{USD}",
"{ # USD}",
] {
let src = format!(
"2014-01-01 *\n Assets:I:AAPL 10 AAPL {spec}\n Assets:I:Cash -100.00 USD\n"
);
assert!(
cost_errors(&src).is_empty(),
"valid cost spec wrongly rejected: {spec} -> {:?}",
cost_errors(&src)
);
}
}