@@ -2,104 +2,53 @@
22{-# LANGUAGE FlexibleContexts #-}
33{-# LANGUAGE GADTs #-}
44{-# LANGUAGE LambdaCase #-}
5+{-# LANGUAGE PolyKinds #-}
56{-# LANGUAGE RankNTypes #-}
67{-# LANGUAGE RecordWildCards #-}
78{-# LANGUAGE ScopedTypeVariables #-}
89{-# LANGUAGE TupleSections #-}
9−{-# LANGUAGE TypeApplications #-}
10−{-# LANGUAGE TypeFamilies #-}
1110
11+{-# LANGUAGE TypeFamilies #-}
1212{-# LANGUAGE UndecidableInstances #-}
1313
1414module Roboservant.Direct
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(..)
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 (..),
2024 )
2125where
2226
2327import 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 )
28+ ( Exception,
29+ Handler (Handler),
30+ SomeAsyncException,
31+ SomeException,
32+ catch,
33+ catches,
34+ handle,
35+ throw,
36+ )
5437import Control.Monad.State.Strict
55− ( MonadIO, MonadState, get, liftIO, modify', runStateT )
56−
57−import Control.Exception.Lifted(throw,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
58−import Control.Monad(void,replicateM,forM_)
59−import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,execStateT)
60−import Control.Monad.Trans.Control(MonadBaseControl)
61−import Data.Dependent.Map (DMap)
62−import Data.Dynamic (Dynamic(..), dynApply, dynTypeRep, fromDynamic)
63−import Data.List.NonEmpty (NonEmpty(..))
64−import Data.Map.Strict(Map)
65−import qualified Data.Set as Set
66−import Data.Maybe (mapMaybe)
67−import Data.Typeable (TypeRep)
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)
81−import System.Timeout.Lifted(timeout)
82−import GHC.Generics ( (:*:)(..) )
83−import GHC.Generics ( (:*:)(..) )
84−
85−import qualified Data.Vinyl.Functor as V
86−import qualified Data.Vinyl.Functor as V
87−
88−import qualified Data.Vinyl.Curry as V
89−import 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)
38+ ( MonadIO (..),
39+ MonadState (get),
40+ StateT (runStateT),
41+ modify',
42+ )
9543import Control.Monad.Trans.Control (MonadBaseControl)
9644import qualified Data.Dependent.Map as DM
97−import Data.Dynamic (Dynamic (..), dynTypeRep)
45+import Data.Dynamic (Dynamic (..))
46+import qualified Data.IntSet as IntSet
9847import Data.List.NonEmpty (NonEmpty (..))
9948import qualified Data.List.NonEmpty as NEL
10049import Data.Maybe (mapMaybe)
10150import qualified Data.Set as Set
102−import Data.Time.Clock
51+import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
10352import qualified Data.Vinyl as V
10453import qualified Data.Vinyl.Curry as V
10554import qualified Data.Vinyl.Functor as V
@@ -107,25 +56,26 @@ import GHC.Generics ((:*:) (..))
10756import Roboservant.Types
10857 ( ApiOffset (..),
10958 Argument (..),
110− FlattenServer (..),
59+ InteractionError(..),
11160 Provenance (..),
61+ ReifiedApi,
11262 ReifiedEndpoint (..),
11363 Stash (..),
11464 StashValue (..),
115− ToReifiedApi (..),
116− TypedF
65+ TypedF,
11766 )
118−import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
119−import System.Random (StdGen, mkStdGen, randomR)
67+import Roboservant.Types.Config
68+
69+import System.Random (Random (randomR), StdGen, mkStdGen)
12070import qualified Type.Reflection as R
121−import qualified Data.IntSet as IntSet
12271
12372data RoboservantException
12473 = RoboservantException
125− { failureReason :: FailureType
126− , serverException :: Maybe SomeException
127− , fuzzState :: FuzzState
128− } deriving (Show)
74+ { failureReason :: FailureType,
75+ serverException :: Maybe SomeException,
76+ fuzzState :: FuzzState
77+ }
78+ deriving (Show)
12979
13080instance Exception RoboservantException
13181
@@ -134,7 +84,7 @@ data FailureType
13484 | CheckerFailed
13585 | NoPossibleMoves
13686 | InsufficientCoverage Double
137− deriving (Show,Eq)
87+ deriving (Show, Eq)
13888
13989data FuzzOp
14090 = FuzzOp
@@ -143,15 +93,6 @@ data FuzzOp
14393 }
14494 deriving (Show, Eq)
14595
146−data Config
147− = Config
148− { seed :: [(Dynamic,Int)],
149− maxRuntime :: Double, -- seconds to test for
150− maxReps :: Integer,
151− rngSeed :: Int,
152− coverageThreshold :: Double
153− }
154−
15596data FuzzState
15697 = FuzzState
15798 { path :: [FuzzOp],
@@ -164,7 +105,7 @@ data EndpointOption
164105 = forall as.
165106 (V.RecordToList as, V.RMap as) =>
166107 EndpointOption
167− { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int)))),
108+ { eoCall :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))),
168109 eoArgs :: V.Rec (TypedF StashValue) as
169110 }
170111
@@ -173,19 +114,25 @@ data StopReason
173114 | HitMaxIterations
174115 deriving (Show, Eq)
175116
176−data Report = Report
177− { textual :: String
178− , rsException :: RoboservantException}
117+data Report
118+ = Report
119+ { textual :: String,
120+ rsException :: RoboservantException
121+ }
179122 deriving (Show)
180123
181−fuzz ::
182− forall api.
183− (FlattenServer api, ToReifiedApi (Endpoints api)) =>
184− Server api ->
124+
125+
126+-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
127+-- fuzzClient = undefined
128+
129+
130+
131+fuzz' ::
132+ ReifiedApi ->
185133 Config ->
186− IO () ->
187134 IO (Maybe Report)
188−fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
135+fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
189136 let path = []
190137 stash = addToStash seed mempty
191138 currentRng = mkStdGen rngSeed
@@ -194,39 +141,36 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
194141 runStateT
195142 (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get))
196143 FuzzState {..}
197− print stopreason
144+ logInfo $ show stopreason
198145 pure Nothing
199146 where
200147 -- something less terrible later
201148 formatException :: RoboservantException -> Report
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)
216−
217− -- evaluateCoverage :: FuzzState -> m ()
218− evaluateCoverage f@FuzzState{..}
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))
162+
163+ evaluateCoverage f@FuzzState {..}
219164 | coverage > coverageThreshold = pure ()
220165 | 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
226−
227−
228− untilDone :: MonadIO m => (Integer,UTCTime) -> m a -> m StopReason
229− untilDone (0,_) _ = pure HitMaxIterations
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
230174 untilDone (n, deadline) action = do
231175 now <- liftIO getCurrentTime
232176 if now > deadline
@@ -234,12 +178,13 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
234178 else do
235179 _ <- action
236180 untilDone (n -1, deadline) action
237− reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
238− routeCount = length reifiedApi
239181
240− elementOrFail :: (MonadState FuzzState m, MonadIO m)
241− => [a] -> m a
242− elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing =<< get
182+ routeCount = length reifiedApi
183+ elementOrFail ::
184+ (MonadState FuzzState m, MonadIO m) =>
185+ [a] ->
186+ m a
187+ elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing =<< get
243188 elementOrFail l = do
244189 st <- get
245190 let (index, newGen) = randomR (0, length l - 1) (currentRng st)
@@ -250,7 +195,7 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
250195 ( forall as.
251196 (V.RecordToList as, V.RMap as) =>
252197 FuzzOp ->
253− V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int)))) ->
198+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
254199 V.Rec (TypedF V.Identity) as ->
255200 m r
256201 ) ->
@@ -291,57 +236,53 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
291236 execute ::
292237 (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
293238 FuzzOp ->
294− V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int)))) ->
239+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
295240 V.Rec (TypedF V.Identity) as ->
296241 m ()
297242 execute fuzzop func args = do
298− (liftIO . print . (fuzzop,) . stash ) =<< get
299− st <- get
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
243+ (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
308244 liftIO (V.runcurry' func argVals) >>= \case
309− -- parameterise this
310− Left (serverError :: ServerError) ->
311− case errHTTPCode serverError of
312− 500 -> throw serverError
313− _ -> do
314− liftIO $ print ("ignoring non-500 error", serverError)
315− Right (dyn :: NEL.NonEmpty (Dynamic,Int)) -> do
316− -- liftIO $ print ("storing", fmap dynTypeRep dyn)
317− modify' (\fs@FuzzState{..} ->
318− fs { stash = addToStash (NEL.toList dyn) stash } )
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+ )
319254 where
320255 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
321− argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
256+ -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
322257 go ::
323258 (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
324259 m ()
325260 go = withOp $ \op func args -> do
326− catches (execute op func args)
327− [ Handler (\(e :: SomeAsyncException) -> throw e)
328− , Handler (\(e :: SomeException) -> do
329− displayDiagnostics =<< get
330− throw . RoboservantException ServerCrashed (Just e) =<< get)
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+ )
331268 ]
332− catch (liftIO checker)
333− (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
269+ catch
270+ (liftIO healthCheck)
271+ (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
334272
335273addToStash ::
336− [(Dynamic,Int)] ->
274+ [(Dynamic, Int)] ->
337275 Stash ->
338276 Stash
339277addToStash result stash =
340278 foldr
341− ( \(Dynamic tr x,hashed) (Stash dict) ->
279+ ( \(Dynamic tr x, hashed) (Stash dict) ->
342280 Stash $
343− DM.insertWith renumber tr
344− (StashValue (([Provenance (R.SomeTypeRep tr) 0], x) :| []) (IntSet.singleton hashed)) dict
281+ DM.insertWith
282+ renumber
283+ tr
284+ (StashValue (([Provenance (R.SomeTypeRep tr) 0], x) :| []) (IntSet.singleton hashed))
285+ dict
345286 )
346287 stash
347288 result
@@ -354,11 +295,11 @@ addToStash result stash =
354295 | not $ IntSet.null (singleHash `IntSet.intersection` intSet) = orig
355296 | otherwise =
356297 StashValue
357− (case NEL.toList singleDyn of
358− [([Provenance tr _], dyn)] ->
359− l <> pure (([Provenance tr (length (NEL.last l) + 1)], dyn))
360− _ -> error "should be impossible")
361−
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+ )
362303 (IntSet.union singleHash intSet)
363304
364305-- why isn't this in vinyl?