Skip to content
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

Provide property tests for changeUTxO #2836

Merged
merged 5 commits into from
Aug 24, 2021

Conversation

jonathanknowles
Copy link
Contributor

Issue Number

ADP-1084

Comments

This PR provides property tests for the changeUTxO function.

Since this function is re-used by totalUTxO (which we want to modify and then verify), it's useful to first verify that changeUTxO is correct.

@jonathanknowles jonathanknowles self-assigned this Aug 23, 2021
@jonathanknowles jonathanknowles force-pushed the jonathanknowles/test-change-utxo branch from 06f0c93 to da7c4df Compare August 23, 2021 12:15
Comment on lines 475 to 480
prop_changeUTxO :: Property
prop_changeUTxO =
forAllShrink (listOf genTxOutputs) (shrinkList shrinkTxOutputs) inner
where
inner :: [TxOutputs] -> Property
inner txOutputs =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
prop_changeUTxO :: Property
prop_changeUTxO =
forAllShrink (listOf genTxOutputs) (shrinkList shrinkTxOutputs) inner
where
inner :: [TxOutputs] -> Property
inner txOutputs =
newtype Classify a = Classify (a -> Bool)
prop_changeUTxO :: Classify Address -> [TxOut] -> Property
prop_changeUTxO (Classify condition) txOutputs =

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rvl Nice suggestion!

I've actually made a similar change in 0ffa43d, which replaces the various types with a single IsOursIf {condition :: a -> Bool} newtype (with a single instance).

-- - 0b11111111 : even (Hamming weight = 8)
--
addressParity :: Address -> Parity
addressParity = parity . addressPopCount
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any other Address -> Bool function would be just as good, as long it splits the set of generated addresses into roughly equal parts.

Copy link
Contributor Author

@jonathanknowles jonathanknowles Aug 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely.

