colonnade-old/siphon/test/Test.hs
2018-10-23 16:39:24 -04:00

341 lines
9.8 KiB
Haskell

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
module Main (main) where
import Test.QuickCheck (Gen, Arbitrary(..), choose, elements, Property)
import Test.QuickCheck.Property (Result, succeeded, exception)
import Test.HUnit (Assertion,(@?=))
import Test.Framework (defaultMain, testGroup, Test)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.Framework.Providers.HUnit (testCase)
import Data.ByteString (ByteString)
import Data.Text (Text)
import GHC.Generics (Generic)
import Data.Either.Combinators
import Siphon.Types
import Data.Functor.Identity
import Data.Functor.Contravariant (contramap)
import Data.Functor.Contravariant.Divisible (divided,conquered)
import Colonnade (headed,headless,Colonnade,Headed,Headless)
import Data.Profunctor (lmap)
import Streaming (Stream,Of(..))
import Control.Exception
import Debug.Trace
import Data.Word (Word8)
import Data.Char (ord)
import qualified Data.Text as Text
import qualified Data.ByteString.Builder as Builder
import qualified Data.ByteString.Lazy as LByteString
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Char8 as BC8
import qualified Data.ByteString as B
import qualified Colonnade as Colonnade
import qualified Siphon as S
import qualified Streaming.Prelude as SMP
import qualified Data.Text.Lazy as LText
import qualified Data.Text.Lazy.Builder as TBuilder
import qualified Data.Text.Lazy.Builder.Int as TBuilder
main :: IO ()
main = defaultMain tests
tests :: [Test]
tests =
[ testGroup "ByteString encode/decode"
[ testCase "Headed Encoding (int,char,bool)"
$ runTestScenario [(4,intToWord8 (ord 'c'),False)]
S.encodeCsvStreamUtf8
encodingB
$ ByteString.concat
[ "number,letter,boolean\n"
, "4,c,false\n"
]
, testCase "Headed Encoding (int,char,bool) monoidal building"
$ runTestScenario [(4,'c',False)]
S.encodeCsvStreamUtf8
encodingC
$ ByteString.concat
[ "boolean,letter\n"
, "false,c\n"
]
, testCase "Headed Encoding (escaped characters)"
$ runTestScenario ["bob","there,be,commas","the \" quote"]
S.encodeCsvStreamUtf8
encodingF
$ ByteString.concat
[ "name\n"
, "bob\n"
, "\"there,be,commas\"\n"
, "\"the \"\" quote\"\n"
]
, testCase "Headed Decoding (int,char,bool)"
$ ( runIdentity . SMP.toList )
( S.decodeCsvUtf8 decodingB
( mapM_ (SMP.yield . BC8.singleton) $ concat
[ "number,letter,boolean\n"
, "244,z,true\n"
]
)
) @?= ([(244,intToWord8 (ord 'z'),True)] :> Nothing)
, testCase "Headed Decoding (geolite)"
$ ( runIdentity . SMP.toList )
( S.decodeCsvUtf8 decodingGeolite
( SMP.yield $ BC8.pack $ concat
[ "network,autonomous_system_number,autonomous_system_organization\n"
, "1,z,y\n"
]
)
) @?= ([(1,intToWord8 (ord 'z'),intToWord8 (ord 'y'))] :> Nothing)
, testCase "Headed Decoding (escaped characters, one big chunk)"
$ ( runIdentity . SMP.toList )
( S.decodeCsvUtf8 decodingF
( SMP.yield $ BC8.pack $ concat
[ "name\n"
, "drew\n"
, "\"martin, drew\"\n"
]
)
) @?= (["drew","martin, drew"] :> Nothing)
, testCase "Headed Decoding (escaped characters, character per chunk)"
$ ( runIdentity . SMP.toList )
( S.decodeCsvUtf8 decodingF
( mapM_ (SMP.yield . BC8.singleton) $ concat
[ "name\n"
, "drew\n"
, "\"martin, drew\"\n"
]
)
) @?= (["drew","martin, drew"] :> Nothing)
, testProperty "Headed Isomorphism (int,char,bool)"
$ propIsoStream BC8.unpack
(S.decodeCsvUtf8 decodingB)
(S.encodeCsvStreamUtf8 encodingB)
]
]
intToWord8 :: Int -> Word8
intToWord8 = fromIntegral
data Foo = FooA | FooB | FooC
deriving (Generic,Eq,Ord,Show,Read,Bounded,Enum)
instance Arbitrary Foo where
arbitrary = elements [minBound..maxBound]
fooToString :: Foo -> String
fooToString x = case x of
FooA -> "Simple"
FooB -> "With,Escaped\nChars"
FooC -> "More\"Escaped,\"\"Chars"
encodeFoo :: (String -> c) -> Foo -> c
encodeFoo f = f . fooToString
fooFromString :: String -> Maybe Foo
fooFromString x = case x of
"Simple" -> Just FooA
"With,Escaped\nChars" -> Just FooB
"More\"Escaped,\"\"Chars" -> Just FooC
_ -> Nothing
decodeFoo :: (c -> String) -> c -> Maybe Foo
decodeFoo f = fooFromString . f
decodingA :: Siphon Headless ByteString (Int,Char,Bool)
decodingA = (,,)
<$> S.headless dbInt
<*> S.headless dbChar
<*> S.headless dbBool
decodingB :: Siphon Headed ByteString (Int,Word8,Bool)
decodingB = (,,)
<$> S.headed "number" dbInt
<*> S.headed "letter" dbWord8
<*> S.headed "boolean" dbBool
decodingF :: Siphon Headed ByteString ByteString
decodingF = S.headed "name" Just
decodingGeolite :: Siphon Headed ByteString (Int,Word8,Word8)
decodingGeolite = (,,)
<$> S.headed "network" dbInt
<*> S.headed "autonomous_system_number" dbWord8
<*> S.headed "autonomous_system_organization" dbWord8
encodingA :: Colonnade Headless (Int,Char,Bool) ByteString
encodingA = mconcat
[ lmap fst3 (headless ebInt)
, lmap snd3 (headless ebChar)
, lmap thd3 (headless ebBool)
]
encodingW :: Colonnade Headless (Int,Char,Bool) Text
encodingW = mconcat
[ lmap fst3 (headless etInt)
, lmap snd3 (headless etChar)
, lmap thd3 (headless etBool)
]
encodingY :: Colonnade Headless (Foo,Foo,Foo) Text
encodingY = mconcat
[ lmap fst3 (headless $ encodeFoo Text.pack)
, lmap snd3 (headless $ encodeFoo Text.pack)
, lmap thd3 (headless $ encodeFoo Text.pack)
]
decodingY :: Siphon Headless Text (Foo,Foo,Foo)
decodingY = (,,)
<$> S.headless (decodeFoo Text.unpack)
<*> S.headless (decodeFoo Text.unpack)
<*> S.headless (decodeFoo Text.unpack)
encodingF :: Colonnade Headed ByteString ByteString
encodingF = headed "name" id
encodingB :: Colonnade Headed (Int,Word8,Bool) ByteString
encodingB = mconcat
[ lmap fst3 (headed "number" ebInt)
, lmap snd3 (headed "letter" ebWord8)
, lmap thd3 (headed "boolean" ebBool)
]
encodingC :: Colonnade Headed (Int,Char,Bool) ByteString
encodingC = mconcat
[ lmap thd3 $ headed "boolean" ebBool
, lmap snd3 $ headed "letter" ebChar
]
tripleToPairs :: (a,b,c) -> (a,(b,(c,())))
tripleToPairs (a,b,c) = (a,(b,(c,())))
propIsoStream :: (Eq a, Show a, Monoid c)
=> (c -> String)
-> (Stream (Of c) Identity () -> Stream (Of a) Identity (Maybe SiphonError))
-> (Stream (Of a) Identity () -> Stream (Of c) Identity ())
-> [a]
-> Result
propIsoStream toStr decode encode as =
let asNew :> m = runIdentity $ SMP.toList $ decode $ encode $ SMP.each as
in case m of
Nothing -> if as == asNew
then succeeded
else exception ("expected " ++ show as ++ " but got " ++ show asNew) myException
Just err ->
let csv = toStr $ mconcat $ runIdentity $ SMP.toList_ $ encode $ SMP.each as
in exception (S.humanizeSiphonError err ++ "\nGenerated CSV\n" ++ csv) myException
data MyException = MyException
deriving (Show,Read,Eq)
instance Exception MyException
myException :: SomeException
myException = SomeException MyException
runTestScenario :: (Monoid c, Eq c, Show c, Eq a, Show a)
=> [a]
-> (Colonnade f a c -> Stream (Of a) Identity () -> Stream (Of c) Identity ())
-> Colonnade f a c
-> c
-> Assertion
runTestScenario as p e c =
( mconcat (runIdentity (SMP.toList_ (p e (mapM_ SMP.yield as))))
) @?= c
-- runCustomTestScenario :: (Monoid c, Eq c, Show c)
-- => Siphon c
-- -> (Siphon c -> Colonnade f a c -> Pipe a c Identity ())
-- -> Colonnade f a c
-- -> a
-- -> c
-- -> Assertion
-- runCustomTestScenario s p e a c =
-- ( mconcat $ Pipes.toList $
-- Pipes.yield a >-> p s e
-- ) @?= c
-- testEncodingA :: Assertion
-- testEncodingA = runTestScenario encodingA "4,c,false\n"
propEncodeDecodeIso :: Eq a => (a -> b) -> (b -> Maybe a) -> a -> Bool
propEncodeDecodeIso f g a = g (f a) == Just a
propMatching :: Eq b => (a -> b) -> (a -> b) -> a -> Bool
propMatching f g a = f a == g a
-- | Take the first item out of a 3 element tuple
fst3 :: (a,b,c) -> a
fst3 (a,b,c) = a
-- | Take the second item out of a 3 element tuple
snd3 :: (a,b,c) -> b
snd3 (a,b,c) = b
-- | Take the third item out of a 3 element tuple
thd3 :: (a,b,c) -> c
thd3 (a,b,c) = c
dbChar :: ByteString -> Maybe Char
dbChar b = case BC8.length b of
1 -> Just (BC8.head b)
_ -> Nothing
dbWord8 :: ByteString -> Maybe Word8
dbWord8 b = case B.length b of
1 -> Just (B.head b)
_ -> Nothing
dbInt :: ByteString -> Maybe Int
dbInt b = do
(a,bsRem) <- BC8.readInt b
if ByteString.null bsRem
then Just a
else Nothing
dbBool :: ByteString -> Maybe Bool
dbBool b
| b == BC8.pack "true" = Just True
| b == BC8.pack "false" = Just False
| otherwise = Nothing
ebChar :: Char -> ByteString
ebChar = BC8.singleton
ebWord8 :: Word8 -> ByteString
ebWord8 = B.singleton
ebInt :: Int -> ByteString
ebInt = LByteString.toStrict
. Builder.toLazyByteString
. Builder.intDec
ebBool :: Bool -> ByteString
ebBool x = case x of
True -> BC8.pack "true"
False -> BC8.pack "false"
ebByteString :: ByteString -> ByteString
ebByteString = id
etChar :: Char -> Text
etChar = Text.singleton
etInt :: Int -> Text
etInt = LText.toStrict
. TBuilder.toLazyText
. TBuilder.decimal
etText :: Text -> Text
etText = id
etBool :: Bool -> Text
etBool x = case x of
True -> Text.pack "true"
False -> Text.pack "false"