packages feed

net-mqtt-0.8.6.0: test/ExpiringSpec.hs

{-# LANGUAGE BlockArguments             #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TupleSections              #-}

module ExpiringSpec where

import           Control.Lens
import           Data.Bool                (bool)
import           Data.Foldable            (foldl', toList, traverse_)
import           Data.Function            ((&))
import qualified Data.Map.Strict          as Map
import qualified Data.Map.Strict.Expiring as ExpiringMap
import           Data.Set                 (Set)
import qualified Data.Set                 as Set
import           GHC.Generics             (Generic)

import           Test.QuickCheck

data SomeKey = Key1 | Key2 | Key3 | Key4 | Key5
  deriving (Bounded, Enum, Eq, Ord, Show)

instance Arbitrary SomeKey where
  arbitrary = arbitraryBoundedEnum
  shrink = shrinkBoundedEnum

newtype GenOffset = GenOffset { getOffset :: Int }
  deriving (Eq, Ord)
  deriving newtype (Show, Num, Bounded)

instance Arbitrary GenOffset where
  arbitrary = GenOffset <$> choose (0, 5)
  shrink = fmap GenOffset . shrink . getOffset

data Mutation = Insert GenOffset SomeKey Int
              | Delete SomeKey
              | Update GenOffset SomeKey Int
              | UpdateNothing GenOffset SomeKey
              | NewGeneration GenOffset
  deriving (Show, Generic)

makePrisms ''Mutation

instance Arbitrary Mutation where
  arbitrary = oneof [Insert <$> arbitrary <*> arbitrary <*> arbitrary,
                     Delete <$> arbitrary,
                     Update <$> arbitrary <*> arbitrary <*> arbitrary,
                     UpdateNothing <$> arbitrary <*> arbitrary,
                     NewGeneration <$> arbitrary
                     ]
  shrink = genericShrink

allOpTypes :: [String]
allOpTypes = ["Insert", "Delete", "Update", "UpdateNothing", "NewGeneration"]

-- Verify that after a series of operations, the map and expiring map return the same values for the given keys.
prop_doesMapStuff :: [Mutation] -> [SomeKey] -> Property
prop_doesMapStuff ops lookups =
  coverTable "mutation types" ((,5) <$> allOpTypes) $ -- The test paths should hit every mutation type (5% min)
  tabulate "mutation types" (takeWhile (/= ' ') . show <$> ops) $ -- We can identify one by the first word in its constructor
  checkCoverage $
  ((`Map.lookup` massocs) <$> lookups) === ((`ExpiringMap.lookup` eassocs) <$> lookups)
  where
    massocs = degenerate $ foldl' applyOpM (0, mempty) ops
    eassocs = foldl' applyOpE (ExpiringMap.new 0) ops

    -- The emulation stores the generation along with the value, so when we're done, we fmap away the generation.
    degenerate :: (GenOffset, Map.Map SomeKey (GenOffset, Int)) -> Map.Map SomeKey Int
    degenerate = fmap snd . snd

    applyOpM (gen, m) = \case
      Insert g k v      -> (gen, Map.insert k (gen+g, v) m)
      Delete k          -> (gen, Map.delete k m)
      Update g k v      -> (gen, snd $ Map.updateLookupWithKey (\_ _ -> Just (gen+g, v)) k m)
      UpdateNothing _ k -> (gen, snd $ Map.updateLookupWithKey (\_ _ -> Nothing) k m)
      NewGeneration n   -> (gen + n, Map.filter ((>= gen + n) . fst) m)

    applyOpE m = \case
      Insert g k v      -> ExpiringMap.insert (gen + g) k v m
      Delete k          -> ExpiringMap.delete k m
      Update g k v      -> snd $ ExpiringMap.updateLookupWithKey (gen + g) (\k' _ -> bool Nothing (Just v) (k == k')) k m
      UpdateNothing g k -> snd $ ExpiringMap.updateLookupWithKey (gen + g) (\_ _ -> Nothing) k m
      NewGeneration g   -> ExpiringMap.newGen (gen + g) m
      where gen = ExpiringMap.generation m

prop_updateReturn :: Int -> Property
prop_updateReturn x = (Just plus2, Just plus2, Nothing, Just plus2) === (up1, ExpiringMap.lookup x m', up2, up3)
    where
        m = ExpiringMap.insert 0 x 0 $ ExpiringMap.new 0
        plus2 = x + 2
        (up1, m') = ExpiringMap.updateLookupWithKey 0 (\_ v -> Just (x + 2)) x m -- New value returns new value
        (up2, m'') = ExpiringMap.updateLookupWithKey 0 (\_ v -> Just (x + 3)) (x + 1) m' -- Missing returns nothing
        up3 = fst $ ExpiringMap.updateLookupWithKey 0 (\_ v -> Nothing) x m'' -- Nothing returns previous value

prop_cannotAcceptExpired :: Positive Int -> Positive Int -> Int -> Property
prop_cannotAcceptExpired (Positive lowGen) (Positive offset) k = ExpiringMap.inspect m === ExpiringMap.inspect m'
  where
    highGen = lowGen + offset
    m = ExpiringMap.new highGen :: ExpiringMap.Map Int Int Int
    m' = ExpiringMap.insert lowGen k k m

prop_cannotUpdateExpired :: Positive Int -> Positive Int -> Int -> Property
prop_cannotUpdateExpired (Positive lowGen) (Positive offset) k = mv === Nothing .&&. ExpiringMap.lookup k m' === Just True
  where
    highGen = lowGen + offset
    m = ExpiringMap.insert highGen k True $ ExpiringMap.new highGen
    (mv, m') = ExpiringMap.updateLookupWithKey lowGen (\_ _ -> Just False) k m

prop_assocs :: [SomeKey] -> Property
prop_assocs keys = ExpiringMap.assocs m === Map.assocs (Map.fromList $ zip keys keys)
  where
    m = foldr (\k -> ExpiringMap.insert 0 k k) (ExpiringMap.new 0) keys

prop_generation :: Int -> Int -> Property
prop_generation g1 g2 = ExpiringMap.inspect m === (0, max g1 g2, 0)
  where
    m :: ExpiringMap.Map Int Int Int
    m = ExpiringMap.newGen g2 $ ExpiringMap.new g1