Resolve merge conflicts with unbiasedGeneration merge

This commit is contained in:
Erik Aker 2017-10-14 08:16:30 -07:00
commit f3b4fcf7a9
3 changed files with 128 additions and 53 deletions

View File

@ -14,106 +14,130 @@ import Prelude.Compat
import Servant import Servant
import Servant.API.ContentTypes (AllMimeRender (..)) import Servant.API.ContentTypes (AllMimeRender (..))
import Servant.Client (BaseUrl (..), Scheme (..)) import Servant.Client (BaseUrl (..), Scheme (..))
import Test.QuickCheck (Arbitrary (..), Gen, elements, oneof) import Test.QuickCheck (Arbitrary (..), Gen, elements, frequency)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS (c2w) import qualified Data.ByteString.Internal as BS (c2w)
-- -----------------------------------------------------------------------------
-- runGenRequest
-- | This function returns a QuickCheck `Gen a` when passed a servant API value,
-- typically a `Proxy API`. The generator returned is a function
-- that accepts a `BaseUrl` and returns a `Request`, which can then be used
-- to issue network requests. This `Gen` type makes it easier to compare distinct
-- APIs across different `BaseUrl`s.
runGenRequest :: HasGenRequest a => Proxy a -> Gen (BaseUrl -> Request)
runGenRequest = snd . genRequest
-- -----------------------------------------------------------------------------
-- HasGenRequest
-- | This is the core Servant-Quickcheck generator, which, when given a `Proxy API`
-- will return a pair of `Int` and `Gen a`, where `a` is a function from
-- `BaseUrl` to a `Network.Http.Client.Request`. The `Int` is a weight for the
-- QuickCheck `frequency` function which ensures a random distribution across
-- all endpoints in an API.
class HasGenRequest a where class HasGenRequest a where
genRequest :: Proxy a -> Gen (BaseUrl -> Request) genRequest :: Proxy a -> (Int, Gen (BaseUrl -> Request))
instance (HasGenRequest a, HasGenRequest b) => HasGenRequest (a :<|> b) where instance (HasGenRequest a, HasGenRequest b) => HasGenRequest (a :<|> b) where
genRequest _ genRequest _
= oneof [ genRequest (Proxy :: Proxy a) = (lf + rf, frequency [l, r])
, genRequest (Proxy :: Proxy b) where
] l@(lf, _) = genRequest (Proxy :: Proxy a)
r@(rf, _) = genRequest (Proxy :: Proxy b)
instance (KnownSymbol path, HasGenRequest b) => HasGenRequest (path :> b) where instance (KnownSymbol path, HasGenRequest b) => HasGenRequest (path :> b) where
genRequest _ = do genRequest _ = (oldf, do
old' <- old old' <- old
return $ \burl -> let r = old' burl return $ \burl -> let r = old' burl
oldPath = path r oldPath = path r
oldPath' = BS.dropWhile (== BS.c2w '/') oldPath oldPath' = BS.dropWhile (== BS.c2w '/') oldPath
paths = filter (not . BS.null) [new, oldPath'] paths = filter (not . BS.null) [new, oldPath']
in r { path = "/" <> BS.intercalate "/" paths } in r { path = "/" <> BS.intercalate "/" paths })
where where
old = genRequest (Proxy :: Proxy b) (oldf, old) = genRequest (Proxy :: Proxy b)
new = cs $ symbolVal (Proxy :: Proxy path) new = cs $ symbolVal (Proxy :: Proxy path)
instance (Arbitrary c, HasGenRequest b, ToHttpApiData c ) instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
=> HasGenRequest (Capture x c :> b) where => HasGenRequest (Capture x c :> b) where
genRequest _ = do genRequest _ = (oldf, do
old' <- old old' <- old
new' <- toUrlPiece <$> new new' <- toUrlPiece <$> new
return $ \burl -> let r = old' burl in r { path = cs new' <> path r } return $ \burl -> let r = old' burl in r { path = cs new' <> path r })
where where
old = genRequest (Proxy :: Proxy b) (oldf, old) = genRequest (Proxy :: Proxy b)
new = arbitrary :: Gen c new = arbitrary :: Gen c
#if MIN_VERSION_servant(0,8,0) #if MIN_VERSION_servant(0,8,0)
instance (Arbitrary c, HasGenRequest b, ToHttpApiData c ) instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
=> HasGenRequest (CaptureAll x c :> b) where => HasGenRequest (CaptureAll x c :> b) where
genRequest _ = do genRequest _ = (oldf, do
old' <- old old' <- old
new' <- fmap (cs . toUrlPiece) <$> new new' <- fmap (cs . toUrlPiece) <$> new
let new'' = BS.intercalate "/" new' let new'' = BS.intercalate "/" new'
return $ \burl -> let r = old' burl in r { path = new'' <> path r } return $ \burl -> let r = old' burl in r { path = new'' <> path r })
where where
old = genRequest (Proxy :: Proxy b) (oldf, old) = genRequest (Proxy :: Proxy b)
new = arbitrary :: Gen [c] new = arbitrary :: Gen [c]
#endif #endif
instance (Arbitrary c, KnownSymbol h, HasGenRequest b, ToHttpApiData c) instance (Arbitrary c, KnownSymbol h, HasGenRequest b, ToHttpApiData c)
=> HasGenRequest (Header h c :> b) where => HasGenRequest (Header h c :> b) where
genRequest _ = do genRequest _ = (oldf, do
old' <- old old' <- old
new' <- toUrlPiece <$> new new' <- toUrlPiece <$> new
return $ \burl -> let r = old' burl in r { return $ \burl -> let r = old' burl in r {
requestHeaders = (hdr, cs new') : requestHeaders r } requestHeaders = (hdr, cs new') : requestHeaders r })
where where
old = genRequest (Proxy :: Proxy b) (oldf, old) = genRequest (Proxy :: Proxy b)
hdr = fromString $ symbolVal (Proxy :: Proxy h) hdr = fromString $ symbolVal (Proxy :: Proxy h)
new = arbitrary :: Gen c new = arbitrary :: Gen c
instance (AllMimeRender x c, Arbitrary c, HasGenRequest b) instance (AllMimeRender x c, Arbitrary c, HasGenRequest b)
=> HasGenRequest (ReqBody x c :> b) where => HasGenRequest (ReqBody x c :> b) where
genRequest _ = do genRequest _ = (oldf, do
old' <- old old' <- old
new' <- new new' <- new
(ct, bd) <- elements $ allMimeRender (Proxy :: Proxy x) new' (ct, bd) <- elements $ allMimeRender (Proxy :: Proxy x) new'
return $ \burl -> let r = old' burl in r { return $ \burl -> let r = old' burl in r {
requestBody = RequestBodyLBS bd requestBody = RequestBodyLBS bd
, requestHeaders = ("Content-Type", renderHeader ct) : requestHeaders r , requestHeaders = ("Content-Type", renderHeader ct) : requestHeaders r
} })
where where
old = genRequest (Proxy :: Proxy b) (oldf, old) = genRequest (Proxy :: Proxy b)
new = arbitrary :: Gen c new = arbitrary :: Gen c
instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b) instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
=> HasGenRequest (QueryParam x c :> b) where => HasGenRequest (QueryParam x c :> b) where
genRequest _ = do genRequest _ = (oldf, do
new' <- new new' <- new
old' <- old old' <- old
return $ \burl -> let r = old' burl return $ \burl -> let r = old' burl
newExpr = param <> "=" <> cs (toQueryParam new') newExpr = param <> "=" <> cs (toQueryParam new')
qs = queryString r in r { qs = queryString r in r {
queryString = if BS.null qs then newExpr else newExpr <> "&" <> qs } queryString = if BS.null qs then newExpr else newExpr <> "&" <> qs })
where where
old = genRequest (Proxy :: Proxy b) (oldf, old) = genRequest (Proxy :: Proxy b)
param = cs $ symbolVal (Proxy :: Proxy x) param = cs $ symbolVal (Proxy :: Proxy x)
new = arbitrary :: Gen c new = arbitrary :: Gen c
instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b) instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
=> HasGenRequest (QueryParams x c :> b) where => HasGenRequest (QueryParams x c :> b) where
genRequest _ = do genRequest _ = (oldf, do
new' <- new new' <- new
old' <- old old' <- old
return $ \burl -> let r = old' burl in r { return $ \burl -> let r = old' burl in r {
queryString = queryString r queryString = queryString r
<> if length new' > 0 then fold (toParam <$> new') else ""} <> if length new' > 0 then fold (toParam <$> new') else ""})
where where
old = genRequest (Proxy :: Proxy b) (oldf, old) = genRequest (Proxy :: Proxy b)
param = cs $ symbolVal (Proxy :: Proxy x) param = cs $ symbolVal (Proxy :: Proxy x)
new = arbitrary :: Gen [c] new = arbitrary :: Gen [c]
toParam c = param <> "[]=" <> cs (toQueryParam c) toParam c = param <> "[]=" <> cs (toQueryParam c)
@ -121,23 +145,23 @@ instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
instance (KnownSymbol x, HasGenRequest b) instance (KnownSymbol x, HasGenRequest b)
=> HasGenRequest (QueryFlag x :> b) where => HasGenRequest (QueryFlag x :> b) where
genRequest _ = do genRequest _ = (oldf, do
old' <- old old' <- old
return $ \burl -> let r = old' burl return $ \burl -> let r = old' burl
qs = queryString r in r { qs = queryString r in r {
queryString = if BS.null qs then param else param <> "&" <> qs } queryString = if BS.null qs then param else param <> "&" <> qs })
where where
old = genRequest (Proxy :: Proxy b) (oldf, old) = genRequest (Proxy :: Proxy b)
param = cs $ symbolVal (Proxy :: Proxy x) param = cs $ symbolVal (Proxy :: Proxy x)
instance (ReflectMethod method) instance (ReflectMethod method)
=> HasGenRequest (Verb (method :: k) (status :: Nat) (cts :: [*]) a) where => HasGenRequest (Verb (method :: k) (status :: Nat) (cts :: [*]) a) where
genRequest _ = return $ \burl -> defaultRequest genRequest _ = (1, return $ \burl -> defaultRequest
{ host = cs $ baseUrlHost burl { host = cs $ baseUrlHost burl
, port = baseUrlPort burl , port = baseUrlPort burl
, secure = baseUrlScheme burl == Https , secure = baseUrlScheme burl == Https
, method = reflectMethod (Proxy :: Proxy method) , method = reflectMethod (Proxy :: Proxy method)
} })
instance (HasGenRequest a) => HasGenRequest (RemoteHost :> a) where instance (HasGenRequest a) => HasGenRequest (RemoteHost :> a) where
genRequest _ = genRequest (Proxy :: Proxy a) genRequest _ = genRequest (Proxy :: Proxy a)

