on master help sign in
Merge pull request #13 from mwotton/morecoverage

Morecoverage

commitbfb5bd43efeb157a312c2b08b50020bb79a519f2 authorMark Wotton <mwotton@gmail.com> dateMon 30 Nov 2020, 14:30 (6 years ago) parent4efbf035b0cfe1
18 files changed+550−342
.github/workflows/ci.yml+4−1 package.yaml+3 roboservant.cabal+9−1 src/Roboservant.hs+2−2 src/Roboservant/Direct.hs+243−144 src/Roboservant/Types.hs+5 src/Roboservant/Types/Breakdown.hs+28−73 src/Roboservant/Types/BuildFrom.hs+41 src/Roboservant/Types/Internal.hs+36 src/Roboservant/Types/ReifiedApi.hs+5−3 stack.yaml+5−17 stack.yaml.lock+22−76 test/Foo.hs+2−1 test/Headers.hs+2−1 test/Post.hs+27 test/Seeded.hs+2 test/Spec.hs+90−22 test/Valid.hs+24−1
.github/workflows/ci.yml+4−1
@@ -18,7 +18,8 @@ jobs:
1818 - uses: actions/checkout@v2
1919 if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
2020
21− - uses: actions/setup-haskell@v1.1
21+ - uses: actions/setup-haskell@v1.1.4
22+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
2223 name: Setup Haskell Stack
2324 with:
2425 ghc-version: ${{ matrix.ghc }}
@@ -26,11 +27,13 @@ jobs:
2627 enable-stack: true
2728
2829 - uses: actions/cache@v1
30+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
2931 name: Cache ~/.stack
3032 with:
3133 path: ~/.stack
3234 key: ${{ runner.os }}-${{ matrix.ghc }}-stack
3335
3436 - name: Test
37+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
3538 run: |
3639 stack test
package.yaml+3
@@ -17,6 +17,7 @@ dependencies:
1717- bytestring
1818- containers
1919- random
20+- hashable
2021- lifted-base
2122- monad-control
2223- mtl
@@ -28,6 +29,7 @@ dependencies:
2829- vinyl
2930- dependent-sum
3031- dependent-map
32+- unordered-containers
3133- text
3234- time
3335
@@ -51,3 +53,4 @@ tests:
5153 - aeson
5254 - hspec
5355 - hspec-core
56+ - http-api-data
roboservant.cabal+9−1
@@ -4,7 +4,7 @@ cabal-version: 1.12
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: 49f8c03988a25b7e35dddaab65f250955a078392aaa293298885aea164315479
7+-- hash: 0c7e475fb1182766bcb6f2b602d95ca2ce986757d1d23c061fb2800fee49f483
88
99name: roboservant
1010version: 0.1.0.2
@@ -31,7 +31,9 @@ library
3131 Roboservant.Direct
3232 Roboservant.Types
3333 Roboservant.Types.Breakdown
34+ Roboservant.Types.BuildFrom
3435 Roboservant.Types.FlattenServer
36+ Roboservant.Types.Internal
3537 Roboservant.Types.ReifiedApi
3638 other-modules:
3739 Paths_roboservant
@@ -44,6 +46,7 @@ library
4446 , containers
4547 , dependent-map
4648 , dependent-sum
49+ , hashable
4750 , lifted-base
4851 , monad-control
4952 , mtl
@@ -55,6 +58,7 @@ library
5558 , string-conversions
5659 , text
5760 , time
61+ , unordered-containers
5862 , vinyl
5963 default-language: Haskell2010
6064
@@ -65,6 +69,7 @@ test-suite roboservant-test
6569 BuildFrom
6670 Foo
6771 Headers
72+ Post
6873 Seeded
6974 UnsafeIO
7075 Valid
@@ -79,8 +84,10 @@ test-suite roboservant-test
7984 , containers
8085 , dependent-map
8186 , dependent-sum
87+ , hashable
8288 , hspec
8389 , hspec-core
90+ , http-api-data
8491 , lifted-base
8592 , monad-control
8693 , mtl
@@ -93,5 +100,6 @@ test-suite roboservant-test
93100 , string-conversions
94101 , text
95102 , time
103+ , unordered-containers
96104 , vinyl
97105 default-language: Haskell2010
src/Roboservant.hs+2−2
@@ -1,5 +1,5 @@
11module Roboservant (module Roboservant.Direct
2− ,module Roboservant.Types.Breakdown) where
2+ ,module Roboservant.Types) where
33
44import Roboservant.Direct
5−import Roboservant.Types.Breakdown
5+import Roboservant.Types
src/Roboservant/Direct.hs+243−144
@@ -1,20 +1,14 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE DerivingStrategies #-}
42{-# LANGUAGE FlexibleContexts #-}
5−{-# LANGUAGE FlexibleInstances #-}
63{-# LANGUAGE GADTs #-}
7−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
84{-# LANGUAGE LambdaCase #-}
9−{-# LANGUAGE PolyKinds #-}
105{-# LANGUAGE RankNTypes #-}
116{-# LANGUAGE RecordWildCards #-}
127{-# LANGUAGE ScopedTypeVariables #-}
13−{-# LANGUAGE StandaloneDeriving #-}
148{-# LANGUAGE TupleSections #-}
159{-# LANGUAGE TypeApplications #-}
1610{-# LANGUAGE TypeFamilies #-}
17−{-# LANGUAGE TypeOperators #-}
11+
1812{-# LANGUAGE UndecidableInstances #-}
1913
2014module Roboservant.Direct
@@ -22,13 +16,44 @@ module Roboservant.Direct
2216
2317 -- TODO come up with something smarter than exporting all this, we should
2418 -- have some nice error-display functions
25− , RoboservantException(..), FuzzState(..), FuzzOp(..)
19+ , RoboservantException(..), FuzzState(..), FuzzOp(..), FailureType(..), Report(..)
2620 )
2721where
2822
29−import Control.Exception.Lifted(throw,handle,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
30−import Control.Monad(void,replicateM)
31−import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,runStateT)
23+import Control.Exception.Lifted
24+ ( throw,
25+ handle,
26+ Handler(..),
27+ Exception,
28+ SomeException,
29+ SomeAsyncException,
30+ catch,
31+ catches )
32+import Control.Exception.Lifted
33+ ( throw,
34+ Handler(..),
35+ Exception,
36+ SomeException,
37+ SomeAsyncException,
38+ catch,
39+ catches )
40+import Control.Exception.Lifted
41+ ( Exception,
42+ Handler(..),
43+ SomeAsyncException,
44+ SomeException,
45+ catch,
46+ catches,
47+ handle,
48+ throw )
49+
50+import Control.Monad.State.Strict
51+ ( MonadState, MonadIO, get, modify', liftIO, runStateT )
52+import Control.Monad.State.Strict
53+ ( MonadState, MonadIO, get, modify', liftIO, execStateT )
54+import Control.Monad.State.Strict
55+ ( MonadIO, MonadState, get, liftIO, modify', runStateT )
56+
3257import Control.Exception.Lifted(throw,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
3358import Control.Monad(void,replicateM,forM_)
3459import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,execStateT)
@@ -40,109 +65,164 @@ import Data.Map.Strict(Map)
4065import qualified Data.Set as Set
4166import Data.Maybe (mapMaybe)
4267import Data.Typeable (TypeRep)
43−import Servant (Endpoints, Proxy (Proxy), Server, ServerError(..))
44−import System.Random(StdGen,randomR,mkStdGen)
68+import Servant ( Endpoints, Proxy(Proxy), Server, ServerError(..) )
69+import Servant ( Endpoints, Proxy(Proxy), Server, ServerError(..) )
70+
71+import System.Random ( StdGen, randomR, mkStdGen )
72+import System.Random ( StdGen, mkStdGen, randomR )
73+
74+import qualified Data.List.NonEmpty as NEL
75+import qualified Data.Map.Strict as Map
76+import Data.Time.Clock
77+import Data.Time.Clock
78+
79+import Data.List(sortOn)
80+import Control.Arrow(second)
4581import System.Timeout.Lifted(timeout)
46−import GHC.Generics ((:*:)(..))
82+import GHC.Generics ( (:*:)(..) )
83+import GHC.Generics ( (:*:)(..) )
84+
4785import qualified Data.Vinyl.Functor as V
86+import qualified Data.Vinyl.Functor as V
87+
88+import qualified Data.Vinyl.Curry as V
4889import qualified Data.Vinyl.Curry as V
90+
91+
92+
93+import Control.Exception.Lifted (Exception, Handler (..), SomeAsyncException, SomeException, catch, catches, handle, throw)
94+import Control.Monad.State.Strict (MonadIO, MonadState, get, liftIO, modify', runStateT)
95+import Control.Monad.Trans.Control (MonadBaseControl)
4996import qualified Data.Dependent.Map as DM
50−import qualified Data.Vinyl as V
97+import Data.Dynamic (Dynamic (..), dynTypeRep)
98+import Data.List.NonEmpty (NonEmpty (..))
5199import qualified Data.List.NonEmpty as NEL
52−import qualified Data.Map.Strict as Map
100+import Data.Maybe (mapMaybe)
101+import qualified Data.Set as Set
53102import Data.Time.Clock
54−import qualified Type.Reflection as R
55−
56−import Roboservant.Types.Breakdown
103+import qualified Data.Vinyl as V
104+import qualified Data.Vinyl.Curry as V
105+import qualified Data.Vinyl.Functor as V
106+import GHC.Generics ((:*:) (..))
57107import Roboservant.Types
58108 ( ApiOffset (..),
109+ Argument (..),
59110 FlattenServer (..),
60−-- ReifiedApi,
111+ Provenance (..),
112+ ReifiedEndpoint (..),
113+ Stash (..),
114+ StashValue (..),
61115 ToReifiedApi (..),
62− ReifiedEndpoint(..),
63− Argument(..),
64116 TypedF
65−
66117 )
118+import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
119+import System.Random (StdGen, mkStdGen, randomR)
120+import qualified Type.Reflection as R
121+import qualified Data.IntSet as IntSet
67122
68123data RoboservantException
69− = RoboservantException FailureType (Maybe SomeException) Int FuzzState
70− deriving (Show)
71−-- we believe in nussink, lebowski
124+ = RoboservantException
125+ { failureReason :: FailureType
126+ , serverException :: Maybe SomeException
127+ , fuzzState :: FuzzState
128+ } deriving (Show)
129+
72130instance Exception RoboservantException
73131
74132data FailureType
75133 = ServerCrashed
76134 | CheckerFailed
77135 | NoPossibleMoves
78− | InsufficientCoverage
136+ | InsufficientCoverage Double
79137 deriving (Show,Eq)
80138
81−
82−data FuzzOp = FuzzOp
83− { apiOffset :: ApiOffset
84− , provenance :: [Provenance]
85− } deriving (Show,Eq)
139+data FuzzOp
140+ = FuzzOp
141+ { apiOffset :: ApiOffset,
142+ provenance :: [Provenance]
143+ }
144+ deriving (Show, Eq)
86145
87146data Config
88147 = Config
89− { seed :: [Dynamic]
90− , maxRuntime :: Integer -- seconds to test for
91− , maxReps :: Integer
92− , rngSeed :: Int
93− , coverageThreshold :: Double
94− }
95−
96−data FuzzState = FuzzState
97− { path :: [FuzzOp]
98− , stash :: Stash
99− , currentRng :: StdGen
100− }
101− deriving Show
102−
103−data EndpointOption = forall as. (V.RecordToList as, V.RMap as) => EndpointOption
104− { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
105− , eoArgs :: V.Rec (TypedF StashValue) as
106− }
148+ { seed :: [(Dynamic,Int)],
149+ maxRuntime :: Double, -- seconds to test for
150+ maxReps :: Integer,
151+ rngSeed :: Int,
152+ coverageThreshold :: Double
153+ }
154+
155+data FuzzState
156+ = FuzzState
157+ { path :: [FuzzOp],
158+ stash :: Stash,
159+ currentRng :: StdGen
160+ }
161+ deriving (Show)
162+
163+data EndpointOption
164+ = forall as.
165+ (V.RecordToList as, V.RMap as) =>
166+ EndpointOption
167+ { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int)))),
168+ eoArgs :: V.Rec (TypedF StashValue) as
169+ }
107170
108171data StopReason
109172 = TimedOut
110173 | HitMaxIterations
111− deriving (Show,Eq)
174+ deriving (Show, Eq)
112175
113176data Report = Report
114− { textual :: String }
115− deriving (Show,Eq)
177+ { textual :: String
178+ , rsException :: RoboservantException}
179+ deriving (Show)
116180
117−fuzz :: forall api. (FlattenServer api, ToReifiedApi (Endpoints api))
118− => Server api
119− -> Config
120− -> IO ()
121− -> IO (Maybe Report)
122−fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
181+fuzz ::
182+ forall api.
183+ (FlattenServer api, ToReifiedApi (Endpoints api)) =>
184+ Server api ->
185+ Config ->
186+ IO () ->
187+ IO (Maybe Report)
188+fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
123189 let path = []
124190 stash = addToStash seed mempty
125191 currentRng = mkStdGen rngSeed
126−
127−
128− deadline :: UTCTime <- addUTCTime (fromInteger $ maxRuntime * 1000000) <$> getCurrentTime
129− (stopreason, fs ) <- runStateT
130− (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get)) FuzzState{..}
192+ deadline :: UTCTime <- addUTCTime (realToFrac $ maxRuntime * 1000000) <$> getCurrentTime
193+ (stopreason, _fs) <-
194+ runStateT
195+ (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get))
196+ FuzzState {..}
131197 print stopreason
132198 pure Nothing
133199 where
134200 -- something less terrible later
135201 formatException :: RoboservantException -> Report
136− formatException = Report . show
202+ formatException r@(RoboservantException failureType exception _state) = Report
203+ (unlines [ show failureType , show exception ])
204+ r
205+
206+ displayDiagnostics FuzzState{..} = liftIO $ do
207+ putStrLn "api endpoints covered"
208+ mapM_ print (Set.toList $ Set.fromList $ map apiOffset path)
209+ putStrLn ""
210+ putStrLn "types in stash"
211+ DM.forWithKey_ (getStash stash) $ \k v ->
212+ print (NEL.length $ getStashValue v)
213+ -- mapM_
214+ -- (print . second NEL.length)
215+ -- (sortOn (show . fst) $ Map.toList $ getStash stash)
137216
138217 -- evaluateCoverage :: FuzzState -> m ()
139218 evaluateCoverage f@FuzzState{..}
140− | hitRoutes / totalRoutes > coverageThreshold = do
141− liftIO $ print ("passed coverage", hitRoutes, totalRoutes)
142− pure ()
143− | otherwise = throw $ RoboservantException InsufficientCoverage Nothing routeCount f
144− where hitRoutes = (fromIntegral . Set.size . Set.fromList $ map apiOffset path)
145− totalRoutes = (fromIntegral routeCount)
219+ | coverage > coverageThreshold = pure ()
220+ | otherwise = do
221+ displayDiagnostics f
222+ throw $ RoboservantException (InsufficientCoverage coverage) Nothing f
223+ where hitRoutes = fromIntegral . Set.size . Set.fromList $ map apiOffset path
224+ totalRoutes = fromIntegral routeCount
225+ coverage = hitRoutes / totalRoutes
146226
147227
148228 untilDone :: MonadIO m => (Integer,UTCTime) -> m a -> m StopReason
@@ -152,121 +232,140 @@ fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
152232 if now > deadline
153233 then pure TimedOut
154234 else do
155− action
156− untilDone (n-1, deadline) action
157−
235+ _ <- action
236+ untilDone (n -1, deadline) action
158237 reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
159238 routeCount = length reifiedApi
160239
161240 elementOrFail :: (MonadState FuzzState m, MonadIO m)
162241 => [a] -> m a
163− elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing routeCount =<< get
242+ elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing =<< get
164243 elementOrFail l = do
165244 st <- get
166− let (index,newGen) = randomR (0, length l - 1) (currentRng st)
167− modify' $ \st' -> st' { currentRng = newGen }
245+ let (index, newGen) = randomR (0, length l - 1) (currentRng st)
246+ modify' $ \st' -> st' {currentRng = newGen}
168247 pure (l !! index)
169−
170− withOp :: (MonadState FuzzState m, MonadIO m)
171− => (forall as. (V.RecordToList as, V.RMap as)
172− => FuzzOp
173− -> V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
174− -> V.Rec (TypedF V.Identity) as -> m r
175− )
176− -> m r
248+ withOp ::
249+ (MonadState FuzzState m, MonadIO m) =>
250+ ( forall as.
251+ (V.RecordToList as, V.RMap as) =>
252+ FuzzOp ->
253+ V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int)))) ->
254+ V.Rec (TypedF V.Identity) as ->
255+ m r
256+ ) ->
257+ m r
177258 withOp callback = do
178259 -- choose a call to make, from the endpoints with fillable arguments.
179− (offset, EndpointOption{..}) <- elementOrFail . options =<< get
180− r <- V.rtraverse
181− (\(tr :*: StashValue svs) -> elementOrFail $
182− zipWith (\i xy -> V.Const i :*: tr :*: xy)
183− [0..]
184− (NEL.toList svs)
185− )
186− eoArgs
187− let pathSegment = FuzzOp offset $
188− recordToList'
189− (\(V.Const index :*: tr :*: _) -> Provenance (R.SomeTypeRep tr) index)
260+ (offset, EndpointOption {..}) <- elementOrFail . options =<< get
261+ r <-
262+ V.rtraverse
263+ ( \(tr :*: StashValue svs _) ->
264+ elementOrFail $
265+ zipWith
266+ (\i xy -> V.Const i :*: tr :*: xy)
267+ [0 ..]
268+ (NEL.toList svs)
269+ )
270+ eoArgs
271+ let pathSegment =
272+ FuzzOp offset $
273+ recordToList'
274+ (\(V.Const index :*: tr :*: _) -> Provenance (R.SomeTypeRep tr) index)
275+ r
276+ argValues =
277+ V.rmap
278+ (\(_ :*: tr :*: (_, x)) -> tr :*: V.Identity x)
190279 r
191− argValues = V.rmap
192− (\(_ :*: tr :*: (_, x)) -> tr :*: V.Identity x)
193− r
194− modify' (\f -> f { path = path f <> [pathSegment] })
280+ modify' (\f -> f {path = path f <> [pathSegment]})
195281 callback pathSegment eoCall argValues
196282 where
197283 options :: FuzzState -> [(ApiOffset, EndpointOption)]
198− options FuzzState{..} =
284+ options FuzzState {..} =
199285 mapMaybe
200− ( \(offset, ReifiedEndpoint{..}) -> do
201− args <- V.rtraverse (\(tr :*: Argument bf) -> (tr :*:) <$> bf stash) reArguments
202− pure (offset, EndpointOption reEndpointFunc args)
286+ ( \(offset, ReifiedEndpoint {..}) -> do
287+ args <- V.rtraverse (\(tr :*: Argument bf) -> (tr :*:) <$> bf stash) reArguments
288+ pure (offset, EndpointOption reEndpointFunc args)
203289 )
204290 reifiedApi
205−
206− execute :: (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as)
207− => FuzzOp
208− -> V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
209− -> V.Rec (TypedF V.Identity) as
210− -> m ()
291+ execute ::
292+ (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
293+ FuzzOp ->
294+ V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int)))) ->
295+ V.Rec (TypedF V.Identity) as ->
296+ m ()
211297 execute fuzzop func args = do
212− liftIO $ print fuzzop
298+ (liftIO . print . (fuzzop,) . stash ) =<< get
213299 st <- get
214− let showable = unlines $ ("args":map show argTypes)
215− <> ["fuzzop"
216− , show fuzzop
217− -- ,"dyncall"
218− -- ,show (dynTypeRep dyncall)
219− ,"state"
220− ,show st]
221− liftIO $ putStrLn showable
300+ -- let showable = unlines $ ("args":map show argTypes)
301+ -- <> ["fuzzop"
302+ -- , show fuzzop
303+ -- -- ,"dyncall"
304+ -- -- ,show (dynTypeRep dyncall)
305+ -- ,"state"
306+ -- ]
307+ -- liftIO $ putStrLn showable
222308 liftIO (V.runcurry' func argVals) >>= \case
223309 -- parameterise this
224310 Left (serverError :: ServerError) ->
225311 case errHTTPCode serverError of
226312 500 -> throw serverError
227313 _ -> do
228− liftIO $ print ("ignoring non-500 error" , serverError)
229− Right (dyn :: NEL.NonEmpty Dynamic) -> do
230− liftIO $ print ("storing", fmap dynTypeRep dyn)
314+ liftIO $ print ("ignoring non-500 error", serverError)
315+ Right (dyn :: NEL.NonEmpty (Dynamic,Int)) -> do
316+ -- liftIO $ print ("storing", fmap dynTypeRep dyn)
231317 modify' (\fs@FuzzState{..} ->
232318 fs { stash = addToStash (NEL.toList dyn) stash } )
233319 where
234320 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
235321 argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
236−
237− go :: (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m)
238− => m ()
322+ go ::
323+ (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
324+ m ()
239325 go = withOp $ \op func args -> do
240326 catches (execute op func args)
241327 [ Handler (\(e :: SomeAsyncException) -> throw e)
242− , Handler (\(e :: SomeException) -> throw . RoboservantException ServerCrashed (Just e) routeCount =<< get)
328+ , Handler (\(e :: SomeException) -> do
329+ displayDiagnostics =<< get
330+ throw . RoboservantException ServerCrashed (Just e) =<< get)
243331 ]
244332 catch (liftIO checker)
245− (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) routeCount =<< get)
333+ (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
246334
247−addToStash :: [Dynamic]
248− -> Stash
249− -> Stash
335+addToStash ::
336+ [(Dynamic,Int)] ->
337+ Stash ->
338+ Stash
250339addToStash result stash =
251− foldr (\(Dynamic tr x) (Stash dict) -> Stash $
252− DM.insertWith renumber tr (StashValue (([Provenance (R.SomeTypeRep tr) 0],x):|[])) dict
340+ foldr
341+ ( \(Dynamic tr x,hashed) (Stash dict) ->
342+ Stash $
343+ DM.insertWith renumber tr
344+ (StashValue (([Provenance (R.SomeTypeRep tr) 0], x) :| []) (IntSet.singleton hashed)) dict
253345 )
254346 stash
255347 result
256348 where
257− renumber :: StashValue a
258− -> StashValue a
259− -> StashValue a
260− renumber (StashValue singleDyn) (StashValue l) = StashValue $ case NEL.toList singleDyn of
261− [([Provenance tr _], dyn)] -> l
262− <> pure ([Provenance tr (length (NEL.last l) + 1)], dyn)
263− _ -> error "should be impossible"
349+ renumber ::
350+ StashValue a ->
351+ StashValue a ->
352+ StashValue a
353+ renumber (StashValue singleDyn singleHash) orig@(StashValue l intSet)
354+ | not $ IntSet.null (singleHash `IntSet.intersection` intSet) = orig
355+ | otherwise =
356+ StashValue
357+ (case NEL.toList singleDyn of
358+ [([Provenance tr _], dyn)] ->
359+ l
360+(([Provenance tr (length (NEL.last l) + 1)], dyn))
361+ _ -> error "should be impossible")
264362
363+ (IntSet.union singleHash intSet)
265364
266365-- why isn't this in vinyl?
267−recordToList'
268− :: (V.RecordToList as, V.RMap as)
269− => (forall x. f x -> a)
270− -> V.Rec f as
271− -> [a]
366+recordToList' ::
367+ (V.RecordToList as, V.RMap as) =>
368+ (forall x. f x -> a) ->
369+ V.Rec f as ->
370+ [a]
272371recordToList' f = V.recordToList . V.rmap (V.Const . f)
src/Roboservant/Types.hs+5
@@ -15,11 +15,16 @@
1515
1616module Roboservant.Types
1717 ( module Roboservant.Types.Breakdown
18+ , module Roboservant.Types.BuildFrom
1819 , module Roboservant.Types.FlattenServer
1920 , module Roboservant.Types.ReifiedApi
21+ , module Roboservant.Types.Internal
22+
2023 )
2124where
2225
2326import Roboservant.Types.Breakdown
27+import Roboservant.Types.BuildFrom
2428import Roboservant.Types.FlattenServer
2529import Roboservant.Types.ReifiedApi
30+import Roboservant.Types.Internal
src/Roboservant/Types/Breakdown.hs+28−73
@@ -1,7 +1,18 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DerivingVia #-}
4+{-# LANGUAGE DerivingVia #-}
5+
6+{-# LANGUAGE StandaloneDeriving #-}
7+{-# LANGUAGE StandaloneDeriving #-}
8+
9+{-# LANGUAGE FlexibleInstances #-}
10+{-# LANGUAGE FlexibleInstances #-}
11+
12+{-# LANGUAGE GADTs #-}
13+{-# LANGUAGE GADTs #-}
14+
315{-# LANGUAGE DefaultSignatures #-}
4−{-# LANGUAGE DeriveFunctor #-}
516{-# LANGUAGE DerivingStrategies #-}
617{-# LANGUAGE FlexibleInstances #-}
718{-# LANGUAGE GADTs #-}
@@ -10,95 +21,39 @@
1021{-# LANGUAGE MultiParamTypeClasses #-}
1122{-# LANGUAGE PolyKinds #-}
1223{-# LANGUAGE ScopedTypeVariables #-}
13−{-# LANGUAGE StandaloneDeriving #-}
14−{-# LANGUAGE TupleSections #-}
1524{-# LANGUAGE TypeApplications #-}
1625{-# LANGUAGE TypeFamilies #-}
17−{-# LANGUAGE TypeOperators #-}
1826{-# LANGUAGE UndecidableInstances #-}
27+{-# LANGUAGE StandaloneDeriving #-}
28+{-# LANGUAGE DerivingVia #-}
29+
1930
2031module Roboservant.Types.Breakdown where
2132
2233import Data.Dynamic (Dynamic, fromDynamic, toDyn)
23−import Data.List.NonEmpty (NonEmpty(..))
34+import Data.List.NonEmpty (NonEmpty (..))
2435import qualified Data.List.NonEmpty as NEL
25−import qualified Data.Map.Strict as Map
2636import Data.Maybe (fromMaybe)
37+import GHC.Generics(Generic)
2738import Data.Typeable (Typeable)
28−import qualified Data.Dependent.Map as DM
29−import Data.Dependent.Map (DMap)
30−import qualified Type.Reflection as R
31−import Data.Dependent.Sum
32−import Data.Kind
33−
34−data Provenance
35− = Provenance R.SomeTypeRep Int
36− deriving (Show,Eq)
37−
38−newtype StashValue a = StashValue { getStashValue :: NonEmpty ([Provenance], a) }
39− deriving (Functor, Show)
40−
41−-- wrap in newtype to give a custom Show instance, since the normal
42−-- instance for DMap is not happy since StashValue needs Show a to show
43−newtype Stash = Stash { getStash :: DMap R.TypeRep StashValue }
44− deriving (Semigroup, Monoid)
45−
46−instance Show Stash where
47− showsPrec i (Stash x) = showsPrec i $
48− Map.fromList . map (\(tr :=> StashValue vs) -> (R.SomeTypeRep tr, fmap fst vs)) $ DM.toList x
49−
50−class Typeable x => BuildFrom (x :: Type) where
51− buildFrom :: Stash -> Maybe (StashValue x)
52− default buildFrom :: Stash -> Maybe (StashValue x)
53− buildFrom = DM.lookup R.typeRep . getStash
54−
55− -- (fmap promisedDyn . NEL.toList) . Map.lookup (typeRep (Proxy @x))
56−
57−
58−-- baseLookup :: TypeRep -> Stash -> Maybe (NonEmpty ([Provenance], Dynamic))
59−-- baseLookup tr mmm = -- Map.lookup (typeRep (Proxy @x)) mmm
60−-- Map.lookup tr mmm
39+import Roboservant.Types.Internal
40+import Data.Hashable
6141
6242-- | only use this when we are using the internal typerep map.
6343promisedDyn :: Typeable a => Dynamic -> a
6444promisedDyn = fromMaybe (error "internal error, typerep map misconstructed") . fromDynamic
6545
66−instance BuildFrom Bool
67−
68−instance (Typeable x, BuildFrom x) => BuildFrom (Maybe x) where
69− buildFrom dict = Just options
70− where options :: StashValue (Maybe x)
71− options = StashValue $
72− ([],Nothing) :|
73− (maybe [] (NEL.toList . getStashValue . fmap Just) $ buildFrom @x dict
74− )
75−
7646class Breakdown x where
77− breakdown :: x -> NonEmpty Dynamic
78− default breakdown :: Typeable x => x -> NonEmpty Dynamic
79− breakdown = pure . toDyn
80−
81−-- | Can't break it down any further -- stuck in your teeth, maybe.
82−newtype Chewy x = Chewy { unChew :: x }
83−
84−instance Breakdown () where
85− breakdown = pure . toDyn
86−
87−instance Breakdown Int where
88− breakdown = pure . toDyn
89−
90−
91−instance Typeable x => Breakdown (Chewy x) where
92− breakdown x = pure (toDyn x)
93−
94−--let d = toDyn x in Map.fromList [(dynTypeRep d, pure d)]
47+ breakdown :: x -> NonEmpty (Dynamic,Int)
48+-- default breakdown :: Typeable x => x -> NonEmpty Dynamic
49+-- breakdown = pure . toDyn
9550
96−-- instance (Typeable x, Generic x) => Breakdown x where
97−-- breakdown = Map.fromListWith (<>) . fmap ((dynTypeRep &&& (\x -> NEL.fromList [x])) . toDyn . Generics.to) . _ . Generics.from
9851
52+instance (Hashable x, Typeable x) => Breakdown (Atom x) where
53+ breakdown (Atom x) = pure (toDyn x, hash x)
9954
100−instance (Typeable a, Breakdown a) => Breakdown [a] where
101− breakdown x = toDyn x :| mconcat (map (NEL.toList . breakdown) x)
55+deriving via (Atom ()) instance Breakdown ()
56+deriving via (Atom Int) instance Breakdown Int
10257
103−instance (BuildFrom a) => BuildFrom [a] -- where
104−-- breakdown x = toDyn x :| mconcat (map (NEL.toList . breakdown) x)
58+-- instance (Typeable a, Breakdown a) => Breakdown [a] where
59+-- breakdown x = toDyn x :| mconcat (map (NEL.toList . breakdown) x)
src/Roboservant/Types/BuildFrom.hsnew file+41
@@ -0,0 +1,41 @@
1+{-# LANGUAGE DefaultSignatures #-}
2+{-# LANGUAGE DerivingStrategies #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
4+{-# LANGUAGE TypeApplications #-}
5+{-# LANGUAGE TypeFamilies #-}
6+{-# LANGUAGE StandaloneDeriving #-}
7+{-# LANGUAGE DerivingVia #-}
8+
9+
10+
11+module Roboservant.Types.BuildFrom where
12+
13+import Data.List.NonEmpty (NonEmpty(..))
14+import qualified Data.List.NonEmpty as NEL
15+import Data.Typeable (Typeable)
16+import qualified Data.Dependent.Map as DM
17+import qualified Type.Reflection as R
18+import Data.Kind
19+import Roboservant.Types.Internal
20+import Data.Hashable
21+
22+class Typeable x => BuildFrom (x :: Type) where
23+ buildFrom :: Stash -> Maybe (StashValue x)
24+
25+-- | this is a little esoteric: basically, we're always going to use this with
26+-- deriving-via, so we need to look up the underlying `x` and then wrap it in Atom.
27+instance Typeable x => BuildFrom (Atom x) where
28+ buildFrom = fmap (fmap Atom) <$> DM.lookup R.typeRep . getStash
29+
30+deriving via (Atom Bool) instance BuildFrom Bool
31+
32+-- instance (Typeable x, Hashable x, BuildFrom x) => BuildFrom (Maybe x) where
33+-- buildFrom dict = Just options
34+-- where options :: StashValue (Maybe x)
35+-- options = StashValue
36+-- ([],Nothing) :|
37+-- (maybe [] (NEL.toList . getStashValue . fmap Just) $ buildFrom @x dict
38+-- )
39+-- _
40+-- -- instance (BuildFrom a) => BuildFrom [a] -- where
41+-- --breakdown x = toDyn x :| mconcat (map (NEL.toList . breakdown) x)
src/Roboservant/Types/Internal.hsnew file+36
@@ -0,0 +1,36 @@
1+{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3+
4+module Roboservant.Types.Internal where
5+
6+import Data.List.NonEmpty (NonEmpty(..))
7+import qualified Data.Map.Strict as Map
8+import qualified Data.Dependent.Map as DM
9+import Data.Dependent.Map (DMap)
10+import qualified Type.Reflection as R
11+import Data.Dependent.Sum
12+import Data.IntSet(IntSet)
13+
14+data Provenance
15+ = Provenance R.SomeTypeRep Int
16+ deriving (Show,Eq)
17+
18+data StashValue a = StashValue { getStashValue :: NonEmpty ([Provenance], a)
19+ , stashHash :: IntSet
20+ }
21+ deriving (Functor, Show)
22+
23+-- wrap in newtype to give a custom Show instance, since the normal
24+-- instance for DMap is not happy since StashValue needs Show a to show
25+newtype Stash = Stash { getStash :: DMap R.TypeRep StashValue }
26+ deriving (Semigroup, Monoid)
27+
28+instance Show Stash where
29+ showsPrec i (Stash x) = showsPrec i $
30+ Map.fromList . map (\(tr :=> StashValue vs _) -> (R.SomeTypeRep tr, fmap fst vs)) $ DM.toList x
31+
32+-- | Can't be built up from parts, can't be broken down further.
33+newtype Atom x = Atom { unAtom :: x }
34+
35+-- | can be broken down and built up from generic pieces
36+newtype Compound x = Compound { unCompound :: x }
src/Roboservant/Types/ReifiedApi.hs+5−3
@@ -29,7 +29,9 @@ import Data.List.NonEmpty (NonEmpty)
2929import Data.Typeable (Typeable)
3030import GHC.Generics ((:*:)(..))
3131import GHC.TypeLits (Symbol)
32+import Roboservant.Types.Internal
3233import Roboservant.Types.Breakdown
34+import Roboservant.Types.BuildFrom
3335import Roboservant.Types.FlattenServer
3436import Servant
3537import Servant.API.Modifiers(FoldRequired,FoldLenient)
@@ -51,7 +53,7 @@ newtype Argument a = Argument
5153
5254data ReifiedEndpoint = forall as. (V.RecordToList as, V.RMap as) => ReifiedEndpoint
5355 { reArguments :: V.Rec (TypedF Argument) as
54− , reEndpointFunc :: V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
56+ , reEndpointFunc :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int))))
5557 }
5658
5759type ReifiedApi = [(ApiOffset, ReifiedEndpoint)]
@@ -77,7 +79,7 @@ instance ToReifiedApi '[] where
7779instance
7880 ( Typeable (EndpointRes endpoint)
7981 , NormalizeFunction (ServerT endpoint Handler)
80− , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either ServerError (NonEmpty Dynamic)))
82+ , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either ServerError (NonEmpty (Dynamic,Int))))
8183 , ToReifiedEndpoint endpoint
8284 , ToReifiedApi endpoints, Typeable (ServerT endpoint Handler)
8385 ) =>
@@ -101,7 +103,7 @@ instance NormalizeFunction x => NormalizeFunction (r -> x) where
101103 normalize = fmap normalize
102104
103105instance (Typeable x, Breakdown x) => NormalizeFunction (Handler x) where
104− type Normal (Handler x) = IO (Either ServerError (NonEmpty Dynamic))
106+ type Normal (Handler x) = IO (Either ServerError (NonEmpty (Dynamic,Int)))
105107 normalize handler = (runExceptT . runHandler') handler >>= \case
106108 Left serverError -> pure (Left serverError)
107109 Right x -> pure $ Right $ breakdown x
stack.yaml+5−17
@@ -1,27 +1,15 @@
1+# nb: 16.8 doesn't work because the version of servant is too old to have FoldLenient', fixme.
12resolver: lts-16.16
23packages:
34- .
45
56extra-deps:
67- servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
7−- markov-chain-usage-model-0.0.0@sha256:1afa95faeb9213c4d960a669190078b41b89169462b8edd910472980671ba8c0,2112
8−- servant-0.18
9−- servant-client-0.18
10−- servant-client-core-0.18
11−- servant-server-0.18
12−# - hspec-hedgehog-0.0.1.2@sha256:aaeeb50a43d2bce8a1850baa3ddf934a964bfe38c64efd59306b0c1421e55b45,1405
13−- QuickCheck-2.14.1@sha256:01e46d7b0a8d3148288ec977625f62d5516ebb5031a50c63f0453863301b4a79,7736
14−- random-1.2.0
15−- splitmix-0.1.0.1@sha256:22f9662e7e8b173421872241edd39350078a9ed4bb9e9f503948c5b483c79276,5253
168- vinyl-0.13.0
179- dependent-sum-0.7.1.0
1810- constraints-extras-0.3.0.2
1911- dependent-map-0.4.0.0
20−- git: https://github.com/hedgehogqa/haskell-hedgehog.git
21− commit: 03714682586e43b5ddf5c00391035471a6e01238
22− subdirs:
23− - hedgehog
24−- git: https://github.com/mwotton/hspec
25− commit: cd9012414a93cf08f86e409544d4d3a8f282b507
26− subdirs:
27− - hspec-core
12+- servant-0.18.2
13+- servant-client-0.18.2
14+- servant-client-core-0.18.2
15+- servant-server-0.18.2
stack.yaml.lock+22−76
@@ -11,62 +11,6 @@ packages:
1111 sha256: 04f12c7bef2c3f9a25d94eb9489752ed498db8e243069fe95838dbb51df1dcb3
1212 original:
1313 hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
14−- completed:
15− hackage: markov-chain-usage-model-0.0.0@sha256:1afa95faeb9213c4d960a669190078b41b89169462b8edd910472980671ba8c0,2112
16− pantry-tree:
17− size: 432
18− sha256: 6871bd9281acf589296d0998a3d62892b036040ab10e74e8a0f356f68c194f4f
19− original:
20− hackage: markov-chain-usage-model-0.0.0@sha256:1afa95faeb9213c4d960a669190078b41b89169462b8edd910472980671ba8c0,2112
21−- completed:
22− hackage: servant-0.18@sha256:2b5c81089540c208b1945b5ca0c3551c862138d71b224a39fa275a62852a5c75,5068
23− pantry-tree:
24− size: 2458
25− sha256: 701b6443e486601dd127b92a28ca052278dea8dc528f013781c1304826e117d8
26− original:
27− hackage: servant-0.18
28−- completed:
29− hackage: servant-client-0.18@sha256:3e3b8d145cf811e19849744ad305aa0bd42301d01ff75e1dac169bc9f5d18221,4589
30− pantry-tree:
31− size: 1299
32− sha256: 445c610d83a6e356ed2a7808af8deac84708be3e44d38cbf76e60fd6105f9715
33− original:
34− hackage: servant-client-0.18
35−- completed:
36− hackage: servant-client-core-0.18@sha256:6702f04f3e5ac47abace5b26ab1948480437334747895407f0697502a403b96a,3593
37− pantry-tree:
38− size: 1444
39− sha256: 20f538ae12091696073a2ac5f3301a4a56686f5e2889b2a9e539c0d61a332cd1
40− original:
41− hackage: servant-client-core-0.18
42−- completed:
43− hackage: servant-server-0.18@sha256:ec7f361bc9848968b1fff8091fa2213b721d3f47549421945c6a7ca1702e22e6,5424
44− pantry-tree:
45− size: 2546
46− sha256: c2470cd8665742cee13faae1db7e3d49d70b027d57c4578e3f7ba66ffc8dc7b4
47− original:
48− hackage: servant-server-0.18
49−- completed:
50− hackage: QuickCheck-2.14.1@sha256:01e46d7b0a8d3148288ec977625f62d5516ebb5031a50c63f0453863301b4a79,7736
51− pantry-tree:
52− size: 2315
53− sha256: 21961d0875fc156cdd97110a6fb277f37aa31faa3c7910895f84c82630e7ba46
54− original:
55− hackage: QuickCheck-2.14.1@sha256:01e46d7b0a8d3148288ec977625f62d5516ebb5031a50c63f0453863301b4a79,7736
56−- completed:
57− hackage: random-1.2.0@sha256:4321209c8faedc034810ea8ed0dbc4a36f1a1df97b75af024219f2f533da57de,6094
58− pantry-tree:
59− size: 1259
60− sha256: 29ff0a7ec5bf123e2c40f849b7af163950854a6c08c68b4a1b4364ce7a515af0
61− original:
62− hackage: random-1.2.0
63−- completed:
64− hackage: splitmix-0.1.0.1@sha256:22f9662e7e8b173421872241edd39350078a9ed4bb9e9f503948c5b483c79276,5253
65− pantry-tree:
66− size: 1149
67− sha256: b9a0c60e2d4786bbb276ce6606027a99615bf703fa3e0f01911e8c6a656c5161
68− original:
69− hackage: splitmix-0.1.0.1@sha256:22f9662e7e8b173421872241edd39350078a9ed4bb9e9f503948c5b483c79276,5253
7014- completed:
7115 hackage: vinyl-0.13.0@sha256:0f247cd3f8682b30881a07de18e6fec52d540646fbcb328420049cc8d63cd407,3724
7216 pantry-tree:
@@ -96,31 +40,33 @@ packages:
9640 original:
9741 hackage: dependent-map-0.4.0.0
9842- completed:
99− subdir: hedgehog
100− name: hedgehog
101− version: 1.0.3
102− git: https://github.com/hedgehogqa/haskell-hedgehog.git
43+ hackage: servant-0.18.2@sha256:f8c9f0e9891a3ada1337a3c0b369333a3b5a2d0909dd3cd09d79bc26adeaca44,5298
44+ pantry-tree:
45+ size: 2662
46+ sha256: e930e814de1aa4d24274bdf18341a50b7ed38604ae4734f730e09238ac5bf7e2
47+ original:
48+ hackage: servant-0.18.2
49+- completed:
50+ hackage: servant-client-0.18.2@sha256:82578ade7468873259bb2fdc9d62290a0f998550900683e1410a237ed4b05410,4591
10351 pantry-tree:
104− size: 2575
105− sha256: 7e69930fef281dfeb47b2f7c745c26234768e56ffb5490065f607c57146ffbab
106− commit: 03714682586e43b5ddf5c00391035471a6e01238
52+ size: 1300
53+ sha256: 6324892c77bedbce32f0d6f1612fc2cb0d82c163d3be39efb951d3cc3792ce4a
54+ original:
55+ hackage: servant-client-0.18.2
56+- completed:
57+ hackage: servant-client-core-0.18.2@sha256:4cac20d0b0d889a5613a95ec18526afd16abcaf0b4b7ba5fed35c7bf85800162,3638
58+ pantry-tree:
59+ size: 1444
60+ sha256: 90279e27ba5a7f8f4f1761e81d0dded343839316fdbf34d395954487701e29df
10761 original:
108− subdir: hedgehog
109− git: https://github.com/hedgehogqa/haskell-hedgehog.git
110− commit: 03714682586e43b5ddf5c00391035471a6e01238
62+ hackage: servant-client-core-0.18.2
11163- completed:
112− subdir: hspec-core
113− name: hspec-core
114− version: 2.7.4
115− git: https://github.com/mwotton/hspec
64+ hackage: servant-server-0.18.2@sha256:56679af62ab8820a2108da6153d9ae9dde37199e62172365bdaea1458c3f7c2d,5482
11665 pantry-tree:
117− size: 3899
118− sha256: 6ece11ad28a7f122a502e766eb1b3ef950815c2d16babb0ea8744646f1c1bd70
119− commit: cd9012414a93cf08f86e409544d4d3a8f282b507
66+ size: 2614
67+ sha256: 3ac7430134439e4b67f0f5333f63b89d0cb7de5e2e07f0af7801c8e223942b9c
12068 original:
121− subdir: hspec-core
122− git: https://github.com/mwotton/hspec
123− commit: cd9012414a93cf08f86e409544d4d3a8f282b507
69+ hackage: servant-server-0.18.2
12470snapshots:
12571- completed:
12672 size: 532380
test/Foo.hs+2−1
@@ -11,13 +11,14 @@ import Data.Aeson
1111import Data.Typeable (Typeable)
1212import GHC.Generics (Generic)
1313import Servant
14+import Data.Hashable
1415
1516newtype Foo = Foo Int
1617 deriving (Generic, Eq, Show, Typeable)
1718 deriving newtype (FromHttpApiData, ToHttpApiData)
1819
20+instance Hashable Foo
1921instance ToJSON Foo
20−
2122instance FromJSON Foo
2223
2324type Api =
test/Headers.hs+2−1
@@ -11,13 +11,14 @@ import Data.Aeson
1111import Data.Typeable (Typeable)
1212import GHC.Generics (Generic)
1313import Servant
14+import Data.Hashable
1415
1516newtype Foo = Foo Int
1617 deriving (Generic, Eq, Show, Typeable)
1718 deriving newtype (FromHttpApiData, ToHttpApiData)
1819
20+instance Hashable Foo
1921instance ToJSON Foo
20−
2122instance FromJSON Foo
2223
2324type Api =
test/Post.hsnew file+27
@@ -0,0 +1,27 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE DerivingStrategies #-}
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE TypeOperators #-}
7+
8+module Post where
9+
10+import GHC.Generics (Generic)
11+import Servant
12+import Data.Aeson
13+import Data.Hashable
14+
15+type Api = Get '[JSON] FooPost
16+ :<|> ReqBody '[JSON] FooPost :> Post '[JSON] ()
17+
18+data FooPost = FooPost
19+ deriving (Eq,Show,Generic)
20+
21+instance Hashable FooPost
22+instance ToJSON FooPost
23+instance FromJSON FooPost
24+
25+server :: Server Api
26+server = pure FooPost
27+ :<|> const (pure ())
test/Seeded.hs+2
@@ -11,6 +11,7 @@ import Data.Aeson
1111import Data.Typeable (Typeable)
1212import GHC.Generics (Generic)
1313import Servant
14+import Data.Hashable
1415
1516newtype Seed = Seed Int
1617 deriving (Generic, Eq, Show, Typeable)
@@ -18,6 +19,7 @@ newtype Seed = Seed Int
1819
1920instance ToJSON Seed
2021instance FromJSON Seed
22+instance Hashable Seed
2123
2224type Api = Capture "seed" Seed :> Get '[JSON] ()
2325 :<|> Get '[JSON] ()
test/Spec.hs+90−22
@@ -1,20 +1,29 @@
1−
21{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE StandaloneDeriving #-}
3+{-# LANGUAGE StandaloneDeriving #-}
4+
5+{-# LANGUAGE DerivingVia #-}
6+{-# LANGUAGE DerivingVia #-}
7+
38{-# LANGUAGE TypeApplications #-}
9+{-# LANGUAGE LambdaCase #-}
10+{-# LANGUAGE DerivingVia #-}
11+{-# LANGUAGE StandaloneDeriving #-}
12+
413{-# OPTIONS_GHC -fno-warn-orphans #-}
514import qualified Foo
615import qualified Seeded
716import qualified Valid
817import qualified Headers
9−-- import qualified UnsafeIO
18+import qualified Post
19+import Test.Hspec.Core.Spec
1020
1121import Data.Dynamic(toDyn)
1222import qualified Roboservant as RS
13−
1423import Test.Hspec
15−import Test.Hspec.Core.Spec(shouldFail)
1624import Data.Void
1725import Data.Maybe
26+import Data.Hashable
1827
1928main :: IO ()
2029main = hspec spec
@@ -23,28 +32,60 @@ noCheck :: IO ()
2332noCheck = pure ()
2433
2534defaultConfig :: RS.Config
26−defaultConfig = RS.Config [] 1 1000 1 0
35+defaultConfig = RS.Config
36+ { RS.seed = []
37+ , RS.maxRuntime = 0.5
38+ , RS.maxReps = 1000
39+ , RS.rngSeed = 0
40+ , RS.coverageThreshold = 0
41+ }
2742
2843spec :: Spec
2944spec = do
3045 describe "Basic usage" $ do
3146 describe "noError" $ do
3247 it "finds no error in a valid app" $ do
33− RS.fuzz @Valid.Api Valid.server defaultConfig noCheck `shouldReturn` Nothing
48+ RS.fuzz @Valid.Api Valid.server defaultConfig noCheck
49+ >>= (`shouldSatisfy` isNothing)
50+ it "finds no error in a valid generic app" $ do
51+ RS.fuzz @Valid.RoutedApi Valid.routedServer defaultConfig noCheck
52+ >>= (`shouldSatisfy` isNothing)
53+
3454 it "does fail coverage check" $ do
3555 r <- RS.fuzz @Valid.Api Valid.server defaultConfig { RS.coverageThreshold = 0.6 } noCheck
36− r `shouldSatisfy` isJust
56+ fmap (RS.failureReason . RS.rsException) r `shouldSatisfy`
57+ (\case
58+ Just (RS.InsufficientCoverage _) -> True
59+ _ -> False)
60+
61+ describe "posted body" $ do
62+ it "passes a coverage check using a posted body" $ do
63+ RS.fuzz @Post.Api Post.server defaultConfig { RS.coverageThreshold = 0.99 } noCheck
64+ >>= (`shouldSatisfy` isNothing)
65+
3766 describe "seeded" $ do
3867 shouldFail $
3968 it "finds an error using information passed in" $
40− RS.fuzz @Seeded.Api Seeded.server (defaultConfig{ RS.seed = [toDyn $ Seeded.Seed 1] }) noCheck
41− `shouldReturn` Nothing
42− shouldFail $ it "finds an error in a basic app" $
69+ let res = Seeded.Seed 1 in
70+ RS.fuzz @Seeded.Api Seeded.server (defaultConfig{ RS.seed = [(toDyn res,hash res) ] }) noCheck
71+ >>= (`shouldSatisfy` isNothing)
72+ describe "Foo" $ do
73+ it "finds an error in a basic app" $
4374 RS.fuzz @Foo.Api Foo.server defaultConfig noCheck
44− `shouldReturn` Nothing
45− shouldFail $ it "should find a failure that's dependent on using header info" $ do
46− RS.fuzz @Headers.Api Headers.server defaultConfig noCheck
47− `shouldReturn` Nothing
75+ >>= (`shouldSatisfy` isJust)
76+
77+ -- describe "headers" $ do
78+ -- it "should find a failure that's dependent on using header info" $ do
79+ -- RS.fuzz @Headers.Api Headers.server defaultConfig noCheck
80+ -- >>= (`shouldSatisfy` isJust)
81+
82+ -- describe "can build from pieces" $ do
83+ -- it "should find a failure that requires some assembly" $ do
84+ -- RS.fuzz @RS.BuildFrom.Api RS.BuildFrom.server defaultConfig noCheck
85+ -- >>= (`shouldSatisfy` isJust)
86+
87+
88+
4889 -- -- -- The UnsafeIO checker does not actually really use the contextually aware stuff, though it
4990 -- -- -- could: it's mostly here to show how to test for concurrency problems.
5091 -- describe "concurrency bugs" $ do
@@ -60,17 +101,44 @@ spec = do
60101 -- RS.prop_concurrent @UnsafeIO.UnsafeApi unsafeServer []
61102
62103
63−instance RS.Breakdown Foo.Foo where
64− breakdown = pure . toDyn
104+deriving via (RS.Atom Foo.Foo) instance RS.Breakdown Foo.Foo
105+deriving via (RS.Atom Foo.Foo) instance RS.BuildFrom Foo.Foo
106+
107+deriving via (RS.Atom Headers.Foo) instance RS.Breakdown Headers.Foo
108+deriving via (RS.Atom Headers.Foo) instance RS.BuildFrom Headers.Foo
109+
110+deriving via (RS.Atom Seeded.Seed) instance RS.Breakdown Seeded.Seed
111+deriving via (RS.Atom Seeded.Seed) instance RS.BuildFrom Seeded.Seed
112+
113+
114+-- instance RS.BuildFrom Seeded.Seed
115+
116+deriving via (RS.Atom Void) instance RS.BuildFrom Void
117+
118+deriving via (RS.Atom Post.FooPost) instance RS.BuildFrom Post.FooPost
119+deriving via (RS.Atom Post.FooPost) instance RS.Breakdown Post.FooPost
120+
121+
122+--deriving via (Compound RS.BuildFrom.Wrapped) instance RS.BuildFrom RS.BuildFrom.Wrapped
123+--deriving via (Compound RS.BuildFrom.Wrapped) instance RS.Breakdown RS.BuildFrom.Wrapped
65124
66125
67−instance RS.Breakdown Headers.Foo where
68− breakdown = pure . toDyn
69126
70−instance RS.BuildFrom Foo.Foo
127+-- deriving via (RS.Atom Void) instance RS.BuildFrom Void
71128
72−instance RS.BuildFrom Headers.Foo
129+-- instance RS.Breakdown Post.FooPost
130+-- instance RS.BuildFrom Post.FooPost
73131
74−instance RS.BuildFrom Seeded.Seed
75132
76−instance RS.BuildFrom Void
133+-- | `shouldFail` allows you to assert that a given `Spec` should contain at least one failing test.
134+-- this is often useful when testing tests.
135+shouldFail :: SpecWith a -> SpecWith a
136+shouldFail = mapSpecItem_ (\i -> i {
137+ itemExample = \p a cb -> do
138+ r <- (itemExample i) p a cb
139+ pure r {resultStatus = case resultStatus r of
140+ Success -> Failure Nothing (Reason "Unexpected success")
141+ Failure _ _ -> Success
142+ x -> x
143+ }
144+ })
test/Valid.hs+24−1
@@ -9,9 +9,32 @@ module Valid where
99
1010import Servant
1111import Data.Void
12+import GHC.Generics
13+import Servant.API.Generic
14+import Servant.Server.Generic
1215
1316type Api = Get '[JSON] Int
1417 :<|> Capture "void" Void :> Get '[JSON] ()
15−
18+
19+data Routes route
20+ = Routes
21+ { getInt :: route :-
22+ Get '[JSON] Int
23+ , captureIt :: route :-
24+ Capture "void" Void :> Get '[JSON] ()
25+ } deriving Generic
26+
27+type RoutedApi = ToServantApi Routes
28+
29+-- routedApi = genericApi (Proxy :: Proxy Routes)
30+routedServer :: Server RoutedApi
31+routedServer = genericServer routes
32+
33+routes :: Routes AsServer
34+routes = Routes
35+ { getInt = pure 7
36+ , captureIt = const (pure ())
37+ }
38+
1639server :: Server Api
1740server = pure 7 :<|> const (pure ())