@@ -2,33 +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+
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
1013
1114module Roboservant.Direct
12− ( fuzz,
15+ ( fuzz',
1316 Config (..),
1417 -- TODO come up with something smarter than exporting all this, we should
1518 -- have some nice error-display functions
1619 RoboservantException (..),
1720 FuzzState (..),
1821 FuzzOp (..),
22+ FailureType (..),
23+ Report (..),
1924 )
2025where
2126
22−import Control.Exception.Lifted (Exception, Handler (..), SomeAsyncException, SomeException, catch, catches, handle, throw)
23−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+ )
2443import Control.Monad.Trans.Control (MonadBaseControl)
2544import qualified Data.Dependent.Map as DM
26−import Data.Dynamic (Dynamic (..), dynTypeRep)
45+import Data.Dynamic (Dynamic (..))
46+import qualified Data.IntSet as IntSet
2747import Data.List.NonEmpty (NonEmpty (..))
2848import qualified Data.List.NonEmpty as NEL
2949import Data.Maybe (mapMaybe)
3050import qualified Data.Set as Set
31−import Data.Time.Clock
51+import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
3252import qualified Data.Vinyl as V
3353import qualified Data.Vinyl.Curry as V
3454import qualified Data.Vinyl.Functor as V
@@ -36,30 +56,34 @@ import GHC.Generics ((:*:) (..))
3656import Roboservant.Types
3757 ( ApiOffset (..),
3858 Argument (..),
39− FlattenServer (..),
59+ InteractionError(..),
4060 Provenance (..),
61+ ReifiedApi,
4162 ReifiedEndpoint (..),
4263 Stash (..),
4364 StashValue (..),
44− ToReifiedApi (..),
4565 TypedF,
4666 )
47−import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
48−import System.Random (StdGen, mkStdGen, randomR)
67+import Roboservant.Types.Config
68+
69+import System.Random (Random (randomR), StdGen, mkStdGen)
4970import qualified Type.Reflection as R
5071
5172data RoboservantException
52− = RoboservantException FailureType (Maybe SomeException) Int FuzzState
73+ = RoboservantException
74+ { failureReason :: FailureType,
75+ serverException :: Maybe SomeException,
76+ fuzzState :: FuzzState
77+ }
5378 deriving (Show)
5479
55−-- we believe in nussink, lebowski
5680instance Exception RoboservantException
5781
5882data FailureType
5983 = ServerCrashed
6084 | CheckerFailed
6185 | NoPossibleMoves
62− | InsufficientCoverage
86+ | InsufficientCoverage Double
6387 deriving (Show, Eq)
6488
6589data FuzzOp
@@ -69,15 +93,6 @@ data FuzzOp
6993 }
7094 deriving (Show, Eq)
7195
72−data Config
73− = Config
74− { seed :: [Dynamic],
75− maxRuntime :: Integer, -- seconds to test for
76− maxReps :: Integer,
77− rngSeed :: Int,
78− coverageThreshold :: Double
79− }
80−
8196data FuzzState
8297 = FuzzState
8398 { path :: [FuzzOp],
@@ -90,7 +105,7 @@ data EndpointOption
90105 = forall as.
91106 (V.RecordToList as, V.RMap as) =>
92107 EndpointOption
93− { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty Dynamic))),
108+ { eoCall :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))),
94109 eoArgs :: V.Rec (TypedF StashValue) as
95110 }
96111
@@ -101,40 +116,59 @@ data StopReason
101116
102117data Report
103118 = Report
104− {textual :: String}
105− deriving (Show, Eq)
119+ { textual :: String,
120+ rsException :: RoboservantException
121+ }
122+ deriving (Show)
123+
106124
107−fuzz ::
108− forall api.
109− (FlattenServer api, ToReifiedApi (Endpoints api)) =>
110− Server api ->
125+
126+-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
127+-- fuzzClient = undefined
128+
129+
130+
131+fuzz' ::
132+ ReifiedApi ->
111133 Config ->
112− IO () ->
113134 IO (Maybe Report)
114−fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
135+fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
115136 let path = []
116137 stash = addToStash seed mempty
117138 currentRng = mkStdGen rngSeed
118− deadline :: UTCTime <- addUTCTime (fromInteger $ maxRuntime * 1000000) <$> getCurrentTime
139+ deadline :: UTCTime <- addUTCTime (realToFrac $ maxRuntime * 1000000) <$> getCurrentTime
119140 (stopreason, _fs) <-
120141 runStateT
121142 (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get))
122143 FuzzState {..}
123− print stopreason
144+ logInfo $ show stopreason
124145 pure Nothing
125146 where
126147 -- something less terrible later
127148 formatException :: RoboservantException -> Report
128− formatException = Report . show
129− -- evaluateCoverage :: FuzzState -> m ()
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))
162+
130163 evaluateCoverage f@FuzzState {..}
131− | hitRoutes / totalRoutes > coverageThreshold = do
132− liftIO $ print ("passed coverage", hitRoutes, totalRoutes)
133− pure ()
134− | otherwise = throw $ RoboservantException InsufficientCoverage Nothing routeCount f
164+ | coverage > coverageThreshold = pure ()
165+ | otherwise = do
166+ displayDiagnostics f
167+ throw $ RoboservantException (InsufficientCoverage coverage) Nothing f
135168 where
136− hitRoutes = (fromIntegral . Set.size . Set.fromList $ map apiOffset path)
137− totalRoutes = (fromIntegral routeCount)
169+ hitRoutes = fromIntegral . Set.size . Set.fromList $ map apiOffset path
170+ totalRoutes = fromIntegral routeCount
171+ coverage = hitRoutes / totalRoutes
138172 untilDone :: MonadIO m => (Integer, UTCTime) -> m a -> m StopReason
139173 untilDone (0, _) _ = pure HitMaxIterations
140174 untilDone (n, deadline) action = do
@@ -144,13 +178,13 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
144178 else do
145179 _ <- action
146180 untilDone (n -1, deadline) action
147− reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
181+
148182 routeCount = length reifiedApi
149183 elementOrFail ::
150184 (MonadState FuzzState m, MonadIO m) =>
151185 [a] ->
152186 m a
153− elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing routeCount =<< get
187+ elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing =<< get
154188 elementOrFail l = do
155189 st <- get
156190 let (index, newGen) = randomR (0, length l - 1) (currentRng st)
@@ -161,7 +195,7 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
161195 ( forall as.
162196 (V.RecordToList as, V.RMap as) =>
163197 FuzzOp ->
164− V.Curried as (IO (Either ServerError (NonEmpty Dynamic))) ->
198+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
165199 V.Rec (TypedF V.Identity) as ->
166200 m r
167201 ) ->
@@ -171,7 +205,7 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
171205 (offset, EndpointOption {..}) <- elementOrFail . options =<< get
172206 r <-
173207 V.rtraverse
174− ( \(tr :*: StashValue svs) ->
208+ ( \(tr :*: StashValue svs _) ->
175209 elementOrFail $
176210 zipWith
177211 (\i xy -> V.Const i :*: tr :*: xy)
@@ -202,39 +236,24 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
202236 execute ::
203237 (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
204238 FuzzOp ->
205− V.Curried as (IO (Either ServerError (NonEmpty Dynamic))) ->
239+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
206240 V.Rec (TypedF V.Identity) as ->
207241 m ()
208242 execute fuzzop func args = do
209− liftIO $ print fuzzop
210− st <- get
211− let showable =
212− unlines $
213− ("args" : map show argTypes)
214− <> [ "fuzzop",
215− show fuzzop,
216− -- ,"dyncall"
217− -- ,show (dynTypeRep dyncall)
218− "state",
219− show st
220− ]
221− liftIO $ putStrLn showable
243+ (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
222244 liftIO (V.runcurry' func argVals) >>= \case
223− -- parameterise this
224− Left (serverError :: ServerError) ->
225− case errHTTPCode serverError of
226− 500 -> throw serverError
227− _ -> do
228− liftIO $ print ("ignoring non-500 error", serverError)
229− Right (dyn :: NEL.NonEmpty Dynamic) -> do
230− liftIO $ print ("storing", fmap dynTypeRep dyn)
245+ Left (e::InteractionError) ->
246+ if fatalError e
247+ then throw e
248+ else pure ()
249+ Right (dyn :: NEL.NonEmpty (Dynamic, Int)) ->
231250 modify'
232− ( \fs@FuzzState {..} ->
233− fs {stash = addToStash (NEL.toList dyn) stash}
234− )
251+ ( \fs@FuzzState {..} ->
252+ fs {stash = addToStash (NEL.toList dyn) stash}
253+ )
235254 where
236255 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
237− argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
256+ -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
238257 go ::
239258 (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
240259 m ()
@@ -242,21 +261,28 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
242261 catches
243262 (execute op func args)
244263 [ Handler (\(e :: SomeAsyncException) -> throw e),
245− Handler (\(e :: SomeException) -> throw . RoboservantException ServerCrashed (Just e) routeCount =<< get)
264+ Handler
265+ ( \(e :: SomeException) ->
266+ throw . RoboservantException ServerCrashed (Just e) =<< get
267+ )
246268 ]
247269 catch
248− (liftIO checker)
249− (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) routeCount =<< get)
270+ (liftIO healthCheck)
271+ (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
250272
251273addToStash ::
252− [Dynamic] ->
274+ [(Dynamic, Int)] ->
253275 Stash ->
254276 Stash
255277addToStash result stash =
256278 foldr
257− ( \(Dynamic tr x) (Stash dict) ->
279+ ( \(Dynamic tr x, hashed) (Stash dict) ->
258280 Stash $
259− DM.insertWith renumber tr (StashValue (([Provenance (R.SomeTypeRep tr) 0], x) :| [])) dict
281+ DM.insertWith
282+ renumber
283+ tr
284+ (StashValue (([Provenance (R.SomeTypeRep tr) 0], x) :| []) (IntSet.singleton hashed))
285+ dict
260286 )
261287 stash
262288 result
@@ -265,11 +291,16 @@ addToStash result stash =
265291 StashValue a ->
266292 StashValue a ->
267293 StashValue a
268− renumber (StashValue singleDyn) (StashValue l) = StashValue $ case NEL.toList singleDyn of
269− [([Provenance tr _], dyn)] ->
270− l
271− <> pure ([Provenance tr (length (NEL.last l) + 1)], dyn)
272− _ -> error "should be impossible"
294+ renumber (StashValue singleDyn singleHash) orig@(StashValue l intSet)
295+ | not $ IntSet.null (singleHash `IntSet.intersection` intSet) = orig
296+ | otherwise =
297+ StashValue
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+ )
303+ (IntSet.union singleHash intSet)
273304
274305-- why isn't this in vinyl?
275306recordToList' ::