@@ -1,311 +0,0 @@
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)