Compare commits

..

3 Commits

18 changed files with 347 additions and 592 deletions

View File

@ -1,158 +1,38 @@
# This Travis job script has been generated by a script via sudo: false
#
# haskell-ci '--config=cabal.haskell-ci' 'servant-quickcheck.cabal'
#
# To regenerate the script (for example after adjusting tested-with) run
#
# haskell-ci regenerate
#
# For more information, see https://github.com/haskell-CI/haskell-ci
#
# version: 0.10.1
#
version: ~> 1.0
language: c language: c
os: linux
dist: xenial env:
git: - STACK_YAML=stack.yaml
# whether to recursively clone submodules - STACK_YAML=stack-lts-7.yaml
submodules: false - STACK_YAML=stack-lts-6.yaml
branches: - STACK_YAML=stack-lts-9.yaml
only:
- master
addons:
apt:
packages: libgmp-dev
install:
# stack
- mkdir -p ~/.local/bin
- export PATH=~/.local/bin:$PATH
- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
- stack --version
script:
- stack setup --no-terminal
- stack build --ghc-options=-Werror --no-terminal
- stack test --ghc-options=-Werror --no-terminal --coverage
- stack haddock --no-terminal
after_script:
# SHC only has a build for 8.0.1, not above
- if [ "$STACK_YAML" == stack-lts-7.yaml ]
- travis_retry curl -L https://github.com/rubik/stack-hpc-coveralls/releases/download/v0.0.4.0/shc-linux-x64-8.0.1.tar.bz2 | tar -xj
- ./shc servant-quickcheck spec
cache: cache:
directories: directories:
- $HOME/.cabal/packages - $HOME/.stack
- $HOME/.cabal/store
- $HOME/.hlint
before_cache:
- rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log
# remove files that are regenerated by 'cabal update'
- rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.*
- rm -fv $CABALHOME/packages/hackage.haskell.org/*.json
- rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache
- rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar
- rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx
- rm -rfv $CABALHOME/packages/head.hackage
jobs:
include:
- compiler: ghc-8.8.3
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.8.3","cabal-install-3.2"]}}
os: linux
- compiler: ghc-8.6.5
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.6.5","cabal-install-3.2"]}}
os: linux
- compiler: ghc-8.4.4
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.4.4","cabal-install-3.2"]}}
os: linux
- compiler: ghc-8.2.2
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.2.2","cabal-install-3.2"]}}
os: linux
- compiler: ghc-8.0.2
addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.0.2","cabal-install-3.2"]}}
os: linux
before_install:
- HC=$(echo "/opt/$CC/bin/ghc" | sed 's/-/\//')
- WITHCOMPILER="-w $HC"
- HADDOCK=$(echo "/opt/$CC/bin/haddock" | sed 's/-/\//')
- HCPKG="$HC-pkg"
- unset CC
- CABAL=/opt/ghc/bin/cabal
- CABALHOME=$HOME/.cabal
- export PATH="$CABALHOME/bin:$PATH"
- TOP=$(pwd)
- "HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\\d+)\\.(\\d+)\\.(\\d+)(\\.(\\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')"
- echo $HCNUMVER
- CABAL="$CABAL -vnormal+nowrap"
- set -o pipefail
- TEST=--enable-tests
- BENCH=--enable-benchmarks
- HEADHACKAGE=false
- rm -f $CABALHOME/config
- |
echo "verbose: normal +nowrap +markoutput" >> $CABALHOME/config
echo "remote-build-reporting: anonymous" >> $CABALHOME/config
echo "write-ghc-environment-files: always" >> $CABALHOME/config
echo "remote-repo-cache: $CABALHOME/packages" >> $CABALHOME/config
echo "logs-dir: $CABALHOME/logs" >> $CABALHOME/config
echo "world-file: $CABALHOME/world" >> $CABALHOME/config
echo "extra-prog-path: $CABALHOME/bin" >> $CABALHOME/config
echo "symlink-bindir: $CABALHOME/bin" >> $CABALHOME/config
echo "installdir: $CABALHOME/bin" >> $CABALHOME/config
echo "build-summary: $CABALHOME/logs/build.log" >> $CABALHOME/config
echo "store-dir: $CABALHOME/store" >> $CABALHOME/config
echo "install-dirs user" >> $CABALHOME/config
echo " prefix: $CABALHOME" >> $CABALHOME/config
echo "repository hackage.haskell.org" >> $CABALHOME/config
echo " url: http://hackage.haskell.org/" >> $CABALHOME/config
install:
- ${CABAL} --version
- echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
- |
echo "program-default-options" >> $CABALHOME/config
echo " ghc-options: $GHCJOBS +RTS -M6G -RTS" >> $CABALHOME/config
- cat $CABALHOME/config
- rm -fv cabal.project cabal.project.local cabal.project.freeze
- travis_retry ${CABAL} v2-update -v
# Generate cabal.project
- rm -rf cabal.project cabal.project.local cabal.project.freeze
- touch cabal.project
- |
echo "packages: ." >> cabal.project
- if [ $HCNUMVER -ge 80200 ] ; then echo 'package servant-quickcheck' >> cabal.project ; fi
- "if [ $HCNUMVER -ge 80200 ] ; then echo ' ghc-options: -Werror=missing-methods' >> cabal.project ; fi"
- |
- "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(servant-quickcheck)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
- cat cabal.project || true
- cat cabal.project.local || true
- if [ -f "./configure.ac" ]; then (cd "." && autoreconf -i); fi
- ${CABAL} v2-freeze $WITHCOMPILER ${TEST} ${BENCH}
- "cat cabal.project.freeze | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'"
- rm cabal.project.freeze
- travis_wait 40 ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} --dep -j2 all
- travis_wait 40 ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --dep -j2 all
script:
- DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
# Packaging...
- ${CABAL} v2-sdist all
# Unpacking...
- mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
- cd ${DISTDIR} || false
- find . -maxdepth 1 -type f -name '*.tar.gz' -exec tar -xvf '{}' \;
- find . -maxdepth 1 -type f -name '*.tar.gz' -exec rm '{}' \;
- PKGDIR_servant_quickcheck="$(find . -maxdepth 1 -type d -regex '.*/servant-quickcheck-[0-9.]*')"
# Generate cabal.project
- rm -rf cabal.project cabal.project.local cabal.project.freeze
- touch cabal.project
- |
echo "packages: ${PKGDIR_servant_quickcheck}" >> cabal.project
- if [ $HCNUMVER -ge 80200 ] ; then echo 'package servant-quickcheck' >> cabal.project ; fi
- "if [ $HCNUMVER -ge 80200 ] ; then echo ' ghc-options: -Werror=missing-methods' >> cabal.project ; fi"
- |
- "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(servant-quickcheck)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
- cat cabal.project || true
- cat cabal.project.local || true
# Building...
# this builds all libraries and executables (without tests/benchmarks)
- ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all
# Building with tests and benchmarks...
# build & run tests, build benchmarks
- ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} all
# Testing...
- ${CABAL} v2-test $WITHCOMPILER ${TEST} ${BENCH} all
# cabal check...
- (cd ${PKGDIR_servant_quickcheck} && ${CABAL} -vnormal check)
# haddock...
- ${CABAL} v2-haddock $WITHCOMPILER --with-haddock $HADDOCK ${TEST} ${BENCH} all
# Building without installed constraints for packages in global-db...
- rm -f cabal.project.local
- ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all
# Constraint sets
- rm -rf cabal.project.local
# Constraint set base-compat-0.10
- ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --constraint='base-compat ==0.10.*' all
# Constraint set base-compat-0.11
- ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --constraint='base-compat ==0.11.*' all
# REGENDATA ("0.10.1",["--config=cabal.haskell-ci","servant-quickcheck.cabal"])
# EOF

