Lines
100 %
Functions
Branches
//! Execution tests for the transit category script (`doc/scripts/metro.org`).
//!
//! The script tags a transaction `category` = `transportation` when its source
//! account (the negative-value split) is "Suica", its target account (the
//! positive-value split) is "Metro", and it carries no `category` tag yet.
use chrono::Utc;
use finance::split::Split;
use finance::transaction::Transaction;
use scripting::executor::ScriptExecutor;
use scripting::format::{ContextType, EntityType};
use scripting::nomiscript::{Compiler, Reader, SymbolTable};
use scripting::runtime::{EngineOpts, build_engine};
use scripting::serializer::{MemorySerializer, TransactionFromArgs};
use uuid::Uuid;
use wasmtime::Engine;
const METRO_SOURCE: &str = include_str!("../../doc/scripts/metro.nms");
fn gc_engine() -> Engine {
build_engine(EngineOpts::baseline()).unwrap()
}
fn compile_metro() -> Vec<u8> {
let program = Reader::parse(METRO_SOURCE).expect("failed to parse metro.nms");
let mut symbols = SymbolTable::with_builtins();
let mut compiler = Compiler::new();
compiler
.compile(&program, &mut symbols)
.expect("failed to compile metro.nms")
fn split(tx_id: Uuid, value_num: i64) -> Split {
Split {
id: Uuid::new_v4(),
tx_id,
account_id: Uuid::new_v4(),
commodity_id: Uuid::new_v4(),
value_num,
value_denom: 100,
reconcile_state: None,
reconcile_date: None,
lot_id: None,
/// Serialize a two-split transaction-create context. The source split (negative
/// value) posts to `source_account`; the target split (positive value) posts to
/// `target_account`. When `category` is set, an existing `category` tag is added
/// to the transaction.
fn build_tx_input(source_account: &str, target_account: &str, category: Option<&str>) -> Vec<u8> {
let tx = Transaction {
post_date: Utc::now(),
enter_date: Utc::now(),
};
let source = split(tx.id, -5000);
let target = split(tx.id, 5000);
let mut ser = MemorySerializer::new();
ser.set_context(ContextType::EntityCreate, EntityType::Transaction);
let tx_idx = ser.add_transaction_from(TransactionFromArgs {
transaction: &tx,
is_primary: true,
split_count: 2,
tag_count: u32::from(category.is_some()),
is_multi_currency: false,
});
ser.set_primary(tx_idx);
ser.add_split_from(&source, tx_idx as i32, source_account);
ser.add_split_from(&target, tx_idx as i32, target_account);
if let Some(value) = category {
ser.add_tag(
Uuid::new_v4().into_bytes(),
tx_idx as i32,
false,
"category",
value,
);
ser.finalize(4096)
fn run(input: &[u8]) -> usize {
let wasm = compile_metro();
ScriptExecutor::with_engine(gc_engine())
.execute(&wasm, input, Some(4096))
.expect("execute")
.len()
#[test]
fn suica_to_metro_uncategorized_gets_transportation() {
let input = build_tx_input("Suica", "Metro", None);
assert_eq!(
run(&input),
2,
"Suica->Metro with no category must tag both splits transportation"
fn reversed_direction_is_untouched() {
// Metro is the source, Suica the target — not a Suica->Metro fare.
let input = build_tx_input("Metro", "Suica", None);
assert_eq!(run(&input), 0, "direction matters: source must be Suica");
fn unrelated_accounts_are_untouched() {
let input = build_tx_input("Assets:Checking", "Expenses:Dining", None);
assert_eq!(run(&input), 0, "non Suica->Metro transaction is left alone");
fn already_categorized_is_untouched() {
let input = build_tx_input("Suica", "Metro", Some("groceries"));
0,
"a transaction that already has a category tag must not be re-tagged"