Skip to content

Testing Guide

This document describes rustledger's testing infrastructure, how to run tests, and how to add new tests.

Overview

rustledger uses multiple testing systems for different purposes:

SystemPurposeWhen to Use
Unit TestsTest individual functions/modulesDuring development
Integration TestsTest crate-level behaviorBefore PRs
Property TestsVerify invariants with random inputsAlgorithm changes
TLA+ SpecsFormal verification of critical algorithmsBooking/inventory changes
Compatibility TestsCompare against Python beancountNightly, major changes

Quick Start

Run All Tests

bash
# All tests (default features)
cargo test

# All tests with all features
cargo test --all-features

# Specific crate
cargo test -p rustledger-core
cargo test -p rustledger-parser
cargo test -p rustledger-query

Run Specific Test Types

bash
# Unit tests only (skip integration)
cargo test --lib

# Integration tests only
cargo test --test '*'

# Property tests (longer running)
cargo test --all-features proptest

# Run tests matching a pattern
cargo test inventory
cargo test parse_transaction

Test Organization

Directory Structure

rustledger/
├── crates/
│   ├── rustledger-core/
│   │   ├── src/
│   │   │   └── inventory.rs        # Unit tests in #[cfg(test)] modules
│   │   └── tests/
│   │       ├── property_tests.rs   # Proptest-based tests
│   │       ├── tla_proptest.rs     # TLA+ invariant verification
│   │       └── tla_fifo_bug_test.rs
│   ├── rustledger-parser/
│   │   └── tests/
│   │       └── parser_integration_test.rs
│   ├── rustledger-query/
│   │   └── tests/
│   │       └── bql_integration_test.rs
│   ├── rustledger-validate/
│   │   └── tests/
│   │       ├── validation_integration_test.rs
│   │       └── tla_proptest.rs
│   ├── rustledger-loader/
│   │   └── tests/
│   │       ├── loader_test.rs
│   │       └── fixtures/           # Per-crate fixtures
│   ├── rustledger-plugin/
│   │   └── tests/
│   │       └── native_plugins_test.rs
│   └── rustledger/
│       └── tests/
│           ├── integration_test.rs
│           └── fixture_tests.rs
├── tests/
│   ├── compat/
│   │   ├── files/                  # 93 curated compatibility files
│   │   ├── README.md
│   │   └── sources.toml
│   ├── compat-full/                # ~800 files (gitignored, fetched on demand)
│   └── compat-results/             # Test results (gitignored)
├── spec/
│   ├── fixtures/                   # Parser and example fixtures
│   │   ├── booking-scenarios.beancount
│   │   ├── validation-errors.beancount
│   │   ├── examples/
│   │   └── lima-tests/
│   └── tla/                        # TLA+ specifications (19 specs)
│       ├── FIFOCorrect.tla
│       ├── Conservation.tla
│       └── ...
└── scripts/
    ├── compat-test.sh              # Run compatibility tests
    ├── compat-bql-test.sh          # BQL compatibility tests
    └── fetch-compat-test-files.sh  # Download full test suite

Test Types by Crate

CrateUnitIntegrationPropertyTLA+
rustledger-coreYes-YesYes
rustledger-parserYesYes--
rustledger-queryYesYes--
rustledger-validateYesYesYesYes
rustledger-loaderYesYes--
rustledger-pluginYesYes--
rustledger-bookingYes---
rustledgerYesYes--

Test Systems in Detail

1. Unit Tests

Located in #[cfg(test)] modules within source files. Test individual functions and edge cases.

rust
// In src/inventory.rs
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add_position() {
        let mut inv = Inventory::new();
        inv.add(Position::simple(Amount::new(dec!(100), "USD")));
        assert_eq!(inv.len(), 1);
    }
}

Run:

bash
cargo test -p rustledger-core inventory

2. Integration Tests

