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

POC for polysemy #771

Closed
wants to merge 1 commit into from
Closed

POC for polysemy #771

wants to merge 1 commit into from

Conversation

newhoggy
Copy link
Contributor

Changelog

- description: |
    POC for polysemy
# uncomment types applicable to the change:
  type:
  # - feature        # introduces a new feature
  # - breaking       # the API has changed in a breaking way
  - compatible     # the API has changed but is non-breaking
  # - optimisation   # measurable performance improvements
  - refactoring    # QoL changes
  # - bugfix         # fixes a defect
  # - test           # fixes/modifies tests
  # - maintenance    # not directly related to the code
  # - release        # related to a new release preparation
  # - documentation  # change in code docs, haddocks...

Context

Using polysemy to simplify tests.

How to trust this PR

Highlight important bits of the PR that will make the review faster. If there are commands the reviewer can run to observe the new behavior, describe them.

Checklist

  • Commit sequence broadly makes sense and commits have useful messages
  • New tests are added if needed and existing tests are updated. See Running tests for more details
  • Self-reviewed the diff

expectedTx <- noteInputFile "test/cardano-cli-golden/files/input/byron/tx/normal.tx"
createdTx <- noteTempFile tempDir "tx"
void $ execCardanoCLI
hprop_byronTx = propertyOnce $ localWorkspace $ do
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The tempDir argument is no longer needed because that information now propagates into our tests via Reader ProjectRoot.

hprop_byronTx = propertyOnce $ localWorkspace $ do
signingKey <- jotPkgInputFile "test/cardano-cli-golden/files/input/byron/keys/byron.skey"
expectedTx <- jotPkgInputFile "test/cardano-cli-golden/files/input/byron/tx/normal.tx"
createdTx <- jotTempFile "tx"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can ask for a temp file without supplying the tempDir because it can get it from Reader ProjectRoot.

compareByronTxs :: ()
=> HasCallStack
=> Member (Embed IO) r
=> Member Hedgehog r
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test functions have a Member Hedgehog r constraint.

=> Member Hedgehog r
=> FilePath
-> FilePath
-> Sem r ()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Even though getTxByteString is not a test function we can use it seamlessly below.

f & moduleWorkspace "cardano-cli"
& runReader (ProjectRoot cabalProjectDir)
& runReader (PackagePath "cardano-cli")
& runResource
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The localWorkspace function sets up all the effects for the tests. For example readers for ProjectRoot and PackagePath are supplied. Additional moduleWorkspace provides a reader for Workspace which is used to create temporary files.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can have multiple readers - very nice!

=> HasCallStack
=> Member (Embed IO) r
=> Member Hedgehog r
=> Member Log r
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can log in test and non-test functions and those will get annotated in the test report.

Copy link
Contributor

Choose a reason for hiding this comment

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

where does this Log effect come from?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Log effect comes from polysemy-log.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The interpreters for the Log effect are registered by propertyOnce here:

https://github.com/haskell-works/hw-polysemy/blob/main/hedgehog/HaskellWorks/Polysemy/Hedgehog/Property.hs

See the lines:

  & interpretLogDataLog
  & setLogLevel (Just Info)
  & interpretDataLogHedgehog formatLogEntry getLogEntryCallStack
  & interpretDataLogHedgehog id (const GHC.callStack)
  & interpretTimeGhc

@newhoggy newhoggy force-pushed the newhoggy/polysemy branch 3 times, most recently from 7203b38 to 7805ddc Compare May 18, 2024 11:37
-> Sem r ()
checkVrfFilePermissions vrfSignKey =
embed (runExceptT (Api.checkVrfFilePermissions vrfSignKey))
& onLeftM throw
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's very easy to create polysemy wrappers for functions.


checkVrfFilePermissions (File vrfSignKey)
-- key-gen-VRF cli command created a VRF signing key file with the wrong permissions
& trapFail @VRFPrivateKeyFilePermissionError
Copy link
Contributor Author

@newhoggy newhoggy May 18, 2024

Choose a reason for hiding this comment

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

Error handling in tests is much more succinct. If an error is thrown, we just trap it here and fail with minimal ceremony. Error handling supports multiple errors and it is always clear which error it is that occured.

@@ -0,0 +1,36 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

Check warning

Code scanning / HLint

Unused LANGUAGE pragma Warning

cardano-cli/src/Cardano/CLI/Polysemy.hs:3:1-34: Warning: Unused LANGUAGE pragma
  