It's an arbitrary choice (excuse the pun!) to use this particular function. It has two nice advantages:

  1. Addresses are "even" or "odd" with equal frequency (meaning we don't have to do anything special with generation).
  2. Address parity can be determined with a pure function (meaning we don't have to keep any state around in our IsOurs instance).

I've added a comment to prop_changeUTxO with these reasons.

@@ -127,6 +130,12 @@ restrictedTo :: UTxO -> Set TxOut -> UTxO
restrictedTo (UTxO utxo) outs =
UTxO $ Map.filter (`Set.member` outs) utxo

null :: UTxO -> Bool
null (UTxO u) = Map.null u
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really? The only place this is used it's with (not (UTxO.null x). But x /= mempty would be fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really? The only place this is used it's with (not (UTxO.null x). But x /= mempty would be fine.

I agree with you that x /= mempty would work fine!

Though I find that UTxO.null is a little more self-documenting (in this case), as:

  • A reader doesn't have to navigate to the definition to figure out the type of thing being tested, because it's stated right at the point of use; and
  • The null function is idiomatic across different container types, so it's familiar.

@sevanspowell
Copy link
Contributor

sevanspowell commented Aug 24, 2021

I really love the way you've set out the comments, makes it easy to understand your logic and reasoning 👍 I'm trying to emulate your style in my own work.

My first thought was that changeUTxO is such a simple function and I wasn't sure how your new tests actually tested it, but now I understand that though the function appears simple, the existence of State s, and the way the behaviour of the function could arbitrarily change based on s and the IsOurs instance, makes more involved testing necessary.

I find this interesting because I'm working in a very similar place at the moment and I've made some modifications, let me know what you think:

  • Using a type class is equivalent to implicitly passing the typeclass members to the function.

  • Therefore we can re-write changeUTxO as:

    changeUTxO
    :: (Address -> s -> (Maybe (NonEmpty DerivationIndex), s))
    -> Set Tx
    -> s
    -> UTxO
    changeUTxO isOurs pending = evalState $
        mconcat <$> mapM (state . utxoOurs isOurs) (Set.toList pending)
    

    (also making the relevant change to utxoOurs and so on...)

  • But utxoOurs, and by extension changeUTxO uses isOurs :: Address -> s -> (Maybe (NonEmpty DerivationIndex), s) purely as a isOurs :: Address -> s -> (Bool, s) function. You confirm this with your isOursIf function, which essentially lifts Address -> Bool to a "proper" isOurs function (in a non-total manner). i.e. we don't care about the result

  • A function can easily be written to demote isOurs :: Address -> s -> (Maybe DerivationIndex, s) to isOurs :: Address -> s -> (Bool, s).

  • and entity -> s -> (result, s) is a special case of entity -> f result, where f is s -> (s,) (a functor)

  • So we can further re-write changeUTxO as:

    changeUTxO
    :: (Address -> f Bool)
    -> Set Tx
    -> f UTxO
    changeUTxO isOurs pending = ...
    

    while retaining the original behaviour, although how we call changeUTxO will change (i.e. evalState $ changeUTxO isOursBool)

  • For the purposes of testing, we can now do a couple of things:

    1. Offload the burden-of-proof that the Address -> f Bool, and the f functor, are working correctly off to the writer of those functions.
      • That's still "us", but maybe testing our demoted isOursBool function in isolation is a little bit easier.
    2. Re-write the tests of this function to not use State at all (e.g. use Identity functor for f). We're making it more explicit that this function can only rely on specific properties of f (e.g. Functor, Applicative, Monad).
  • Maybe we can even write a property that asserts the equivalence between changeUTxO and some other similar exisiting function like filterM:
    ∀f set. toList (changeUTxO f set) = filterM f (toList set)

I haven't tested this out so your mileage may vary. One thing to note is that we can "not use typeclasses" in changeUTxO, but continue using typeclasses everywhere else, it's an incremental change rather than a "don't ever use IsOurs", although "some" people "out there" do have an aversion to using typeclases that don't have laws (i.e. Functor typeclass has laws, but not IsOurs). That's a bit irrelevant to all this though.

TLDR; the type signature of changeUTxO can be changed to more accurately reflect it's usage, and so shift responsibility of some of the more complicated parts off to another module's tests, which /might/ be worth it.

Otherwise looks really good! This is just food for thought and I'm happy to approve and merge as-is.

jonathanknowles added a commit that referenced this pull request Aug 24, 2021
@jonathanknowles
Copy link
Contributor Author

bors r+

iohk-bors bot added a commit that referenced this pull request Aug 24, 2021
2836: Provide property tests for `changeUTxO` r=jonathanknowles a=jonathanknowles

### Issue Number

ADP-1084

### Comments

This PR provides property tests for the `changeUTxO` function.

Since this function is re-used by `totalUTxO` (which we want to modify and then verify), it's useful to first verify that `changeUTxO` is correct.

Co-authored-by: Jonathan Knowles <[email protected]>
@iohk-bors
Copy link
Contributor

iohk-bors bot commented Aug 24, 2021

Build failed:

Looks like a failure of the "Lint bash shell scripts" stage:
https://buildkite.com/input-output-hk/cardano-wallet/builds/16305#da2c1f53-cc01-4dfd-93b8-8b4d689cad8c

I'm marking as
#expected
since it was due to a manual merge rather than due to flakiness.

jonathanknowles and others added 5 commits August 24, 2021 11:00
These operations just reuse the underlying operations provided by the
`Map` module.

Exporting these operations means that we don't have to unwrap UTxO
values in order to determine their size.
Since #2840 was merged manually, this wasn't caught there.

I re-tested the command, and it seems to still work.
@Anviking Anviking force-pushed the jonathanknowles/test-change-utxo branch from 761ecc2 to ea1717c Compare August 24, 2021 09:02
@Anviking
Copy link
Member

bors r+

@iohk-bors
Copy link
Contributor

iohk-bors bot commented Aug 24, 2021

Build succeeded:

@iohk-bors iohk-bors bot merged commit db9a17a into master Aug 24, 2021
@iohk-bors iohk-bors bot deleted the jonathanknowles/test-change-utxo branch August 24, 2021 10:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants