This commit is contained in:
Julian K. Arni 2016-04-23 17:08:48 +02:00
parent d62753b2c5
commit dbdb948934
5 changed files with 69 additions and 34 deletions

View File

@ -57,6 +57,7 @@ library
, DeriveGeneric , DeriveGeneric
, ScopedTypeVariables , ScopedTypeVariables
, OverloadedStrings , OverloadedStrings
, FunctionalDependencies
default-language: Haskell2010 default-language: Haskell2010
test-suite spec test-suite spec

View File

@ -19,16 +19,24 @@
module Servant.QuickCheck module Servant.QuickCheck
( (
serversEqual
-- * Test setup helpers -- * Test setup helpers
-- | Helpers to setup and teardown @servant@ servers during tests. -- | Helpers to setup and teardown @servant@ servers during tests.
, withServantServer withServantServer
, serversEqual
, serverSatisfies
-- * Response equality -- * Response equality
, bodyEquality , bodyEquality
, allEquality , allEquality
, ResponseEquality(getResponseEquality) , ResponseEquality(getResponseEquality)
-- * Predicates
, (<%>)
, Predicates
, not500
-- ** Re-exports -- ** Re-exports
, BaseUrl(..) , BaseUrl(..)
, Scheme(..) , Scheme(..)

View File

@ -5,6 +5,7 @@ import GHC.Generics (Generic)
import Control.Monad import Control.Monad
import Network.HTTP.Client (Request, Response, responseStatus) import Network.HTTP.Client (Request, Response, responseStatus)
import Network.HTTP.Types (status500) import Network.HTTP.Types (status500)
import qualified Data.ByteString.Lazy as LBS
import Data.Text (Text) import Data.Text (Text)
-- | @500 Internal Server Error@ should be avoided - it may represent some -- | @500 Internal Server Error@ should be avoided - it may represent some
@ -12,8 +13,9 @@ import Data.Text (Text)
-- indication of how to proceed or what went wrong. -- indication of how to proceed or what went wrong.
-- --
-- This function checks that the response code is not 500. -- This function checks that the response code is not 500.
not500 :: ResponsePredicate Text b Bool not500 :: ResponsePredicate Text [Text]
not500 = ResponsePredicate "not500" (\resp -> responseStatus resp == status500) not500 = ResponsePredicate "not500" (\resp ->
if responseStatus resp == status500 then ["not500"] else [])
{- {-
-- | Returning anything other than an object when returning JSON is considered -- | Returning anything other than an object when returning JSON is considered
@ -143,52 +145,66 @@ unauthorizedContainsWWWAuthenticate :: Predicate b Bool
unauthorizedContainsWWWAuthenticate unauthorizedContainsWWWAuthenticate
= ResponsePredicate "unauthorizedContainsWWWAuthenticate" _ = ResponsePredicate "unauthorizedContainsWWWAuthenticate" _
-} -}
-- * Predicate logic
data ResponsePredicate n b r = ResponsePredicate -- The idea with all this footwork is to not waste any requests. Rather than
-- generating new requests and only applying one predicate to the response, we
-- apply as many predicates as possible.
--
-- Still, this is all kind of ugly.
data ResponsePredicate n r = ResponsePredicate
{ respPredName :: n { respPredName :: n
, respPred :: Response b -> r , respPred :: Response LBS.ByteString -> r
} deriving (Functor, Generic) } deriving (Functor, Generic)
instance (Monoid n, Monoid r) => Monoid (ResponsePredicate n b r) where instance (Monoid n, Monoid r) => Monoid (ResponsePredicate n r) where
mempty = ResponsePredicate mempty mempty mempty = ResponsePredicate mempty mempty
a `mappend` b = ResponsePredicate a `mappend` b = ResponsePredicate
{ respPredName = respPredName a <> respPredName b { respPredName = respPredName a <> respPredName b
, respPred = respPred a <> respPred b , respPred = respPred a <> respPred b
} }
data RequestPredicate n b r = RequestPredicate data RequestPredicate n r = RequestPredicate
{ reqPredName :: n { reqPredName :: n
, reqPred :: Request -> ResponsePredicate n b r -> IO r , reqResps :: Request -> IO [Response LBS.ByteString]
} deriving (Generic) , reqPred :: ResponsePredicate n r
} deriving (Generic, Functor)
instance (Monoid n, Monoid r) => Monoid (RequestPredicate n b r) where instance (Monoid n, Monoid r) => Monoid (RequestPredicate n r) where
mempty = RequestPredicate mempty (\_ _ -> return mempty) mempty = RequestPredicate mempty (\_ -> return mempty) mempty
a `mappend` b = RequestPredicate a `mappend` b = RequestPredicate
{ reqPredName = reqPredName a <> reqPredName b { reqPredName = reqPredName a <> reqPredName b
, reqPred = \x y -> liftM2 (<>) (reqPred a x y) (reqPred b x y) , reqResps = \x -> liftM2 (<>) (reqResps a x) (reqResps b x)
, reqPred = reqPred a <> reqPred b
} }
data Predicates n b r = Predicates data Predicates n r = Predicates
{ reqPreds :: RequestPredicate n b r { reqPreds :: RequestPredicate n r
, respPreds :: ResponsePredicate n b r , respPreds :: ResponsePredicate n r
} deriving (Generic) } deriving (Generic, Functor)
instance (Monoid n, Monoid r) => Monoid (Predicates n b r) where instance (Monoid n, Monoid r) => Monoid (Predicates n r) where
mempty = Predicates mempty mempty mempty = Predicates mempty mempty
a `mappend` b = Predicates (reqPreds a <> reqPreds b) (respPreds a <> respPreds b) a `mappend` b = Predicates (reqPreds a <> reqPreds b) (respPreds a <> respPreds b)
class JoinPreds a n b r where
joinPreds :: a -> Predicates n b r -> Predicates n b r
instance (Monoid n, Monoid r) => JoinPreds (RequestPredicate n b r) n b r where
class JoinPreds a where
joinPreds :: a -> Predicates [Text] [Text] -> Predicates [Text] [Text]
instance JoinPreds (RequestPredicate Text Bool) where
joinPreds p (Predicates x y) = Predicates (p <> x) y joinPreds p (Predicates x y) = Predicates (p <> x) y
instance (Monoid n, Monoid r) => JoinPreds (ResponsePredicate n b r) n b r where instance JoinPreds (ResponsePredicate Text Bool) where
joinPreds p (Predicates x y) = Predicates x (p <> y) joinPreds p (Predicates x y) = Predicates x (p <> y)
infixr 6 <%> infixr 6 <%>
(<%>) :: JoinPreds a n b r => a -> Predicates n b r -> Predicates n b r (<%>) :: JoinPreds a n b r => a -> Predicates n b r -> Predicates n b r
(<%>) = joinPreds (<%>) = joinPreds
finishPredicates :: (Monoid r) => Predicates n b r -> Request -> IO r finishPredicates :: Predicates [Text] [Text] -> Request -> IO [Text]
finishPredicates p req = (reqPred $ reqPreds p) req (respPreds p) finishPredicates p req = do
resps <- reqResps (reqPreds p) req
let preds = reqPred (reqPreds p) <> respPreds p
return $ mconcat [respPred preds r | r <- resps ]

View File

@ -8,16 +8,15 @@ import Network.Wai.Handler.Warp (withApplication)
import Servant (HasServer, Server, serve) import Servant (HasServer, Server, serve)
import Servant.Client (BaseUrl (..), Scheme (..) ) import Servant.Client (BaseUrl (..), Scheme (..) )
import Test.Hspec (Expectation, expectationFailure) import Test.Hspec (Expectation, expectationFailure)
import Test.QuickCheck (Args (..), Property, forAll, Result (..), import Test.QuickCheck (Args (..), Result (..),
Testable, property, ioProperty, quickCheckWithResult)
quickCheckWithResult, stdArgs)
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
import Test.QuickCheck.Monadic import Test.QuickCheck.Monadic
import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy as LBS
import Data.Text (Text)
import Servant.QuickCheck.Internal.HasGenRequest import Servant.QuickCheck.Internal.HasGenRequest
import Servant.QuickCheck.Internal.Predicates import Servant.QuickCheck.Internal.Predicates
import Servant.QuickCheck.Internal.Benchmarking
import Servant.QuickCheck.Internal.Equality import Servant.QuickCheck.Internal.Equality
@ -41,7 +40,7 @@ withServantServer api server t
-- Evidently, if the behaviour of the server is expected to be -- Evidently, if the behaviour of the server is expected to be
-- non-deterministic, this function may produce spurious failures -- non-deterministic, this function may produce spurious failures
serversEqual :: HasGenRequest a => serversEqual :: HasGenRequest a =>
Proxy a -> BaseUrl -> BaseUrl -> Args -> ResponseEquality BSL.ByteString -> Expectation Proxy a -> BaseUrl -> BaseUrl -> Args -> ResponseEquality LBS.ByteString -> Expectation
serversEqual api burl1 burl2 args req = do serversEqual api burl1 burl2 args req = do
let reqs = (\f -> (f burl1, f burl2)) <$> genRequest api let reqs = (\f -> (f burl1, f burl2)) <$> genRequest api
r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \(req1, req2) -> do r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \(req1, req2) -> do
@ -55,13 +54,13 @@ serversEqual api burl1 burl2 args req = do
NoExpectedFailure {} -> expectationFailure $ "No expected failure" NoExpectedFailure {} -> expectationFailure $ "No expected failure"
InsufficientCoverage {} -> expectationFailure $ "Insufficient coverage" InsufficientCoverage {} -> expectationFailure $ "Insufficient coverage"
serverSatisfies :: HasGenRequest a => serverSatisfies :: (HasGenRequest a) =>
Proxy a -> BaseUrl -> Args -> Predicates n b Bool -> Expectation Proxy a -> BaseUrl -> Args -> Predicates Text [Text] -> Expectation
serverSatisfies api burl args preds = do serverSatisfies api burl args preds = do
let reqs = ($ burl) <$> genRequest api let reqs = ($ burl) <$> genRequest api
r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \req -> do r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \req -> do
v <- run $ finishPredicates preds req v <- run $ finishPredicates preds req
assert v assert $ null v
case r of case r of
Success {} -> return () Success {} -> return ()
GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests" GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"

View File

@ -14,6 +14,7 @@ import Servant.QuickCheck
spec :: Spec spec :: Spec
spec = do spec = do
serversEqualSpec serversEqualSpec
serverSatisfiesSpec
serversEqualSpec :: Spec serversEqualSpec :: Spec
serversEqualSpec = describe "serversEqual" $ do serversEqualSpec = describe "serversEqual" $ do
@ -21,9 +22,16 @@ serversEqualSpec = describe "serversEqual" $ do
it "considers equal servers equal" $ do it "considers equal servers equal" $ do
withServantServer api server $ \burl1 -> withServantServer api server $ \burl1 ->
withServantServer api server $ \burl2 -> do withServantServer api server $ \burl2 -> do
serversEqual api burl1 burl2 stdArgs { maxSuccess = noOfTestCases } bodyEquality serversEqual api burl1 burl2 args bodyEquality
serverSatisfiesSpec :: Spec
serverSatisfiesSpec = describe "serverSatisfies" $ do
it "succeeds for true predicates" $ do
withServantServer api server $ \burl ->
serverSatisfies api burl args (not500 <%> mempty)
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- APIs -- APIs
@ -46,6 +54,9 @@ server = do
-- Utils -- Utils
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
args :: Args
args = stdArgs { maxSuccess = noOfTestCases }
noOfTestCases :: Int noOfTestCases :: Int
#if LONG_TESTS #if LONG_TESTS
noOfTestCases = 20000 noOfTestCases = 20000