git.iko.soy/servant-quickcheck
on 70fed09 help sign in
.gitignore−1
since splitsnapshots
unifiedsplit
@@ -3,4 +3,3 @@ scripts/
3samples/
3samples/
4test-servers/
4test-servers/
5/doc/
5/doc/
6−.stack-work/
.travis.yml+30−154
@@ -1,158 +1,34 @@
1−# This Travis job script has been generated by a script via
1+sudo: false
2−#
2+
3−# haskell-ci '--config=cabal.haskell-ci' 'servant-quickcheck.cabal'
4−#
5−# To regenerate the script (for example after adjusting tested-with) run
6−#
7−# haskell-ci regenerate
8−#
9−# For more information, see https://github.com/haskell-CI/haskell-ci
10−#
11−# version: 0.10.1
12−#
13−version: ~> 1.0
14language: c
3language: c
15−os: linux
4+
16−dist: xenial
5+env:
17−git:
6+ - GHCVER=7.8.4
18− # whether to recursively clone submodules
7+ - GHCVER=7.10.2
19− submodules: false
8+
20−branches:
9+addons:
21− only:
10+ apt:
22− - master
11+ sources:
23−cache:
12+ - hvr-ghc
24− directories:
13+ packages:
25− - $HOME/.cabal/packages
14+ - ghc-7.8.4
26− - $HOME/.cabal/store
15+ - ghc-7.10.2
27− - $HOME/.hlint
16+ - cabal-install-1.22
28−before_cache:
17+ - libgmp-dev
29− - rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log
18+ - wrk
30− # remove files that are regenerated by 'cabal update'
19+
31− - rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.*
32− - rm -fv $CABALHOME/packages/hackage.haskell.org/*.json
33− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache
34− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar
35− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx
36− - rm -rfv $CABALHOME/packages/head.hackage
37−jobs:
38− include:
39− - compiler: ghc-8.8.3
40− 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"]}}
41− os: linux
42− - compiler: ghc-8.6.5
43− 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"]}}
44− os: linux
45− - compiler: ghc-8.4.4
46− 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"]}}
47− os: linux
48− - compiler: ghc-8.2.2
49− 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"]}}
50− os: linux
51− - compiler: ghc-8.0.2
52− 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"]}}
53− os: linux
54−before_install:
55− - HC=$(echo "/opt/$CC/bin/ghc" | sed 's/-/\//')
56− - WITHCOMPILER="-w $HC"
57− - HADDOCK=$(echo "/opt/$CC/bin/haddock" | sed 's/-/\//')
58− - HCPKG="$HC-pkg"
59− - unset CC
60− - CABAL=/opt/ghc/bin/cabal
61− - CABALHOME=$HOME/.cabal
62− - export PATH="$CABALHOME/bin:$PATH"
63− - TOP=$(pwd)
64− - "HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\\d+)\\.(\\d+)\\.(\\d+)(\\.(\\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')"
65− - echo $HCNUMVER
66− - CABAL="$CABAL -vnormal+nowrap"
67− - set -o pipefail
68− - TEST=--enable-tests
69− - BENCH=--enable-benchmarks
70− - HEADHACKAGE=false
71− - rm -f $CABALHOME/config
72− - |
73− echo "verbose: normal +nowrap +markoutput" >> $CABALHOME/config
74− echo "remote-build-reporting: anonymous" >> $CABALHOME/config
75− echo "write-ghc-environment-files: always" >> $CABALHOME/config
76− echo "remote-repo-cache: $CABALHOME/packages" >> $CABALHOME/config
77− echo "logs-dir: $CABALHOME/logs" >> $CABALHOME/config
78− echo "world-file: $CABALHOME/world" >> $CABALHOME/config
79− echo "extra-prog-path: $CABALHOME/bin" >> $CABALHOME/config
80− echo "symlink-bindir: $CABALHOME/bin" >> $CABALHOME/config
81− echo "installdir: $CABALHOME/bin" >> $CABALHOME/config
82− echo "build-summary: $CABALHOME/logs/build.log" >> $CABALHOME/config
83− echo "store-dir: $CABALHOME/store" >> $CABALHOME/config
84− echo "install-dirs user" >> $CABALHOME/config
85− echo " prefix: $CABALHOME" >> $CABALHOME/config
86− echo "repository hackage.haskell.org" >> $CABALHOME/config
87− echo " url: http://hackage.haskell.org/" >> $CABALHOME/config
88install:
20install:
89− - ${CABAL} --version
21+ - (mkdir -p $HOME/.local/bin && cd $HOME/.local/bin && wget https://zalora-public.s3.amazonaws.com/tinc && chmod +x tinc)
90− - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
22+ - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/1.22/bin:$PATH
91− - |
23+ - ghc --version
92− echo "program-default-options" >> $CABALHOME/config
24+ - cabal --version
93− echo " ghc-options: $GHCJOBS +RTS -M6G -RTS" >> $CABALHOME/config
94− - cat $CABALHOME/config
95− - rm -fv cabal.project cabal.project.local cabal.project.freeze
96− - travis_retry ${CABAL} v2-update -v
25+ - travis_retry cabal update
97− # Generate cabal.project
26+ - sed -i 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config
98− - rm -rf cabal.project cabal.project.local cabal.project.freeze
27+
99− - touch cabal.project
100− - |
101− echo "packages: ." >> cabal.project
102− - if [ $HCNUMVER -ge 80200 ] ; then echo 'package servant-quickcheck' >> cabal.project ; fi
103− - "if [ $HCNUMVER -ge 80200 ] ; then echo ' ghc-options: -Werror=missing-methods' >> cabal.project ; fi"
104− - |
105− - "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"
106− - cat cabal.project || true
107− - cat cabal.project.local || true
108− - if [ -f "./configure.ac" ]; then (cd "." && autoreconf -i); fi
109− - ${CABAL} v2-freeze $WITHCOMPILER ${TEST} ${BENCH}
110− - "cat cabal.project.freeze | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'"
111− - rm cabal.project.freeze
112− - travis_wait 40 ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} --dep -j2 all
113− - travis_wait 40 ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --dep -j2 all
114script:
28script:
115− - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
29+ - tinc && cabal configure --enable-tests && cabal build && cabal test
116− # Packaging...
30+ #- (cd doc && tinc cabal configure --enable-tests && cabal build && cabal test)
117− - ${CABAL} v2-sdist all
118− # Unpacking...
119− - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
120− - cd ${DISTDIR} || false
121− - find . -maxdepth 1 -type f -name '*.tar.gz' -exec tar -xvf '{}' \;
122− - find . -maxdepth 1 -type f -name '*.tar.gz' -exec rm '{}' \;
123− - PKGDIR_servant_quickcheck="$(find . -maxdepth 1 -type d -regex '.*/servant-quickcheck-[0-9.]*')"
124− # Generate cabal.project
125− - rm -rf cabal.project cabal.project.local cabal.project.freeze
126− - touch cabal.project
127− - |
128− echo "packages: ${PKGDIR_servant_quickcheck}" >> cabal.project
129− - if [ $HCNUMVER -ge 80200 ] ; then echo 'package servant-quickcheck' >> cabal.project ; fi
130− - "if [ $HCNUMVER -ge 80200 ] ; then echo ' ghc-options: -Werror=missing-methods' >> cabal.project ; fi"
131− - |
132− - "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"
133− - cat cabal.project || true
134− - cat cabal.project.local || true
135− # Building...
136− # this builds all libraries and executables (without tests/benchmarks)
137− - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all
138− # Building with tests and benchmarks...
139− # build & run tests, build benchmarks
140− - ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} all
141− # Testing...
142− - ${CABAL} v2-test $WITHCOMPILER ${TEST} ${BENCH} all
143− # cabal check...
144− - (cd ${PKGDIR_servant_quickcheck} && ${CABAL} -vnormal check)
145− # haddock...
146− - ${CABAL} v2-haddock $WITHCOMPILER --with-haddock $HADDOCK ${TEST} ${BENCH} all
147− # Building without installed constraints for packages in global-db...
148− - rm -f cabal.project.local
149− - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all
150− # Constraint sets
151− - rm -rf cabal.project.local
152− # Constraint set base-compat-0.10
153− - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --constraint='base-compat ==0.10.*' all
154− # Constraint set base-compat-0.11
155− - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --constraint='base-compat ==0.11.*' all
156
31
157−# REGENDATA ("0.10.1",["--config=cabal.haskell-ci","servant-quickcheck.cabal"])
32+cache:
158−# EOF
33+ directories:
34+ - $HOME/.tinc/cache
CHANGELOG.yamldeleted−214
@@ -1,214 +0,0 @@
1−releases:
2− - version: "0.0.9.0"
3− changes:
4− - description: "Support servant-0.15 (#65)"
5− authors: fizruk
6− date: 2020-06-25
7−
8− - description: "Relax constraints for GHC 8.10 (#70)"
9− authors: felixmulder
10− date: 2020-06-20
11−
12− - version: "0.0.8.0"
13− changes:
14− - description: Support for servant-0.17
15− authors: phadej
16− date: 2019-01-23
17−
18− - version: "0.0.7.3"
19− changes:
20− - description: Support for servant-0.14
21− issue: 53
22− authors: phadej
23− date: 2018-06-12
24−
25− - description: Fix a failure from OnlyJsonObjects when there is no content-type.
26− issue: 55
27− authors: Phenitei
28− date: 2018-08-27
29−
30− - description: A bug fix where notAllowedContainsAllowHeader would print the initial request alongside the failure instead of the request causing the failure when it failed.
31− issue: 57
32− authors: Phenitei
33− date: 2018-08-29
34−
35− - description: QuickCheck 2.12 compatibility
36− issue: 58
37− authors: parsonsmatt
38− date: 2018-10-12
39−
40− - description: GHC 8.6 compatibility
41− issue: 59
42− authors: phadej
43− date: 2018-10-15
44−
45− - version: "0.0.7.2"
46− changes:
47−
48− - description: Allow client to pass an HTTP Manager in to functions
49− issue: 47
50− authors: parsonsmatt
51− date: 2018-05-10
52−
53− - description: Fix "should not happen" error when exceptions are thrown
54− issue: 48
55− authors: parsonsmatt
56− date: 2018-05-10
57−
58− - version: "0.0.7.0"
59− changes:
60−
61− - description: Support for base-compat-0.10
62− issue: none
63− authors: phadej
64− date: 2018-04-12
65−
66− - version: "0.0.7.0"
67− changes:
68−
69− - description: Support for GHC-8.4.1
70− issue: none
71− authors: phadej
72− date: 2018-03-23
73−
74− - description: Requires hspec-2.5
75− issue: none
76− authors: phadej
77− date: 2018-03-23
78−
79− - version: "0.0.6.0"
80− changes:
81−
82− - description: Support for servant-0.12
83− issue: none
84− authors: phadej
85− date: 2018-02-09
86−
87− - version: "0.0.5.0"
88− changes:
89−
90− - description: Export forgotten predicates
91− issue: none
92− pr: 40
93− authors: Phenitei
94− date: 2017-12-14
95−
96− - version: "0.0.4"
97− changes:
98−
99− - description: Support for Servant 0.12
100− issue: none
101− authors: phadej
102− date: 2017-11-07
103−
104− - description: Support for Servant 0.11
105− issue: none
106− pr: 32
107− authors: adinapoli-iohk
108− date: 2017-10-18
109− notes: Includes 0-weighted instance for EmptyAPI
110−
111− - version: "0.0.3.0"
112− changes:
113−
114− - description: Add jsonEquality
115− issue: 2
116− pr: 30
117− authors: erewok
118− date: 2017-10-15
119−
120− - description: Support hspec >= 2.4
121− issue: 27
122− pr: 29
123− authors: erewok, jkarni
124− date: 2017-10-15
125−
126− - version: "0.0.2.4"
127− changes:
128−
129− - description: Don't append slashes to paths
130− issue: 22
131− authors: declension
132− date: 2017-03-11
133−
134− - version: "0.0.2.3"
135− changes:
136−
137− - description: Fix QueryParam and QueryFlag requests
138− issue: 23
139− authors: declension
140− date: 2017-03-10
141−
142− - version: "0.0.2.2"
143− changes:
144−
145− - description: Make onlyJsonObjects succeed in non-JSON endpoints
146− issue: 20
147− authors: jkarni
148− date: 2016-10-18
149−
150−
151− - version: "0.0.2.1"
152− changes:
153−
154− - description: Add notLongerThan predicate
155− pr: 17
156− authors: jkarni
157− date: 2016-10-05
158−
159− - description: Add getsHaveLastModifiedHeader predicate
160− pr: none
161− authors: jkarni
162− date: 2016-10-03
163−
164− - description: Raise upper bounds
165− notes: >
166− For Quickcheck, aeson, http-client, servant, servant-client and
167− servant-server.
168− pr: none
169− authors: jkarni
170− date: 2016-10-03
171−
172−
173− - version: "0.0.2.0"
174− changes:
175−
176− - description: Update CHANGELOG to YAML syntax.
177− pr: 16
178− authors: jkarni
179− date: 2016-09-14
180−
181− - description: Support new CaptureAll combinator
182− pr: 16
183− authors: jkarni
184− date: 2016-09-14
185−
186− - description: Support GHC 8
187− pr: 16
188− authors: jkarni
189− date: 2016-09-14
190−
191− - version: "0.0.1.1"
192− changes:
193−
194− - description: Exclude GHC 7.8 (by bumping base lower bound to 4.8)
195− pr: none
196− authors: jkarni
197−
198− - description: More generous bounds for other packages.
199− pr: none
200− authors: jkarni
201−
202− - version: "0.0.1.0"
203− changes:
204−
205− - description: Better error messages.
206− notes: >
207− Error messages now contain failing predicate, failing response and
208− (except for response predicates), failing requests.
209− pr: none
210− authors: jkarni
211−
212− - description: Signicant changes to RequestPredicate and ResponsePredicate types.
213− pr: none
214− authors: jkarni
cabal.haskell-cideleted−7
@@ -1,7 +0,0 @@
1−branches: master
2−
3−constraint-set base-compat-0.10
4− constraints: base-compat ==0.10.*
5−
6−constraint-set base-compat-0.11
7− constraints: base-compat ==0.11.*
cabal.projectdeleted−4
@@ -1,4 +0,0 @@
1−packages: .
2−tests: true
3−
4−allow-newer: servant-blaze:servant
example/Main.hsdeleted−31
@@ -1,31 +0,0 @@
1−{-# LANGUAGE OverloadedStrings, DataKinds #-}
2−module Main (main) where
3−
4−import Servant
5−import Servant.QuickCheck
6−import Test.Hspec
7−import Data.Text (Text)
8−import System.Environment (getArgs)
9−
10−main :: IO ()
11−main = do
12− args <- getArgs
13− case args of
14− [] -> putStrLn "Not running without arguments. Try --help or --fail-fast."
15− _ -> hspec spec
16−
17−-- Change to String to reproduce
18−-- https://github.com/haskell-servant/servant-quickcheck/issues/41
19−type API = Get '[PlainText] Text
20−
21−api :: Proxy API
22−api = Proxy
23−
24−server :: Server API
25−server = return "Sigurð Fáfnirslayer"
26−
27−spec :: Spec
28−spec = describe "example server" $
29− it "mangles UTF-8 in error messages" $
30− withServantServer api (return server) $ \burl ->
31− serverSatisfies api burl defaultArgs (getsHaveCacheControlHeader <%> mempty)
servant-quickcheck.cabal+87−126
@@ -1,138 +1,99 @@
1−name: servant-quickcheck
1+name: servant-quickcheck
2−version: 0.0.9.1
2+version: 0.0.0.0
3−synopsis: QuickCheck entire APIs
3+synopsis: QuickCheck entire APIs
4description:
4description:
5− This packages provides QuickCheck properties that are tested across an entire
5+ This packages provides QuickCheck properties that are tested across an entire
6− API.
6+ API.
7
7
8−license: BSD3
8+license: BSD3
9−license-file: LICENSE
9+license-file: LICENSE
10−author: Julian K. Arni
10+author: Julian K. Arni
11−maintainer: jkarni@gmail.com
11+maintainer: jkarni@gmail.com
12−category: Web
12+category: Web
13−build-type: Simple
13+build-type: Simple
14−cabal-version: >=1.10
14+cabal-version: >=1.10
15−extra-source-files: CHANGELOG.yaml
16−tested-with: GHC ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || == 8.8.3
17−
18−source-repository head
19− type: git
20− location: https://github.com/haskell-servant/servant-quickcheck
21
15
22flag long-tests
16flag long-tests
23 description: Run more QuickCheck tests
17 description: Run more QuickCheck tests
24 default: False
18 default: False
25
19
26library
20library
27− exposed-modules:
28− Servant.QuickCheck
21+ exposed-modules: Servant.QuickCheck
29− Servant.QuickCheck.Internal
22+ , Servant.QuickCheck.Internal
30− Servant.QuickCheck.Internal.Equality
23+ , Servant.QuickCheck.Internal.Predicates
31− Servant.QuickCheck.Internal.ErrorTypes
32− Servant.QuickCheck.Internal.HasGenRequest
24+ , Servant.QuickCheck.Internal.HasGenRequest
33− Servant.QuickCheck.Internal.Predicates
25+ , Servant.QuickCheck.Internal.QuickCheck
34− Servant.QuickCheck.Internal.QuickCheck
26+ , Servant.QuickCheck.Internal.Equality
35−
36− build-depends:
27+ build-depends: base >=4.7 && <4.9
37− aeson >=0.8 && <2
38− , base >=4.9 && <4.15
39− , base-compat-batteries >=0.10.1 && <0.12
28+ , base-compat == 0.9.*
29+ , QuickCheck == 2.8.*
40− , bytestring >=0.10 && <0.11
30+ , bytestring == 0.10.*
41− , case-insensitive >=1.2 && <1.3
31+ , aeson > 0.10 && < 0.12
42− , clock >=0.7 && <0.9
43− , data-default-class >=0.0 && <0.2
44− , hspec >=2.5.6 && <2.8
45− , http-client >=0.4.30 && <0.8
46− , http-media >=0.6 && <0.9
47− , http-types >=0.8 && <0.13
48− , mtl >=2.1 && <2.3
32+ , mtl == 2.2.*
49− , pretty >=1.1 && <1.2
33+ , http-client == 0.4.*
50− , process >=1.2 && <1.7
34+ , http-types == 0.9.*
51− , QuickCheck >=2.7 && <2.15
35+ , http-media == 0.6.*
52− , servant >=0.17 && <0.19
53− , servant-client >=0.17 && <0.19
36+ , servant-client == 0.7.*
54− , servant-server >=0.17 && <0.19
37+ , servant-server == 0.7.*
55− , split >=0.2 && <0.3
56− , string-conversions >=0.3 && <0.5
38+ , string-conversions == 0.4.*
39+ , data-default-class == 0.0.*
40+ , servant == 0.7.*
41+ , warp >= 3.2.4 && < 3.3
42+ , process == 1.2.*
57− , temporary >=1.2 && <1.4
43+ , temporary == 1.2.*
44+ , split == 0.2.*
45+ , case-insensitive == 1.2.*
46+ , hspec == 2.2.*
58− , text >=1 && <2
47+ , text == 1.*
59− , time >=1.5 && <1.11
48+ , pretty == 1.1.*
60− , warp >=3.2.4 && <3.4
61−
62− if !impl(ghc >=8.0)
49+ if impl(ghc < 7.10)
63− build-depends: semigroups >=0.18.3 && <0.20
50+ build-depends: bifunctors == 5.*
64−
65− hs-source-dirs: src
66− default-extensions:
67− NoImplicitPrelude
68− ConstraintKinds
69− DataKinds
70− DeriveDataTypeable
71− DeriveFunctor
72− DeriveGeneric
73− FlexibleContexts
74− FlexibleInstances
75− FunctionalDependencies
76− GADTs
77− KindSignatures
78− MultiParamTypeClasses
79− OverloadedStrings
80− RankNTypes
81− ScopedTypeVariables
82− TypeOperators
83
51
52+ hs-source-dirs: src
53+ default-extensions: TypeOperators
54+ , FlexibleInstances
55+ , FlexibleContexts
56+ , DataKinds
57+ , GADTs
58+ , MultiParamTypeClasses
59+ , DeriveFunctor
60+ , KindSignatures
61+ , RankNTypes
62+ , ConstraintKinds
63+ , DeriveGeneric
64+ , ScopedTypeVariables
65+ , OverloadedStrings
66+ , FunctionalDependencies
67+ , NoImplicitPrelude
84− default-language: Haskell2010
68+ default-language: Haskell2010
85
69
86test-suite spec
70test-suite spec
87− type: exitcode-stdio-1.0
71+ type: exitcode-stdio-1.0
88− ghc-options: -Wall -threaded
72+ ghc-options: -Wall -threaded
89− default-language: Haskell2010
73+ default-language: Haskell2010
90− hs-source-dirs: test
74+ hs-source-dirs: test
91− main-is: Spec.hs
75+ main-is: Spec.hs
92− other-modules: Servant.QuickCheck.InternalSpec
76+ other-modules: Servant.QuickCheck.InternalSpec
93− build-tool-depends: hspec-discover:hspec-discover -any
77+ build-depends: base == 4.*
94− build-depends:
95− aeson
96− , base
97− , base-compat-batteries
78+ , base-compat
98− , blaze-html
79+ , servant-quickcheck
99− , bytestring
100− , hspec
80+ , hspec
101− , hspec-core >=2.5.5 && <2.8
81+ , hspec-core
102− , http-client
82+ , http-client
103− , QuickCheck
83+ , warp
104− , quickcheck-io
105− , servant
106− , servant-blaze
107− , servant-client
108− , servant-quickcheck
109− , servant-server
84+ , servant-server
85+ , servant-client
86+ , servant
110− , transformers
87+ , transformers
111− , warp
88+ , QuickCheck
112−
89+ , quickcheck-io
113− default-extensions:
90+ default-extensions: TypeOperators
114− NoImplicitPrelude
115− DataKinds
116− FlexibleContexts
117− FlexibleInstances
91+ , FlexibleInstances
92+ , FlexibleContexts
118− GADTs
93+ , GADTs
94+ , DataKinds
95+ , NoImplicitPrelude
119− OverloadedStrings
96+ , OverloadedStrings
120− ScopedTypeVariables
97+ , ScopedTypeVariables
121− TypeOperators
122−
123 if flag(long-tests)
98 if flag(long-tests)
124− cpp-options: -DLONG_TESTS
99+ cpp-options: -DLONG_TESTS
125−
126−test-suite example
127− type: exitcode-stdio-1.0
128− main-is: Main.hs
129− hs-source-dirs: example
130− ghc-options: -Wall
131− build-depends:
132− base
133− , hspec
134− , servant-quickcheck
135− , servant-server
136− , text
137−
138− default-language: Haskell2010
src/Servant/QuickCheck.hs+3−10
@@ -27,18 +27,12 @@ module Servant.QuickCheck
27 -- in RFCs. The __Best Practices__ includes, in addition to RFC
27 -- in RFCs. The __Best Practices__ includes, in addition to RFC
28 -- recommendations, recommendations found elsewhere or generally accepted.
28 -- recommendations, recommendations found elsewhere or generally accepted.
29 , not500
29 , not500
30− , notLongerThan
31 , onlyJsonObjects
30 , onlyJsonObjects
32− , honoursAcceptHeader
33 , notAllowedContainsAllowHeader
31 , notAllowedContainsAllowHeader
34 , unauthorizedContainsWWWAuthenticate
32 , unauthorizedContainsWWWAuthenticate
35− , getsHaveLastModifiedHeader
36 , getsHaveCacheControlHeader
33 , getsHaveCacheControlHeader
37 , headsHaveCacheControlHeader
34 , headsHaveCacheControlHeader
38 , createContainsValidLocation
35 , createContainsValidLocation
39− -- * Html Predicates
40− , htmlIncludesDoctype
41−
42 -- *** Predicate utilities and types
36 -- *** Predicate utilities and types
43 , (<%>)
37 , (<%>)
44 , Predicates
38 , Predicates
@@ -54,7 +48,6 @@ module Servant.QuickCheck
54 -- represents other means of checking equality
48 -- represents other means of checking equality
55 -- *** Useful @ResponseEquality@s
49 -- *** Useful @ResponseEquality@s
56 , bodyEquality
50 , bodyEquality
57− , jsonEquality
58 , allEquality
51 , allEquality
59 -- ** Response equality type
52 -- ** Response equality type
60 , ResponseEquality(..)
53 , ResponseEquality(..)
@@ -76,10 +69,10 @@ module Servant.QuickCheck
76
69
77 ) where
70 ) where
78
71
79−import Data.Proxy (Proxy (..))
80−import Servant.Client (BaseUrl (..), Scheme (..))
81import Servant.QuickCheck.Internal
72import Servant.QuickCheck.Internal
73+import Servant.Client (BaseUrl(..), Scheme(..))
82−import Test.QuickCheck (Args (..), stdArgs)
74+import Test.QuickCheck (Args(..), stdArgs)
75+import Data.Proxy (Proxy(..))
83
76
84-- | QuickCheck @Args@ with 1000 rather than 100 test cases.
77-- | QuickCheck @Args@ with 1000 rather than 100 test cases.
85--
78--
src/Servant/QuickCheck/Internal.hs+3−4
@@ -1,7 +1,6 @@
1module Servant.QuickCheck.Internal (module X) where
1module Servant.QuickCheck.Internal (module X) where
2
2
3−import Servant.QuickCheck.Internal.Equality as X
4−import Servant.QuickCheck.Internal.ErrorTypes as X
5import Servant.QuickCheck.Internal.HasGenRequest as X
3import Servant.QuickCheck.Internal.HasGenRequest as X
6−import Servant.QuickCheck.Internal.Predicates as X
4+import Servant.QuickCheck.Internal.Predicates as X
7−import Servant.QuickCheck.Internal.QuickCheck as X
5+import Servant.QuickCheck.Internal.QuickCheck as X
6+import Servant.QuickCheck.Internal.Equality as X
src/Servant/QuickCheck/Internal/Equality.hs+5−38
@@ -1,23 +1,16 @@
1module Servant.QuickCheck.Internal.Equality where
1module Servant.QuickCheck.Internal.Equality where
2
2
3−import Data.Aeson (Value, decode, decodeStrict)
4−import Data.ByteString (ByteString)
5−import qualified Data.ByteString.Lazy as LB
6−import Data.Function (on)
3+import Data.Function (on)
7−import Network.HTTP.Client (Response, responseBody)
4+import Network.HTTP.Client (Response, responseBody)
8−import Data.Semigroup (Semigroup (..))
9−import Prelude.Compat
5+import Prelude.Compat
10
6
11newtype ResponseEquality b
7newtype ResponseEquality b
12 = ResponseEquality { getResponseEquality :: Response b -> Response b -> Bool }
8 = ResponseEquality { getResponseEquality :: Response b -> Response b -> Bool }
13
9
14−instance Semigroup (ResponseEquality b) where
15− ResponseEquality a <> ResponseEquality b = ResponseEquality $ \x y ->
16− a x y && b x y
17−
18instance Monoid (ResponseEquality b) where
10instance Monoid (ResponseEquality b) where
19 mempty = ResponseEquality $ \_ _ -> True
11 mempty = ResponseEquality $ \_ _ -> True
20− mappend = (<>)
12+ ResponseEquality a `mappend` ResponseEquality b = ResponseEquality $ \x y ->
13+ a x y && b x y
21
14
22-- | Use `Eq` instance for `Response`
15-- | Use `Eq` instance for `Response`
23--
16--
@@ -30,29 +23,3 @@ allEquality = ResponseEquality (==)
30-- /Since 0.0.0.0/
23-- /Since 0.0.0.0/
31bodyEquality :: Eq b => ResponseEquality b
24bodyEquality :: Eq b => ResponseEquality b
32bodyEquality = ResponseEquality ((==) `on` responseBody)
25bodyEquality = ResponseEquality ((==) `on` responseBody)
33−
34−-- | Equality as 'Value'. This means that if two bodies are equal as JSON
35−-- (e.g., insignificant whitespace difference) they are considered equal.
36−--
37−-- /Since 0.0.3.0/
38−jsonEquality :: (JsonEq b) => ResponseEquality b
39−jsonEquality = ResponseEquality (jsonEq `on` responseBody)
40−
41−class JsonEq a where
42− decode' :: a -> Maybe Value
43− jsonEq :: a -> a -> Bool
44− jsonEq first second = compareDecodedResponses (decode' first) (decode' second)
45−
46−instance JsonEq LB.ByteString where
47− decode' = decode
48−
49−instance JsonEq ByteString where
50− decode' = decodeStrict
51−
52−compareDecodedResponses :: Maybe Value -> Maybe Value -> Bool
53−compareDecodedResponses resp1 resp2 =
54− case resp1 of
55− Nothing -> False -- if decoding fails we assume failure
56− (Just r1) -> case resp2 of
57− Nothing -> False -- another decode failure
58− (Just r2) -> r1 == r2
src/Servant/QuickCheck/Internal/ErrorTypes.hs+32−77
@@ -1,82 +1,37 @@
1−{-# LANGUAGE CPP #-}
2module Servant.QuickCheck.Internal.ErrorTypes where
1module Servant.QuickCheck.Internal.ErrorTypes where
3
2
4−import Control.Exception (Exception (..))
5−import qualified Data.ByteString.Lazy as LBS
6−import Data.String.Conversions (cs)
7−import qualified Data.Text as T
8−import Data.Typeable (Typeable)
9−import GHC.Generics (Generic)
10−import qualified Network.HTTP.Client as C
11−import Network.HTTP.Types (Header, statusCode)
12−import Text.PrettyPrint
3+import Text.PrettyPrint
13
4
14−import Prelude.Compat hiding ((<>))
5+data Request = Request
6+ { requestBody :: String
7+ , requestHeaders :: [String]
8+ , requestUrl :: String
9+ } deriving (Eq, Show, Read, Generic)
15
10
16−data PredicateFailure
17− = PredicateFailure T.Text (Maybe C.Request) (C.Response LBS.ByteString)
18− deriving (Typeable, Generic)
19−
20−instance Exception ServerEqualityFailure where
21−
22−instance Show PredicateFailure where
23− show = render . prettyPredicateFailure
24−
25−
26−data ServerEqualityFailure
27− = ServerEqualityFailure C.Request (C.Response LBS.ByteString) (C.Response LBS.ByteString)
28− deriving (Typeable, Generic)
29−
30−instance Show ServerEqualityFailure where
31− show = render . prettyServerEqualityFailure
32−
33−
34−instance Exception PredicateFailure where
35−
36−-- * Pretty printing
37−
38−prettyHeaders :: [Header] -> Doc
39−prettyHeaders hdrs = vcat $ prettyHdr <$> hdrs
40− where
41− prettyHdr (hn, h) = text (show hn) <> colon <+> text (show h)
42−
43−prettyReq :: C.Request -> Doc
11+prettyReq :: Doc
44prettyReq r =
12prettyReq r =
45− text "Request:" $$ (nest 5 $
13+ text "Request:" $ nest 5 $
46− text "Method:" <+> (nest 5 $ text . show $ C.method r)
14+ text "URL:" <+> text (nest 5 $ requestUrl r)
47− $$ text "Path:" <+> (nest 5 $ text . cs $ C.path r)
15+ $$ text "Headers:" <+>
48− $$ text "Headers:" <+> (nest 5 $ prettyHeaders $ C.requestHeaders r)
49− $$ text "Body:" <+> (nest 5 $ text . getReqBody $ C.requestBody r))
16+ $$ text "Body:" <+> text (nest 5 $ requestBody r)
50− where
17+
51− getReqBody (C.RequestBodyLBS lbs ) = cs lbs
18+instance IsString Request where
52− getReqBody (C.RequestBodyBS bs ) = cs bs
19+ fromString url = Request "" [] url
53− getReqBody _ = error "expected bytestring body"
20+
54−
21+data Response = Response
55−prettyResp :: C.Response LBS.ByteString -> Doc
22+ { responseBody :: String
56−prettyResp r =
23+ , responseHeaders :: [String]
57− text "Response:" $$ (nest 5 $
24+ } deriving (Eq, Show, Read, Generic)
58− text "Status code:" <+> (nest 5 $ text . show . statusCode $ C.responseStatus r)
25+
59− $$ text "Headers:" $$ (nest 10 $ prettyHeaders $ C.responseHeaders r)
26+instance IsString Response where
60− $$ text "Body:" <+> (nest 5 $ text . cs $ C.responseBody r))
27+ fromString body = Response body []
61−
28+
62−
29+-- The error that occurred.
63−
30+data Failure
64−prettyServerEqualityFailure :: ServerEqualityFailure -> Doc
31+ = PredicateFailure String Request Response
65−prettyServerEqualityFailure (ServerEqualityFailure req resp1 resp2) =
32+ | ServerEqualityFailure Request Response Response
66− text "Server equality failed" $$ (nest 5 $
33+ deriving (Eq, Show, Read, Generic)
67− prettyReq req
34+
68− $$ prettyResp resp1
35+instance Show Failure where
69− $$ prettyResp resp2)
70−
71−
72−prettyPredicateFailure :: PredicateFailure -> Doc
73−prettyPredicateFailure (PredicateFailure predicate req resp) =
36+ show (PredicateFailure pred req resp)
74− text "Predicate failed" $$ (nest 5 $
37+ = "Predicate failed for " <> pred <> "
75− text "Predicate:" <+> (text $ T.unpack predicate)
76− $$ r
77− $$ prettyResp resp)
78− where
79− r = case req of
80− Nothing -> text ""
81− Just v -> prettyReq v
82−
src/Servant/QuickCheck/Internal/HasGenRequest.hs+53−113
@@ -1,148 +1,98 @@
1−{-# LANGUAGE CPP #-}
2{-# LANGUAGE PolyKinds #-}
1{-# LANGUAGE PolyKinds #-}
3module Servant.QuickCheck.Internal.HasGenRequest where
2module Servant.QuickCheck.Internal.HasGenRequest where
4
3
4+import Data.Default.Class (def)
5+import Data.Monoid ((<>))
5−import Data.String (fromString)
6+import Data.String (fromString)
6−import Data.String.Conversions (cs)
7+import Data.String.Conversions (cs)
7−import GHC.TypeLits (KnownSymbol, Nat, symbolVal)
8+import GHC.TypeLits (KnownSymbol, Nat, symbolVal)
8−import Network.HTTP.Client (Request, RequestBody (..), host, method, path,
9+import Network.HTTP.Client (Request, RequestBody (..), host,
10+ method, path, port, queryString,
9− port, queryString, requestBody, requestHeaders,
11+ requestBody, requestHeaders, secure)
10− secure, defaultRequest)
11−import Network.HTTP.Media (renderHeader)
12+import Network.HTTP.Media (renderHeader)
12−import Prelude.Compat
13+import Prelude.Compat
13−import Servant
14+import Servant
14−import Servant.API.ContentTypes (AllMimeRender (..))
15+import Servant.API.ContentTypes (AllMimeRender (..))
15−import Servant.Client (BaseUrl (..), Scheme (..))
16+import Servant.Client (BaseUrl (..), Scheme (..))
16−import Test.QuickCheck (Arbitrary (..), Gen, elements, frequency)
17+import Test.QuickCheck (Arbitrary (..), Gen, elements, oneof)
17−
18+
18−import qualified Data.ByteString as BS
19−import qualified Data.ByteString.Internal as BS (c2w)
20−
21−
22−-- -----------------------------------------------------------------------------
23−-- runGenRequest
24−
25−-- | This function returns a QuickCheck `Gen a` when passed a servant API value,
26−-- typically a `Proxy API`. The generator returned is a function
27−-- that accepts a `BaseUrl` and returns a `Request`, which can then be used
28−-- to issue network requests. This `Gen` type makes it easier to compare distinct
29−-- APIs across different `BaseUrl`s.
30−runGenRequest :: HasGenRequest a => Proxy a -> Gen (BaseUrl -> Request)
31−runGenRequest = snd . genRequest
32−
33−
34−-- -----------------------------------------------------------------------------
35−-- HasGenRequest
36−
37−-- | This is the core Servant-Quickcheck generator, which, when given a `Proxy API`
38−-- will return a pair of `Int` and `Gen a`, where `a` is a function from
39−-- `BaseUrl` to a `Network.Http.Client.Request`. The `Int` is a weight for the
40−-- QuickCheck `frequency` function which ensures a random distribution across
41−-- all endpoints in an API.
42−class HasGenRequest a where
43− genRequest :: Proxy a -> (Int, Gen (BaseUrl -> Request))
44
19
20+class HasGenRequest a where
21+ genRequest :: Proxy a -> Gen (BaseUrl -> Request)
45
22
46instance (HasGenRequest a, HasGenRequest b) => HasGenRequest (a :<|> b) where
23instance (HasGenRequest a, HasGenRequest b) => HasGenRequest (a :<|> b) where
47 genRequest _
24 genRequest _
48− = (lf + rf, frequency [l, r])
49− where
50− l@(lf, _) = genRequest (Proxy :: Proxy a)
25+ = oneof [ genRequest (Proxy :: Proxy a)
51− r@(rf, _) = genRequest (Proxy :: Proxy b)
26+ , genRequest (Proxy :: Proxy b)
52−
27+ ]
53
28
54instance (KnownSymbol path, HasGenRequest b) => HasGenRequest (path :> b) where
29instance (KnownSymbol path, HasGenRequest b) => HasGenRequest (path :> b) where
55− genRequest _ = (oldf, do
30+ genRequest _ = do
56 old' <- old
31 old' <- old
57− return $ \burl -> let r = old' burl
32+ return $ \burl -> let r = old' burl in r { path = new <> "/" <> path r }
58− oldPath = path r
59− oldPath' = BS.dropWhile (== BS.c2w '/') oldPath
60− paths = filter (not . BS.null) [new, oldPath']
61− in r { path = "/" <> BS.intercalate "/" paths })
62 where
33 where
63− (oldf, old) = genRequest (Proxy :: Proxy b)
34+ old = genRequest (Proxy :: Proxy b)
64 new = cs $ symbolVal (Proxy :: Proxy path)
35 new = cs $ symbolVal (Proxy :: Proxy path)
65
36
66−instance HasGenRequest EmptyAPI where
67− genRequest _ = (0, error "EmptyAPIs cannot be queried.")
68−
69−instance HasGenRequest api => HasGenRequest (Summary d :> api) where
70− genRequest _ = genRequest (Proxy :: Proxy api)
71−
72−instance HasGenRequest api => HasGenRequest (Description d :> api) where
73− genRequest _ = genRequest (Proxy :: Proxy api)
74−
75instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
37instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
76− => HasGenRequest (Capture' mods x c :> b) where
38+ => HasGenRequest (Capture x c :> b) where
77− genRequest _ = (oldf, do
39+ genRequest _ = do
78 old' <- old
40 old' <- old
79 new' <- toUrlPiece <$> new
41 new' <- toUrlPiece <$> new
80− return $ \burl -> let r = old' burl in r { path = cs new' <> path r })
42+ return $ \burl -> let r = old' burl in r { path = cs new' <> "/" <> path r }
81 where
43 where
82− (oldf, old) = genRequest (Proxy :: Proxy b)
44+ old = genRequest (Proxy :: Proxy b)
83 new = arbitrary :: Gen c
45 new = arbitrary :: Gen c
84
46
85−instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
86− => HasGenRequest (CaptureAll x c :> b) where
87− genRequest _ = (oldf, do
88− old' <- old
89− new' <- fmap (cs . toUrlPiece) <$> new
90− let new'' = BS.intercalate "/" new'
91− return $ \burl -> let r = old' burl in r { path = new'' <> path r })
92− where
93− (oldf, old) = genRequest (Proxy :: Proxy b)
94− new = arbitrary :: Gen [c]
95−
96instance (Arbitrary c, KnownSymbol h, HasGenRequest b, ToHttpApiData c)
47instance (Arbitrary c, KnownSymbol h, HasGenRequest b, ToHttpApiData c)
97− => HasGenRequest (Header' mods h c :> b) where
48+ => HasGenRequest (Header h c :> b) where
98− genRequest _ = (oldf, do
49+ genRequest _ = do
99 old' <- old
50 old' <- old
100− new' <- toUrlPiece <$> new -- TODO: generate lenient or/and optional
51+ new' <- toUrlPiece <$> new
101 return $ \burl -> let r = old' burl in r {
52 return $ \burl -> let r = old' burl in r {
102− requestHeaders = (hdr, cs new') : requestHeaders r })
53+ requestHeaders = (hdr, cs new') : requestHeaders r }
103 where
54 where
104− (oldf, old) = genRequest (Proxy :: Proxy b)
55+ old = genRequest (Proxy :: Proxy b)
105 hdr = fromString $ symbolVal (Proxy :: Proxy h)
56 hdr = fromString $ symbolVal (Proxy :: Proxy h)
106 new = arbitrary :: Gen c
57 new = arbitrary :: Gen c
107
58
108instance (AllMimeRender x c, Arbitrary c, HasGenRequest b)
59instance (AllMimeRender x c, Arbitrary c, HasGenRequest b)
109− => HasGenRequest (ReqBody' mods x c :> b) where
60+ => HasGenRequest (ReqBody x c :> b) where
110− genRequest _ = (oldf, do
61+ genRequest _ = do
111− old' <- old -- TODO: generate lenient
62+ old' <- old
112 new' <- new
63 new' <- new
113 (ct, bd) <- elements $ allMimeRender (Proxy :: Proxy x) new'
64 (ct, bd) <- elements $ allMimeRender (Proxy :: Proxy x) new'
114 return $ \burl -> let r = old' burl in r {
65 return $ \burl -> let r = old' burl in r {
115 requestBody = RequestBodyLBS bd
66 requestBody = RequestBodyLBS bd
116 , requestHeaders = ("Content-Type", renderHeader ct) : requestHeaders r
67 , requestHeaders = ("Content-Type", renderHeader ct) : requestHeaders r
117− })
68+ }
118 where
69 where
119− (oldf, old) = genRequest (Proxy :: Proxy b)
70+ old = genRequest (Proxy :: Proxy b)
120 new = arbitrary :: Gen c
71 new = arbitrary :: Gen c
121
72
122instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
73instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
123− => HasGenRequest (QueryParam' mods x c :> b) where
74+ => HasGenRequest (QueryParam x c :> b) where
124− genRequest _ = (oldf, do
75+ genRequest _ = do
125− new' <- new -- TODO: generate lenient or/and optional
76+ new' <- new
126 old' <- old
77 old' <- old
127− return $ \burl -> let r = old' burl
78+ return $ \burl -> let r = old' burl in r {
79+ queryString = queryString r
128− newExpr = param <> "=" <> cs (toQueryParam new')
80+ <> param <> "=" <> cs (toQueryParam new') }
129− qs = queryString r in r {
130− queryString = if BS.null qs then newExpr else newExpr <> "&" <> qs })
131 where
81 where
132− (oldf, old) = genRequest (Proxy :: Proxy b)
82+ old = genRequest (Proxy :: Proxy b)
133 param = cs $ symbolVal (Proxy :: Proxy x)
83 param = cs $ symbolVal (Proxy :: Proxy x)
134 new = arbitrary :: Gen c
84 new = arbitrary :: Gen c
135
85
136instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
86instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
137 => HasGenRequest (QueryParams x c :> b) where
87 => HasGenRequest (QueryParams x c :> b) where
138− genRequest _ = (oldf, do
88+ genRequest _ = do
139 new' <- new
89 new' <- new
140 old' <- old
90 old' <- old
141 return $ \burl -> let r = old' burl in r {
91 return $ \burl -> let r = old' burl in r {
142 queryString = queryString r
92 queryString = queryString r
143− <> if length new' > 0 then fold (toParam <$> new') else ""})
93+ <> if length new' > 0 then fold (toParam <$> new') else ""}
144 where
94 where
145− (oldf, old) = genRequest (Proxy :: Proxy b)
95+ old = genRequest (Proxy :: Proxy b)
146 param = cs $ symbolVal (Proxy :: Proxy x)
96 param = cs $ symbolVal (Proxy :: Proxy x)
147 new = arbitrary :: Gen [c]
97 new = arbitrary :: Gen [c]
148 toParam c = param <> "[]=" <> cs (toQueryParam c)
98 toParam c = param <> "[]=" <> cs (toQueryParam c)
@@ -150,32 +100,22 @@ instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
150
100
151instance (KnownSymbol x, HasGenRequest b)
101instance (KnownSymbol x, HasGenRequest b)
152 => HasGenRequest (QueryFlag x :> b) where
102 => HasGenRequest (QueryFlag x :> b) where
153− genRequest _ = (oldf, do
103+ genRequest _ = do
154 old' <- old
104 old' <- old
155− return $ \burl -> let r = old' burl
105+ return $ \burl -> let r = old' burl in r {
156− qs = queryString r in r {
157− queryString = if BS.null qs then param else param <> "&" <> qs })
106+ queryString = queryString r <> param <> "=" }
158 where
107 where
159− (oldf, old) = genRequest (Proxy :: Proxy b)
108+ old = genRequest (Proxy :: Proxy b)
160 param = cs $ symbolVal (Proxy :: Proxy x)
109 param = cs $ symbolVal (Proxy :: Proxy x)
161
110
162instance (ReflectMethod method)
111instance (ReflectMethod method)
163 => HasGenRequest (Verb (method :: k) (status :: Nat) (cts :: [*]) a) where
112 => HasGenRequest (Verb (method :: k) (status :: Nat) (cts :: [*]) a) where
164− genRequest _ = (1, return $ \burl -> defaultRequest
113+ genRequest _ = return $ \burl -> def
165− { host = cs $ baseUrlHost burl
166− , port = baseUrlPort burl
167− , secure = baseUrlScheme burl == Https
168− , method = reflectMethod (Proxy :: Proxy method)
169− })
170−
171−instance (ReflectMethod method)
172− => HasGenRequest (NoContentVerb (method :: k)) where
173− genRequest _ = (1, return $ \burl -> defaultRequest
174 { host = cs $ baseUrlHost burl
114 { host = cs $ baseUrlHost burl
175 , port = baseUrlPort burl
115 , port = baseUrlPort burl
176 , secure = baseUrlScheme burl == Https
116 , secure = baseUrlScheme burl == Https
177 , method = reflectMethod (Proxy :: Proxy method)
117 , method = reflectMethod (Proxy :: Proxy method)
178− })
118+ }
179
119
180instance (HasGenRequest a) => HasGenRequest (RemoteHost :> a) where
120instance (HasGenRequest a) => HasGenRequest (RemoteHost :> a) where
181 genRequest _ = genRequest (Proxy :: Proxy a)
121 genRequest _ = genRequest (Proxy :: Proxy a)
src/Servant/QuickCheck/Internal/Predicates.hs+134−218
@@ -1,36 +1,28 @@
1module Servant.QuickCheck.Internal.Predicates where
1module Servant.QuickCheck.Internal.Predicates where
2
2
3−import Control.Exception (catch, throw)
3+import Control.Monad (liftM2)
4−import Control.Monad (liftM2, unless, when)
5import Data.Aeson (Object, decode)
4import Data.Aeson (Object, decode)
6−import Data.Bifunctor (first)
5+import Data.Bifunctor (Bifunctor (..))
6+import Prelude.Compat
7import qualified Data.ByteString as SBS
7import qualified Data.ByteString as SBS
8import qualified Data.ByteString.Char8 as SBSC
8import qualified Data.ByteString.Char8 as SBSC
9import qualified Data.ByteString.Lazy as LBS
9import qualified Data.ByteString.Lazy as LBS
10−import Data.CaseInsensitive (foldCase, foldedCase, mk)
10+import Data.CaseInsensitive (mk)
11import Data.Either (isRight)
11import Data.Either (isRight)
12import Data.List.Split (wordsBy)
12import Data.List.Split (wordsBy)
13import Data.Maybe (fromMaybe, isJust)
13import Data.Maybe (fromMaybe, isJust)
14−import Data.Semigroup (Semigroup (..))
14+import Data.Monoid ((<>))
15−import qualified Data.Text as T
15+import Data.Text (Text)
16−import Data.Time (UTCTime, defaultTimeLocale, parseTimeM,
17− rfc822DateFormat)
18import GHC.Generics (Generic)
16import GHC.Generics (Generic)
19import Network.HTTP.Client (Manager, Request, Response, httpLbs,
17import Network.HTTP.Client (Manager, Request, Response, httpLbs,
20− method, parseRequest, requestHeaders,
18+ method, parseUrl, requestHeaders,
21 responseBody, responseHeaders,
19 responseBody, responseHeaders,
22 responseStatus)
20 responseStatus)
23import Network.HTTP.Media (matchAccept)
21import Network.HTTP.Media (matchAccept)
24import Network.HTTP.Types (methodGet, methodHead, parseMethod,
22import Network.HTTP.Types (methodGet, methodHead, parseMethod,
25− renderStdMethod, status100, status200,
23+ renderStdMethod, status200, status201,
26− status201, status300, status401,
24+ status300, status401, status405,
27− status405, status500)
25+ status500, status100)
28−import Prelude.Compat
29−import System.Clock (Clock (Monotonic), diffTimeSpec,
30− getTime, toNanoSecs)
31−
32−import Servant.QuickCheck.Internal.ErrorTypes
33−
34
26
35-- | [__Best Practice__]
27-- | [__Best Practice__]
36--
28--
@@ -41,25 +33,8 @@ import Servant.QuickCheck.Internal.ErrorTypes
41-- This function checks that the response code is not 500.
33-- This function checks that the response code is not 500.
42--
34--
43-- /Since 0.0.0.0/
35-- /Since 0.0.0.0/
44−not500 :: ResponsePredicate
36+not500 :: ResponsePredicate Text Bool
45−not500 = ResponsePredicate $ \resp ->
37+not500 = ResponsePredicate "not500" (\resp -> not $ responseStatus resp == status500)
46− when (responseStatus resp == status500) $ throw $ PredicateFailure "not500" Nothing resp
47−
48−-- | [__Optional__]
49−--
50−-- This function checks that the response from the server does not take longer
51−-- than the specified number of nanoseconds.
52−--
53−-- /Since 0.0.2.1/
54−notLongerThan :: Integer -> RequestPredicate
55−notLongerThan maxAllowed
56− = RequestPredicate $ \req mgr -> do
57− start <- getTime Monotonic
58− resp <- httpLbs req mgr
59− end <- getTime Monotonic
60− when (toNanoSecs (end `diffTimeSpec` start) > maxAllowed) $
61− throw $ PredicateFailure "notLongerThan" (Just req) resp
62− return []
63
38
64-- | [__Best Practice__]
39-- | [__Best Practice__]
65--
40--
@@ -82,15 +57,11 @@ notLongerThan maxAllowed
82-- * JSON Grammar: <https://tools.ietf.org/html/rfc4627#section-2 RFC 4627 Section 2>
57-- * JSON Grammar: <https://tools.ietf.org/html/rfc4627#section-2 RFC 4627 Section 2>
83--
58--
84-- /Since 0.0.0.0/
59-- /Since 0.0.0.0/
85−onlyJsonObjects :: ResponsePredicate
60+onlyJsonObjects :: ResponsePredicate Text Bool
86onlyJsonObjects
61onlyJsonObjects
87− = ResponsePredicate (\resp -> do
88− case lookup "content-type" (first foldedCase <$> responseHeaders resp) of
89− Nothing -> return ()
90− Just ctype -> when ("application/json" `SBS.isPrefixOf` ctype) $ do
91− case (decode (responseBody resp) :: Maybe Object) of
62+ = ResponsePredicate "onlyJsonObjects" (\resp -> case decode (responseBody resp) of
92− Nothing -> throw $ PredicateFailure "onlyJsonObjects" Nothing resp
63+ Nothing -> False
93− Just _ -> return ())
64+ Just (_ :: Object) -> True)
94
65
95-- | __Optional__
66-- | __Optional__
96--
67--
@@ -111,59 +82,29 @@ onlyJsonObjects
111-- * Location header: <https://tools.ietf.org/html/rfc7231#section-7.1.2 RFC 7231 Section 7.1.2>
82-- * Location header: <https://tools.ietf.org/html/rfc7231#section-7.1.2 RFC 7231 Section 7.1.2>
112--
83--
113-- /Since 0.0.0.0/
84-- /Since 0.0.0.0/
114−createContainsValidLocation :: RequestPredicate
85+createContainsValidLocation :: RequestPredicate Text Bool
115createContainsValidLocation
86createContainsValidLocation
116− = RequestPredicate $ \req mgr -> do
87+ = RequestPredicate
117− let n = "createContainsValidLocation"
88+ { reqPredName = "createContainsValidLocation"
118− resp <- httpLbs req mgr
89+ , reqResps = \req mgr -> do
119− if responseStatus resp == status201
120− then case lookup "Location" $ responseHeaders resp of
121− Nothing -> throw $ PredicateFailure n (Just req) resp
122− Just l -> case parseRequest $ SBSC.unpack l of
123− Nothing -> throw $ PredicateFailure n (Just req) resp
124− Just x -> do
125− resp2 <- httpLbs x mgr
126− status2XX (Just req) resp2 n
127− return [resp, resp2]
128− else return [resp]
129−
130−-- | [__Optional__]
131−--
132−-- The @Last-Modified@ header represents the time a resource was last
133−-- modified. It is used to drive caching and conditional requests.
134−--
135−-- When using this mechanism, the server adds the @Last-Modified@ header to
136−-- responses. Clients may then make requests with the @If-Modified-Since@
137−-- header to conditionally request resources. If the resource has not
138−-- changed since that date, the server responds with a status code of 304
139−-- (@Not Modified@) without a response body.
140−--
141−-- The @Last-Modified@ header can also be used in conjunction with the
142−-- @If-Unmodified-Since@ header to drive optimistic concurrency.
143−--
144−-- The @Last-Modified@ date must be in RFC 822 format.
145−--
146−-- __References__:
147−--
148−-- * 304 Not Modified: <https://tools.ietf.org/html/rfc7232#section-4.1 RFC 7232 Section 4.1>
149−-- * Last-Modified header: <https://tools.ietf.org/html/rfc7232#section-2.2 RFC 7232 Section 2.2>
150−-- * If-Modified-Since header: <https://tools.ietf.org/html/rfc7232#section-3.3 RFC 7232 Section 3.3>
151−-- * If-Unmodified-Since header: <https://tools.ietf.org/html/rfc7232#section-3.4 RFC 7232 Section 3.4>
152−-- * Date format: <https://tools.ietf.org/html/rfc2616#section-3.3 RFC 2616 Section 3.3>
153−--
154−-- /Since 0.0.2.1/
155−getsHaveLastModifiedHeader :: RequestPredicate
156−getsHaveLastModifiedHeader
157− = RequestPredicate $ \req mgr ->
158− if (method req == methodGet)
159− then do
160 resp <- httpLbs req mgr
90 resp <- httpLbs req mgr
161− unless (hasValidHeader "Last-Modified" isRFC822Date resp) $ do
91+ if responseStatus resp == status201
162− throw $ PredicateFailure "getsHaveLastModifiedHeader" (Just req) resp
92+ then case lookup "Location" $ responseHeaders resp of
163− return [resp]
93+ Nothing -> return (False, [resp])
94+ Just l -> case parseUrl $ SBSC.unpack l of
95+ Nothing -> return (False, [resp])
96+ Just x -> do
97+ resp2 <- httpLbs x mgr
98+ return (status2XX resp2, [resp, resp2])
164− else return []
99+ else return (True, [resp])
100+ }
165
101
102+{-
103+getsHaveLastModifiedHeader :: ResponsePredicate Text Bool
104+getsHaveLastModifiedHeader
105+ = ResponsePredicate "getsHaveLastModifiedHeader" (\resp ->
166
106
107+-}
167
108
168-- | [__RFC Compliance__]
109-- | [__RFC Compliance__]
169--
110--
@@ -175,27 +116,24 @@ getsHaveLastModifiedHeader
175-- This function checks that every @405 Method Not Allowed@ response contains
116-- This function checks that every @405 Method Not Allowed@ response contains
176-- an @Allow@ header with a list of standard HTTP methods.
117-- an @Allow@ header with a list of standard HTTP methods.
177--
118--
178−-- Note that 'servant' itself does not currently set the @Allow@ headers.
179−--
180-- __References__:
119-- __References__:
181--
120--
182-- * @Allow@ header: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html RFC 2616 Section 14.7>
121-- * @Allow@ header: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html RFC 2616 Section 14.7>
183-- * Status 405: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html RFC 2616 Section 10.4.6>
122-- * Status 405: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html RFC 2616 Section 10.4.6>
184−-- * Servant Allow header issue: <https://github.com/haskell-servant/servant/issues/489 Issue #489>
185--
123--
186-- /Since 0.0.0.0/
124-- /Since 0.0.0.0/
187−notAllowedContainsAllowHeader :: RequestPredicate
125+notAllowedContainsAllowHeader :: RequestPredicate Text Bool
188notAllowedContainsAllowHeader
126notAllowedContainsAllowHeader
189− = RequestPredicate $ \req mgr -> do
127+ = RequestPredicate
128+ { reqPredName = "notAllowedContainsAllowHeader"
129+ , reqResps = \req mgr -> do
190− let reqs = [ req { method = renderStdMethod m } | m <- [minBound .. maxBound]
130+ resp <- mapM (flip httpLbs mgr) $ [ req { method = renderStdMethod m }
131+ | m <- [minBound .. maxBound ]
191− , renderStdMethod m /= method req ]
132+ , renderStdMethod m /= method req ]
192− resp <- mapM (flip httpLbs mgr) reqs
133+ return (all pred' resp, resp)
193−
134+ }
194− case filter pred' (zip reqs resp) of
195− (x:_) -> throw $ PredicateFailure "notAllowedContainsAllowHeader" (Just $ fst x) (snd x)
196− [] -> return resp
197 where
135 where
198− pred' (_, resp) = responseStatus resp == status405 && not (hasValidHeader "Allow" go resp)
136+ pred' resp = responseStatus resp /= status405 || hasValidHeader "Allow" go resp
199 where
137 where
200 go x = all (\y -> isRight $ parseMethod $ SBSC.pack y)
138 go x = all (\y -> isRight $ parseMethod $ SBSC.pack y)
201 $ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x)
139 $ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x)
@@ -216,19 +154,19 @@ notAllowedContainsAllowHeader
216-- * @Accept@ header: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html RFC 2616 Section 14.1>
154-- * @Accept@ header: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html RFC 2616 Section 14.1>
217--
155--
218-- /Since 0.0.0.0/
156-- /Since 0.0.0.0/
219−honoursAcceptHeader :: RequestPredicate
157+honoursAcceptHeader :: RequestPredicate Text Bool
220honoursAcceptHeader
158honoursAcceptHeader
221− = RequestPredicate $ \req mgr -> do
159+ = RequestPredicate
160+ { reqPredName = "honoursAcceptHeader"
161+ , reqResps = \req mgr -> do
222− resp <- httpLbs req mgr
162+ resp <- httpLbs req mgr
223− let scode = responseStatus resp
163+ let scode = responseStatus resp
224− sctype = lookup "Content-Type" $ responseHeaders resp
164+ sctype = lookup "Content-Type" $ responseHeaders resp
225− sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
165+ sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
226− if status100 < scode && scode < status300
166+ if status100 < scode && scode < status300
227− then if isJust $ sctype >>= \x -> matchAccept [x] sacc
167+ then return (isJust $ sctype >>= \x -> matchAccept [x] sacc, [resp])
228− then throw $ PredicateFailure "honoursAcceptHeader" (Just req) resp
229− else return [resp]
168+ else return (True, [resp])
230− else return [resp]
169+ }
231−
232
170
233-- | [__Best Practice__]
171-- | [__Best Practice__]
234--
172--
@@ -244,32 +182,34 @@ honoursAcceptHeader
244-- * @Cache-Control@ header: <https://tools.ietf.org/html/rfc7234#section-5.2 RFC 7234 Section 5.2>
182-- * @Cache-Control@ header: <https://tools.ietf.org/html/rfc7234#section-5.2 RFC 7234 Section 5.2>
245--
183--
246-- /Since 0.0.0.0/
184-- /Since 0.0.0.0/
247−getsHaveCacheControlHeader :: RequestPredicate
185+getsHaveCacheControlHeader :: RequestPredicate Text Bool
248getsHaveCacheControlHeader
186getsHaveCacheControlHeader
249− = RequestPredicate $ \req mgr ->
187+ = RequestPredicate
188+ { reqPredName = "getsHaveCacheControlHeader"
250− if (method req == methodGet)
189+ , reqResps = \req mgr -> if method req == methodGet
251− then do
190+ then do
252− resp <- httpLbs req mgr
191+ resp <- httpLbs req mgr
253− unless (hasValidHeader "Cache-Control" (const True) resp) $ do
192+ let good = isJust $ lookup "Cache-Control" $ responseHeaders resp
254− throw $ PredicateFailure "getsHaveCacheControlHeader" (Just req) resp
193+ return (good, [resp])
255− return [resp]
256− else return []
194+ else return (True, [])
195+ }
257
196
258-- | [__Best Practice__]
197-- | [__Best Practice__]
259--
198--
260-- Like 'getsHaveCacheControlHeader', but for @HEAD@ requests.
199-- Like 'getsHaveCacheControlHeader', but for @HEAD@ requests.
261--
200--
262-- /Since 0.0.0.0/
201-- /Since 0.0.0.0/
263−headsHaveCacheControlHeader :: RequestPredicate
202+headsHaveCacheControlHeader :: RequestPredicate Text Bool
264headsHaveCacheControlHeader
203headsHaveCacheControlHeader
265− = RequestPredicate $ \req mgr ->
204+ = RequestPredicate
205+ { reqPredName = "headsHaveCacheControlHeader"
266− if (method req == methodHead)
206+ , reqResps = \req mgr -> if method req == methodHead
267− then do
207+ then do
268− resp <- httpLbs req mgr
208+ resp <- httpLbs req mgr
269− unless (hasValidHeader "Cache-Control" (const True) resp) $
209+ let good = hasValidHeader "Cache-Control" (const True) resp
270− throw $ PredicateFailure "headsHaveCacheControlHeader" (Just req) resp
210+ return (good, [resp])
271− return [resp]
272− else return []
211+ else return (True, [])
212+ }
273{-
213{-
274-- |
214-- |
275--
215--
@@ -331,35 +271,12 @@ linkHeadersAreValid
331-- * @WWW-Authenticate@ header: <https://tools.ietf.org/html/rfc7235#section-4.1 RFC 7235 Section 4.1>
271-- * @WWW-Authenticate@ header: <https://tools.ietf.org/html/rfc7235#section-4.1 RFC 7235 Section 4.1>
332--
272--
333-- /Since 0.0.0.0/
273-- /Since 0.0.0.0/
334−unauthorizedContainsWWWAuthenticate :: ResponsePredicate
274+unauthorizedContainsWWWAuthenticate :: ResponsePredicate Text Bool
335unauthorizedContainsWWWAuthenticate
275unauthorizedContainsWWWAuthenticate
336− = ResponsePredicate $ \resp ->
276+ = ResponsePredicate "unauthorizedContainsWWWAuthenticate" (\resp ->
337 if responseStatus resp == status401
277 if responseStatus resp == status401
338− then unless (hasValidHeader "WWW-Authenticate" (const True) resp) $
278+ then hasValidHeader "WWW-Authenticate" (const True) resp
339− throw $ PredicateFailure "unauthorizedContainsWWWAuthenticate" Nothing resp
340− else return ()
279+ else True)
341−
342−
343−-- | [__RFC Compliance__]
344−--
345−-- [An HTML] document will start with exactly this string: <!DOCTYPE html>
346−--
347−-- This function checks that HTML documents (those with `Content-Type: text/html...`)
348−-- include a DOCTYPE declaration at the top. We do not enforce capital case for the string `DOCTYPE`.
349−--
350−-- __References__:
351−--
352−-- * HTML5 Doctype: <https://tools.ietf.org/html/rfc7992#section-6.1 RFC 7992 Section 6.1>
353−-- /Since 0.3.0.0/
354−htmlIncludesDoctype :: ResponsePredicate
355−htmlIncludesDoctype
356− = ResponsePredicate $ \resp ->
357− if hasValidHeader "Content-Type" (SBS.isPrefixOf . foldCase $ "text/html") resp
358− then do
359− let htmlContent = foldCase . LBS.take 20 $ responseBody resp
360− unless (LBS.isPrefixOf (foldCase "<!doctype html>") htmlContent) $
361− throw $ PredicateFailure "htmlIncludesDoctype" Nothing resp
362− else return ()
363
280
364-- * Predicate logic
281-- * Predicate logic
365
282
@@ -372,56 +289,67 @@ htmlIncludesDoctype
372-- | A predicate that depends only on the response.
289-- | A predicate that depends only on the response.
373--
290--
374-- /Since 0.0.0.0/
291-- /Since 0.0.0.0/
375−newtype ResponsePredicate = ResponsePredicate
292+data ResponsePredicate n r = ResponsePredicate
293+ { respPredName :: n
376− { getResponsePredicate :: Response LBS.ByteString -> IO ()
294+ , respPred :: Response LBS.ByteString -> r
377− } deriving (Generic)
295+ } deriving (Functor, Generic)
378−
296+
379−instance Semigroup ResponsePredicate where
297+instance Bifunctor ResponsePredicate where
380− ResponsePredicate a <> ResponsePredicate b = ResponsePredicate $ \x -> a x >> b x
298+ first f (ResponsePredicate a b) = ResponsePredicate (f a) b
381−
299+ second = fmap
300+
382−instance Monoid ResponsePredicate where
301+instance (Monoid n, Monoid r) => Monoid (ResponsePredicate n r) where
383− mempty = ResponsePredicate $ const $ return ()
302+ mempty = ResponsePredicate mempty mempty
384− mappend = (<>)
303+ a `mappend` b = ResponsePredicate
304+ { respPredName = respPredName a <> respPredName b
305+ , respPred = respPred a <> respPred b
306+ }
385
307
386-- | A predicate that depends on both the request and the response.
308-- | A predicate that depends on both the request and the response.
387--
309--
388-- /Since 0.0.0.0/
310-- /Since 0.0.0.0/
389−newtype RequestPredicate = RequestPredicate
311+data RequestPredicate n r = RequestPredicate
312+ { reqPredName :: n
390− { getRequestPredicate :: Request -> Manager -> IO [Response LBS.ByteString]
313+ , reqResps :: Request -> Manager -> IO (r, [Response LBS.ByteString])
391− } deriving (Generic)
314+ } deriving (Generic, Functor)
392
315
393−-- TODO: This isn't actually a monoid
394−instance Monoid RequestPredicate where
316+instance Bifunctor RequestPredicate where
395− mempty = RequestPredicate (\r m -> httpLbs r m >>= \x -> return ([x]))
317+ first f (RequestPredicate a b) = RequestPredicate (f a) b
396− mappend = (<>)
318+ second = fmap
397
319
398-- TODO: This isn't actually a monoid
320-- TODO: This isn't actually a monoid
399−instance Semigroup RequestPredicate where
321+instance (Monoid n, Monoid r) => Monoid (RequestPredicate n r) where
322+ mempty = RequestPredicate mempty (\r m -> httpLbs r m >>= \x -> return (mempty, [x]))
400− RequestPredicate a <> RequestPredicate b = RequestPredicate $ \r mgr ->
323+ a `mappend` b = RequestPredicate
401− liftM2 (<>) (a r mgr) (b r mgr)
324+ { reqPredName = reqPredName a <> reqPredName b
325+ , reqResps = \x m -> liftM2 (<>) (reqResps a x m) (reqResps b x m)
326+ }
402
327
403-- | A set of predicates. Construct one with 'mempty' and '<%>'.
328-- | A set of predicates. Construct one with 'mempty' and '<%>'.
404−data Predicates = Predicates
329+data Predicates n r = Predicates
405− { requestPredicates :: RequestPredicate
330+ { reqPreds :: RequestPredicate n r
406− , responsePredicates :: ResponsePredicate
331+ , respPreds :: ResponsePredicate n r
407− } deriving (Generic)
332+ } deriving (Generic, Functor)
408
333
409−instance Semigroup Predicates where
334+instance (Monoid n, Monoid r) => Monoid (Predicates n r) where
410− a <> b = Predicates (requestPredicates a <> requestPredicates b)
411− (responsePredicates a <> responsePredicates b)
412−
413−instance Monoid Predicates where
414 mempty = Predicates mempty mempty
335 mempty = Predicates mempty mempty
415− mappend = (<>)
336+ a `mappend` b = Predicates (reqPreds a <> reqPreds b) (respPreds a <> respPreds b)
337+
338+
416
339
417class JoinPreds a where
340class JoinPreds a where
418− joinPreds :: a -> Predicates -> Predicates
341+ joinPreds :: a -> Predicates [Text] [Text] -> Predicates [Text] [Text]
342+
343+instance JoinPreds (RequestPredicate Text Bool) where
344+ joinPreds p (Predicates x y) = Predicates (go <> x) y
345+ where go = let p' = first return p
346+ in fmap (\z -> if z then [] else reqPredName p') p'
419
347
420−instance JoinPreds (RequestPredicate ) where
348+instance JoinPreds (ResponsePredicate Text Bool) where
421− joinPreds p (Predicates x y) = Predicates (p <> x) y
349+ joinPreds p (Predicates x y) = Predicates x (go <> y)
350+ where go = let p' = first return p
351+ in fmap (\z -> if z then [] else respPredName p') p'
422
352
423−instance JoinPreds (ResponsePredicate ) where
424− joinPreds p (Predicates x y) = Predicates x (p <> y)
425
353
426-- | Adds a new predicate (either `ResponsePredicate` or `RequestPredicate`) to
354-- | Adds a new predicate (either `ResponsePredicate` or `RequestPredicate`) to
427-- the existing predicates.
355-- the existing predicates.
@@ -429,17 +357,14 @@ instance JoinPreds (ResponsePredicate ) where
429-- > not500 <%> onlyJsonObjects <%> empty
357-- > not500 <%> onlyJsonObjects <%> empty
430--
358--
431-- /Since 0.0.0.0/
359-- /Since 0.0.0.0/
432−(<%>) :: JoinPreds a => a -> Predicates -> Predicates
360+(<%>) :: JoinPreds a => a -> Predicates [Text] [Text] -> Predicates [Text] [Text]
433(<%>) = joinPreds
361(<%>) = joinPreds
434infixr 6 <%>
362infixr 6 <%>
435
363
436−finishPredicates :: Predicates -> Request -> Manager -> IO (Maybe PredicateFailure)
364+finishPredicates :: Predicates [Text] [Text] -> Request -> Manager -> IO [Text]
437−finishPredicates p req mgr = go `catch` \(e :: PredicateFailure) -> return $ Just e
365+finishPredicates p req mgr = do
438− where
366+ (soFar, resps) <- reqResps (reqPreds p) req mgr
439− go = do
367+ return $ soFar <> mconcat [respPred (respPreds p) r | r <- resps]
440− resps <- getRequestPredicate (requestPredicates p) req mgr
441− mapM_ (getResponsePredicate $ responsePredicates p) resps
442− return Nothing
443
368
444-- * helpers
369-- * helpers
445
370
@@ -448,14 +373,5 @@ hasValidHeader hdr p r = case lookup (mk hdr) (responseHeaders r) of
448 Nothing -> False
373 Nothing -> False
449 Just v -> p v
374 Just v -> p v
450
375
451−isRFC822Date :: SBS.ByteString -> Bool
376+status2XX :: Response b -> Bool
452−isRFC822Date s
453− = case parseTimeM True defaultTimeLocale rfc822DateFormat (SBSC.unpack s) of
454− Nothing -> False
455− Just (_ :: UTCTime) -> True
456−
457−status2XX :: Monad m => Maybe Request -> Response LBS.ByteString -> T.Text -> m ()
458−status2XX mreq resp t
459− | status200 <= responseStatus resp && responseStatus resp < status300
377+status2XX r = status200 <= responseStatus r && responseStatus r < status300
460− = return ()
461− | otherwise = throw $ PredicateFailure t mreq resp
src/Servant/QuickCheck/Internal/QuickCheck.hs+27−95
@@ -1,31 +1,26 @@
1−{-# LANGUAGE RecordWildCards #-}
2−{-# LANGUAGE CPP #-}
3module Servant.QuickCheck.Internal.QuickCheck where
1module Servant.QuickCheck.Internal.QuickCheck where
4
2
5−import Control.Concurrent (tryReadMVar, newEmptyMVar, tryPutMVar)
6−import Control.Monad (unless)
7import qualified Data.ByteString.Lazy as LBS
3import qualified Data.ByteString.Lazy as LBS
8import Data.Proxy (Proxy)
4import Data.Proxy (Proxy)
5+import Data.String (IsString (..))
6+import Data.Text (Text)
7+import GHC.Generics (Generic)
9import qualified Network.HTTP.Client as C
8import qualified Network.HTTP.Client as C
10import Network.Wai.Handler.Warp (withApplication)
9import Network.Wai.Handler.Warp (withApplication)
11import Prelude.Compat
10import Prelude.Compat
12import Servant (Context (EmptyContext), HasServer,
11import Servant (Context (EmptyContext), HasServer,
13 Server, serveWithContext)
12 Server, serveWithContext)
14−#if MIN_VERSION_servant_server(0,18,0)
15−import Servant (DefaultErrorFormatters, ErrorFormatters, HasContextEntry, type (.++))
16−#endif
17import Servant.Client (BaseUrl (..), Scheme (..))
13import Servant.Client (BaseUrl (..), Scheme (..))
18import System.IO.Unsafe (unsafePerformIO)
14import System.IO.Unsafe (unsafePerformIO)
19import Test.Hspec (Expectation, expectationFailure)
15import Test.Hspec (Expectation, expectationFailure)
20−import Test.QuickCheck (Args (..), Result (..), quickCheckWithResult)
16+import Test.QuickCheck (Args (..), Result (..),
17+ quickCheckWithResult)
21−import Test.QuickCheck.Monadic (assert, forAllM, monadicIO, monitor,
18+import Test.QuickCheck.Monadic (assert, forAllM, monadicIO, run)
22− run)
23−import Test.QuickCheck.Property (counterexample)
24
19
25import Servant.QuickCheck.Internal.Equality
20import Servant.QuickCheck.Internal.Equality
26−import Servant.QuickCheck.Internal.ErrorTypes
27import Servant.QuickCheck.Internal.HasGenRequest
21import Servant.QuickCheck.Internal.HasGenRequest
28import Servant.QuickCheck.Internal.Predicates
22import Servant.QuickCheck.Internal.Predicates
23+import Servant.QuickCheck.Internal.ErrorTypes
29
24
30
25
31-- | Start a servant application on an open port, run the provided function,
26-- | Start a servant application on an open port, run the provided function,
@@ -40,11 +35,7 @@ withServantServer api = withServantServerAndContext api EmptyContext
40-- application.
35-- application.
41--
36--
42-- /Since 0.0.0.0/
37-- /Since 0.0.0.0/
43−#if MIN_VERSION_servant_server(0,18,0)
44−withServantServerAndContext :: (HasServer a ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters)
45−#else
46withServantServerAndContext :: HasServer a ctx
38withServantServerAndContext :: HasServer a ctx
47−#endif
48 => Proxy a -> Context ctx -> IO (Server a) -> (BaseUrl -> IO r) -> IO r
39 => Proxy a -> Context ctx -> IO (Server a) -> (BaseUrl -> IO r) -> IO r
49withServantServerAndContext api ctx server t
40withServantServerAndContext api ctx server t
50 = withApplication (return . serveWithContext api ctx =<< server) $ \port ->
41 = withApplication (return . serveWithContext api ctx =<< server) $ \port ->
@@ -62,47 +53,21 @@ withServantServerAndContext api ctx server t
62-- Evidently, if the behaviour of the server is expected to be
53-- Evidently, if the behaviour of the server is expected to be
63-- non-deterministic, this function may produce spurious failures
54-- non-deterministic, this function may produce spurious failures
64--
55--
65−-- Note that only valid requests are generated and tested. As an example of why
66−-- this matters, let's say your API specifies that a particular endpoint can
67−-- only generate @JSON@. @serversEqual@ will then not generate any requests
68−-- with an @Accept@ header _other_ than @application/json@. It may therefore
69−-- fail to notice that one application, when the request has @Accept:
70−-- text/html@, returns a @406 Not Acceptable@ HTTP response, and another
71−-- returns a @200 Success@, but with @application/json@ as the content-type.
72−--
73−-- The fact that only valid requests are tested also means that no endpoints
74−-- not listed in the API type are tested.
75−--
76-- /Since 0.0.0.0/
56-- /Since 0.0.0.0/
77serversEqual :: HasGenRequest a =>
57serversEqual :: HasGenRequest a =>
78 Proxy a -> BaseUrl -> BaseUrl -> Args -> ResponseEquality LBS.ByteString -> Expectation
58 Proxy a -> BaseUrl -> BaseUrl -> Args -> ResponseEquality LBS.ByteString -> Expectation
79serversEqual api burl1 burl2 args req = do
59serversEqual api burl1 burl2 args req = do
80− let reqs = (\f -> (f burl1, f burl2)) <$> runGenRequest api
60+ let reqs = (\f -> (f burl1, f burl2)) <$> genRequest api
81− -- This MVar stuff is clunky! But there doesn't seem to be an easy way to
82− -- return results when a test fails, since an exception is throw.
83− deetsMVar <- newEmptyMVar
84− r <- quickCheckWithResult args { chatty = False } $ monadicIO $ forAllM reqs $ \(req1, req2) -> do
61+ r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \(req1, req2) -> do
85 resp1 <- run $ C.httpLbs (noCheckStatus req1) defManager
62 resp1 <- run $ C.httpLbs (noCheckStatus req1) defManager
86 resp2 <- run $ C.httpLbs (noCheckStatus req2) defManager
63 resp2 <- run $ C.httpLbs (noCheckStatus req2) defManager
87− unless (getResponseEquality req resp1 resp2) $ do
64+ assert $ getResponseEquality req resp1 resp2
88− monitor (counterexample "hi" )
89− _ <- run $ tryPutMVar deetsMVar $ ServerEqualityFailure req1 resp1 resp2
90− assert False
91 case r of
65 case r of
92 Success {} -> return ()
66 Success {} -> return ()
93− Failure{..} -> do
94− mx <- tryReadMVar deetsMVar
95− case mx of
96− Just x ->
97− expectationFailure $ "Failed:\n" ++ show x
98− Nothing ->
99− expectationFailure $ "We failed to record a reason for failure: " <> show r
100 GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
67 GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
68+ Failure { output = m } -> expectationFailure $ "Failed:\n" ++ show m
101− NoExpectedFailure {} -> expectationFailure "No expected failure"
69+ NoExpectedFailure {} -> expectationFailure $ "No expected failure"
102−#if MIN_VERSION_QuickCheck(2,12,0)
103−#else
104− InsufficientCoverage {} -> expectationFailure "Insufficient coverage"
70+ InsufficientCoverage {} -> expectationFailure $ "Insufficient coverage"
105−#endif
106
71
107-- | Check that a server satisfies the set of properties specified.
72-- | Check that a server satisfies the set of properties specified.
108--
73--
@@ -124,69 +89,36 @@ serversEqual api burl1 burl2 args req = do
124--
89--
125-- /Since 0.0.0.0/
90-- /Since 0.0.0.0/
126serverSatisfies :: (HasGenRequest a) =>
91serverSatisfies :: (HasGenRequest a) =>
127− Proxy a -> BaseUrl -> Args -> Predicates -> Expectation
92+ Proxy a -> BaseUrl -> Args -> Predicates [Text] [Text] -> Expectation
128−serverSatisfies api = serverSatisfiesMgr api defManager
129−
130−-- | Check that a server satisfies the set of properties specified, and
131−-- accept a 'Manager' for running the HTTP requests through.
132−--
133−-- See 'serverSatisfies' for more details.
134−--
135−-- @since 0.0.7.2
136−serverSatisfiesMgr :: (HasGenRequest a) =>
137− Proxy a -> C.Manager -> BaseUrl -> Args -> Predicates -> Expectation
138−serverSatisfiesMgr api manager burl args preds = do
93+serverSatisfies api burl args preds = do
139− let reqs = ($ burl) <$> runGenRequest api
94+ let reqs = ($ burl) <$> genRequest api
140− deetsMVar <- newEmptyMVar
141− r <- quickCheckWithResult args { chatty = False } $ monadicIO $ forAllM reqs $ \req -> do
95+ r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \req -> do
142− v <- run $ finishPredicates preds (noCheckStatus req) manager
96+ v <- run $ finishPredicates preds (noCheckStatus req) defManager
143− _ <- run $ tryPutMVar deetsMVar v
144− case v of
145− Just _ -> assert False
97+ assert $ null v
146− _ -> return ()
147 case r of
98 case r of
148 Success {} -> return ()
99 Success {} -> return ()
149− Failure {..} -> do
150− mx <- tryReadMVar deetsMVar
151− case mx of
152− Just x ->
153− expectationFailure $ "Failed:\n" ++ show x
154− Nothing ->
155− expectationFailure $ "We failed to record a reason for failure: " <> show r
156 GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
100 GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
101+ Failure { output = m } -> expectationFailure $ "Failed:\n" ++ show m
157 NoExpectedFailure {} -> expectationFailure $ "No expected failure"
102 NoExpectedFailure {} -> expectationFailure $ "No expected failure"
158−#if MIN_VERSION_QuickCheck(2,12,0)
159−#else
160− InsufficientCoverage {} -> expectationFailure "Insufficient coverage"
103+ InsufficientCoverage {} -> expectationFailure $ "Insufficient coverage"
161−#endif
162
104
163−serverDoesntSatisfy :: (HasGenRequest a) =>
164− Proxy a -> BaseUrl -> Args -> Predicates -> Expectation
165−serverDoesntSatisfy api = serverDoesntSatisfyMgr api defManager
166
105
167−serverDoesntSatisfyMgr :: (HasGenRequest a) =>
106+serverDoesntSatisfy :: (HasGenRequest a) =>
168− Proxy a -> C.Manager -> BaseUrl -> Args -> Predicates -> Expectation
107+ Proxy a -> BaseUrl -> Args -> Predicates [Text] [Text] -> Expectation
169−serverDoesntSatisfyMgr api manager burl args preds = do
108+serverDoesntSatisfy api burl args preds = do
170− let reqs = ($ burl) <$> runGenRequest api
109+ let reqs = ($ burl) <$> genRequest api
171 r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \req -> do
110 r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \req -> do
172− v <- run $ finishPredicates preds (noCheckStatus req) manager
111+ v <- run $ finishPredicates preds (noCheckStatus req) defManager
173 assert $ not $ null v
112 assert $ not $ null v
174 case r of
113 case r of
175 Success {} -> return ()
114 Success {} -> return ()
176 GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
115 GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
177 Failure { output = m } -> expectationFailure $ "Failed:\n" ++ show m
116 Failure { output = m } -> expectationFailure $ "Failed:\n" ++ show m
178 NoExpectedFailure {} -> expectationFailure $ "No expected failure"
117 NoExpectedFailure {} -> expectationFailure $ "No expected failure"
179−#if MIN_VERSION_QuickCheck(2,12,0)
180−#else
181− InsufficientCoverage {} -> expectationFailure "Insufficient coverage"
118+ InsufficientCoverage {} -> expectationFailure $ "Insufficient coverage"
182−#endif
183
119
184noCheckStatus :: C.Request -> C.Request
120noCheckStatus :: C.Request -> C.Request
185−#if MIN_VERSION_http_client(0,5,0)
186−noCheckStatus = id
187−#else
188noCheckStatus r = r { C.checkStatus = \_ _ _ -> Nothing}
121noCheckStatus r = r { C.checkStatus = \_ _ _ -> Nothing}
189−#endif
190
122
191defManager :: C.Manager
123defManager :: C.Manager
192defManager = unsafePerformIO $ C.newManager C.defaultManagerSettings
124defManager = unsafePerformIO $ C.newManager C.defaultManagerSettings
stack.yaml+31−5
@@ -1,10 +1,36 @@
1+# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
1−resolver: nightly-2018-09-03
2+resolver: nightly-2016-04-20
3+
4+# Local packages, usually specified by relative directory name
2packages:
5packages:
3- '.'
6- '.'
7+
8+# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
4extra-deps:
9extra-deps:
5−- hspec-discover-2.5.6
10+- servant-0.7
6−- hspec-core-2.5.6
11+- servant-client-0.7
7−- hspec-2.5.6
12+- servant-server-0.7
8−- QuickCheck-2.12
13+
14+# Override default flag values for local packages and extra-deps
9flags: {}
15flags: {}
16+
17+# Extra package databases containing global packages
10extra-package-dbs: []
18extra-package-dbs: []
19+
20+# Control whether we use the GHC we find on the path
21+# system-ghc: true
22+
23+# Require a specific version of stack, using version ranges
24+# require-stack-version: -any # Default
25+# require-stack-version: >= 1.0.0
26+
27+# Override the architecture used by stack, especially useful on Windows
28+# arch: i386
29+# arch: x86_64
30+
31+# Extra directories used by stack for building
32+# extra-include-dirs: [/path/to/dir]
33+# extra-lib-dirs: [/path/to/dir]
34+
35+# Allow a newer minor version of GHC than the snapshot specifies
36+# compiler-check: newer-minor
test/Doctest.hsnew file+44
@@ -0,0 +1,44 @@
1+module Main where
2+
3+import Data.List (isPrefixOf)
4+import System.Directory
5+import System.FilePath
6+import System.FilePath.Find
7+import Test.DocTest
8+
9+main :: IO ()
10+main = do
11+ files <- find always (extension ==? ".hs") "src"
12+ mCabalMacrosFile <- getCabalMacrosFile
13+ doctest $ "-isrc" : "-Iinclude" :
14+ (maybe [] (\ f -> ["-optP-include", "-optP" ++ f]) mCabalMacrosFile) ++
15+ "-XOverloadedStrings" :
16+ "-XDeriveFunctor" :
17+ "-XFlexibleInstances" :
18+ "-XFlexibleContexts" :
19+ "-XMultiParamTypeClasses" :
20+ "-XDataKinds" :
21+ "-XTypeOperators" :
22+ "-XGADTs" :
23+ files
24+
25+getCabalMacrosFile :: IO (Maybe FilePath)
26+getCabalMacrosFile = do
27+ exists <- doesDirectoryExist "dist"
28+ if exists
29+ then do
30+ contents <- getDirectoryContents "dist"
31+ let rest = "build" </> "autogen" </> "cabal_macros.h"
32+ whenExists $ case filter ("dist-sandbox-" `isPrefixOf`) contents of
33+ [x] -> "dist" </> x </> rest
34+ [] -> "dist" </> rest
35+ xs -> error $ "ran doctests with multiple dist/dist-sandbox-xxxxx's: \n"
36+ ++ show xs ++ "\nTry cabal clean"
37+ else return Nothing
38+ where
39+ whenExists :: FilePath -> IO (Maybe FilePath)
40+ whenExists file = do
41+ exists <- doesFileExist file
42+ return $ if exists
43+ then Just file
44+ else Nothing
test/Servant/QuickCheck/InternalSpec.hs+33−274
@@ -1,46 +1,28 @@
1{-# LANGUAGE CPP #-}
1{-# LANGUAGE CPP #-}
2module Servant.QuickCheck.InternalSpec (spec) where
2module Servant.QuickCheck.InternalSpec (spec) where
3
3
4−
5−import Control.Concurrent.MVar (newMVar, readMVar, swapMVar)
4+import Control.Concurrent.MVar (newMVar, readMVar,
6−import Control.Exception (SomeException)
5+ swapMVar)
7−import Control.Monad (replicateM)
8−import Control.Monad.IO.Class (liftIO)
6+import Control.Monad.IO.Class (liftIO)
9−import qualified Data.ByteString as BS
10−import qualified Data.ByteString.Char8 as C
11−import Data.Maybe (fromJust)
12−import Network.HTTP.Client (path, queryString)
13import Prelude.Compat
7import Prelude.Compat
14import Servant
8import Servant
15−import Servant.HTML.Blaze (HTML)
9+import Servant.API.Internal.Test.ComprehensiveAPI (comprehensiveAPI)
16−import qualified Text.Blaze.Html as Blaze
17−import qualified Text.Blaze.Html5 as Blaze5
18−import Test.Hspec (Spec, context, describe, it, shouldBe,
10+import Test.Hspec (Spec, context,
19− shouldContain)
11+ describe, it,
12+ pending, shouldBe)
20−import Test.Hspec.Core.Spec (Arg, Example, Result (..), ResultStatus (..),
13+import Test.Hspec.Core.Spec (Arg, Example,
14+ Result (..),
21− defaultParams, safeEvaluateExample)
15+ defaultParams,
22−import Test.QuickCheck.Gen (generate, unGen)
16+ evaluateExample)
23−import Test.QuickCheck.Random (mkQCGen)
17+
24−
25−
26−import Servant.Test.ComprehensiveAPI (comprehensiveAPIWithoutStreamingOrRaw)
27−
28−import Servant.QuickCheck
18+import Servant.QuickCheck
29−import Servant.QuickCheck.Internal (genRequest, runGenRequest,
19+import Servant.QuickCheck.Internal (genRequest, Failure(..), serverDoesntSatisfy)
30− serverDoesntSatisfy)
31
20
32spec :: Spec
21spec :: Spec
33spec = do
22spec = do
34 serversEqualSpec
23 serversEqualSpec
35 serverSatisfiesSpec
24 serverSatisfiesSpec
36 isComprehensiveSpec
25 isComprehensiveSpec
37− onlyJsonObjectSpec
38− notLongerThanSpec
39− queryParamsSpec
40− queryFlagsSpec
41− deepPathSpec
42− htmlDocTypesSpec
43− unbiasedGenerationSpec
44
26
45serversEqualSpec :: Spec
27serversEqualSpec :: Spec
46serversEqualSpec = describe "serversEqual" $ do
28serversEqualSpec = describe "serversEqual" $ do
@@ -51,41 +33,21 @@ serversEqualSpec = describe "serversEqual" $ do
51 serversEqual api burl1 burl2 args bodyEquality
33 serversEqual api burl1 burl2 args bodyEquality
52
34
53 context "when servers are not equal" $ do
35 context "when servers are not equal" $ do
36+
54− it "provides the failing responses in the error message" $ do
37+ it "provides the failing requests in the error message" $ do
55− FailedWith err <- withServantServer api2 server2 $ \burl1 ->
38+ Fail _ err <- withServantServer api2 server2 $ \burl1 ->
56 withServantServer api2 server3 $ \burl2 -> do
39 withServantServer api2 server3 $ \burl2 -> do
57 evalExample $ serversEqual api2 burl1 burl2 args bodyEquality
40 evalExample $ serversEqual api2 burl1 burl2 args bodyEquality
58− show err `shouldContain` "Server equality failed"
41+ let ServerEqualityFailure req _ _ = read err
59− show err `shouldContain` "Body: 1"
42+ req `shouldBe` "failplz"
60− show err `shouldContain` "Body: 2"
61− show err `shouldContain` "Path: /failplz"
62−
63− context "when JSON is equal but looks a bit different as a ByteString" $ do
64−
65− it "sanity check: different whitespace same JSON objects bodyEquality fails" $ do
66− FailedWith err <- withServantServer jsonApi jsonServer1 $ \burl1 ->
67− withServantServer jsonApi jsonServer2 $ \burl2 -> do
68− evalExample $ serversEqual jsonApi burl1 burl2 args bodyEquality
69− show err `shouldContain` "Server equality failed"
70−
71− it "jsonEquality considers equal JSON apis equal regardless of key ordering or whitespace" $ do
72− withServantServerAndContext jsonApi ctx jsonServer1 $ \burl1 ->
73− withServantServerAndContext jsonApi ctx jsonServer2 $ \burl2 ->
74− serversEqual jsonApi burl1 burl2 args jsonEquality
75
43
76− it "sees when JSON apis are not equal because any value is different" $ do
44+ it "provides the failing responses in the error message" $ do
77− FailedWith err <- withServantServer jsonApi jsonServer2 $ \burl1 ->
45+ Fail _ err <- withServantServer api2 server2 $ \burl1 ->
78− withServantServer jsonApi jsonServer3 $ \burl2 -> do
46+ withServantServer api2 server3 $ \burl2 -> do
79− evalExample $ serversEqual jsonApi burl1 burl2 args jsonEquality
47+ evalExample $ serversEqual api2 burl1 burl2 args bodyEquality
80− show err `shouldContain` "Server equality failed"
48+ let ServerEqualityFailure _ r1 r2 = read err
81− show err `shouldContain` "Path: /jsonComparison"
49+ r1 `shouldBe` "1"
82−
50+ r2 `shouldBe` "2"
83− it "sees when JSON apis are not equal due to different keys but same values" $ do
84− FailedWith err <- withServantServer jsonApi jsonServer2 $ \burl1 ->
85− withServantServer jsonApi jsonServer4 $ \burl2 -> do
86− evalExample $ serversEqual jsonApi burl1 burl2 args jsonEquality
87− show err `shouldContain` "Server equality failed"
88− show err `shouldContain` "Path: /jsonComparison"
89
51
90
52
91serverSatisfiesSpec :: Spec
53serverSatisfiesSpec :: Spec
@@ -105,126 +67,16 @@ serverSatisfiesSpec = describe "serverSatisfies" $ do
105 <%> notAllowedContainsAllowHeader
67 <%> notAllowedContainsAllowHeader
106 <%> mempty)
68 <%> mempty)
107
69
108− context "when predicates are false" $ do
70+ context "when predicates are false" $
109−
110− it "fails with informative error messages" $ do
71+ it "fails with informative error messages" $ pending
111− FailedWith err <- withServantServerAndContext api ctx server $ \burl -> do
112− evalExample $ serverSatisfies api burl args (notAllowedContainsAllowHeader <%> mempty)
113− show err `shouldContain` "notAllowedContainsAllowHeader"
114− show err `shouldContain` "Headers"
115− show err `shouldContain` "Body"
116−
117−
118−onlyJsonObjectSpec :: Spec
119−onlyJsonObjectSpec = describe "onlyJsonObjects" $ do
120−
121− it "fails correctly" $ do
122− FailedWith err <- withServantServerAndContext api ctx server $ \burl -> do
123− evalExample $ serverSatisfies (Proxy :: Proxy (Get '[JSON] Int)) burl args
124− (onlyJsonObjects <%> mempty)
125− show err `shouldContain` "onlyJsonObjects"
126−
127− it "accepts non-JSON endpoints" $ do
128− withServantServerAndContext octetAPI ctx serverOctetAPI $ \burl ->
129− serverSatisfies octetAPI burl args (onlyJsonObjects <%> mempty)
130−
131− it "does not fail when there is no content-type" $ do
132− withServantServerAndContext api2 ctx serverFailing $ \burl ->
133− serverSatisfies api2 burl args (onlyJsonObjects <%> mempty)
134−
135−notLongerThanSpec :: Spec
136−notLongerThanSpec = describe "notLongerThan" $ do
137−
138− it "fails correctly" $ do
139− FailedWith err <- withServantServerAndContext api ctx server $ \burl -> do
140− evalExample $ serverSatisfies (Proxy :: Proxy (Get '[JSON] Int)) burl args
141− (notLongerThan 1 <%> mempty)
142− show err `shouldContain` "notLongerThan"
143−
144− it "succeeds correctly" $ do
145− withServantServerAndContext api ctx server $ \burl ->
146− serverSatisfies api burl args (notLongerThan 1000000000000 <%> mempty)
147
72
148isComprehensiveSpec :: Spec
73isComprehensiveSpec :: Spec
149isComprehensiveSpec = describe "HasGenRequest" $ do
74isComprehensiveSpec = describe "HasGenRequest" $ do
150
75
151 it "has instances for all 'servant' combinators" $ do
76 it "has instances for all 'servant' combinators" $ do
152− let _g = genRequest comprehensiveAPIWithoutStreamingOrRaw
77+ let _g = genRequest comprehensiveAPI
153 True `shouldBe` True -- This is a type-level check
78 True `shouldBe` True -- This is a type-level check
154
79
155−deepPathSpec :: Spec
156−deepPathSpec = describe "Path components" $ do
157−
158− it "are separated by slashes, without a trailing slash" $ do
159− let rng = mkQCGen 0
160− burl = BaseUrl Http "localhost" 80 ""
161− gen = runGenRequest deepAPI
162− req = (unGen gen rng 0) burl
163− path req `shouldBe` ("/one/two/three")
164−
165−
166−queryParamsSpec :: Spec
167−queryParamsSpec = describe "QueryParams" $ do
168−
169− it "reduce to an HTTP query string correctly" $ do
170− let rng = mkQCGen 0
171− burl = BaseUrl Http "localhost" 80 ""
172− gen = runGenRequest paramsAPI
173− req = (unGen gen rng 0) burl
174− qs = C.unpack $ queryString req
175− qs `shouldBe` "one=_&two=_"
176−
177−queryFlagsSpec :: Spec
178−queryFlagsSpec = describe "QueryFlags" $ do
179−
180− it "reduce to an HTTP query string correctly" $ do
181− let rng = mkQCGen 0
182− burl = BaseUrl Http "localhost" 80 ""
183− gen = runGenRequest flagsAPI
184− req = (unGen gen rng 0) burl
185− qs = C.unpack $ queryString req
186− qs `shouldBe` "one&two"
187−
188−htmlDocTypesSpec :: Spec
189−htmlDocTypesSpec = describe "HtmlDocTypes" $ do
190−
191− it "fails HTML without doctype correctly" $ do
192− err <- withServantServerAndContext docTypeApi ctx noDocTypeServer $ \burl -> do
193− evalExample $ serverSatisfies docTypeApi burl args
194− (htmlIncludesDoctype <%> mempty)
195− show err `shouldContain` "htmlIncludesDoctype"
196−
197− it "passes HTML with a doctype at start" $ do
198− withServantServerAndContext docTypeApi ctx docTypeServer $ \burl ->
199− serverSatisfies docTypeApi burl args (htmlIncludesDoctype <%> mempty)
200−
201− it "accepts json endpoints and passes over them in silence" $ do
202− withServantServerAndContext api ctx server $ \burl -> do
203− serverSatisfies (Proxy :: Proxy (Get '[JSON] Int)) burl args
204− (htmlIncludesDoctype <%> mempty)
205−
206−
207−makeRandomRequest :: Proxy LargeAPI -> BaseUrl -> IO Integer
208−makeRandomRequest large burl = do
209− req <- generate $ runGenRequest large
210− pure $ fst . fromJust . C.readInteger . C.drop 1 . path $ req burl
211−
212−
213−unbiasedGenerationSpec :: Spec
214−unbiasedGenerationSpec = describe "Unbiased Generation of requests" $
215−
216− it "frequency paired with generated endpoint should be more randomly distributed" $ do
217− let burl = BaseUrl Http "localhost" 80 ""
218− let runs = 10000 :: Double
219− someRequests <- replicateM 10000 (makeRandomRequest largeApi burl)
220− let mean = (sum $ map fromIntegral someRequests) / runs
221− let variancer x = let ix = fromIntegral x in (ix - mean) * (ix - mean)
222− let variance = (sum $ map variancer someRequests) / runs - 1
223− -- mean should be around 8.5. If this fails, we likely need more runs (or there's a bug!)
224− mean > 8 `shouldBe` True
225− mean < 9 `shouldBe` True
226− -- Std dev is likely around 4. Variance is probably greater than 20.
227− variance > 19.5 `shouldBe` True
228
80
229------------------------------------------------------------------------------
81------------------------------------------------------------------------------
230-- APIs
82-- APIs
@@ -237,17 +89,6 @@ type API = ReqBody '[JSON] String :> Post '[JSON] String
237api :: Proxy API
89api :: Proxy API
238api = Proxy
90api = Proxy
239
91
240−type ParamsAPI = QueryParam "one" () :> QueryParam "two" () :> Get '[JSON] ()
241−
242−paramsAPI :: Proxy ParamsAPI
243−paramsAPI = Proxy
244−
245−type FlagsAPI = QueryFlag "one" :> QueryFlag "two" :> Get '[JSON] ()
246−
247−flagsAPI :: Proxy FlagsAPI
248−flagsAPI = Proxy
249−
250−
251server :: IO (Server API)
92server :: IO (Server API)
252server = do
93server = do
253 mvar <- newMVar ""
94 mvar <- newMVar ""
@@ -261,105 +102,23 @@ type API2 = "failplz" :> Get '[JSON] Int
261api2 :: Proxy API2
102api2 :: Proxy API2
262api2 = Proxy
103api2 = Proxy
263
104
264−type DeepAPI = "one" :> "two" :> "three":> Get '[JSON] ()
265−
266−deepAPI :: Proxy DeepAPI
267−deepAPI = Proxy
268−
269−
270server2 :: IO (Server API2)
105server2 :: IO (Server API2)
271server2 = return $ return 1
106server2 = return $ return 1
272
107
273server3 :: IO (Server API2)
108server3 :: IO (Server API2)
274server3 = return $ return 2
109server3 = return $ return 2
275
110
276−serverFailing :: IO (Server API2)
277−serverFailing = return . throwError $ err405
278−
279−-- With Doctypes
280−type HtmlDoctype = Get '[HTML] Blaze.Html
281−
282−docTypeApi :: Proxy HtmlDoctype
283−docTypeApi = Proxy
284−
285−docTypeServer :: IO (Server HtmlDoctype)
286−docTypeServer = pure $ pure $ Blaze5.docTypeHtml $ Blaze5.span "Hello Test!"
287−
288−noDocTypeServer :: IO (Server HtmlDoctype)
289−noDocTypeServer = pure $ pure $ Blaze.text "Hello Test!"
290−
291−
292−-- Api for unbiased generation of requests tests
293−largeApi :: Proxy LargeAPI
294−largeApi = Proxy
295−
296−type LargeAPI
297− = "1" :> Get '[JSON] Int
298− :<|> "2" :> Get '[JSON] Int
299− :<|> "3" :> Get '[JSON] Int
300− :<|> "4" :> Get '[JSON] Int
301− :<|> "5" :> Get '[JSON] Int
302− :<|> "6" :> Get '[JSON] Int
303− :<|> "7" :> Get '[JSON] Int
304− :<|> "8" :> Get '[JSON] Int
305− :<|> "9" :> Get '[JSON] Int
306− :<|> "10" :> Get '[JSON] Int
307− :<|> "11" :> Get '[JSON] Int
308− :<|> "12" :> Get '[JSON] Int
309− :<|> "13" :> Get '[JSON] Int
310− :<|> "14" :> Get '[JSON] Int
311− :<|> "15" :> Get '[JSON] Int
312− :<|> "16" :> Get '[JSON] Int
313−
314−
315−type OctetAPI = Get '[OctetStream] BS.ByteString
316−
317−octetAPI :: Proxy OctetAPI
318−octetAPI = Proxy
319−
320−serverOctetAPI :: IO (Server OctetAPI)
321−serverOctetAPI = return $ return "blah"
322−
323−type JsonApi = "jsonComparison" :> Get '[OctetStream] BS.ByteString
324−
325−jsonApi :: Proxy JsonApi
326−jsonApi = Proxy
327−
328−jsonServer1 :: IO (Server JsonApi)
329−jsonServer1 = return $ return "{ \"b\": [\"b\"], \"a\": 1 }" -- whitespace, ordering different
330−
331−jsonServer2 :: IO (Server JsonApi)
332−jsonServer2 = return $ return "{\"a\": 1,\"b\":[\"b\"]}"
333−
334−jsonServer3 :: IO (Server JsonApi)
335−jsonServer3 = return $ return "{\"a\": 2, \"b\": [\"b\"]}"
336−
337−jsonServer4 :: IO (Server JsonApi)
338−jsonServer4 = return $ return "{\"c\": 1, \"d\": [\"b\"]}"
339−
340−
341ctx :: Context '[BasicAuthCheck ()]
111ctx :: Context '[BasicAuthCheck ()]
342ctx = BasicAuthCheck (const . return $ NoSuchUser) :. EmptyContext
112ctx = BasicAuthCheck (const . return $ NoSuchUser) :. EmptyContext
343------------------------------------------------------------------------------
113------------------------------------------------------------------------------
344-- Utils
114-- Utils
345------------------------------------------------------------------------------
115------------------------------------------------------------------------------
116+
346−evalExample :: (Example e, Arg e ~ ()) => e -> IO EvalResult
117+evalExample :: (Example e, Arg e ~ ()) => e -> IO Result
347−evalExample e = do
348− r <- safeEvaluateExample e defaultParams ($ ()) progCallback
118+evalExample e = evaluateExample e defaultParams ($ ()) progCallback
349− case resultStatus r of
350− Success -> return $ AllGood
351− Failure _ reason -> return $ FailedWith $ show reason
352− Pending {} -> error "should not happen"
353 where
119 where
354 progCallback _ = return ()
120 progCallback _ = return ()
355
121
356−data EvalResult
357− = AnException SomeException
358− | AllGood
359− | FailedWith String
360− deriving (Show)
361−
362−
363args :: Args
122args :: Args
364args = defaultArgs { maxSuccess = noOfTestCases }
123args = defaultArgs { maxSuccess = noOfTestCases }
365
124
@@ -367,5 +126,5 @@ noOfTestCases :: Int
367#if LONG_TESTS
126#if LONG_TESTS
368noOfTestCases = 20000
127noOfTestCases = 20000
369#else
128#else
370−noOfTestCases = 1000
129+noOfTestCases = 500
371#endif
130#endif