@@ -0,0 +1,311 @@
1+{-# LANGUAGE AllowAmbiguousTypes #-}
2+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE LambdaCase #-}
5+{-# LANGUAGE PolyKinds #-}
6+{-# LANGUAGE RankNTypes #-}
7+{-# LANGUAGE RecordWildCards #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TupleSections #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
14+module Roboservant.Direct
17+ -- TODO come up with something smarter than exporting all this, we should
18+ -- have some nice error-display functions
19+ RoboservantException (..),
27+import Control.Exception.Lifted
37+import Control.Monad.State.Strict
43+import Control.Monad.Trans.Control (MonadBaseControl)
44+import qualified Data.Dependent.Map as DM
45+import Data.Dynamic (Dynamic (..))
46+import qualified Data.IntSet as IntSet
47+import Data.List.NonEmpty (NonEmpty (..))
48+import qualified Data.List.NonEmpty as NEL
49+import Data.Maybe (mapMaybe)
50+import qualified Data.Set as Set
51+import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
52+import qualified Data.Vinyl as V
53+import qualified Data.Vinyl.Curry as V
54+import qualified Data.Vinyl.Functor as V
55+import GHC.Generics ((:*:) (..))
56+import Roboservant.Types
59+ InteractionError(..),
62+ ReifiedEndpoint (..),
67+import Roboservant.Types.Config
69+import System.Random (Random (randomR), StdGen, mkStdGen)
70+import qualified Type.Reflection as R
72+data RoboservantException
73+ = RoboservantException
74+ { failureReason :: FailureType,
75+ serverException :: Maybe SomeException,
76+ fuzzState :: FuzzState
80+instance Exception RoboservantException
86+ | InsufficientCoverage Double
91+ { apiOffset :: ApiOffset,
92+ provenance :: [Provenance]
100+ currentRng :: StdGen
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
119+ { textual :: String,
120+ rsException :: RoboservantException
126+-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
127+-- fuzzClient = undefined
135+fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
137+ stash = addToStash seed mempty
138+ currentRng = mkStdGen rngSeed
139+ deadline :: UTCTime <- addUTCTime (realToFrac $ maxRuntime * 1000000) <$> getCurrentTime
140+ (stopreason, _fs) <-
142+ (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get))
144+ logInfo $ show stopreason
147+ -- something less terrible later
148+ formatException :: RoboservantException -> Report
149+ formatException r@(RoboservantException failureType exception _state) =
151+ (unlines [show failureType, show exception])
153+ displayDiagnostics FuzzState {..} = liftIO $
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))
161+ -- (show . NEL.length $ getStashValue v))
163+ evaluateCoverage f@FuzzState {..}
164+ | coverage > coverageThreshold = pure ()
166+ displayDiagnostics f
167+ throw $ RoboservantException (InsufficientCoverage coverage) Nothing f
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
174+ untilDone (n, deadline) action = do
175+ now <- liftIO getCurrentTime
180+ untilDone (n -1, deadline) action
182+ routeCount = length reifiedApi
184+ (MonadState FuzzState m, MonadIO m) =>
187+ elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing =<< get
188+ elementOrFail l = do
190+ let (index, newGen) = randomR (0, length l - 1) (currentRng st)
191+ modify' $ \st' -> st' {currentRng = newGen}
194+ (MonadState FuzzState m, MonadIO m) =>
196+ (V.RecordToList as, V.RMap as) =>
198+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
199+ V.Rec (TypedF V.Identity) as ->
203+ withOp callback = do
204+ -- choose a call to make, from the endpoints with fillable arguments.
205+ (offset, EndpointOption {..}) <- elementOrFail . options =<< get
208+ ( \(tr :*: StashValue svs _) ->
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)
225+ modify' (\f -> f {path = path f <> [pathSegment]})
226+ callback pathSegment eoCall argValues
228+ options :: FuzzState -> [(ApiOffset, EndpointOption)]
229+ options FuzzState {..} =
231+ ( \(offset, ReifiedEndpoint {..}) -> do
232+ args <- V.rtraverse (\(tr :*: Argument bf) -> (tr :*:) <$> bf stash) reArguments
233+ pure (offset, EndpointOption reEndpointFunc args)
237+ (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
239+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
240+ V.Rec (TypedF V.Identity) as ->
242+ execute fuzzop func args = do
243+ (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
244+ liftIO (V.runcurry' func argVals) >>= \case
245+ Left (e::InteractionError) ->
249+ Right (dyn :: NEL.NonEmpty (Dynamic, Int)) ->
251+ ( \fs@FuzzState {..} ->
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
258+ (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
260+ go = withOp $ \op func args -> do
262+ (execute op func args)
263+ [ Handler (\(e :: SomeAsyncException) -> throw e),
265+ ( \(e :: SomeException) ->
266+ throw . RoboservantException ServerCrashed (Just e) =<< get
270+ (liftIO healthCheck)
271+ (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
277+addToStash result stash =
279+ ( \(Dynamic tr x, hashed) (Stash dict) ->
284+ (StashValue (([Provenance (R.SomeTypeRep tr) 0], x) :| []) (IntSet.singleton hashed))
294+ renumber (StashValue singleDyn singleHash) orig@(StashValue l intSet)
295+ | not $ IntSet.null (singleHash `IntSet.intersection` intSet) = orig
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"
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)