packages feed

kazura-queue-0.1.0.3: test/Test/Expectations.hs

{-# LANGUAGE ScopedTypeVariables #-}

module Test.Expectations
    ( module Test.Expectations
    , module EX
    ) where

import Test.Hspec.Expectations as EX hiding (shouldBe, shouldContain,
                                      shouldNotBe, shouldReturn, shouldSatisfy,
                                      shouldThrow)

import qualified Test.Concurrent as T
import qualified Test.Util       as T

import qualified Test.HUnit as HU

import qualified Control.Concurrent.Async as AS
import qualified Control.Exception        as E
import qualified Control.Monad            as M

import qualified GHC.Conc as CC

import qualified Data.List     as L
import qualified Data.Maybe    as MB
import           Data.Monoid   ((<>))
import qualified Data.Typeable as TP

import qualified System.Mem      as Mem
import qualified System.Mem.Weak as Weak
import qualified System.Timeout  as ST

import Prelude hiding (and, fail, or)

assertFailure :: String -> IO x
assertFailure desc = do
    M.void $ HU.assertFailure desc
    error "dummy"

failWithException :: (String -> String) -> IO x -> IO y
failWithException descf wait = proc `E.catch` returnError
  where
    returnError (E.SomeException err) = do
        assertFailure . descf $ "but aborted with: " <> show err
    proc = do
        M.void wait
        assertFailure $ descf "died with no exception (test maybe wrong)"

--------------------
-- assertions

assertTrue :: Bool -> String -> IO ()
assertTrue True  _    = return ()
assertTrue False desc = HU.assertFailure desc

assertEqual :: (Show x, Eq x) => x -> x -> IO ()
assertEqual expected actual = assertTrue (expected == actual) desc
  where
    desc = mkDesc 80 "expected" expectedStr <> "\n" <>
           mkDesc 80 "actual  " actualStr
    expectedStr = show expected
    actualStr   = show actual

assertNotEqual :: (Show x, Eq x) => x -> x -> IO ()
assertNotEqual expected actual = assertTrue (expected /= actual) desc
  where
    desc = mkDesc 80 "expected not to be" expectedStr <> "\n" <>
           mkDesc 80 "actual" actualStr
    expectedStr = show expected
    actualStr   = show actual

shouldBe :: (Show x, Eq x) => x -> x -> IO ()
shouldBe = flip assertEqual

shouldNotBe :: (Show x, Eq x) => x -> x -> IO ()
shouldNotBe = flip assertNotEqual

shouldSatisfy :: Show x => x -> (x -> Bool) -> IO ()
shouldSatisfy x f = assertTrue (f x) desc
  where
    desc = "assertion failed on: " ++ show x

shouldContain :: (Show x, Eq x) => [x] -> [x] -> IO ()
shouldContain actual expected = assertTrue result $ desc
  where
    result = expected `L.isInfixOf` actual
    desc = mkDesc 80 "expected to contain" expectedStr <> "\n" <>
           mkDesc 80 "but actual" actualStr
    expectedStr = show expected
    actualStr   = show actual

shouldReturn :: (Show x, Eq x) => IO x -> x -> IO ()
shouldReturn act expected = do
    ex <- E.try act
    case ex of
        Right x                    -> x `shouldBe` expected
        Left (E.SomeException err) -> HU.assertFailure $ desc err
  where
    desc err = mkDesc 80 "expected" expectedStr <> "\n" <>
               mkDesc 80 "but exception throwed" (show err)
    expectedStr = show expected

shouldThrow :: (Show x, E.Exception e) => IO x -> Selector e -> IO ()
shouldThrow act selector = do
    ex <- E.try act
    case ex of
        Right x                       -> HU.assertFailure $ descX x
        Left (err :: E.SomeException) -> case E.fromException err of
            Just e | selector e -> return ()
                   | otherwise  -> HU.assertFailure $ descF err
            Nothing             -> HU.assertFailure $ descE err
  where
    descX x = expectedStr <> mkDesc 80 "but returned" (show x)
    descF e = expectedStr <> mkDesc 80 "but selector failed for" (show e)
    descE e = expectedStr <> mkDesc 80 "but exception throwed" (show e)
    expectedStr = mkDesc 80 "expected to throw" exceptedType <> "\n"
    exceptedType = (show . TP.typeOf . instanceOf) selector
    instanceOf :: Selector a -> a
    instanceOf _ = error "dummy data of shouldThrow"

-- gc expectations

shouldBeGarbageCollected :: Weak.Weak x -> IO ()
shouldBeGarbageCollected weak = do
    Mem.performGC
    mref <- Weak.deRefWeak weak
    case mref of
        Just _  -> assertFailure "expected to be garbage collected but does not"
        Nothing -> return ()

shouldNotBeGarbageCollected :: Weak.Weak x -> IO ()
shouldNotBeGarbageCollected weak = do
    Mem.performGC
    mv <- Weak.deRefWeak weak
    case mv of
        Just _  -> return ()
        Nothing -> assertFailure "expected not to be garbage collected but garbage collected"

-- concurrent expectations

shouldBlock :: IO x -> Int -> IO (AS.Async x)
shouldBlock act time = do
    async <- AS.async act
    mstatus <- ST.timeout time $ T.waitStop async
    case mstatus of
        Just CC.ThreadFinished    -> HU.assertFailure $ desc "but finished"
        Just CC.ThreadDied        -> failWithException desc $ AS.wait async
        Just (CC.ThreadBlocked _) -> return ()
        _                         -> HU.assertFailure $ desc "but still running"
    return async
  where
    desc str = "expected to block in " <> show time <> " nanosec\n" <> str

shouldStillBlock :: AS.Async x -> Int -> IO ()
shouldStillBlock async time = M.void $ AS.wait async `shouldBlock` time

shouldAwakeFinish :: AS.Async x -> Int -> IO x
shouldAwakeFinish async time = do
    mstatus <- ST.timeout time $ T.waitFinish async
    status <- MB.fromMaybe (T.threadStatus async) $ return <$> mstatus
    let wait = AS.wait async
    case status of
        CC.ThreadFinished       -> return ()
        CC.ThreadDied           -> failWithException desc $ AS.wait async
        CC.ThreadBlocked reason ->
            HU.assertFailure . desc $ "but still blocked with " <> show reason
        CC.ThreadRunning        ->
            HU.assertFailure . desc $ "but still running"
    wait
  where
    desc str = "expected to awake and finish in " <>
               show time <> " nanosec\n" <> str

onlyOneShouldAwakeFinish :: [AS.Async x] -> Int -> IO (x, [AS.Async x])
onlyOneShouldAwakeFinish asyncs0 time = do
    mret <- ST.timeout time $ T.waitFinishAny asyncs0
    (idx, status) <- case mret of
        Just [ret] -> return ret
        Just _     -> assertFailure . desc $ "but not only one finished"
        Nothing    -> assertFailure . desc $ "but still running"
    let (async, asyncs1) = T.pickUp idx asyncs0
        wait = AS.wait async
    x <- case status of
        CC.ThreadFinished -> wait
        CC.ThreadDied     -> failWithException desc wait
        _                 -> assertFailure . desc $ "unknown case"
    return (x, asyncs1)
  where
    desc str = "expected to awake only one and finish in " <>
               show time <> " nanosec\n" <> str

shouldNotBlock :: IO x -> Int -> IO x
shouldNotBlock act time = do
    async <- AS.async act
    mstatus <- ST.timeout time $ T.waitStop async
    let wait = AS.wait async
    case mstatus of
        Just CC.ThreadFinished         -> return ()
        Just CC.ThreadDied             -> failWithException desc wait
        Just (CC.ThreadBlocked reason) -> do
            HU.assertFailure . desc $ "but blocked with " <> show reason
        _ -> HU.assertFailure $ desc "but still running"
    wait
  where
    desc str = "expected not to block and finish in " <>
               show time <> " nanosec\n" <> str

shouldFinish :: IO x -> Int -> IO x
shouldFinish act time = do
    async <- AS.async act
    mstatus <- ST.timeout time $ T.waitFinish async
    let wait = AS.wait async
    case mstatus of
        Just CC.ThreadFinished         -> return ()
        Just CC.ThreadDied             -> failWithException desc wait
        Just (CC.ThreadBlocked reason) -> do
            HU.assertFailure . desc $ "but blocked with " <> show reason
        _ -> HU.assertFailure $ desc "but still running"
    wait
  where
    desc str = "expected to finish in " <> show time <>
               " nanosec\n" <> str

---------------------------
--- util

mkDesc :: Int -> String -> String -> String
mkDesc len s1 s2
    | length s1 + length str > len = s1 <> ":\n\t" <> str
    | otherwise                    = s1 <> ": "    <> str
  where
    str = truncateString 512 s2

truncateString :: Int -> String -> String
truncateString len str
    | null rest = res
    | otherwise = res <> "..."
  where
    (res, rest) = L.splitAt len str

and :: [a -> Bool] -> (a -> Bool)
and (c:cs) a | c a       = and cs a
             | otherwise = False
and []     _             = True