module Test.Framework.Runners.Core ( RunTest(..), RunningTest, SomeImproving(..), FinishedTest, runTests, TestRunner(..), runTestTree ) where import Test.Framework.Core import Test.Framework.Improving import Test.Framework.Options import Test.Framework.Runners.Options import Test.Framework.Runners.TestPattern import Test.Framework.Runners.ThreadPool import Test.Framework.Seed import Test.Framework.Utilities import Control.Concurrent.MVar import Control.Exception (mask, finally, onException) import Control.Monad import Data.Maybe import Data.Monoid import Data.Typeable -- | A test that has been executed or is in the process of execution data RunTest a = RunTest TestName TestTypeName a | RunTestGroup TestName [RunTest a] deriving (Show) data SomeImproving = forall i r. TestResultlike i r => SomeImproving (i :~> r) type RunningTest = RunTest SomeImproving type FinishedTest = RunTest (String, Bool) runTests :: CompleteRunnerOptions -- ^ Top-level runner options -> [Test] -- ^ Tests to run -> IO [RunningTest] runTests ropts tests = do let test_patterns = unK $ ropt_test_patterns ropts test_options = unK $ ropt_test_options ropts (run_tests, actions) <- runTests' $ map (runTestTree test_options test_patterns) tests _ <- executeOnPool (unK $ ropt_threads ropts) actions return run_tests -- | 'TestRunner' class simplifies folding a 'Test'. You need to specify -- the important semantic actions by instantiating this class, and -- 'runTestTree' will take care of recursion and test filtering. class TestRunner b where -- | How to handle a single test runSimpleTest :: (Testlike i r t, Typeable t) => TestOptions -> TestName -> t -> b -- | How to skip a test that doesn't satisfy the pattern skipTest :: b -- | How to handle an IO test (created with 'buildTestBracketed') runIOTest :: IO (b, IO ()) -> b -- | How to run a test group runGroup :: TestName -> [b] -> b -- | Run the test tree using a 'TestRunner' runTestTree :: TestRunner b => TestOptions -> [TestPattern] -- ^ skip the tests that do not match any of these patterns, unless -- the list is empty -> Test -> b runTestTree initialOpts pats topTest = go initialOpts [] topTest where go opts path t = case t of Test name testlike -> if null pats || any (`testPatternMatches` (path ++ [name])) pats then runSimpleTest opts name testlike else skipTest TestGroup name tests -> let path' = path ++ [name] in runGroup name $ map (go opts path') tests PlusTestOptions extra_topts test -> go (opts `mappend` extra_topts) path test BuildTestBracketed build -> runIOTest $ onLeft (go opts path) `fmap` build newtype StdRunner = StdRunner { run :: IO (Maybe (RunningTest, [IO ()])) } instance TestRunner StdRunner where runSimpleTest topts name testlike = StdRunner $ do (result, action) <- runTest (completeTestOptions topts) testlike return (Just (RunTest name (testTypeName testlike) (SomeImproving result), [action])) skipTest = StdRunner $ return Nothing runGroup name tests = StdRunner $ do (results, actions) <- runTests' tests return $ if null results then Nothing else Just ((RunTestGroup name results), actions) runIOTest ioTest = StdRunner $ mask $ \restore -> ioTest >>= \(StdRunner test, cleanup) -> do mb_res <- restore test `onException` cleanup case mb_res of -- No sub-tests: perform the cleanup NOW Nothing -> cleanup >> return Nothing Just (run_test, actions) -> do -- Sub-tests: perform the cleanup as soon as each of them have completed (mvars, actions') <- liftM unzip $ forM actions $ \action -> do mvar <- newEmptyMVar return (mvar, action `finally` putMVar mvar ()) -- NB: the takeMVar action MUST be last in the list because the returned actions are -- scheduled left-to-right, and we want all the actions we depend on to be scheduled -- before we wait for them to complete, or we might deadlock. -- -- FIXME: this is a bit of a hack because it uses one pool thread just waiting -- for some other pool threads to complete! Switch to parallel-io? return $ Just (run_test, actions' ++ [(cleanup >> mapM_ takeMVar mvars)]) runTests' :: [StdRunner] -> IO ([RunningTest], [IO ()]) runTests' = fmap (onRight concat . unzip . catMaybes) . mapM run completeTestOptions :: TestOptions -> CompleteTestOptions completeTestOptions to = TestOptions { topt_seed = K $ topt_seed to `orElse` RandomSeed, topt_maximum_generated_tests = K $ topt_maximum_generated_tests to `orElse` 100, topt_maximum_unsuitable_generated_tests = K $ topt_maximum_unsuitable_generated_tests to `orElse` 1000, topt_maximum_test_size = K $ topt_maximum_test_size to `orElse` 100, topt_maximum_test_depth = K $ topt_maximum_test_depth to `orElse` 5, topt_timeout = K $ topt_timeout to `orElse` Nothing }