{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Main ( main ) where
import Control.Monad.Trans.Resource (runResourceT)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.Function (on)
import Data.Functor.Compose (Compose(..))
import Data.Functor.Identity
import qualified Data.IORef as IOR
import qualified Data.List as L
import Data.String (fromString)
import Streaming (Of(..))
import qualified Streaming as SM
import qualified Streaming.ByteString as Q
import qualified Streaming.ByteString.Char8 as Q8
import qualified Streaming.ByteString.Internal as QI
import qualified Streaming.Prelude as S
import System.IO
import Test.SmallCheck.Series
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.SmallCheck
import Text.Printf (printf)
listOf :: Monad m => Series m a -> Series m [a]
listOf a = decDepth $
pure [] \/ ((:) <$> a <~> listOf a)
strSeries :: Monad m => Series m String
strSeries = listOf (generate $ const ['a', 'b', '\n'])
strSeriesCrlf :: Monad m => Series m String
strSeriesCrlf = L.concat <$> listOf (generate $ const ["a", "b", "\r\n"])
chunksSeries :: Monad m => Series m [String]
chunksSeries = listOf strSeries
nats :: Monad m => Series m Int
nats = generate $ \d -> [1..d]
fromChunks :: [String] -> Q8.ByteStream Identity ()
fromChunks = Q8.fromChunks . S.each . map B.pack
unix2dos :: String -> String
unix2dos = concatMap $ \c -> if c == '\n' then "\r\n" else [c]
unpackToString :: Q8.ByteStream Identity () -> String
unpackToString = runIdentity . S.toList_ . Q8.unpack
sLines :: Q8.ByteStream Identity () -> [B.ByteString]
sLines
= runIdentity
. S.toList_
. S.mapped Q8.toStrict
. Q8.lines
noNullChunks :: S.Stream (Q8.ByteStream Identity) Identity () -> Bool
noNullChunks = SM.streamFold (\() -> True) runIdentity go
where
go :: Q8.ByteStream Identity Bool -> Bool
go (QI.Empty b) = b
go (QI.Chunk bs sbs) = not (B.null bs) && go sbs
go (QI.Go (Identity sbs)) = go sbs
handleIsOpen :: Assertion
handleIsOpen = do
h <- openBinaryFile "tests/sample.txt" ReadMode
hIsOpen h >>= assertBool "Expected file handle to be open!"
l <- Q8.length_ $ Q8.hGetContents h
l @?= 73
hIsOpen h >>= assertBool "Still expected file handle to be open!"
groupCrash :: Assertion
groupCrash = do
a <- runResourceT . S.sum_ . SM.mapsM Q8.length . Q8.group $ Q8.readFile "tests/groupBy.txt"
a @?= 39925
b <- runResourceT . S.sum_ . SM.mapsM Q8.length . Q8.groupBy (\_ _ -> True) $ Q8.readFile "tests/groupBy.txt"
b @?= 39925
groupCharOrder :: Assertion
groupCharOrder = do
a <- S.toList_ . SM.mapsM Q8.toLazy $ Q8.group $ Q8.fromLazy "1234"
a @?= (["1", "2", "3", "4"] :: [BL.ByteString])
b <- S.toList_ . SM.mapsM Q8.toLazy $ Q8.group $ Q8.fromLazy "1122"
b @?= (["11", "22"] :: [BL.ByteString])
groupByCharOrder :: Assertion
groupByCharOrder = do
-- What about when everything fits into one group?
y <- S.toList_ . SM.mapsM Q8.toLazy $ Q8.groupBy (\_ _ -> True) $ Q8.fromLazy "abcd"
y @?= ["abcd"]
-- Prove it's not an issue with the Char-based wrapper.
z <- S.toList_ . SM.mapsM Q.toLazy $ Q.groupBy (\a b -> a - 1 == b) $ Q.fromLazy "98764321"
z @?= ["98", "76", "43", "21"]
-- Char-based variant
a <- S.toList_ . SM.mapsM Q8.toLazy $ Q8.groupBy (\a b -> succ a == b) $ Q8.fromLazy "12346789"
a @?= ["12", "34", "67", "89"]
b <- S.toList_ . SM.mapsM Q8.toLazy $ Q8.groupBy (on (==) (== '5')) $ Q8.fromLazy "5678"
b @?= ["5", "678"]
goodFindIndex :: Assertion
goodFindIndex = do
assertBool "Expected the length of the string" $ QI.findIndexOrEnd (const False) "1234" == 4
assertBool "Expected 0" $ QI.findIndexOrEnd (const True) "1234" == 0
firstI :: Assertion
firstI = do
l <- runResourceT
. S.length_ -- IO Int
. S.filter (== 'i')
. S.concat -- Stream (Of Char) IO ()
. S.mapped Q8.head -- Stream (Of (Maybe Char)) IO ()
. Q8.denull -- Stream (ByteStream IO) IO ()
. Q8.lines -- Stream (ByteStream IO) IO ()
$ Q8.readFile "tests/groupBy.txt" -- ByteStream IO ()
l @?= 57
readIntCases :: Assertion
readIntCases = do
let imax = maxBound :: Int
imin = minBound :: Int
imax1 = fromIntegral imax + 1 :: Integer
imax10 = fromIntegral imax + 10 :: Integer
imin1 = fromIntegral imin - 1 :: Integer
imin10 = fromIntegral imin - 10 :: Integer
smax = B.pack $ show imax
smin = B.pack $ show imin
smax1 = B.pack $ show imax1
smax10 = B.pack $ show imax10
smin1 = B.pack $ show imin1
smin10 = B.pack $ show imin10
maxfill = QI.defaultChunkSize
cnt <- IOR.newIORef 0 -- number of effects in stream.
-- Empty input
IOR.writeIORef cnt 1
res <- Q8.readInt
$ QI.Chunk ""
$ addEffect cnt
$ QI.Chunk ""
$ QI.Empty 0
check cnt res Nothing ("" :> 0)
-- Basic unsigned
IOR.writeIORef cnt 1
res <- Q8.readInt
$ QI.Chunk "123"
$ addEffect cnt
$ QI.Empty 1
check cnt res (Just 123) ("" :> 1)
-- Basic negative
IOR.writeIORef cnt 2
res <- Q8.readInt
$ QI.Chunk "-123"
$ addEffect cnt
$ QI.Chunk "456+789"
$ addEffect cnt
$ QI.Empty 2
check cnt res (Just (-123456)) ("+789" :> 2)
-- minBound with leading whitespace
IOR.writeIORef cnt 4
res <- readIntSkip
$ QI.Chunk " \t\n\v\f\r\xa0"
$ addEffect cnt
$ QI.Chunk (B.take 4 smin)
$ addEffect cnt
$ QI.Chunk (B.drop 4 smin)
$ addEffect cnt
$ QI.Chunk "-42"
$ addEffect cnt
$ QI.Empty 3
check cnt res (Just imin) ("-42" :> 3)
-- maxBound with leading whitespace
IOR.writeIORef cnt 4
res <- readIntSkip
$ QI.Chunk " \t\n\v\f\r\xa0"
$ addEffect cnt
$ QI.Chunk (B.take 4 smax)
$ addEffect cnt
$ QI.Chunk (B.drop 4 smax)
$ addEffect cnt
$ QI.Chunk "+42"
$ addEffect cnt
$ QI.Empty 4
check cnt res (Just imax) ("+42" :> 4)
-- minbound-1 with whitespace
IOR.writeIORef cnt 4
res <- readIntSkip
$ QI.Chunk " \t\n\v\f\r\xa0"
$ addEffect cnt
$ QI.Chunk (B.take 4 smin1)
$ addEffect cnt
$ QI.Chunk (B.drop 4 smin1)
$ addEffect cnt
$ QI.Chunk ""
$ addEffect cnt
$ QI.Empty 5
check cnt res Nothing (smin1 :> 5)
-- maxbound+1 with whitespace
IOR.writeIORef cnt 4
res <- readIntSkip
$ QI.Chunk " \t\n\v\f\r\xa0"
$ addEffect cnt
$ QI.Chunk (B.take 4 smax1)
$ addEffect cnt
$ QI.Chunk (B.drop 4 smax1)
$ addEffect cnt
$ QI.Chunk ""
$ addEffect cnt
$ QI.Empty 6
check cnt res Nothing (smax1 :> 6)
-- maxBound with explicit plus sign
IOR.writeIORef cnt 2
res <- readIntSkip
$ QI.Chunk " +"
$ addEffect cnt
$ QI.Chunk smax
$ QI.Chunk "tail"
$ addEffect cnt
$ QI.Empty 7
check cnt res (Just imax) ("tail" :> 7)
-- maxBound with almost excessive leading whitepace/zeros
IOR.writeIORef cnt 4
res <- readIntSkip
$ QI.Chunk (B.replicate (maxfill-1) ' ')
$ addEffect cnt
$ QI.Chunk " +"
$ QI.Chunk (B.replicate (maxfill-1) '0')
$ addEffect cnt
$ QI.Chunk ("000000" `B.append` smax)
$ addEffect cnt
$ QI.Chunk "tail"
$ addEffect cnt
$ QI.Empty 8
check cnt res (Just imax) ("tail" :> 8)
-- (Exactly) too much leading whitespace
IOR.writeIORef cnt 3
res <- readIntSkip
$ QI.Chunk (B.replicate maxfill ' ')
$ addEffect cnt
$ QI.Chunk " 1"
$ addEffect cnt
$ QI.Chunk ""
$ addEffect cnt
$ QI.Empty 9
check cnt res Nothing (" 1" :> 9)
-- (Exactly) too many leading zeros
IOR.writeIORef cnt 3
res <- readIntSkip
$ QI.Chunk (B.replicate maxfill '0')
$ addEffect cnt
$ QI.Chunk "1"
$ addEffect cnt
$ QI.Chunk ""
$ addEffect cnt
$ QI.Empty 10
check cnt res Nothing (B.replicate maxfill '0' `B.append` "1" :> 10)
-- Bare plus
IOR.writeIORef cnt 1
res <- readIntSkip
$ QI.Chunk " +"
$ addEffect cnt
$ QI.Chunk "foo"
$ QI.Empty 11
check cnt res Nothing ("+foo" :> 11)
-- Bare minus
IOR.writeIORef cnt 1
res <- readIntSkip
$ QI.Chunk " -"
$ addEffect cnt
$ QI.Chunk " bar"
$ QI.Empty 12
check cnt res Nothing ("- bar" :> 12)
--
IOR.writeIORef cnt 1
let msg = " nothing to see here move along "
res <- Q8.readInt
$ QI.Chunk msg
$ addEffect cnt
$ QI.Empty 13
check cnt res Nothing (msg :> 13)
-- whitespace-only input
IOR.writeIORef cnt 1
res <- readIntSkip
$ QI.Chunk " "
$ addEffect cnt
$ QI.Chunk "\n"
$ QI.Empty 14
check cnt res Nothing ("" :> 14)
-- maxbound+10 with whitespace
IOR.writeIORef cnt 4
res <- readIntSkip
$ QI.Chunk " \t\n\v\f\r\xa0"
$ addEffect cnt
$ QI.Chunk (B.take 4 smax10)
$ addEffect cnt
$ QI.Chunk (B.drop 4 smax10)
$ addEffect cnt
$ QI.Chunk ""
$ addEffect cnt
$ QI.Empty 15
check cnt res Nothing (smax10 :> 15)
-- minbound-10 with whitespace
IOR.writeIORef cnt 4
res <- readIntSkip
$ QI.Chunk " \t\n\v\f\r\xa0"
$ addEffect cnt
$ QI.Chunk (B.take 4 smin10)
$ addEffect cnt
$ QI.Chunk (B.drop 4 smin10)
$ addEffect cnt
$ QI.Chunk ""
$ addEffect cnt
$ QI.Empty 16
check cnt res Nothing (smin10 :> 16)
-- maxBound with cross-chunk overflow
IOR.writeIORef cnt 4
res <- readIntSkip
$ QI.Chunk " \t\n\v\f\r\xa0"
$ addEffect cnt
$ QI.Chunk (B.take 4 smax)
$ addEffect cnt
$ QI.Chunk (B.drop 4 smax)
$ addEffect cnt
$ QI.Chunk "00"
$ addEffect cnt
$ QI.Empty 17
check cnt res Nothing (smax `B.append` "00" :> 17)
where
-- Count down to zero from initial value
readIntSkip = Q8.readInt . Q8.skipSomeWS
addEffect cnt str = QI.Go $ const str <$> IOR.modifyIORef' cnt pred
check :: IOR.IORef Int
-> Compose (Of (Maybe Int)) (QI.ByteStream IO) Int
-> Maybe Int
-> Of B.ByteString Int
-> Assertion
check cnt (Compose (gotInt :> str)) wantInt (wantStr :> wantR ) = do
( gotStr :> gotR ) <- Q.toStrict str
c <- IOR.readIORef cnt
assertBool ("Correct readInt effects " ++ show wantR) $ c == 0
assertBool ("Correct readInt value " ++ show wantR ++ ": " ++ show gotInt) $ gotInt == wantInt
assertBool ("Correct readInt tail " ++ show wantR ++ ": " ++ show gotStr) $ gotStr == wantStr
assertBool ("Correct readInt residue " ++ show wantR ++ ": " ++ show gotR) $ gotR == wantR
main :: IO ()
main = defaultMain $ testGroup "Tests"
[ testGroup "Property Tests"
[ testProperty "Streaming.ByteString.Char8.lines is equivalent to Prelude.lines" $ over chunksSeries $ \chunks ->
-- This only makes sure that the streaming-bytestring lines function
-- matches the Prelude lines function when no carriage returns
-- are present. They are not expected to have the same behavior
-- with dos-style line termination.
let expected = lines $ concat chunks
got = (map B.unpack . sLines . fromChunks) chunks
in
if expected == got
then Right ("" :: String)
else Left (printf "Expected %s; got %s" (show expected) (show got) :: String)
, testProperty "lines recognizes DOS line endings" $ over strSeries $ \str ->
sLines (Q8.string $ unix2dos str) == sLines (Q8.string str)
, testProperty "lines recognizes DOS line endings with tiny chunks" $ over strSeries $ \str ->
sLines (mapM_ Q8.singleton $ unix2dos str) == sLines (mapM_ Q8.singleton str)
, testProperty "lineSplit does not create null chunks (LF)" $ over ((,) <$> nats <~> strSeries) $ \(n,str) ->
noNullChunks (Q8.lineSplit n (fromString str))
, testProperty "lineSplit does not create null chunks (CRLF)" $ over ((,) <$> nats <~> strSeriesCrlf) $ \(n,str) ->
noNullChunks (Q8.lineSplit n (fromString str))
, testProperty "concat after lineSplit round trips (LF)" $ over ((,) <$> nats <~> strSeries) $ \(n,str) ->
unpackToString (Q8.concat (Q8.lineSplit n (fromString str))) == str
, testProperty "concat after lineSplit round trips (CRLF)" $ over ((,) <$> nats <~> strSeriesCrlf) $ \(n,str) ->
unpackToString (Q8.concat (Q8.lineSplit n (fromString str))) == str
, testProperty "'for' is equivalent to the one in streaming" $ over chunksSeries $ \ss ->
let doubleStream b = S.each [b, b]
doubleByteStream b = Q.chunk b *> Q.chunk b
in unpackToString (Q.fromChunks (S.for (Q.toChunks (fromChunks ss)) doubleStream))
== unpackToString (Q.for (fromChunks ss) doubleByteStream)
]
, testGroup "Unit Tests"
[ testCase "hGetContents: Handle stays open" handleIsOpen
, testCase "group(By): Don't crash" groupCrash
, testCase "group: Char order" groupCharOrder
, testCase "groupBy: Char order" groupByCharOrder
, testCase "findIndexOrEnd" goodFindIndex
, testCase "Stream Interop" firstI
, testCase "readInt" readIntCases
]
]