packages feed

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

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# 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
    <+ fun (genList @Department)
    <+ funTo @Gen Department
    <+ fun (genList @Employee)
    <+ funTo @Gen Employee
    <+ funTo @Gen Age
    <+ funTo @Gen Fixed
    <+ funTo @Gen Name
    <+ fun genInt
    <+ fun genText
    <+ fun genDouble

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

-- * WITH VARIANTS

registry' =
  fun (sequence . replicate @(Gen Salary) 100)
    <: 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 . make @(Gen a)

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

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

runWith :: Monad m => Registry ins out -> RegistryProperty m a -> PropertyT m a
runWith r rp = evalStateT rp r