@@ -6,6 +6,7 @@
6{-# LANGUAGE GADTs #-}
6{-# LANGUAGE GADTs #-}
7{-# LANGUAGE LambdaCase #-}
7{-# LANGUAGE LambdaCase #-}
8{-# LANGUAGE PolyKinds #-}
8{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE RankNTypes #-}
9{-# LANGUAGE RecordWildCards #-}
10{-# LANGUAGE RecordWildCards #-}
10{-# LANGUAGE ScopedTypeVariables #-}
11{-# LANGUAGE ScopedTypeVariables #-}
11{-# LANGUAGE TupleSections #-}
12{-# LANGUAGE TupleSections #-}
@@ -18,14 +19,20 @@ module Roboservant.Direct
18 ( fuzz, Config(..)
19 ( fuzz, Config(..)
20 -- TODO come up with something smarter than exporting all this, we should
21 -- TODO come up with something smarter than exporting all this, we should
21− -- have some nice error-display functions
22+ -- have some nice error-display functions
22 , RoboservantException(..), FuzzState(..), FuzzOp(..), FailureType(..), Report(..)
23 , RoboservantException(..), FuzzState(..), FuzzOp(..), FailureType(..), Report(..)
26import Control.Exception.Lifted(throw,handle,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
27import Control.Exception.Lifted(throw,handle,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
27import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,runStateT)
28import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,runStateT)
29+import Control.Exception.Lifted(throw,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
30+import Control.Monad(void,replicateM,forM_)
31+import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,execStateT)
32+import Control.Monad.Trans.Control(MonadBaseControl)
33+import Data.Dependent.Map (DMap)
28−import Data.Dynamic (Dynamic, dynApply, dynTypeRep, fromDynamic)
34+import Data.Dynamic (Dynamic(..), dynApply, dynTypeRep, fromDynamic)
35+import Data.List.NonEmpty (NonEmpty(..))
29import Data.Map.Strict(Map)
36import Data.Map.Strict(Map)
30import qualified Data.Set as Set
37import qualified Data.Set as Set
31import Data.Maybe (mapMaybe)
38import Data.Maybe (mapMaybe)
@@ -37,6 +44,16 @@ import qualified Data.Map.Strict as Map
37import Data.Time.Clock
44import Data.Time.Clock
38import Data.List(sortOn)
45import Data.List(sortOn)
39import Control.Arrow(second)
46import Control.Arrow(second)
47+import System.Timeout.Lifted(timeout)
48+import GHC.Generics ((:*:)(..))
49+import qualified Data.Vinyl.Functor as V
50+import qualified Data.Vinyl.Curry as V
51+import qualified Data.Dependent.Map as DM
52+import qualified Data.Vinyl as V
53+import qualified Data.List.NonEmpty as NEL
54+import qualified Data.Map.Strict as Map
55+import Data.Time.Clock
56+import qualified Type.Reflection as R
41import Roboservant.Types.Breakdown
58import Roboservant.Types.Breakdown
42import Roboservant.Types
59import Roboservant.Types
@@ -44,11 +61,19 @@ import Roboservant.Types
44 FlattenServer (..),
61 FlattenServer (..),
45-- ReifiedApi,
62-- ReifiedApi,
46 ToReifiedApi (..),
63 ToReifiedApi (..),
48data RoboservantException
72data RoboservantException
49 = RoboservantException
73 = RoboservantException
50 { failureReason :: FailureType
74 { failureReason :: FailureType
51 , serverException :: Maybe SomeException
75 , serverException :: Maybe SomeException
52 , fuzzState :: FuzzState
77 , fuzzState :: FuzzState
53 } deriving (Show)
78 } deriving (Show)
@@ -81,7 +106,12 @@ data FuzzState = FuzzState
81 , stash :: Stash
106 , stash :: Stash
82 , currentRng :: StdGen
107 , currentRng :: StdGen
84− deriving (Show)
109+ deriving Show
111+data EndpointOption = forall as. (V.RecordToList as, V.RMap as) => EndpointOption
112+ { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
113+ , eoArgs :: V.Rec (TypedF StashValue) as
86data StopReason
116data StopReason
87 = TimedOut
117 = TimedOut
@@ -107,9 +137,8 @@ fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
107 deadline :: UTCTime <- addUTCTime (fromInteger $ maxRuntime * 1000000) <$> getCurrentTime
137 deadline :: UTCTime <- addUTCTime (fromInteger $ maxRuntime * 1000000) <$> getCurrentTime
108 (stopreason, fs ) <- runStateT
138 (stopreason, fs ) <- runStateT
109 (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get)) FuzzState{..}
139 (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get)) FuzzState{..}
110 pure Nothing
141 pure Nothing
111− -- mapM_ (print . (\(offset, (args, dyn) ) -> (offset, map fst args, dyn))) reifiedApi
114 -- something less terrible later
143 -- something less terrible later
115 formatException :: RoboservantException -> Report
144 formatException :: RoboservantException -> Report
@@ -159,67 +188,77 @@ fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
159 modify' $ \st' -> st' { currentRng = newGen }
188 modify' $ \st' -> st' { currentRng = newGen }
160 pure (l !! index)
189 pure (l !! index)
162− genOp :: (MonadState FuzzState m, MonadIO m)
191+ withOp :: (MonadState FuzzState m, MonadIO m)
192+ => (forall as. (V.RecordToList as, V.RMap as)
163− => m (FuzzOp, Dynamic, [Dynamic])
193+ => FuzzOp
164− genOp = do -- fs@FuzzState{..} = do
194+ -> V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
195+ -> V.Rec (TypedF V.Identity) as -> m r
198+ withOp callback = do
165 -- choose a call to make, from the endpoints with fillable arguments.
199 -- choose a call to make, from the endpoints with fillable arguments.
166− (offset, dynCall, args) <- elementOrFail . options =<< get
200+ (offset, EndpointOption{..}) <- elementOrFail . options =<< get
167− r <- mapM (elementOrFail . zip [0..] . NEL.toList) args
202+ (\(tr :*: StashValue svs) -> elementOrFail $
203+ zipWith (\i xy -> V.Const i :*: tr :*: xy)
168− let pathSegment = FuzzOp offset (map (\(index,(_,dyn) ) -> Provenance (dynTypeRep dyn) index) r)
208+ let pathSegment = FuzzOp offset $
210+ (\(V.Const index :*: tr :*: _) -> Provenance (R.SomeTypeRep tr) index)
213+ (\(_ :*: tr :*: (_, x)) -> tr :*: V.Identity x)
169 modify' (\f -> f { path = path f <> [pathSegment] })
215 modify' (\f -> f { path = path f <> [pathSegment] })
170− pure (pathSegment, dynCall, fmap (snd . snd) r)
216+ callback pathSegment eoCall argValues
173− options :: FuzzState -> [(ApiOffset, Dynamic, [NEL.NonEmpty ([Provenance], Dynamic)])]
218+ options :: FuzzState -> [(ApiOffset, EndpointOption)]
174 options FuzzState{..} =
219 options FuzzState{..} =
176− ( \(offset, (argreps, dynCall)) -> (offset,dynCall,) <$>
221+ ( \(offset, ReifiedEndpoint{..}) -> do
177− mapM (\(_tr,bf) -> bf stash ) argreps
222+ args <- V.rtraverse (\(tr :*: Argument bf) -> (tr :*:) <$> bf stash) reArguments
223+ pure (offset, EndpointOption reEndpointFunc args)
179 reifiedApi
225 reifiedApi
181− execute :: (MonadState FuzzState m, MonadIO m)
227+ execute :: (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as)
182− => (FuzzOp,Dynamic,[Dynamic]) -> m ()
228+ => FuzzOp
229+ -> V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
230+ -> V.Rec (TypedF V.Identity) as
183− execute (fuzzop, dyncall, args) = do
232+ execute fuzzop func args = do
184− -- liftIO $ print fuzzop
233+ liftIO $ print fuzzop
185− -- now, magic happens: we apply some dynamic arguments to a dynamic
186− -- function and hopefully something useful pops out the end.
187− let func = foldr (\arg curr -> flip dynApply arg =<< curr) (Just dyncall) (reverse args)
188 st <- get
234 st <- get
189− let showable = unlines $ ("args":map (show . dynTypeRep) args)
235+ let showable = unlines $ ("args":map show argTypes)
190 <> ["fuzzop"
236 <> ["fuzzop"
191 , show fuzzop
237 , show fuzzop
192− ,"dyncall"
238+ -- ,"dyncall"
193− ,show (dynTypeRep dyncall)
239+ -- ,show (dynTypeRep dyncall)
195 ,show st]
241 ,show st]
196− -- liftIO $ putStrLn showable
242+ liftIO $ putStrLn showable
197−
243+ liftIO (V.runcurry' func argVals) >>= \case
199− Nothing -> error ("all screwed up 1: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
202− case fromDynamic f' of
203− Nothing -> error ("all screwed up 2: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
204− Just f -> liftIO f >>= \case
205− -- parameterise this
244+ -- parameterise this
206− Left (serverError :: ServerError) ->
245+ Left (serverError :: ServerError) ->
207− case errHTTPCode serverError of
246+ case errHTTPCode serverError of
208− 500 -> throw serverError
247+ 500 -> throw serverError
210− liftIO $ print ("ignoring non-500 error" , serverError)
249+ liftIO $ print ("ignoring non-500 error" , serverError)
212− Right (dyn :: NEL.NonEmpty Dynamic) -> do
250+ Right (dyn :: NEL.NonEmpty Dynamic) -> do
213− -- liftIO $ print ("storing", fmap dynTypeRep dyn)
251+ liftIO $ print ("storing", fmap dynTypeRep dyn)
214− modify' (\fs@FuzzState{..} ->
252+ modify' (\fs@FuzzState{..} ->
215− fs { stash = addToStash (NEL.toList dyn) stash } )
253+ fs { stash = addToStash (NEL.toList dyn) stash } )
217−
255+ argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
256+ argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
218−-- go :: (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m)
258+ go :: (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m)
219−-- => m ()
259+ => m ()
220− go = do
260+ go = withOp $ \op func args -> do
222− catches (execute op)
261+ catches (execute op func args)
223 [ Handler (\(e :: SomeAsyncException) -> throw e)
262 [ Handler (\(e :: SomeAsyncException) -> throw e)
224 , Handler (\(e :: SomeException) -> do
263 , Handler (\(e :: SomeException) -> do
225 displayDiagnostics =<< get
264 displayDiagnostics =<< get
@@ -228,25 +267,29 @@ fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
228 catch (liftIO checker)
267 catch (liftIO checker)
229 (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
268 (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
234− -- (Range.linear 1 100)
236− -- (fmap preload seed <> [callEndpoint reifiedApi])
237− -- executeSequential emptyState actions
238addToStash :: [Dynamic]
270addToStash :: [Dynamic]
239− -> Map TypeRep (NEL.NonEmpty ([Provenance], Dynamic))
271+ -> Stash
240− -> Map TypeRep (NEL.NonEmpty ([Provenance], Dynamic))
272+ -> Stash
241addToStash result stash =
273addToStash result stash =
242− foldr (\dyn dict -> let tr = dynTypeRep dyn in
274+ foldr (\(Dynamic tr x) (Stash dict) -> Stash $
243− Map.insertWith renumber tr (pure ([Provenance tr 0],dyn)) dict) stash result
275+ DM.insertWith renumber tr (StashValue (([Provenance (R.SomeTypeRep tr) 0],x):|[])) dict
244−-- Map.insertWith (flip (<>)) (dynTypeRep result) (_pure result) stash })
276+ )
246− renumber :: NEL.NonEmpty ([Provenance],Dynamic)
280+ renumber :: StashValue a
247− -> NEL.NonEmpty ([Provenance],Dynamic)
281+ -> StashValue a
248− -> NEL.NonEmpty ([Provenance],Dynamic)
282+ -> StashValue a
249− renumber singleDyn l = case NEL.toList singleDyn of
283+ renumber (StashValue singleDyn) (StashValue l) = StashValue $ case NEL.toList singleDyn of
250 [([Provenance tr _], dyn)] -> l
284 [([Provenance tr _], dyn)] -> l
251 <> pure ([Provenance tr (length (NEL.last l) + 1)], dyn)
285 <> pure ([Provenance tr (length (NEL.last l) + 1)], dyn)
252 _ -> error "should be impossible"
286 _ -> error "should be impossible"
289+-- why isn't this in vinyl?
291+ :: (V.RecordToList as, V.RMap as)
292+ => (forall x. f x -> a)
295+recordToList' f = V.recordToList . V.rmap (V.Const . f)