From d1a9e1509c8fc3b8d445a4ce0bdbf6d9c4037ab5 Mon Sep 17 00:00:00 2001 From: codingp110 Date: Tue, 19 Aug 2025 10:36:50 +0530 Subject: [PATCH 1/3] feat: add utilities for testing persistence Added the following functions to test if persistence of `bdk_chain` is happening correctly. - `persist_txgraph_changeset` - `persist_indexer_changeset` - `persist_local_chain_changeset` which leverage a more general `persist_changeset`. In order to not panic and instead return errors raised by the persistence backend being tested, introduced `PersistErr`. To ensure downcasting and to allow usage of anyhow, `PersisterErr::Persister` requires an error of with 'static + Send + Sync. --- crates/chain/Cargo.toml | 2 + crates/chain/tests/test_rusqlite_impl.rs | 109 ++++++++ crates/file_store/Cargo.toml | 3 + crates/file_store/src/store.rs | 65 +++++ crates/testenv/Cargo.toml | 4 +- crates/testenv/src/lib.rs | 3 + crates/testenv/src/persist_test_utils.rs | 308 +++++++++++++++++++++++ crates/testenv/src/utils.rs | 73 +++++- 8 files changed, 564 insertions(+), 3 deletions(-) create mode 100644 crates/chain/tests/test_rusqlite_impl.rs create mode 100644 crates/testenv/src/persist_test_utils.rs diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index 949f177c8b..ac449b19c1 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -29,6 +29,8 @@ rand = "0.8" proptest = "1.2.0" bdk_testenv = { path = "../testenv" } criterion = { version = "0.7" } +tempfile = "3" +anyhow = "1.0.102" [features] default = ["std", "miniscript"] diff --git a/crates/chain/tests/test_rusqlite_impl.rs b/crates/chain/tests/test_rusqlite_impl.rs new file mode 100644 index 0000000000..a9f377b1f3 --- /dev/null +++ b/crates/chain/tests/test_rusqlite_impl.rs @@ -0,0 +1,109 @@ +#![cfg(feature = "rusqlite")] +use bdk_chain::{keychain_txout, local_chain, tx_graph, ConfirmationBlockTime}; +use bdk_testenv::persist_test_utils::{ + persist_indexer_changeset, persist_local_chain_changeset, persist_txgraph_changeset, +}; + +fn tx_graph_changeset_init( + db: &mut rusqlite::Connection, +) -> rusqlite::Result> { + let db_tx = db.transaction()?; + tx_graph::ChangeSet::::init_sqlite_tables(&db_tx)?; + let changeset = tx_graph::ChangeSet::::from_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(changeset) +} + +fn tx_graph_changeset_persist( + db: &mut rusqlite::Connection, + changeset: &tx_graph::ChangeSet, +) -> rusqlite::Result<()> { + let db_tx = db.transaction()?; + changeset.persist_to_sqlite(&db_tx)?; + db_tx.commit() +} + +fn keychain_txout_changeset_init( + db: &mut rusqlite::Connection, +) -> rusqlite::Result { + let db_tx = db.transaction()?; + keychain_txout::ChangeSet::init_sqlite_tables(&db_tx)?; + let changeset = keychain_txout::ChangeSet::from_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(changeset) +} + +fn keychain_txout_changeset_persist( + db: &mut rusqlite::Connection, + changeset: &keychain_txout::ChangeSet, +) -> rusqlite::Result<()> { + let db_tx = db.transaction()?; + changeset.persist_to_sqlite(&db_tx)?; + db_tx.commit() +} + +fn local_chain_changeset_init( + db: &mut rusqlite::Connection, +) -> rusqlite::Result { + let db_tx = db.transaction()?; + local_chain::ChangeSet::init_sqlite_tables(&db_tx)?; + let changeset = local_chain::ChangeSet::from_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(changeset) +} + +fn local_chain_changeset_persist( + db: &mut rusqlite::Connection, + changeset: &local_chain::ChangeSet, +) -> rusqlite::Result<()> { + let db_tx = db.transaction()?; + changeset.persist_to_sqlite(&db_tx)?; + db_tx.commit() +} + +#[test] +fn txgraph_is_persisted() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir().unwrap(); + Ok(persist_txgraph_changeset::( + || { + Ok(rusqlite::Connection::open( + temp_dir.path().join("wallet.sqlite"), + )?) + }, + |db| Ok(tx_graph_changeset_init(db)?), + |db, changeset| Ok(tx_graph_changeset_persist(db, changeset)?), + )?) +} + +#[test] +fn indexer_is_persisted() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir().unwrap(); + Ok(persist_indexer_changeset::( + || { + Ok(rusqlite::Connection::open( + temp_dir.path().join("wallet.sqlite"), + )?) + }, + |db| Ok(keychain_txout_changeset_init(db)?), + |db, changeset| Ok(keychain_txout_changeset_persist(db, changeset)?), + )?) +} + +#[test] +fn local_chain_is_persisted() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir().unwrap(); + Ok(persist_local_chain_changeset::< + rusqlite::Connection, + _, + _, + _, + >( + || { + Ok(rusqlite::Connection::open( + temp_dir.path().join("wallet.sqlite"), + )?) + }, + |db| Ok(local_chain_changeset_init(db)?), + |db, changeset| Ok(local_chain_changeset_persist(db, changeset)?), + )?) +} diff --git a/crates/file_store/Cargo.toml b/crates/file_store/Cargo.toml index 8fbdc358de..c7b5e336fc 100644 --- a/crates/file_store/Cargo.toml +++ b/crates/file_store/Cargo.toml @@ -21,3 +21,6 @@ serde = { version = "1", features = ["derive"] } [dev-dependencies] tempfile = "3" +anyhow = { version = "1.0.102", default-features = false} +bdk_testenv = {path = "../testenv"} +bdk_chain = { path = "../chain", version = "0.23.1", default-features = false, features = ["serde"]} \ No newline at end of file diff --git a/crates/file_store/src/store.rs b/crates/file_store/src/store.rs index 858b9d2cdf..949fd3c3ae 100644 --- a/crates/file_store/src/store.rs +++ b/crates/file_store/src/store.rs @@ -295,6 +295,11 @@ mod test { const TEST_MAGIC_BYTES: [u8; TEST_MAGIC_BYTES_LEN] = [98, 100, 107, 102, 115, 49, 49, 49, 49, 49, 49, 49]; + use bdk_chain::{keychain_txout, local_chain, tx_graph, ConfirmationBlockTime}; + use bdk_testenv::persist_test_utils::{ + persist_indexer_changeset, persist_local_chain_changeset, persist_txgraph_changeset, + }; + type TestChangeSet = BTreeSet; /// Check behavior of [`Store::create`] and [`Store::load`]. @@ -599,4 +604,64 @@ mod test { // current position matches EOF assert_eq!(current_pointer, expected_pointer); } + + #[test] + fn txgraph_is_persisted() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir().unwrap(); + Ok(persist_txgraph_changeset::< + Store>, + _, + _, + _, + >( + || { + Ok(Store::create( + &TEST_MAGIC_BYTES, + temp_dir.path().join("store.db"), + )?) + }, + |db| Ok(db.dump().map(Option::unwrap_or_default)?), + |db, changeset| Ok(db.append(changeset)?), + )?) + } + + #[test] + fn indexer_is_persisted() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir().unwrap(); + Ok(persist_indexer_changeset::< + Store, + _, + _, + _, + >( + || { + Ok(Store::create( + &TEST_MAGIC_BYTES, + temp_dir.path().join("store.db"), + )?) + }, + |db| Ok(db.dump().map(Option::unwrap_or_default)?), + |db, changeset| Ok(db.append(changeset)?), + )?) + } + + #[test] + fn local_chain_is_persisted() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir().unwrap(); + Ok(persist_local_chain_changeset::< + Store, + _, + _, + _, + >( + || { + Ok(Store::create( + &TEST_MAGIC_BYTES, + temp_dir.path().join("store.db"), + )?) + }, + |db| Ok(db.dump().map(Option::unwrap_or_default)?), + |db, changeset| Ok(db.append(changeset)?), + )?) + } } diff --git a/crates/testenv/Cargo.toml b/crates/testenv/Cargo.toml index 6cedcf4d30..4f497e06ae 100644 --- a/crates/testenv/Cargo.toml +++ b/crates/testenv/Cargo.toml @@ -24,10 +24,10 @@ bitcoin = { version = "0.32.0", default-features = false } bdk_testenv = { path = "." } [features] -default = ["std", "download"] +default = ["std", "download", "miniscript"] download = ["electrsd/bitcoind_download", "electrsd/bitcoind_28_2", "electrsd/esplora_a33e97e1"] std = ["bdk_chain/std", "bitcoin/rand-std"] serde = ["bdk_chain/serde"] - +miniscript = ["bdk_chain/miniscript"] [package.metadata.docs.rs] no-default-features = true diff --git a/crates/testenv/src/lib.rs b/crates/testenv/src/lib.rs index 3c3f8a6f48..2a13a3766c 100644 --- a/crates/testenv/src/lib.rs +++ b/crates/testenv/src/lib.rs @@ -1,5 +1,6 @@ #![cfg_attr(coverage_nightly, feature(coverage_attribute))] +pub mod persist_test_utils; pub mod utils; use anyhow::Context; @@ -14,6 +15,8 @@ use core::time::Duration; use electrsd::bitcoind::mtype::GetBlockTemplate; use electrsd::bitcoind::{TemplateRequest, TemplateRules}; +extern crate alloc; + pub use electrsd; pub use electrsd::bitcoind; pub use electrsd::bitcoind::anyhow; diff --git a/crates/testenv/src/persist_test_utils.rs b/crates/testenv/src/persist_test_utils.rs new file mode 100644 index 0000000000..9f6c6c0e0f --- /dev/null +++ b/crates/testenv/src/persist_test_utils.rs @@ -0,0 +1,308 @@ +//! This module provides utility functions for testing custom persistence backends. +use crate::{block_id, hash}; +use alloc::sync::Arc; +use bdk_chain::{ + bitcoin::{self, OutPoint}, + local_chain, tx_graph, ConfirmationBlockTime, Merge, +}; + +#[cfg(feature = "miniscript")] +use bdk_chain::{indexer::keychain_txout, DescriptorExt, SpkIterator}; +use core::{ + cmp::PartialEq, + fmt::{Debug, Display}, +}; + +use core::error::Error as Err; + +use crate::utils::{create_test_tx, create_txout}; + +#[cfg(feature = "miniscript")] +use crate::utils::{parse_descriptor, spk_at_index}; + +const ADDRS: [&str; 2] = [ + "bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5", + "bcrt1q8an5jfmpq8w2hr648nn34ecf9zdtxk0qyqtrfl", +]; + +/// Errors caused by a failed persister test. +#[derive(Debug)] +pub enum PersistErr { + /// ChangeSet Mismatch + ChangeSetMismatch { + /// the resulting changeset + got: Box, + /// the expected changeset + expected: Box, + }, + /// Errors thrown by underlying persistence backend. + Persister(Box), +} + +impl From> for PersistErr { + fn from(value: Box) -> Self { + PersistErr::Persister(value) + } +} + +impl Display for PersistErr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + PersistErr::ChangeSetMismatch { got, expected } => write!( + f, + "ChangeSet mismatch! Got: {:?}, Expected: {:?}", + got, expected + ), + PersistErr::Persister(err) => write!(f, "{err}"), + } + } +} + +impl Err for PersistErr {} + +/// Tests if `ChangeSet` is being persisted correctly. +/// +/// We create a dummy `ChangeSet`, persist it and check if loaded `ChangeSet` matches +/// the persisted one. We then create another such dummy `ChangeSet`, persist it and load it to +/// check if merged `ChangeSet` is returned. +pub fn persist_changeset( + create_store: CreateStore, + initialize: Initialize, + persist: Persist, + changesets: &[CS], +) -> Result<(), PersistErr> +where + CS: Debug + PartialEq + Default + Merge + Clone, + CreateStore: Fn() -> Result>, + Initialize: Fn(&mut Store) -> Result>, + Persist: Fn(&mut Store, &CS) -> Result<(), Box>, +{ + let mut store = create_store()?; + + let init_changeset = initialize(&mut store)?; + + if init_changeset != CS::default() { + return Err(PersistErr::ChangeSetMismatch { + expected: Box::new(CS::default()), + got: Box::new(init_changeset), + }); + } + + let mut merged_changeset = CS::default(); + + for changeset in changesets { + persist(&mut store, changeset)?; + merged_changeset.merge(changeset.clone()); + } + + let persisted_changeset = initialize(&mut store)?; + + if persisted_changeset != merged_changeset { + return Err(PersistErr::ChangeSetMismatch { + expected: Box::new(merged_changeset), + got: Box::new(persisted_changeset), + }); + } + + Ok(()) +} + +/// Tests if [`TxGraph`](tx_graph::TxGraph) is being persisted correctly. +/// +/// We create a dummy [`tx_graph::ChangeSet`], persist it and check if loaded +/// `ChangeSet` matches the persisted one. We then create another such dummy `ChangeSet`, persist it +/// and load it to check if merged `ChangeSet` is returned. +pub fn persist_txgraph_changeset( + create_store: CreateStore, + initialize: Initialize, + persist: Persist, +) -> Result<(), PersistErr>> +where + CreateStore: Fn() -> Result>, + Initialize: Fn( + &mut Store, + ) -> Result< + tx_graph::ChangeSet, + Box, + >, + Persist: Fn( + &mut Store, + &tx_graph::ChangeSet, + ) -> Result<(), Box>, +{ + let changesets = tx_graph_changesets(); + persist_changeset::< + tx_graph::ChangeSet, + Store, + CreateStore, + Initialize, + Persist, + >(create_store, initialize, persist, &changesets) +} + +/// Tests if [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex) is being persisted +/// correctly. +/// +/// See [`persist_txgraph_changeset`]. +#[cfg(feature = "miniscript")] +pub fn persist_indexer_changeset( + create_store: CreateStore, + initialize: Initialize, + persist: Persist, +) -> Result<(), PersistErr> +where + CreateStore: Fn() -> Result>, + Initialize: + Fn(&mut Store) -> Result>, + Persist: Fn( + &mut Store, + &keychain_txout::ChangeSet, + ) -> Result<(), Box>, +{ + let changesets = keychain_txout_changesets(); + persist_changeset::( + create_store, + initialize, + persist, + &changesets, + ) +} + +/// Tests if [`LocalChain`](local_chain::LocalChain) is being persisted correctly. +/// +/// See [`persist_txgraph_changeset`]. +pub fn persist_local_chain_changeset( + create_store: CreateStore, + initialize: Initialize, + persist: Persist, +) -> Result<(), PersistErr> +where + CreateStore: Fn() -> Result>, + Initialize: + Fn(&mut Store) -> Result>, + Persist: + Fn(&mut Store, &local_chain::ChangeSet) -> Result<(), Box>, +{ + let changesets = local_chain_changesets(); + persist_changeset::( + create_store, + initialize, + persist, + &changesets, + ) +} + +/// Get two [`tx_graph::ChangeSet`](tx_graph::ChangeSet)s. +fn tx_graph_changesets() -> [tx_graph::ChangeSet; 2] { + use tx_graph::ChangeSet; + + let tx1 = Arc::new(create_test_tx( + [hash!("BTC")], + [0], + [30_000], + [ADDRS[0]], + 1, + 0, + )); + + let conf_anchor: ConfirmationBlockTime = ConfirmationBlockTime { + block_id: block_id!(910425, "Rust"), + confirmation_time: 1755416660, + }; + + let tx_graph_changeset1 = ChangeSet:: { + txs: [tx1.clone()].into(), + txouts: [ + (OutPoint::new(hash!("BDK"), 0), create_txout(1300, ADDRS[1])), + ( + OutPoint::new(hash!("Bitcoin_fixes_things"), 0), + create_txout(1400, ADDRS[1]), + ), + ] + .into(), + anchors: [(conf_anchor, tx1.compute_txid())].into(), + last_seen: [(tx1.compute_txid(), 1755416650)].into(), + first_seen: [(tx1.compute_txid(), 1755416655)].into(), + last_evicted: [(tx1.compute_txid(), 1755416660)].into(), + }; + + let tx2 = Arc::new(create_test_tx( + [tx1.compute_txid()], + [0], + [20_000], + [ADDRS[0]], + 1, + 0, + )); + + let conf_anchor: ConfirmationBlockTime = ConfirmationBlockTime { + block_id: block_id!(910426, "BOSS"), + confirmation_time: 1755416700, + }; + + let tx_graph_changeset2 = ChangeSet:: { + txs: [tx2.clone()].into(), + txouts: [( + OutPoint::new(hash!("Magical_Bitcoin"), 0), + create_txout(10000, ADDRS[1]), + )] + .into(), + anchors: [(conf_anchor, tx2.compute_txid())].into(), + last_seen: [(tx2.compute_txid(), 1755416700)].into(), + first_seen: [(tx2.compute_txid(), 1755416670)].into(), + last_evicted: [(tx2.compute_txid(), 1755416760)].into(), + }; + + [tx_graph_changeset1, tx_graph_changeset2] +} + +/// Get two [`keychain_txout::ChangeSet`](keychain_txout::ChangeSet)s. +#[cfg(feature = "miniscript")] +fn keychain_txout_changesets() -> [keychain_txout::ChangeSet; 2] { + use crate::utils::DESCRIPTORS; + use keychain_txout::ChangeSet; + + let descriptor_ids = DESCRIPTORS.map(|d| parse_descriptor(d).0.descriptor_id()); + let descs = DESCRIPTORS.map(|desc| parse_descriptor(desc).0); + + let changeset = ChangeSet { + last_revealed: [(descriptor_ids[0], 1), (descriptor_ids[1], 100)].into(), + spk_cache: [ + ( + descriptor_ids[0], + SpkIterator::new_with_range(&descs[0], 0..=26).collect(), + ), + ( + descriptor_ids[1], + SpkIterator::new_with_range(&descs[1], 0..=125).collect(), + ), + ] + .into(), + }; + + let changeset_new = ChangeSet { + last_revealed: [(descriptor_ids[0], 2)].into(), + spk_cache: [( + descriptor_ids[0], + [(27, spk_at_index(&descs[0], 27))].into(), + )] + .into(), + }; + + [changeset, changeset_new] +} + +/// Get two [`local_chain::ChangeSet`](local_chain::ChangeSet)s. +fn local_chain_changesets() -> [local_chain::ChangeSet; 2] { + use local_chain::ChangeSet; + + let changeset = ChangeSet { + blocks: [(910425, Some(hash!("B"))), (910426, Some(hash!("D")))].into(), + }; + + let changeset_new = ChangeSet { + blocks: [(910427, Some(hash!("K")))].into(), + }; + + [changeset, changeset_new] +} diff --git a/crates/testenv/src/utils.rs b/crates/testenv/src/utils.rs index 93ca1f217f..ee4df4176f 100644 --- a/crates/testenv/src/utils.rs +++ b/crates/testenv/src/utils.rs @@ -1,4 +1,10 @@ -use bdk_chain::bitcoin; +use bdk_chain::bitcoin::{ + self, absolute, transaction, Address, Amount, OutPoint, Transaction, TxIn, TxOut, Txid, +}; +use core::str::FromStr; + +#[cfg(feature = "miniscript")] +use bdk_chain::miniscript::{descriptor::KeyMap, Descriptor, DescriptorPublicKey}; #[allow(unused_macros)] #[macro_export] @@ -77,6 +83,71 @@ pub fn new_tx(lt: u32) -> bitcoin::Transaction { } } +/// Utility function to create a [`TxOut`] given amount (in satoshis) and address. +pub fn create_txout(sats: u64, addr: &str) -> TxOut { + TxOut { + value: Amount::from_sat(sats), + script_pubkey: Address::from_str(addr) + .unwrap() + .assume_checked() + .script_pubkey(), + } +} + +/// Utility function to create a transaction given txids, vouts of inputs and amounts (in satoshis), +/// addresses of outputs. +/// +/// The locktime should be in the form given to `OP_CHEKCLOCKTIMEVERIFY`. +pub fn create_test_tx( + txids: impl IntoIterator, + vouts: impl IntoIterator, + amounts: impl IntoIterator, + addrs: impl IntoIterator, + version: u32, + locktime: u32, +) -> Transaction { + let input_vec = core::iter::zip(txids, vouts) + .map(|(txid, vout)| TxIn { + previous_output: OutPoint::new(txid, vout), + ..TxIn::default() + }) + .collect(); + let output_vec = core::iter::zip(amounts, addrs) + .map(|(amount, addr)| create_txout(amount, addr)) + .collect(); + let version = transaction::Version::non_standard(version as i32); + assert!(version.is_standard()); + let lock_time = absolute::LockTime::from_consensus(locktime); + assert_eq!(lock_time.to_consensus_u32(), locktime); + Transaction { + version, + lock_time, + input: input_vec, + output: output_vec, + } +} + +/// Generates `script_pubkey` corresponding to `index` on keychain of `descriptor`. +#[cfg(feature = "miniscript")] +pub fn spk_at_index( + descriptor: &Descriptor, + index: u32, +) -> bdk_chain::bitcoin::ScriptBuf { + use bdk_chain::bitcoin::key::Secp256k1; + descriptor + .derived_descriptor(&Secp256k1::verification_only(), index) + .expect("must derive") + .script_pubkey() +} + +/// Parses a descriptor string. +#[cfg(feature = "miniscript")] +pub fn parse_descriptor(descriptor: &str) -> (Descriptor, KeyMap) { + use bdk_chain::bitcoin::key::Secp256k1; + let secp = Secp256k1::signing_only(); + Descriptor::::parse_descriptor(&secp, descriptor).unwrap() +} + #[allow(unused)] pub const DESCRIPTORS: [&str; 7] = [ "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)", From f2a3f873351e171bbc08a1dcf37c5e5cedea67c6 Mon Sep 17 00:00:00 2001 From: codingp110 Date: Sun, 17 May 2026 22:24:16 +0530 Subject: [PATCH 2/3] fix Renamed `persist_changesets` to `assert_persist_changesets`. Removed changeset specific persistence test functions and made the generic util pub instead. Also made apis providing dummy changeset pub. Also removed unnecessary functions. --- crates/chain/tests/test_rusqlite_impl.rs | 122 ++++++++++------------- crates/file_store/Cargo.toml | 2 +- crates/file_store/src/store.rs | 31 ++---- crates/testenv/src/persist_test_utils.rs | 99 ++---------------- 4 files changed, 69 insertions(+), 185 deletions(-) diff --git a/crates/chain/tests/test_rusqlite_impl.rs b/crates/chain/tests/test_rusqlite_impl.rs index a9f377b1f3..ebfc51c7dc 100644 --- a/crates/chain/tests/test_rusqlite_impl.rs +++ b/crates/chain/tests/test_rusqlite_impl.rs @@ -1,109 +1,87 @@ #![cfg(feature = "rusqlite")] use bdk_chain::{keychain_txout, local_chain, tx_graph, ConfirmationBlockTime}; use bdk_testenv::persist_test_utils::{ - persist_indexer_changeset, persist_local_chain_changeset, persist_txgraph_changeset, + assert_persist_changesets, keychain_txout_changesets, local_chain_changesets, + tx_graph_changesets, }; -fn tx_graph_changeset_init( - db: &mut rusqlite::Connection, -) -> rusqlite::Result> { - let db_tx = db.transaction()?; - tx_graph::ChangeSet::::init_sqlite_tables(&db_tx)?; - let changeset = tx_graph::ChangeSet::::from_sqlite(&db_tx)?; - db_tx.commit()?; - Ok(changeset) -} - -fn tx_graph_changeset_persist( - db: &mut rusqlite::Connection, - changeset: &tx_graph::ChangeSet, -) -> rusqlite::Result<()> { - let db_tx = db.transaction()?; - changeset.persist_to_sqlite(&db_tx)?; - db_tx.commit() -} - -fn keychain_txout_changeset_init( - db: &mut rusqlite::Connection, -) -> rusqlite::Result { - let db_tx = db.transaction()?; - keychain_txout::ChangeSet::init_sqlite_tables(&db_tx)?; - let changeset = keychain_txout::ChangeSet::from_sqlite(&db_tx)?; - db_tx.commit()?; - Ok(changeset) -} - -fn keychain_txout_changeset_persist( - db: &mut rusqlite::Connection, - changeset: &keychain_txout::ChangeSet, -) -> rusqlite::Result<()> { - let db_tx = db.transaction()?; - changeset.persist_to_sqlite(&db_tx)?; - db_tx.commit() -} - -fn local_chain_changeset_init( - db: &mut rusqlite::Connection, -) -> rusqlite::Result { - let db_tx = db.transaction()?; - local_chain::ChangeSet::init_sqlite_tables(&db_tx)?; - let changeset = local_chain::ChangeSet::from_sqlite(&db_tx)?; - db_tx.commit()?; - Ok(changeset) -} - -fn local_chain_changeset_persist( - db: &mut rusqlite::Connection, - changeset: &local_chain::ChangeSet, -) -> rusqlite::Result<()> { - let db_tx = db.transaction()?; - changeset.persist_to_sqlite(&db_tx)?; - db_tx.commit() -} - #[test] fn txgraph_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); - Ok(persist_txgraph_changeset::( + let changesets = tx_graph_changesets(); + Ok(assert_persist_changesets( || { Ok(rusqlite::Connection::open( temp_dir.path().join("wallet.sqlite"), )?) }, - |db| Ok(tx_graph_changeset_init(db)?), - |db, changeset| Ok(tx_graph_changeset_persist(db, changeset)?), + |db| { + let db_tx = db.transaction()?; + tx_graph::ChangeSet::::init_sqlite_tables(&db_tx)?; + let changeset = tx_graph::ChangeSet::::from_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(changeset) + }, + |db, changeset| { + let db_tx = db.transaction()?; + changeset.persist_to_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(()) + }, + &changesets, )?) } #[test] fn indexer_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); - Ok(persist_indexer_changeset::( + let changesets = keychain_txout_changesets(); + Ok(assert_persist_changesets( || { Ok(rusqlite::Connection::open( temp_dir.path().join("wallet.sqlite"), )?) }, - |db| Ok(keychain_txout_changeset_init(db)?), - |db, changeset| Ok(keychain_txout_changeset_persist(db, changeset)?), + |db| { + let db_tx = db.transaction()?; + keychain_txout::ChangeSet::init_sqlite_tables(&db_tx)?; + let changeset = keychain_txout::ChangeSet::from_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(changeset) + }, + |db, changeset| { + let db_tx = db.transaction()?; + changeset.persist_to_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(()) + }, + &changesets, )?) } #[test] fn local_chain_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); - Ok(persist_local_chain_changeset::< - rusqlite::Connection, - _, - _, - _, - >( + let changesets = local_chain_changesets(); + Ok(assert_persist_changesets( || { Ok(rusqlite::Connection::open( temp_dir.path().join("wallet.sqlite"), )?) }, - |db| Ok(local_chain_changeset_init(db)?), - |db, changeset| Ok(local_chain_changeset_persist(db, changeset)?), + |db| { + let db_tx = db.transaction()?; + local_chain::ChangeSet::init_sqlite_tables(&db_tx)?; + let changeset = local_chain::ChangeSet::from_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(changeset) + }, + |db, changeset| { + let db_tx = db.transaction()?; + changeset.persist_to_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(()) + }, + &changesets, )?) } diff --git a/crates/file_store/Cargo.toml b/crates/file_store/Cargo.toml index c7b5e336fc..d446034afb 100644 --- a/crates/file_store/Cargo.toml +++ b/crates/file_store/Cargo.toml @@ -21,6 +21,6 @@ serde = { version = "1", features = ["derive"] } [dev-dependencies] tempfile = "3" -anyhow = { version = "1.0.102", default-features = false} +anyhow = { version = "1", default-features = false} bdk_testenv = {path = "../testenv"} bdk_chain = { path = "../chain", version = "0.23.1", default-features = false, features = ["serde"]} \ No newline at end of file diff --git a/crates/file_store/src/store.rs b/crates/file_store/src/store.rs index 949fd3c3ae..dfed65bd5d 100644 --- a/crates/file_store/src/store.rs +++ b/crates/file_store/src/store.rs @@ -295,9 +295,9 @@ mod test { const TEST_MAGIC_BYTES: [u8; TEST_MAGIC_BYTES_LEN] = [98, 100, 107, 102, 115, 49, 49, 49, 49, 49, 49, 49]; - use bdk_chain::{keychain_txout, local_chain, tx_graph, ConfirmationBlockTime}; use bdk_testenv::persist_test_utils::{ - persist_indexer_changeset, persist_local_chain_changeset, persist_txgraph_changeset, + assert_persist_changesets, keychain_txout_changesets, local_chain_changesets, + tx_graph_changesets, }; type TestChangeSet = BTreeSet; @@ -608,12 +608,8 @@ mod test { #[test] fn txgraph_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); - Ok(persist_txgraph_changeset::< - Store>, - _, - _, - _, - >( + let changesets = tx_graph_changesets(); + Ok(assert_persist_changesets( || { Ok(Store::create( &TEST_MAGIC_BYTES, @@ -622,18 +618,15 @@ mod test { }, |db| Ok(db.dump().map(Option::unwrap_or_default)?), |db, changeset| Ok(db.append(changeset)?), + &changesets, )?) } #[test] fn indexer_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); - Ok(persist_indexer_changeset::< - Store, - _, - _, - _, - >( + let changesets = keychain_txout_changesets(); + Ok(assert_persist_changesets( || { Ok(Store::create( &TEST_MAGIC_BYTES, @@ -642,18 +635,15 @@ mod test { }, |db| Ok(db.dump().map(Option::unwrap_or_default)?), |db, changeset| Ok(db.append(changeset)?), + &changesets, )?) } #[test] fn local_chain_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); - Ok(persist_local_chain_changeset::< - Store, - _, - _, - _, - >( + let changesets = local_chain_changesets(); + Ok(assert_persist_changesets( || { Ok(Store::create( &TEST_MAGIC_BYTES, @@ -662,6 +652,7 @@ mod test { }, |db| Ok(db.dump().map(Option::unwrap_or_default)?), |db, changeset| Ok(db.append(changeset)?), + &changesets, )?) } } diff --git a/crates/testenv/src/persist_test_utils.rs b/crates/testenv/src/persist_test_utils.rs index 9f6c6c0e0f..3013b83b71 100644 --- a/crates/testenv/src/persist_test_utils.rs +++ b/crates/testenv/src/persist_test_utils.rs @@ -65,7 +65,7 @@ impl Err for PersistErr {} /// We create a dummy `ChangeSet`, persist it and check if loaded `ChangeSet` matches /// the persisted one. We then create another such dummy `ChangeSet`, persist it and load it to /// check if merged `ChangeSet` is returned. -pub fn persist_changeset( +pub fn assert_persist_changesets( create_store: CreateStore, initialize: Initialize, persist: Persist, @@ -107,93 +107,8 @@ where Ok(()) } -/// Tests if [`TxGraph`](tx_graph::TxGraph) is being persisted correctly. -/// -/// We create a dummy [`tx_graph::ChangeSet`], persist it and check if loaded -/// `ChangeSet` matches the persisted one. We then create another such dummy `ChangeSet`, persist it -/// and load it to check if merged `ChangeSet` is returned. -pub fn persist_txgraph_changeset( - create_store: CreateStore, - initialize: Initialize, - persist: Persist, -) -> Result<(), PersistErr>> -where - CreateStore: Fn() -> Result>, - Initialize: Fn( - &mut Store, - ) -> Result< - tx_graph::ChangeSet, - Box, - >, - Persist: Fn( - &mut Store, - &tx_graph::ChangeSet, - ) -> Result<(), Box>, -{ - let changesets = tx_graph_changesets(); - persist_changeset::< - tx_graph::ChangeSet, - Store, - CreateStore, - Initialize, - Persist, - >(create_store, initialize, persist, &changesets) -} - -/// Tests if [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex) is being persisted -/// correctly. -/// -/// See [`persist_txgraph_changeset`]. -#[cfg(feature = "miniscript")] -pub fn persist_indexer_changeset( - create_store: CreateStore, - initialize: Initialize, - persist: Persist, -) -> Result<(), PersistErr> -where - CreateStore: Fn() -> Result>, - Initialize: - Fn(&mut Store) -> Result>, - Persist: Fn( - &mut Store, - &keychain_txout::ChangeSet, - ) -> Result<(), Box>, -{ - let changesets = keychain_txout_changesets(); - persist_changeset::( - create_store, - initialize, - persist, - &changesets, - ) -} - -/// Tests if [`LocalChain`](local_chain::LocalChain) is being persisted correctly. -/// -/// See [`persist_txgraph_changeset`]. -pub fn persist_local_chain_changeset( - create_store: CreateStore, - initialize: Initialize, - persist: Persist, -) -> Result<(), PersistErr> -where - CreateStore: Fn() -> Result>, - Initialize: - Fn(&mut Store) -> Result>, - Persist: - Fn(&mut Store, &local_chain::ChangeSet) -> Result<(), Box>, -{ - let changesets = local_chain_changesets(); - persist_changeset::( - create_store, - initialize, - persist, - &changesets, - ) -} - -/// Get two [`tx_graph::ChangeSet`](tx_graph::ChangeSet)s. -fn tx_graph_changesets() -> [tx_graph::ChangeSet; 2] { +/// Get two [`tx_graph::ChangeSet`]s. +pub fn tx_graph_changesets() -> [tx_graph::ChangeSet; 2] { use tx_graph::ChangeSet; let tx1 = Arc::new(create_test_tx( @@ -256,9 +171,9 @@ fn tx_graph_changesets() -> [tx_graph::ChangeSet; 2] { [tx_graph_changeset1, tx_graph_changeset2] } -/// Get two [`keychain_txout::ChangeSet`](keychain_txout::ChangeSet)s. +/// Get two [`keychain_txout::ChangeSet`]s. #[cfg(feature = "miniscript")] -fn keychain_txout_changesets() -> [keychain_txout::ChangeSet; 2] { +pub fn keychain_txout_changesets() -> [keychain_txout::ChangeSet; 2] { use crate::utils::DESCRIPTORS; use keychain_txout::ChangeSet; @@ -292,8 +207,8 @@ fn keychain_txout_changesets() -> [keychain_txout::ChangeSet; 2] { [changeset, changeset_new] } -/// Get two [`local_chain::ChangeSet`](local_chain::ChangeSet)s. -fn local_chain_changesets() -> [local_chain::ChangeSet; 2] { +/// Get two [`local_chain::ChangeSet`]s. +pub fn local_chain_changesets() -> [local_chain::ChangeSet; 2] { use local_chain::ChangeSet; let changeset = ChangeSet { From de27ded17cefe0ed86d1438e997d278933ae8b29 Mon Sep 17 00:00:00 2001 From: codingp110 Date: Sun, 21 Jun 2026 21:34:21 +0530 Subject: [PATCH 3/3] fix: modify persistence helper Modified the persistence helper to return a `String` as error in order to be more expressive while keeping things simple. Also modified the helper to load and check the `ChangeSet` after each `ChangeSet` in `changesets` is persisted. It also checks if the `ChangeSet` loaded after reopening the `Store` matches the final aggregate `ChangeSet`. --- crates/chain/Cargo.toml | 2 +- crates/chain/tests/test_rusqlite_impl.rs | 46 ++++---- crates/file_store/src/store.rs | 38 +++---- crates/testenv/src/persist_test_utils.rs | 131 +++++++++++------------ 4 files changed, 102 insertions(+), 115 deletions(-) diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index ac449b19c1..c0713aa268 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -30,7 +30,7 @@ proptest = "1.2.0" bdk_testenv = { path = "../testenv" } criterion = { version = "0.7" } tempfile = "3" -anyhow = "1.0.102" +anyhow = "1" [features] default = ["std", "miniscript"] diff --git a/crates/chain/tests/test_rusqlite_impl.rs b/crates/chain/tests/test_rusqlite_impl.rs index ebfc51c7dc..4de0311885 100644 --- a/crates/chain/tests/test_rusqlite_impl.rs +++ b/crates/chain/tests/test_rusqlite_impl.rs @@ -1,4 +1,5 @@ #![cfg(feature = "rusqlite")] +use anyhow::anyhow; use bdk_chain::{keychain_txout, local_chain, tx_graph, ConfirmationBlockTime}; use bdk_testenv::persist_test_utils::{ assert_persist_changesets, keychain_txout_changesets, local_chain_changesets, @@ -9,17 +10,17 @@ use bdk_testenv::persist_test_utils::{ fn txgraph_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); let changesets = tx_graph_changesets(); - Ok(assert_persist_changesets( + assert_persist_changesets( || { - Ok(rusqlite::Connection::open( - temp_dir.path().join("wallet.sqlite"), - )?) + let mut db = rusqlite::Connection::open(temp_dir.path().join("wallet.sqlite"))?; + let db_tx = db.transaction()?; + tx_graph::ChangeSet::::init_sqlite_tables(&db_tx)?; + db_tx.commit()?; + Ok(db) }, |db| { let db_tx = db.transaction()?; - tx_graph::ChangeSet::::init_sqlite_tables(&db_tx)?; let changeset = tx_graph::ChangeSet::::from_sqlite(&db_tx)?; - db_tx.commit()?; Ok(changeset) }, |db, changeset| { @@ -29,24 +30,25 @@ fn txgraph_is_persisted() -> anyhow::Result<()> { Ok(()) }, &changesets, - )?) + ) + .map_err(|err| anyhow!(err)) } #[test] fn indexer_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); let changesets = keychain_txout_changesets(); - Ok(assert_persist_changesets( + assert_persist_changesets( || { - Ok(rusqlite::Connection::open( - temp_dir.path().join("wallet.sqlite"), - )?) + let mut db = rusqlite::Connection::open(temp_dir.path().join("wallet.sqlite"))?; + let db_tx = db.transaction()?; + keychain_txout::ChangeSet::init_sqlite_tables(&db_tx)?; + db_tx.commit()?; + Ok(db) }, |db| { let db_tx = db.transaction()?; - keychain_txout::ChangeSet::init_sqlite_tables(&db_tx)?; let changeset = keychain_txout::ChangeSet::from_sqlite(&db_tx)?; - db_tx.commit()?; Ok(changeset) }, |db, changeset| { @@ -56,24 +58,25 @@ fn indexer_is_persisted() -> anyhow::Result<()> { Ok(()) }, &changesets, - )?) + ) + .map_err(|err| anyhow!(err)) } #[test] fn local_chain_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); let changesets = local_chain_changesets(); - Ok(assert_persist_changesets( + assert_persist_changesets( || { - Ok(rusqlite::Connection::open( - temp_dir.path().join("wallet.sqlite"), - )?) + let mut db = rusqlite::Connection::open(temp_dir.path().join("wallet.sqlite"))?; + let db_tx = db.transaction()?; + local_chain::ChangeSet::init_sqlite_tables(&db_tx)?; + db_tx.commit()?; + Ok(db) }, |db| { let db_tx = db.transaction()?; - local_chain::ChangeSet::init_sqlite_tables(&db_tx)?; let changeset = local_chain::ChangeSet::from_sqlite(&db_tx)?; - db_tx.commit()?; Ok(changeset) }, |db, changeset| { @@ -83,5 +86,6 @@ fn local_chain_is_persisted() -> anyhow::Result<()> { Ok(()) }, &changesets, - )?) + ) + .map_err(|err| anyhow!(err)) } diff --git a/crates/file_store/src/store.rs b/crates/file_store/src/store.rs index dfed65bd5d..e4c5c7c4ab 100644 --- a/crates/file_store/src/store.rs +++ b/crates/file_store/src/store.rs @@ -291,6 +291,8 @@ mod test { io::{Seek, Write}, }; + use anyhow::anyhow; + const TEST_MAGIC_BYTES_LEN: usize = 12; const TEST_MAGIC_BYTES: [u8; TEST_MAGIC_BYTES_LEN] = [98, 100, 107, 102, 115, 49, 49, 49, 49, 49, 49, 49]; @@ -609,50 +611,38 @@ mod test { fn txgraph_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); let changesets = tx_graph_changesets(); - Ok(assert_persist_changesets( - || { - Ok(Store::create( - &TEST_MAGIC_BYTES, - temp_dir.path().join("store.db"), - )?) - }, + assert_persist_changesets( + || Ok(Store::load_or_create(&TEST_MAGIC_BYTES, temp_dir.path().join("store.db"))?.0), |db| Ok(db.dump().map(Option::unwrap_or_default)?), |db, changeset| Ok(db.append(changeset)?), &changesets, - )?) + ) + .map_err(|err| anyhow!(err)) } #[test] fn indexer_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); let changesets = keychain_txout_changesets(); - Ok(assert_persist_changesets( - || { - Ok(Store::create( - &TEST_MAGIC_BYTES, - temp_dir.path().join("store.db"), - )?) - }, + assert_persist_changesets( + || Ok(Store::load_or_create(&TEST_MAGIC_BYTES, temp_dir.path().join("store.db"))?.0), |db| Ok(db.dump().map(Option::unwrap_or_default)?), |db, changeset| Ok(db.append(changeset)?), &changesets, - )?) + ) + .map_err(|err| anyhow!(err)) } #[test] fn local_chain_is_persisted() -> anyhow::Result<()> { let temp_dir = tempfile::tempdir().unwrap(); let changesets = local_chain_changesets(); - Ok(assert_persist_changesets( - || { - Ok(Store::create( - &TEST_MAGIC_BYTES, - temp_dir.path().join("store.db"), - )?) - }, + assert_persist_changesets( + || Ok(Store::load_or_create(&TEST_MAGIC_BYTES, temp_dir.path().join("store.db"))?.0), |db| Ok(db.dump().map(Option::unwrap_or_default)?), |db, changeset| Ok(db.append(changeset)?), &changesets, - )?) + ) + .map_err(|err| anyhow!(err)) } } diff --git a/crates/testenv/src/persist_test_utils.rs b/crates/testenv/src/persist_test_utils.rs index 3013b83b71..9bff161edc 100644 --- a/crates/testenv/src/persist_test_utils.rs +++ b/crates/testenv/src/persist_test_utils.rs @@ -8,10 +8,7 @@ use bdk_chain::{ #[cfg(feature = "miniscript")] use bdk_chain::{indexer::keychain_txout, DescriptorExt, SpkIterator}; -use core::{ - cmp::PartialEq, - fmt::{Debug, Display}, -}; +use core::{cmp::PartialEq, fmt::Debug}; use core::error::Error as Err; @@ -25,83 +22,79 @@ const ADDRS: [&str; 2] = [ "bcrt1q8an5jfmpq8w2hr648nn34ecf9zdtxk0qyqtrfl", ]; -/// Errors caused by a failed persister test. -#[derive(Debug)] -pub enum PersistErr { - /// ChangeSet Mismatch - ChangeSetMismatch { - /// the resulting changeset - got: Box, - /// the expected changeset - expected: Box, - }, - /// Errors thrown by underlying persistence backend. - Persister(Box), -} - -impl From> for PersistErr { - fn from(value: Box) -> Self { - PersistErr::Persister(value) - } -} - -impl Display for PersistErr { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - PersistErr::ChangeSetMismatch { got, expected } => write!( - f, - "ChangeSet mismatch! Got: {:?}, Expected: {:?}", - got, expected - ), - PersistErr::Persister(err) => write!(f, "{err}"), - } - } -} - -impl Err for PersistErr {} - -/// Tests if `ChangeSet` is being persisted correctly. +/// A helper to check if a custom persistence backend persists `ChangeSet`s correctly. +/// +/// This first tries to create a `Store` using `init`, `load`s from it and checks if +/// the result is an empty `ChangeSet`. +/// It then tries to `persist` each `ChangeSet` in `changesets` one by one, doing a `load` +/// each time and checks that the aggregated `ChangeSet` matches the one loaded. /// -/// We create a dummy `ChangeSet`, persist it and check if loaded `ChangeSet` matches -/// the persisted one. We then create another such dummy `ChangeSet`, persist it and load it to -/// check if merged `ChangeSet` is returned. -pub fn assert_persist_changesets( - create_store: CreateStore, - initialize: Initialize, +/// Finally it closes the `Store`, reopens it using `init`, `load`s from it and checks if the loaded +/// `ChangeSet` matches the final aggregated `ChangeSet`. +pub fn assert_persist_changesets( + init: Init, + load: Load, persist: Persist, - changesets: &[CS], -) -> Result<(), PersistErr> + changesets: &[C], +) -> Result<(), String> where - CS: Debug + PartialEq + Default + Merge + Clone, - CreateStore: Fn() -> Result>, - Initialize: Fn(&mut Store) -> Result>, - Persist: Fn(&mut Store, &CS) -> Result<(), Box>, + C: Debug + PartialEq + Default + Merge + Clone, + Init: Fn() -> Result>, + Load: Fn(&mut Store) -> Result>, + Persist: Fn(&mut Store, &C) -> Result<(), Box>, { - let mut store = create_store()?; + let mut merged_changeset = C::default(); + { + let mut store = init().map_err(|err| { + format!( + "Encountered an error from the persister while initializing the store.\nGot:\n{}", + err + ) + })?; + + let init_changeset = load(&mut store).map_err(|err| format!("Encountered an error from the persister while loading from the new store.\nGot:\n{}",err))?; + + if init_changeset != C::default() { + Err("Loading from a new store should return an empty changeset.")?; + } - let init_changeset = initialize(&mut store)?; + for (i, changeset) in changesets.iter().enumerate() { + persist(&mut store, changeset).map_err(|err| format!("Persisting changeset no. {} failed. Got an error from the persister instead:\n{} ", i+1, err) )?; - if init_changeset != CS::default() { - return Err(PersistErr::ChangeSetMismatch { - expected: Box::new(CS::default()), - got: Box::new(init_changeset), - }); - } + merged_changeset.merge(changeset.clone()); - let mut merged_changeset = CS::default(); + let persisted_changeset = load(&mut store).map_err(|err| format!("Encountered an error from the persister while loading (after persisting changeset no. {}).\nGot:\n {}", i+1, err))?; - for changeset in changesets { - persist(&mut store, changeset)?; - merged_changeset.merge(changeset.clone()); + if persisted_changeset != merged_changeset { + Err(format!( + "Persisting changeset no. {} failed.\nExpected:\n\n{:?}\n\n\nLoaded:\n\n{:?};", + i + 1, + merged_changeset, + persisted_changeset + ))?; + } + } } - let persisted_changeset = initialize(&mut store)?; + let mut store = init().map_err(|err| { + format!( + "Encountered an error while reopening the store.\nGot:\n{}", + err + ) + })?; + + let persisted_changeset = load(&mut store).map_err(|err| { + format!( + "Unable to load the persisted changeset after reopening the store.\nGot an error from the persister:\n{}", + err + ) + })?; if persisted_changeset != merged_changeset { - return Err(PersistErr::ChangeSetMismatch { - expected: Box::new(merged_changeset), - got: Box::new(persisted_changeset), - }); + Err(format!( + "Did not get the expected changeset after reopening the store and loading.\nExpected:\n\n{:?}\n\n\nLoaded:\n\n{:?};", + merged_changeset, persisted_changeset + ))?; } Ok(())