packages feed

hans-2.4.0.0: tests/Tcp/Packet.hs

{-# OPTIONS_GHC -fno-warn-orphans #-}

module Tcp.Packet where

import Utils

import Hans.Message.Tcp
    (TcpHeader(..),emptyTcpHeader,TcpPort(..),TcpOption(..),SackBlock(..)
    ,getTcpHeader,putTcpHeader,getTcpOption,putTcpOption,tcpOptionsLength)

import Control.Applicative (pure,(<*>),(<$>))
import Control.Monad (replicateM)
import Data.Serialize (runGet,runPut)
import Data.Word (Word8)
import System.Random ()
import Test.Framework (Test,testGroup)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.QuickCheck
    (Gen,choose,arbitrarySizedIntegral,sized,arbitrary,listOf,oneof,forAll
    ,suchThat)
import Test.QuickCheck.Property (Result(..),failed,succeeded)
import qualified Data.ByteString as S
import qualified Data.Sequence as Seq


-- Utilities -------------------------------------------------------------------

-- | Generate a strict bytestring of nonzero length, that falls within the MSS
-- off the HaNS tcp layer.
arbitraryPayload :: Gen S.ByteString
arbitraryPayload  = do
  len   <- choose (1,10)
  S.pack `fmap` replicateM len arbitrarySizedIntegral

-- | Generate the starting point of a data packet stream.
arbitraryDataPacket :: Gen (TcpHeader,S.ByteString)
arbitraryDataPacket  = do
  body <- arbitraryPayload
  sn   <- arbitrarySizedIntegral
  an   <- arbitrarySizedIntegral
  let hdr = emptyTcpHeader
        { tcpSeqNum = sn
        , tcpAckNum = an
        , tcpAck    = True
        }
  return (hdr,body)

-- | Increment the sequence number, and generate some new data.
nextDataPacket :: (TcpHeader,S.ByteString) -> Gen (TcpHeader,S.ByteString)
nextDataPacket (hdr,body) = do
  body' <- arbitraryPayload
  let hdr' = hdr { tcpSeqNum = tcpSeqNum hdr + fromIntegral (S.length body) }
  return (hdr',body')

-- | Generate a sequence of headers and packet payloads.  This will yield a
-- non-empty stream whose length depends on the size parameter.
packetStream :: Gen (Seq.Seq (TcpHeader,S.ByteString))
packetStream  = do
  pkt@(hdr,body) <- arbitraryDataPacket
  sized (loop Seq.empty pkt)
  where
  loop segs pkt len
    | len == 0  = return (segs Seq.|> pkt)
    | otherwise = do
      pkt' <- nextDataPacket pkt
      loop (segs Seq.|> pkt) pkt' (len - 1)


arbitraryTcpPort :: Gen TcpPort
arbitraryTcpPort  = TcpPort `fmap` arbitrarySizedIntegral

-- | Generate a completely arbitrary header.  This may be a semantically
-- incorrect header, and is mainly useful for testing the parser.
arbitraryTcpHeader :: Gen TcpHeader
arbitraryTcpHeader  = TcpHeader
                  <$> arbitraryTcpPort
                  <*> arbitraryTcpPort
                  <*> arbitrarySizedIntegral
                  <*> arbitrarySizedIntegral
                  <*> arbitrary
                  <*> arbitrary
                  <*> arbitrary
                  <*> arbitrary
                  <*> arbitrary
                  <*> arbitrary
                  <*> arbitrary
                  <*> arbitrary
                  <*> arbitrarySizedIntegral
                  <*> pure 0 -- checksum
                  <*> pure 0 -- urgent pointer
                  <*> arbitraryTcpOptions

arbitraryTcpOptions :: Gen [TcpOption]
arbitraryTcpOptions  = listOf arbitraryTcpOption `suchThat` (not . tooBig)
  where
  tooBig opts = fst (tcpOptionsLength opts) > 10

arbitraryTcpOption :: Gen TcpOption
arbitraryTcpOption  = oneof
  [ pure OptNoOption
  , OptMaxSegmentSize <$> arbitrarySizedIntegral
  , OptWindowScaling  <$> arbitrarySizedIntegral
  , pure OptSackPermitted
  , do len <- choose (0,10)
       OptSack <$> replicateM len arbitrarySackBlock
  , OptTimestamp      <$> arbitrarySizedIntegral
                      <*> arbitrarySizedIntegral
  , do code  <- unusedTcpOptionNumber
       len   <- choose (0, 20)
       bytes <- replicateM (fromIntegral len) arbitrarySizedIntegral
       return (OptUnknown code (len + 2) (S.pack bytes))
  ]

-- | Choose an unused tcp option number.
unusedTcpOptionNumber :: Gen Word8
unusedTcpOptionNumber  =
  arbitrarySizedIntegral `suchThat` (not . (`elem` avoid))
  where
  -- it would be nice if this could could be generated from TcpOptionTag
  avoid = [0, 1, 2, 3, 4, 5, 8]

arbitrarySackBlock :: Gen SackBlock
arbitrarySackBlock  = SackBlock
                  <$> arbitrarySizedIntegral
                  <*> arbitrarySizedIntegral


-- Properties ------------------------------------------------------------------

tcpPacketTests :: Test
tcpPacketTests  = testGroup "packet parsing"
  [ testProperty "prop_headerRoundTrip" prop_headerRoundTrip
  , testProperty "prop_optionRoundTrip" prop_optionRoundTrip
  ]

prop_headerRoundTrip =
  roundTrip arbitraryTcpHeader (fst <$> getTcpHeader) putTcpHeader

prop_optionRoundTrip =
  roundTrip arbitraryTcpOption getTcpOption putTcpOption