depq-0.4.0.0: test/Spec.hs
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.DEPQ as D
import Data.DEPQ (DEPQ)
import Test.QuickCheck (Arbitrary(..), Property, elements, property, (===))
import Test.Hspec (Spec, describe, hspec, it)
import Test.Hspec.QuickCheck (prop, modifyMaxSuccess)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import qualified Data.List as L
import Data.Maybe (fromJust)
import qualified Data.Foldable as F (Foldable(..))
main :: IO ()
main = hspec $ modifyMaxSuccess (const 1000) $ do
validitySpec
postConditionSpec
metamorphicSpec
modelBasedSpec
--------------------------------------------------------------------------------
-- Setting everything up
--------------------------------------------------------------------------------
-- We want our QuickCheck generators to generate edge cases which are:
-- 1. When we try to insert a new value into DEPQ
-- 2. When modification function tries to overwrite existing values
-- 3. When DEPQ contains multiple key-pair values in which the key is different
-- (e.g insert 4 A "Val" $ insert 5 A "Val2" empty)
-- To enable this, we use Priority type in which the range of values
-- are somewhat small and readable.
data Priority
= A
| B
| C
| D
| E
| F
| G
| H
deriving (Eq, Enum, Ord, Show)
instance Arbitrary Priority where
arbitrary = elements [A, B, C, D, E, F, G, H]
type Val = Int
type TestDEPQ = DEPQ Priority Val
--------------------------------------------------------------------------------
-- Validity testing
-- These sets of tests prove that given function will always return valid DEPQ
--------------------------------------------------------------------------------
validitySpec :: Spec
validitySpec = describe "Validity test" $ do
it "null" empty_is_null
prop "Generated DEPQ always valid" generator_valid
prop "insert" insert_valid
prop "fromList" fromList_valid
prop "deleteMin" deleteMin_valid
prop "deleteMax" deleteMax_valid
prop "popMin" popMin_valid
prop "popMax" popMax_valid
-- Test that DEPQ generated from QuickCheck are always valid
generator_valid :: TestDEPQ -> Property
generator_valid = property . D.valid
insert_valid :: Int -> Priority -> Val -> TestDEPQ -> Property
insert_valid key priority value depq =
property $ D.valid $ D.insert key priority value depq
fromList_valid :: [(Int, Priority, Val)] -> Property
fromList_valid = property . D.valid . D.fromList
deleteMin_valid :: TestDEPQ -> Property
deleteMin_valid = property . D.valid . D.deleteMin
deleteMax_valid :: TestDEPQ -> Property
deleteMax_valid = property . D.valid . D.deleteMax
popMin_valid :: TestDEPQ -> Property
popMin_valid depq =
property $ maybe True (\(_val, depq') -> D.valid depq') (D.popMin depq)
popMax_valid :: TestDEPQ -> Property
popMax_valid depq =
property $ maybe True (\(_val, depq') -> D.valid depq') (D.popMin depq)
empty_is_null :: Bool
empty_is_null = D.null D.empty
--------------------------------------------------------------------------------
-- Post condition
-- Here, we test that given function returns expected value
--------------------------------------------------------------------------------
postConditionSpec :: Spec
postConditionSpec = describe "Post condition" $ do
prop "findMin" findMin_post_condition
prop "findMax" findMax_post_condition
prop "size" size_post_condition
prop "topK" topK_post_condition
prop "bottomK" bottomK_post_condition
findMin_post_condition :: Int -> Priority -> Val -> Property
findMin_post_condition priority key val =
let depq = D.insert priority key val D.empty
in D.findMin depq === Just (priority, key, val)
findMax_post_condition :: Int -> Priority -> Val -> Property
findMax_post_condition priority key val =
let depq = D.insert priority key val D.empty
in D.findMax depq === Just (priority, key, val)
size_post_condition :: Int -> Priority -> Val -> TestDEPQ -> Property
size_post_condition key priority val depq =
property $ D.size (D.insert key priority val depq) >= D.size depq
topK_post_condition :: Int -> TestDEPQ -> Property
topK_post_condition num depq =
let s = D.topK num depq
in if num <= (D.size depq) && num > 0
then Seq.length s === num
else Seq.length s === 0
bottomK_post_condition :: Int -> TestDEPQ -> Property
bottomK_post_condition num depq =
let s = D.bottomK num depq
in if num <= (D.size depq) && num > 0
then Seq.length s === num
else Seq.length s === 0
popMin_post_condition :: TestDEPQ -> Property
popMin_post_condition depq =
let mPoped = D.popMin depq
in if D.size depq == 0
then mPoped === Nothing
else property $ maybe
False
(\(_val, depq') -> D.size depq' < D.size depq)
mPoped
popMax_post_condition :: TestDEPQ -> Property
popMax_post_condition depq =
let mPoped = D.popMax depq
in if D.size depq == 0
then mPoped === Nothing
else property $ maybe
False
(\(_val, depq') -> D.size depq' < D.size depq)
mPoped
--------------------------------------------------------------------------------
-- Metamorphic
-- Instead of testing single function, we're now going to test multiple set of
-- functions
--------------------------------------------------------------------------------
metamorphicSpec :: Spec
metamorphicSpec = describe "Metamorphic" $ do
prop "insert twice, findMax" insert_insert_findMax
prop "insert twice, findMin" insert_insert_findMin
-- Insert 2 key value pairs, test that findMax return expected value
insert_insert_findMax :: (Int, Priority, Val) -> (Int, Priority, Val) -> Property
insert_insert_findMax t1@(k1, p1, v1) t2@(k2, p2, v2)
| k1 == k2 = D.findMax depq === Just t2
| p1 < p2 = D.findMax depq === Just t2
| p1 > p2 = D.findMax depq === Just t1
| k1 > k2 = D.findMax depq === Just t2
| otherwise = D.findMax depq === Just t1
where
depq = D.insert k2 p2 v2 $ D.insert k1 p1 v1 D.empty
-- Insert 2 key value pairs, test that findMin return expected value
insert_insert_findMin :: (Int, Priority, Val) -> (Int, Priority, Val) -> Property
insert_insert_findMin t1@(k1, p1, v1) t2@(k2, p2, v2)
| k1 == k2 = D.findMin depq === Just t2
| p1 < p2 = D.findMin depq === Just t1
| p1 > p2 = D.findMin depq === Just t2
| k1 > k2 = D.findMin depq === Just t2
| otherwise = D.findMin depq === Just t1
where
depq = D.insert k2 p2 v2 $ D.insert k1 p1 v1 D.empty
--------------------------------------------------------------------------------
-- Model based testing
-- Here, we test against a data structure that would act as an model
-- (i.e. Something that would behave similar to DEPQ and is well tested)
--------------------------------------------------------------------------------
{-|
-------- fromDEPQ ---------
| DEPQ | ------ > | Model |
-------- ---------
| |
| f(DEPQ) | g(Model)
| |
-------------------------
| f(DEPQ) == g(Model) |
------------------------
-}
-- | Model of 'DEPQ'
type Model = Map Int (Priority, Val)
-- | /O(n)/ Convert a queue to a Sequence of (key, priority, value) tuples.
toList :: (Ord p, Ord a) => DEPQ p a -> [(Int, p, a)]
toList depq = L.sort $ F.toList $ D.topK (D.size depq) depq
fromDEPQ :: TestDEPQ -> Model
fromDEPQ depq = L.foldl' f mempty (toList depq)
where
f :: Model -> (Int, Priority, Val) -> Model
f accum (key, priority, val) = M.insert key (priority, val) accum
modelToList :: Model -> [(Int, Priority, Val)]
modelToList model = L.sort $ map convert $ M.toList model
-- Helful functions
-- This is helpful when converting
convert :: (Int, (Priority, Val)) -> (Int, Priority, Val)
convert (key, (priority, val)) = (key, priority, val)
-- Get minimal value of an given 'Priority'
getVal :: Priority -> Model -> Maybe (Int, (Priority, Val))
getVal priority model = M.lookupMin $ M.filter (\(p, _) -> p == priority) model
getPriority :: (a, Priority, b) -> Priority
getPriority (_, p, _) = p
-- Set of functions which should behave the same way as the one that we're testing
-- against
size :: Model -> Int
size model = length $ map convert $ M.toList model
insert :: Int -> Priority -> Val -> Model -> Model
insert key priority val model = M.insert key (priority, val) model
lookupMax :: Model -> Maybe (Int, Priority, Val)
lookupMax model =
if M.null model
then Nothing
else
let (_, maxPriority, _) = L.maximumBy
(\t1 t2 -> getPriority t1 `compare` getPriority t2) $ modelToList model
in convert <$> getVal maxPriority model
lookupMin :: Model -> Maybe (Int, Priority, Val)
lookupMin model =
if M.null model
then Nothing
else
let (_, minPriority, _) = L.minimumBy
(\t1 t2 -> getPriority t1 `compare` getPriority t2) (modelToList model)
in convert <$> getVal minPriority model
takeTop :: Int -> Model -> Seq (Int, Priority, Val)
takeTop num model =
let sortedList = L.sortBy
(\t1 t2 -> getPriority t2 `compare` getPriority t1) $ modelToList model
in Seq.fromList $ take num sortedList
takeBottom :: Int -> Model -> Seq (Int, Priority, Val)
takeBottom num model =
let sortedList =
L.sortBy (\t1 t2 -> getPriority t1 `compare` getPriority t2) $ modelToList model
in Seq.fromList $ take num sortedList
deleteMax :: Model -> Model
deleteMax model =
if M.null model
then model
-- It's safe to use partial functions since we know that the model has
-- at least 1 element
else
let (_, maxPriority, _) = L.maximumBy
(\t1 t2 -> getPriority t1 `compare` getPriority t2)
(modelToList model)
(k, _) = fromJust $ getVal maxPriority model
in M.delete k model
deleteMin :: Model -> Model
deleteMin model =
if M.null model
then model
else
let (_, minPriority, _) = L.minimumBy
(\t1 t2 -> getPriority t1 `compare` getPriority t2) $ modelToList model
(k, _) = fromJust $ getVal minPriority model
in M.delete k model
-- Tests
modelBasedSpec :: Spec
modelBasedSpec = describe "Model based testing" $ do
it "empty" nil_model
prop "size" size_model
prop "findMax" findMax_model
prop "findMin" findMin_model
prop "bottomK" bottomK_model
prop "topK" topK_model
prop "deleteMax" deleteMax_model
prop "deleteMin" deleteMin_model
-- Tests
nil_model :: Property
nil_model = (fromDEPQ D.empty) === (mempty :: Model)
size_model :: TestDEPQ -> Property
size_model depq = D.size depq === size (fromDEPQ depq)
findMax_model :: TestDEPQ -> Property
findMax_model depq = D.findMax depq === lookupMax (fromDEPQ depq)
findMin_model :: TestDEPQ -> Property
findMin_model depq = D.findMin depq === lookupMin (fromDEPQ depq)
bottomK_model :: Int -> TestDEPQ -> Property
bottomK_model num depq =
let s = D.bottomK num depq
in if num <= D.size depq
then s === takeBottom num (fromDEPQ depq)
else s === mempty
topK_model :: Int -> TestDEPQ -> Property
topK_model num depq =
let s = D.topK num depq
in if num <= D.size depq
then s === takeTop num (fromDEPQ depq)
else s === mempty
-- Here, we check that the Model and DEPQ contains exact same content after
-- applying an function
hasContent :: TestDEPQ -> Model -> Property
hasContent depq model = toList depq === (modelToList model)
insert_model :: Int -> Priority -> Val -> TestDEPQ -> Property
insert_model key priority val depq =
D.insert key priority val depq `hasContent` insert key priority val (fromDEPQ depq)
deleteMax_model :: TestDEPQ -> Property
deleteMax_model depq = D.deleteMax depq `hasContent` (deleteMax $ fromDEPQ depq)
deleteMin_model :: TestDEPQ -> Property
deleteMin_model depq = D.deleteMin depq `hasContent` (deleteMin $ fromDEPQ depq)