on master help sign in
Merge pull request #10 from mwotton/coverage

some coverage code

commitf72ca223610e6e5b0ea702ecebd811864f1af2c2 authorMark Wotton <mwotton@gmail.com> dateTue 17 Nov 2020, 15:31 (6 years ago) parentc9076d73aec8c1
6 files changed+97−33
package.yaml+1 roboservant.cabal+3−1 src/Roboservant/Direct.hs+68−20 src/Roboservant/Types/ReifiedApi.hs+4−2 test/Spec.hs+17−8 test/Valid.hs+4−2
package.yaml+1
@@ -27,6 +27,7 @@ dependencies:
2727- servant-flatten
2828- servant-server # >= 0.17
2929- string-conversions
30+- time
3031
3132ghc-options:
3233- -Wall
roboservant.cabal+3−1
@@ -4,7 +4,7 @@ cabal-version: 1.12
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: 8359b70b81d2fdfc7001a1f607745da953f6e8f7b8188a54857d658406c238a2
7+-- hash: 838fa22fc3ea6673432f485653caaca6c43bccce724777fafbb903dad8ce30cd
88
99name: roboservant
1010version: 0.1.0.2
@@ -53,6 +53,7 @@ library
5353 , servant-flatten
5454 , servant-server
5555 , string-conversions
56+ , time
5657 default-language: Haskell2010
5758
5859test-suite roboservant-test
@@ -85,4 +86,5 @@ test-suite roboservant-test
8586 , servant-flatten
8687 , servant-server
8788 , string-conversions
89+ , time
8890 default-language: Haskell2010
src/Roboservant/Direct.hs+68−20
@@ -18,15 +18,20 @@
1818
1919module Roboservant.Direct
2020 ( fuzz, Config(..)
21+
22+ -- TODO come up with something smarter than exporting all this, we should
23+ -- have some nice error-display functions
24+ , RoboservantException(..), FuzzState(..), FuzzOp(..)
2125 )
2226where
2327
24−import Control.Exception.Lifted(throw,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
28+import Control.Exception.Lifted(throw,handle,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
2529import Control.Monad(void,replicateM)
26−import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,execStateT)
30+import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,runStateT)
2731import Control.Monad.Trans.Control(MonadBaseControl)
2832import Data.Dynamic (Dynamic, dynApply, dynTypeRep, fromDynamic)
2933import Data.Map.Strict(Map)
34+import qualified Data.Set as Set
3035import Data.Maybe (mapMaybe)
3136import Data.Typeable (TypeRep)
3237import Servant (Endpoints, Proxy (Proxy), Server, ServerError(..))
@@ -34,6 +39,7 @@ import System.Random(StdGen,randomR,mkStdGen)
3439import System.Timeout.Lifted(timeout)
3540import qualified Data.List.NonEmpty as NEL
3641import qualified Data.Map.Strict as Map
42+import Data.Time.Clock
3743
3844import Roboservant.Types.Breakdown
3945import Roboservant.Types
@@ -44,7 +50,7 @@ import Roboservant.Types
4450 )
4551
4652data RoboservantException
47− = RoboservantException FailureType (Maybe SomeException) FuzzState
53+ = RoboservantException FailureType (Maybe SomeException) Int FuzzState
4854 deriving (Show)
4955-- we believe in nussink, lebowski
5056instance Exception RoboservantException
@@ -53,18 +59,22 @@ data FailureType
5359 = ServerCrashed
5460 | CheckerFailed
5561 | NoPossibleMoves
62+ | InsufficientCoverage
5663 deriving (Show,Eq)
5764
5865
59−data FuzzOp = FuzzOp ApiOffset [Provenance]
60− deriving (Show,Eq)
66+data FuzzOp = FuzzOp
67+ { apiOffset :: ApiOffset
68+ , provenance :: [Provenance]
69+ } deriving (Show,Eq)
6170
6271data Config
6372 = Config
6473 { seed :: [Dynamic]
65− , maxRuntime :: Int -- seconds to test for
66− , maxReps :: Int
74+ , maxRuntime :: Integer -- seconds to test for
75+ , maxReps :: Integer
6776 , rngSeed :: Int
77+ , coverageThreshold :: Double
6878 }
6979
7080data FuzzState = FuzzState
@@ -74,28 +84,66 @@ data FuzzState = FuzzState
7484 }
7585 deriving (Show)
7686
87+data StopReason
88+ = TimedOut
89+ | HitMaxIterations
90+ deriving (Show,Eq)
91+
92+data Report = Report
93+ { textual :: String }
94+ deriving (Show,Eq)
95+
7796fuzz :: forall api. (FlattenServer api, ToReifiedApi (Endpoints api))
7897 => Server api
7998 -> Config
8099 -> IO ()
81− -> IO ()
82−fuzz server Config{..} checker = do
100+ -> IO (Maybe Report)
101+fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
83102 let path = []
84103 stash = addToStash seed mempty
85104 currentRng = mkStdGen rngSeed
105+
106+
107+ deadline :: UTCTime <- addUTCTime (fromInteger $ maxRuntime * 1000000) <$> getCurrentTime
86108 -- either we time out without finding an error, which is fine, or we find an error
87109 -- and throw an exception that propagates through this.
88−
89− void $ timeout (maxRuntime * 1000000) ( execStateT (replicateM maxReps go) FuzzState{..})
90− mapM_ (print . (\(offset, (args, dyn) ) -> (offset, map fst args, dyn))) reifiedApi
110+
111+ (stopreason, fs ) <- runStateT
112+ (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get)) FuzzState{..}
113+ pure Nothing
114+ -- mapM_ (print . (\(offset, (args, dyn) ) -> (offset, map fst args, dyn))) reifiedApi
91115
92116 where
93−
117+ -- something less terrible later
118+ formatException :: RoboservantException -> Report
119+ formatException = Report . show
120+
121+ -- evaluateCoverage :: FuzzState -> m ()
122+ evaluateCoverage f@FuzzState{..}
123+ | hitRoutes / totalRoutes > coverageThreshold = do
124+ liftIO $ print ("passed coverage", hitRoutes, totalRoutes)
125+ pure ()
126+ | otherwise = throw $ RoboservantException InsufficientCoverage Nothing routeCount f
127+ where hitRoutes = (fromIntegral . Set.size . Set.fromList $ map apiOffset path)
128+ totalRoutes = (fromIntegral routeCount)
129+
130+
131+ untilDone :: MonadIO m => (Integer,UTCTime) -> m a -> m StopReason
132+ untilDone (0,_) _ = pure HitMaxIterations
133+ untilDone (n, deadline) action = do
134+ now <- liftIO getCurrentTime
135+ if now > deadline
136+ then pure TimedOut
137+ else do
138+ action
139+ untilDone (n-1, deadline) action
140+
94141 reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
95−
142+ routeCount = length reifiedApi
143+
96144 elementOrFail :: (MonadState FuzzState m, MonadIO m)
97145 => [a] -> m a
98− elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing =<< get
146+ elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing routeCount =<< get
99147 elementOrFail l = do
100148 st <- get
101149 let (index,newGen) = randomR (0, length l - 1) (currentRng st)
@@ -124,7 +172,7 @@ fuzz server Config{..} checker = do
124172 execute :: (MonadState FuzzState m, MonadIO m)
125173 => (FuzzOp,Dynamic,[Dynamic]) -> m ()
126174 execute (fuzzop, dyncall, args) = do
127− liftIO $ print fuzzop
175+ -- liftIO $ print fuzzop
128176 -- now, magic happens: we apply some dynamic arguments to a dynamic
129177 -- function and hopefully something useful pops out the end.
130178 let func = foldr (\arg curr -> flip dynApply arg =<< curr) (Just dyncall) (reverse args)
@@ -136,7 +184,7 @@ fuzz server Config{..} checker = do
136184 ,show (dynTypeRep dyncall)
137185 ,"state"
138186 ,show st]
139− liftIO $ putStrLn showable
187+ -- liftIO $ putStrLn showable
140188
141189 case func of
142190 Nothing -> error ("all screwed up 1: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
@@ -153,7 +201,7 @@ fuzz server Config{..} checker = do
153201 liftIO $ print ("ignoring non-500 error" , serverError)
154202
155203 Right (dyn :: NEL.NonEmpty Dynamic) -> do
156− liftIO $ print ("storing", fmap dynTypeRep dyn)
204+ -- liftIO $ print ("storing", fmap dynTypeRep dyn)
157205 modify' (\fs@FuzzState{..} ->
158206 fs { stash = addToStash (NEL.toList dyn) stash } )
159207 pure ()
@@ -164,10 +212,10 @@ fuzz server Config{..} checker = do
164212 op <- genOp
165213 catches (execute op)
166214 [ Handler (\(e :: SomeAsyncException) -> throw e)
167− , Handler (\(e :: SomeException) -> throw . RoboservantException ServerCrashed (Just e) =<< get)
215+ , Handler (\(e :: SomeException) -> throw . RoboservantException ServerCrashed (Just e) routeCount =<< get)
168216 ]
169217 catch (liftIO checker)
170− (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
218+ (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) routeCount =<< get)
171219
172220 -- actions <-
173221 -- forAll $ do
src/Roboservant/Types/ReifiedApi.hs+4−2
@@ -30,10 +30,12 @@ import Data.List.NonEmpty (NonEmpty)
3030
3131
3232newtype ApiOffset = ApiOffset Int
33− deriving (Eq, Show)
33+ deriving (Eq, Show, Ord)
3434 deriving newtype (Enum, Num)
3535
36−type ReifiedEndpoint = ([(TypeRep, Stash -> Maybe (NonEmpty ([Provenance], Dynamic)))], Dynamic)
36+type ReifiedEndpoint = ([(TypeRep
37+ , Stash -> Maybe (NonEmpty ([Provenance], Dynamic)))]
38+ , Dynamic)
3739
3840type ReifiedApi = [(ApiOffset, ReifiedEndpoint)]
3941
test/Spec.hs+17−8
@@ -13,7 +13,8 @@ import qualified Roboservant as RS
1313
1414import Test.Hspec
1515import Test.Hspec.Core.Spec(shouldFail)
16−
16+import Data.Void
17+import Data.Maybe
1718
1819main :: IO ()
1920main = hspec spec
@@ -21,23 +22,29 @@ main = hspec spec
2122noCheck :: IO ()
2223noCheck = pure ()
2324
25+defaultConfig :: RS.Config
26+defaultConfig = RS.Config [] 1 1000 1 0
27+
2428spec :: Spec
2529spec = do
2630 describe "Basic usage" $ do
2731 describe "noError" $ do
2832 it "finds no error in a valid app" $ do
29− RS.fuzz @Valid.Api Valid.server (RS.Config [toDyn $ Seeded.Seed 1] 1 1000 1) noCheck
33+ RS.fuzz @Valid.Api Valid.server defaultConfig noCheck `shouldReturn` Nothing
34+ it "does fail coverage check" $ do
35+ r <- RS.fuzz @Valid.Api Valid.server defaultConfig { RS.coverageThreshold = 0.6 } noCheck
36+ r `shouldSatisfy` isJust
3037 describe "seeded" $ do
3138 shouldFail $
3239 it "finds an error using information passed in" $
33− RS.fuzz @Seeded.Api Seeded.server (RS.Config [toDyn $ Seeded.Seed 1] 5 1000 1) noCheck
34−
40+ RS.fuzz @Seeded.Api Seeded.server (defaultConfig{ RS.seed = [toDyn $ Seeded.Seed 1] }) noCheck
41+ `shouldReturn` Nothing
3542 shouldFail $ it "finds an error in a basic app" $
36− RS.fuzz @Foo.Api Foo.server (RS.Config [] 5 1000 1) noCheck
37−
43+ RS.fuzz @Foo.Api Foo.server defaultConfig noCheck
44+ `shouldReturn` Nothing
3845 shouldFail $ it "should find a failure that's dependent on using header info" $ do
39− RS.fuzz @Headers.Api Headers.server (RS.Config [] 5 10000 1) noCheck
40−
46+ RS.fuzz @Headers.Api Headers.server defaultConfig noCheck
47+ `shouldReturn` Nothing
4148 -- -- -- The UnsafeIO checker does not actually really use the contextually aware stuff, though it
4249 -- -- -- could: it's mostly here to show how to test for concurrency problems.
4350 -- describe "concurrency bugs" $ do
@@ -65,3 +72,5 @@ instance RS.BuildFrom Foo.Foo
6572instance RS.BuildFrom Headers.Foo
6673
6774instance RS.BuildFrom Seeded.Seed
75+
76+instance RS.BuildFrom Void
test/Valid.hs+4−2
@@ -8,8 +8,10 @@
88module Valid where
99
1010import Servant
11+import Data.Void
1112
1213type Api = Get '[JSON] Int
13−
14+ :<|> Capture "void" Void :> Get '[JSON] ()
15+
1416server :: Server Api
15−server = pure 7
17+server = pure 7 :<|> const (pure ())