View File

@ -1,112 +1,15 @@
upcoming:
releases: releases:
- version: "0.0.9.0"
- version: "0.0.3.1"
changes: changes:
- description: "Support servant-0.15 (#65)"
authors: fizruk
date: 2020-06-25
- description: "Relax constraints for GHC 8.10 (#70)"
authors: felixmulder
date: 2020-06-20
- version: "0.0.8.0"
changes:
- description: Support for servant-0.17
authors: phadej
date: 2019-01-23
- version: "0.0.7.3"
changes:
- description: Support for servant-0.14
issue: 53
authors: phadej
date: 2018-06-12
- description: Fix a failure from OnlyJsonObjects when there is no content-type.
issue: 55
authors: Phenitei
date: 2018-08-27
- description: A bug fix where notAllowedContainsAllowHeader would print the initial request alongside the failure instead of the request causing the failure when it failed.
issue: 57
authors: Phenitei
date: 2018-08-29
- description: QuickCheck 2.12 compatibility
issue: 58
authors: parsonsmatt
date: 2018-10-12
- description: GHC 8.6 compatibility
issue: 59
authors: phadej
date: 2018-10-15
- version: "0.0.7.2"
changes:
- description: Allow client to pass an HTTP Manager in to functions
issue: 47
authors: parsonsmatt
date: 2018-05-10
- description: Fix "should not happen" error when exceptions are thrown
issue: 48
authors: parsonsmatt
date: 2018-05-10
- version: "0.0.7.0"
changes:
- description: Support for base-compat-0.10
issue: none
authors: phadej
date: 2018-04-12
- version: "0.0.7.0"
changes:
- description: Support for GHC-8.4.1
issue: none
authors: phadej
date: 2018-03-23
- description: Requires hspec-2.5
issue: none
authors: phadej
date: 2018-03-23
- version: "0.0.6.0"
changes:
- description: Support for servant-0.12
issue: none
authors: phadej
date: 2018-02-09
- version: "0.0.5.0"
changes:
- description: Export forgotten predicates
issue: none
pr: 40
authors: Phenitei
date: 2017-12-14
- version: "0.0.4"
changes:
- description: Support for Servant 0.12
issue: none
authors: phadej
date: 2017-11-07
- description: Support for Servant 0.11 - description: Support for Servant 0.11
issue: none issue: none
pr: 32 pr: 32
authors: adinapoli-iohk authors: adinapoli-iohk
date: 2017-10-18 date: 2017-10-18
notes: Includes 0-weighted instance for EmptyAPI
- version: "0.0.3.0" - version: "0.0.3.0"
changes: changes:

View File

@ -1,7 +0,0 @@
branches: master
constraint-set base-compat-0.10
constraints: base-compat ==0.10.*
constraint-set base-compat-0.11
constraints: base-compat ==0.11.*

View File

@ -1,4 +0,0 @@
packages: .
tests: true
allow-newer: servant-blaze:servant

View File

