{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TemplateHaskell #-}
-- | Fuzz testing for math programming backends.
module Math.Programming.Tests.Fuzz where
import Control.Monad.IO.Class
import Control.Monad.State
import Control.Monad.Writer
import qualified Data.Map as M
import qualified Data.Sequence as S
import qualified Data.Text as T
import Lens.Micro
import Lens.Micro.Mtl
import Lens.Micro.TH
import Math.Programming
import System.Random
import System.Random.Stateful
import Test.Hspec hiding (focus, pending)
import Test.Hspec.QuickCheck
import Test.QuickCheck
newtype Variable = Variable Int
deriving
( Show,
Eq,
Ord
)
newtype Constraint = Constraint Int
deriving
( Show,
Eq,
Ord
)
newtype Objective = Objective Int
deriving
( Show,
Eq,
Ord
)
-- | The types of actions we can perform on a linear program
data LPAction
= AddVariable Variable
| AddThenRemoveVariable Variable
| AddConstraint Constraint
| AddThenRemoveConstraint Constraint
| AddObjective Objective
| AddThenRemoveObjective Objective
| Optimize
deriving (Show)
newtype LPActions = LPActions [LPAction]
deriving (Show)
instance Arbitrary LPActions where
arbitrary = do
NonNegative actionCount <- arbitrary
actions <- forM [1 .. actionCount] $ \i -> do
d7 <- fmap (`mod` (7 :: Int)) arbitrary
pure $ case d7 of
0 -> AddVariable (Variable i)
1 -> AddThenRemoveVariable (Variable i)
2 -> AddConstraint (Constraint i)
3 -> AddThenRemoveConstraint (Constraint i)
4 -> AddObjective (Objective i)
5 -> AddThenRemoveObjective (Objective i)
_ -> Optimize
pure (LPActions actions)
data LPState v c o = LPState
{ _variables :: M.Map Variable v,
_variableNames :: M.Map Variable T.Text,
_constraints :: M.Map Constraint c,
_constraintNames :: M.Map Constraint T.Text,
_objectives :: M.Map Objective o,
_objectiveNames :: M.Map Objective T.Text,
_pending :: [LPAction],
_randomGen :: IOGenM StdGen
}
makeLenses ''LPState
initLPState :: Int -> [LPAction] -> IO (LPState v c o)
initLPState seed todo = do
g <- newIOGenM (mkStdGen seed)
pure
LPState
{ _variables = M.empty,
_variableNames = M.empty,
_constraints = M.empty,
_constraintNames = M.empty,
_objectives = M.empty,
_objectiveNames = M.empty,
_pending = todo,
_randomGen = g
}
type LPFuzz v c o m =
( MonadState (LPState v c o) m,
MonadLP v c o m,
MonadWriter (S.Seq String) m,
MonadIO m
)
evalPending :: LPFuzz v c o m => m ()
evalPending = do
todo <- use pending
case todo of
[] -> pure ()
(x : xs) -> do
assign pending xs
evalAction x
evalPending
evalAction :: LPFuzz v c o m => LPAction -> m ()
evalAction action = tell (S.singleton (show action)) >> evalAction' action
evalAction' :: LPFuzz v c o m => LPAction -> m ()
evalAction' (AddVariable k) = add k addVariable variables
evalAction' (AddThenRemoveVariable k) = addThenRemove k addVariable deleteVariable variables
evalAction' (AddConstraint k) = add k makeConstraint constraints
evalAction' (AddThenRemoveConstraint k) = addThenRemove k makeConstraint deleteConstraint constraints
evalAction' (AddObjective k) = add k makeObjective objectives
evalAction' (AddThenRemoveObjective k) = addThenRemove k makeObjective deleteObjective objectives
evalAction' Optimize = void optimizeLP
add :: (LPFuzz v c o m, Ord k) => k -> m a -> ASetter' (LPState v c o) (M.Map k a) -> m ()
add k create focus =
create >>= modifying focus . M.insert k
addThenRemove :: (LPFuzz v c o m, Ord k) => k -> (m a) -> (a -> m ()) -> Lens' (LPState v c o) (M.Map k a) -> m ()
addThenRemove k create destroy focus = do
collection <- use focus
case M.lookup k collection of
Just v -> destroy v >> modifying focus (M.delete k)
Nothing -> add k create focus
makeConstraint :: LPFuzz v c o m => m c
makeConstraint = do
lhs <- chooseExpr
rhs <- chooseExpr
op <- chooseInequality
lhs `op` rhs
chooseExpr :: LPFuzz v c o m => m (Expr v)
chooseExpr = do
g <- use randomGen
vs <- use variables
terms <- forM (M.elems vs) $ \v -> do
c <- liftIO (uniformRM (-1e10, 1e10) g)
pure (c *. v)
pure (esum terms)
chooseInequality :: LPFuzz v c o m => m (Expr v -> Expr v -> m c)
chooseInequality = do
g <- use randomGen
d3 <- liftIO (uniformRM (0 :: Int, 2) g)
case d3 of
0 -> pure (.<=.)
1 -> pure (.>=.)
_ -> pure (.==.)
makeObjective :: LPFuzz v c o m => m o
makeObjective = do
g <- use randomGen
minimizing <- liftIO (uniformM g)
if minimizing
then chooseExpr >>= minimize
else chooseExpr >>= maximize
makeFuzzTests ::
(MonadIO m, MonadLP v c o m) =>
-- | The runner for the API being tested.
(m (S.Seq String) -> IO ()) ->
-- | The resulting test suite.
Spec
makeFuzzTests runner =
describe "Fuzz testing" $ do
prop "finds no failures" $ \seed (LPActions todo) -> do
initState <- liftIO (initLPState seed todo)
runner . execWriterT
. flip evalStateT initState
$ evalPending