@@ -1,60 +1,81 @@
1{-# LANGUAGE AllowAmbiguousTypes #-}
1{-# LANGUAGE AllowAmbiguousTypes #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE DerivingStrategies #-}
4{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE FlexibleContexts #-}
5−{-# LANGUAGE FlexibleInstances #-}
6{-# LANGUAGE GADTs #-}
3{-# LANGUAGE GADTs #-}
7−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8{-# LANGUAGE LambdaCase #-}
4{-# LANGUAGE LambdaCase #-}
9{-# LANGUAGE PolyKinds #-}
5{-# LANGUAGE PolyKinds #-}
6+{-# LANGUAGE RankNTypes #-}
10{-# LANGUAGE RecordWildCards #-}
7{-# LANGUAGE RecordWildCards #-}
11{-# LANGUAGE ScopedTypeVariables #-}
8{-# LANGUAGE ScopedTypeVariables #-}
12−{-# LANGUAGE StandaloneDeriving #-}
13{-# LANGUAGE TupleSections #-}
9{-# LANGUAGE TupleSections #-}
14−{-# LANGUAGE TypeApplications #-}
10+
15{-# LANGUAGE TypeFamilies #-}
11{-# LANGUAGE TypeFamilies #-}
16−{-# LANGUAGE TypeOperators #-}
17{-# LANGUAGE UndecidableInstances #-}
12{-# LANGUAGE UndecidableInstances #-}
19module Roboservant.Direct
14module Roboservant.Direct
20− ( fuzz, Config(..)
16+ Config (..),
22− -- TODO come up with something smarter than exporting all this, we should
17+ -- TODO come up with something smarter than exporting all this, we should
23− -- have some nice error-display functions
18+ -- have some nice error-display functions
24− , RoboservantException(..), FuzzState(..), FuzzOp(..), FailureType(..), Report(..)
19+ RoboservantException (..),
28−import Control.Exception.Lifted(throw,handle,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
27+import Control.Exception.Lifted
29−import Control.Monad(void,replicateM)
28+ ( Exception,
30−import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,runStateT)
37+import Control.Monad.State.Strict
31−import Control.Monad.Trans.Control(MonadBaseControl)
43+import Control.Monad.Trans.Control (MonadBaseControl)
32−import Data.Dynamic (Dynamic, dynApply, dynTypeRep, fromDynamic)
44+import qualified Data.Dependent.Map as DM
33−import Data.Map.Strict(Map)
45+import Data.Dynamic (Dynamic (..))
34−import qualified Data.Set as Set
46+import qualified Data.IntSet as IntSet
35−import Data.Maybe (mapMaybe)
47+import Data.List.NonEmpty (NonEmpty (..))
36−import Data.Typeable (TypeRep)
37−import Servant (Endpoints, Proxy (Proxy), Server, ServerError(..))
38−import System.Random(StdGen,randomR,mkStdGen)
39import qualified Data.List.NonEmpty as NEL
48import qualified Data.List.NonEmpty as NEL
49+import Data.Maybe (mapMaybe)
40−import qualified Data.Map.Strict as Map
50+import qualified Data.Set as Set
41−import Data.Time.Clock
51+import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
42−import Data.List(sortOn)
52+import qualified Data.Vinyl as V
43−
53+import qualified Data.Vinyl.Curry as V
44−import Roboservant.Types.Breakdown
54+import qualified Data.Vinyl.Functor as V
55+import GHC.Generics ((:*:) (..))
45import Roboservant.Types
56import Roboservant.Types
46 ( ApiOffset (..),
57 ( ApiOffset (..),
47− FlattenServer (..),
58+ Argument (..),
59+ InteractionError(..),
48−-- ReifiedApi,
61+ ReifiedApi,
49− ToReifiedApi (..),
62+ ReifiedEndpoint (..),
67+import Roboservant.Types.Config
69+import System.Random (Random (randomR), StdGen, mkStdGen)
70+import qualified Type.Reflection as R
52data RoboservantException
72data RoboservantException
53 = RoboservantException
73 = RoboservantException
54− { failureReason :: FailureType
74+ { failureReason :: FailureType,
55− , serverException :: Maybe SomeException
75+ serverException :: Maybe SomeException,
56− , fuzzState :: FuzzState
76+ fuzzState :: FuzzState
57− } deriving (Show)
78+ deriving (Show)
59instance Exception RoboservantException
80instance Exception RoboservantException
@@ -63,192 +84,228 @@ data FailureType
63 | CheckerFailed
84 | CheckerFailed
64 | NoPossibleMoves
85 | NoPossibleMoves
65 | InsufficientCoverage Double
86 | InsufficientCoverage Double
66− deriving (Show,Eq)
87+ deriving (Show, Eq)
69−data FuzzOp = FuzzOp
89+data FuzzOp
70− { apiOffset :: ApiOffset
91+ { apiOffset :: ApiOffset,
71− , provenance :: [Provenance]
92+ provenance :: [Provenance]
72− } deriving (Show,Eq)
94+ deriving (Show, Eq)
74−data Config
96+data FuzzState
77− , maxRuntime :: Integer -- seconds to test for
80− , coverageThreshold :: Double
83−data FuzzState = FuzzState
97+ = FuzzState
84− { path :: [FuzzOp]
98+ { path :: [FuzzOp],
85− , stash :: Stash
99+ stash :: Stash,
86− , currentRng :: StdGen
100+ currentRng :: StdGen
88 deriving (Show)
102 deriving (Show)
106+ (V.RecordToList as, V.RMap as) =>
108+ { eoCall :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))),
109+ eoArgs :: V.Rec (TypedF StashValue) as
90data StopReason
112data StopReason
91 = TimedOut
113 = TimedOut
92 | HitMaxIterations
114 | HitMaxIterations
93− deriving (Show,Eq)
115+ deriving (Show, Eq)
95−data Report = Report
117+data Report
96− { textual :: String
119+ { textual :: String,
97− , rsException :: RoboservantException}
120+ rsException :: RoboservantException
98− deriving (Show)
122+ deriving (Show)
100−fuzz :: forall api. (FlattenServer api, ToReifiedApi (Endpoints api))
124+
102− -> Config
126+-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
103− -> IO ()
127+-- fuzzClient = undefined
104− -> IO (Maybe Report)
134+ IO (Maybe Report)
105−fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
135+fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
106 let path = []
136 let path = []
107 stash = addToStash seed mempty
137 stash = addToStash seed mempty
108 currentRng = mkStdGen rngSeed
138 currentRng = mkStdGen rngSeed
111− deadline :: UTCTime <- addUTCTime (fromInteger $ maxRuntime * 1000000) <$> getCurrentTime
139+ deadline :: UTCTime <- addUTCTime (realToFrac $ maxRuntime * 1000000) <$> getCurrentTime
112− (stopreason, fs ) <- runStateT
140+ (stopreason, _fs) <-
113− (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get)) FuzzState{..}
142+ (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get))
144+ logInfo $ show stopreason
114 pure Nothing
145 pure Nothing
115− -- mapM_ (print . (\(offset, (args, dyn) ) -> (offset, map fst args, dyn))) reifiedApi
118 -- something less terrible later
147 -- something less terrible later
119 formatException :: RoboservantException -> Report
148 formatException :: RoboservantException -> Report
120− formatException r@(RoboservantException failureType exception _state) = Report
149+ formatException r@(RoboservantException failureType exception _state) =
121− (unlines [ show failureType , show exception ])
151+ (unlines [show failureType, show exception])
124− displayDiagnostics FuzzState{..} = liftIO $ do
153+ displayDiagnostics FuzzState {..} = liftIO $
125− putStrLn "api endpoints covered"
155+ ["api endpoints covered"]
126− mapM_ print (Set.toList $ Set.fromList $ map apiOffset path)
156+ <> (map show . Set.toList . Set.fromList $ map apiOffset path)
128− putStrLn "types in stash"
157+ <> ["", "types in stash"]
129− mapM_ print (map (\x -> (fst x, NEL.length (snd x))) . sortOn fst . Map.toList $ stash)
158+ <> DM.foldrWithKey (\_ v r -> (show . NEL.length . getStashValue $ v) : r) [] (getStash stash)
130−
159+ -- <> (map (show . NEL.length . getStashValue ) $ DM.assocs (getStash stash))
131− -- evaluateCoverage :: FuzzState -> m ()
160+ -- $ \_k v ->
161+ -- (show . NEL.length $ getStashValue v))
132− evaluateCoverage f@FuzzState{..}
163+ evaluateCoverage f@FuzzState {..}
133 | coverage > coverageThreshold = pure ()
164 | coverage > coverageThreshold = pure ()
134 | otherwise = do
165 | otherwise = do
135− displayDiagnostics f
166+ displayDiagnostics f
136− throw $ RoboservantException (InsufficientCoverage coverage) Nothing f
167+ throw $ RoboservantException (InsufficientCoverage coverage) Nothing f
137− where hitRoutes = (fromIntegral . Set.size . Set.fromList $ map apiOffset path)
169+ hitRoutes = fromIntegral . Set.size . Set.fromList $ map apiOffset path
138− totalRoutes = (fromIntegral routeCount)
170+ totalRoutes = fromIntegral routeCount
139− coverage = hitRoutes / totalRoutes
171+ coverage = hitRoutes / totalRoutes
142− untilDone :: MonadIO m => (Integer,UTCTime) -> m a -> m StopReason
172+ untilDone :: MonadIO m => (Integer, UTCTime) -> m a -> m StopReason
143− untilDone (0,_) _ = pure HitMaxIterations
173+ untilDone (0, _) _ = pure HitMaxIterations
144 untilDone (n, deadline) action = do
174 untilDone (n, deadline) action = do
145 now <- liftIO getCurrentTime
175 now <- liftIO getCurrentTime
146 if now > deadline
176 if now > deadline
147 then pure TimedOut
177 then pure TimedOut
149− action
179+ _ <- action
150− untilDone (n-1, deadline) action
180+ untilDone (n -1, deadline) action
152− reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
153 routeCount = length reifiedApi
182 routeCount = length reifiedApi
154−
183+ elementOrFail ::
155− elementOrFail :: (MonadState FuzzState m, MonadIO m)
184+ (MonadState FuzzState m, MonadIO m) =>
156− => [a] -> m a
186+ m a
157− elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing =<< get
187+ elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing =<< get
158 elementOrFail l = do
188 elementOrFail l = do
159 st <- get
189 st <- get
160− let (index,newGen) = randomR (0, length l - 1) (currentRng st)
190+ let (index, newGen) = randomR (0, length l - 1) (currentRng st)
161− modify' $ \st' -> st' { currentRng = newGen }
191+ modify' $ \st' -> st' {currentRng = newGen}
162 pure (l !! index)
192 pure (l !! index)
164− genOp :: (MonadState FuzzState m, MonadIO m)
194+ (MonadState FuzzState m, MonadIO m) =>
196+ (V.RecordToList as, V.RMap as) =>
165− => m (FuzzOp, Dynamic, [Dynamic])
197+ FuzzOp ->
166− genOp = do -- fs@FuzzState{..} = do
198+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
199+ V.Rec (TypedF V.Identity) as ->
203+ withOp callback = do
167 -- choose a call to make, from the endpoints with fillable arguments.
204 -- choose a call to make, from the endpoints with fillable arguments.
168− (offset, dynCall, args) <- elementOrFail . options =<< get
205+ (offset, EndpointOption {..}) <- elementOrFail . options =<< get
208+ ( \(tr :*: StashValue svs _) ->
169− r <- mapM (elementOrFail . zip [0..] . NEL.toList) args
209+ elementOrFail $
170− let pathSegment = FuzzOp offset (map (\(index,(_,dyn) ) -> Provenance (dynTypeRep dyn) index) r)
210+ zipWith
211+ (\i xy -> V.Const i :*: tr :*: xy)
219+ (\(V.Const index :*: tr :*: _) -> Provenance (R.SomeTypeRep tr) index)
223+ (\(_ :*: tr :*: (_, x)) -> tr :*: V.Identity x)
171− modify' (\f -> f { path = path f <> [pathSegment] })
225+ modify' (\f -> f {path = path f <> [pathSegment]})
172− pure (pathSegment, dynCall, fmap (snd . snd) r)
226+ callback pathSegment eoCall argValues
175− options :: FuzzState -> [(ApiOffset, Dynamic, [NEL.NonEmpty ([Provenance], Dynamic)])]
228+ options :: FuzzState -> [(ApiOffset, EndpointOption)]
176− options FuzzState{..} =
229+ options FuzzState {..} =
178− ( \(offset, (argreps, dynCall)) -> (offset,dynCall,) <$> do
231+ ( \(offset, ReifiedEndpoint {..}) -> do
179− mapM (\(_tr,bf) -> bf stash ) argreps
232+ args <- V.rtraverse (\(tr :*: Argument bf) -> (tr :*:) <$> bf stash) reArguments
233+ pure (offset, EndpointOption reEndpointFunc args)
181 reifiedApi
235 reifiedApi
183− execute :: (MonadState FuzzState m, MonadIO m)
237+ (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
184− => (FuzzOp,Dynamic,[Dynamic]) -> m ()
238+ FuzzOp ->
185− execute (fuzzop, dyncall, args) = do
239+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
186− -- liftIO $ print fuzzop
240+ V.Rec (TypedF V.Identity) as ->
187− -- now, magic happens: we apply some dynamic arguments to a dynamic
241+ m ()
188− -- function and hopefully something useful pops out the end.
242+ execute fuzzop func args = do
189− let func = foldr (\arg curr -> flip dynApply arg =<< curr) (Just dyncall) (reverse args)
243+ (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
190− st <- get
244+ liftIO (V.runcurry' func argVals) >>= \case
191− let showable = unlines $ ("args":map (show . dynTypeRep) args)
245+ Left (e::InteractionError) ->
192− <> ["fuzzop"
246+ if fatalError e
193− , show fuzzop
247+ then throw e
194− ,"dyncall"
248+ else pure ()
195− ,show (dynTypeRep dyncall)
198− -- liftIO $ putStrLn showable
201− Nothing -> error ("all screwed up 1: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
204− case fromDynamic f' of
205− Nothing -> error ("all screwed up 2: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
206− Just (f) -> liftIO f >>= \case
207− -- parameterise this
208− Left (serverError :: ServerError) ->
209− case errHTTPCode serverError of
210− 500 -> throw serverError
212− liftIO $ print ("ignoring non-500 error" , serverError)
214− Right (dyn :: NEL.NonEmpty Dynamic) -> do
249+ Right (dyn :: NEL.NonEmpty (Dynamic, Int)) ->
215− -- liftIO $ print ("storing", fmap dynTypeRep dyn)
250+ modify'
216− modify' (\fs@FuzzState{..} ->
251+ ( \fs@FuzzState {..} ->
217− fs { stash = addToStash (NEL.toList dyn) stash } )
252+ fs {stash = addToStash (NEL.toList dyn) stash}
255+ argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
256+ -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
220−-- go :: (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m)
258+ (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
222− go = do
260+ go = withOp $ \op func args -> do
224− catches (execute op)
261+ catches
262+ (execute op func args)
225− [ Handler (\(e :: SomeAsyncException) -> throw e)
263+ [ Handler (\(e :: SomeAsyncException) -> throw e),
226− , Handler (\(e :: SomeException) -> do
265+ ( \(e :: SomeException) ->
227− displayDiagnostics =<< get
228− throw . RoboservantException ServerCrashed (Just e) =<< get)
266+ throw . RoboservantException ServerCrashed (Just e) =<< get
230− catch (liftIO checker)
269+ catch
270+ (liftIO healthCheck)
231− (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
271+ (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
236− -- (Range.linear 1 100)
238− -- (fmap preload seed <> [callEndpoint reifiedApi])
239− -- executeSequential emptyState actions
240−addToStash :: [Dynamic]
273+addToStash ::
241− -> Map TypeRep (NEL.NonEmpty ([Provenance], Dynamic))
274+ [(Dynamic, Int)] ->
242− -> Map TypeRep (NEL.NonEmpty ([Provenance], Dynamic))
275+ Stash ->
243addToStash result stash =
277addToStash result stash =
244− foldr (\dyn dict -> let tr = dynTypeRep dyn in
278+ foldr
245− Map.insertWith renumber tr (pure ([Provenance tr 0],dyn)) dict) stash result
279+ ( \(Dynamic tr x, hashed) (Stash dict) ->
246−-- Map.insertWith (flip (<>)) (dynTypeRep result) (_pure result) stash })
280+ Stash $
284+ (StashValue (([Provenance (R.SomeTypeRep tr) 0], x) :| []) (IntSet.singleton hashed))
248− renumber :: NEL.NonEmpty ([Provenance],Dynamic)
290+ renumber ::
249− -> NEL.NonEmpty ([Provenance],Dynamic)
291+ StashValue a ->
250− -> NEL.NonEmpty ([Provenance],Dynamic)
292+ StashValue a ->
294+ renumber (StashValue singleDyn singleHash) orig@(StashValue l intSet)
295+ | not $ IntSet.null (singleHash `IntSet.intersection` intSet) = orig
251− renumber singleDyn l = case NEL.toList singleDyn of
298+ ( case NEL.toList singleDyn of
252− [([Provenance tr _], dyn)] -> l
299+ [([Provenance tr _], dyn)] ->
253− <> pure ([Provenance tr (length (NEL.last l) + 1)], dyn)
300+ l <> pure ([Provenance tr (length (NEL.last l) + 1)], dyn)
254− _ -> error "should be impossible"
301+ _ -> error "should be impossible"
303+ (IntSet.union singleHash intSet)
305+-- why isn't this in vinyl?
307+ (V.RecordToList as, V.RMap as) =>
308+ (forall x. f x -> a) ->
311+recordToList' f = V.recordToList . V.rmap (V.Const . f)