@@ -1,229 +1,311 @@
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
17+ -- TODO come up with something smarter than exporting all this, we should
18+ -- have some nice error-display functions
19+ RoboservantException (..),
24−import Control.Monad.Trans.Control
27+import Control.Exception.Lifted
25−import Control.Monad.State.Strict
28+ ( Exception,
26−import Control.Exception.Lifted(throw,Exception,SomeException,catch)
29+ Handler (Handler),
27−import System.Random
30+ SomeAsyncException,
28−import System.Timeout.Lifted
31+ SomeException,
29−import Control.Monad (guard)
32+ catch,
30−import Control.Monad.IO.Class (MonadIO, liftIO)
33+ catches,
31−import Data.Dynamic (Dynamic, dynApply, dynTypeRep, fromDynamic)
34+ handle,
32−import qualified Data.List.NonEmpty as NEL
35+ throw,
33−import qualified Data.Map.Strict as Map
34−import Data.Map.Strict(Map)
35−import Data.Maybe (mapMaybe)
36−import Data.Typeable (TypeRep)
50−import qualified Hedgehog.Gen as Gen
37+import Control.Monad.State.Strict
51−import qualified Hedgehog.Range as Range
38+ ( MonadIO (..),
52−import Roboservant.Hedgehog (elementOrFail)
39+ MonadState (get),
53−import Roboservant.Types
40+ StateT (runStateT),
54− ( ApiOffset (..),
41+ modify',
62−import Servant (Endpoints, Proxy (Proxy), Server, ServerError)
43+import Control.Monad.Trans.Control (MonadBaseControl)
63−import Type.Reflection (SomeTypeRep)
44+import qualified Data.Dependent.Map as DM
64−
45+import Data.Dynamic (Dynamic (..))
65−-- prop_concurrent,
46+import qualified Data.IntSet as IntSet
66−import Control.Monad (guard)
47+import Data.List.NonEmpty (NonEmpty (..))
67−import Control.Monad.IO.Class (MonadIO, liftIO)
68−import Data.Dynamic (Dynamic, dynApply, dynTypeRep, fromDynamic)
69import qualified Data.List.NonEmpty as NEL
48import qualified Data.List.NonEmpty as NEL
70−import qualified Data.Map.Strict as Map
71import Data.Maybe (mapMaybe)
49import Data.Maybe (mapMaybe)
72−import Data.Typeable (TypeRep)
50+import qualified Data.Set as Set
51+import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
73−import qualified Hedgehog.Gen as Gen
52+import qualified Data.Vinyl as V
74−import qualified Hedgehog.Range as Range
53+import qualified Data.Vinyl.Curry as V
75−import Roboservant.Hedgehog (elementOrFail)
54+import qualified Data.Vinyl.Functor as V
55+import GHC.Generics ((:*:) (..))
76import Roboservant.Types
56import Roboservant.Types
77 ( ApiOffset (..),
57 ( ApiOffset (..),
78− FlattenServer (..),
58+ Argument (..),
79− Op (..),
59+ InteractionError(..),
80 ReifiedApi,
61 ReifiedApi,
81− State (..),
62+ ReifiedEndpoint (..),
82− ToReifiedApi (..),
63+ Stash (..),
83− emptyState,
64+ StashValue (..),
85−import Servant (Endpoints, Proxy (Proxy), Server, ServerError)
67+import Roboservant.Types.Config
69+import System.Random (Random (randomR), StdGen, mkStdGen)
86−import Type.Reflection (SomeTypeRep)
70+import qualified Type.Reflection as R
88data RoboservantException
72data RoboservantException
89− = RoboservantException FailureType SomeException FuzzOp FuzzState
73+ = RoboservantException
74+ { failureReason :: FailureType,
75+ serverException :: Maybe SomeException,
76+ fuzzState :: FuzzState
90 deriving (Show)
78 deriving (Show)
91−-- we believe in nussink, lebowski
79+
92instance Exception RoboservantException
80instance Exception RoboservantException
94data FailureType
82data FailureType
95 = ServerCrashed
83 = ServerCrashed
96 | CheckerFailed
84 | CheckerFailed
97 | NoPossibleMoves
85 | NoPossibleMoves
98− deriving (Show,Eq)
86+ | InsufficientCoverage Double
99−
87+ deriving (Show, Eq)
100−-- update :: Callback Op (Opaque (NEL.NonEmpty Dynamic)) State
88+
101−-- update = Update $ \s@State {..} op o' ->
89+data FuzzOp
102−-- case op of
90+ = FuzzOp
103−-- Chewable offset tr v -> case _extract offset trs chewable of
91+ { apiOffset :: ApiOffset,
104−-- Nothing -> error "internal error"
92+ provenance :: [Provenance]
105−-- Just (tr, _extracted, rest) ->
93+ }
108−-- -- so, what's going on here... we need to be able to grab and merge
109−-- -- a list of (typeref, var). this means that we need to be able to pull
110−-- -- out typereps symbolically too, we can't just rely on the dynamic being there.
111−-- -- Map.insertWith (<>) (dynTypeRep v) _ (pure o') stateRefs,
112−-- Map.insertWith (<>) tr (_ o') stateRefs,
115−-- (Op (ApiOffset _offset) _args) ->
117−-- { chewable = _ o' : chewable
126−data Reference = Reference
129− } deriving (Show,Eq)
131−data FuzzOp = FuzzOp ApiOffset [Reference]
132− deriving (Show,Eq)
94+ deriving (Show, Eq)
134−data Config
96+data FuzzState
135− = Config
97+ = FuzzState
136− { seed :: [Dynamic]
98+ { path :: [FuzzOp],
137− , maxRuntime :: Int -- seconds to test for
99+ stash :: Stash,
139− , currentRng :: StdGen
100+ currentRng :: StdGen
142−data FuzzState = FuzzState
144− , stash :: Map TypeRep (NEL.NonEmpty (Reference,Dynamic))
146 deriving (Show)
102 deriving (Show)
148−fuzz :: forall api. (FlattenServer api, ToReifiedApi (Endpoints api))
104+data EndpointOption
149− => Server api
105+ = forall as.
150− -> Config
106+ (V.RecordToList as, V.RMap as) =>
151− -> IO ()
107+ EndpointOption
152− -> IO ()
108+ { eoCall :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))),
153−fuzz server Config{..} checker =
109+ eoArgs :: V.Rec (TypedF StashValue) as
154− -- either we time out without finding an error, which is fine, or we find an error
110+ }
155− -- and throw an exception that propagates through this.
156− void $ timeout (maxRuntime * 1000000) ( execStateT (forever go) FuzzState{..})
158− where
112+data StopReason
160− reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
117+data Report
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))
162− elementOrFail :: MonadState FuzzState m
163+ evaluateCoverage f@FuzzState {..}
163− => [a] -> m a
164+ | coverage > coverageThreshold = pure ()
164− elementOrFail = undefined
165+ | otherwise = do
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
166− genOp :: MonadState FuzzState m
184+ (MonadState FuzzState m, MonadIO m) =>
167− => m FuzzOp
185+ [a] ->
168− genOp = do -- fs@FuzzState{..} = do
186+ m a
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
169 -- choose a call to make, from the endpoints with fillable arguments.
204 -- choose a call to make, from the endpoints with fillable arguments.
170− (offset, args) <- elementOrFail . options =<< get
205+ (offset, EndpointOption {..}) <- elementOrFail . options =<< get
171− -- choose a (symbolic) argument from each. we drop the Dynamic here because it's
206+ r <-
172− -- not showable, which is no good for tests - though possibly it would make sense
207+ V.rtraverse
173− -- to use the constrained-dynamic stuff here? (or even just stash the string form)
208+ ( \(tr :*: StashValue svs _) ->
174− r <- (FuzzOp offset) <$> mapM (elementOrFail . fmap fst . NEL.toList) args
209+ elementOrFail $
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)
175− modify' (\f -> f { path = r:path f })
225+ modify' (\f -> f {path = path f <> [pathSegment]})
176− pure r
226+ callback pathSegment eoCall argValues
179−-- chooseOne opts = do
181−-- (offset,) <$> mapM elementOrFail args
183− options :: FuzzState -> [(ApiOffset, [NEL.NonEmpty (Reference, Dynamic)])]
228+ options :: FuzzState -> [(ApiOffset, EndpointOption)]
184− options FuzzState{..} =
229+ options FuzzState {..} =
186− ( \(offset, (argreps, _dynCall)) -> (offset,) <$> do
231+ ( \(offset, ReifiedEndpoint {..}) -> do
187− mapM (flip Map.lookup stash) argreps
232+ args <- V.rtraverse (\(tr :*: Argument bf) -> (tr :*:) <$> bf stash) reArguments
233+ pure (offset, EndpointOption reEndpointFunc args)
189 reifiedApi
235 reifiedApi
190− execute :: MonadState FuzzState m
237+ (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
191− => FuzzOp -> m ()
238+ FuzzOp ->
192− execute = undefined
239+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
193−
240+ V.Rec (TypedF V.Identity) as ->
195−
242+ execute fuzzop func args = do
196− -- execute (Op (ApiOffset offset) args) = do
243+ (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
197− -- fmap Opaque . liftIO $ do
244+ liftIO (V.runcurry' func argVals) >>= \case
198− -- let realArgs = map opaque args
245+ Left (e::InteractionError) ->
199− -- let (_offset, (_staticArgs, endpoint)) = staticRoutes !! offset
246+ if fatalError e
200− -- -- now, magic happens: we apply some dynamic arguments to a dynamic
247+ then throw e
201− -- -- function and hopefully something useful pops out the end.
248+ else pure ()
202− -- func = foldr (\arg curr -> flip dynApply arg =<< curr) (Just endpoint) realArgs
249+ Right (dyn :: NEL.NonEmpty (Dynamic, Int)) ->
203− -- let showable = "blah" -- map dynTypeRep (_endpoint : realArgs)
250+ modify'
204− -- case func of
251+ ( \fs@FuzzState {..} ->
205− -- Nothing -> error ("all screwed up: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
252+ fs {stash = addToStash (NEL.toList dyn) stash}
206− -- Just (f') -> do
253+ )
207− -- case fromDynamic f' of
254+ where
208− -- Nothing -> error ("all screwed up: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
255+ argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
209− -- Just f -> liftIO f >>= \case
256+ -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
210− -- Left (serverError :: ServerError) -> error (show serverError)
257+ go ::
211− -- Right (_typeRep :: SomeTypeRep, (dyn :: NEL.NonEmpty Dynamic)) -> pure dyn
214− go :: (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m)
258+ (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
216− go = do
260+ go = withOp $ \op func args -> do
217− fuzzOp <- genOp
261+ catches
218− catch (execute fuzzOp)
262+ (execute op func args)
263+ [ Handler (\(e :: SomeAsyncException) -> throw e),
265+ ( \(e :: SomeException) ->
219− (\(e :: SomeException) -> throw . RoboservantException ServerCrashed e fuzzOp =<< get)
266+ throw . RoboservantException ServerCrashed (Just e) =<< get
220− catch (liftIO checker)
269+ catch
270+ (liftIO healthCheck)
221− (\(e :: SomeException) -> throw . RoboservantException CheckerFailed e fuzzOp =<< get)
271+ (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
223− -- actions <-
273+addToStash ::
224− -- forAll $ do
274+ [(Dynamic, Int)] ->
225− -- Gen.sequential
275+ Stash ->
226− -- (Range.linear 1 100)
276+ Stash
227− -- emptyState
277+addToStash result stash =
228− -- (fmap preload seed <> [callEndpoint reifiedApi])
278+ foldr
229− -- executeSequential emptyState actions
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)