@@ -2,53 +2,60 @@
22{-# LANGUAGE FlexibleContexts #-}
33{-# LANGUAGE GADTs #-}
44{-# LANGUAGE LambdaCase #-}
5−{-# LANGUAGE PolyKinds #-}
65{-# LANGUAGE RankNTypes #-}
76{-# LANGUAGE RecordWildCards #-}
87{-# LANGUAGE ScopedTypeVariables #-}
98{-# LANGUAGE TupleSections #-}
10−
9+{-# LANGUAGE TypeApplications #-}
1110{-# LANGUAGE TypeFamilies #-}
11+
1212{-# LANGUAGE UndecidableInstances #-}
1313
1414module Roboservant.Direct
15− ( fuzz',
16− Config (..),
17− -- TODO come up with something smarter than exporting all this, we should
18− -- have some nice error-display functions
19− RoboservantException (..),
20− FuzzState (..),
21− FuzzOp (..),
22− FailureType (..),
23− Report (..),
15+ ( fuzz, Config(..)
16+
17+ -- TODO come up with something smarter than exporting all this, we should
18+ -- have some nice error-display functions
19+ , RoboservantException(..), FuzzState(..), FuzzOp(..), FailureType(..), Report(..)
2420 )
2521where
2622
27−import Control.Exception.Lifted
28− ( Exception,
29− Handler (Handler),
30− SomeAsyncException,
31− SomeException,
32− catch,
33− catches,
34− handle,
35− throw,
36− )
37−import Control.Monad.State.Strict
38− ( MonadIO (..),
39− MonadState (get),
40− StateT (runStateT),
41− modify',
42− )
23+import Control.Exception.Lifted(throw,handle,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
24+import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,runStateT)
25+import Control.Exception.Lifted(throw,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
26+import Control.Monad(void,replicateM,forM_)
27+import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,execStateT)
28+import Control.Monad.Trans.Control(MonadBaseControl)
29+import Data.Dependent.Map (DMap)
30+import Data.Dynamic (Dynamic(..), dynApply, dynTypeRep, fromDynamic)
31+import Data.List.NonEmpty (NonEmpty(..))
32+import Data.Map.Strict(Map)
33+import qualified Data.Set as Set
34+import Data.Maybe (mapMaybe)
35+import Data.Typeable (TypeRep)
36+import Servant (Endpoints, Proxy (Proxy), Server, ServerError(..))
37+import System.Random(StdGen,randomR,mkStdGen)
38+import qualified Data.List.NonEmpty as NEL
39+import qualified Data.Map.Strict as Map
40+import Data.Time.Clock
41+import Data.List(sortOn)
42+import Control.Arrow(second)
43+import System.Timeout.Lifted(timeout)
44+import GHC.Generics ((:*:)(..))
45+import qualified Data.Vinyl.Functor as V
46+import qualified Data.Vinyl.Curry as V
47+
48+
49+import Control.Exception.Lifted (Exception, Handler (..), SomeAsyncException, SomeException, catch, catches, handle, throw)
50+import Control.Monad.State.Strict (MonadIO, MonadState, get, liftIO, modify', runStateT)
4351import Control.Monad.Trans.Control (MonadBaseControl)
4452import qualified Data.Dependent.Map as DM
45−import Data.Dynamic (Dynamic (..))
46−import qualified Data.IntSet as IntSet
53+import Data.Dynamic (Dynamic (..), dynTypeRep)
4754import Data.List.NonEmpty (NonEmpty (..))
4855import qualified Data.List.NonEmpty as NEL
4956import Data.Maybe (mapMaybe)
5057import qualified Data.Set as Set
51−import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
58+import Data.Time.Clock
5259import qualified Data.Vinyl as V
5360import qualified Data.Vinyl.Curry as V
5461import qualified Data.Vinyl.Functor as V
@@ -56,26 +63,25 @@ import GHC.Generics ((:*:) (..))
5663import Roboservant.Types
5764 ( ApiOffset (..),
5865 Argument (..),
59− InteractionError(..),
66+ FlattenServer (..),
6067 Provenance (..),
61− ReifiedApi,
6268 ReifiedEndpoint (..),
6369 Stash (..),
6470 StashValue (..),
65− TypedF,
71+ ToReifiedApi (..),
72+ TypedF
6673 )
67−import Roboservant.Types.Config
68−
69−import System.Random (Random (randomR), StdGen, mkStdGen)
74+import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
75+import System.Random (StdGen, mkStdGen, randomR)
7076import qualified Type.Reflection as R
7177
78+
7279data RoboservantException
7380 = RoboservantException
74− { failureReason :: FailureType,
75− serverException :: Maybe SomeException,
76− fuzzState :: FuzzState
77− }
78− deriving (Show)
81+ { failureReason :: FailureType
82+ , serverException :: Maybe SomeException
83+ , fuzzState :: FuzzState
84+ } deriving (Show)
7985
8086instance Exception RoboservantException
8187
@@ -84,7 +90,7 @@ data FailureType
8490 | CheckerFailed
8591 | NoPossibleMoves
8692 | InsufficientCoverage Double
87− deriving (Show, Eq)
93+ deriving (Show,Eq)
8894
8995data FuzzOp
9096 = FuzzOp
@@ -93,6 +99,15 @@ data FuzzOp
9399 }
94100 deriving (Show, Eq)
95101
102+data Config
103+ = Config
104+ { seed :: [Dynamic],
105+ maxRuntime :: Double, -- seconds to test for
106+ maxReps :: Integer,
107+ rngSeed :: Int,
108+ coverageThreshold :: Double
109+ }
110+
96111data FuzzState
97112 = FuzzState
98113 { path :: [FuzzOp],
@@ -105,7 +120,7 @@ data EndpointOption
105120 = forall as.
106121 (V.RecordToList as, V.RMap as) =>
107122 EndpointOption
108− { eoCall :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))),
123+ { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty Dynamic))),
109124 eoArgs :: V.Rec (TypedF StashValue) as
110125 }
111126
@@ -114,25 +129,19 @@ data StopReason
114129 | HitMaxIterations
115130 deriving (Show, Eq)
116131
117−data Report
118− = Report
119− { textual :: String,
120− rsException :: RoboservantException
121− }
132+data Report = Report
133+ { textual :: String
134+ , rsException :: RoboservantException}
122135 deriving (Show)
123136
124−
125−
126−-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
127−-- fuzzClient = undefined
128−
129−
130−
131−fuzz' ::
132− ReifiedApi ->
137+fuzz ::
138+ forall api.
139+ (FlattenServer api, ToReifiedApi (Endpoints api)) =>
140+ Server api ->
133141 Config ->
142+ IO () ->
134143 IO (Maybe Report)
135−fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
144+fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
136145 let path = []
137146 stash = addToStash seed mempty
138147 currentRng = mkStdGen rngSeed
@@ -141,36 +150,39 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
141150 runStateT
142151 (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get))
143152 FuzzState {..}
144− logInfo $ show stopreason
153+ print stopreason
145154 pure Nothing
146155 where
147156 -- something less terrible later
148157 formatException :: RoboservantException -> Report
149− formatException r@(RoboservantException failureType exception _state) =
150− Report
151− (unlines [show failureType, show exception])
152− r
153− displayDiagnostics FuzzState {..} = liftIO $
154− logInfo $ unlines $
155− ["api endpoints covered"]
156− <> (map show . Set.toList . Set.fromList $ map apiOffset path)
157− <> ["", "types in stash"]
158− <> DM.foldrWithKey (\_ v r -> (show . NEL.length . getStashValue $ v) : r) [] (getStash stash)
159− -- <> (map (show . NEL.length . getStashValue ) $ DM.assocs (getStash stash))
160− -- $ \_k v ->
161− -- (show . NEL.length $ getStashValue v))
158+ formatException r@(RoboservantException failureType exception _state) = Report
159+ (unlines [ show failureType , show exception ])
160+ r
162161
163− evaluateCoverage f@FuzzState {..}
162+ displayDiagnostics FuzzState{..} = liftIO $ do
163+ putStrLn "api endpoints covered"
164+ mapM_ print (Set.toList $ Set.fromList $ map apiOffset path)
165+ putStrLn ""
166+ putStrLn "types in stash"
167+ DM.forWithKey_ (getStash stash) $ \k v ->
168+ print (NEL.length $ getStashValue v)
169+ -- mapM_
170+ -- (print . second NEL.length)
171+ -- (sortOn (show . fst) $ Map.toList $ getStash stash)
172+
173+ -- evaluateCoverage :: FuzzState -> m ()
174+ evaluateCoverage f@FuzzState{..}
164175 | coverage > coverageThreshold = pure ()
165176 | otherwise = do
166− displayDiagnostics f
167− throw $ RoboservantException (InsufficientCoverage coverage) Nothing f
168− where
169− hitRoutes = fromIntegral . Set.size . Set.fromList $ map apiOffset path
170− totalRoutes = fromIntegral routeCount
171− coverage = hitRoutes / totalRoutes
172− untilDone :: MonadIO m => (Integer, UTCTime) -> m a -> m StopReason
173− untilDone (0, _) _ = pure HitMaxIterations
177+ displayDiagnostics f
178+ throw $ RoboservantException (InsufficientCoverage coverage) Nothing f
179+ where hitRoutes = fromIntegral . Set.size . Set.fromList $ map apiOffset path
180+ totalRoutes = fromIntegral routeCount
181+ coverage = hitRoutes / totalRoutes
182+
183+
184+ untilDone :: MonadIO m => (Integer,UTCTime) -> m a -> m StopReason
185+ untilDone (0,_) _ = pure HitMaxIterations
174186 untilDone (n, deadline) action = do
175187 now <- liftIO getCurrentTime
176188 if now > deadline
@@ -178,13 +190,12 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
178190 else do
179191 _ <- action
180192 untilDone (n -1, deadline) action
181−
193+ reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
182194 routeCount = length reifiedApi
183− elementOrFail ::
184− (MonadState FuzzState m, MonadIO m) =>
185− [a] ->
186− m a
187− elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing =<< get
195+
196+ elementOrFail :: (MonadState FuzzState m, MonadIO m)
197+ => [a] -> m a
198+ elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing =<< get
188199 elementOrFail l = do
189200 st <- get
190201 let (index, newGen) = randomR (0, length l - 1) (currentRng st)
@@ -195,7 +206,7 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
195206 ( forall as.
196207 (V.RecordToList as, V.RMap as) =>
197208 FuzzOp ->
198− V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
209+ V.Curried as (IO (Either ServerError (NonEmpty Dynamic))) ->
199210 V.Rec (TypedF V.Identity) as ->
200211 m r
201212 ) ->
@@ -205,7 +216,7 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
205216 (offset, EndpointOption {..}) <- elementOrFail . options =<< get
206217 r <-
207218 V.rtraverse
208− ( \(tr :*: StashValue svs _) ->
219+ ( \(tr :*: StashValue svs) ->
209220 elementOrFail $
210221 zipWith
211222 (\i xy -> V.Const i :*: tr :*: xy)
@@ -236,53 +247,56 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
236247 execute ::
237248 (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
238249 FuzzOp ->
239− V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
250+ V.Curried as (IO (Either ServerError (NonEmpty Dynamic))) ->
240251 V.Rec (TypedF V.Identity) as ->
241252 m ()
242253 execute fuzzop func args = do
243− (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
254+ (liftIO . print . (fuzzop,) . stash ) =<< get
255+ st <- get
256+ let showable = unlines $ ("args":map show argTypes)
257+ <> ["fuzzop"
258+ , show fuzzop
259+ -- ,"dyncall"
260+ -- ,show (dynTypeRep dyncall)
261+ ,"state"
262+ ]
263+ -- liftIO $ putStrLn showable
244264 liftIO (V.runcurry' func argVals) >>= \case
245− Left (e::InteractionError) ->
246− if fatalError e
247− then throw e
248− else pure ()
249− Right (dyn :: NEL.NonEmpty (Dynamic, Int)) ->
250− modify'
251− ( \fs@FuzzState {..} ->
252− fs {stash = addToStash (NEL.toList dyn) stash}
253− )
265+ -- parameterise this
266+ Left (serverError :: ServerError) ->
267+ case errHTTPCode serverError of
268+ 500 -> throw serverError
269+ _ -> do
270+ liftIO $ print ("ignoring non-500 error", serverError)
271+ Right (dyn :: NEL.NonEmpty Dynamic) -> do
272+ -- liftIO $ print ("storing", fmap dynTypeRep dyn)
273+ modify' (\fs@FuzzState{..} ->
274+ fs { stash = addToStash (NEL.toList dyn) stash } )
254275 where
255276 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
256− -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
277+ argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
257278 go ::
258279 (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
259280 m ()
260281 go = withOp $ \op func args -> do
261− catches
262− (execute op func args)
263− [ Handler (\(e :: SomeAsyncException) -> throw e),
264− Handler
265− ( \(e :: SomeException) ->
266− throw . RoboservantException ServerCrashed (Just e) =<< get
267− )
282+ catches (execute op func args)
283+ [ Handler (\(e :: SomeAsyncException) -> throw e)
284+ , Handler (\(e :: SomeException) -> do
285+ displayDiagnostics =<< get
286+ throw . RoboservantException ServerCrashed (Just e) =<< get)
268287 ]
269− catch
270− (liftIO healthCheck)
271− (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
288+ catch (liftIO checker)
289+ (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
272290
273291addToStash ::
274− [(Dynamic, Int)] ->
292+ [Dynamic] ->
275293 Stash ->
276294 Stash
277295addToStash result stash =
278296 foldr
279− ( \(Dynamic tr x, hashed) (Stash dict) ->
297+ ( \(Dynamic tr x) (Stash dict) ->
280298 Stash $
281− DM.insertWith
282− renumber
283− tr
284− (StashValue (([Provenance (R.SomeTypeRep tr) 0], x) :| []) (IntSet.singleton hashed))
285− dict
299+ DM.insertWith renumber tr (StashValue (([Provenance (R.SomeTypeRep tr) 0], x) :| [])) dict
286300 )
287301 stash
288302 result
@@ -291,16 +305,11 @@ addToStash result stash =
291305 StashValue a ->
292306 StashValue a ->
293307 StashValue a
294− renumber (StashValue singleDyn singleHash) orig@(StashValue l intSet)
295− | not $ IntSet.null (singleHash `IntSet.intersection` intSet) = orig
296− | otherwise =
297− StashValue
298− ( case NEL.toList singleDyn of
299− [([Provenance tr _], dyn)] ->
300− l <> pure ([Provenance tr (length (NEL.last l) + 1)], dyn)
301− _ -> error "should be impossible"
302− )
303− (IntSet.union singleHash intSet)
308+ renumber (StashValue singleDyn) (StashValue l) = StashValue $ case NEL.toList singleDyn of
309+ [([Provenance tr _], dyn)] ->
310+ l
311+ <> pure ([Provenance tr (length (NEL.last l) + 1)], dyn)
312+ _ -> error "should be impossible"
304313
305314-- why isn't this in vinyl?
306315recordToList' ::