packages feed

cleveland-0.1.1: src/Test/Cleveland/Doc/Lorentz.hs

-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

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

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

    -- ** Re-exports
  , module Test.Cleveland.Doc.Common
  ) where

import Data.Typeable (typeRep)
import Fmt (Buildable(..), blockListF, fmt, nameF, (+|), (|+))
import Test.HUnit (assertBool, assertFailure)

import Lorentz.Entrypoints.Doc
import Lorentz.Errors
import Morley.Michelson.Doc
import Morley.Util.Markdown
import Morley.Util.Text (dquotes)
import Test.Cleveland.Doc.Common
import Test.Cleveland.Doc.Michelson (testDocBasic)

-- | 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" $
      any check (allContractLayers contractDoc)
  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
        DEntrypointDocItem _ -> True
        _ -> False

-- | Check that no group contains two 'DDescription' or 'DEntrypointReference'
-- items.
--
-- This is a stricter version of 'Test.Cleveland.Doc.Michelson.testNoAdjacentDescriptions' test.
testNoAdjacentEpDescriptions :: DocTest
testNoAdjacentEpDescriptions =
  mkDocTest "No two 'DDescription' appear under the same group" $
  \contractDoc ->
    forM_ (allContractLayers contractDoc) $ \(_, block) ->
      case lookupDocEpDescription block of
        ds@(_ : _ : _) ->
          assertFailure . fmt $
             nameF "Found multiple adjacent entrypoint descriptions" $
             blockListF $ map (dquotes . build) (toList ds)
        _ -> pass

-- | It's a common issue to forget to describe an entrypoint.
testEachEntrypointIsDescribed :: DocTest
testEachEntrypointIsDescribed =
  mkDocTest "Each entrypoint has 'DDescription'" $
  \contractDoc -> do
    missingDescs :: [Text] <-
      fmap catMaybes . sequence $ allContractLayers contractDoc <&>
        \(mDocItem, block) -> runMaybeT $ do
          DEntrypointDocItem dep <- MaybeT . pure $ mDocItem
          [] <- pure $ lookupDocEpDescription block
          return (depName dep)

    case nonEmpty missingDescs of
      Nothing -> pass
      Just descs ->
        assertFailure . fmt $
          "Descriptions for the following entrypoints are not found: \n" <>
          blockListF descs <>
          "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 -> do
    forM_ (allContractDocItems contractDoc) $ \DEntrypointArg{..} ->
      unless (areFinalizedParamBuildingSteps epaBuilding) $
        assertFailure
          "Found unfinalized param building steps, \
          \'How to call this entrypoint' section will not be acknowledged of \
          \contract entrypoints coming from field annotations and may be incorrect.\n\
          \Do you pick documentation of the entire 'Contract', not just contract code? \
          \If the latter is necessary, please call 'finalizeParamCallingDoc' manually."

-- | 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. 'Lorentz.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 t'Lorentz.Entrypoints.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 ->
    forM_ (allContractDocItems contractDoc) $ \DEntrypointArg{..} ->
      forM_ epaBuilding $ \case
        PbsUncallable pbs ->
          assertFailure . fmt $
            "Found an uncallable entrypoint.\n\
            \Dummy parameter building steps for it: " <> blockListF (reverse pbs)
        _ -> pass

-- | Check that no error is thrown outside of entrypoint.
--
-- This is possible e.g. when some check is performed in the beginning of the
-- contract, before an entrypoint is selected and executed.
--
-- All errors (except for internal ones) must belong to some entrypoint for
-- documetation to be constructed sanely. Thus it might be necessary to create
-- a special section common for multiple entrypoints to include that error.
testAllErrorsBelongToEntrypoints :: DocTest
testAllErrorsBelongToEntrypoints =
  mkDocTest "All errors belong to some entrypoint" $
  \contractDoc ->
    forM_ (allContractLayers contractDoc) $ \(mGroup, block) ->
      case lookupDocBlockSection @DThrows block of
        Nothing -> pass
        Just (DThrows (_ :: Proxy err) :| _) ->
          if | Just DEntrypointDocItem{} <- mGroup -> pass
                -- Internal errors might be freely used everywhere and we do not
                -- display them on per-entrypoint basis, so we don't really care
                -- if they appear outside of all entrypoints
              | isInternalErrorClass (errorDocClass @err) -> pass
              | otherwise -> assertFailure . fmt $
                "Found an error `" +| show @Text (typeRep (Proxy @err)) |+ "` \
                \thrown outside of any entrypoint.\n\n\
                \If this should not belong to any existing entrypoint,\n\
                \consider marking the code as common for other entrypoints using\n\
                \CommonContractBehaviourKind or CommonEntrypointsBehaviourKind."

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