View File

@ -70,7 +70,7 @@ withServantServerAndContext api ctx server t
serversEqual :: HasGenRequest a => serversEqual :: HasGenRequest a =>
Proxy a -> BaseUrl -> BaseUrl -> Args -> ResponseEquality LBS.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)) <$> runGenRequest api
-- This MVar stuff is clunky! But there doesn't seem to be an easy way to -- This MVar stuff is clunky! But there doesn't seem to be an easy way to
-- return results when a test fails, since an exception is throw. -- return results when a test fails, since an exception is throw.
deetsMVar <- newMVar $ error "should not be called" deetsMVar <- newMVar $ error "should not be called"
@ -111,7 +111,7 @@ serversEqual api burl1 burl2 args req = do
serverSatisfies :: (HasGenRequest a) => serverSatisfies :: (HasGenRequest a) =>
Proxy a -> BaseUrl -> Args -> Predicates -> Expectation Proxy a -> BaseUrl -> Args -> Predicates -> Expectation
serverSatisfies api burl args preds = do serverSatisfies api burl args preds = do
let reqs = ($ burl) <$> genRequest api let reqs = ($ burl) <$> runGenRequest api
deetsMVar <- newMVar $ error "should not be called" deetsMVar <- newMVar $ error "should not be called"
r <- quickCheckWithResult args { chatty = False } $ monadicIO $ forAllM reqs $ \req -> do r <- quickCheckWithResult args { chatty = False } $ monadicIO $ forAllM reqs $ \req -> do
v <- run $ finishPredicates preds (noCheckStatus req) defManager v <- run $ finishPredicates preds (noCheckStatus req) defManager
@ -131,7 +131,7 @@ serverSatisfies api burl args preds = do
serverDoesntSatisfy :: (HasGenRequest a) => serverDoesntSatisfy :: (HasGenRequest a) =>
Proxy a -> BaseUrl -> Args -> Predicates -> Expectation Proxy a -> BaseUrl -> Args -> Predicates -> Expectation
serverDoesntSatisfy api burl args preds = do serverDoesntSatisfy api burl args preds = do
let reqs = ($ burl) <$> genRequest api let reqs = ($ burl) <$> runGenRequest api
r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \req -> do r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \req -> do
v <- run $ finishPredicates preds (noCheckStatus req) defManager v <- run $ finishPredicates preds (noCheckStatus req) defManager
assert $ not $ null v assert $ not $ null v

