packages feed

cleveland-0.3.2: src/Test/Cleveland/Doc/Michelson.hs

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

-- | Testing predicates for documentation of Michelson contracts
module Test.Cleveland.Doc.Michelson
  ( testDocBasic

    -- ** Individual test predicates
  , testContractNameAtTop
  , testNoGitInfo
  , testDocNotEmpty
  , testNoAdjacentDescriptions
  , testStorageIsDocumented

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

import Data.Text qualified as T
import Fmt (blockListF, fmt, nameF, pretty)
import Test.HUnit (assertBool, assertFailure)

import Morley.Michelson.Doc
import Morley.Michelson.Typed.Haskell.Doc
import Morley.Util.Text (dquotes)
import Test.Cleveland.Doc.Common

----------------------------------------------------------------------------
-- Test predicates
----------------------------------------------------------------------------

-- Basic predicates
----------------------------------------------------------------------------

-- | Check that contract documentation is wrapped with @contractName@.
testContractNameAtTop :: DocTest
testContractNameAtTop =
  mkDocTest "The whole contract is wrapped into 'DName'" $
  \contractDoc -> do
    let mSections = lookupDocBlockSection @DName (cdContents contractDoc)
    case mSections of
      Nothing -> assertFailure "There is no 'DName' at the top"
      Just _ -> return ()

-- | Check that contracts themselves do not set the git revision. It is supposed to be filled only
-- in the executable.
testNoGitInfo :: DocTest
testNoGitInfo =
  mkDocTest "Git revision is not set in the contract" $
  \contractDoc -> do
    found <- forM (allContractDocItems contractDoc) $ \case
      DGitRevisionUnknown -> return ()
      _ -> assertFailure
        "Git revision already attached.\n\
        \This is considered a bad practice to attach it right in the library\n\
        \because updating git info will require more modules to rebuild.\n\n\
        \Consider using `DGitRevisionUnknown` in the contract and then\n\
        \`attachGitInfo` in executable, or derivative methods."
    case found of
      [] -> assertFailure
        "No Git revision placeholder found, Git info won't be included.\n\
        \Consider inserting 'contractGeneralDefault' to your contract."
      [()] -> pass
      _ -> assertFailure
        "Too many `DGitRevisionUnknown`s in the contract."

-- | Check that there is at least one non-grouping doc item.
--
-- If there is no such, rendered documentation will be empty which signals about
-- most of the documentation pieces being lost.
testDocNotEmpty :: DocTest
testDocNotEmpty =
  mkDocTest "There is at least one DOC_ITEM" $
  \contractDoc ->
    assertBool "No doc items found" $
      any hasDocItem (allContractLayers contractDoc)
  where
    hasDocItem (_, block) = not . null @[()] $ do
      DocSection docElems <- toList block
      docElem <- toList docElems
      guard (deIsAtomic docElem)

-- | Check that no group contains two 'DDescription' items.
--
-- Normally such behaviour is allowed and can be exploited, but often it is not
-- and multiple descriptions appearence under the same group signals about
-- missing grouping wrapper (e.g. use of 'Lorentz.caseT' instead of 'Lorentz.entryCase').
testNoAdjacentDescriptions :: DocTest
testNoAdjacentDescriptions =
  mkDocTest "No two 'DDescription' appear under the same group" $
  \contractDoc ->
    forM_ (allContractLayers contractDoc) $ \(_, block) ->
      case lookupDocBlockSection @DDescription block of
        Just ds@(_ :| _ : _) ->
          let txts = ds <&> \(DDescription txt) -> txt
          in assertFailure . fmt $
             nameF "Found multiple adjacent descriptions" $
             blockListF $ map dquotes txts
        _ -> pass

-- | Check that all descriptions are proper.
testDescriptionsAreWellFormatted :: DocTest
testDescriptionsAreWellFormatted =
  mkDocTest "Descriptions are well-formatted" $
  \contractDoc ->
    let res =
          allContractDocItems contractDoc <&> \(DDescription desc) ->
            check (pretty desc)
    in case nonEmpty (lefts res) of
      Nothing -> pass
      Just errs -> assertFailure . fmt $
        "Some descripions are not well formatted:\n" <> blockListF errs
  where
    check desc
      | T.null desc =
          Left "Empty description."
      | T.last (T.stripEnd desc) /= '.' =
          Left $ "Description does not end with a dot:\n\"" <>
                 toString desc <> "\""
      | otherwise = pass

-- | Test whether storage documentation is included in the contract's documentation.
testStorageIsDocumented :: DocTest
testStorageIsDocumented =
  mkDocTest "Storage documentation is included" $
  \contractDoc ->
    assertBool "No documentation for storage in the contract.\
               \Consider using `dStorage` to attach it." $
      not . null $ allContractDocItems @DStorageType contractDoc

-- | Base properties which should comply for all documentations.
testDocBasic :: [DocTest]
testDocBasic =
  [ testContractNameAtTop
  , testNoGitInfo
  , testDocNotEmpty
  , testNoAdjacentDescriptions
  , testDescriptionsAreWellFormatted
  , testStorageIsDocumented
  ]