@@ -2,60 +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
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)
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+ )
5143import Control.Monad.Trans.Control (MonadBaseControl)
5244import qualified Data.Dependent.Map as DM
53−import Data.Dynamic (Dynamic (..), dynTypeRep)
45+import Data.Dynamic (Dynamic (..))
46+import qualified Data.IntSet as IntSet
5447import Data.List.NonEmpty (NonEmpty (..))
5548import qualified Data.List.NonEmpty as NEL
5649import Data.Maybe (mapMaybe)
5750import qualified Data.Set as Set
58−import Data.Time.Clock
51+import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
5952import qualified Data.Vinyl as V
6053import qualified Data.Vinyl.Curry as V
6154import qualified Data.Vinyl.Functor as V
@@ -63,25 +56,26 @@ import GHC.Generics ((:*:) (..))
6356import Roboservant.Types
6457 ( ApiOffset (..),
6558 Argument (..),
66− FlattenServer (..),
59+ InteractionError(..),
6760 Provenance (..),
61+ ReifiedApi,
6862 ReifiedEndpoint (..),
6963 Stash (..),
7064 StashValue (..),
71− ToReifiedApi (..),
72− TypedF
65+ TypedF,
7366 )
74−import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
75−import System.Random (StdGen, mkStdGen, randomR)
67+import Roboservant.Types.Config
68+
69+import System.Random (Random (randomR), StdGen, mkStdGen)
7670import qualified Type.Reflection as R
77−import qualified Data.IntSet as IntSet
7871
7972data RoboservantException
8073 = RoboservantException
81− { failureReason :: FailureType
82− , serverException :: Maybe SomeException
83− , fuzzState :: FuzzState
84− } deriving (Show)
74+ { failureReason :: FailureType,
75+ serverException :: Maybe SomeException,
76+ fuzzState :: FuzzState
77+ }
78+ deriving (Show)
8579
8680instance Exception RoboservantException
8781
@@ -90,7 +84,7 @@ data FailureType
9084 | CheckerFailed
9185 | NoPossibleMoves
9286 | InsufficientCoverage Double
93− deriving (Show,Eq)
87+ deriving (Show, Eq)
9488
9589data FuzzOp
9690 = FuzzOp
@@ -99,15 +93,6 @@ data FuzzOp
9993 }
10094 deriving (Show, Eq)
10195
102−data Config
103− = Config
104− { seed :: [(Dynamic,Int)],
105− maxRuntime :: Double, -- seconds to test for
106− maxReps :: Integer,
107− rngSeed :: Int,
108− coverageThreshold :: Double
109− }
110−
11196data FuzzState
11297 = FuzzState
11398 { path :: [FuzzOp],
@@ -120,7 +105,7 @@ data EndpointOption
120105 = forall as.
121106 (V.RecordToList as, V.RMap as) =>
122107 EndpointOption
123− { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int)))),
108+ { eoCall :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))),
124109 eoArgs :: V.Rec (TypedF StashValue) as
125110 }
126111
@@ -129,19 +114,25 @@ data StopReason
129114 | HitMaxIterations
130115 deriving (Show, Eq)
131116
132−data Report = Report
133− { textual :: String
134− , rsException :: RoboservantException}
117+data Report
118+ = Report
119+ { textual :: String,
120+ rsException :: RoboservantException
121+ }
135122 deriving (Show)
136123
137−fuzz ::
138− forall api.
139− (FlattenServer api, ToReifiedApi (Endpoints api)) =>
140− Server api ->
124+
125+
126+-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
127+-- fuzzClient = undefined
128+
129+
130+
131+fuzz' ::
132+ ReifiedApi ->
141133 Config ->
142− IO () ->
143134 IO (Maybe Report)
144−fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
135+fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
145136 let path = []
146137 stash = addToStash seed mempty
147138 currentRng = mkStdGen rngSeed
@@ -150,39 +141,36 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
150141 runStateT
151142 (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get))
152143 FuzzState {..}
153− print stopreason
144+ logInfo $ show stopreason
154145 pure Nothing
155146 where
156147 -- something less terrible later
157148 formatException :: RoboservantException -> Report
158− formatException r@(RoboservantException failureType exception _state) = Report
159− (unlines [ show failureType , show exception ])
160− r
161−
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)
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))
172162
173− -- evaluateCoverage :: FuzzState -> m ()
174− evaluateCoverage f@FuzzState{..}
163+ evaluateCoverage f@FuzzState {..}
175164 | coverage > coverageThreshold = pure ()
176165 | otherwise = do
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
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
186174 untilDone (n, deadline) action = do
187175 now <- liftIO getCurrentTime
188176 if now > deadline
@@ -190,12 +178,13 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
190178 else do
191179 _ <- action
192180 untilDone (n -1, deadline) action
193− reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
194− routeCount = length reifiedApi
195181
196− elementOrFail :: (MonadState FuzzState m, MonadIO m)
197− => [a] -> m a
198− 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
199188 elementOrFail l = do
200189 st <- get
201190 let (index, newGen) = randomR (0, length l - 1) (currentRng st)
@@ -206,7 +195,7 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
206195 ( forall as.
207196 (V.RecordToList as, V.RMap as) =>
208197 FuzzOp ->
209− V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int)))) ->
198+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
210199 V.Rec (TypedF V.Identity) as ->
211200 m r
212201 ) ->
@@ -247,57 +236,53 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
247236 execute ::
248237 (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
249238 FuzzOp ->
250− V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int)))) ->
239+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
251240 V.Rec (TypedF V.Identity) as ->
252241 m ()
253242 execute fuzzop func args = do
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
243+ (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
264244 liftIO (V.runcurry' func argVals) >>= \case
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,Int)) -> do
272− -- liftIO $ print ("storing", fmap dynTypeRep dyn)
273− modify' (\fs@FuzzState{..} ->
274− 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+ )
275254 where
276255 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
277− argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
256+ -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
278257 go ::
279258 (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
280259 m ()
281260 go = withOp $ \op func args -> do
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)
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+ )
287268 ]
288− catch (liftIO checker)
289− (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
269+ catch
270+ (liftIO healthCheck)
271+ (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
290272
291273addToStash ::
292− [(Dynamic,Int)] ->
274+ [(Dynamic, Int)] ->
293275 Stash ->
294276 Stash
295277addToStash result stash =
296278 foldr
297− ( \(Dynamic tr x,hashed) (Stash dict) ->
279+ ( \(Dynamic tr x, hashed) (Stash dict) ->
298280 Stash $
299− DM.insertWith renumber tr
300− (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
301286 )
302287 stash
303288 result
@@ -310,12 +295,11 @@ addToStash result stash =
310295 | not $ IntSet.null (singleHash `IntSet.intersection` intSet) = orig
311296 | otherwise =
312297 StashValue
313− (case NEL.toList singleDyn of
314− [([Provenance tr _], dyn)] ->
315− l
316− <> pure (([Provenance tr (length (NEL.last l) + 1)], dyn))
317− _ -> error "should be impossible")
318−
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+ )
319303 (IntSet.union singleHash intSet)
320304
321305-- why isn't this in vinyl?