Located in crates/*/tests/. Test crate-level functionality with realistic inputs.

Key integration tests:

  • parser_integration_test.rs - Parser with snapshot testing (insta)
  • bql_integration_test.rs - BQL query execution
  • validation_integration_test.rs - Full validation pipeline
  • loader_test.rs - File loading and includes
  • native_plugins_test.rs - Plugin registration and execution

Run:

bash
cargo test -p rustledger-parser --test parser_integration_test
cargo test -p rustledger-query --test bql_integration_test

3. Property Tests (Proptest)

Use random inputs to verify invariants hold across many cases.

Located in:

  • crates/rustledger-core/tests/property_tests.rs
  • crates/rustledger-core/tests/tla_proptest.rs
  • crates/rustledger-validate/tests/tla_proptest.rs

Example:

rust
proptest! {
    #[test]
    fn inventory_units_always_non_negative(
        amounts in prop::collection::vec(1i64..100, 1..10)
    ) {
        let mut inv = Inventory::new();
        for a in amounts {
            inv.add(Position::simple(Amount::new(Decimal::from(a), "USD")));
        }
        // Invariant: all positions have positive units
        for pos in inv.iter() {
            prop_assert!(pos.units().number() > Decimal::ZERO);
        }
    }
}

Run:

bash
cargo test -p rustledger-core proptest
cargo test -p rustledger-validate proptest

4. TLA+ Model Checking

Formal specifications in spec/tla/ verify critical algorithms:

SpecPurpose
Conservation.tlaValue conservation in transactions
DoubleEntry.tlaDouble-entry bookkeeping invariant
FIFOCorrect.tlaFIFO booking method correctness
LIFOCorrect.tlaLIFO booking method correctness
AVERAGECorrect.tlaAverage cost booking
Interpolation.tlaMissing amount interpolation
ValidationCorrect.tlaValidation rules

Run locally (requires Java):

bash
# In nix develop
java -jar ~/tla2tools.jar -config spec/tla/Conservation.cfg spec/tla/Conservation.tla

CI: Runs on changes to spec/tla/, inventory.rs, or booking code.

Proptest integration: TLA+ invariants are also verified via proptest:

bash
cargo test -p rustledger-core tla_proptest

5. Compatibility Tests

Compare rustledger against Python beancount to ensure identical behavior.

Curated files (93 files): Committed to tests/compatibility/files/

bash
./scripts/compat-test.sh tests/compatibility/files

Full suite (~800 files): Downloaded on demand

bash
# Inside nix develop
./scripts/fetch-compat-test-files.sh
./scripts/compat-test.sh

BQL compatibility:

bash
./scripts/compat-bql-test.sh

CI: Runs nightly at 3 AM UTC via .github/workflows/compat.yml

Test Fixtures

Parser Fixtures (tests/fixtures/)

FilePurpose
booking-scenarios.beancountCost basis booking edge cases
validation-errors.beancountAll validation error types
syntax-edge-cases.beancountParser edge cases
examples/Complete example ledgers
lima-tests/beancount-parser-lima test cases

Crate Fixtures (crates/*/tests/fixtures/)

Per-crate test data:

  • rustledger-loader/tests/fixtures/ - Include paths, cycles, errors
  • rustledger/tests/fixtures/ - CLI integration test files

Compatibility Fixtures (tests/compatibility/files/)

Organized by category:

  • parser/ - Parser edge cases (~25 files)
  • validation/ - Validation scenarios (~20 files)
  • plugins/ - Plugin configurations (~5 files)
  • real-world/ - Community examples (~35 files)
  • edge-cases/ - Known differences (~10 files)

CI Workflows

WorkflowTriggerTests Run
ci.ymlPush, PRcargo test --all-features
compat.ymlNightlyFull compatibility suite
tla.ymlChanges to TLA+/inventory/bookingTLA+ model checking

Adding New Tests

Unit Test

Add to the #[cfg(test)] module in the source file:

rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_my_feature() {
        // ...
    }
}

Integration Test

Create or add to crates/<crate>/tests/<name>_test.rs:

rust
use rustledger_core::*;

#[test]
fn test_integration_scenario() {
    // ...
}

Property Test

Add to crates/<crate>/tests/property_tests.rs:

rust
use proptest::prelude::*;

proptest! {
    #[test]
    fn my_invariant_holds(input in any::<u32>()) {
        // ... property assertions
    }
}

Fixture File

  1. Determine the appropriate location:

    • Parser/syntax: tests/fixtures/
    • Compatibility: tests/compatibility/files/<category>/
    • Crate-specific: crates/<crate>/tests/fixtures/
  2. Create a minimal .beancount file that reproduces the case

  3. Update tests/compatibility/sources.toml if adding compatibility files

TLA+ Specification

  1. Create spec/tla/MySpec.tla with the specification
  2. Create spec/tla/MySpec.cfg with model configuration
  3. Add model check step to .github/workflows/tla.yml
  4. Optionally add proptest integration in tla_proptest.rs

Snapshot Testing

Parser tests use insta for snapshot testing:

rust
#[test]
fn test_parse_transaction() {
    let result = parse("2024-01-01 * \"Test\"\n  Assets:Cash 100 USD");
    insta::assert_debug_snapshot!(result);
}

Update snapshots:

bash
cargo insta review

Test Coverage

Generate coverage report:

bash
# Using cargo-llvm-cov
cargo llvm-cov --all-features --html
open target/llvm-cov/html/index.html

Troubleshooting

Tests timeout or run slowly

Property tests can take longer. Set a shorter case count:

bash
PROPTEST_CASES=10 cargo test proptest

Compatibility tests fail to find beancount

Run inside the nix development shell:

bash
nix develop
./scripts/compat-test.sh

TLA+ model checking fails

Ensure Java 17+ is installed:

bash
java -version  # Should be 17+

Snapshot tests fail after expected changes

Review and update snapshots:

bash
cargo insta test
cargo insta review

See Also