packages feed

lorentz-0.2.0: src/Lorentz/Test/Doc.hs

-- | Testing predicates for documentation of Lorentz contracts.
module Lorentz.Test.Doc
  ( -- * Test predicates
    testLorentzDoc

    -- * Individual test predicates
  , testDeclaresParameter
  , testEachEntrypointIsDescribed
  , testParamBuildingStepsAreFinalized
  , testAllEntrypointsAreCallable

  , module Michelson.Doc.Test
  ) where

import Fmt (Buildable(..), blockListF, fmt, nameF, pretty)
import Test.HUnit (assertBool, assertFailure)

import Lorentz.EntryPoints.Doc
import Michelson.Doc
import Michelson.Doc.Test
import Util.Markdown
import Util.Typeable

-- | All ways of describing an entrypoint behaviour.
data DocEpDescription
  = DocEpDescription DDescription
  | DocEpReference DEntryPointReference

instance Buildable DocEpDescription where
  build = \case
    DocEpDescription (DDescription txt) ->
      "description: " <> txt
    DocEpReference (DEntryPointReference name (Anchor anchor)) ->
      "reference \"" <> build name <> "\" (" <> build anchor <> ")"

-- | Extract 'DocEpDescription's of a documentation block.
lookupDocEpDescription :: DocBlock -> [DocEpDescription]
lookupDocEpDescription block = mconcat
  [ map DocEpDescription . maybe [] toList $ lookupDocBlockSection block
  , map DocEpReference . maybe [] toList $ lookupDocBlockSection block
  ]

-- | Check that contract documents its parameter.
testDeclaresParameter :: DocTest
testDeclaresParameter =
  mkDocTest "Contract parameter is documented" $
  \contractDoc ->
    assertBool "No doc items describing contract parameter found" $
      or $ forEachContractLayer contractDoc check
  where
    check Nothing _ = False
    check (Just sdi) _ =
      -- Currently the only way to document parameter (mentioning type of each arm)
      -- is using 'entryCase'. This may not suit for small contracts, then
      -- someone needs to invent another way to document parameter and also mention
      -- it below.
      case sdi of
        SomeDocItem (castIgnoringPhantom -> Just DEntryPoint{}) -> True
        _ -> False

-- | Check that no group contains two 'DDescription' or 'DEntryPointReference'
-- items.
--
-- This is a stricter version of 'testNoAdjacentDescriptions' test.
testNoAdjacentEpDescriptions :: DocTest
testNoAdjacentEpDescriptions =
  mkDocTest "No two 'DDescription' appear under the same group" $
  \contractDoc ->
    sequence_ . forEachContractLayer contractDoc $ \_ block ->
      case lookupDocEpDescription block of
        ds@(_ : _ : _) ->
          assertFailure . fmt $
             nameF "Found multiple adjacent entrypoint descriptions" $
             blockListF $ map (quotes . build) (toList ds)
        _ -> pass
    where
      quotes t = "\"" <> t <> "\""

-- | It's a common issue to forget to describe an entrypoint.
testEachEntrypointIsDescribed :: DocTest
testEachEntrypointIsDescribed =
  mkDocTest "Each entrypoint has 'DDescription'" $
  \contractDoc ->
    sequence_ . forEachContractLayer contractDoc $ \mDocItem block ->
      runMaybeT $ do
        SomeDocItem docItem <- MaybeT . pure $ mDocItem
        dep@DEntryPoint{} <- MaybeT . pure $ castIgnoringPhantom docItem
        [] <- pure $ lookupDocEpDescription block
        MaybeT . assertFailure $
          "Entrypoint '" <> pretty (depName dep) <> "' does not contain \
          \any description.\n\
          \Put e.g. `doc $ DDescription \"text\"` in the entrypoint logic to \
          \fix this."

-- | Check that 'finalizeParamCallingDoc' is applied to the contract as it
-- always should.
testParamBuildingStepsAreFinalized :: DocTest
testParamBuildingStepsAreFinalized =
  mkDocTest "'finalizeParamCallingDoc' is applied" $
  \contractDoc ->
    sequence_ . forEachContractDocItem contractDoc $ \DEntryPointArg{..} ->
      unless (areFinalizedParamBuildingSteps epaBuilding) $
        assertFailure
          "Found unfinalized param building steps, \
          \'How to call this entrypoint' section may be incorrect.\n\
          \Have you applied 'finalizeParamCallingDoc' to your contract?"

-- | Check that all documented entrypoints are callable.
--
-- Sometimes having such an entrypoint is fine, e.g. when you have an explicit
-- default entrypoint deep in one arm then other arms (entire arms, not
-- individual entrypoints within them) are uncallable unless also assigned a
-- field annotation; for example see [doc for uncallable entrypoints] note.
-- If this is your case, exclude this test suite with 'excludeDocTest'.
-- But such situations are rare.
--
-- More often, this test failure indicates that entrypoints are documented
-- incorrectly, e.g. `caseT` is used in some place instead of `entryCase`.
-- Check whether printed building steps are correct.
--
-- NB: another, simplified example of case when disabling this test is
-- justified:
--
-- @
-- data SubParam1 = Do1 | Default
-- data SubParam2 = Do2 | Do3
-- data Param = Arm1 SubParam1 | Arm2 SubParam2
--   -- ^ with entrypoints derived via 'EpdRecursive'
-- @
--
-- In this case entire @Arm1@ and @Arm2@ are not true entrypoints, only @Default@
-- and @Do{1,2,3}@ are, but @Arm1@ and @Arm2@ will still appear in documentation
-- as entrypoints.
testAllEntrypointsAreCallable :: DocTest
testAllEntrypointsAreCallable =
  mkDocTest "All entrypoints are callable" $
  \contractDoc ->
    sequence_ . forEachContractDocItem contractDoc $ \DEntryPointArg{..} ->
      forM_ epaBuilding $ \case
        PbsUncallable pbs ->
          assertFailure . fmt $
            "Found an uncallable entrypoint.\n\
            \Dummy parameter building steps for it: " <> blockListF (reverse pbs)
        _ -> pass

-- | Tests all properties.
testLorentzDoc :: [DocTest]
testLorentzDoc = mconcat
  [ testDocBasic
  , [ testDeclaresParameter
    , testNoAdjacentEpDescriptions
    , testEachEntrypointIsDescribed
    , testParamBuildingStepsAreFinalized
    , testAllEntrypointsAreCallable
    ]
  ]