packages feed

tasty-hspec-1.2: src/Test/Tasty/Hspec.hs

{-# LANGUAGE CPP #-}

-- | @hspec@ and @tasty@ serve similar purposes; consider using one or the
-- other.
--
-- However, in a pinch, this module allows you to run an @hspec@ 'H.Spec' as a
-- @tasty@ 'T.TestTree'.
module Test.Tasty.Hspec
  ( -- * Tests
    testSpec,
    testSpecs,

    -- * Options
    TreatPendingAs (..),

    -- * Examples
    -- $examples
  )
where

import Control.Monad (guard)
import Data.Maybe (catMaybes, fromMaybe, mapMaybe)
import Data.Proxy
import Data.Typeable (Typeable)
import qualified Test.Hspec as H
import qualified Test.Hspec.Core.Formatters as H
import qualified Test.Hspec.Core.Spec as H
import qualified Test.Tasty as T
import Test.Tasty.Hspec.Compat
import qualified Test.Tasty.Options as T
import qualified Test.Tasty.Providers as T
import qualified Test.Tasty.QuickCheck as TQC
import qualified Test.Tasty.Runners as T
import qualified Test.Tasty.SmallCheck as TSC

-- $examples
--
-- The simplest usage of this library involves first creating a 'T.TestTree' in @IO@, then running it with
-- 'T.defaultMain'.
--
-- @
-- main = do
--   spec <- 'testSpec' "spec" mySpec
--   'T.defaultMain'
--     ('T.testGroup' "tests"
--       [ spec
--       , ...
--       ])
-- @
--
-- You can treat an 'H.pending'/'H.pendingWith' test as a success instead of a
-- failure (the default):
--
-- @
-- tests :: TestTree
-- tests =
--   localOption TreatPendingAsSuccess $ testGroup "My Hspec TestTree"
--     [ unsafePerformIO (testSpec "My first Hspec test" spec_firstHspecTest)
--     , ...
--     ]
-- @
--
-- If you don't do any @IO@ during 'Spec' creation, or the @IO@ need
-- not be performed at any particular time relative to other @IO@ actions, it's
-- perfectly fine to use 'System.IO.unsafePerformIO'.
--
-- @
-- main = do
--   'T.defaultMain'
--     ('T.testGroup' "tests"
--       [ 'System.IO.unsafePerformIO' ('testSpec' "spec" mySpec)
--       , ...
--       ])
-- @

-- | Create a <https://hackage.haskell.org/package/tasty tasty> 'T.TestTree' from an
-- <https://hackage.haskell.org/package/hspec hspec> 'H.Spec'.
testSpec :: T.TestName -> H.Spec -> IO T.TestTree
testSpec name spec = do
  trees <- testSpecs spec
  pure (T.testGroup name trees)

-- | Create a list of <https://hackage.haskell.org/package/tasty tasty> 'T.TestTree' from an
-- <https://hackage.haskell.org/package/hspec hspec> 'H.Spec'. This returns the same tests as 'testSpec'
-- but doesn't create a <https://hackage.haskell.org/package/tasty tasty> test group from them.
testSpecs :: H.Spec -> IO [T.TestTree]
testSpecs spec = do
  -- Here we do as hspec does, which is pre-process a spec by focusing the whole thing, which is a no-op if
  -- anything inside is already focused, but otherwise focuses every item. Then, when creating a tasty test tree,
  -- we just toss the unfocused items.
  trees <- H.runSpecM (focus spec)
  pure (mapMaybe specTreeToTestTree trees)

specTreeToTestTree :: H.SpecTree () -> Maybe T.TestTree
specTreeToTestTree = \case
  Node name trees -> pure (T.testGroup name (mapMaybe specTreeToTestTree trees))
  NodeWithCleanup cleanup trees -> do
    tree <- specTreeToTestTree (H.Node "(unnamed)" trees)
    pure (T.WithResource (T.ResourceSpec (return ()) cleanup) (const tree))
  Leaf item -> do
    guard (itemIsFocused item)
    pure (T.singleTest (H.itemRequirement item) (Item item))

newtype Item
  = Item (H.Item ())
  deriving (Typeable)

instance T.IsTest Item where
  run opts (Item item) progress = do
    qcArgs <- optionSetToQuickCheckArgs opts
    H.Result _ result <- itemExample item (params qcArgs) ($ ()) progress'
    pure
      ( case result of
          H.Success -> T.testPassed ""
          H.Pending _ reason ->
            case T.lookupOption opts of
              TreatPendingAsFailure -> T.testFailed reason'
              TreatPendingAsSuccess -> T.testPassed reason'
            where
              reason' = "# PENDING: " ++ fromMaybe "No reason given" reason
          H.Failure _ reason ->
            case reason of
              H.NoReason -> T.testFailed ""
              H.Reason x -> T.testFailed x
              H.ExpectedButGot preface expected actual ->
                T.testFailed . unlines . catMaybes $
                  [ preface,
                    Just ("expected: " ++ expected),
                    Just (" but got: " ++ actual)
                  ]
              H.Error _ exception -> T.testFailed ("uncaught exception: " ++ H.formatException exception)
      )
    where
      params qcArgs =
        H.Params
          { H.paramsQuickCheckArgs = qcArgs,
            H.paramsSmallCheckDepth =
              case T.lookupOption opts of
                TSC.SmallCheckDepth depth ->
                  depth
          }

      progress' (x, y) =
        progress
          T.Progress
            { T.progressText = "",
              T.progressPercent = fromIntegral x / fromIntegral y
            }

  testOptions =
    pure
      [ T.Option (Proxy :: Proxy TreatPendingAs),
        T.Option (Proxy :: Proxy TQC.QuickCheckTests),
        T.Option (Proxy :: Proxy TQC.QuickCheckReplay),
        T.Option (Proxy :: Proxy TQC.QuickCheckMaxSize),
        T.Option (Proxy :: Proxy TQC.QuickCheckMaxRatio),
        T.Option (Proxy :: Proxy TSC.SmallCheckDepth)
      ]

-- | How to treat @hspec@ pending tests.
--
-- @tasty@ does not have the concept of pending tests, so we must map them to
-- either successes or failures. By default, they are treated as failures.
--
-- Set via the command line flag @--treat-pending-as (success|failure)@.
data TreatPendingAs
  = -- | Default.
    TreatPendingAsFailure
  | TreatPendingAsSuccess

instance T.IsOption TreatPendingAs where
  defaultValue =
    TreatPendingAsFailure

  parseValue = \case
    "failure" -> Just TreatPendingAsFailure
    "success" -> Just TreatPendingAsSuccess
    _ -> Nothing

  optionName =
    pure "treat-pending-as"

  optionHelp =
    pure "How to treat pending hspec tests ('failure' or 'success')"

#if MIN_VERSION_tasty(1,3,0)
  showDefaultValue _ =
    Just "failure"
#endif