View File

@ -1,20 +1,24 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
module Servant.QuickCheck.InternalSpec (spec) where module Servant.QuickCheck.InternalSpec (spec) where
import Control.Concurrent.MVar (newMVar, readMVar, swapMVar)
import Control.Exception import Control.Concurrent.MVar (newMVar, readMVar, swapMVar)
import Control.Monad.IO.Class (liftIO) import Control.Exception (SomeException)
import qualified Data.ByteString as BS import Control.Monad (replicateM)
import qualified Data.ByteString.Char8 as C import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as C
import Data.Maybe (fromJust)
import Prelude.Compat import Prelude.Compat
import Servant import Servant
import Test.Hspec (Spec, context, describe, it, shouldBe, import Test.Hspec (Spec, context, describe, it, shouldBe,
shouldContain) shouldContain)
import Test.Hspec.Core.Spec (Arg, Example, Result (..), import Test.Hspec.Core.Spec (Arg, Example, Result (..),
defaultParams, safeEvaluateExample) defaultParams, safeEvaluateExample)
import Test.QuickCheck.Gen (unGen) import Test.QuickCheck.Gen (unGen, generate)
import Test.QuickCheck.Random (mkQCGen) import Test.QuickCheck.Random (mkQCGen)
import Network.HTTP.Client (queryString, path) import Network.HTTP.Client (queryString, path)
#if MIN_VERSION_servant(0,8,0) #if MIN_VERSION_servant(0,8,0)
import Servant.API.Internal.Test.ComprehensiveAPI (comprehensiveAPIWithoutRaw) import Servant.API.Internal.Test.ComprehensiveAPI (comprehensiveAPIWithoutRaw)
@ -24,7 +28,8 @@ import Servant.API.Internal.Test.ComprehensiveAPI (ComprehensiveAPI,
#endif #endif
import Servant.QuickCheck import Servant.QuickCheck
import Servant.QuickCheck.Internal (genRequest, serverDoesntSatisfy) import Servant.QuickCheck.Internal (genRequest, runGenRequest, serverDoesntSatisfy)
spec :: Spec spec :: Spec
spec = do spec = do
@ -36,6 +41,7 @@ spec = do
queryParamsSpec queryParamsSpec
queryFlagsSpec queryFlagsSpec
deepPathSpec deepPathSpec
unbiasedGenerationSpec
serversEqualSpec :: Spec serversEqualSpec :: Spec
serversEqualSpec = describe "serversEqual" $ do serversEqualSpec = describe "serversEqual" $ do
@ -46,7 +52,6 @@ serversEqualSpec = describe "serversEqual" $ do
serversEqual api burl1 burl2 args bodyEquality serversEqual api burl1 burl2 args bodyEquality
context "when servers are not equal" $ do context "when servers are not equal" $ do
it "provides the failing responses in the error message" $ do it "provides the failing responses in the error message" $ do
Right (Failure _ err) <- withServantServer api2 server2 $ \burl1 -> Right (Failure _ err) <- withServantServer api2 server2 $ \burl1 ->
withServantServer api2 server3 $ \burl2 -> do withServantServer api2 server3 $ \burl2 -> do
@ -77,11 +82,12 @@ serverSatisfiesSpec = describe "serverSatisfies" $ do
it "fails with informative error messages" $ do it "fails with informative error messages" $ do
Right (Failure _ err) <- withServantServerAndContext api ctx server $ \burl -> do Right (Failure _ err) <- withServantServerAndContext api ctx server $ \burl -> do
safeEvalExample $ serverSatisfies api burl args (getsHaveCacheControlHeader <%> mempty) safeEvalExample $ serverSatisfies api burl args (notAllowedContainsAllowHeader <%> mempty)
show err `shouldContain` "getsHaveCacheControlHeader" show err `shouldContain` "notAllowedContainsAllowHeader"
show err `shouldContain` "Headers" show err `shouldContain` "Headers"
show err `shouldContain` "Body" show err `shouldContain` "Body"
onlyJsonObjectSpec :: Spec onlyJsonObjectSpec :: Spec
onlyJsonObjectSpec = describe "onlyJsonObjects" $ do onlyJsonObjectSpec = describe "onlyJsonObjects" $ do
@ -121,7 +127,7 @@ deepPathSpec = describe "Path components" $ do
it "are separated by slashes, without a trailing slash" $ do it "are separated by slashes, without a trailing slash" $ do
let rng = mkQCGen 0 let rng = mkQCGen 0
burl = BaseUrl Http "localhost" 80 "" burl = BaseUrl Http "localhost" 80 ""
gen = genRequest deepAPI gen = runGenRequest deepAPI
req = (unGen gen rng 0) burl req = (unGen gen rng 0) burl
path req `shouldBe` ("/one/two/three") path req `shouldBe` ("/one/two/three")
@ -132,7 +138,7 @@ queryParamsSpec = describe "QueryParams" $ do
it "reduce to an HTTP query string correctly" $ do it "reduce to an HTTP query string correctly" $ do
let rng = mkQCGen 0 let rng = mkQCGen 0
burl = BaseUrl Http "localhost" 80 "" burl = BaseUrl Http "localhost" 80 ""
gen = genRequest paramsAPI gen = runGenRequest paramsAPI
req = (unGen gen rng 0) burl req = (unGen gen rng 0) burl
qs = C.unpack $ queryString req qs = C.unpack $ queryString req
qs `shouldBe` "one=_&two=_" qs `shouldBe` "one=_&two=_"
@ -143,11 +149,33 @@ queryFlagsSpec = describe "QueryFlags" $ do
it "reduce to an HTTP query string correctly" $ do it "reduce to an HTTP query string correctly" $ do
let rng = mkQCGen 0 let rng = mkQCGen 0
burl = BaseUrl Http "localhost" 80 "" burl = BaseUrl Http "localhost" 80 ""
gen = genRequest flagsAPI gen = runGenRequest flagsAPI
req = (unGen gen rng 0) burl req = (unGen gen rng 0) burl
qs = C.unpack $ queryString req qs = C.unpack $ queryString req
qs `shouldBe` "one&two" qs `shouldBe` "one&two"
makeRandomRequest :: Proxy LargeAPI -> BaseUrl -> IO Integer
makeRandomRequest large burl = do
req <- generate $ runGenRequest large
pure $ fst . fromJust . C.readInteger . C.drop 1 . path $ req burl
unbiasedGenerationSpec :: Spec
unbiasedGenerationSpec = describe "Unbiased Generation of requests" $
it "frequency paired with generated endpoint should be more randomly distributed" $ do
let burl = BaseUrl Http "localhost" 80 ""
let runs = 10000 :: Double
someRequests <- replicateM 10000 (makeRandomRequest largeApi burl)
let mean = (sum $ map fromIntegral someRequests) / runs
let variancer x = let ix = fromIntegral x in (ix - mean) * (ix - mean)
let variance = (sum $ map variancer someRequests) / runs - 1
-- mean should be around 8.5. If this fails, we likely need more runs (or there's a bug!)
mean > 8 `shouldBe` True
mean < 9 `shouldBe` True
-- Std dev is likely around 4. Variance is probably greater than 20.
variance > 19.5 `shouldBe` True
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- APIs -- APIs
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
@ -195,6 +223,29 @@ server2 = return $ return 1
server3 :: IO (Server API2) server3 :: IO (Server API2)
server3 = return $ return 2 server3 = return $ return 2
largeApi :: Proxy LargeAPI
largeApi = Proxy
type LargeAPI
= "1" :> Get '[JSON] Int
:<|> "2" :> Get '[JSON] Int
:<|> "3" :> Get '[JSON] Int
:<|> "4" :> Get '[JSON] Int
:<|> "5" :> Get '[JSON] Int
:<|> "6" :> Get '[JSON] Int
:<|> "7" :> Get '[JSON] Int
:<|> "8" :> Get '[JSON] Int
:<|> "9" :> Get '[JSON] Int
:<|> "10" :> Get '[JSON] Int
:<|> "11" :> Get '[JSON] Int
:<|> "12" :> Get '[JSON] Int
:<|> "13" :> Get '[JSON] Int
:<|> "14" :> Get '[JSON] Int
:<|> "15" :> Get '[JSON] Int
:<|> "16" :> Get '[JSON] Int
type OctetAPI = Get '[OctetStream] BS.ByteString type OctetAPI = Get '[OctetStream] BS.ByteString
octetAPI :: Proxy OctetAPI octetAPI :: Proxy OctetAPI