Add functions to test persistence#2012
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
|
|
|
|
6d1738b to
6f09bde
Compare
|
Since we use v13 of |
|
This PR will be important to have if we still want to do #1980 to help test persistence implementations. |
tvpeter
left a comment
There was a problem hiding this comment.
Hi @110CodingP , this is incredible work on a massive PR.
I have left some nit comments.
In addition to the comments, I think it will also be great if you could consider including test utils for handling edge cases/conflicts, especially in the persist_test_utils (if that will not make the PR too big)
| let tx2 = Arc::new(create_test_tx( | ||
| [tx1.compute_txid()], | ||
| [0], | ||
| [20_000], | ||
| [ADDRS[0]], | ||
| 1, | ||
| 0, | ||
| )); |
There was a problem hiding this comment.
https://github.com/rust-fuzz/arbitrary could help remove work here, at the same time is exposing the API for fuzzers and other tests.
| input: input_vec, | ||
| output: output_vec, | ||
| } | ||
| } |
There was a problem hiding this comment.
Again, https://github.com/rust-fuzz/arbitrary could help with this.
|
@tvpeter and @nymius thank you for the reviews! I will restructure the PR to address the concerns. Also looking at mammal's restructuring of the wallet tests.
|
There was a problem hiding this comment.
Concept ACK
Thanks for working on this. Having some sort of tooling to ensure persistence implementations is not only helpful, but also crucial if we ever want to do #1980.
Approach NACK
There is quite a lot of boilerplate happening here and lots of repeated code making this approach hard to read and hard to maintain. All the test methods essentially all do the same thing, but with different changeset types. As mentioned by @nymius here, we should really abstract this.
Proposal
We can represent all that we want to test with a single generic helper. Then just have predefined sets of changesets (C).
pub fn assert_persists<C, S, CREATE, LOAD, PERSIST>(
create: CREATE,
load: LOAD,
persist: PERSIST,
changesets: &[C],
) where
C: Default + Merge + PartialEq + Clone + core::fmt::Debug,
CREATE: Fn(&Path) -> anyhow::Result<S>,
LOAD: Fn(&mut S) -> anyhow::Result<C>,
PERSIST: Fn(&mut S, &C) -> anyhow::Result<()>,
{
// Persist one at a time, reload after each, asserting merged accumulation.
let dir = tempfile::tempdir().expect("tempdir");
let mut store = create(dir.path()).expect("interleaved: create store");
assert_eq!(
load(&mut store).expect("interleaved: load empty"),
C::default(),
);
let mut acc = C::default();
for cs in changesets {
persist(&mut store, cs).expect("interleaved: persist");
acc.merge(cs.clone());
assert_eq!(
load(&mut store).expect("interleaved: reload"),
acc,
);
}
// Persist all into a fresh store, reload once.
let dir = tempfile::tempdir().expect("tempdir");
let mut store = create(dir.path()).expect("batched: create store");
for cs in changesets {
persist(&mut store, cs).expect("batched: persist");
}
assert_eq!(
load(&mut store).expect("batched: reload"),
acc,
);
}Predefined test changesets via builders:
pub fn tx_graph_fixture() -> [tx_graph::ChangeSet<ConfirmationBlockTime>; 2] { … }
pub fn keychain_txout_fixture() -> [keychain_txout::ChangeSet; 2] { … }
pub fn local_chain_fixture() -> [local_chain::ChangeSet; 2] { … }For fuzzing, proptest, we can even create methods that generates changeset sets on the fly.
6f09bde to
0d0ac5b
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2012 +/- ##
==========================================
+ Coverage 78.65% 81.40% +2.74%
==========================================
Files 30 30
Lines 5909 5909
Branches 279 279
==========================================
+ Hits 4648 4810 +162
+ Misses 1185 1025 -160
+ Partials 76 74 -2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
|
|
||
| for changeset in changesets { | ||
| persist(&mut store, changeset)?; | ||
| merged_changeset.merge(changeset.clone()); |
There was a problem hiding this comment.
I proposed loading from the db state after each merge - the reasoning is so that we can catch persistence problems earlier. I think it's a good idea to have this.
| 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 .
| }, | ||
| |db| { | ||
| let db_tx = db.transaction()?; | ||
| keychain_txout::ChangeSet::init_sqlite_tables(&db_tx)?; |
There was a problem hiding this comment.
Let's call init_sqlite_tables AFTER opening the connection. Not here.
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.
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`.
75fa86d to
de27ded
Compare
Description
Added basic tests to the testenv for testing custom persistence backends. Partially solves bdk_wallet#14 and might help with bdk_wallet#234.
Notes to the reviewers
An alternative approach could be to create a struct whose methods are these tests but the current approach seems better since these tests are meant to be used as unit tests. For the same reason redundant functions to check persistence of individual fields of the
ChangeSets are provided.Changelog notice
Todo
Checklists
All Submissions:
New Features: