-
Notifications
You must be signed in to change notification settings - Fork 214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate DBLayer tests with quickcheck-state-machine #259
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting 🤔 .. I got to re-read Edsko's blog post on the matter. One thing that bother me is that I couldn't find where pre-conditions for commands are located in this implementation. I wouldn't expect a "removeWallet" to be a valid transition if there hasn't been any createWallet
before...
so, I might be missing something from what is being tested here maybe?
mReadPrivateKey :: MWid -> MockOp (Maybe MPrivKey) | ||
mReadPrivateKey wid m@(M cp _ _ pk _) | ||
| wid `Map.member` cp = (Right (Map.lookup wid pk), m) | ||
| otherwise = (Left (NoSuchWallet wid), m) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be possible to leverage our already existing MVar
implementation as the Mock, instead of re-encoding everything here 🤔 ? We've already some good unit tests which make sure that the MVar implementation works rather okay.
The property in this PR checks that the DBLayer implementation matches the Model. This Model is even simpler than the MVar DB because it doesn't use MVars and isn't wrapped up in a DBLayer type. Therefore the model value can be shown in counterexamples. The idea about preconditions (e.g. valid wallet id) is that the QSM generator should also generate negative test cases. The QSM tests should have tags to ensure that certain scenarios are tested, and the generator should have weightings to ensure interesting paths are covered. |
@rvl I had a deeper look into this but still haven't got my head fully around it. As a result, things are slightly simpler, and I could remove the |
The MWid wrapper was not necesary, and I already had a note to remove it. Yes I intentionally put DB connection as part of the state machine model. This "implementation detail" is something that I would like to test. |
Indeed, that's why I removed it 😅 ... But I thought it was there to make the whole thing compiles somehow. Apparently not.
👍 |
492dd74
to
07e79d5
Compare
4040: Batch Import Addresses to 1.5.0 r=disassembler a=KtorZ ## Description <!--- A brief description of this PR and the problem is trying to solve --> Backporting cardano-foundation/cardano-wallet#259 to 1.5.0 ## Linked issue <!--- Put here the relevant issue from YouTrack --> Co-authored-by: KtorZ <matthias.benkort@gmail.com>
instance TxId DummyTarget where | ||
txId = Hash . B8.pack . show | ||
|
||
newtype DummyState = DummyState Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DummyState
probably should be like :
newtype DummyStateSqlite = DummyStateSqlite (UTxO, PendingTx)
deriving (Show, Eq, NFData)
Or even better to prepare test machine to take any state, ie. use state type parameter and then check this both in MVarSpec
and SqliteSpec
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to admit that I didn't understand around 50% of the mechanics here on a first skim although I was using this lib a year ago. I guess edsko did use it in a more advanced way than I did.
Would have to skim it once again after I read edsko's blog post
c56248a
to
a7a7b23
Compare
87d7633
to
5caaf21
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting some failures while running the parallel tests.
SQLite Parallel failure
uncaught exception: SqliteException
SQLite3 returned ErrorError while attempting to perform step: cannot start a transaction within a transaction
(after 5 tests and 2 shrinks)
ParallelCommands
{ prefix = Commands { unCommands = [] }
, suffixes =
[ Pair
{ proj1 =
Commands
{ unCommands =
[ Command
(At (ReadPrivateKey (Val (MWid "c"))))
(At (Resp (Right (PrivateKey Nothing))))
[]
]
}
, proj2 =
Commands
{ unCommands =
[ Command
(At
(PutWalletMeta
(Val (MWid "c"))
WalletMetadata
{ name = WalletName { getWalletName = "bulbazaur" }
, passphraseInfo =
Just
WalletPassphraseInfo
{ lastUpdatedAt =
(1864 - 05 - 11) (22 : 24 : 03.767284750574) UTC
}
, status = Ready
, delegation = NotDelegating
}))
(At (Resp (Left (NoSuchWallet (Reference (Symbolic (Var 0)))))))
[ Var 0 ]
]
}
}
]
}
SQLite parallel failure (2)
uncaught exception: SqliteException
SQLite3 returned ErrorError while attempting to perform step: not an error
(after 4 tests)
ParallelCommands
{ prefix = Commands { unCommands = [] }
, suffixes =
[ Pair
{ proj1 =
Commands
{ unCommands =
[ Command
(At
(CreateWallet
(MWid "c")
(Wallet
UTxO { getUTxO = fromList [] }
(fromList [])
SlotId { epochNumber = 0 , slotNumber = 0 }
SeqState
{ internalPool =
AddressPool
{ accountPubKey =
Key
{ getKey =
XPub
{ xpubPublicKey =
"Q\170\GS\202\198\&2KA\203\CANN'X\154 \139\DEL\FS\148\FSb\SO\RS\r\DLEAL\151\153\137\167\194"
, xpubChaincode =
ChainCode
"\204\196\"I\225y\132\196L\243\128\180\137\246,W\248@\137\225P$[\244\156Cm\v\151\t\197\143"
}
}
, gap = AddressPoolGap { getAddressPoolGap = 15 }
, indexedAddresses =
fromList [ ... ]
}
, externalPool =
AddressPool
{ accountPubKey =
Key
{ getKey =
XPub
{ xpubPublicKey =
"Q\170\GS\202\198\&2KA\203\CANN'X\154 \139\DEL\FS\148\FSb\SO\RS\r\DLEAL\151\153\137\167\194"
, xpubChaincode =
ChainCode
"\204\196\"I\225y\132\196L\243\128\180\137\246,W\248@\137\225P$[\244\156Cm\v\151\t\197\143"
}
}
, gap = AddressPoolGap { getAddressPoolGap = 10 }
, indexedAddresses =
fromList [ ... ]
}
, pendingChangeIxs = PendingIxs { pendingIxsToList = [] }
})
WalletMetadata
{ name = WalletName { getWalletName = "bulbazaur" }
, passphraseInfo =
Just
WalletPassphraseInfo
{ lastUpdatedAt =
(1864 - 05 - 06) (09 : 58 : 17.690387360868) UTC
}
, status = Restoring (Quantity Percentage { getPercentage = 66 })
, delegation = NotDelegating
}))
(At (Resp (Right (NewWallet (Reference (Symbolic (Var 0)))))))
[ Var 0 ]
]
}
, proj2 =
Commands
{ unCommands =
[ Command
(At (ReadWalletMeta (Val (MWid "b"))))
(At (Resp (Right (Metadata Nothing))))
[]
]
}
}
]
}
The parallel MVar tests are also failing sometimes:
MVar parallel failure
1) Cardano.Wallet.DB.MVar, MVar state machine tests, Parallel
*** Failed! (after 95 tests and 203 shrinks):
Exception:
insertConcrets: impossible.
CallStack (from HasCallStack):
error, called at src/Test/StateMachine/Types/Environment.hs:73:3 in quickcheck-state-machine-0.6.0-w8NmrpYN7DCoqRW8X5Sia:Test.StateMachine.Types.Environment
I am not sure whether this is due to our implementation, SQLite itself, or, due to the QSM library... I remember Edsko saying quite a few times that the parallel testing wasn't really ready for serious uses...
Beside, on parallel tests, some pre-conditions are rather hard to satisfy / meet. I believe this is / ought to be handled properly by the state-machine library. If I recall correctly, the theory behind doing parallel QSM testing is like running every possible interleaves of the commands sequentially. But for instance, what if you create a wallet in one worker, and simply query it with the other worker. If the first worker runs first, the second end up with a wallet, if the second runs first, it got nothing.
To be honest, I'd rather have us focusing on the sequential one in this PR to get moving already, instead of a never-ending stream of work which adds always more and more complexity. Let's try to keep the scope of PR well-defined and open new PRs for additions that are out-of-scope.
1df0e8a
to
75c0b6c
Compare
prop_parallelOK, I have disabled the parallel tests in HSpec so we can close this. I ran putTxHistoryYou are right, changing the Arbitrary TxId instance to not collide was a bad idea, because we want to test error cases. To handle rollback, we need to update the TxMeta SQL schema to reference the checkpoint. The unique constraint would have a checkpoint ID added to it. That should work fine. Because TxId is a hash of inputs and outputs, we expect a transaction of a certain TxId to be immutable and unique. If it were a different transaction, then it would have a different ID. I think throwing an ErrorConstraint exception was the wrong thing for the DB layer to do, because there may be multiple TxMeta entries for a single Tx (multiple checkpoints of multiple wallets). When adding a (Tx, TxMeta), the DBLayer needs to decide which of the "old" or "new" Tx to keep. But it doesn't matter because they are the same Tx. I will pick the "new" Tx. It is a valid action to insert the exact same Tx twice (it may belong to multiple TxHistories). It's not a valid action to put a different Tx at the same ID. In that case, last Tx wins (an arbitrary choice). So I have updated the QSM model accordingly, and fixed the Sqlite DBLayer. It might be a good consistency check to recalculate the TxId after reading it from the database (if that's not too expensive). |
QSM test was failing with this: Open CreateWallet b CreateWallet a ListWallets PostconditionFailed "PredicateC (Resp (Right (WalletIds [MWid \"b\",MWid \"a\"])) :/= Resp (Right (WalletIds [MWid \"a\",MWid \"b\"])))" /= Ok
And put back the Arbitrary (Hash "Tx") instance.
The model of dbPropertyTests doesn't match how the relational database works.
@@ -213,7 +215,8 @@ newDBLayer fp = do | |||
Nothing -> pure $ Left $ ErrNoSuchWallet wid | |||
|
|||
, listWallets = runQuery' $ | |||
map (PrimaryKey . unWalletKey) <$> selectKeysList [] [] | |||
map (PrimaryKey . unWalletKey) <$> | |||
selectKeysList [] [Asc WalTableId] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wondering: is there any particular reason for this ordering?
There is no complex dependency between DBLayer commands, so the Expr type is not needed.
-------------------------------------------------------------------------------} | ||
|
||
newtype At f r | ||
= At (f (Reference WalletId r)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to add some kind of comment to describe what this type is for? For example, what do f
and r
refer to?
insertState (wid, sl) _ = insert_ (SeqState wid sl) | ||
selectState (wid, sl) = fmap (const DummyState) <$> | ||
selectFirst [SeqStateTableWalletId ==. wid, SeqStateTableCheckpointSlot ==. sl] [] | ||
deleteState wid = deleteWhere [SeqStateTableWalletId ==. wid] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't be in the source code IMO :/
|
||
-- | Clean a database by removing all wallets. | ||
cleanDB :: Monad m => DBLayer m s t -> m () | ||
cleanDB db = listWallets db >>= mapM_ (runExceptT . removeWallet db) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure we do want to keep this in the source code. This is purely for testing.
data SkipTests | ||
= RunAllTests | ||
| SkipTxHistoryReplaceTest | ||
deriving (Show, Eq) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks a bit dodgy 😶 . What exactly is the issue with this test? If there's an issue, let's disable it temporarily and open a bug ticket about fixing it instead?
|
||
{-# OPTIONS_GHC -fno-warn-orphans #-} | ||
|
||
module Cardano.Wallet.DB.StateMachine |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized Edsko's blog post isn't mentioned anywhere in this file. It'll be nice to have a reference to it as a comment 👍
unMockWid (MWid wid) = WalletId . hash . B8.pack $ wid | ||
|
||
-- | Represent (XPrv, Hash) as a string. | ||
type MPrivKey = String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder why we do have this mock XPrv
and Hash
in a first place? The generator only takes keys among a fixed list of 3 elements anyway, so we could as well just go for the right type from the start 🤔 ?
|
||
-- | Mock wallet ID -- simple and easy to read | ||
newtype MWid = MWid String | ||
deriving (Show, Eq, Ord, Generic) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same remark here, I don't think the extra indirection is helpful here. It adds another layer of indirection that has little value? Arbitrary wallet id are easy to generate (especially if just taken from a fixed list of elements) and, this does increase the cognitive load necessary to process this module. Don't you think?
Problem here was that our generator would generate inputs, outputs and transaction ids independently. While this didn't matter much with the MVar implementation, it does create weird inconsistency with the relational model because indeed, we expect inputs associated with a particular tx id to not differ when associated with the same tx id. Since a transaction id is actually a hash over the content of the transaction, changing the transaction should yield a different id. So now, we generate "fake" txid by computing them from the actual transaction content (as done in production, we simply don't hash it in the test as this has little value here but cost quite some computing power).
We use to create a fresh new DB layer before the run of each property because this was very cheap. Now that we do create a db layer only once before a given property, we need to make sure that the state of the DB is at least cleaned up between each property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merging as-is. We can iterate on a few remaining parts in future PRs.
Nice job on pulling that out @rvl
@jonathanknowles Feedback is still welcome afterwards.
I've also ran the sequential tests ~10000 times trying to catch something. Seems pretty stable now.
4041: Batch Import Addresses to 1.4.2 r=disassembler a=KtorZ ## Description <!--- A brief description of this PR and the problem is trying to solve --> Backporting cardano-foundation/cardano-wallet#259 to 1.4.2 ## Linked issue <!--- Put here the relevant issue from YouTrack --> Co-authored-by: KtorZ <matthias.benkort@gmail.com>
Relates to #154.
Overview
We can generate our DBLayer tests with quickcheck.
I used Edsko's blog post as a guide.
Comments
Todo list:
put/readPrivateKey
Use QSM parallel testingand MVarDBLayers.