packages feed

quickcheck-state-machine-0.7.0: test/RQlite.hs

{-# LANGUAGE DeriveAnyClass       #-}
{-# LANGUAGE DeriveGeneric        #-}
{-# LANGUAGE DeriveTraversable    #-}
{-# LANGUAGE DerivingStrategies   #-}
{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE FlexibleInstances    #-}
{-# LANGUAGE PackageImports       #-}
{-# LANGUAGE PolyKinds            #-}
{-# LANGUAGE RankNTypes           #-}
{-# LANGUAGE RecordWildCards      #-}
{-# LANGUAGE ScopedTypeVariables  #-}
{-# LANGUAGE StandaloneDeriving   #-}
{-# LANGUAGE UndecidableInstances #-}

module RQlite (
      Level (..)
    , prop_sequential_rqlite
    , prop_parallel_rqlite
    , prop_nparallel_rqlite
    , runCmds
    ) where

import           Control.Concurrent
                   (threadDelay)
import           Control.Concurrent.MVar
import           Control.Exception
                   (bracketOnError, try)
import           Control.Monad
                   (void, when)
import           Control.Monad.IO.Class
                   (liftIO)
import           Data.Aeson                         hiding
                   (Result)
import           Data.Foldable
import           Data.Functor.Classes
import           Data.Kind
import           Data.List
import           Data.Map
                   (Map)
import qualified Data.Map                           as M
import           Data.Maybe
import           Data.TreeDiff
import           GHC.Generics
import           Prelude
import           "hs-rqlite" Rqlite
import           "hs-rqlite" Rqlite.Status
import           System.Directory
import           System.IO
import           System.Process
import           System.Random
import           Test.QuickCheck                    hiding
                   (Result)
import           Test.QuickCheck.Monadic
import           Test.StateMachine
import           Test.StateMachine.DotDrawing
import           Test.StateMachine.Types
                   (History(..), ParallelCommandsF(..), interleavings)
import qualified Test.StateMachine.Types.Rank2      as Rank2
import           Test.StateMachine.Types.References

------------------------------------------------------------------------

newtype At t r = At { unAt :: t (NodeRef r) }
  deriving stock (Generic)

instance Show (t (NodeRef r)) => Show (At t r) where
  show = show . unAt

data Person = Person
  { name :: String
  , age  :: Int
  }
  deriving stock (Show, Read, Eq, Generic)

-- HTTP requests utilities

instance FromJSON Person where
    parseJSON j = do
        (n, a) <- parseJSON j
        return $ Person n a

createQ :: String
createQ =  "CREATE TABLE Person (name text, age Int)"

insertQuery :: String -> Person -> IO PostResult
insertQuery host Person{..} =
    postQuery True host $ "INSERT INTO Person(name, age) VALUES('" ++ name ++ "', " ++ show age ++ ")"

selectQuery :: Maybe Level -> String -> IO (GetResult Person)
selectQuery lvl host = do
    putStrLnM $ "querying " ++ host
    getQuery lvl host True "SELECT * FROM Person"

createQuery :: String -> IO PostResult
createQuery host = postQuery True host createQ

------------------------

-- Docker utilities

data Container = Container {
        counter  :: Int
      , cn       :: Int
      , nodeName :: String
      , cport    :: Int
} deriving stock (Generic)

instance Eq Container where
    n1 == n2 = counter n1 == counter n2

instance Show Container where
    show Container{..} = "Node@" ++ show cport

instance ToExpr Container where
    toExpr Container{..} = App ("Node@" ++ show cport) []

ignoreIOError :: IO a -> IO ()
ignoreIOError action = do
    (_ :: Either IOError a) <- try action
    return ()

stopContainer :: String -> IO ()
stopContainer ndName = do
    stdErr <- openFile "/dev/null" AppendMode
    -- stopping or removing a non-existing container should be a no-op
    ignoreIOError $ readCreateProcess (proc "docker"
      [ "stop"
      , ndName
      ]) {std_err = UseHandle stdErr}
      ""
    ignoreIOError $ readCreateProcess (proc "docker"
      [ "rm"
      , ndName
      ]) {std_err = UseHandle stdErr}
      ""

mkNodeTime :: Int -> Int -> Timeout -> Timeout -> Maybe Int -> IO Container
mkNodeTime c n timeout elTimeout mjoin = do
    (ndName, p) <- setupNode n timeout elTimeout mjoin
    return $ Container c n ndName p

stopNode :: Container -> IO ()
stopNode Container {..} = stopContainer nodeName

setupNode :: Int -> Timeout -> Timeout -> Maybe Int -> IO (String, Int)
setupNode n timeout elTimeout mjoin = do
  r :: Word <- randomIO
  let p   = leaderPort + 2 * n
      p'  = 4002 + 2 * n
      ip  = "172.19.0." ++ show (10 + n) -- http
      ip' = "172.18.0." ++ show (10 + n)
      mjoin' = fmap (\j -> "http://" ++ httpHost j) mjoin
      ndName = mkNodeName r
      jls = case mjoin' of
        Nothing   -> []
        Just join -> ["-join", join]
  stopContainer ndName

  void $ readProcess "docker"
        ([ "create"
        , "--net"
        , "rqlite-http"
        , "--ip"
        , ip
        , "-p"
        , show p ++ ":" ++ show p
        , "--name"
        , ndName
        , "rqlite/rqlite:4.5.0"
        , "-http-addr"
        , ip ++ ":" ++ show p
        , "-raft-addr"
        , ip' ++ ":" ++ show p'
        , "-raft-timeout"
        , show timeout
        , "-raft-election-timeout"
        , show elTimeout
        ] ++ jls) ""

  -- connect the container to the rqlite network.
  void $ readProcess "docker"
        [ "network"
        , "connect"
        , "--ip"
        , ip'
        , "rqlite-network"
        , ndName
        ] ""

  stdOut' <- openFile (testPath ++ "/out-" ++ show n) AppendMode
  stdErr' <- openFile (testPath ++ "/err-" ++ show n) AppendMode
  stdIn'  <- openFile "/dev/null" ReadMode
  -- we don't use readProcess here, because we don't want to block.
  void $ createProcess (proc "docker"
        [ "start"
        , "-a" -- this attaches the stdout and stderr to the one we specify.
        , ndName
        ]) {std_in = UseHandle stdIn', std_out = UseHandle stdOut', std_err = UseHandle stdErr'}

  putStrLnM "RAN"
  return (ndName, p)


pauseNode :: Container -> IO ()
pauseNode Container{..} =
    void $ readProcess "docker"
        [ "pause"
        , nodeName
        ] ""

unPauseNode :: Container -> IO ()
unPauseNode Container{..} =
    void $ readProcess "docker"
        [ "unpause"
        , nodeName
        ] ""

mkNodeName :: Word -> String
mkNodeName n = "rqlite-node-" ++ show n

disconnectNode :: String -> IO ()
disconnectNode ndName =
    void $ readProcess "docker"
        [ "network"
        , "disconnect"
        , "rqlite-network"
        , ndName
        ] ""

connectNode :: Container -> IO ()
connectNode Container {..} =
    void $ readProcess "docker"
        [ "network"
        , "connect"
        , "--ip"
        , "172.18.0." ++ show (10 + cn)
        , "rqlite-network"
        , nodeName
        ] ""

createRQNetworks :: IO ()
createRQNetworks = do
    -- this way we don't "polute" test output with messages (i.e. network
    -- may already be present).
    stdErr  <- openFile "/dev/null" AppendMode
    void $ ignoreIOError $ readCreateProcess (proc "docker"
        [ "network"
        , "create"
        , "--subnet"
        , "172.19.0.0/16"
        , "rqlite-http"]) {std_err = UseHandle stdErr}
        ""
    void $ ignoreIOError $ readCreateProcess (proc "docker"
        [ "network"
        , "create"
        , "--subnet"
        , "172.18.0.0/16"
        , "rqlite-network"]) {std_err = UseHandle stdErr}
        ""

-- retryUntilElections :: IO a -> IO a
-- retryUntilElections action = go 20
--     where
--     go n = do
--         res <- try action
--         let retry e = if n > 0 then do
--                 putStrLnM "LeadershipLost. Retrying"
--                 threadDelay 500000
--                 go (n-1)
--             else throwIO e
--         case res of
--             Right a -> return a
--             Left e@(LeadershipLost _) -> retry e
--             Left e@NotLeader -> retry e
--             Left e -> throwIO e


------------------------

sameElements :: Eq a => [a] -> [a] -> Bool
sameElements x y = null (x \\ y) && null (y \\ x)

type NodeRef =  Reference Container

data Model r = Model {
        dbModel :: DBModel
      , nodes   :: [(NodeRef r, Int)]
      } deriving stock (Generic, Show)

data DBModel = DBModel {
      persons   :: [Person]
    , nodeState :: Map Int NodeState
    , cutLines  :: [(Int, Int)]
    , cWhere    :: Int
    , cRef      :: Int
    } deriving stock (Generic, Show)

type Join = Maybe Int

data Timeout =
      Ms Int
    | Sec Int

instance Show Timeout where
    show (Ms n)  = show n ++ "ms"
    show (Sec n) = show n ++ "s"

data Cmd node =
      Spawn Int Timeout Timeout Join
    | ReSpawn Int Int Timeout Timeout Join
    | Disconnect node
    | Connect node
    | Stop node
    | Insert node Person
    | Get node (Maybe Level)
    | Pause node
    | UnPause node
    | Delay Int
    deriving stock (Generic, Show, Functor, Foldable, Traversable)

newtype Resp node = Resp (Either SomeError (Success node))
    deriving stock (Show, Eq, Functor, Foldable, Traversable, Generic)

newtype SomeError = SomeError String
    deriving stock (Show, Eq)

data Success node =
      Unit
    | Spawned node
    | Got [Person]
    deriving stock (Show, Functor, Foldable, Traversable, Generic)

deriving stock    instance Generic1          (At Cmd)
deriving anyclass instance Rank2.Functor     (At Cmd)
deriving anyclass instance Rank2.Foldable    (At Cmd)
deriving anyclass instance Rank2.Traversable (At Cmd)

deriving anyclass instance ToExpr DBModel
deriving anyclass instance ToExpr (Model Concrete)
deriving anyclass instance ToExpr Person

deriving stock    instance Generic1          (At Resp)
deriving anyclass instance Rank2.Functor     (At Resp)
deriving anyclass instance Rank2.Foldable    (At Resp)
deriving anyclass instance Rank2.Traversable (At Resp)

instance Eq node => Eq (Success node) where
    Unit == Unit          = True
    (Got ps) == (Got ps') = sameElements ps ps'
    Spawned n == Spawned n' = n == n'
    _ == _                = False

instance CommandNames (At Cmd) where
    cmdName (At Spawn {})      = "Spawn"
    cmdName (At ReSpawn {})    = "ReSpawn"
    cmdName (At Connect {})    = "Connect"
    cmdName (At Disconnect {}) = "Disconnect"
    cmdName (At Stop {})       = "Stop"
    cmdName (At Insert {})     = "Insert"
    cmdName (At Get {})        = "Get"
    cmdName (At Pause {})      = "Pause"
    cmdName (At UnPause {})    = "UnPause"
    cmdName (At Delay {})      = "Delay"

unitResp :: Resp node
unitResp = Resp (Right Unit)

data Event (r :: Type -> Type) = Event
  { eventBefore   :: Model  r
  , eventCmd      :: At Cmd r
  , eventAfter    :: Model  r
  , eventMockResp :: Resp Int
  }

lockstep :: forall  (r :: Type -> Type).
            (Eq1 r, Show1 r)
         => Model   r
         -> At Cmd  r
         -> At Resp r
         -> Event   r
lockstep model@Model {..} cmd (At resp) = Event
    { eventBefore   = model
    , eventCmd      = cmd
    , eventAfter    = model'
    , eventMockResp = mockResp
    }
  where
    (mockResp, dbModel') = step model cmd
    newNodes = zip (toList resp) (toList mockResp)
    model' = Model {
          dbModel = dbModel'
        , nodes   = nodes `union'` newNodes
        }

union' :: forall k v. (Eq k, Eq v, Show k, Show v)
              => [(k, v)] -- Mapping known to have duplicate keys
              -> [(k, v)] -- With potential duplicates
              -> [(k, v)]
union' acc []             = acc
union' acc ((k, v) : kvs) =
    case lookup k acc of
      Just v' | v /= v' -> error $ renderError v'
      _otherwise        -> union' ((k, v) : acc) kvs
  where
    renderError :: v -> String
    renderError v' = unwords [
          "Key"
        , show k
        , "with two different values"
        , show v
        , "and"
        , show v'
        ]

(!) :: Eq1 r => [(NodeRef r, v)] -> NodeRef r -> v
env ! r = case lookup r env of
            Just a  -> a
            Nothing -> error "(!): key not found"

step :: Eq1 r => Model r -> At Cmd r -> (Resp Int, DBModel)
step (Model m@DBModel{..} nodes) (At cmd) = case cmd of
    Insert _ p      -> (unitResp, m {persons = p : persons})
    Get _ _         -> (Resp $ Right $ Got $ persons, m)
    Spawn wh _ _ _  -> ( Resp $ Right $ Spawned cRef, m {
                          nodeState = M.insert cRef (Running wh) nodeState
                        , cWhere = wh + 1 -- while shrinking, node locations may not be consecutive.
                        , cRef = cRef + 1
                        })
    ReSpawn pRef wh _ _ _ ->
                        ( Resp $ Right $ Spawned cRef, m {
                          nodeState = M.union (M.fromList [(cRef, Running wh), (pRef, Done)]) nodeState
                        , cRef = cRef + 1
                        })
    Connect ref       -> (unitResp, m {
                          nodeState = M.update connectST (nodes ! ref) nodeState
                        })
    Disconnect ref    -> (unitResp, m {
                          nodeState = M.update disconnectST (nodes ! ref) nodeState
                        })
    Stop ref          -> (unitResp, m {
                          nodeState = M.update stopST (nodes ! ref) nodeState
                        })
    Pause ref         -> (unitResp, m {
                          nodeState = M.update pauseST (nodes ! ref) nodeState
                        })
    UnPause ref         -> (unitResp, m {
                          nodeState = M.update unPauseST (nodes ! ref) nodeState
                        })
    Delay _           -> (unitResp, m)

data NodeState
  = Running Int
  | Disconnected Int
  | Stopped Int
  | Paused Int
  | Done
  deriving stock (Show, Generic)
  deriving anyclass ToExpr

connectST :: NodeState -> Maybe NodeState
connectST (Disconnected wh) = Just $ Running wh
connectST st = error $ "is not dicsonnected but " ++ show st

disconnectST :: NodeState -> Maybe NodeState
disconnectST (Running wh) = Just $ Disconnected wh
disconnectST st           = error $ "is not running but " ++ show st

stopST :: NodeState -> Maybe NodeState
stopST (Running wh) = Just $ Stopped wh
stopST st           = error $ "is not running but " ++ show st

pauseST :: NodeState -> Maybe NodeState
pauseST (Running wh) = Just $ Paused wh
pauseST st           = error $ "is not running but " ++ show st

unPauseST :: NodeState -> Maybe NodeState
unPauseST (Paused wh) = Just $ Running wh
unPauseST st          = error $ "is not paused but " ++ show st

runsMp :: Int -> Map Int NodeState -> Bool
runsMp n mp = case M.lookup n mp of
    Just (Running _) -> True
    _                -> False

pausedMP :: Int -> Map Int NodeState -> Bool
pausedMP n mp = case M.lookup n mp of
    Just (Paused _) -> True
    _               -> False

disconnectedMP :: Int -> Map Int NodeState -> Bool
disconnectedMP n mp = case M.lookup n mp of
    Just (Disconnected _) -> True
    _                     -> False

stoppedMp :: Int -> Map Int NodeState -> Bool
stoppedMp n mp = case M.lookup n mp of
    Just (Stopped _) -> True
    _                -> False

getRunning :: Map Int NodeState -> [(Int, Int)]
getRunning = mapMaybe f . M.toList
    where
        f (a, Running wh) = Just (a, wh)
        f _               = Nothing

getNotRunning :: Map Int NodeState -> [Int]
getNotRunning = mapMaybe f . M.toList
    where
        f (a, Stopped _)      = Just a
        f (a, Done)           = Just a
        f (a, Disconnected _) = Just a
        f (a, Paused _)       = Just a
        f _                   = Nothing

isStopped :: NodeState -> Bool
isStopped (Stopped _) = True
isStopped _           = False

isDisconnected :: NodeState -> Bool
isDisconnected (Disconnected _) = True
isDisconnected _                = False

initModel :: Model r
initModel = Model (DBModel [] M.empty [] 0 0) []

transition :: forall r. (Eq1 r, Show1 r) => Model r -> At Cmd r -> At Resp r -> Model r
transition model cmd =  eventAfter . lockstep model cmd

precondition :: Model Symbolic -> At Cmd Symbolic -> Logic
precondition (Model DBModel{..} nodes) (At cmd) = case cmd of
    Spawn n _ _ j
               -> Boolean $ length (getRunning nodeState) <= 2
                         && (not (null (getRunning nodeState)) || n == 0)
                         && joinsRunning j nodeState
                         && n < 3
    ReSpawn prevRef _ _ _ j
               -> Boolean $ elem prevRef (snd <$> nodes)
                         && stoppedMp prevRef nodeState
                         && length (getRunning nodeState) <= 2
                         && joinsRunning j nodeState
    Stop n     -> Boolean $ elem n (fst <$> nodes)
                         && runsMp (nodes ! n) nodeState
                         && nodes ! n /= 0
                         && null (getNotRunning nodeState)
                         && length (getRunning nodeState) == 3
    Disconnect n
               -> Boolean $ elem n (fst <$> nodes)
                         && runsMp (nodes ! n) nodeState
                         && null (getNotRunning nodeState)
                         && nodes ! n /= 0
                         && length (getRunning nodeState) == 3
    Connect n  -> Boolean $ elem n (fst <$> nodes)
                         && disconnectedMP (nodes ! n) nodeState
    Insert n _ -> Boolean $ elem n (fst <$> nodes)
                         && runsMp (nodes ! n) nodeState
    Get n _    -> Boolean $ elem n (fst <$> nodes)
                         && runsMp (nodes ! n) nodeState
    Pause n    -> Boolean $ elem n (fst <$> nodes)
                         && runsMp (nodes ! n) nodeState
    UnPause n  -> Boolean $ elem n (fst <$> nodes)
                         && pausedMP (nodes ! n) nodeState
    Delay _    -> Top

postcondition :: Model Concrete -> At Cmd Concrete -> At Resp Concrete -> Logic
postcondition m@Model{..} cmd resp =
    toMock (eventAfter ev) resp .== eventMockResp ev
    where
        ev = lockstep m cmd resp

toMock :: Eq1 r => Model r -> At Resp r ->  Resp Int
toMock Model{..} (At r) = fmap
    (\n -> fromMaybe (error "could not find person ref") $ Prelude.lookup n nodes) r

shrinker :: Model Symbolic -> At Cmd Symbolic -> [At Cmd Symbolic]
shrinker _ _ = []

semantics :: MVar Int -> At Cmd Concrete -> IO (At Resp Concrete)
semantics counter (At cmd) = At . Resp <$> case cmd of
    Insert node p -> do
        _ <- insertQuery (httpHost $ cn $ concrete node) p
        return $ Right Unit
    Get node lvl -> do
        res <- selectQuery lvl $ httpHost $ cn $ concrete node
        return $ case res of
            GetResult p -> Right $ Got p
            GetError e  -> Left $ SomeError $ show e
    Spawn n t elt join -> do
            putStrLnM $ "Starting at " ++ show n ++ " joining " ++ show join
            c <- modifyMVar counter $ \m -> return (m + 1, m)
            bracketOnError (mkNodeTime c n t elt join)
                            stopNode
                            $ \rqNode -> do
                threadDelay 500000
                _ <- retryUntilAlive $ httpHost n
                _ <- createQuery $ httpHost n
                return $ Right $ Spawned $ reference rqNode
    ReSpawn n _ t elt join -> do
            putStrLnM $ "Restarting at " ++ show n ++ " joining " ++ show join
            c <- modifyMVar counter $ \m -> return (m + 1, m)
            bracketOnError (mkNodeTime c n t elt join)
                            stopNode
                            $ \rqNode -> do
                threadDelay 500000
                _ <- retryUntilAlive $ httpHost n
                return $ Right $ Spawned $ reference rqNode
    Stop node -> do
        putStrLnM $ "Stopping at " ++ show (cport $ concrete node)
        stopNode $ concrete node
        return $ Right Unit
    Disconnect node -> do
        disconnectNode $ nodeName $ concrete node
        return $ Right Unit
    Connect node -> do
        connectNode $ concrete node
        _ <- retryUntilAlive $ httpHost $ cn $ concrete node
        return $ Right Unit
    Pause node -> do
        pauseNode $ concrete node
        return $ Right Unit
    UnPause node -> do
        unPauseNode $ concrete node
        return $ Right Unit
    Delay n -> do
        threadDelay n
        return $ Right Unit

httpHost :: Int -> String
httpHost n = "172.19.0." ++ show (10 + n) ++ ":" ++ show (leaderPort + 2 * n)

mock :: Model Symbolic -> At Cmd Symbolic -> GenSym (At Resp Symbolic)
mock (Model DBModel{..} _) (At cmd) = At <$> case cmd of
    Spawn {}      -> Resp . Right . Spawned <$> genSym
    ReSpawn {}    -> Resp . Right . Spawned <$> genSym
    Stop {}       -> return unitResp
    Disconnect {} -> return unitResp
    Connect {}    -> return unitResp
    Insert {}     -> return unitResp
    Get {}        -> return $ Resp $ Right $ Got persons
    Pause {}      -> return unitResp
    UnPause {}    -> return unitResp
    Delay {}      -> return unitResp

sm :: MVar Int -> Maybe Level -> StateMachine Model (At Cmd) IO (At Resp)
sm counter lvl = StateMachine initModel transition precondition postcondition
    Nothing (generatorImpl lvl) shrinker (semantics counter) mock garbageCollect

prop_sequential_rqlite :: Maybe Level -> Property
prop_sequential_rqlite lvl =
    forAllCommands (sm undefined lvl) (Just 7) $ \cmds -> checkCommandNames cmds $ monadicIO $ do
        liftIO $ do
            createRQNetworks
            removePathForcibly testPath
            createDirectory testPath
            threadDelay 10000
        c <- liftIO $ newMVar 0
        (hist, _, res) <- runCommands (sm c lvl) cmds
        prettyCommands (sm undefined lvl) hist $ res === Ok

prop_parallel_rqlite :: Maybe Level -> Property
prop_parallel_rqlite lvl =
    forAllParallelCommands (sm undefined lvl) (Just 7) $ \cmds -> checkCommandNamesParallel cmds $ monadicIO $ do
        liftIO $ do
            createRQNetworks
            removePathForcibly testPath
            createDirectory testPath
            threadDelay 10000
        c <- liftIO $ newMVar 0
        prettyParallelCommandsWithOpts cmds (Just $ GraphOptions "rqlite-test-output.png" Png)
                =<< runParallelCommandsNTimes 2 (sm c lvl) cmds

prop_nparallel_rqlite :: Int -> Maybe Level -> Property
prop_nparallel_rqlite np lvl =
    forAllNParallelCommands (sm undefined lvl) np $ \cmds ->
      monadicIO $ do
        liftIO $ do
            removePathForcibly testPath
            createDirectory testPath
            threadDelay 10000
        c <- liftIO $ newMVar 0
        prettyNParallelCommandsWithOpts cmds (Just $ GraphOptions "rqlite-test-output.png" Png)
                =<< runNParallelCommandsNTimes 1 (sm c lvl) cmds


runCmds :: ParallelCommandsF [] (At Cmd) (At Resp) -> Property
runCmds cmds = withMaxSuccess 1 $ noShrinking $ monadicIO $ do
    liftIO $ do
            removePathForcibly testPath
            createDirectory testPath
    c <- liftIO $ newMVar 0
    ls <- runNParallelCommandsNTimes 1 (sm c $ Just Weak) cmds
    prettyNParallelCommandsWithOpts cmds (Just $ GraphOptions "rqlite-test-output.png" Png) ls
    liftIO $ print $ fst $ head ls
    liftIO $ print $ interleavings $ unHistory $ fst $ head ls

garbageCollect :: Model Concrete -> IO ()
garbageCollect (Model _ nodes) =
    forM_ nodes $ \(Reference (Concrete node), p) -> do
        putStrLnM $ "stopping " ++ show p
        stopNode node

generatorImpl :: Maybe Level -> Model Symbolic -> Maybe (Gen (At Cmd Symbolic))
generatorImpl _ (Model _ [])    = Just $ return $ At initCmd
generatorImpl lvl (Model DBModel{..} nodes) = Just $ At <$> do
    node <- fst <$> elements nodes
    let
        reconnect :: (Int, Gen (Cmd (NodeRef Symbolic)))
        reconnect = case find (isDisconnected . snd) (M.toList nodeState) of
            (Just (_, Disconnected _)) ->
                (100, return $ Connect node)
            _                       -> (0, undefined)
    frequency
        [ (5, Insert node <$> arbitrary)
        , (5, return $ Get node lvl)
        , (if cWhere <= 3 then 20 else 0, return $ mkSpawn Nothing cWhere $ joinNode False nodeState)
        , (if cWhere == 3 then 20 else 0, return $ Stop node)
        , (if cWhere == 3 then 20 else 0, return $ Disconnect node)
        , respawn
        , reconnect]
        where

        respawn :: (Int, Gen (Cmd (NodeRef Symbolic)))
        respawn = case find (isStopped . snd) (M.toList nodeState) of
            (Just (ref, Stopped n)) ->
                -- Sometimes, when a node respawns the rqlite files are not found
                -- and the test fails. I'm not sure why this happens.
                -- Until this is properly fixed, we disable this command.
                (0, return $ mkSpawn (Just ref) n $ joinNode True nodeState)
            _                       -> (0, undefined)

joinNode :: Bool -> Map Int NodeState -> Maybe Int
joinNode avoid0 ndState =
    case snd <$> getRunning ndState of
        [] -> Nothing
        rs -> Just $ case (elem 0 rs, delete 0 rs, avoid0) of
            (False, _, _)     -> head rs
            (True, _, False)  -> 0
            (True, rs', True) -> head rs'

joinsRunning :: Maybe Int -> Map Int NodeState -> Bool
joinsRunning Nothing _   = True
joinsRunning (Just n) mp = elem n $ snd <$> getRunning mp

initCmd :: Cmd node
initCmd = Spawn 0 (Sec 1) (Sec 1) Nothing

mkSpawn :: Maybe Int -> Int -> Maybe Int -> Cmd node
mkSpawn respawm n =
    (case respawm of
        Nothing  -> Spawn
        Just ref -> ReSpawn ref) n (Sec 1) (Sec 1)

testPath :: String
testPath = "rqlite-test-output"

names :: [String]
names = ["John", "Stevan", "Kostas", "Curry", "Robert"]

leaderPort :: Int
leaderPort = 4001

instance Arbitrary Person where
    arbitrary = do
        n <- elements names
        a <- choose (0, 100)
        return $ Person n a

putStrLnM :: String -> IO ()
putStrLnM str = when isDebugging $ putStrLn str

isDebugging :: Bool
isDebugging = False