packages feed

registry-0.1.2.4: test/Test/Data/Registry/GenSpec.hs

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}

{-
  This module shows how to use the registry with hedgehog generators
-}
module Test.Data.Registry.GenSpec where

import           Data.List             (partition)
import           Data.Registry
import           Hedgehog.Gen          as Gen
import           Hedgehog.Range        as Range
import           Protolude             as P
import           Test.Tasty.Extensions

-- * DATA MODEL

newtype Company =
  Company { departments :: [Department] }
  deriving (Eq, Show)

newtype Department =
  Department { employees :: [Employee] }
  deriving (Eq, Show)

data Employee = Employee {
    name   :: Name
  , age    :: Age
  , salary :: Salary
} deriving (Eq, Show)

newtype Name = Name Text deriving (Eq, Show)
newtype Age  = Age Int deriving (Eq, Show, Ord, Num)

data Salary =
    Fixed Int
  | Variable Int Double
  deriving (Eq, Show)

isFixed (Fixed _) = True
isFixed _ = False

-- * GENERATORS

genText :: Gen Text
genText = Gen.text (Range.linear 2 10) Gen.ascii

genList :: forall a . (Typeable a) => Gen a -> Gen [a]
genList = Gen.list (Range.linear 0 3)

genInt :: Gen Int
genInt = Gen.int (Range.linear 1 100)

genDouble :: Gen Double
genDouble = Gen.double (Range.linearFrac 1 100)

setDepartmentWithOneEmployee :: Monad m => RegistryProperty m ()
setDepartmentWithOneEmployee = do
  e <- forall @Employee
  tweakGen @[Employee] (const $ pure [e])

setCompanyWithOneDepartment :: Monad m => RegistryProperty m ()
setCompanyWithOneDepartment = do
  d <- forall @Department
  tweakGen @[Department] (const (pure [d]))

setMinimalCompany :: Monad m => RegistryProperty m ()
setMinimalCompany =
  -- be careful, this is NOT commutative!
  -- if you set a company with one department first you may end up
  -- with a department with no employees, generated once and forall
  setDepartmentWithOneEmployee >>
  setCompanyWithOneDepartment

-- | Create a registry for all generators
registry =
     funTo @Gen Company
  +: funTo @Gen Department
  +: funTo @Gen Employee
  +: funTo @Gen Fixed
  +: funTo @Gen Name
  +: funTo @Gen Age
  +: fun (genList @Department)
  +: fun (genList @Employee)
  +: fun genInt
  +: fun genText
  +: fun genDouble
  +: end

test_company_with_one_employee = noShrink $ prop "generate just one employee" $ run $ do
  setMinimalCompany
  company <- forall @Company
  let allEmployees = company & departments >>= (& employees)
  length allEmployees === 1


-- * WITH VARIANTS

registry' =
     fun (sequence . replicate @(Gen Salary) 10)
  +: fun salaryGen
  +: funTo @Gen (tag @"Fixed" Fixed)
  +: funTo @Gen (tag @"Variable" Variable)
  +: registry

salaryGen :: Gen (Tag "Fixed" Salary) -> Gen (Tag "Variable" Salary) -> Gen Salary
salaryGen fixed variable = choice [unTag <$> fixed, unTag <$> variable]

test_with_different_salaries = noShrink $ prop "generate both fixed and variable salaries" $ runWith registry' $ do
  salaries <- forall @[Salary]
  let (fixed, variables) = partition isFixed salaries

  annotate "the choice operator allows us to generate both fixed and variable salaries"
  not (null fixed)     === True
  not (null variables) === True


-- * HELPERS

type RegistryProperty m a = forall ins out . StateT (Registry ins out) (PropertyT m) a

forall :: forall a m . (HasCallStack, Typeable a, Show a, Monad m) => RegistryProperty m a
forall = withFrozenCallStack $ get >>= P.lift . forAll . makeUnsafe @(Gen a)

tweakGen :: forall a m . (Typeable a, Monad m) => (Gen a -> Gen a) -> RegistryProperty m ()
tweakGen f = modify $ tweakUnsafe @(Gen a) f

run :: Monad m => RegistryProperty m a -> PropertyT m a
run = runWith registry

runWith :: Monad m => Registry ins out -> RegistryProperty m a -> PropertyT m a
runWith = flip evalStateT
----
tests = $(testGroupGenerator)