1
//! Execution tests for the transit category script (`doc/scripts/metro.org`).
2
//!
3
//! The script tags a transaction `category` = `transportation` when its source
4
//! account (the negative-value split) is "Suica", its target account (the
5
//! positive-value split) is "Metro", and it carries no `category` tag yet.
6

            
7
use chrono::Utc;
8
use finance::split::Split;
9
use finance::transaction::Transaction;
10
use scripting::executor::ScriptExecutor;
11
use scripting::format::{ContextType, EntityType};
12
use scripting::nomiscript::{Compiler, Reader, SymbolTable};
13
use scripting::runtime::{EngineOpts, build_engine};
14
use scripting::serializer::{MemorySerializer, TransactionFromArgs};
15
use uuid::Uuid;
16
use wasmtime::Engine;
17

            
18
const METRO_SOURCE: &str = include_str!("../../doc/scripts/metro.nms");
19

            
20
4
fn gc_engine() -> Engine {
21
4
    build_engine(EngineOpts::baseline()).unwrap()
22
4
}
23

            
24
4
fn compile_metro() -> Vec<u8> {
25
4
    let program = Reader::parse(METRO_SOURCE).expect("failed to parse metro.nms");
26
4
    let mut symbols = SymbolTable::with_builtins();
27
4
    let mut compiler = Compiler::new();
28
4
    compiler
29
4
        .compile(&program, &mut symbols)
30
4
        .expect("failed to compile metro.nms")
31
4
}
32

            
33
8
fn split(tx_id: Uuid, value_num: i64) -> Split {
34
8
    Split {
35
8
        id: Uuid::new_v4(),
36
8
        tx_id,
37
8
        account_id: Uuid::new_v4(),
38
8
        commodity_id: Uuid::new_v4(),
39
8
        value_num,
40
8
        value_denom: 100,
41
8
        reconcile_state: None,
42
8
        reconcile_date: None,
43
8
        lot_id: None,
44
8
    }
45
8
}
46

            
47
/// Serialize a two-split transaction-create context. The source split (negative
48
/// value) posts to `source_account`; the target split (positive value) posts to
49
/// `target_account`. When `category` is set, an existing `category` tag is added
50
/// to the transaction.
51
4
fn build_tx_input(source_account: &str, target_account: &str, category: Option<&str>) -> Vec<u8> {
52
4
    let tx = Transaction {
53
4
        id: Uuid::new_v4(),
54
4
        post_date: Utc::now(),
55
4
        enter_date: Utc::now(),
56
4
    };
57
4
    let source = split(tx.id, -5000);
58
4
    let target = split(tx.id, 5000);
59

            
60
4
    let mut ser = MemorySerializer::new();
61
4
    ser.set_context(ContextType::EntityCreate, EntityType::Transaction);
62
4
    let tx_idx = ser.add_transaction_from(TransactionFromArgs {
63
4
        transaction: &tx,
64
4
        is_primary: true,
65
4
        split_count: 2,
66
4
        tag_count: u32::from(category.is_some()),
67
4
        is_multi_currency: false,
68
4
    });
69
4
    ser.set_primary(tx_idx);
70
4
    ser.add_split_from(&source, tx_idx as i32, source_account);
71
4
    ser.add_split_from(&target, tx_idx as i32, target_account);
72
4
    if let Some(value) = category {
73
1
        ser.add_tag(
74
1
            Uuid::new_v4().into_bytes(),
75
1
            tx_idx as i32,
76
1
            false,
77
1
            false,
78
1
            "category",
79
1
            value,
80
1
        );
81
3
    }
82
4
    ser.finalize(4096)
83
4
}
84

            
85
4
fn run(input: &[u8]) -> usize {
86
4
    let wasm = compile_metro();
87
4
    ScriptExecutor::with_engine(gc_engine())
88
4
        .execute(&wasm, input, Some(4096))
89
4
        .expect("execute")
90
4
        .len()
91
4
}
92

            
93
#[test]
94
1
fn suica_to_metro_uncategorized_gets_transportation() {
95
1
    let input = build_tx_input("Suica", "Metro", None);
96
1
    assert_eq!(
97
1
        run(&input),
98
        2,
99
        "Suica->Metro with no category must tag both splits transportation"
100
    );
101
1
}
102

            
103
#[test]
104
1
fn reversed_direction_is_untouched() {
105
    // Metro is the source, Suica the target — not a Suica->Metro fare.
106
1
    let input = build_tx_input("Metro", "Suica", None);
107
1
    assert_eq!(run(&input), 0, "direction matters: source must be Suica");
108
1
}
109

            
110
#[test]
111
1
fn unrelated_accounts_are_untouched() {
112
1
    let input = build_tx_input("Assets:Checking", "Expenses:Dining", None);
113
1
    assert_eq!(run(&input), 0, "non Suica->Metro transaction is left alone");
114
1
}
115

            
116
#[test]
117
1
fn already_categorized_is_untouched() {
118
1
    let input = build_tx_input("Suica", "Metro", Some("groceries"));
119
1
    assert_eq!(
120
1
        run(&input),
121
        0,
122
        "a transaction that already has a category tag must not be re-tagged"
123
    );
124
1
}