Found:
  {-# LANGUAGE OverloadedStrings #-}
  
Perhaps you should remove it.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

Check warning

Code scanning / HLint

Unused LANGUAGE pragma Warning

cardano-cli/src/Cardano/CLI/Polysemy.hs:4:1-33: Warning: Unused LANGUAGE pragma
  
Found:
  {-# LANGUAGE TypeApplications #-}
  
Perhaps you should remove it.
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}

Check warning

Code scanning / HLint

Unused LANGUAGE pragma Warning

cardano-cli/src/Cardano/CLI/Polysemy.hs:5:1-30: Warning: Unused LANGUAGE pragma
  
Found:
  {-# LANGUAGE TypeOperators #-}
  
Perhaps you should remove it.
  
Note: may require {-#&nbsp;LANGUAGE&nbsp;ExplicitNamespaces&nbsp;#-} adding to the top of the file
Comment on lines +35 to +36
(embed $ runExceptT $ Cli.readByronSigningKey bKeyFormat fp)
& onLeftM throw

Check notice

Code scanning / HLint

Move brackets to avoid $ Note

cardano-cli/src/Cardano/CLI/Polysemy.hs:(35,3)-(36,19): Suggestion: Move brackets to avoid $
  
Found:
  (embed $ runExceptT $ Cli.readByronSigningKey bKeyFormat fp)
    & onLeftM throw
  
Perhaps:
  embed (runExceptT $ Cli.readByronSigningKey bKeyFormat fp)
    & onLeftM throw
=> Member Log r
=> Member (Embed IO) r
=> Sem
( Reader Workspace
Copy link
Contributor

Choose a reason for hiding this comment

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

Where does Workspace type come from?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@carbolymer carbolymer left a comment

Choose a reason for hiding this comment

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

Very nice! I love the idea of using of inversion of control for our tests! We should do it for cardano-cli at some point too.

I haven't used polysemy much, so I'm wondering why polysemy and not some other effect system?

Sould we be worried about its peformance? I guess not much, becase it's just for tests, but it's something to be mindful of.

I'm not approving yet, I'm wondering what rest of the team thinks about it. Definitely there's some learning curve here.

cabal.project Outdated

source-repository-package
type: git
location: https://github.com/newhoggy/polysemy-conc
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this SRP? I don't see a difference between your fork and @tek's version.

And also I don't see a dependency to polysemy-conc in the modified cabal files!?

Copy link
Contributor Author

@newhoggy newhoggy May 25, 2024

Choose a reason for hiding this comment

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

We don't use it directly. The SRP is because polysemy-conc is a transitive dependency and it has a dependency that doesn't compile on Windows.

See: tek/polysemy-conc#4

Comment on lines +73 to +74
createdATxAuxBS <- getTxByteString createdTx & trapFail @ByronTxError
expectedATxAuxBS <- getTxByteString expectedTx & trapFail @ByronTxError
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if we don't write those trapFail (where is this function defined btw? I could not find it)?

I'm worried we're not saving on boilerplate if for every IO call, we have to annotate it with trapFail theExceptionThatItCanThrow.

Copy link
Contributor Author

@newhoggy newhoggy May 25, 2024

Choose a reason for hiding this comment

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

What trapFail gives to is a quick way to catch the error and annotate it at the point of failure. The failure annotation will attach to the specific trapFail that failed so you can tell which one it was that failed very easily.

It is possible to trapFail once for the entire test, however what you get if you do that is the at the failure annotation will be at the trapFail at the catch-all level not at the operation that failed.

If you don't care for the usefulness of getting the annotation at the point of failure you can reduce boilerplate further.

Copy link
Contributor Author

@newhoggy newhoggy May 25, 2024

Choose a reason for hiding this comment

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

The trapFail function is defined here:

https://hackage.haskell.org/package/hw-polysemy-0.2.5.0/docs/src/HaskellWorks.Polysemy.Hedgehog.Assert.html#trapFail

The library hw-polysemy is like hedgehog-extras by written using polysemy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What happens if we don't write those trapFail?

If you don't call trapFail you get a compile error. For example:

test/cardano-cli-golden/Test/Golden/Byron/Tx.hs:74:23: error: [GHC-39999]
    • Could not deduce ‘Member (Error ByronTxError) r’
        arising from a use of ‘getTxByteString’
      from the context: (HasCallStack, Member (Embed IO) r,
                         Member Hedgehog r)
        bound by the type signature for:
                   compareByronTxs :: forall (r :: Polysemy.Internal.Kind.EffectRow).
                                      (HasCallStack, Member (Embed IO) r, Member Hedgehog r) =>
                                      FilePath -> FilePath -> Sem r ()
        at test/cardano-cli-golden/Test/Golden/Byron/Tx.hs:(65,1)-(71,13)
    • In a stmt of a 'do' block:
        expectedATxAuxBS <- getTxByteString expectedTx
      In the expression:
        do createdATxAuxBS <- getTxByteString createdTx
                                & trapFail @ByronTxError
           expectedATxAuxBS <- getTxByteString expectedTx
           normalByronTxToGenTx expectedATxAuxBS
             === normalByronTxToGenTx createdATxAuxBS
      In an equation for ‘compareByronTxs’:
          compareByronTxs createdTx expectedTx
            = do createdATxAuxBS <- getTxByteString createdTx
                                      & trapFail @ByronTxError
                 expectedATxAuxBS <- getTxByteString expectedTx
                 normalByronTxToGenTx expectedATxAuxBS
                   === normalByronTxToGenTx createdATxAuxBS
   |
74 |   expectedATxAuxBS <- getTxByteString expectedTx
   |                       ^^^^^^^^^^^^^^^

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you get an error that says Could not deduce ‘Member (Error MyError) r’, you have two choices. You can handle it right there use trap, which is like catch, or trapFail which is like trap by fails immediately with a hedgehog annotation or you can propagate the error by adding Member (Error MyError) r to the function signature in which case you punt the responsibility of handling the error to the caller.

Note that a function can declare that it throws multiple errors so you don't have to create a new sum type with many constructors to propagate multiple errors.

@smelc
Copy link
Contributor

smelc commented May 21, 2024

In the current state, I am neutral:

  1. We don't really save on boilerplate
  2. We don't add boilerplate either
  3. The multiple reader context is nice to have, but not essential.
  4. If we introduce a dependency to https://github.com/tek/polysemy-conc (unclear if we do really), it's not great; because this is a library with very few users (even if, I must say, I am the manger of the library's author @tek 🤣 who works at Modus Create)

@newhoggy
Copy link
Contributor Author

newhoggy commented May 25, 2024

One advantage is that you could log in the functions being tested and then capture those logs in a test annotation like this:

           ┏━━ test/cardano-cli-golden/Test/Golden/Byron/SigningKeys.hs ━━━
        38 ┃ hprop_deserialise_nonLegacy_signing_Key :: Property
        39 ┃ hprop_deserialise_nonLegacy_signing_Key = propertyOnce $ localWorkspace $ do
        40 ┃   skeyBs <- LBS.readFile "test/cardano-cli-golden/files/input/byron/keys/byron.skey1"
           ┃   │ [info]  [T.G.B.SigningKeys#40] Reading bytestring from file: test/cardano-cli-golden/files/input/byron/keys/byron.skey1
        41 ┃     & trapFail @IOException
           ┃     ^^^^^^^^^^^^^^^^^^^^^^^
           ┃     │ Expected Right: test/cardano-cli-golden/files/input/byron/keys/byron.skey1: openBinaryFile: does not exist (No such file or directory)
        42 ┃ 
        43 ┃   fromEither (deserialiseFromBytes Crypto.fromCBORXPrv skeyBs)
        44 ┃     & trapFail @DeserialiseFailure
        45 ┃     & void

The line:

           ┃   │ [info]  [T.G.B.SigningKeys#40] Reading bytestring from file: test/cardano-cli-golden/files/input/byron/keys/byron.skey1

Is logging from the readFile function, which is pretty short but we could have logging in a more elaborate function and when we get a failure that logging could give some insight into what the function we are testing isn't behaving as expected.

@newhoggy
Copy link
Contributor Author

so I'm wondering why polysemy and not some other effect system?

Mainly due to familiarity and ecosystem - in particular the logging library polysemy-log. We could select a different effect system but we may need to implement the libraries we want and we should also apart from performance evaluate how good the errors are.

@newhoggy newhoggy force-pushed the newhoggy/polysemy branch 2 times, most recently from 5ad1e8c to 8f77407 Compare May 25, 2024 12:22
@smelc
Copy link
Contributor

smelc commented May 27, 2024

All in all, I'm not convinced:

  1. There is no significant reduction of boilerplate
  2. By adding dependencies to https://github.com/tek/polysemy-conc and https://github.com/haskell-works/hw-polysemy we put ourselves in a very niche part of the Haskell ecosystem, and this is not good for long term maintenance. The bus factor here would be very small.

If we are convinced this is important, I would wait a few months to see if this part of the ecosystem gains traction.

@newhoggy newhoggy force-pushed the newhoggy/polysemy branch 2 times, most recently from b11d193 to e06c483 Compare May 30, 2024 21:52
Copy link

github-actions bot commented Aug 3, 2024

This PR is stale because it has been open 45 days with no activity.

@github-actions github-actions bot added the Stale label Aug 3, 2024
Copy link

github-actions bot commented Oct 2, 2024

This issue was closed because it has been stalled for 60 days with no activity.

@github-actions github-actions bot closed this Oct 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants