Repository navigation
Add functions to test persistence - #2012
110CodingP wants to merge 3 commits into
Conversation
8f06dc6 to
247d478
Compare
247d478 to
83b54a6
Compare
|
I tried re-running CI to no avail, maybe try force pushing. |
83b54a6 to
51d148e
Compare
|
Great idea. ConceptACK |
f2623db to
b77a114
Compare
|
In 10c42dc I added a |
We could have the testenv default feature enable |
|
Should the miniscript feature introduced be removed? I was thinking if some users might not want bdk_chain/miniscript feature to be enabled? |
1eee95b to
fd9429c
Compare
|
|
|
|
0d0ac5b to
48912ee
Compare
|
Thanks a lot @evanlinjin for the review! Also for convenience the PR still contains changeset specific persistence tests ( I will try to leverage |
|
|
||
| [dev-dependencies] | ||
| tempfile = "3" | ||
| anyhow = { version = "1.0.102", default-features = false} |
There was a problem hiding this comment.
| anyhow = { version = "1.0.102", default-features = false} | |
| anyhow = { version = "1", default-features = false} |
No need to explicitly define the minor and patch versions.
|
@110CodingP How about we focus this PR on getting the API shape correct and introduce something like |
| pub enum PersistErr<C: Debug> { | ||
| /// ChangeSet Mismatch | ||
| ChangeSetMismatch { | ||
| /// the resulting changeset | ||
| got: Box<C>, | ||
| /// the expected changeset | ||
| expected: Box<C>, | ||
| }, | ||
| /// Errors thrown by underlying persistence backend. | ||
| Persister(Box<dyn Err + 'static + Send + Sync>), | ||
| } |
There was a problem hiding this comment.
This is overengineered. We don't need to programmatically inspect different failure types. These helpers are for tests - failure just needs to panic!/assert!.
assert_persist_changesets should not need a return type and just panic on failure.
There was a problem hiding this comment.
The reason I changed to this approach is that
1.) We have more control on the error message in this case.
2.) I was initially thinking that throwing a panic! would help users identify the line number in the tests that caused the panic but then we can't expect the users to go into the library code right?
3.) Users can downcast the error and handle it accordingly?
There was a problem hiding this comment.
Thanks for presenting the arguments, I have changed my stance.
-
Yes, however we can also just return a
Stringas the error type. Currently, the error variants are missing context that are useful to have. We are reusing theChangeSetMismatchfor different test errors where it would be more useful to have context-specific error messages. -
You are right about this -
Resultis better. -
YAGNI. I don't see a usecase for this?
My updated recommendation
Return Result<(), String> or anyhow::Result<()> where the error string gives context of the error (check my other review comments for details).
Yup, agreed! |
b0b2cb2 to
75fa86d
Compare
| /// check if merged `ChangeSet` is returned. | ||
| pub fn assert_persist_changesets<CS, Store, CreateStore, Initialize, Persist>( | ||
| create_store: CreateStore, | ||
| initialize: Initialize, |
There was a problem hiding this comment.
| initialize: Initialize, | |
| load: Load, |
This makes it easier to reason about. As "loading from db" is the main operation we care about.
initialize(&mut store) reads like we are setting up the store, but then it returns a changeset which is confusing.
| 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), | ||
| }); | ||
| } |
There was a problem hiding this comment.
The returned error does not explicitly convey what we are testing here. This is proof that PersistErr is not earning it's keep.
The returned error should state what was being tested. In this case - a newly created store should load an empty changeset.
| let mut merged_changeset = CS::default(); | ||
|
|
||
| for changeset in changesets { | ||
| persist(&mut store, changeset)?; |
There was a problem hiding this comment.
It will be helpful to know which changeset index we failed on alongside the db error.
| pub enum PersistErr<C: Debug> { | ||
| /// ChangeSet Mismatch | ||
| ChangeSetMismatch { | ||
| /// the resulting changeset | ||
| got: Box<C>, | ||
| /// the expected changeset | ||
| expected: Box<C>, | ||
| }, | ||
| /// Errors thrown by underlying persistence backend. | ||
| Persister(Box<dyn Err + 'static + Send + Sync>), | ||
| } |
There was a problem hiding this comment.
Thanks for presenting the arguments, I have changed my stance.
-
Yes, however we can also just return a
Stringas the error type. Currently, the error variants are missing context that are useful to have. We are reusing theChangeSetMismatchfor different test errors where it would be more useful to have context-specific error messages. -
You are right about this -
Resultis better. -
YAGNI. I don't see a usecase for this?
My updated recommendation
Return Result<(), String> or anyhow::Result<()> where the error string gives context of the error (check my other review comments for details).
| /// 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<CS, Store, CreateStore, Initialize, Persist>( | ||
| create_store: CreateStore, |
There was a problem hiding this comment.
| create_store: CreateStore, | |
| init: Init, |
CreateStore conveys "make a brand new store". However, this closure's job is actually "give me a db handle", or in other words "Open or create".
Another note is that this test does not actually test a reopen. I propose having three sections to this test:
- Load cs from a newly created db. The cs should be empty.
- Interweave cs persists and loads. Ensure persists succeeds and loaded changesets are expected.
- Reopen the db, load cs from db. Ensure cs is expected.
| /// 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. |
There was a problem hiding this comment.
This does not look correct. changesets are provided to this method, not created by this method.
We should mention what exactly is being tested. I.e. load from newly created db, interleaved persists and loads of changesets, load after reopen should succeed.
We should also explain the responsibilities of Init, Load and Persist.
There was a problem hiding this comment.
Sorry I forgot to update the docs. Done in de27ded .
75fa86d to
de27ded
Compare
evanlinjin
left a comment
There was a problem hiding this comment.
We are close!
- Compilation issues without
miniscriptfeature. - Incomplete docs.
- Various nits.
|
|
||
| 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() { |
There was a problem hiding this comment.
Nit
| if init_changeset != C::default() { | |
| if !init_changeset.is_empty() { |
| Persist: Fn(&mut Store, &C) -> Result<(), Box<dyn Err>>, | ||
| { | ||
| let mut merged_changeset = C::default(); | ||
| { |
There was a problem hiding this comment.
| { |
Unnecessary block.
There was a problem hiding this comment.
Ok, so do you want me to use Got it, we don't need to drop the store.drop instead for dropping the store?
noahjoeris
left a comment
There was a problem hiding this comment.
LGTM 🚀
Agree with Evan's last comments.
7058eba to
cb1cec0
Compare
|
Ready for next review! |
tvpeter
left a comment
There was a problem hiding this comment.
Weldone @110CodingP,
I have left some minor comments.
There was a problem hiding this comment.
This review was generated by Claude Code and posted from my account. I haven’t hand-verified every claim — treat it as machine-generated input, not a maintainer sign-off. Findings already raised elsewhere (@tvpeter’s
drop(store)suggestion, theOP_CHECKLOCKTIMEVERIFYtypo, and “Won’t this always return true?” onutils.rs:119) are omitted.Tested against
cb1cec09in three configs (-p bdk_file_store,-p bdk_chain --features rusqlite,--workspace --no-default-features --features bdk_chain/hashbrown). All 6 new tests pass.
Opened #2284 for the test-vector gaps so they don’t block this PR.
file_store dev-dep now downloads bitcoind + esplora binaries
bdk_testenv = { path = "../testenv", features = ["serde"] } keeps defaults, which pull in download → bitcoind/esplora binaries. cargo test -p bdk_file_store now compiles and downloads those; previously it only needed tempfile. Offline/sandboxed runs break.
bdk_testenv = { path = "../testenv", default-features = false, features = ["std", "serde", "miniscript"] }(Separate from @noahjoeris’ crate-choice suggestion.)
Wrong crate’s feature in #[cfg]
#[cfg(feature = "miniscript")] checks bdk_chain’s feature, but guards a symbol gated on bdk_testenv’s. They only align today by accident. A consumer enabling bdk_chain/miniscript with bdk_testenv defaults off gets an unresolved import instead of a skipped test.
zip silently truncates in create_test_tx
Four independent iterators are zipped. Mismatched lengths quietly produce fewer inputs/outputs (e.g. 2 txids + 1 vout → 1-input tx) with no warning. Failures then appear in unrelated tests. A debug_assert_eq! on lengths would catch this at the source.
Reference vectors only exercise inserts (tracked in #2284)
Not blocking, but third-party backends should know the gaps:
local_chain_changesets— disjoint heights only; no reorg (same height, different hash). PlainINSERTstill passes.tx_graph_changesets— changeset 2 adds a new tx, so existing txids never get updatedlast_seen/first_seen. Upsert path (INSERT OR IGNORE) is untested. (keychain_txoutalready covers this.)- Block removal
(height, None)can’t be added:from_sqlitedeletes the row and never returnsNone, so strictload() == merge(...)fails for correct backends. Document as out of scope, or compare applied state. persist_test_utils.rs:144—last_seen<first_seenfor the same txid (looks like a swap). Harmless for round-trips, but backends that validate the ordering will reject it.- Idempotency untested. Same changeset is never persisted twice. Split into
assert_persists/assert_persists_idempotentfor the stronger claim.
cb1cec0 to
0e7f694
Compare
`assert_persist_changesets` persists each changeset in turn, checks that loading returns their aggregate, then reopens the store and checks again. It panics on any mismatch or backend error. Also add `utils` helpers that build non-empty `tx_graph`, `keychain_txout` and `local_chain` changesets, and a `miniscript` feature (on by default) that the keychain changeset needs. Co-Authored-By: 志宇 <hello@evanlinjin.me> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: 志宇 <hello@evanlinjin.me> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…esets` Co-Authored-By: 志宇 <hello@evanlinjin.me> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93be1ea to
c537631
Compare
Description
Added a helper
assert_persist_changesetsto the testenv for testing custom persistence backends. Partially solves bdk_wallet#14 and might help with bdk_wallet#234.Changelog notice
Todo
Checklists
All Submissions:
New Features: