packages feed

dfinity-radix-tree-0.4.0: test/Properties.hs

{-# LANGUAGE LambdaCase #-}

{- |
This module contains a (very simple) property based test for the RadixTree data
structure.

It uses QuickCheck to generate a list of operations (insert, delete, lookup),
executes them both against
 * "Data.Map" as a known-to-be-good specification and
 * RadixTree (with "Data.Map" as the backend)
records the resulting values in each case and compares these lists.

This is very rudimentary. In particular, 'generateOps' can be further tweaked
to produce “interesting” sequences -- there is little (but some) value in
deleting mostly entries that are not there.

You can sample what 'generateOps' produces by opening this file in ghci
(e.g. cabal new-repl property-tests) and then running @sample generateOps@.

-}
module Properties (tests) where

import qualified Data.Map as M
import Data.ByteString.Char8 (ByteString, pack)
import Data.ByteString.Short (fromShort)
import Test.QuickCheck
import Test.Tasty
import Test.Tasty.QuickCheck
import Data.List
import Data.Bifunctor
import Control.Monad.State.Strict

import Network.DFINITY.RadixTree

{-
In this module we test the RadixTree module against a pure implementation of
a map (Data.Map). We generate random sequences of Insert, Delete and Lookup
calls, run them in both implementations, and compare the results.
 -}

data Op
   = Insert ByteString ByteString
   | Delete ByteString
   | Lookup ByteString
  deriving Show

generateOps :: Gen [Op]
generateOps = do
    -- Create a small, non-zero number of keys to consider
    len <- getSmall . getPositive <$> arbitrary
    keys <- vectorOf len arbBS
    let pickKey = elements keys
    listOf $ frequency
        [ (1, Insert <$> pickKey <*> arbBS )
        , (1, Delete <$> pickKey)
        , (2, Lookup <$> pickKey)
        ]

arbBS :: Gen ByteString
arbBS = pack <$> arbitrary


runPure :: [Op] -> [Maybe ByteString]
runPure ops = snd $ mapAccumL go M.empty ops
  where
    go m (Insert k v) = (M.insert k v m, Nothing)
    go m (Delete k)   = (M.delete k m,   Nothing)
    go m (Lookup k)   = (m,              M.lookup k m)


type M = State (M.Map ByteString ByteString)

runRadix :: [Op] -> [Maybe ByteString]
runRadix ops0 = evalState (initTree >>= go ops0) M.empty
  where
    initTree :: M (RadixTree ())
    initTree = createRadixTree 262144 2028 Nothing ()

    go :: [Op] -> RadixTree () -> M [Maybe ByteString]
    go []                 _ = return []
    go (Insert k v : ops) t = do
        t' <- insertRadixTree k v t
        (Nothing :) <$> go ops t'
    go (Delete k   : ops) t = do
        t' <- deleteRadixTree k t
        (Nothing :) <$> go ops t'
    go (Lookup k   : ops) t =
        lookupRadixTree k t >>= \case
            Nothing     -> (Nothing :) <$> go ops t
            Just (v,t') -> (Just v :)  <$> go ops t'

prop_lookup :: Property
prop_lookup  = forAll generateOps $ \ops ->
    runPure ops === runRadix ops

{-
We want to test if two different ways of generating the same map yield the same
state root. We first generate one final state (a Data.Map). Then we generate two
different set of operations that should yield that state. Then we run them and
compare the output.
-}

genMap :: Gen (M.Map ByteString ByteString)
genMap = M.fromList . map (bimap pack pack) . M.toList <$> arbitrary

genOpsForMap :: M.Map ByteString ByteString -> Gen [Op]
genOpsForMap m = do
    inserts <- forM (M.toList m) $ \(k,v) -> do
        ops <- genOpsForKey k
        return $ ops ++ [ Insert k v ]
    -- Extra keys (which we delete as the last action)
    extra_keys <- listOf $ arbBS `suchThat` (`M.notMember` m)
    deletes <- forM extra_keys $ \k -> do
        ops <- genOpsForKey k
        return $ ops ++ [ Delete k ]
    interleaves (inserts ++ deletes)
  where
    genOpsForKey k = listOf (oneof [ Insert k <$> arbBS , pure (Delete k) ])


-- TODO
interleaves :: [[a]] -> Gen [a]
interleaves xss = frequency'
    [ (length (x:xs), (x:) <$> interleaves (xs : rest))
    | ((x:xs):rest) <- rots xss ]
 where
   frequency' [] = return []
   frequency' xs = frequency xs
   rots xs = tail $ zipWith (++) (tails xs) (inits xs)


type TreeGenOps = [ (ByteString, [Maybe ByteString]) ]

prop_stateRoot :: Property
prop_stateRoot = forAll genMap $ \m ->
    forAll (genOpsForMap m) $ \ops1 ->
    forAll (genOpsForMap m) $ \ops2 ->
    run ops1 === run ops2
  where
    run :: [Op] -> ByteString
    run ops0 = evalState (initTree >>= go ops0) M.empty
      where
        initTree :: M (RadixTree ())
        initTree = createRadixTree 262144 2028 Nothing ()

        go :: [Op] -> RadixTree () -> M ByteString
        go []                 t = fromShort . fst <$> merkleizeRadixTree t
        go (Insert k v : ops) t = insertRadixTree k v t >>= go ops
        go (Delete k   : ops) t = deleteRadixTree k t   >>= go ops
        go (Lookup _   : _  ) _ = error "no lookup in this test please"

tests :: TestTree
tests = testGroup "Property tests"
    [ testProperty "lookup" prop_lookup
    ]