{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Trans where
-- base
-- base
import Control.Monad (forever, void)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
-- transformers
import Control.Monad.Trans.Class
import Control.Monad.Trans.Writer (Writer, tell, runWriter, execWriter)
-- QuickCheck
import Test.QuickCheck
import qualified Test.QuickCheck as QuickCheck
-- test-framework
import Test.Framework
-- test-framework-hunit
import Test.Framework.Providers.HUnit
-- HUnit
import Test.HUnit hiding (Test)
-- monad-schedule
import Control.Monad.Schedule.Trans
import Control.Monad.Schedule.Class (scheduleAndFinish)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Control.Arrow
import Control.Monad.Free (_Free)
sampleActions :: NonEmpty (MySchedule ())
sampleActions = [wait 23, wait 42]
tests = testGroup "Trans"
[ testCase "Only leftover time is waited"
$ assertRunsLike sampleActions [Waited 23, Waited (42 - 23)]
, testCase "Scheduling two waits"
$ assertRunsEqual sampleActions (NonEmpty.reverse sampleActions)
, testCase "Different number of waits"
$ assertRunsLike
[ myLog "Thread 1 starts" >> wait 5 >> myLog "Thread 1 action" >> wait 5 >> myLog "Thread 1 done"
, myLog "Thread 2 starts" >> wait 7 >> myLog "Thread 2 done"
]
[ Log "Thread 1 starts"
, Log "Thread 2 starts"
, Waited 5
, Log "Thread 1 action"
, Waited 2
, Log "Thread 2 done"
, Waited 3
, Log "Thread 1 done"
]
, testCase "Blocking thread doesn't starve other thread (positive wait times)"
$ assertRunContains
[ forever $ myLog "Busy loop starts" >> wait 1 >> myLog "Busy loop ends"
, myLog "One off thread starts" >> wait 2 >> myLog "One off thread does a thing" >> wait 1 >> myLog "One off thread done"
]
$ Log "One off thread done"
, testCase "Blocking thread doesn't starve other thread (0 waits)"
$ assertRunContains
[ forever $ myLog "Busy loop starts" >> wait 0 >> myLog "Busy loop ends"
, myLog "One off thread starts" >> wait 0 >> myLog "One off thread does a thing" >> wait 0 >> myLog "One off thread done"
]
$ Log "One off thread done"
, testProperty "Every thread is eventually woken up"
$ withMaxSuccess 1000
$ \(scripts :: Scripts) (skip :: Positive Int) ->
let steps
-- In principle, every iteration of the whole script, every thread should be woken up, but allow for some extra overhead
= take (3 * sizeScripts scripts + 3)
-- Randomly skip some steps ahead
$ drop (getPositive skip)
$ runMySchedule $ interpretScripts scripts
in counterexample ("steps: " ++ show steps)
$ conjoin $ map (Log >>> (`elem` steps)) $ NonEmpty.toList $ threadNames scripts
]
assertRunsEqual :: NonEmpty (MySchedule a1) -> NonEmpty (MySchedule a2) -> Assertion
assertRunsEqual actions1 actions2 = assertEqual "Should run the same under scheduling" (runMySchedule actions1) (runMySchedule actions2)
assertRunsLike :: NonEmpty (MySchedule a) -> [Event] -> Assertion
assertRunsLike actions events = assertEqual "Should run like the following under scheduling" events $ runMySchedule actions
assertRunContains :: NonEmpty (MySchedule a) -> Event -> Assertion
assertRunContains actions event = assertBool ("The run should contain the event " ++ show event) $ event `elem` runMySchedule actions
assertInitiallyRunsLike :: NonEmpty (MySchedule a) -> [Event] -> Assertion
assertInitiallyRunsLike actions events = assertEqual "Should, at the beginning, run like the following under scheduling" events $ take (length events) $ runMySchedule actions
data Event
= Log String
| Waited Integer
deriving (Eq, Show)
type MySchedule a = ScheduleT Integer (Writer [Event]) a
myLog :: String -> MySchedule ()
myLog = lift . tell . pure . Log
runMySchedule :: NonEmpty (MySchedule a) -> [Event]
runMySchedule = execWriter . runScheduleT (tell . pure . Waited) . scheduleAndFinish
data Script = Script
{ prefix :: [Positive Integer]
, loop :: NonEmpty (Positive Integer)
, threadName :: String
}
deriving Show
-- FIXME Why is this not in QuickCheck?
instance Arbitrary a => Arbitrary (NonEmpty a) where
arbitrary = (NonEmpty.:|) <$> arbitrary <*> arbitrary
genScript :: ThreadName -> Gen Script
genScript threadName = do
prefix <- arbitrary
loop <- arbitrary
return Script { .. }
instance Arbitrary Scripts where
arbitrary = do
nScripts <- getPositive <$> (arbitrary :: Gen (Positive Integer))
getScripts <- mapM genScript $ show <$> NonEmpty.fromList [1..nScripts]
return Scripts { .. }
newtype Scripts = Scripts { getScripts :: NonEmpty Script }
deriving Show
type ThreadName = String
interpretScript :: Script -> MySchedule ()
interpretScript Script { .. } = do
let perform interval = myLog threadName >> wait (getPositive interval)
mapM_ perform prefix
forever $ mapM_ perform loop
interpretScripts :: Scripts -> NonEmpty (MySchedule ())
interpretScripts = NonEmpty.map interpretScript . getScripts
sizeScript :: Script -> Int
sizeScript Script { .. } = fromInteger $ sum (getPositive <$> prefix) + sum (getPositive <$> loop)
sizeScripts :: Scripts -> Int
sizeScripts = sum . fmap sizeScript . getScripts
threadNames :: Scripts -> NonEmpty ThreadName
threadNames = fmap threadName . getScripts