asif-5.0.1: test/Arbor/File/Format/Asif/WriteSpec.hs
module Arbor.File.Format.Asif.WriteSpec where
import Arbor.File.Format.Asif.Data.Ip
import Arbor.File.Format.Asif.Format
import Arbor.File.Format.Asif.Segment
import Arbor.File.Format.Asif.Write
import Control.Lens
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class
import Control.Monad.Trans.Resource
import Data.Int
import Data.List (elemIndex, nub)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Semigroup ((<>))
import Data.Word
import HaskellWorks.Data.Network.Ip.Validity (Canonical)
import System.IO.Temp (openBinaryTempFile)
import HaskellWorks.Hspec.Hedgehog
import Hedgehog
import Test.Hspec
import qualified Data.Attoparsec.ByteString as AP
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.Encoding as T
import qualified Data.Thyme.Clock.POSIX as TY
import qualified Data.Thyme.Time.Core as TY
import qualified HaskellWorks.Data.Network.Ip.Ipv4 as IP4
import qualified HaskellWorks.Data.Network.Ip.Ipv6 as IP6
import qualified Hedgehog.Gen as G
import qualified Hedgehog.Range as R
import qualified System.IO as IO
import Debug.Trace
{-# ANN module ("HLint: ignore Redundant do" :: String) #-}
spec :: Spec
spec = describe "Arbor.File.Format.Asif.Write" $ do
it "should write file and read it back again" $ require $ property $ do
tplList <- forAll $ G.list (R.linear 0 50) genTriple
let fld = int64Segment (\(a,_,_) -> a) "first"
<> word16Segment (\(_,a,_) -> a) "second"
<> nullTerminatedStringSegment (\(_,_,a) -> T.toStrict a) "third"
(_, f, h) <- openBinaryTempFile Nothing "wtite-asif-test.asif"
_ <- writeAsif h "wxyz" Nothing fld tplList
liftIO $ IO.hSeek h IO.AbsoluteSeek 0
lbs <- liftIO $ LBS.hGetContents h
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, l1,l2,l3] <- forAll $ pure (segmentValues <$> segments)
((\(a,_,_) -> SInt64 a) <$> tplList) === l1
((\(_,a,_) -> SWord16 a) <$> tplList) === l2
((\(_,_,a) -> SString . T.encodeUtf8 $ a) <$> tplList) === l3
it "should write several segments, then read them back in" $ require $ property $ do
tplList <- forAll $ G.list (R.linear 0 50) genTriple
let f1 = int64Segment (\(a,_,_) -> a) "first"
let f2 = word16Segment (\(_,a,_) -> a) "second"
let f3 = nullTerminatedStringSegment (\(_,_,a) -> T.toStrict a) "third"
let fld = f1 <> f2 <> f3
lbs <- asifContent "wxyz" Nothing fld tplList
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, l1,l2,l3] <- forAll $ pure (segmentValues <$> segments)
((\(a,_,_) -> SInt64 a) <$> tplList) === l1
((\(_,a,_) -> SWord16 a) <$> tplList) === l2
((\(_,_,a) -> SString . T.encodeUtf8 $ a) <$> tplList) === l3
-----
it "should write out and read back in a bool segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) G.bool
lbs <- asifContent "wxyz" Nothing (boolSegment id "bool") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SBool <$> lst) === seg
it "should write out and read back in a word8 segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) (G.word8 R.linearBounded)
lbs <- asifContent "wxyz" Nothing (word8Segment id "word8") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SWord8 <$> lst) === seg
it "should write out and read back in a word16 segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) (G.word16 R.linearBounded)
lbs <- asifContent "wxyz" Nothing (word16Segment id "word16") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SWord16 <$> lst) === seg
it "should write out and read back in a word32 segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) (G.word32 R.linearBounded)
lbs <- asifContent "wxyz" Nothing (word32Segment id "word32") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SWord32 <$> lst) === seg
it "should write out and read back in a word64 segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) (G.word64 R.linearBounded)
lbs <- asifContent "wxyz" Nothing (word64Segment id "word64") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SWord64 <$> lst) === seg
-----
it "should write out and read back in a int8 segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) (G.int8 R.linearBounded)
lbs <- asifContent "wxyz" Nothing (int8Segment id "int8") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SInt8 <$> lst) === seg
it "should write out and read back in a int16 segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) (G.int16 R.linearBounded)
lbs <- asifContent "wxyz" Nothing (int16Segment id "int16") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SInt16 <$> lst) === seg
it "should write out and read back in a int32 segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) (G.int32 R.linearBounded)
lbs <- asifContent "wxyz" Nothing (int32Segment id "int32") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SInt32 <$> lst) === seg
it "should write out and read back in a int64 segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) (G.int64 R.linearBounded)
lbs <- asifContent "wxyz" Nothing (int64Segment id "int64") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SInt64 <$> lst) === seg
-----
it "should write out and read back in a ipv4 segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) genIpv4
lbs <- asifContent "wxyz" Nothing (ipv4Segment id "ipv4") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SIpv4 <$> lst) === seg
it "should write out and read back in a ipv6 segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) genIpv6
lbs <- asifContent "wxyz" Nothing (ipv6Segment id "ipv6") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SIpv6 <$> lst) === seg
it "should write out and read back in a ipv4 block segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) genIpv4Block
lbs <- asifContent "wxyz" Nothing (ipv4BlockSegment id "ipv4Block") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SIpv4Block <$> lst) === seg
it "should write out and read back in a ipv6 block segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) genIpv6Block
lbs <- asifContent "wxyz" Nothing (ipv6BlockSegment id "ipv6Block") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SIpv6Block <$> lst) === seg
-----
it "should write out and read back in a time segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) (TY.posixSecondsToUTCTime . TY.fromMicroseconds <$> G.int64 R.linearBounded)
lbs <- asifContent "wxyz" Nothing (utcTimeMicrosSegment id "time") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(STime <$> lst) === seg
-----
it "should write out and read back in a char segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) G.ascii
lbs <- asifContent "wxyz" Nothing (asciiSegment id "char") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SChar <$> lst) === seg
it "should write out and read back in a null-terminated string segment" $ require $ property $ do
lst <- forAll $ G.list (R.linear 0 50) genNonNullText
lbs <- asifContent "wxyz" Nothing (nullTerminatedStringSegment id "nullterminatedstring") (T.toStrict <$> lst)
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
(SString . T.encodeUtf8 <$> lst) === seg
it "should write out and read back in a text segment" $ require $ property $ do
-- Explaination of this test:
-- We can't deliniate 'breaks' in Text when we read it back in,
-- so regardless of how many we write out, when we read it back we only
-- get a single Text value.
lst <- forAll $ G.list (R.linear 1 50) (G.text (R.linear 0 50) G.unicode)
lbs <- asifContent "wxyz" Nothing (textSegment id "text") lst
let Right segments = extractSegments (AP.string "seg:wxyz") lbs
[names, times, types, seg] <- forAll $ pure (segmentValues <$> segments)
[SText . T.encodeUtf8 . T.concat $ T.fromStrict <$> lst] === seg
it "should write and read back a lookup segment" $ require $ property $ do
let pairGen = (,) <$> genIpv4 <*> G.maybe (G.element ["US", "KZ", "AU"])
pairs <- forAll $ G.list (R.linear 1 100) pairGen
let countriesFold = nullTerminatedStringSegment id "countries"
let countriesLookup = word16LookupSegment "ip-to-countries" snd countriesFold
let resFold = ipv4Segment fst "ip" <> countriesLookup
content <- asifContent "ipct" Nothing resFold pairs
let Right segments = extractSegments (AP.string "seg:ipct") content
[names, times, types, ips, lkp, dict] <- forAll $ pure (segmentValues <$> segments)
let expectedVals = pairs ^.. each . _2 . _Just & nub
let expectedLkpVals = pairs ^.. each . _2 . to (\x -> x >>= flip elemIndex expectedVals) . to (fromMaybe maxBound)
expectedDict <- forAll . pure $
expectedVals <&> (SString . T.encodeUtf8 . T.fromStrict)
expectedLkp <- forAll . pure $
expectedLkpVals <&> (SWord16 . fromIntegral)
dict === expectedDict
lkp === expectedLkp
genTriple :: MonadGen m => m (Int64, Word16, T.Text)
genTriple
= (,,)
<$> G.int64 R.linearBounded
<*> G.word16 R.linearBounded
<*> genNonNullText
genIpv4 :: MonadGen m => m IP4.IpAddress
genIpv4 = word32ToIpv4 <$> G.word32 R.linearBounded
genIpv6 :: MonadGen m => m IP6.IpAddress
genIpv6 = word32x4ToIpv6 <$> gen4
where
gen4 :: MonadGen m => m (Word32, Word32, Word32, Word32)
gen4 =
let g = G.word32 R.linearBounded
in (,,,) <$> g <*> g <*> g <*> g
genIpv4Block :: MonadGen m => m (IP4.IpBlock Canonical)
genIpv4Block = do
ip <- genIpv4
mask <- IP4.IpNetMask <$> G.word8 (R.linear 0 32)
pure . IP4.canonicaliseIpBlock $ IP4.IpBlock ip mask
genIpv6Block :: MonadGen m => m (IP6.IpBlock Canonical)
genIpv6Block = do
ip <- genIpv6
mask <- IP6.IpNetMask <$> G.word8 (R.linear 0 128)
pure . IP6.canonicaliseIpBlock $ IP6.IpBlock ip mask
genNonNullText :: MonadGen m => m T.Text
genNonNullText =
T.fromStrict <$> G.text (R.linear 0 32) (G.filter (/= toEnum 0) G.unicode)
instance MonadResource m => MonadResource (PropertyT m) where
liftResourceT = lift . liftResourceT
-- I have no idea how this doesn't exist, but whatever. ¯\_(ツ)_/¯
instance MonadResource IO where
liftResourceT = runResourceT