@ -1,31 +0,0 @@
{-# LANGUAGE OverloadedStrings, DataKinds #-}
module Main (main) where
import Servant
import Servant.QuickCheck
import Test.Hspec
import Data.Text (Text)
import System.Environment (getArgs)
main :: IO ()
main = do
args <- getArgs
case args of
[] -> putStrLn "Not running without arguments. Try --help or --fail-fast."
_ -> hspec spec
-- Change to String to reproduce
-- https://github.com/haskell-servant/servant-quickcheck/issues/41
type API = Get '[PlainText] Text
api :: Proxy API
api = Proxy
server :: Server API
server = return "Sigurð Fáfnirslayer"
spec :: Spec
spec = describe "example server" $
it "mangles UTF-8 in error messages" $
withServantServer api (return server) $ \burl ->
serverSatisfies api burl defaultArgs (getsHaveCacheControlHeader <%> mempty)

View File

@ -1,22 +1,22 @@
name: servant-quickcheck name: servant-quickcheck
version: 0.0.9.1 version: 0.0.3.1
synopsis: QuickCheck entire APIs synopsis: QuickCheck entire APIs
description: description:
This packages provides QuickCheck properties that are tested across an entire This packages provides QuickCheck properties that are tested across an entire
API. API.
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Julian K. Arni author: Julian K. Arni
maintainer: jkarni@gmail.com maintainer: jkarni@gmail.com
category: Web category: Web
build-type: Simple build-type: Simple
cabal-version: >=1.10 cabal-version: >=1.10
extra-source-files: CHANGELOG.yaml extra-source-files:
tested-with: GHC ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || == 8.8.3 CHANGELOG.yaml
source-repository head source-repository head
type: git type: git
location: https://github.com/haskell-servant/servant-quickcheck location: https://github.com/haskell-servant/servant-quickcheck
flag long-tests flag long-tests
@ -24,115 +24,88 @@ flag long-tests
default: False default: False
library library
exposed-modules: exposed-modules: Servant.QuickCheck
Servant.QuickCheck , Servant.QuickCheck.Internal
Servant.QuickCheck.Internal , Servant.QuickCheck.Internal.Predicates
Servant.QuickCheck.Internal.Equality , Servant.QuickCheck.Internal.HasGenRequest
Servant.QuickCheck.Internal.ErrorTypes , Servant.QuickCheck.Internal.QuickCheck
Servant.QuickCheck.Internal.HasGenRequest , Servant.QuickCheck.Internal.Equality
Servant.QuickCheck.Internal.Predicates , Servant.QuickCheck.Internal.ErrorTypes
Servant.QuickCheck.Internal.QuickCheck build-depends: base >=4.8 && <4.10
, base-compat == 0.9.*
, aeson > 0.8 && < 2
, bytestring == 0.10.*
, case-insensitive == 1.2.*
, clock >= 0.7 && < 0.8
, data-default-class >= 0.0 && < 0.2
, hspec >= 2.2 && < 2.5
, http-client >= 0.4.30 && < 0.6
, http-media == 0.6.*
, http-types > 0.8 && < 0.10
, mtl > 2.1 && < 2.3
, pretty == 1.1.*
, process >= 1.2 && < 1.5
, QuickCheck > 2.7 && < 2.11
, servant > 0.6 && < 0.12
, servant-client > 0.6 && < 0.12
, servant-server > 0.6 && < 0.12
, split == 0.2.*
, string-conversions > 0.3 && < 0.5
, temporary == 1.2.*
, text == 1.*
, time >= 1.5 && < 1.7
, warp >= 3.2.4 && < 3.3
build-depends: hs-source-dirs: src
aeson >=0.8 && <2 default-extensions: TypeOperators
, base >=4.9 && <4.15 , FlexibleInstances
, base-compat-batteries >=0.10.1 && <0.12 , FlexibleContexts
, bytestring >=0.10 && <0.11 , DataKinds
, case-insensitive >=1.2 && <1.3 , GADTs
, clock >=0.7 && <0.9 , MultiParamTypeClasses
, data-default-class >=0.0 && <0.2 , DeriveFunctor
, hspec >=2.5.6 && <2.8 , KindSignatures
, http-client >=0.4.30 && <0.8 , RankNTypes
, http-media >=0.6 && <0.9 , ConstraintKinds
, http-types >=0.8 && <0.13 , DeriveGeneric
, mtl >=2.1 && <2.3 , ScopedTypeVariables
, pretty >=1.1 && <1.2 , OverloadedStrings
, process >=1.2 && <1.7 , FunctionalDependencies
, QuickCheck >=2.7 && <2.15 , NoImplicitPrelude
, servant >=0.17 && <0.19 , DeriveDataTypeable
, servant-client >=0.17 && <0.19 default-language: Haskell2010
, servant-server >=0.17 && <0.19
, split >=0.2 && <0.3
, string-conversions >=0.3 && <0.5
, temporary >=1.2 && <1.4
, text >=1 && <2
, time >=1.5 && <1.11
, warp >=3.2.4 && <3.4
if !impl(ghc >=8.0)
build-depends: semigroups >=0.18.3 && <0.20
hs-source-dirs: src
default-extensions:
NoImplicitPrelude
ConstraintKinds
DataKinds
DeriveDataTypeable
DeriveFunctor
DeriveGeneric
FlexibleContexts
FlexibleInstances
FunctionalDependencies
GADTs
KindSignatures
MultiParamTypeClasses
OverloadedStrings
RankNTypes
ScopedTypeVariables
TypeOperators
default-language: Haskell2010
test-suite spec test-suite spec
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
ghc-options: -Wall -threaded ghc-options: -Wall -threaded
default-language: Haskell2010 default-language: Haskell2010
hs-source-dirs: test hs-source-dirs: test
main-is: Spec.hs main-is: Spec.hs
other-modules: Servant.QuickCheck.InternalSpec other-modules: Servant.QuickCheck.InternalSpec
build-tool-depends: hspec-discover:hspec-discover -any build-depends: base == 4.*
build-depends: , base-compat
aeson , aeson
, base , servant-quickcheck
, base-compat-batteries , bytestring
, blaze-html , hspec
, bytestring , hspec-core
, hspec , http-client
, hspec-core >=2.5.5 && <2.8 , blaze-html
, http-client , warp
, QuickCheck , servant-server
, quickcheck-io , servant-client
, servant , servant
, servant-blaze , servant-blaze
, servant-client , transformers
, servant-quickcheck , QuickCheck
, servant-server , quickcheck-io
, transformers default-extensions: TypeOperators
, warp , FlexibleInstances
, FlexibleContexts
default-extensions: , GADTs
NoImplicitPrelude , DataKinds
DataKinds , NoImplicitPrelude
FlexibleContexts , OverloadedStrings
FlexibleInstances , ScopedTypeVariables
GADTs
OverloadedStrings
ScopedTypeVariables
TypeOperators
if flag(long-tests) if flag(long-tests)
cpp-options: -DLONG_TESTS cpp-options: -DLONG_TESTS
test-suite example
type: exitcode-stdio-1.0
main-is: Main.hs
hs-source-dirs: example
ghc-options: -Wall
build-depends:
base
, hspec
, servant-quickcheck
, servant-server
, text
default-language: Haskell2010

View File

@ -29,10 +29,8 @@ module Servant.QuickCheck
, not500 , not500
, notLongerThan , notLongerThan
, onlyJsonObjects , onlyJsonObjects
, honoursAcceptHeader
, notAllowedContainsAllowHeader , notAllowedContainsAllowHeader
, unauthorizedContainsWWWAuthenticate , unauthorizedContainsWWWAuthenticate
, getsHaveLastModifiedHeader
, getsHaveCacheControlHeader , getsHaveCacheControlHeader
, headsHaveCacheControlHeader , headsHaveCacheControlHeader
, createContainsValidLocation , createContainsValidLocation

View File

@ -5,19 +5,15 @@ import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString.Lazy as LB
import Data.Function (on) import Data.Function (on)
import Network.HTTP.Client (Response, responseBody) import Network.HTTP.Client (Response, responseBody)
import Data.Semigroup (Semigroup (..))
import Prelude.Compat import Prelude.Compat
newtype ResponseEquality b newtype ResponseEquality b
= ResponseEquality { getResponseEquality :: Response b -> Response b -> Bool } = ResponseEquality { getResponseEquality :: Response b -> Response b -> Bool }
instance Semigroup (ResponseEquality b) where
ResponseEquality a <> ResponseEquality b = ResponseEquality $ \x y ->
a x y && b x y
instance Monoid (ResponseEquality b) where instance Monoid (ResponseEquality b) where
mempty = ResponseEquality $ \_ _ -> True mempty = ResponseEquality $ \_ _ -> True
mappend = (<>) ResponseEquality a `mappend` ResponseEquality b = ResponseEquality $ \x y ->
a x y && b x y
-- | Use `Eq` instance for `Response` -- | Use `Eq` instance for `Response`
-- --

View File

@ -1,4 +1,3 @@
{-# LANGUAGE CPP #-}
module Servant.QuickCheck.Internal.ErrorTypes where module Servant.QuickCheck.Internal.ErrorTypes where
import Control.Exception (Exception (..)) import Control.Exception (Exception (..))
@ -9,12 +8,11 @@ import Data.Typeable (Typeable)
import GHC.Generics (Generic) import GHC.Generics (Generic)
import qualified Network.HTTP.Client as C import qualified Network.HTTP.Client as C
import Network.HTTP.Types (Header, statusCode) import Network.HTTP.Types (Header, statusCode)
import Prelude.Compat
import Text.PrettyPrint import Text.PrettyPrint
import Prelude.Compat hiding ((<>))
data PredicateFailure data PredicateFailure
= PredicateFailure T.Text (Maybe C.Request) (C.Response LBS.ByteString) = PredicateFailure T.Text (C.Request) (C.Response LBS.ByteString)
deriving (Typeable, Generic) deriving (Typeable, Generic)
instance Exception ServerEqualityFailure where instance Exception ServerEqualityFailure where
@ -73,10 +71,5 @@ prettyPredicateFailure :: PredicateFailure -> Doc
prettyPredicateFailure (PredicateFailure predicate req resp) = prettyPredicateFailure (PredicateFailure predicate req resp) =
text "Predicate failed" $$ (nest 5 $ text "Predicate failed" $$ (nest 5 $
text "Predicate:" <+> (text $ T.unpack predicate) text "Predicate:" <+> (text $ T.unpack predicate)
$$ r $$ prettyReq req
$$ prettyResp resp) $$ prettyResp resp)
where
r = case req of
Nothing -> text ""
Just v -> prettyReq v

View File

@ -2,6 +2,7 @@
{-# LANGUAGE PolyKinds #-} {-# LANGUAGE PolyKinds #-}
module Servant.QuickCheck.Internal.HasGenRequest where module Servant.QuickCheck.Internal.HasGenRequest where
import Data.Monoid ((<>))
import Data.String (fromString) import Data.String (fromString)
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import GHC.TypeLits (KnownSymbol, Nat, symbolVal) import GHC.TypeLits (KnownSymbol, Nat, symbolVal)
@ -63,17 +64,13 @@ instance (KnownSymbol path, HasGenRequest b) => HasGenRequest (path :> b) where
(oldf, old) = genRequest (Proxy :: Proxy b) (oldf, old) = genRequest (Proxy :: Proxy b)
new = cs $ symbolVal (Proxy :: Proxy path) new = cs $ symbolVal (Proxy :: Proxy path)
#if MIN_VERSION_servant(0,11,0)
instance HasGenRequest EmptyAPI where instance HasGenRequest EmptyAPI where
genRequest _ = (0, error "EmptyAPIs cannot be queried.") genRequest _ = (0, error "EmptyAPIs cannot be queried.")
#endif
instance HasGenRequest api => HasGenRequest (Summary d :> api) where
genRequest _ = genRequest (Proxy :: Proxy api)
instance HasGenRequest api => HasGenRequest (Description d :> api) where
genRequest _ = genRequest (Proxy :: Proxy api)
instance (Arbitrary c, HasGenRequest b, ToHttpApiData c ) instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
=> HasGenRequest (Capture' mods x c :> b) where => HasGenRequest (Capture x c :> b) where
genRequest _ = (oldf, do genRequest _ = (oldf, do
old' <- old old' <- old
new' <- toUrlPiece <$> new new' <- toUrlPiece <$> new
@ -82,6 +79,7 @@ instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
(oldf, 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)
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 _ = (oldf, do genRequest _ = (oldf, do
@ -92,12 +90,13 @@ instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
where where
(oldf, old) = genRequest (Proxy :: Proxy b) (oldf, old) = genRequest (Proxy :: Proxy b)
new = arbitrary :: Gen [c] new = arbitrary :: Gen [c]
#endif
instance (Arbitrary c, KnownSymbol h, HasGenRequest b, ToHttpApiData c) instance (Arbitrary c, KnownSymbol h, HasGenRequest b, ToHttpApiData c)
=> HasGenRequest (Header' mods h c :> b) where => HasGenRequest (Header h c :> b) where
genRequest _ = (oldf, do genRequest _ = (oldf, do
old' <- old old' <- old
new' <- toUrlPiece <$> new -- TODO: generate lenient or/and optional 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
@ -106,9 +105,9 @@ instance (Arbitrary c, KnownSymbol h, HasGenRequest b, ToHttpApiData c)
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' mods x c :> b) where => HasGenRequest (ReqBody x c :> b) where
genRequest _ = (oldf, do genRequest _ = (oldf, do
old' <- old -- TODO: generate lenient 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 {
@ -120,9 +119,9 @@ instance (AllMimeRender x c, Arbitrary c, HasGenRequest 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' mods x c :> b) where => HasGenRequest (QueryParam x c :> b) where
genRequest _ = (oldf, do genRequest _ = (oldf, do
new' <- new -- TODO: generate lenient or/and optional 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')
@ -168,15 +167,6 @@ instance (ReflectMethod method)
, method = reflectMethod (Proxy :: Proxy method) , method = reflectMethod (Proxy :: Proxy method)
}) })
instance (ReflectMethod method)
=> HasGenRequest (NoContentVerb (method :: k)) where
genRequest _ = (1, return $ \burl -> defaultRequest
{ host = cs $ baseUrlHost burl
, port = baseUrlPort burl
, secure = baseUrlScheme burl == Https
, 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

@ -11,7 +11,7 @@ import Data.CaseInsensitive (foldCase, foldedCase, mk)
import Data.Either (isRight) import Data.Either (isRight)
import Data.List.Split (wordsBy) import Data.List.Split (wordsBy)
import Data.Maybe (fromMaybe, isJust) import Data.Maybe (fromMaybe, isJust)
import Data.Semigroup (Semigroup (..)) import Data.Monoid ((<>))
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time (UTCTime, defaultTimeLocale, parseTimeM, import Data.Time (UTCTime, defaultTimeLocale, parseTimeM,
rfc822DateFormat) rfc822DateFormat)
@ -42,8 +42,9 @@ import Servant.QuickCheck.Internal.ErrorTypes
-- --
-- /Since 0.0.0.0/ -- /Since 0.0.0.0/
not500 :: ResponsePredicate not500 :: ResponsePredicate
not500 = ResponsePredicate $ \resp -> not500 = ResponsePredicate $ \req resp ->
when (responseStatus resp == status500) $ throw $ PredicateFailure "not500" Nothing resp when (responseStatus resp == status500) $
throw $ PredicateFailure "not500" req resp
-- | [__Optional__] -- | [__Optional__]
-- --
@ -58,7 +59,7 @@ notLongerThan maxAllowed
resp <- httpLbs req mgr resp <- httpLbs req mgr
end <- getTime Monotonic end <- getTime Monotonic
when (toNanoSecs (end `diffTimeSpec` start) > maxAllowed) $ when (toNanoSecs (end `diffTimeSpec` start) > maxAllowed) $
throw $ PredicateFailure "notLongerThan" (Just req) resp throw $ PredicateFailure "notLongerThan" req resp
return [] return []
-- | [__Best Practice__] -- | [__Best Practice__]
@ -84,13 +85,15 @@ notLongerThan maxAllowed
-- /Since 0.0.0.0/ -- /Since 0.0.0.0/
onlyJsonObjects :: ResponsePredicate onlyJsonObjects :: ResponsePredicate
onlyJsonObjects onlyJsonObjects
= ResponsePredicate (\resp -> do = ResponsePredicate (\req resp -> case go resp of
case lookup "content-type" (first foldedCase <$> responseHeaders resp) of Nothing -> throw $ PredicateFailure "onlyJsonObjects" req resp
Nothing -> return () Just () -> return ())
Just ctype -> when ("application/json" `SBS.isPrefixOf` ctype) $ do where
case (decode (responseBody resp) :: Maybe Object) of go r = do
Nothing -> throw $ PredicateFailure "onlyJsonObjects" Nothing resp ctyp <- lookup "content-type" (first foldedCase <$> responseHeaders r)
Just _ -> return ()) when ("application/json" `SBS.isPrefixOf` ctyp) $ do
(_ :: Object) <- decode (responseBody r)
return ()
-- | __Optional__ -- | __Optional__
-- --
@ -118,12 +121,12 @@ createContainsValidLocation
resp <- httpLbs req mgr resp <- httpLbs req mgr
if responseStatus resp == status201 if responseStatus resp == status201
then case lookup "Location" $ responseHeaders resp of then case lookup "Location" $ responseHeaders resp of
Nothing -> throw $ PredicateFailure n (Just req) resp Nothing -> throw $ PredicateFailure n req resp
Just l -> case parseRequest $ SBSC.unpack l of Just l -> case parseRequest $ SBSC.unpack l of
Nothing -> throw $ PredicateFailure n (Just req) resp Nothing -> throw $ PredicateFailure n req resp
Just x -> do Just x -> do
resp2 <- httpLbs x mgr resp2 <- httpLbs x mgr
status2XX (Just req) resp2 n status2XX req resp2 n
return [resp, resp2] return [resp, resp2]
else return [resp] else return [resp]
@ -158,8 +161,8 @@ getsHaveLastModifiedHeader
if (method req == methodGet) if (method req == methodGet)
then do then do
resp <- httpLbs req mgr resp <- httpLbs req mgr
unless (hasValidHeader "Last-Modified" isRFC822Date resp) $ do unless (hasValidHeader "Last-Modified" isRFC822Date resp) $
throw $ PredicateFailure "getsHaveLastModifiedHeader" (Just req) resp throw $ PredicateFailure "getsHaveLastModifiedHeader" req resp
return [resp] return [resp]
else return [] else return []
@ -187,15 +190,14 @@ getsHaveLastModifiedHeader
notAllowedContainsAllowHeader :: RequestPredicate notAllowedContainsAllowHeader :: RequestPredicate
notAllowedContainsAllowHeader notAllowedContainsAllowHeader
= RequestPredicate $ \req mgr -> do = RequestPredicate $ \req mgr -> do
let reqs = [ req { method = renderStdMethod m } | m <- [minBound .. maxBound] resp <- mapM (flip httpLbs mgr) $ [ req { method = renderStdMethod m }
, renderStdMethod m /= method req ] | m <- [minBound .. maxBound ]
resp <- mapM (flip httpLbs mgr) reqs , renderStdMethod m /= method req ]
case filter pred' resp of
case filter pred' (zip reqs resp) of (x:_) -> throw $ PredicateFailure "notAllowedContainsAllowHeader" req x
(x:_) -> throw $ PredicateFailure "notAllowedContainsAllowHeader" (Just $ fst x) (snd x)
[] -> return resp [] -> return resp
where where
pred' (_, resp) = responseStatus resp == status405 && not (hasValidHeader "Allow" go resp) pred' resp = responseStatus resp == status405 && not (hasValidHeader "Allow" go resp)
where where
go x = all (\y -> isRight $ parseMethod $ SBSC.pack y) go x = all (\y -> isRight $ parseMethod $ SBSC.pack y)
$ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x) $ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x)
@ -225,7 +227,7 @@ honoursAcceptHeader
sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req) sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
if status100 < scode && scode < status300 if status100 < scode && scode < status300
then if isJust $ sctype >>= \x -> matchAccept [x] sacc then if isJust $ sctype >>= \x -> matchAccept [x] sacc
then throw $ PredicateFailure "honoursAcceptHeader" (Just req) resp then throw $ PredicateFailure "honoursAcceptHeader" req resp
else return [resp] else return [resp]
else return [resp] else return [resp]
@ -250,8 +252,8 @@ getsHaveCacheControlHeader
if (method req == methodGet) if (method req == methodGet)
then do then do
resp <- httpLbs req mgr resp <- httpLbs req mgr
unless (hasValidHeader "Cache-Control" (const True) resp) $ do unless (hasValidHeader "Cache-Control" (const True) resp) $
throw $ PredicateFailure "getsHaveCacheControlHeader" (Just req) resp throw $ PredicateFailure "getsHaveCacheControlHeader" req resp
return [resp] return [resp]
else return [] else return []
@ -267,7 +269,7 @@ headsHaveCacheControlHeader
then do then do
resp <- httpLbs req mgr resp <- httpLbs req mgr
unless (hasValidHeader "Cache-Control" (const True) resp) $ unless (hasValidHeader "Cache-Control" (const True) resp) $
throw $ PredicateFailure "headsHaveCacheControlHeader" (Just req) resp throw $ PredicateFailure "headsHaveCacheControlHeader" req resp
return [resp] return [resp]
else return [] else return []
{- {-
@ -333,10 +335,10 @@ linkHeadersAreValid
-- /Since 0.0.0.0/ -- /Since 0.0.0.0/
unauthorizedContainsWWWAuthenticate :: ResponsePredicate unauthorizedContainsWWWAuthenticate :: ResponsePredicate
unauthorizedContainsWWWAuthenticate unauthorizedContainsWWWAuthenticate
= ResponsePredicate $ \resp -> = ResponsePredicate $ \req resp ->
if responseStatus resp == status401 if responseStatus resp == status401
then unless (hasValidHeader "WWW-Authenticate" (const True) resp) $ then unless (hasValidHeader "WWW-Authenticate" (const True) resp) $
throw $ PredicateFailure "unauthorizedContainsWWWAuthenticate" Nothing resp throw $ PredicateFailure "unauthorizedContainsWWWAuthenticate" req resp
else return () else return ()
@ -353,12 +355,12 @@ unauthorizedContainsWWWAuthenticate
-- /Since 0.3.0.0/ -- /Since 0.3.0.0/
htmlIncludesDoctype :: ResponsePredicate htmlIncludesDoctype :: ResponsePredicate
htmlIncludesDoctype htmlIncludesDoctype
= ResponsePredicate $ \resp -> = ResponsePredicate $ \req resp ->
if hasValidHeader "Content-Type" (SBS.isPrefixOf . foldCase $ "text/html") resp if hasValidHeader "Content-Type" (SBS.isPrefixOf . foldCase $ "text/html") resp
then do then do
let htmlContent = foldCase . LBS.take 20 $ responseBody resp let htmlContent = foldCase . LBS.take 20 $ responseBody resp
unless (LBS.isPrefixOf (foldCase "<!doctype html>") htmlContent) $ unless (LBS.isPrefixOf (foldCase "<!doctype html>") htmlContent) $
throw $ PredicateFailure "htmlIncludesDoctype" Nothing resp throw $ PredicateFailure "htmlIncludesDoctype" req resp
else return () else return ()
-- * Predicate logic -- * Predicate logic
@ -373,15 +375,12 @@ htmlIncludesDoctype
-- --
-- /Since 0.0.0.0/ -- /Since 0.0.0.0/
newtype ResponsePredicate = ResponsePredicate newtype ResponsePredicate = ResponsePredicate
{ getResponsePredicate :: Response LBS.ByteString -> IO () { getResponsePredicate :: Request -> Response LBS.ByteString -> IO ()
} deriving (Generic) } deriving (Generic)
instance Semigroup ResponsePredicate where
ResponsePredicate a <> ResponsePredicate b = ResponsePredicate $ \x -> a x >> b x
instance Monoid ResponsePredicate where instance Monoid ResponsePredicate where
mempty = ResponsePredicate $ const $ return () mempty = ResponsePredicate (\req resp -> return ())
mappend = (<>) ResponsePredicate a `mappend` ResponsePredicate b = ResponsePredicate $ \x y -> a x y >> b x y
-- | A predicate that depends on both the request and the response. -- | A predicate that depends on both the request and the response.
-- --
@ -393,11 +392,7 @@ newtype RequestPredicate = RequestPredicate
-- TODO: This isn't actually a monoid -- TODO: This isn't actually a monoid
instance Monoid RequestPredicate where instance Monoid RequestPredicate where
mempty = RequestPredicate (\r m -> httpLbs r m >>= \x -> return ([x])) mempty = RequestPredicate (\r m -> httpLbs r m >>= \x -> return ([x]))
mappend = (<>) RequestPredicate a `mappend` RequestPredicate b = RequestPredicate $ \r mgr ->
-- TODO: This isn't actually a monoid
instance Semigroup RequestPredicate where
RequestPredicate a <> RequestPredicate b = RequestPredicate $ \r mgr ->
liftM2 (<>) (a r mgr) (b r mgr) liftM2 (<>) (a r mgr) (b r mgr)
-- | A set of predicates. Construct one with 'mempty' and '<%>'. -- | A set of predicates. Construct one with 'mempty' and '<%>'.
@ -406,13 +401,10 @@ data Predicates = Predicates
, responsePredicates :: ResponsePredicate , responsePredicates :: ResponsePredicate
} deriving (Generic) } deriving (Generic)
instance Semigroup Predicates where
a <> b = Predicates (requestPredicates a <> requestPredicates b)
(responsePredicates a <> responsePredicates b)
instance Monoid Predicates where instance Monoid Predicates where
mempty = Predicates mempty mempty mempty = Predicates mempty mempty
mappend = (<>) a `mappend` b = Predicates (requestPredicates a <> requestPredicates b)
(responsePredicates a <> responsePredicates b)
class JoinPreds a where class JoinPreds a where
joinPreds :: a -> Predicates -> Predicates joinPreds :: a -> Predicates -> Predicates
@ -438,7 +430,8 @@ finishPredicates p req mgr = go `catch` \(e :: PredicateFailure) -> return $ Jus
where where
go = do go = do
resps <- getRequestPredicate (requestPredicates p) req mgr resps <- getRequestPredicate (requestPredicates p) req mgr
mapM_ (getResponsePredicate $ responsePredicates p) resps let responder = getResponsePredicate (responsePredicates p) req
mapM_ responder resps
return Nothing return Nothing
-- * helpers -- * helpers
@ -454,8 +447,8 @@ isRFC822Date s
Nothing -> False Nothing -> False
Just (_ :: UTCTime) -> True Just (_ :: UTCTime) -> True
status2XX :: Monad m => Maybe Request -> Response LBS.ByteString -> T.Text -> m () status2XX :: Monad m => Request -> Response LBS.ByteString -> T.Text -> m ()
status2XX mreq resp t status2XX req resp t
| status200 <= responseStatus resp && responseStatus resp < status300 | status200 <= responseStatus resp && responseStatus resp < status300
= return () = return ()
| otherwise = throw $ PredicateFailure t mreq resp | otherwise = throw $ PredicateFailure t req resp

View File

@ -2,7 +2,7 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
module Servant.QuickCheck.Internal.QuickCheck where module Servant.QuickCheck.Internal.QuickCheck where
import Control.Concurrent (tryReadMVar, newEmptyMVar, tryPutMVar) import Control.Concurrent (modifyMVar_, newMVar, readMVar)
import Control.Monad (unless) import Control.Monad (unless)
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import Data.Proxy (Proxy) import Data.Proxy (Proxy)
@ -11,9 +11,6 @@ import Network.Wai.Handler.Warp (withApplication)
import Prelude.Compat import Prelude.Compat
import Servant (Context (EmptyContext), HasServer, import Servant (Context (EmptyContext), HasServer,
Server, serveWithContext) Server, serveWithContext)
#if MIN_VERSION_servant_server(0,18,0)
import Servant (DefaultErrorFormatters, ErrorFormatters, HasContextEntry, type (.++))
#endif
import Servant.Client (BaseUrl (..), Scheme (..)) import Servant.Client (BaseUrl (..), Scheme (..))
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
import Test.Hspec (Expectation, expectationFailure) import Test.Hspec (Expectation, expectationFailure)
@ -40,11 +37,7 @@ withServantServer api = withServantServerAndContext api EmptyContext
-- application. -- application.
-- --
-- /Since 0.0.0.0/ -- /Since 0.0.0.0/
#if MIN_VERSION_servant_server(0,18,0)
withServantServerAndContext :: (HasServer a ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters)
#else
withServantServerAndContext :: HasServer a ctx withServantServerAndContext :: HasServer a ctx
#endif
=> Proxy a -> Context ctx -> IO (Server a) -> (BaseUrl -> IO r) -> IO r => Proxy a -> Context ctx -> IO (Server a) -> (BaseUrl -> IO r) -> IO r
withServantServerAndContext api ctx server t withServantServerAndContext api ctx server t
= withApplication (return . serveWithContext api ctx =<< server) $ \port -> = withApplication (return . serveWithContext api ctx =<< server) $ \port ->
@ -80,29 +73,21 @@ serversEqual api burl1 burl2 args req = do
let reqs = (\f -> (f burl1, f burl2)) <$> runGenRequest 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 <- newEmptyMVar deetsMVar <- newMVar $ error "should not be called"
r <- quickCheckWithResult args { chatty = False } $ monadicIO $ forAllM reqs $ \(req1, req2) -> do r <- quickCheckWithResult args { chatty = False } $ monadicIO $ forAllM reqs $ \(req1, req2) -> do
resp1 <- run $ C.httpLbs (noCheckStatus req1) defManager resp1 <- run $ C.httpLbs (noCheckStatus req1) defManager
resp2 <- run $ C.httpLbs (noCheckStatus req2) defManager resp2 <- run $ C.httpLbs (noCheckStatus req2) defManager
unless (getResponseEquality req resp1 resp2) $ do unless (getResponseEquality req resp1 resp2) $ do
monitor (counterexample "hi" ) monitor (counterexample "hi" )
_ <- run $ tryPutMVar deetsMVar $ ServerEqualityFailure req1 resp1 resp2 run $ modifyMVar_ deetsMVar $ const $ return $
ServerEqualityFailure req1 resp1 resp2
assert False assert False
case r of case r of
Success {} -> return () Success {} -> return ()
Failure{..} -> do Failure{..} -> readMVar deetsMVar >>= \x -> expectationFailure $ "Failed:\n" ++ show x
mx <- tryReadMVar deetsMVar
case mx of
Just x ->
expectationFailure $ "Failed:\n" ++ show x
Nothing ->
expectationFailure $ "We failed to record a reason for failure: " <> show r
GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests" GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
NoExpectedFailure {} -> expectationFailure "No expected failure" NoExpectedFailure {} -> expectationFailure "No expected failure"
#if MIN_VERSION_QuickCheck(2,12,0)
#else
InsufficientCoverage {} -> expectationFailure "Insufficient coverage" InsufficientCoverage {} -> expectationFailure "Insufficient coverage"
#endif
-- | Check that a server satisfies the set of properties specified. -- | Check that a server satisfies the set of properties specified.
-- --
@ -125,61 +110,37 @@ serversEqual api burl1 burl2 args req = do
-- /Since 0.0.0.0/ -- /Since 0.0.0.0/
serverSatisfies :: (HasGenRequest a) => serverSatisfies :: (HasGenRequest a) =>
Proxy a -> BaseUrl -> Args -> Predicates -> Expectation Proxy a -> BaseUrl -> Args -> Predicates -> Expectation
serverSatisfies api = serverSatisfiesMgr api defManager serverSatisfies api burl args preds = do
-- | Check that a server satisfies the set of properties specified, and
-- accept a 'Manager' for running the HTTP requests through.
--
-- See 'serverSatisfies' for more details.
--
-- @since 0.0.7.2
serverSatisfiesMgr :: (HasGenRequest a) =>
Proxy a -> C.Manager -> BaseUrl -> Args -> Predicates -> Expectation
serverSatisfiesMgr api manager burl args preds = do
let reqs = ($ burl) <$> runGenRequest api let reqs = ($ burl) <$> runGenRequest api
deetsMVar <- newEmptyMVar 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) manager v <- run $ finishPredicates preds (noCheckStatus req) defManager
_ <- run $ tryPutMVar deetsMVar v run $ modifyMVar_ deetsMVar $ const $ return v
case v of case v of
Just _ -> assert False Just _ -> assert False
_ -> return () _ -> return ()
case r of case r of
Success {} -> return () Success {} -> return ()
Failure {..} -> do Failure{..} -> readMVar deetsMVar >>= \x -> expectationFailure $
mx <- tryReadMVar deetsMVar "Failed:\n" ++ show x
case mx of
Just x ->
expectationFailure $ "Failed:\n" ++ show x
Nothing ->
expectationFailure $ "We failed to record a reason for failure: " <> show r
GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests" GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
NoExpectedFailure {} -> expectationFailure $ "No expected failure" NoExpectedFailure {} -> expectationFailure $ "No expected failure"
#if MIN_VERSION_QuickCheck(2,12,0) InsufficientCoverage {} -> expectationFailure $ "Insufficient coverage"
#else
InsufficientCoverage {} -> expectationFailure "Insufficient coverage"
#endif
serverDoesntSatisfy :: (HasGenRequest a) => serverDoesntSatisfy :: (HasGenRequest a) =>
Proxy a -> BaseUrl -> Args -> Predicates -> Expectation Proxy a -> BaseUrl -> Args -> Predicates -> Expectation
serverDoesntSatisfy api = serverDoesntSatisfyMgr api defManager serverDoesntSatisfy api burl args preds = do
serverDoesntSatisfyMgr :: (HasGenRequest a) =>
Proxy a -> C.Manager -> BaseUrl -> Args -> Predicates -> Expectation
serverDoesntSatisfyMgr api manager burl args preds = do
let reqs = ($ burl) <$> runGenRequest 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) manager v <- run $ finishPredicates preds (noCheckStatus req) defManager
assert $ not $ null v assert $ not $ 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"
Failure { output = m } -> expectationFailure $ "Failed:\n" ++ show m Failure { output = m } -> expectationFailure $ "Failed:\n" ++ show m
NoExpectedFailure {} -> expectationFailure $ "No expected failure" NoExpectedFailure {} -> expectationFailure $ "No expected failure"
#if MIN_VERSION_QuickCheck(2,12,0) InsufficientCoverage {} -> expectationFailure $ "Insufficient coverage"
#else
InsufficientCoverage {} -> expectationFailure "Insufficient coverage"
#endif
noCheckStatus :: C.Request -> C.Request noCheckStatus :: C.Request -> C.Request
#if MIN_VERSION_http_client(0,5,0) #if MIN_VERSION_http_client(0,5,0)

6
stack-lts-6.yaml Normal file
View File

@ -0,0 +1,6 @@
resolver: lts-6.30
packages:
- '.'
extra-deps: []
flags: {}
extra-package-dbs: []

6
stack-lts-7.yaml Normal file
View File

@ -0,0 +1,6 @@
resolver: lts-7.19
packages:
- '.'
extra-deps: []
flags: {}
extra-package-dbs: []

6
stack-lts-9.yaml Normal file
View File

@ -0,0 +1,6 @@
resolver: lts-9.1
packages:
- '.'
extra-deps: []
flags: {}
extra-package-dbs: []

View File

@ -1,10 +1,10 @@
resolver: nightly-2018-09-03 resolver: lts-8.4
packages: packages:
- '.' - '.'
extra-deps: extra-deps:
- hspec-discover-2.5.6 - hspec-2.4.4
- hspec-core-2.5.6 - hspec-core-2.4.4
- hspec-2.5.6 - hspec-discover-2.4.4
- QuickCheck-2.12 - quickcheck-io-0.2.0
flags: {} flags: {}
extra-package-dbs: [] extra-package-dbs: []

44
test/Doctest.hs Normal file
View File

@ -0,0 +1,44 @@
module Main where
import Data.List (isPrefixOf)
import System.Directory
import System.FilePath
import System.FilePath.Find
import Test.DocTest
main :: IO ()
main = do
files <- find always (extension ==? ".hs") "src"
mCabalMacrosFile <- getCabalMacrosFile
doctest $ "-isrc" : "-Iinclude" :
(maybe [] (\ f -> ["-optP-include", "-optP" ++ f]) mCabalMacrosFile) ++
"-XOverloadedStrings" :
"-XDeriveFunctor" :
"-XFlexibleInstances" :
"-XFlexibleContexts" :
"-XMultiParamTypeClasses" :
"-XDataKinds" :
"-XTypeOperators" :
"-XGADTs" :
files
getCabalMacrosFile :: IO (Maybe FilePath)
getCabalMacrosFile = do
exists <- doesDirectoryExist "dist"
if exists
then do
contents <- getDirectoryContents "dist"
let rest = "build" </> "autogen" </> "cabal_macros.h"
whenExists $ case filter ("dist-sandbox-" `isPrefixOf`) contents of
[x] -> "dist" </> x </> rest
[] -> "dist" </> rest
xs -> error $ "ran doctests with multiple dist/dist-sandbox-xxxxx's: \n"
++ show xs ++ "\nTry cabal clean"
else return Nothing
where
whenExists :: FilePath -> IO (Maybe FilePath)
whenExists file = do
exists <- doesFileExist file
return $ if exists
then Just file
else Nothing

View File

@ -17,13 +17,25 @@ import qualified Text.Blaze.Html as Blaze
import qualified Text.Blaze.Html5 as Blaze5 import qualified Text.Blaze.Html5 as Blaze5
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 (..), ResultStatus (..), import Test.Hspec.Core.Spec (Arg, Example, Result (..),
defaultParams, safeEvaluateExample) defaultParams)
import Test.QuickCheck.Gen (generate, unGen) import Test.QuickCheck.Gen (generate, unGen)
import Test.QuickCheck.Random (mkQCGen) import Test.QuickCheck.Random (mkQCGen)
import Servant.Test.ComprehensiveAPI (comprehensiveAPIWithoutStreamingOrRaw) #if MIN_VERSION_servant(0,8,0)
import Servant.API.Internal.Test.ComprehensiveAPI (comprehensiveAPIWithoutRaw)
#else
import Servant.API.Internal.Test.ComprehensiveAPI (ComprehensiveAPI,
comprehensiveAPI)
#endif
#if MIN_VERSION_hspec(2,4,0)
import Test.Hspec.Core.Spec (safeEvaluateExample)
#else
import Control.Exception (try)
import Test.Hspec.Core.Spec (evaluateExample)
#endif
import Servant.QuickCheck import Servant.QuickCheck
import Servant.QuickCheck.Internal (genRequest, runGenRequest, import Servant.QuickCheck.Internal (genRequest, runGenRequest,
@ -34,11 +46,13 @@ spec = do
serversEqualSpec serversEqualSpec
serverSatisfiesSpec serverSatisfiesSpec
isComprehensiveSpec isComprehensiveSpec
no500s
onlyJsonObjectSpec onlyJsonObjectSpec
notLongerThanSpec notLongerThanSpec
queryParamsSpec queryParamsSpec
queryFlagsSpec queryFlagsSpec
deepPathSpec deepPathSpec
authServerCheck
htmlDocTypesSpec htmlDocTypesSpec
unbiasedGenerationSpec unbiasedGenerationSpec
@ -115,6 +129,15 @@ serverSatisfiesSpec = describe "serverSatisfies" $ do
show err `shouldContain` "Body" show err `shouldContain` "Body"
no500s :: Spec
no500s = describe "no500s" $ do
it "fails correctly" $ do
FailedWith err <- withServantServerAndContext api2 ctx server500fail $ \burl -> do
evalExample $ serverSatisfies api2 burl args
(not500 <%> mempty)
show err `shouldContain` "not500"
onlyJsonObjectSpec :: Spec onlyJsonObjectSpec :: Spec
onlyJsonObjectSpec = describe "onlyJsonObjects" $ do onlyJsonObjectSpec = describe "onlyJsonObjects" $ do
@ -128,10 +151,6 @@ onlyJsonObjectSpec = describe "onlyJsonObjects" $ do
withServantServerAndContext octetAPI ctx serverOctetAPI $ \burl -> withServantServerAndContext octetAPI ctx serverOctetAPI $ \burl ->
serverSatisfies octetAPI burl args (onlyJsonObjects <%> mempty) serverSatisfies octetAPI burl args (onlyJsonObjects <%> mempty)
it "does not fail when there is no content-type" $ do
withServantServerAndContext api2 ctx serverFailing $ \burl ->
serverSatisfies api2 burl args (onlyJsonObjects <%> mempty)
notLongerThanSpec :: Spec notLongerThanSpec :: Spec
notLongerThanSpec = describe "notLongerThan" $ do notLongerThanSpec = describe "notLongerThan" $ do
@ -149,7 +168,7 @@ isComprehensiveSpec :: Spec
isComprehensiveSpec = describe "HasGenRequest" $ do isComprehensiveSpec = describe "HasGenRequest" $ do
it "has instances for all 'servant' combinators" $ do it "has instances for all 'servant' combinators" $ do
let _g = genRequest comprehensiveAPIWithoutStreamingOrRaw let _g = genRequest comprehensiveAPIWithoutRaw
True `shouldBe` True -- This is a type-level check True `shouldBe` True -- This is a type-level check
deepPathSpec :: Spec deepPathSpec :: Spec
@ -185,6 +204,17 @@ queryFlagsSpec = describe "QueryFlags" $ do
qs = C.unpack $ queryString req qs = C.unpack $ queryString req
qs `shouldBe` "one&two" qs `shouldBe` "one&two"
authServerCheck :: Spec
authServerCheck = describe "authenticate endpoints" $ do
it "authorization failure without WWWAuthenticate header fails correctly" $ do
FailedWith err <- withServantServerAndContext api2 ctx authFailServer $ \burl -> do
evalExample $ serverSatisfies api2 burl args
(unauthorizedContainsWWWAuthenticate <%> mempty)
show err `shouldContain` "unauthorizedContainsWWWAuthenticate"
-- Large API Randomness Testing Helper
htmlDocTypesSpec :: Spec htmlDocTypesSpec :: Spec
htmlDocTypesSpec = describe "HtmlDocTypes" $ do htmlDocTypesSpec = describe "HtmlDocTypes" $ do
@ -209,7 +239,6 @@ makeRandomRequest large burl = do
req <- generate $ runGenRequest large req <- generate $ runGenRequest large
pure $ fst . fromJust . C.readInteger . C.drop 1 . path $ req burl pure $ fst . fromJust . C.readInteger . C.drop 1 . path $ req burl
unbiasedGenerationSpec :: Spec unbiasedGenerationSpec :: Spec
unbiasedGenerationSpec = describe "Unbiased Generation of requests" $ unbiasedGenerationSpec = describe "Unbiased Generation of requests" $
@ -266,15 +295,17 @@ type DeepAPI = "one" :> "two" :> "three":> Get '[JSON] ()
deepAPI :: Proxy DeepAPI deepAPI :: Proxy DeepAPI
deepAPI = Proxy deepAPI = Proxy
server2 :: IO (Server API2) server2 :: IO (Server API2)
server2 = return $ return 1 server2 = return $ return 1
server3 :: IO (Server API2) server3 :: IO (Server API2)
server3 = return $ return 2 server3 = return $ return 2
serverFailing :: IO (Server API2) server500fail :: IO (Server API2)
serverFailing = return . throwError $ err405 server500fail = return $ throwError $ err500 { errBody = "BOOM!" }
authFailServer :: IO (Server API2)
authFailServer = return $ throwError $ err401 { errBody = "Login failure but missing header"}
-- With Doctypes -- With Doctypes
type HtmlDoctype = Get '[HTML] Blaze.Html type HtmlDoctype = Get '[HTML] Blaze.Html
@ -288,7 +319,6 @@ docTypeServer = pure $ pure $ Blaze5.docTypeHtml $ Blaze5.span "Hello Test!"
noDocTypeServer :: IO (Server HtmlDoctype) noDocTypeServer :: IO (Server HtmlDoctype)
noDocTypeServer = pure $ pure $ Blaze.text "Hello Test!" noDocTypeServer = pure $ pure $ Blaze.text "Hello Test!"
-- Api for unbiased generation of requests tests -- Api for unbiased generation of requests tests
largeApi :: Proxy LargeAPI largeApi :: Proxy LargeAPI
largeApi = Proxy largeApi = Proxy
@ -344,14 +374,27 @@ ctx = BasicAuthCheck (const . return $ NoSuchUser) :. EmptyContext
-- Utils -- Utils
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
evalExample :: (Example e, Arg e ~ ()) => e -> IO EvalResult evalExample :: (Example e, Arg e ~ ()) => e -> IO EvalResult
#if MIN_VERSION_hspec(2,4,0)
evalExample e = do evalExample e = do
r <- safeEvaluateExample e defaultParams ($ ()) progCallback r <- safeEvaluateExample e defaultParams ($ ()) progCallback
case resultStatus r of case r of
Success -> return $ AllGood Left err -> return $ AnException err
Failure _ reason -> return $ FailedWith $ show reason Right Success -> return $ AllGood
Pending {} -> error "should not happen" Right (Failure _ reason) -> return $ FailedWith $ show reason
Right (Pending _) -> error "should not happen"
where where
progCallback _ = return () progCallback _ = return ()
#else
evalExample e = do
r <- try $ evaluateExample e defaultParams ($ ()) progCallback
case r of
Left err -> return $ AnException err
Right Success -> return $ AllGood
Right (Fail _ reason) -> return $ FailedWith reason
Right (Pending _) -> error "should not happen"
where
progCallback _ = return ()
#endif
data EvalResult data EvalResult
= AnException SomeException = AnException SomeException
@ -369,3 +412,8 @@ noOfTestCases = 20000
#else #else
noOfTestCases = 1000 noOfTestCases = 1000
#endif #endif
#if !MIN_VERSION_servant(0,8,0)
comprehensiveAPIWithoutRaw :: Proxy ComprehensiveAPI
comprehensiveAPIWithoutRaw = comprehensiveAPI
#endif