mod aggregate;
use aggregate::{Command, Ledger};
use distributed::{AggregateBuilder, HashMapRepository};
use proptest::prelude::*;
use tokio::runtime::Runtime;
fn command_strategy() -> impl Strategy<Value = Command> {
prop_oneof![
(1i64..1_000).prop_map(Command::Deposit),
(1i64..1_000).prop_map(Command::Withdraw),
"[a-z]{1,8}".prop_map(Command::Annotate),
]
}
fn commands_strategy() -> impl Strategy<Value = Vec<Command>> {
prop::collection::vec(command_strategy(), 0..40)
}
fn in_memory_ledger(id: &str, commands: &[Command]) -> Ledger {
let mut ledger = Ledger::default();
ledger.open(id.to_string()).expect("open should succeed");
for command in commands {
command.apply(&mut ledger);
}
ledger
}
fn assert_same_domain_state(actual: &Ledger, expected: &Ledger, context: &str) {
assert_eq!(
actual.balance, expected.balance,
"balance mismatch ({context})"
);
assert_eq!(
actual.applied, expected.applied,
"applied mismatch ({context})"
);
assert_eq!(
actual.last_note, expected.last_note,
"last_note mismatch ({context})"
);
assert_eq!(
actual.entity.id(),
expected.entity.id(),
"id mismatch ({context})"
);
}
fn assert_same_persisted_state(actual: &Ledger, expected: &Ledger, context: &str) {
assert_same_domain_state(actual, expected, context);
assert_eq!(
actual.entity.committed_version(),
expected.entity.committed_version(),
"version mismatch ({context})"
);
}
proptest! {
#![proptest_config(ProptestConfig { cases: 96, ..ProptestConfig::default() })]
#[test]
fn reload_reproduces_in_memory_state(commands in commands_strategy()) {
let runtime = Runtime::new().expect("runtime");
runtime.block_on(async move {
let id = "ledger-reload";
let repo = HashMapRepository::new();
let ledger_repo = repo.aggregate::<Ledger>();
let mut expected = in_memory_ledger(id, &commands);
ledger_repo.commit(&mut expected).await.expect("commit");
let reloaded = ledger_repo
.get(id)
.await
.expect("reload")
.expect("ledger should exist");
assert_same_persisted_state(&reloaded, &expected, "reload round-trip");
});
}
#[test]
fn snapshot_hydration_matches_full_replay(
commands in commands_strategy(),
frequency in 1u64..7,
) {
let runtime = Runtime::new().expect("runtime");
runtime.block_on(async move {
let id = "ledger-snapshot";
let base = HashMapRepository::new();
let snap_repo = base.clone().aggregate::<Ledger>().with_snapshots(frequency);
{
let mut ledger = Ledger::default();
ledger.open(id.to_string()).expect("open");
snap_repo.commit(&mut ledger).await.expect("open commit");
}
for command in &commands {
let mut ledger = snap_repo
.get(id)
.await
.expect("load")
.expect("ledger exists");
command.apply(&mut ledger);
snap_repo.commit(&mut ledger).await.expect("cycle commit");
}
let via_snapshot = snap_repo
.get(id)
.await
.expect("snapshot reload")
.expect("ledger exists");
let via_full_replay = base
.aggregate::<Ledger>()
.get(id)
.await
.expect("full replay reload")
.expect("ledger exists");
let expected = in_memory_ledger(id, &commands);
assert_same_domain_state(&via_full_replay, &expected, "full replay vs in-memory");
assert_same_persisted_state(&via_snapshot, &via_full_replay, "snapshot vs full replay");
});
}
}