packages feed

sydtest-0.13.0.4: src/Test/Syd/Def/TestDefM.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE IncoherentInstances #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

module Test.Syd.Def.TestDefM where

import Control.Monad.Random
import Control.Monad.Reader
import Control.Monad.Writer.Strict
import Data.Kind
import Data.Text (Text)
import GHC.Generics (Generic)
import Test.QuickCheck.IO ()
import Test.Syd.OptParse
import Test.Syd.Run
import Test.Syd.SpecDef

-- | A synonym for easy migration from hspec
type Spec = SpecWith ()

-- | A synonym for easy migration from hspec
type SpecWith inner = SpecM inner ()

-- | A synonym for easy migration from hspec
type SpecM inner result = TestDefM '[] inner result

-- | A synonym for a test suite definition
type TestDef outers inner = TestDefM outers inner ()

-- | The test definition monad
--
-- This type has three parameters:
--
-- * @outers@: A type-level list of the outer resources. These are resources that are prived once, around a group of tests. (This is the type of the results of `aroundAll`.)
-- * @inner@: The inner resource. This is a resource that is set up around every test, and even every example of a property test. (This is the type of the result of `around`.)
-- * @result@: The result ('TestDefM' is a monad.)
--
-- In practice, all of these three parameters should be '()' at the top level.
newtype TestDefM (outers :: [Type]) inner result = TestDefM
  { unTestDefM :: WriterT (TestForest outers inner) (ReaderT TestDefEnv IO) result
  }
  deriving
    ( Functor,
      Applicative,
      Monad,
      MonadIO,
      MonadReader TestDefEnv,
      MonadWriter (TestForest outers inner)
    )

data TestDefEnv = TestDefEnv
  { testDefEnvDescriptionPath :: ![Text],
    testDefEnvTestRunSettings :: !TestRunSettings
  }
  deriving (Show, Eq, Generic)

execTestDefM :: Settings -> TestDefM outers inner result -> IO (TestForest outers inner)
execTestDefM sets = fmap snd . runTestDefM sets

runTestDefM :: Settings -> TestDefM outers inner result -> IO (result, TestForest outers inner)
runTestDefM sets defFunc = do
  let func = unTestDefM defFunc
  let testDefEnv =
        TestDefEnv
          { testDefEnvDescriptionPath = [],
            testDefEnvTestRunSettings = toTestRunSettings sets
          }
  (a, testForest) <- runReaderT (runWriterT func) testDefEnv
  let testForest' = filterTestForest (settingFilters sets) testForest
  stdgen <- case settingSeed sets of
    FixedSeed seed -> pure $ mkStdGen seed
    RandomSeed -> newStdGen
  let testForest'' =
        if settingRandomiseExecutionOrder sets
          then evalRand (randomiseTestForest testForest') stdgen
          else testForest'
  pure (a, testForest'')

-- | Get the path of 'describe' strings upwards.
--
-- Note that using this function makes tests less movable, depending on what
-- you do with these strings.
-- For example, if you use these strings to define the path to a golden test
-- file, then that path will change if you move the tests somewhere else.
-- This combines unfortunately with the way @sydtest-discover@ makes the module
-- name part of this path.
-- Indeed: moving your tests to another module will change their path as well,
-- if you use @sydtest-discover@.
-- Also note that while test forests can be randomised, their description path
-- upwards will not, because of how trees are structured.
getTestDescriptionPath :: TestDefM outers inner [Text]
getTestDescriptionPath = asks testDefEnvDescriptionPath

toTestRunSettings :: Settings -> TestRunSettings
toTestRunSettings Settings {..} =
  TestRunSettings
    { testRunSettingSeed = settingSeed,
      testRunSettingMaxSuccess = settingMaxSuccess,
      testRunSettingMaxSize = settingMaxSize,
      testRunSettingMaxDiscardRatio = settingMaxDiscard,
      testRunSettingMaxShrinks = settingMaxShrinks,
      testRunSettingGoldenStart = settingGoldenStart,
      testRunSettingGoldenReset = settingGoldenReset
    }