network-control-0.1.0: test/Network/Control/FlowSpec.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wno-orphans -Wno-incomplete-patterns #-}
module Network.Control.FlowSpec where
import Data.List
import Data.Text.Lazy (unpack)
import Network.Control
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
import Text.Pretty.Simple
-- types
data Op = Consume | Receive
deriving (Eq, Show, Bounded, Enum)
data OpWithResult = ConsumeWithResult (Maybe Int) | ReceiveWithResult Bool
deriving (Eq, Show)
data Step op = Step {stepOp :: op, stepArg :: Int}
deriving (Eq, Show)
data Trace = Trace
{ traceStart :: RxFlow
, traceSteps :: [(Int, Step OpWithResult, RxFlow)]
}
deriving (Eq, Show)
-- arbitrary instances
maxWindowSize :: Int
maxWindowSize = 200 -- (more realistic: 2_000_000)
minFrameSize :: Int
minFrameSize = -20
instance Arbitrary RxFlow where
arbitrary = newRxFlow <$> chooseInt (1, maxWindowSize)
instance Arbitrary Op where
arbitrary = elements [minBound ..]
instance Arbitrary Trace where
arbitrary = do
initialFlow <- arbitrary
len <- chooseInt (0, 500)
Trace initialFlow <$> runManySteps len 0 initialFlow
where
runManySteps :: Int -> Int -> RxFlow -> Gen [(Int, Step OpWithResult, RxFlow)]
runManySteps 0 _ _ = pure []
runManySteps len ix oldFlow | len > 0 = do
(newStep, newFlow) <- runStep oldFlow <$> genStep oldFlow
((ix, newStep, newFlow) :) <$> runManySteps (len - 1) (ix + 1) newFlow
-- Not sure frame size > window size or 0 or engative consumed or received bytes are
-- legal, but RxFlow works fine with them. :)
genStep :: RxFlow -> Gen (Step Op)
genStep oldFlow = oneof [mkConsume, mkReceive]
where
mkReceive =
Step Receive <$> chooseInt (minFrameSize, rxfBufSize oldFlow * 2)
mkConsume =
let recv = rxfReceived oldFlow
in if recv > 0
then Step Consume <$> chooseInt (minFrameSize, rxfReceived oldFlow)
else mkReceive
runStep :: RxFlow -> Step Op -> (Step OpWithResult, RxFlow)
runStep oldFlow = \case
Step Consume arg ->
let (newFlow, limitDelta) = maybeOpenRxWindow arg FCTWindowUpdate oldFlow
in (Step (ConsumeWithResult limitDelta) arg, newFlow)
Step Receive arg ->
let (newFlow, isAcceptable) = checkRxLimit arg oldFlow
in (Step (ReceiveWithResult isAcceptable) arg, newFlow)
shrink trace@(Trace initialFlow steps) =
trunc trace <> (Trace initialFlow <$> init (inits steps))
where
trunc :: Trace -> [Trace]
trunc (Trace _ stp) = case reverse stp of
[] -> []
[_] -> []
((ix, lastStep, lastFlow) : (_, _, initFlow) : _) -> [Trace initFlow [(ix, lastStep, lastFlow)]]
-- invariants
assertTrace :: Trace -> Property
assertTrace (Trace initialFlow steps) = assertStep initialFlow steps
assertStep :: RxFlow -> [(Int, Step OpWithResult, RxFlow)] -> Property
assertStep _ [] = property True
assertStep oldFlow ((ix, step, newFlow) : steps) =
(counterexample ("step #" <> show ix) check) .&. assertStep newFlow steps
where
check :: Expectation
check = case step of
Step (ConsumeWithResult limitDelta) arg -> do
newFlow
`shouldBe` RxFlow
{ rxfBufSize = rxfBufSize newFlow
, rxfConsumed = rxfConsumed oldFlow + arg
, rxfReceived = rxfReceived oldFlow
, rxfLimit =
if rxfLimit oldFlow - rxfReceived oldFlow < rxfBufSize oldFlow `div` 2
then rxfConsumed oldFlow + arg + rxfBufSize oldFlow
else rxfLimit oldFlow
}
limitDelta
`shouldBe` case rxfLimit newFlow - rxfLimit oldFlow of
0 -> Nothing
n -> Just n
Step (ReceiveWithResult isAcceptable) arg -> do
newFlow
`shouldBe` if isAcceptable
then
RxFlow
{ rxfBufSize = rxfBufSize newFlow
, rxfConsumed = rxfConsumed oldFlow
, rxfReceived = rxfReceived oldFlow + arg
, rxfLimit = rxfLimit oldFlow
}
else oldFlow
spec :: Spec
spec = do
focus . prop "state transition graph checks out" $
\trace -> counterexample (unpack $ pShowNoColor trace) (assertTrace trace)