packages feed

sydtest-hspec-0.0.0.0: src/Test/Syd/Hspec.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

module Test.Syd.Hspec (fromHspec) where

import Control.Exception
import Control.Monad.Writer
import Data.List
import qualified Test.Hspec.Core.Spec as Hspec
import Test.Syd as Syd

-- | Import an Hspec 'Test.Hspec.Spec' as a Sydtest 'Test.Syd.Spec'.
--
-- The reasoning behind this function is that, eventhough migration from hspec
-- to sydtest is usually very simple, you might depend on certain libraries
-- beyond your control that still use hspec.  In that case you want to be able
-- to still use those libraries but also use sydtest already.
--
-- For this reason, and because hspec doesn't tell you wether a test is pending
-- until after you run it, pending tests are imported as passing tests.
fromHspec :: Hspec.Spec -> Syd.Spec
fromHspec (Hspec.SpecM specWriter) = do
  (result, trees) <- liftIO $ runWriterT specWriter
  mapM_ importSpecTree trees
  pure result

importSpecTree :: Hspec.SpecTree () -> Syd.Spec
importSpecTree = go
  where
    go = \case
      Hspec.Leaf item -> importItem item
      Hspec.Node d ts -> describe d $ mapM_ go ts
      -- Hspec.NodeWithCleanup's semantics are so weird that we can only do
      -- this translation if inner equals ().
      Hspec.NodeWithCleanup cleanup ts -> afterAll_ (cleanup ()) $ mapM_ go ts

importItem :: forall inner. Hspec.Item inner -> Syd.TestDefM '[] inner ()
importItem item@Hspec.Item {..} =
  let parallelMod = case itemIsParallelizable of
        Just True -> parallel
        Just False -> sequential
        Nothing -> id
   in parallelMod $
        it itemRequirement (ImportedItem item :: ImportedItem inner)

newtype ImportedItem a = ImportedItem (Hspec.Item a)

instance IsTest (ImportedItem a) where
  type Arg1 (ImportedItem a) = ()
  type Arg2 (ImportedItem a) = a
  runTest = runImportedItem

runImportedItem ::
  ImportedItem inner ->
  TestRunSettings ->
  ((() -> inner -> IO ()) -> IO ()) ->
  IO TestRunResult
runImportedItem (ImportedItem Hspec.Item {..}) trs wrapper = do
  errOrRes <- applyWrapper2 wrapper $ \() inner -> do
    let params :: Hspec.Params
        params =
          Hspec.Params
            { Hspec.paramsQuickCheckArgs = makeQuickCheckArgs trs,
              -- TODO use the right depth when sydtest supports smallcheck
              Hspec.paramsSmallCheckDepth = Hspec.paramsSmallCheckDepth Hspec.defaultParams
            }
        callback :: Hspec.ProgressCallback
        callback = const $ pure ()
    itemExample params (\takeInner -> takeInner inner) callback
  let (testRunResultStatus, testRunResultException) = case errOrRes of
        Left ex -> (TestFailed, Just ex)
        Right result -> case Hspec.resultStatus result of
          Hspec.Success -> (TestPassed, Nothing)
          -- This is certainly a debatable choice, but there's no need to make
          -- tests fail here, and there's no way to know ahead of time whether
          -- a test is pending so we have no choice.
          Hspec.Pending _ _ -> (TestPassed, Nothing)
          Hspec.Failure mloc fr ->
            let withExtraContext :: Maybe String -> Assertion -> Assertion
                withExtraContext = maybe id (\extraContext a -> Context a extraContext)
                niceLocation :: Hspec.Location -> String
                niceLocation Hspec.Location {..} = intercalate ":" [locationFile, show locationLine, show locationColumn]
                withLocationContext :: Assertion -> Assertion
                withLocationContext = withExtraContext $ niceLocation <$> mloc
                assertion = case fr of
                  Hspec.NoReason -> Right $ ExpectationFailed "Hspec had no more information about this failure."
                  Hspec.Reason s -> Right $ ExpectationFailed s
                  Hspec.ExpectedButGot mExtraContext expected actual -> Right $ withExtraContext mExtraContext $ NotEqualButShouldHaveBeenEqual actual expected
                  Hspec.Error mExtraContext e -> withExtraContext mExtraContext <$> Left (displayException e)
             in ( TestFailed,
                  Just
                    ( Context
                        <$> ( withLocationContext <$> assertion
                            )
                          <*> pure
                            (Hspec.resultInfo result)
                    )
                )
  let testRunResultNumTests = Nothing
  let testRunResultNumShrinks = Nothing
  let testRunResultGoldenCase = Nothing
  let testRunResultFailingInputs = []
  let testRunResultExtraInfo = Nothing
  let testRunResultLabels = Nothing
  let testRunResultClasses = Nothing
  let testRunResultTables = Nothing

  pure TestRunResult {..}