lattest-lib-0.1.0.0: test/Test/System/IO/Streams/Synchronized.hs
module Test.System.IO.Streams.Synchronized (
prop_consumeBufferedWith,
testConsumeBufferedWith_short,
testConsumeBufferedWith,
prop_jsonStream
)
where
import Control.Applicative((<|>))
import Control.Concurrent.STM.TQueue(TQueue, newTQueueIO, writeTQueue, readTQueue, isEmptyTQueue)
import Control.Concurrent(threadDelay)
import Control.Arrow(first)
import Data.Aeson(FromJSON,ToJSON,encode,fromJSON)
import qualified Data.Aeson as Aeson(Result(..))
import Data.Aeson.Parser(jsonNoDup)
import qualified Data.Attoparsec.ByteString.Char8 as Parse
import Data.Bits.Utils(c2w8)
import Data.ByteString(ByteString)
import Data.ByteString.Lazy(toStrict,snoc)
import qualified Data.ByteString as BS(splitAt,length)
import Data.Functor(void)
import qualified Data.List as List(splitAt)
import GHC.Conc(atomically, newTVar, readTVar, writeTVar)
import Lattest.Streams.Synchronized (tryReadIO, Streamed(Available), consumeBufferedWith, makeTInputStream)
import qualified Lattest.Streams.Synchronized as Streams (hasInput,read,map)
import Lattest.Streams.Synchronized.Attoparsec(parserToInputStream)
import Test.HUnit
import Test.QuickCheck
import Test.QuickCheck.Monadic (assertWith, monadicIO, run)
data WaitTime = NoWT | ShortWT | LongWT deriving (Eq, Show, Ord)
instance Arbitrary WaitTime where
arbitrary = elements [NoWT, ShortWT, LongWT]
shrink NoWT = []
shrink ShortWT = [NoWT]
shrink LongWT = [NoWT, ShortWT]
waitTimeToMillis :: Num a => WaitTime -> a
waitTimeToMillis NoWT = 0
waitTimeToMillis ShortWT = 2
waitTimeToMillis LongWT = 20
prop_consumeBufferedWith :: ([[(Int, WaitTime)]], WaitTime) -> Property
prop_consumeBufferedWith = prop_consumeBufferedWith'
prop_consumeBufferedWith' :: (Eq a, Show a) => ([[(a, WaitTime)]], WaitTime) -> Property
prop_consumeBufferedWith' (testInput', lastWaitTime) = withMaxSuccess 15 $ monadicIO $ do
let lastTestInput = (Nothing, lastWaitTime)
let testInput = mapToLast (fmap (fmap (first Just)) testInput') (\x -> x ++ [lastTestInput]) [lastTestInput]
queue <- run $ do
queue' <- newTQueueIO :: IO (TQueue [Maybe a])
sequence_ $ (atomically . writeTQueue queue' . fmap fst) <$> testInput -- write all lists to the queue, without wait times
return queue'
-- test the input stream created with consumeBufferedWith: for random lists of inputs, the stream should produce those original inputs, regardless of random waiting times in between reading
is <- run $ consumeBufferedWith (readTQueue queue) (not <$> isEmptyTQueue queue)
let (expected, waitTimes) = unzip $ concat testInput
assertBufferedIS is expected waitTimes
hasLast <- run $ atomically $ Streams.hasInput is
assertWith hasLast "mbt test expected input, received no input at end of test"
lastInput <- run $ atomically $ Streams.read is
assertWith (lastInput == Nothing) ("expected Nothing, received " ++ show lastInput ++ " at end of test")
where
assertBufferedIS is es wts = assertBufferedIS' is es wts es
assertBufferedIS' _ [] [] _ = return ()
assertBufferedIS' is (e:es) (wt:wts) oes = do
run $ threadDelay $ waitTimeToMillis wt * 1000
hasNext <- run $ atomically $ Streams.hasInput is
assertWith hasNext $ "mbt test expected " ++ show e ++ ", received no input in " ++ show oes
next <- run $ atomically $ Streams.read is
assertWith (e == next) ("expected " ++ show e ++ ", received " ++ show next ++ " in " ++ show oes)
assertBufferedIS' is es wts oes
mapToLast [] _ a = [a]
mapToLast [lastElem] f _ = [f lastElem]
mapToLast (a:as) f _ = (a:mapToLast as f (error "unused"))
testConsumeBufferedWith_short :: Test
testConsumeBufferedWith_short = TestCase $ do
queue <- newTQueueIO :: IO (TQueue [Maybe a])
is <- consumeBufferedWith (readTQueue queue) (not <$> isEmptyTQueue queue)
sequence_ $ (atomically . writeTQueue queue) <$> [[Just "a"],[Nothing]]
shouldBeA <- atomically $ Streams.read is
assertEqual ("testConsumeBufferedWith checking a") (Just "a") shouldBeA
shouldBeNothing <- atomically $ Streams.read is
assertEqual ("testConsumeBufferedWith checking Nothing") (Nothing) shouldBeNothing
testConsumeBufferedWith :: Test
testConsumeBufferedWith = TestCase $ do
queue <- newTQueueIO :: IO (TQueue [Maybe a])
is <- consumeBufferedWith (readTQueue queue) (not <$> isEmptyTQueue queue)
sequence_ $ (atomically . writeTQueue queue) <$> [[Just "a", Just "b"], [Just "c", Just "d"],[Just "e", Nothing]]
shouldBeA <- atomically $ Streams.read is
assertEqual ("testConsumeBufferedWith checking a") (Just "a") shouldBeA
shouldBeB <- atomically $ Streams.read is
assertEqual ("testConsumeBufferedWith checking b") (Just "b") shouldBeB
shouldBeC <- atomically $ Streams.read is
assertEqual ("testConsumeBufferedWith checking c") (Just "c") shouldBeC
shouldBeD <- atomically $ Streams.read is
assertEqual ("testConsumeBufferedWith checking d") (Just "d") shouldBeD
shouldBeE <- atomically $ Streams.read is
assertEqual ("testConsumeBufferedWith checking e") (Just "e") shouldBeE
shouldBeNothing <- atomically $ Streams.read is
assertEqual ("testConsumeBufferedWith checking Nothing") (Nothing) shouldBeNothing
prop_jsonStream :: (FromJSON a, ToJSON a, Show a, Eq a) => [(a,Bool,Bool)] -> Property
prop_jsonStream testInput = monadicIO $ do
-- first bool in list states that the bytes of the next elements should be appended to the bytes of this elements. If the element is the last one, then this boolean states that the second half is included, as opposed to being omitted entirely (if the element is streamed as half bytestring), or is ignored (if the element is streamed as single bytestring).
-- second bool in the list state that the bytes of the element should be streamed as two half bytestrings, as opposed to a single bytestring
-- final bool states whether the bytes of the last bool are incomplete
-- FIXME this does not test whether checkReadyness which results in False can later be succeeded by True, and by a succesfull read
(typedData, byteData) <- run $ createTestData testInput -- make an input stream from someData by serializing them as bytestrings reporesenting JSON, and potentially cutting off the bytestring half way
is <- run $ makeReader byteData
actionStream <- run $ parserToInputStream ((Parse.endOfInput >> pure Nothing) <|> (Just <$> (Parse.skipSpace *> jsonNoDup))) is
actionStream' <- run $ Streams.map (fromResult . fromJSON) actionStream
-- assert that the parsed objects are equal to the original objects, minus the cut-off
--return Discard
void $ checkObjs actionStream' typedData
where
checkObjs _ [] = return []
checkObjs actionStream [x] = checkObj actionStream x >>= \y -> return [y]
checkObjs actionStream (x:xs) = do
y <- checkObj actionStream x
ys <- checkObjs actionStream xs
return (y:ys)
checkObj actionStream obj = do
received <- run $ tryReadIO actionStream
--assert (isJust maybeReceived)
assertWith (Available obj == received) ("checkObjs expected " ++ show obj ++ ", received " ++ show received)
return $ fromAvailable received
fromAvailable (Available o) = o
createTestData [] = return ([], [])
createTestData [(a,app,True)] = do
let b = encode' a
(h1, h2) <- splitAtRandom b
return (if app then [a] else [], h1 : if app then [h2] else [])
createTestData [(a,_,False)] = return ([a],[encode' a])
createTestData ((a,app,half):rest) = do
(as,bytes) <- createTestData rest
let b = encode' a
bytes'' <- if half
then do
(h1, h2) <- splitAtRandom b
return $ if app
then let ([h2'],bytes') = List.splitAt 1 bytes
in h1:(h2 `mappend` h2'):bytes'
else h1:h2:bytes
else return $ if app
then let ([b'],bytes') = List.splitAt 1 bytes
in (b `mappend` b'):bytes'
else b:bytes
return (a:as,bytes'')
splitAtRandom :: ByteString -> IO (ByteString,ByteString)
splitAtRandom b =
if BS.length b > 1
then do
i <- generate $ chooseInt (1, BS.length b - 1)
return $ BS.splitAt i b
else discard
encode' = toStrict . (flip snoc $ c2w8 '\n') . encode
makeReader someData = do
tSomeData <- atomically $ newTVar someData
makeTInputStream (consume tSomeData) (return True)
where
consume tSomeData = do
someData' <- readTVar tSomeData
case someData' of
[] -> return Nothing
(x:xs) -> do
writeTVar tSomeData xs
return $ Just x
fromResult (Aeson.Success s) = s
fromResult (Aeson.Error e) = undefined e -- TODO handle error case