rewrite echidna to work better

remotes/origin/dev-2-chidna-2-furious
JP Smith 6 years ago
parent d183c3a483
commit 468def9b81
  1. 404
      lib/Echidna/ABI.hs
  2. 90
      lib/Echidna/Campaign.hs
  3. 106
      lib/Echidna/Config.hs
  4. 106
      lib/Echidna/Coverage.hs
  5. 237
      lib/Echidna/Exec.hs
  6. 219
      lib/Echidna/Internal/JsonRunner.hs
  7. 925
      lib/Echidna/Internal/Runner.hs
  8. 15
      lib/Echidna/Property.hs
  9. 164
      lib/Echidna/Solidity.hs
  10. 46
      lib/Echidna/Test.hs
  11. 94
      lib/Echidna/Transaction.hs
  12. 121
      lib/Echidna/UI.hs
  13. 38
      package.yaml
  14. 6
      solidity/cli.sol
  15. 101
      src/Main.hs
  16. 6
      stack.yaml

@ -1,238 +1,182 @@
{-# LANGUAGE ConstraintKinds, FlexibleContexts, LambdaCase, RankNTypes, TupleSections, TypeFamilies #-}
module Echidna.ABI (
SolCall
, SolSignature
, encodeAbiCall
, encodeSig
, displayAbiCall
, genAbiAddress
, genAbiArray
, genAbiArrayDynamic
, genAbiBool
, genAbiBytes
, genAbiBytesDynamic
, genAbiCall
, genAbiInt
, genInteractions
, genAbiString
, genAbiType
, genAbiUInt
, genAbiValue
, mutateCall
, mutateCallSeq
, mutateValue
, prettyPrint
) where
import Control.Lens ((<&>), (&), view)
import Control.Monad (join, liftM2, replicateM)
import Control.Monad.Reader (MonadReader)
import Data.Bool (bool)
import Data.DoubleWord (Word128(..), Word160(..))
import Data.Monoid ((<>))
import Data.ByteString (ByteString)
import Data.Text (Text, unpack)
import Data.Vector (Vector, generateM)
import Hedgehog.Internal.Gen (MonadGen)
import GHC.Exts (IsList(..), Item)
import Hedgehog.Range (exponential, exponentialFrom, constant, singleton, Range)
import Numeric (showHex)
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
module Echidna.ABI where
import Control.Applicative ((<**>))
import Control.Monad.Catch (Exception, MonadThrow(..))
import Control.Monad.Reader.Class (MonadReader, asks)
import Control.Monad.Random.Strict
import Data.Bits (Bits(..))
import Data.Bool (bool)
import Data.ByteString (ByteString)
import Data.Has (Has(..))
import Data.Hashable (Hashable)
import Data.HashMap.Strict (HashMap)
import Data.List (group, sort)
import Data.Maybe (listToMaybe, mapMaybe)
import Data.Text (Text)
import Data.Vector (Vector)
import Data.Word8 (Word8)
import EVM.ABI (AbiType(..), AbiValue(..), abiValueType)
import qualified Data.ByteString as BS
import qualified Data.List as L
import qualified Data.Text as T
import qualified Hedgehog.Gen as Gen
import qualified Data.HashMap.Strict as M
import qualified Data.Vector as V
import Echidna.Config (Config, addrList)
-- Quick helper
import EVM.ABI
import EVM.Types (Addr(..))
newtype ElemException = ElemException String
type SolCall = (Text, [AbiValue])
instance Show ElemException where
show (ElemException s) = "Exception: tried to get element of " ++ s ++ ", but it was empty!"
instance Exception ElemException
rElem :: (MonadThrow m, MonadRandom m) => String -> [a] -> m a
rElem s [] = throwM $ ElemException s
rElem _ l = (l !!) <$> getRandomR (0, length l - 1)
-- Types
-- Don't construct this directly! Use mkConf
type SolCall = (Text, [AbiValue])
type SolSignature = (Text, [AbiType])
prettyPrint :: AbiValue -> String
prettyPrint (AbiUInt _ n) = show n
prettyPrint (AbiInt _ n) = show n
prettyPrint (AbiAddress n) = showHex n ""
prettyPrint (AbiBool b) = bool "true" "false" b
prettyPrint (AbiBytes _ b) = show b
prettyPrint (AbiBytesDynamic b) = show b
prettyPrint (AbiString s) = show s
prettyPrint (AbiArrayDynamic _ v) =
"[" ++ L.intercalate ", " (prettyPrint <$> toList v) ++ "]"
prettyPrint (AbiArray _ _ v) =
"[" ++ L.intercalate ", " (prettyPrint <$> toList v) ++ "]"
encodeSig :: Text -> [AbiType] -> Text
encodeSig n ts = n <> "(" <> T.intercalate "," (map abiTypeSolidity ts) <> ")"
genSize :: MonadGen m => m Int
genSize = (8 *) <$> Gen.enum 1 32
genAbiAddress :: (MonadGen m, MonadReader Config m) => m AbiValue
genAbiAddress = view addrList >>= \case (Just xs) -> fmap (AbiAddress . addressWord160) (Gen.element xs)
Nothing -> let w64 = Gen.word64 $ constant minBound maxBound in
fmap AbiAddress . liftM2 Word160 Gen.enumBounded
$ liftM2 Word128 w64 w64
genAbiUInt :: MonadGen m => Int -> m AbiValue
genAbiUInt n = AbiUInt n . fromInteger <$> genUInt
where genUInt = Gen.integral $ exponential 0 $ 2 ^ toInteger n - 1
genAbiInt :: MonadGen m => Int -> m AbiValue
genAbiInt n = AbiInt n . fromInteger <$> genInt
where genInt = Gen.integral $ exponentialFrom 0 (-1 * 2 ^ toInteger n) (2 ^ (toInteger n - 1))
genAbiBool :: MonadGen m => m AbiValue
genAbiBool = AbiBool <$> Gen.bool
genAbiBytes :: MonadGen m => Int -> m AbiValue
genAbiBytes = liftM2 fmap AbiBytes $ Gen.bytes . singleton
genAbiBytesDynamic :: MonadGen m => m AbiValue
genAbiBytesDynamic = AbiBytesDynamic <$> Gen.bytes (constant 1 256)
genAbiString :: MonadGen m => m AbiValue
genAbiString = let fromRange = fmap AbiString . Gen.utf8 (constant 1 256) in
Gen.choice $ fromRange <$> [Gen.ascii, Gen.digit, Gen.alpha, Gen.element ['a','b','c'], Gen.unicode]
genStaticAbiType :: MonadGen m => m AbiType
genStaticAbiType = go (16 :: Int) where
go n = Gen.choice $ [ AbiUIntType <$> genSize
, AbiIntType <$> genSize
, pure AbiAddressType
, pure AbiBoolType
, AbiBytesType <$> Gen.enum 1 32
] ++ [AbiArrayType <$> Gen.enum 0 256 <*> go (n - 1) | n > 0]
genAbiType :: MonadGen m => m AbiType
genAbiType = Gen.choice [ pure AbiBytesDynamicType
, pure AbiStringType
, AbiArrayDynamicType <$> genStaticAbiType
, genStaticAbiType
]
genVecOfType :: (MonadReader Config m, MonadGen m) => AbiType -> Range Int -> m (Vector AbiValue)
genVecOfType t r = do
s <- Gen.integral r
generateM s $ \_ -> case t of
AbiUIntType n -> genAbiUInt n
AbiIntType n -> genAbiInt n
AbiAddressType -> genAbiAddress
AbiBoolType -> genAbiBool
AbiBytesType n -> genAbiBytes n
AbiArrayType n t' -> genAbiArray n t'
_ -> error "Arrays must only contain statically sized types"
genAbiArrayDynamic :: (MonadReader Config m, MonadGen m) => AbiType -> m AbiValue
genAbiArrayDynamic t = AbiArrayDynamic t <$> genVecOfType t (constant 0 256)
genAbiArray :: (MonadReader Config m, MonadGen m) => Int -> AbiType -> m AbiValue
genAbiArray n t = AbiArray n t <$> genVecOfType t (singleton n)
genAbiValue :: (MonadReader Config m, MonadGen m) => m AbiValue
genAbiValue = Gen.choice [ genAbiUInt =<< genSize
, genAbiInt =<< genSize
, genAbiAddress
, genAbiBool
, genAbiBytes =<< Gen.enum 1 32
, genAbiBytesDynamic
, genAbiString
, genAbiArrayDynamic =<< genAbiType
, join $ liftM2 genAbiArray (Gen.enum 0 256) genAbiType
]
genAbiValueOfType :: (MonadReader Config m, MonadGen m) => AbiType -> m AbiValue
genAbiValueOfType t = case t of
AbiUIntType n -> genAbiUInt n
AbiIntType n -> genAbiInt n
AbiAddressType -> genAbiAddress
AbiBoolType -> genAbiBool
AbiBytesType n -> genAbiBytes n
AbiBytesDynamicType -> genAbiBytesDynamic
AbiStringType -> genAbiString
AbiArrayDynamicType t' -> genAbiArrayDynamic t'
AbiArrayType n t' -> genAbiArray n t'
genAbiCall :: (MonadReader Config m, MonadGen m) => SolSignature -> m SolCall
genAbiCall (s,ts) = (s,) <$> mapM genAbiValueOfType ts
encodeAbiCall :: SolCall -> ByteString
encodeAbiCall (t, vs) = abiCalldata t $ fromList vs
displayAbiCall :: SolCall -> String
displayAbiCall (t, vs) = unpack t ++ "(" ++ L.intercalate "," (map prettyPrint vs) ++ ")"
-- genInteractions generates a function call from a list of type signatures of
-- the form (Function name, [arg0 type, arg1 type...])
genInteractions :: (MonadReader Config m, MonadGen m) => [SolSignature] -> m SolCall
genInteractions ls = genAbiCall =<< Gen.element ls
type Listy t a = (IsList (t a), Item (t a) ~ a)
switchElem :: (Listy t a, MonadGen m) => m a -> t a -> m (t a)
switchElem g t = let l = toList t; n = length l in do
i <- Gen.element [0..n]
x <- g
return . fromList $ take i l <> [x] <> drop (i+1) l
changeChar :: MonadGen m => ByteString -> m ByteString
changeChar = fmap BS.pack . switchElem Gen.enumBounded . BS.unpack
addBS :: MonadGen m => ByteString -> m ByteString
addBS b = Gen.element [(<> b), (b <>)] <*> Gen.utf8 (constant 0 (256 - BS.length b)) Gen.unicode
dropBS :: MonadGen m => ByteString -> m ByteString
dropBS b = Gen.choice [ BS.drop <$> Gen.element [1..BS.length b] <*> pure b
, BS.take <$> Gen.element [0..BS.length b-1] <*> pure b
]
changeDynamicBS :: MonadGen m => ByteString -> m ByteString
changeDynamicBS b = Gen.choice $ [changeChar, addBS, dropBS] <&> ($ b)
changeNumber :: (Enum a, Integral a, MonadGen m) => a -> m a
changeNumber n = let x = fromIntegral n :: Integer in fromIntegral . (+ x) <$> Gen.element [-10..10]
changeList :: (Listy t a, MonadGen m) => m (t a) -> m a -> t a -> m (t a)
changeList g0 g1 x = case toList x of
[] -> g0
l -> Gen.choice [ Gen.element [(<> l), (l <>)] <*> fmap toList g0
, drop <$> Gen.element [1..length l] <*> pure l
, take <$> Gen.element [0..length l-1] <*> pure l
, switchElem g1 l
] <&> fromList
newOrMod :: MonadGen m => m AbiValue -> (a -> AbiValue) -> m a -> m AbiValue
newOrMod m f n = Gen.choice [m, f <$> n]
mutateValue :: (MonadReader Config m, MonadGen m) => AbiValue -> m AbiValue
mutateValue (AbiUInt s n) =
newOrMod (genAbiUInt s) (AbiUInt s) (changeNumber n)
mutateValue (AbiInt s n) =
newOrMod (genAbiInt s) (AbiInt s) (changeNumber n)
mutateValue (AbiAddress a) =
newOrMod genAbiAddress AbiAddress (changeNumber a)
mutateValue (AbiBool _) = genAbiBool
mutateValue (AbiBytes s b) =
newOrMod (genAbiBytes s) (AbiBytes s) (changeChar b)
mutateValue (AbiBytesDynamic b) =
newOrMod genAbiBytesDynamic AbiBytesDynamic (changeDynamicBS b)
mutateValue (AbiString b) =
newOrMod genAbiString AbiString (changeDynamicBS b)
mutateValue (AbiArrayDynamic t a) = let g0 = genVecOfType t (constant 0 (256 - length a)); g1 = genAbiValueOfType t in
newOrMod (genAbiArrayDynamic t) (AbiArrayDynamic t) (changeList g0 g1 a)
mutateValue (AbiArray s t a) =
newOrMod (genAbiArray s t) (AbiArray s t) (switchElem (genAbiValueOfType t) a)
changeOrId :: (Traversable t, MonadGen m) => (a -> m a) -> t a -> m (t a)
changeOrId f = mapM $ (Gen.element [f, pure] >>=) . (&)
mutateCall :: (MonadReader Config m, MonadGen m) => SolCall -> m SolCall
mutateCall (t, vs) = (t,) <$> changeOrId mutateValue vs
mutateCallSeq :: (MonadReader Config m, MonadGen m) => [SolSignature] -> [SolCall] -> m [SolCall]
mutateCallSeq s cs = let g = genInteractions s in
changeOrId mutateCall cs >>= changeList (Gen.element [1..10] >>= flip replicateM g) g
data GenConf = GenConf { pSynthA :: Float -- Fraction of time to use dictionary vs. synthesize
, constants :: HashMap AbiType [AbiValue] -- Constants to use, sorted by type
, wholeCalls :: HashMap SolSignature [SolCall] -- Whole calls to use, sorted by type
}
-- This instance is the only way for mkConf to work nicely, and is well-formed.
{-# ANN module ("HLint: ignore Unused LANGUAGE pragma" :: String) #-}
-- We need the above since hlint doesn't notice DeriveAnyClass in StandaloneDeriving.
deriving instance Hashable AbiType
mkConf :: Float -> [AbiValue] -> [SolCall] -> GenConf
mkConf p vs cs = GenConf p (tsOf id vs) (tsOf (fmap . fmap) cs) where
tsOf f = M.fromList . mapMaybe (liftM2 fmap (\l x -> (f abiValueType x, l)) listToMaybe) . group . sort
-- Generation (synthesis)
genAbiValue :: MonadRandom m => AbiType -> m AbiValue
genAbiValue (AbiUIntType n) = AbiUInt n . fromInteger <$> getRandomR (0, 2 ^ n - 1)
genAbiValue (AbiIntType n) = AbiInt n . fromInteger <$> getRandomR (-1 * 2 ^ n, 2 ^ (n - 1))
genAbiValue AbiAddressType = AbiAddress . fromInteger <$> getRandomR (0, 2 ^ (160 :: Integer) - 1)
genAbiValue AbiBoolType = AbiBool <$> getRandom
genAbiValue (AbiBytesType n) = AbiBytes n . BS.pack . take n <$> getRandoms
genAbiValue AbiBytesDynamicType = liftM2 (\n -> AbiBytesDynamic . BS.pack . take n) (getRandomR (1, 32)) getRandoms
genAbiValue AbiStringType = liftM2 (\n -> AbiString . BS.pack . take n) (getRandomR (1, 32)) getRandoms
genAbiValue (AbiArrayType n t) = AbiArray n t <$> V.replicateM n (genAbiValue t)
genAbiValue (AbiArrayDynamicType t) = fmap (AbiArrayDynamic t) $ getRandomR (1, 32) >>= flip V.replicateM (genAbiValue t)
genAbiCall :: MonadRandom m => SolSignature -> m SolCall
genAbiCall = traverse $ mapM genAbiValue
genInteractions :: (MonadThrow m, MonadRandom m) => [SolSignature] -> m SolCall
genInteractions l = genAbiCall =<< rElem "ABI" l
-- Mutation helper functions
mutateNum :: (Integral a, Num a, MonadRandom m) => a -> m a
mutateNum x = bool (+) (-) <$> getRandom <*> pure x <*> (fromIntegral <$> getRandomR (0 :: Integer, fromIntegral x))
addChars :: MonadRandom m => m Word8 -> ByteString -> m ByteString
addChars c b = foldM withR b . enumFromTo 0 =<< rand where
rand = getRandomR (0, BS.length b)
withR b' n = (\x -> BS.take n b' <> BS.singleton x <> BS.drop (n + 1) b') <$> c
addNulls :: MonadRandom m => ByteString -> m ByteString
addNulls = addChars $ pure 0
shrinkWith :: MonadRandom m => (Int -> a -> a) -> (Int -> a -> a) -> (a -> Int) -> a -> m a
shrinkWith f g l t = let rand = getRandomR (0, l t) in liftM2 (\x y -> f x $ g y t) rand rand
shrinkBS :: MonadRandom m => ByteString -> m ByteString
shrinkBS = shrinkWith BS.take BS.drop BS.length
shrinkL :: MonadRandom m => [a] -> m [a]
shrinkL = shrinkWith take drop length
shrinkV :: MonadRandom m => Vector a -> m (Vector a)
shrinkV = shrinkWith V.take V.drop V.length
growWith :: MonadRandom m => m b -> (b -> a -> a) -> (a -> b -> a) -> (a -> Int) -> a -> m a
growWith m f g l t = foldM withR t =<< flip replicateM m =<< rand where
rand = getRandomR (0, l t)
withR t' x = bool (f x t') (g t' x) <$> getRandom
mutateBS :: MonadRandom m => ByteString -> m ByteString
mutateBS b = addChars getRandom =<< changeSize where
changeSize = bool (shrinkBS b) (growWith getRandom BS.cons BS.snoc BS.length b) =<< getRandom
mutateV :: MonadRandom m => AbiType -> Vector AbiValue -> m (Vector AbiValue)
mutateV t v = mapM mutateAbiValue =<< changeSize where
changeSize = bool (shrinkV v) (growWith (genAbiValue t) V.cons V.snoc V.length v) =<< getRandom
-- Mutation
canShrinkAbiValue :: AbiValue -> Bool
canShrinkAbiValue (AbiUInt _ 0) = False
canShrinkAbiValue (AbiInt _ 0) = False
canShrinkAbiValue (AbiBool b) = b
canShrinkAbiValue (AbiBytes _ b) = BS.any (/= 0) b
canShrinkAbiValue (AbiBytesDynamic "") = True
canShrinkAbiValue (AbiString "") = True
canShrinkAbiValue (AbiArray _ _ l) = any canShrinkAbiValue l
canShrinkAbiValue (AbiArrayDynamic _ l) = l == mempty
canShrinkAbiValue _ = True
dropBits :: forall a m. (Bits a, Bounded a, Integral a, MonadRandom m) => a -> m a
dropBits x = (x .&.) . fromIntegral <$> getRandomR bounds where
bounds :: (Integer, Integer)
bounds = (fromIntegral (minBound :: a), fromIntegral (maxBound :: a))
shrinkAbiValue :: MonadRandom m => AbiValue -> m AbiValue
shrinkAbiValue (AbiUInt n m) = AbiUInt n <$> dropBits m
shrinkAbiValue (AbiInt n m) = AbiInt n <$> dropBits m
shrinkAbiValue x@AbiAddress{} = pure x
shrinkAbiValue (AbiBool _) = pure $ AbiBool False
shrinkAbiValue (AbiBytes n b) = AbiBytes n <$> addNulls b
shrinkAbiValue (AbiBytesDynamic b) = fmap AbiBytesDynamic $ addNulls =<< shrinkBS b
shrinkAbiValue (AbiString b) = fmap AbiString $ addNulls =<< shrinkBS b
shrinkAbiValue (AbiArray n t l) = AbiArray n t <$> mapM shrinkAbiValue l
shrinkAbiValue (AbiArrayDynamic t l) = fmap (AbiArrayDynamic t) $ mapM shrinkAbiValue =<< shrinkV l
shrinkAbiCall :: MonadRandom m => SolCall -> m SolCall
shrinkAbiCall = traverse $ mapM shrinkAbiValue
mutateAbiValue :: MonadRandom m => AbiValue -> m AbiValue
mutateAbiValue (AbiUInt n x) = AbiUInt n <$> mutateNum x
mutateAbiValue (AbiInt n x) = AbiInt n <$> mutateNum x
mutateAbiValue (AbiAddress _) = genAbiValue AbiAddressType
mutateAbiValue (AbiBool _) = genAbiValue AbiBoolType
mutateAbiValue (AbiBytes n b) = AbiBytes n <$> addChars getRandom b
mutateAbiValue (AbiBytesDynamic b) = AbiBytesDynamic <$> mutateBS b
mutateAbiValue (AbiString b) = AbiString <$> mutateBS b
mutateAbiValue (AbiArray n t l) = AbiArray n t <$> mapM mutateAbiValue l
mutateAbiValue (AbiArrayDynamic t l) = AbiArrayDynamic t <$> mutateV t l
mutateAbiCall :: MonadRandom m => SolCall -> m SolCall
mutateAbiCall = traverse $ mapM mutateAbiValue
-- Generation, with dictionary
genWithDict :: (Eq a, Hashable a, MonadReader x m, Has GenConf x, MonadRandom m, MonadThrow m)
=> (GenConf -> HashMap a [b]) -> (a -> m b) -> a -> m b
genWithDict f g t = asks getter >>= \c -> do
useD <- (pSynthA c <) <$> getRandom
g t <**> case (M.lookup t (f c), useD) of (Just l@(_:_), True) -> const <$> rElem "" l
_ -> pure id
genAbiValueM :: (MonadReader x m, Has GenConf x, MonadRandom m, MonadThrow m) => AbiType -> m AbiValue
genAbiValueM = genWithDict constants genAbiValue
genAbiCallM :: (MonadReader x m, Has GenConf x, MonadRandom m, MonadThrow m) => SolSignature -> m SolCall
genAbiCallM = genWithDict wholeCalls genAbiCall
genInteractionsM :: (MonadReader x m, Has GenConf x, MonadRandom m, MonadThrow m) => [SolSignature] -> m SolCall
genInteractionsM l = genAbiCallM =<< rElem "ABI" l

@ -0,0 +1,90 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}
module Echidna.Campaign where
import Control.Lens
import Control.Monad (liftM2, replicateM)
import Control.Monad.Catch (MonadCatch(..))
import Control.Monad.Random.Strict (MonadRandom)
import Control.Monad.Reader.Class (MonadReader)
import Control.Monad.State.Strict (MonadState(..), StateT, evalStateT, execStateT)
import Data.Bool (bool)
import Data.Map (Map)
import Data.Has (Has(..))
import Data.Set (Set)
import EVM
import EVM.Types (W256)
import Echidna.ABI
import Echidna.Exec
import Echidna.Test
import Echidna.Transaction
data CampaignConf = CampaignConf { testLimit :: Int
, seqLen :: Int
, shrinkLimit :: Int
, knownCoverage :: Maybe (Map W256 (Set Int))
}
data TestState = Open Int | Large Int [Tx] | Passed | Solved [Tx] | Failed ExecException deriving Show
data Campaign = Campaign { _tests :: [(SolTest, TestState)]
, _coverage :: Maybe (Map W256 (Set Int))
}
makeLenses ''Campaign
isDone :: (MonadReader x m, Has CampaignConf x) => Campaign -> m Bool
isDone (Campaign ts _) = view (hasLens . to (liftM2 (,) testLimit shrinkLimit)) <&> \(tl, sl) ->
all (\case Open i -> i >= tl; Large i _ -> i >= sl; _ -> True) $ snd <$> ts
updateTest :: ( MonadCatch m, MonadRandom m, MonadReader x m, Has TestConf x, Has CampaignConf x)
=> VM -> Maybe (VM, [Tx]) -> (SolTest, TestState) -> m (SolTest, TestState)
updateTest v (Just (v', xs)) (n, t) = view (hasLens . to testLimit) >>= \tl -> (n,) <$> case t of
Open i | i >= tl -> pure Passed
Open i -> catch (evalStateT (checkETest n) v' <&> bool (Large (-1) xs) (Open (i + 1)))
(pure . Failed)
_ -> snd <$> updateTest v Nothing (n,t)
updateTest v Nothing (n, t) = view (hasLens . to shrinkLimit) >>= \sl -> (n,) <$> case t of
Large i x | i >= sl -> pure $ Solved x
Large i x -> if any canShrinkTx x then Large (i + 1) <$> evalStateT (shrinkSeq n x) v
else pure $ Solved x
_ -> pure t
callseq :: ( MonadCatch m, MonadRandom m, MonadReader x m, MonadState y m
, Has GenConf x, Has TestConf x, Has CampaignConf x, Has Campaign y)
=> VM -> World -> Int -> m (Set Tx)
callseq v w ql = replicateM ql (evalStateT genTxM w) >>= \is -> use hasLens >>= \ca -> case ca ^. coverage of
Nothing -> execStateT (evalSeq v execTx is) (v, ca) >>= assign hasLens . view _2 >> return mempty
(Just co) -> do (_, co', ca', s) <- execStateT (evalSeq v execTxRecC is) (v, co, ca, mempty :: Set Tx)
hasLens .= (ca' & coverage ?~ co')
return s
campaign :: ( MonadCatch m, MonadRandom m, MonadReader x m, Has GenConf x, Has TestConf x, Has CampaignConf x)
=> StateT Campaign m a -> VM -> World -> [SolTest] -> m Campaign
campaign u v w ts = view (hasLens . to knownCoverage) >>= \c ->
execStateT runCampaign (Campaign ((,Open (-1)) <$> ts) c) where
step = runUpdate (updateTest v Nothing) >> u >> runCampaign
runCampaign = use (hasLens . tests . to (fmap snd)) >>= update
update c = view hasLens >>= \(CampaignConf tl q sl _) ->
if | any (\case Open n -> n < tl; _ -> False) c -> callseq v w q >> step
| any (\case Large n _ -> n < sl; _ -> False) c -> step
| otherwise -> u
evalSeq :: ( MonadCatch m, MonadRandom m, MonadReader x m, MonadState y m
, Has TestConf x, Has CampaignConf x, Has Campaign y, Has VM y)
=> VM -> (Tx -> m a) -> [Tx] -> m ()
evalSeq v e l = go [] l where
go r xs = use hasLens >>= \v' -> runUpdate (updateTest v $ Just (v',reverse r)) >>
case xs of [] -> pure ()
(y:ys) -> e y >> go (y:r) ys
runUpdate :: (MonadState x m, Has Campaign x) => ((SolTest, TestState) -> m (SolTest, TestState)) -> m ()
runUpdate f = use (hasLens . tests) >>= mapM f >>= (hasLens . tests .=)

@ -1,72 +1,74 @@
{-# LANGUAGE FlexibleContexts, TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
module Echidna.Config where
import Control.Monad.Catch (MonadThrow(..))
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Reader (ReaderT, runReaderT)
import Control.Lens
import Control.Exception (Exception)
import Control.Monad.Catch (MonadThrow)
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Reader (ReaderT(..))
import Data.Has (Has(..))
import Data.Aeson
import Data.Text (Text)
import Hedgehog (ShrinkLimit, TestLimit)
import Data.Maybe (fromMaybe)
import EVM (result)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString as BS
import qualified Data.Yaml as Y
import Echidna.Property (PropertyType(..))
import Echidna.Campaign
import Echidna.ABI
import Echidna.Solidity
import Echidna.Test
import Echidna.UI
import EVM.Types (Addr, W256)
data EConfig = EConfig { _cConf :: CampaignConf
, _gConf :: GenConf
, _nConf :: Names
, _sConf :: SolConf
, _tConf :: TestConf
}
makeLenses ''EConfig
data Config = Config
{ _solcArgs :: Maybe String
, _epochs :: Int
, _range :: Int
, _contractAddr :: Addr
, _sender :: Addr
, _addrList :: Maybe [Addr]
, _gasLimit :: W256
, _testLimit :: TestLimit
, _shrinkLimit :: ShrinkLimit
, _returnType :: PropertyType
, _prefix :: Text
, _printCoverage :: Bool
, _outputJson :: Bool
}
deriving Show
instance Has CampaignConf EConfig where
hasLens = cConf
makeLenses ''Config
instance Has GenConf EConfig where
hasLens = gConf
instance FromJSON Config where
parseJSON (Object v) =
let fromInt s n = ((v .:? s :: Y.Parser (Maybe Int)) <&> fmap fromIntegral) .!= n in
Config <$> v .:? "solcArgs" .!= Nothing
<*> v .:? "epochs" .!= 2
<*> v .:? "range" .!= 10
<*> v .:? "contractAddr" .!= 0x00a329c0648769a73afac7f9381e08fb43dbea72
<*> v .:? "sender" .!= 0x00a329c0648769a73afac7f9381e08fb43dbea70
<*> v .:? "addrList" .!= Nothing
<*> v .:? "gasLimit" .!= 0xffffffffffffffff
<*> fromInt "testLimit" 10000
<*> fromInt "shrinkLimit" 1000
<*> v .:? "returnType" .!= ShouldReturnTrue
<*> v .:? "prefix" .!= "echidna_"
<*> v .:? "printCoverage" .!= False
<*> v .:? "outputJson" .!= False
parseJSON _ = parseJSON (Object mempty)
instance Has Names EConfig where
hasLens = nConf
newtype ParseException = ParseException FilePath
instance Has SolConf EConfig where
hasLens = sConf
defaultConfig :: Config
defaultConfig = either (error "Config parser got messed up :(") id $ Y.decodeEither' ""
instance Has TestConf EConfig where
hasLens = tConf
instance Show ParseException where
show (ParseException f) = "Could not parse config file " ++ show f
instance FromJSON EConfig where
parseJSON (Object v) =
let tc = do reverts <- v .:? "reverts" .!= True
sender <- v .:? "sender" .!= 0x00a329c0648769a73afac7f9381e08fb43dbea70
let good = if reverts then (`elem` [ResTrue, ResRevert]) else (== ResTrue)
return $ TestConf (good . fromMaybe ResOther . fmap classifyRes . view result) (const sender) in
EConfig <$> (CampaignConf <$> v .:? "testLimit" .!= 10000
<*> v .:? "seqLen" .!= 10
<*> v .:? "shrinkLimit" .!= 5000
<*> pure Nothing)
<*> pure (GenConf 0 mempty mempty)
<*> pure (const $ const mempty)
<*> (SolConf <$> v .:? "contractAddr" .!= 0x00a329c0648769a73afac7f9381e08fb43dbea72
<*> v .:? "deployer" .!= 0x00a329c0648769a73afac7f9381e08fb43dbea70
<*> v .:? "prefix " .!= "echidna_"
<*> v .:? "solcArgs " .!= "")
<*> tc
parseJSON _ = parseJSON (Object mempty)
instance Exception ParseException
defaultConfig :: EConfig
defaultConfig = either (error "Config parser got messed up :(") id $ Y.decodeEither' ""
parseConfig :: (MonadThrow m, MonadIO m) => FilePath -> m Config
parseConfig :: (MonadThrow m, MonadIO m) => FilePath -> m EConfig
parseConfig f = liftIO (BS.readFile f) >>= Y.decodeThrow
withDefaultConfig :: ReaderT Config m a -> m a
withDefaultConfig :: ReaderT EConfig m a -> m a
withDefaultConfig = (`runReaderT` defaultConfig)

@ -1,106 +0,0 @@
{-# LANGUAGE BangPatterns, DeriveGeneric, FlexibleContexts, KindSignatures, LambdaCase, StrictData #-}
module Echidna.Coverage (
CoverageInfo
, CoverageRef
, CoverageReport(..)
, eCommandCoverage
, ePropertySeqCoverage
, execCallCoverage
, getCover
, getCoverageReport
, module Echidna.Internal.Runner
, module Echidna.Internal.JsonRunner
) where
import Control.DeepSeq (force)
import Control.Concurrent.MVar (MVar, modifyMVar_)
import Control.Lens ((&), use)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.State.Strict (MonadState, StateT, evalStateT, runState)
import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask)
import Data.Aeson (ToJSON(..), encode)
import Data.ByteString.Lazy.Char8 (unpack)
import Data.Foldable (Foldable(..), foldl')
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Data.Ord (comparing)
import Data.Set (Set, insert, size)
import Data.Vector (Vector, fromList)
import Data.Vector.Generic (maximumBy)
import GHC.Generics
import qualified Control.Monad.State.Strict as S
import Hedgehog
import Hedgehog.Gen (choice)
import EVM
import Echidna.ABI (SolCall, SolSignature, genInteractions, mutateCall)
import Echidna.Config (Config(..))
import Echidna.Internal.Runner
import Echidna.Internal.JsonRunner
import Echidna.Exec
-----------------------------------------
-- Coverage data types and printing
type CoverageInfo = (SolCall, Set Int)
type CoverageRef = IORef CoverageInfo
data CoverageReport = CoverageReport {coverage :: Int} deriving (Show,Generic)
instance ToJSON CoverageReport
getCoverageReport :: Set Int -> String
getCoverageReport = unpack . encode . toJSON . CoverageReport . size
-----------------------------------------
-- Set cover algo
getCover :: (Foldable t, Monoid (t b)) => [(a, t b)] -> [a]
getCover [] = []
getCover xs = setCover (fromList xs) mempty totalCoverage []
where totalCoverage = length $ foldl' (\acc -> mappend acc . snd) mempty xs
setCover :: (Foldable t, Monoid (t b)) => Vector (a, t b) -> t b -> Int -> [a] -> [a]
setCover vs cov tot calls = best : calls & if length new == tot then id
else setCover vs new tot where
(best, new) = mappend cov <$> maximumBy (comparing $ length . mappend cov . snd) vs
-----------------------------------------
-- Echidna exec with coverage
execCallCoverage :: (MonadState VM m, MonadReader CoverageRef m, MonadIO m) => SolCall -> m VMResult
execCallCoverage sol = execCallUsing (go mempty) sol where
go !c = use result >>= \case
Just x -> do ref <- ask
liftIO $ modifyIORef' ref (const (sol, c))
return x
_ -> do current <- use $ state . pc
S.state (runState exec1)
go . force $ insert current c
eCommandCoverage :: (MonadGen n, MonadTest m, MonadState VM m, MonadReader CoverageRef m, MonadIO m)
=> [SolCall] -> (VM -> Bool) -> [SolSignature] -> Config -> [Command n m VMState]
eCommandCoverage cov p ts conf = let useConf = flip runReaderT conf in case cov of
[] -> [eCommandUsing (useConf $ genInteractions ts) (\(Call c) -> execCallCoverage c) p]
xs -> map (\x -> eCommandUsing (choice $ useConf <$> [mutateCall x, genInteractions ts])
(\(Call c) -> execCallCoverage c) p) xs
ePropertySeqCoverage :: (MonadReader Config m)
=> [SolCall]
-> MVar [CoverageInfo]
-> (VM -> Bool)
-> [SolSignature]
-> VM
-> m Property
ePropertySeqCoverage calls cov p ts v = ask >>= \c -> ePropertyUsing (eCommandCoverage calls p ts c) writeCoverage v
where writeCoverage :: MonadIO m => ReaderT CoverageRef (StateT VM m) a -> m a
writeCoverage m = do
threadCovRef <- liftIO $ newIORef mempty
let s = runReaderT m threadCovRef
a <- evalStateT s v
threadCov <- liftIO $ readIORef threadCovRef
liftIO $ modifyMVar_ cov (\xs -> pure $ threadCov:xs)
return a

@ -1,146 +1,95 @@
{-# LANGUAGE BangPatterns, DeriveGeneric, FlexibleContexts, KindSignatures, LambdaCase, StrictData #-}
module Echidna.Exec (
VMState(..)
, VMAction(..)
, checkTest
, checkBoolExpTest
, checkRevertTest
, checkTrueOrRevertTest
, checkFalseOrRevertTest
, eCommand
, eCommandUsing
, ePropertySeq
, ePropertyUsing
, execCall
, execCallUsing
, module Echidna.Internal.Runner
, module Echidna.Internal.JsonRunner
) where
import Control.Lens ((&), (^.), (.=), (?~))
import Control.Monad.Catch (MonadCatch)
import Control.Monad.State.Strict (MonadState, evalState, execState, get, put)
import Control.Monad.Reader (MonadReader, runReaderT, ask)
import Data.List (intercalate)
import Data.Text (Text)
import Data.Typeable (Typeable)
import Data.Vector (fromList)
import Hedgehog
import Hedgehog.Gen (sequential)
import Hedgehog.Internal.State (Action(..))
import Hedgehog.Internal.Property (PropertyConfig(..), mapConfig)
import Hedgehog.Range (linear)
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
module Echidna.Exec where
import Control.Lens
import Control.Monad.Catch (Exception, MonadThrow(..))
import Control.Monad.State.Strict (MonadState, execState, get, put)
import Data.Either (isRight)
import Data.Has (Has(..))
import Data.Map.Strict (Map)
import Data.Maybe (fromMaybe)
import Data.Ord (comparing)
import Data.Set (Set)
import EVM
import EVM.ABI (AbiValue(..), abiCalldata, abiValueType, encodeAbiValue)
import EVM.Concrete (Blob(..))
import EVM.Exec (exec)
import Echidna.ABI (SolCall, SolSignature, displayAbiCall, encodeSig, genInteractions)
import Echidna.Config (Config(..), testLimit, range, shrinkLimit)
import Echidna.Internal.Runner
import Echidna.Internal.JsonRunner
import Echidna.Property (PropertyType(..))
-------------------------------------------------------------------
-- Fuzzing and Hedgehog Init
execCall :: MonadState VM m => SolCall -> m VMResult
execCall = execCallUsing exec
execCallUsing :: MonadState VM m => m VMResult -> SolCall -> m VMResult
execCallUsing m (t,vs) = do og <- get
cleanUp
state . calldata .= cd
m >>= \case x@VMFailure{} -> put (og & result ?~ x) >> return x
x@VMSuccess{} -> return x
where cd = B . abiCalldata (encodeSig t $ abiValueType <$> vs) $ fromList vs
cleanUp = sequence_ [result .= Nothing, state . pc .= 0, state . stack .= mempty]
checkTest :: PropertyType -> VM -> Text -> Bool
checkTest ShouldReturnTrue = checkBoolExpTest True
checkTest ShouldReturnFalse = checkBoolExpTest False
checkTest ShouldRevert = checkRevertTest
checkTest ShouldReturnFalseRevert = checkFalseOrRevertTest
checkBoolExpTest :: Bool -> VM -> Text -> Bool
checkBoolExpTest b v t = case evalState (execCall (t, [])) v of
VMSuccess (B s) -> s == encodeAbiValue (AbiBool b)
_ -> False
checkRevertTest :: VM -> Text -> Bool
checkRevertTest v t = case evalState (execCall (t, [])) v of
(VMFailure Revert) -> True
_ -> False
checkTrueOrRevertTest :: VM -> Text -> Bool
checkTrueOrRevertTest v t = case evalState (execCall (t, [])) v of
(VMSuccess (B s)) -> s == encodeAbiValue (AbiBool True)
(VMFailure Revert) -> True
_ -> False
checkFalseOrRevertTest :: VM -> Text -> Bool
checkFalseOrRevertTest v t = case evalState (execCall (t, [])) v of
(VMSuccess (B s)) -> s == encodeAbiValue (AbiBool False)
(VMFailure Revert) -> True
_ -> False
newtype VMState (v :: * -> *) =
VMState VM
instance Show (VMState v) where
show (VMState v) = "EVM state, current result: " ++ show (v ^. result)
newtype VMAction (v :: * -> *) =
Call SolCall
instance Show (VMAction v) where
show (Call c) = displayAbiCall c
instance HTraversable VMAction where
htraverse _ (Call b) = pure $ Call b
eCommandUsing :: (MonadGen n, MonadTest m, Typeable a)
=> n SolCall
-> (VMAction Concrete -> m a)
-> (VM -> Bool)
-> Command n m VMState
eCommandUsing gen ex p = Command (\_ -> pure $ Call <$> gen) ex
[ Ensure $ \_ (VMState v) _ _ -> assert $ p v
, Update $ \(VMState v) (Call c) _ -> VMState $ execState (execCall c) v
]
eCommand :: (MonadGen n, MonadTest m) => n SolCall -> (VM -> Bool) -> Command n m VMState
eCommand = flip eCommandUsing (\ _ -> pure ())
configProperty :: Config -> PropertyConfig -> PropertyConfig
configProperty config x = x { propertyTestLimit = config ^. testLimit
, propertyShrinkLimit = config ^. shrinkLimit
}
ePropertyUsing :: (MonadCatch m, MonadTest m, MonadReader Config n)
=> [Command Gen m VMState]
-> (m () -> PropertyT IO ())
-> VM
-> n Property
ePropertyUsing cs f v = do
config <- ask
return $ mapConfig (configProperty config) . property $
f . executeSequential (VMState v) =<< forAllWith printCallSeq
(sequential (linear 1 (config ^. range)) (VMState v) cs)
where printCallSeq = ("Call sequence: " ++) . intercalate "\n " .
map showCall . sequentialActions
showCall (Action i _ _ _ _ _) = show i ++ ";"
ePropertySeq :: (MonadReader Config m)
=> (VM -> Bool) -- Predicate to fuzz for violations of
-> [SolSignature] -- Type signatures to fuzz
-> VM -- Initial state
-> m Property
ePropertySeq p ts vm = ask >>= \c -> ePropertyUsing [eCommand (runReaderT (genInteractions ts) c) p] id vm
import EVM.Exec (exec)
import EVM.Types (W256(..))
import qualified Data.Map as M
import qualified Data.Set as S
import Echidna.Transaction
data ErrorClass = RevertE | IllegalE | UnknownE
classifyError :: Error -> ErrorClass
classifyError Revert = RevertE
classifyError (UnrecognizedOpcode _) = RevertE
classifyError StackUnderrun = IllegalE
classifyError BadJumpDestination = IllegalE
classifyError StackLimitExceeded = IllegalE
classifyError IllegalOverflow = IllegalE
classifyError _ = UnknownE
pattern Reversion :: VMResult
pattern Reversion <- VMFailure (classifyError -> RevertE)
pattern Illegal :: VMResult
pattern Illegal <- VMFailure (classifyError -> IllegalE)
data ExecException = IllegalExec Error | UnknownFailure Error
instance Show ExecException where
show (IllegalExec e) = "VM attempted an illegal operation: " ++ show e
show (UnknownFailure e) = "VM failed for unhandled reason, " ++ show e
++ ". This shouldn't happen. Please file a ticket with this error message and steps to reproduce!"
instance Exception ExecException
vmExcept :: MonadThrow m => Error -> m ()
vmExcept e = throwM $ case VMFailure e of {Illegal -> IllegalExec e; _ -> UnknownFailure e}
execTxWith :: (MonadState x m, Has VM x) => (Error -> m ()) -> m VMResult -> Tx -> m VMResult
execTxWith h m t = do og <- get
setupTx t
res <- m
case (res, isRight $ t ^. call) of
(Reversion, _) -> put og
(VMFailure x, _) -> h x
(VMSuccess (B bc), True) -> hasLens %= execState ( replaceCodeOfSelf bc
>> loadContract (t ^. dst))
_ -> pure ()
return res
execTx :: (MonadState x m, Has VM x, MonadThrow m) => Tx -> m VMResult
execTx = execTxWith vmExcept $ liftSH exec
pointCoverage :: (MonadState x m, Has (Map W256 (Set Int)) x, Has VM x) => m ()
pointCoverage = use hasLens >>= \v ->
hasLens %= M.insertWith (const . S.insert $ v ^. state . pc) (h v) mempty where
h v = fromMaybe (W256 maxBound) $ v ^? env . contracts . at (v ^. state . contract) . _Just . codehash
fastCoverage :: (MonadState x m, Has (Set Int) x, Has VM x) => m ()
fastCoverage = use hasLens >>= \v -> hasLens %= S.insert (v ^. state . pc)
usingCoverage :: (MonadState x m, Has VM x) => m () -> m VMResult
usingCoverage cov = maybe (cov >> liftSH exec1 >> usingCoverage cov) pure =<< use (hasLens . result)
execTxRecC :: (MonadState x m, Has VM x, Has (Map W256 (Set Int)) x, MonadThrow m) => Tx -> m VMResult
execTxRecC = execTxWith vmExcept (usingCoverage pointCoverage)
coveragePoints :: Map W256 (Set Int) -> Int
coveragePoints = sum . M.map S.size
execTxOptC :: (MonadState x m, Has VM x, Has (Map W256 (Set Int)) x, Has (Set Tx) x, MonadThrow m) => Tx -> m VMResult
execTxOptC t = let hint = id :: Map W256 (Set Int) -> Map W256 (Set Int) in do
og <- hasLens <<.= mempty
res <- execTxRecC t
new <- M.unionWith S.union og . hint <$> use hasLens
if comparing coveragePoints new og == GT then hasLens %= S.insert t else pure ()
return res

@ -1,219 +0,0 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
module Echidna.Internal.JsonRunner (
checkParallelJson
) where
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (ToJSON, encode)
import Data.ByteString.Lazy.Char8 (unpack)
import Data.Maybe (mapMaybe)
import GHC.Generics
import System.IO (hFlush, stdout)
import Hedgehog.Internal.Config
import Hedgehog.Internal.Gen (runDiscardEffect, runGenT)
import Hedgehog.Internal.Property
(Failure(..), Group(..), Property(..), PropertyT(..),
PropertyConfig(..), ShrinkLimit(..), ShrinkRetries(..),
Log(..), Diff(..), runTestT, unPropertyName)
import Hedgehog.Internal.Queue
import Hedgehog.Internal.Report
(FailedAnnotation(..), FailureReport(..), Result(..), ShrinkCount(..))
import Hedgehog.Internal.Runner (RunnerConfig(..))
import Hedgehog.Internal.Seed (Seed)
import qualified Hedgehog.Internal.Seed as Seed
import Hedgehog.Internal.Source
import Hedgehog.Internal.Tree (Node(..), Tree(..), runTree)
import Hedgehog.Range (Size)
data JsonOutput = JsonOutput {
propName :: !String
, propTrue :: !Bool
, propCall :: !(Maybe [String])
} deriving (Generic, Show)
instance ToJSON JsonOutput
checkParallelJson :: MonadIO m => Group -> m Bool
checkParallelJson =
checkGroup
RunnerConfig {
runnerWorkers =
Nothing
, runnerColor =
Nothing
, runnerVerbosity =
Nothing
}
checkGroup :: MonadIO m => RunnerConfig -> Group -> m Bool
checkGroup config (Group _ props) =
liftIO $ do
n <- resolveWorkers (runnerWorkers config)
updateNumCapabilities (n + 2)
_ <- runTasks n props st noop noop $ \(name, prop) -> do
result <- checkProp 0 prop
putStrLn $ unpack $ encode (format name result)
hFlush stdout
pure ()
pure True
where st _ _ (name,prop) = pure (name,prop)
noop = const $ pure ()
failVals (FailureReport _ _ _ xs _ _ _ _) = map (\(FailedAnnotation _ v) -> v) xs
format n r = let name = unPropertyName n in
case r of
OK -> JsonOutput { propName = name, propTrue = True, propCall = Nothing }
GaveUp -> JsonOutput { propName = name, propTrue = False, propCall = Nothing }
Failed s -> JsonOutput { propName = name, propTrue = False, propCall = Just (failVals s) }
checkProp :: Size -> Property -> IO Result
checkProp size0 (Property conf test) = Seed.random >>= loop (0 :: Integer) (0 :: Integer) size0
where loop !tests !discards !size !seed =
if size > 99 then
-- size has reached limit, reset to 0
loop tests discards 0 seed
else if tests >= fromIntegral (propertyTestLimit conf) then
-- we've hit the test limit, test was successful
pure $ OK
else if discards >= fromIntegral (propertyDiscardLimit conf) then
-- we've hit the discard limit, give up
pure $ GaveUp
else
case Seed.split seed of
(s0, s1) -> do
node@(Node x _) <-
runTree . runDiscardEffect $ runGenT size s0 . runTestT $ unPropertyT test
case x of
Nothing ->
loop tests (discards + 1) (size + 1) s1
Just (Left _, _) ->
takeSmallest
size
seed
0
(propertyShrinkLimit conf)
(propertyShrinkRetries conf)
node
Just (Right (), _) ->
loop (tests + 1) discards (size + 1) s1
takeSmallest ::
Size
-> Seed
-> ShrinkCount
-> ShrinkLimit
-> ShrinkRetries
-> Node IO (Maybe (Either Failure (), [Log]))
-> IO Result
takeSmallest size seed shrinks slimit retries = \case
Node Nothing _ ->
pure GaveUp
Node (Just (x, w)) xs ->
case x of
Left (Failure loc err mdiff) -> do
let failure = mkFailure size seed shrinks loc err mdiff (reverse w)
if shrinks >= fromIntegral slimit then
-- if we've hit the shrink limit, don't shrink any further
pure $ Failed failure
else
findM xs (Failed failure) $ \m -> do
o <- runTreeN retries m
if isFailure o then
Just <$> takeSmallest size seed (shrinks + 1) slimit retries o
else
return Nothing
Right () ->
return OK
isFailure :: Node m (Maybe (Either x a, b)) -> Bool
isFailure = \case
Node (Just (Left _, _)) _ ->
True
_ ->
False
isSuccess :: Node m (Maybe (Either x a, b)) -> Bool
isSuccess =
not . isFailure
runTreeN ::
Monad m
=> ShrinkRetries
-> Tree m (Maybe (Either x a, b))
-> m (Node m (Maybe (Either x a, b)))
runTreeN n m = do
o <- runTree m
if n > 0 && isSuccess o then
runTreeN (n - 1) m
else
pure o
findM :: Monad m => [a] -> b -> (a -> m (Maybe b)) -> m b
findM xs0 def p =
case xs0 of
[] ->
return def
x0 : xs ->
p x0 >>= \m ->
case m of
Nothing ->
findM xs def p
Just x ->
return x
takeAnnotation :: Log -> Maybe FailedAnnotation
takeAnnotation = \case
Annotation loc val ->
Just $ FailedAnnotation loc val
_ ->
Nothing
takeFootnote :: Log -> Maybe String
takeFootnote = \case
Footnote x ->
Just x
_ ->
Nothing
mkFailure ::
Size
-> Seed
-> ShrinkCount
-> Maybe Span
-> String
-> Maybe Diff
-> [Log]
-> FailureReport
mkFailure size seed shrinks location message diff logs =
let
inputs =
mapMaybe takeAnnotation logs
footnotes =
mapMaybe takeFootnote logs
in
FailureReport size seed shrinks inputs location message diff footnotes

@ -1,925 +0,0 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
module Echidna.Internal.Runner (
checkParallel
) where
import Control.Concurrent.STM (TVar, atomically)
import qualified Control.Concurrent.STM.TVar as TVar
import Control.DeepSeq (NFData)
import Control.Monad (zipWithM)
import Control.Monad.Catch
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Trans.Maybe (MaybeT(..))
import Data.Bifunctor (first, second)
import Data.DoubleWord(Word128,Word256)
import Data.Either (partitionEithers)
import qualified Data.List as List
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (mapMaybe, catMaybes)
import Data.Semigroup (Semigroup(..))
import Hedgehog.Internal.Config
import Hedgehog.Internal.Gen (runDiscardEffect, runGenT)
import Hedgehog.Internal.Property
(Failure(..), Group(..), GroupName(..), Property(..), PropertyT(..),
PropertyConfig(..), PropertyName(..), ShrinkLimit(..), ShrinkRetries(..),
Log(..), Diff(..), runTestT)
import Hedgehog.Internal.Queue
import Hedgehog.Internal.Region
import Hedgehog.Internal.Report
(DiscardCount(..), FailedAnnotation(..), FailureReport(..), PropertyCount(..),
Progress(..), Report(..), Result(..), ShrinkCount(..), TestCount(..))
import Hedgehog.Internal.Runner (RunnerConfig(..))
import Hedgehog.Internal.Seed (Seed)
import qualified Hedgehog.Internal.Seed as Seed
import Hedgehog.Internal.Show
import Hedgehog.Internal.Source
import Hedgehog.Internal.Tree (Node(..), Tree(..), runTree)
import Hedgehog.Range (Size)
import System.Console.ANSI (ColorIntensity(..), Color(..))
import System.Console.ANSI (ConsoleLayer(..), ConsoleIntensity(..))
import System.Console.ANSI (SGR(..), setSGRCode)
import Text.PrettyPrint.Annotated.WL (Doc, (<+>))
import qualified Text.PrettyPrint.Annotated.WL as WL
import EVM.Types (W256)
instance NFData Word128
instance NFData Word256
instance NFData W256
checkParallel :: MonadIO m => Group -> m Bool
checkParallel =
checkGroup
RunnerConfig {
runnerWorkers =
Nothing
, runnerColor =
Nothing
, runnerVerbosity =
Nothing
}
checkGroup :: MonadIO m => RunnerConfig -> Group -> m Bool
checkGroup config (Group group props) =
liftIO $ do
n <- resolveWorkers (runnerWorkers config)
-- ensure few spare capabilities for concurrent-output, it's likely that
-- our tests will saturate all the capabilities they're given.
updateNumCapabilities (n + 2)
putStrLn $ "━━━ " ++ unGroupName group ++ " ━━━"
verbosity <- resolveVerbosity (runnerVerbosity config)
summary <- checkGroupWith n verbosity (runnerColor config) props
pure $
summaryFailed summary == 0 &&
summaryGaveUp summary == 0
checkGroupWith ::
WorkerCount
-> Verbosity
-> Maybe UseColor
-> [(PropertyName, Property)]
-> IO Summary
checkGroupWith n verbosity mcolor props =
displayRegion $ \sregion -> do
svar <- atomically . TVar.newTVar $ mempty { summaryWaiting = PropertyCount (length props) }
let
start (TasksRemaining tasks) _ix (name, prop) =
liftIO $ do
updateSummary sregion svar mcolor $ \x -> x {
summaryWaiting =
PropertyCount tasks
, summaryRunning =
summaryRunning x + 1
}
atomically $ do
region <-
case verbosity of
Quiet ->
newEmptyRegion
Normal ->
newOpenRegion
moveToBottom sregion
pure (name, prop, region)
finish (_name, _prop, _region) =
updateSummary sregion svar mcolor $ \x -> x {
summaryRunning =
summaryRunning x - 1
}
finalize (_name, _prop, region) =
finishRegion region
summary <-
fmap (mconcat . fmap (fromResult . reportStatus)) $
runTasks n props start finish finalize $ \(name, prop, region) -> do
result <- checkNamed region mcolor (Just name) prop
updateSummary sregion svar mcolor
(<> fromResult (reportStatus result))
pure result
updateSummary sregion svar mcolor (const summary)
pure summary
checkNamed ::
MonadIO m
=> Region
-> Maybe UseColor
-> Maybe PropertyName
-> Property
-> m (Report Result)
checkNamed region mcolor name prop = do
seed <- liftIO Seed.random
checkRegion region mcolor name 0 seed prop
checkRegion ::
MonadIO m
=> Region
-> Maybe UseColor
-> Maybe PropertyName
-> Size
-> Seed
-> Property
-> m (Report Result)
checkRegion region mcolor name size seed prop =
liftIO $ do
result <-
checkReport (propertyConfig prop) size seed (propertyTest prop) $ \progress -> do
ppprogress <- renderProgress mcolor name progress
case reportStatus progress of
Running ->
setRegion region ppprogress
Shrinking _ ->
openRegion region ppprogress
ppresult <- renderResult mcolor name result
case reportStatus result of
Failed _ ->
openRegion region ppresult
GaveUp ->
openRegion region ppresult
OK ->
setRegion region ppresult
pure result
checkReport ::
forall m.
MonadIO m
=> MonadCatch m
=> PropertyConfig
-> Size
-> Seed
-> PropertyT m ()
-> (Report Progress -> m ())
-> m (Report Result)
checkReport cfg size0 seed0 test0 updateUI =
let
test =
catchAll test0 (fail . show)
loop !tests !discards !size !seed = do
updateUI $ Report tests discards Running
if size > 99 then
-- size has reached limit, reset to 0
loop tests discards 0 seed
else if tests >= fromIntegral (propertyTestLimit cfg) then
-- we've hit the test limit, test was successful
pure $ Report tests discards OK
else if discards >= fromIntegral (propertyDiscardLimit cfg) then
-- we've hit the discard limit, give up
pure $ Report tests discards GaveUp
else
case Seed.split seed of
(s0, s1) -> do
node@(Node x _) <-
runTree . runDiscardEffect $ runGenT size s0 . runTestT $ unPropertyT test
case x of
Nothing ->
loop tests (discards + 1) (size + 1) s1
Just (Left _, _) ->
let
mkReport =
Report (tests + 1) discards
in
fmap mkReport $
takeSmallest
size
seed
0
(propertyShrinkLimit cfg)
(propertyShrinkRetries cfg)
(updateUI . mkReport)
node
Just (Right (), _) ->
loop (tests + 1) discards (size + 1) s1
in
loop 0 0 size0 seed0
takeSmallest ::
MonadIO m
=> Size
-> Seed
-> ShrinkCount
-> ShrinkLimit
-> ShrinkRetries
-> (Progress -> m ())
-> Node m (Maybe (Either Failure (), [Log]))
-> m Result
takeSmallest size seed shrinks slimit retries updateUI = \case
Node Nothing _ ->
pure GaveUp
Node (Just (x, w)) xs ->
case x of
Left (Failure loc err mdiff) -> do
let
failure =
mkFailure size seed shrinks loc err mdiff (reverse w)
updateUI $ Shrinking failure
if shrinks >= fromIntegral slimit then
-- if we've hit the shrink limit, don't shrink any further
pure $ Failed failure
else
findM xs (Failed failure) $ \m -> do
o <- runTreeN retries m
if isFailure o then
Just <$> takeSmallest size seed (shrinks + 1) slimit retries updateUI o
else
return Nothing
Right () ->
return OK
updateSummary :: Region -> TVar Summary -> Maybe UseColor -> (Summary -> Summary) -> IO ()
updateSummary sregion svar mcolor f = do
summary <- atomically (TVar.modifyTVar' svar f >> TVar.readTVar svar)
setRegion sregion =<< renderSummary mcolor summary
isFailure :: Node m (Maybe (Either x a, b)) -> Bool
isFailure = \case
Node (Just (Left _, _)) _ ->
True
_ ->
False
isSuccess :: Node m (Maybe (Either x a, b)) -> Bool
isSuccess =
not . isFailure
runTreeN ::
Monad m
=> ShrinkRetries
-> Tree m (Maybe (Either x a, b))
-> m (Node m (Maybe (Either x a, b)))
runTreeN n m = do
o <- runTree m
if n > 0 && isSuccess o then
runTreeN (n - 1) m
else
pure o
findM :: Monad m => [a] -> b -> (a -> m (Maybe b)) -> m b
findM xs0 def p =
case xs0 of
[] ->
return def
x0 : xs ->
p x0 >>= \m ->
case m of
Nothing ->
findM xs def p
Just x ->
return x
-- liftIO . displayRegion $ \region ->
-- (== OK) . reportStatus <$> checkNamed region Nothing Nothing prop
------------------------------------------------------------------------
-- Data
-- | A summary of all the properties executed.
--
data Summary =
Summary {
summaryWaiting :: !PropertyCount
, summaryRunning :: !PropertyCount
, summaryFailed :: !PropertyCount
, summaryGaveUp :: !PropertyCount
, summaryOK :: !PropertyCount
} deriving (Show)
instance Monoid Summary where
mempty =
Summary 0 0 0 0 0
mappend (Summary x1 x2 x3 x4 x5) (Summary y1 y2 y3 y4 y5) =
Summary
(x1 + y1)
(x2 + y2)
(x3 + y3)
(x4 + y4)
(x5 + y5)
instance Semigroup Summary where
(<>) = mappend
-- | Construct a summary from a single result.
--
fromResult :: Result -> Summary
fromResult = \case
Failed _ ->
mempty { summaryFailed = 1 }
GaveUp ->
mempty { summaryGaveUp = 1 }
OK ->
mempty { summaryOK = 1 }
summaryCompleted :: Summary -> PropertyCount
summaryCompleted (Summary _ _ x3 x4 x5) =
x3 + x4 + x5
summaryTotal :: Summary -> PropertyCount
summaryTotal (Summary x1 x2 x3 x4 x5) =
x1 + x2 + x3 + x4 + x5
------------------------------------------------------------------------
-- Pretty Printing Helpers
data Line a =
Line a LineNo String
deriving (Eq, Ord, Show, Functor)
data Declaration a =
Declaration {
declarationFile :: !FilePath
, declarationLine :: !LineNo
, _declarationName :: !String
, declarationSource :: !(Map LineNo (Line a))
} deriving (Eq, Ord, Show, Functor)
data Style =
StyleDefault
| StyleAnnotation
| StyleFailure
deriving (Eq, Ord, Show)
data Markup =
WaitingIcon
| WaitingHeader
| RunningIcon
| RunningHeader
| ShrinkingIcon
| ShrinkingHeader
| FailedIcon
| FailedHeader
| GaveUpIcon
| GaveUpHeader
| SuccessIcon
| SuccessHeader
| DeclarationLocation
| StyledLineNo !Style
| StyledBorder !Style
| StyledSource !Style
| AnnotationGutter
| AnnotationValue
| FailureArrows
| FailureGutter
| FailureMessage
| DiffPrefix
| DiffInfix
| DiffSuffix
| DiffSame
| DiffRemoved
| DiffAdded
| ReproduceHeader
| ReproduceGutter
| ReproduceSource
deriving (Eq, Ord, Show)
instance Semigroup Style where
(<>) x y =
case (x, y) of
(StyleFailure, _) ->
StyleFailure
(_, StyleFailure) ->
StyleFailure
(StyleAnnotation, _) ->
StyleAnnotation
(_, StyleAnnotation) ->
StyleAnnotation
(StyleDefault, _) ->
StyleDefault
------------------------------------------------------------------------
takeAnnotation :: Log -> Maybe FailedAnnotation
takeAnnotation = \case
Annotation loc val ->
Just $ FailedAnnotation loc val
_ ->
Nothing
takeFootnote :: Log -> Maybe String
takeFootnote = \case
Footnote x ->
Just x
_ ->
Nothing
mkFailure ::
Size
-> Seed
-> ShrinkCount
-> Maybe Span
-> String
-> Maybe Diff
-> [Log]
-> FailureReport
mkFailure size seed shrinks location message diff logs =
let
inputs =
mapMaybe takeAnnotation logs
footnotes =
mapMaybe takeFootnote logs
in
FailureReport size seed shrinks inputs location message diff footnotes
------------------------------------------------------------------------
-- Pretty Printing
ppShow :: Show x => x -> Doc a
ppShow = -- unfortunate naming clash
WL.text . show
markup :: Markup -> Doc Markup -> Doc Markup
markup =
WL.annotate
icon :: Markup -> Char -> Doc Markup -> Doc Markup
icon m i x =
markup m (WL.char i) <+> x
ppTestCount :: TestCount -> Doc a
ppTestCount = \case
TestCount 1 ->
"1 test"
TestCount n ->
ppShow n <+> "tests"
ppDiscardCount :: DiscardCount -> Doc a
ppDiscardCount = \case
DiscardCount 1 ->
"1 discard"
DiscardCount n ->
ppShow n <+> "discards"
ppShrinkCount :: ShrinkCount -> Doc a
ppShrinkCount = \case
ShrinkCount 1 ->
"1 shrink"
ShrinkCount n ->
ppShow n <+> "shrinks"
ppRawPropertyCount :: PropertyCount -> Doc a
ppRawPropertyCount (PropertyCount n) =
ppShow n
ppWithDiscardCount :: DiscardCount -> Doc Markup
ppWithDiscardCount = \case
DiscardCount 0 ->
mempty
n ->
" with" <+> ppDiscardCount n
ppShrinkDiscard :: ShrinkCount -> DiscardCount -> Doc Markup
ppShrinkDiscard s d =
case (s, d) of
(0, 0) ->
""
(0, _) ->
" and" <+> ppDiscardCount d
(_, 0) ->
" and" <+> ppShrinkCount s
(_, _) ->
"," <+> ppShrinkCount s <+> "and" <+> ppDiscardCount d
mapSource :: (Map LineNo (Line a) -> Map LineNo (Line a)) -> Declaration a -> Declaration a
mapSource f decl =
decl {
declarationSource =
f (declarationSource decl)
}
ppFailedInputTypedArgument :: Int -> FailedAnnotation -> Doc Markup
ppFailedInputTypedArgument ix (FailedAnnotation _ val) =
WL.vsep [
WL.text "forAll" <> ppShow ix <+> "="
, WL.indent 2 . WL.vsep . fmap (markup AnnotationValue . WL.text) $ lines val
]
fixedDecl :: Declaration (Style, [t])
fixedDecl = Declaration {
declarationFile = "",
declarationLine = LineNo 1,
_declarationName = "",
declarationSource = Map.fromList [ (LineNo 1, Line (StyleDefault,[]) (LineNo 1) "") ]
}
ppFailedInputDeclaration ::
MonadIO m
=> FailedAnnotation
-> m (Maybe (Declaration (Style, [(Style, Doc Markup)])))
ppFailedInputDeclaration (FailedAnnotation _ val) =
runMaybeT $ do
let
ppValLine =
WL.indent 0 .
(markup AnnotationGutter (WL.text "") <>) .
markup AnnotationValue .
WL.text
valDocs =
fmap ((StyleAnnotation, ) . ppValLine) $
List.lines val
styleInput kvs =
foldr (Map.adjust . fmap . first $ const StyleAnnotation) kvs [0,1]
insertDoc =
Map.adjust (fmap . second $ const valDocs) 1
pure $
mapSource (styleInput . insertDoc) fixedDecl
ppFailedInput ::
MonadIO m
=> Int
-> FailedAnnotation
-> m (Either (Doc Markup) (Declaration (Style, [(Style, Doc Markup)])))
ppFailedInput ix input = do
mdecl <- ppFailedInputDeclaration input
case mdecl of
Nothing ->
pure . Left $ ppFailedInputTypedArgument ix input
Just decl ->
pure $ Right decl
ppLineDiff :: LineDiff -> Doc Markup
ppLineDiff = \case
LineSame x ->
markup DiffSame $
" " <> WL.text x
LineRemoved x ->
markup DiffRemoved $
"- " <> WL.text x
LineAdded x ->
markup DiffAdded $
"+ " <> WL.text x
ppDiff :: Diff -> [Doc Markup]
ppDiff (Diff prefix removed infix_ added suffix diff) = [
markup DiffPrefix (WL.text prefix) <>
markup DiffRemoved (WL.text removed) <+>
markup DiffInfix (WL.text infix_) <+>
markup DiffAdded (WL.text added) <>
markup DiffSuffix (WL.text suffix)
] ++ fmap ppLineDiff (toLineDiff diff)
ppDeclaration :: Declaration (Style, [(Style, Doc Markup)]) -> Doc Markup
ppDeclaration decl =
case Map.maxView $ declarationSource decl of
Nothing ->
mempty
Just _ ->
let
ppLines = do
Line (_, xs) _ _ <- Map.elems $ declarationSource decl
fmap snd xs
in
WL.vsep (ppLines)
mergeLine :: Semigroup a => Line a -> Line a -> Line a
mergeLine (Line x no src) (Line y _ _) =
Line (x <> y) no src
mergeDeclaration :: Semigroup a => Declaration a -> Declaration a -> Declaration a
mergeDeclaration (Declaration file line name src0) (Declaration _ _ _ src1) =
Declaration file line name $
Map.unionWith mergeLine src0 src1
mergeDeclarations :: Semigroup a => [Declaration a] -> [Declaration a]
mergeDeclarations =
Map.elems .
Map.fromListWith mergeDeclaration .
fmap (\d -> ((declarationFile d, declarationLine d), d))
ppTextLines :: String -> [Doc Markup]
ppTextLines =
fmap WL.text . List.lines
ppFailureReport :: MonadIO m => Maybe PropertyName -> FailureReport -> m (Doc Markup)
ppFailureReport _ (FailureReport _ _ _ inputs0 _ msg mdiff msgs0) = do
_ <- let
msgs1 =
msgs0 ++
(if null msg then [] else [msg])
docs =
concatMap ppTextLines msgs1 ++
maybe [] ppDiff mdiff
in
pure (docs, Nothing)
(_, idecls) <- partitionEithers <$> zipWithM ppFailedInput [0..] inputs0
let
decls =
mergeDeclarations .
catMaybes $
fmap pure idecls
with xs f =
if null xs then
[]
else
[f xs]
pure . WL.indent 2 . WL.vsep . WL.punctuate WL.line $ concat [
with decls $
WL.vsep . WL.punctuate WL.line . fmap ppDeclaration
]
ppName :: Maybe PropertyName -> Doc a
ppName = \case
Nothing ->
"<interactive>"
Just (PropertyName name) ->
WL.text name
ppProgress :: MonadIO m => Maybe PropertyName -> Report Progress -> m (Doc Markup)
ppProgress name (Report tests discards status) =
case status of
Running ->
pure . icon RunningIcon '●' . WL.annotate RunningHeader $
ppName name <+>
"passed" <+>
ppTestCount tests <>
ppWithDiscardCount discards <+>
"(running)"
Shrinking failure ->
pure . icon ShrinkingIcon '↯' . WL.annotate ShrinkingHeader $
ppName name <+>
"failed after" <+>
ppTestCount tests <>
ppShrinkDiscard (failureShrinks failure) discards <+>
"(shrinking)"
ppResult :: MonadIO m => Maybe PropertyName -> Report Result -> m (Doc Markup)
ppResult name (Report tests discards result) =
case result of
Failed failure -> do
pfailure <- ppFailureReport name failure
pure . WL.vsep $ [
icon FailedIcon '✗' . WL.annotate FailedHeader $
ppName name <+>
"failed after" <+>
ppTestCount tests <>
ppShrinkDiscard (failureShrinks failure) discards <>
"."
, mempty
, pfailure
, mempty
]
GaveUp ->
pure . icon GaveUpIcon '⚐' . WL.annotate GaveUpHeader $
ppName name <+>
"gave up after" <+>
ppDiscardCount discards <>
", passed" <+>
ppTestCount tests <>
"."
OK ->
pure . icon SuccessIcon '✓' . WL.annotate SuccessHeader $
ppName name <+>
"passed" <+>
ppTestCount tests <>
"."
ppWhenNonZero :: Doc a -> PropertyCount -> Maybe (Doc a)
ppWhenNonZero suffix n =
if n <= 0 then
Nothing
else
Just $ ppRawPropertyCount n <+> suffix
annotateSummary :: Summary -> Doc Markup -> Doc Markup
annotateSummary summary =
if summaryFailed summary > 0 then
icon FailedIcon '✗' . WL.annotate FailedHeader
else if summaryGaveUp summary > 0 then
icon GaveUpIcon '⚐' . WL.annotate GaveUpHeader
else if summaryWaiting summary > 0 || summaryRunning summary > 0 then
icon WaitingIcon '○' . WL.annotate WaitingHeader
else
icon SuccessIcon '✓' . WL.annotate SuccessHeader
ppSummary :: MonadIO m => Summary -> m (Doc Markup)
ppSummary summary =
let
complete =
summaryCompleted summary == summaryTotal summary
prefix end =
if complete then
mempty
else
ppRawPropertyCount (summaryCompleted summary) <>
"/" <>
ppRawPropertyCount (summaryTotal summary) <+>
"complete" <> end
addPrefix xs =
if null xs then
prefix mempty : []
else
prefix ": " : xs
suffix =
if complete then
"."
else
" (running)"
in
pure .
annotateSummary summary .
(<> suffix) .
WL.hcat .
addPrefix .
WL.punctuate ", " $
catMaybes [
ppWhenNonZero "failed" (summaryFailed summary)
, ppWhenNonZero "gave up" (summaryGaveUp summary)
, if complete then
ppWhenNonZero "succeeded" (summaryOK summary)
else
Nothing
]
renderDoc :: MonadIO m => Maybe UseColor -> Doc Markup -> m String
renderDoc mcolor doc = do
let
dull =
SetColor Foreground Dull
vivid =
SetColor Foreground Vivid
bold =
SetConsoleIntensity BoldIntensity
start = \case
WaitingIcon ->
setSGRCode []
WaitingHeader ->
setSGRCode []
RunningIcon ->
setSGRCode []
RunningHeader ->
setSGRCode []
ShrinkingIcon ->
setSGRCode [vivid Red]
ShrinkingHeader ->
setSGRCode [vivid Red]
FailedIcon ->
setSGRCode [vivid Red]
FailedHeader ->
setSGRCode [vivid Red]
GaveUpIcon ->
setSGRCode [dull Yellow]
GaveUpHeader ->
setSGRCode [dull Yellow]
SuccessIcon ->
setSGRCode [dull Green]
SuccessHeader ->
setSGRCode [dull Green]
DeclarationLocation ->
setSGRCode []
StyledLineNo StyleDefault ->
setSGRCode []
StyledSource StyleDefault ->
setSGRCode []
StyledBorder StyleDefault ->
setSGRCode []
StyledLineNo StyleAnnotation ->
setSGRCode [dull Magenta]
StyledSource StyleAnnotation ->
setSGRCode []
StyledBorder StyleAnnotation ->
setSGRCode []
AnnotationGutter ->
setSGRCode [dull Magenta]
AnnotationValue ->
setSGRCode [dull Magenta]
StyledLineNo StyleFailure ->
setSGRCode [vivid Red]
StyledSource StyleFailure ->
setSGRCode [vivid Red, bold]
StyledBorder StyleFailure ->
setSGRCode []
FailureArrows ->
setSGRCode [vivid Red]
FailureMessage ->
setSGRCode []
FailureGutter ->
setSGRCode []
DiffPrefix ->
setSGRCode []
DiffInfix ->
setSGRCode []
DiffSuffix ->
setSGRCode []
DiffSame ->
setSGRCode []
DiffRemoved ->
setSGRCode [dull Red]
DiffAdded ->
setSGRCode [dull Green]
ReproduceHeader ->
setSGRCode []
ReproduceGutter ->
setSGRCode []
ReproduceSource ->
setSGRCode []
end _ =
setSGRCode [Reset]
color <- resolveColor mcolor
let
display =
case color of
EnableColor ->
WL.displayDecorated start end id
DisableColor ->
WL.display
pure .
display .
WL.renderSmart 100 $
WL.indent 2 doc
renderProgress :: MonadIO m => Maybe UseColor -> Maybe PropertyName -> Report Progress -> m String
renderProgress mcolor name x =
renderDoc mcolor =<< ppProgress name x
renderResult :: MonadIO m => Maybe UseColor -> Maybe PropertyName -> Report Result -> m String
renderResult mcolor name x =
renderDoc mcolor =<< ppResult name x
renderSummary :: MonadIO m => Maybe UseColor -> Summary -> m String
renderSummary mcolor x =
renderDoc mcolor =<< ppSummary x

@ -1,15 +0,0 @@
module Echidna.Property where
import Data.Text ( unpack )
import Data.Yaml ( Value(..), FromJSON, parseJSON )
data PropertyType = ShouldReturnTrue | ShouldReturnFalse | ShouldRevert | ShouldReturnFalseRevert
deriving (Show)
instance FromJSON PropertyType where
parseJSON (String "Success") = pure ShouldReturnTrue
parseJSON (String "Fail") = pure ShouldReturnFalse
parseJSON (String "Throw") = pure ShouldRevert
parseJSON (String "Fail or Throw") = pure ShouldReturnFalseRevert
parseJSON (String s) = fail $ "Expected return type, not " ++ unpack s
parseJSON _ = fail "Expected return type, should be a string"

@ -1,49 +1,47 @@
{-# LANGUAGE FlexibleContexts, OverloadedStrings, LambdaCase, TupleSections #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
module Echidna.Solidity where
import Control.Lens ((^.), (%=), _1, assign, use, view)
import Control.Lens
import Control.Exception (Exception)
import Control.Monad (liftM2)
import Control.Monad.Catch (MonadThrow(..))
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Reader (MonadReader, ask)
import Control.Monad.State.Strict (MonadState, execState, modify, runState)
import Control.Monad.Reader (MonadReader)
import Control.Monad.State.Strict (execStateT)
import Data.Foldable (toList)
import Data.Has (Has(..))
import Data.List (find, partition)
import Data.Map (insert)
import Data.Maybe (isNothing, fromMaybe)
import Data.Maybe (isNothing)
import Data.Monoid ((<>))
import Data.Text (Text, isPrefixOf, pack, unpack)
import System.Process (readProcess)
import System.IO.Temp (writeSystemTempFile)
import qualified Data.Map as Map (lookup)
import Echidna.ABI (SolSignature)
import Echidna.Config (Config(..), sender, contractAddr, gasLimit, prefix, solcArgs)
import Echidna.ABI (SolSignature)
import Echidna.Exec (execTx)
import Echidna.Transaction (Tx(..))
import EVM
(Contract, VM, VMResult(..), caller, contract, codeContract, contracts, env, gas, loadContract, replaceCodeOfSelf, resetState, state)
import EVM.Concrete (Blob(..), w256)
import EVM.Exec (exec, vmForEthrunCreation)
import EVM.Keccak (newContractAddress)
import EVM hiding (contracts)
import EVM.Exec (vmForEthrunCreation)
import EVM.Solidity (abiMap, contractName, creationCode, methodInputs, methodName, readSolc, SolcContract)
import EVM.Types (Addr)
data EchidnaException = BadAddr Addr
| CompileFailure
| NoContracts
| TestArgsFound Text
| ContractNotFound Text
| NoBytecode Text
| NoFuncs
| NoTests
| OnlyTests
instance Show EchidnaException where
data SolException = BadAddr Addr
| CompileFailure
| NoContracts
| TestArgsFound Text
| ContractNotFound Text
| NoBytecode Text
| NoFuncs
| NoTests
| OnlyTests
instance Show SolException where
show = \case
BadAddr a -> "No contract at " ++ show a ++ " exists"
CompileFailure -> "Couldn't compile given file"
@ -55,76 +53,46 @@ instance Show EchidnaException where
NoTests -> "No tests found in ABI"
OnlyTests -> "Only tests and no public functions found in ABI"
instance Exception EchidnaException
-- | parses additional solc arguments
solcArguments :: FilePath -> Maybe Text -> [String]
solcArguments filePath argStr = args <> fromMaybe [] additional
where args = ["--combined-json=bin-runtime,bin,srcmap,srcmap-runtime,abi,ast", filePath]
additional = words . unpack <$> argStr
-- | reads all contracts within the solidity file at `filepath` and passes optional solc params to compiler
readContracts :: (MonadIO m, MonadThrow m, MonadReader Config m) => FilePath -> m [SolcContract]
readContracts filePath = do
conf <- ask
liftIO (solc conf) >>= \case
Nothing -> throwM CompileFailure
Just m -> return $ toList $ fst m
where solc c = readSolc =<< writeSystemTempFile "" =<< readProcess
"solc" (solcArguments filePath (pack <$> (c ^. solcArgs))) ""
-- | reads either the first contract found or the contract named `selectedContractName` within the solidity file at `filepath`
readContract :: (MonadIO m, MonadThrow m, MonadReader Config m) => FilePath -> Maybe Text -> m SolcContract
readContract filePath selectedContractName = do
cs <- readContracts filePath
c <- chooseContract cs $ ((pack filePath <> ":") <>) <$> selectedContractName
warn (isNothing selectedContractName && 1 < length cs)
"Multiple contracts found in file, only analyzing the first"
liftIO $ print $ "Analyzing contract: " <> c ^. contractName
return c
where chooseContract :: (MonadThrow m) => [SolcContract] -> Maybe Text -> m SolcContract
chooseContract [] _ = throwM NoContracts
chooseContract (c:_) Nothing = return c
chooseContract cs (Just name) = case find (\x -> name == x ^. contractName) cs of
Nothing -> throwM $ ContractNotFound name
Just c -> return c
warn :: (MonadIO m) => Bool -> Text -> m ()
warn p s = if p then liftIO $ print s else pure ()
-- | loads the solidity file at `filePath` and selects either the default or specified contract to analyze
loadSolidity :: (MonadIO m, MonadThrow m, MonadReader Config m)
=> FilePath
-> Maybe Text
-> m (VM, [SolSignature], [Text])
loadSolidity filePath selectedContract = do
conf <- ask
c <- readContract filePath selectedContract
let (VMSuccess (B bc), vm) = runState exec . vmForEthrunCreation $ c ^. creationCode
load = do resetState
assign (state . gas) (w256 $ conf ^. gasLimit)
assign (state . contract) (conf ^. contractAddr)
assign (state . codeContract) (conf ^. contractAddr)
assign (state . caller) (conf ^. sender)
loadContract (vm ^. state . contract)
loaded = execState load $ execState (replaceCodeOfSelf bc) vm
abi = map (liftM2 (,) (view methodName) (map snd . view methodInputs)) . toList $ c ^. abiMap
(tests, funs) = partition (isPrefixOf (conf ^. prefix) . fst) abi
if null abi then throwM NoFuncs else pure ()
instance Exception SolException
data SolConf = SolConf { _contractAddr :: Addr
, _deployer :: Addr
, _prefix :: Text
, _solcArgs :: String
}
makeLenses ''SolConf
contracts :: (MonadIO m, MonadThrow m, MonadReader x m, Has SolConf x) => FilePath -> m [SolcContract]
contracts fp = view (hasLens . solcArgs) >>= liftIO . solc >>= (\case
Nothing -> throwM CompileFailure
Just m -> pure . toList $ fst m) where
solc a = readSolc =<< writeSystemTempFile "" =<< readProcess "solc" (usual <> words a) ""
usual = ["--combined-json=bin-runtime,bin,srcmap,srcmap-runtime,abi,ast", fp]
selected :: (MonadIO m, MonadThrow m, MonadReader x m, Has SolConf x) => FilePath -> Maybe Text -> m SolcContract
selected fp name = do cs <- contracts fp
c <- choose cs $ ((pack fp <> ":") <>) <$> name
liftIO $ if (isNothing name && length cs > 1)
then putStrLn "Multiple contracts found in file, only analyzing the first"
else pure ()
liftIO . putStrLn $ "Analyzing contract: " <> unpack (c ^. contractName)
return c
where choose [] _ = throwM NoContracts
choose (c:_) Nothing = return c
choose cs (Just n) = maybe (throwM $ ContractNotFound n) pure $
find ((n ==) . view contractName) cs
loadSolidity :: (MonadIO m, MonadThrow m, MonadReader x m, Has SolConf x)
=> FilePath -> Maybe Text -> m (VM, [SolSignature], [Text])
loadSolidity fp name = do
c <- selected fp name
(SolConf ca d pref _) <- view hasLens
let bc = c ^. creationCode
abi = map (liftM2 (,) (view methodName) (fmap snd . view methodInputs)) . toList $ c ^. abiMap
(tests, funs) = partition (isPrefixOf pref . fst) abi
loaded <- execStateT (execTx $ Tx (Right bc) d ca 0) $ vmForEthrunCreation bc
if null abi then throwM NoFuncs else pure ()
if null funs then throwM OnlyTests else pure ()
case find (not . null . snd) tests of
Nothing -> return (loaded, funs, fst <$> tests)
(Just (t,_)) -> throwM $ TestArgsFound t
insertContract :: MonadState VM m => Contract -> m ()
insertContract c = do a <- (`newContractAddress` 1) <$> use (state . contract)
env . contracts %= insert a c
modify . execState $ loadContract a
currentContract :: MonadThrow m => VM -> m Contract
currentContract v = let a = v ^. state . contract in
maybe (throwM $ BadAddr a) pure . Map.lookup a $ v ^. env . contracts
addSolidity :: (MonadIO m, MonadReader Config m, MonadThrow m, MonadState VM n) => FilePath -> Maybe Text -> m (n ())
addSolidity f mc = pure . insertContract =<< currentContract =<< view _1 <$> loadSolidity f mc
Nothing -> return (loaded, funs, fst <$> tests)

@ -0,0 +1,46 @@
{-# LANGUAGE FlexibleContexts #-}
module Echidna.Test where
import Control.Monad (ap)
import Control.Monad.Catch (MonadThrow)
import Control.Monad.Random.Strict (MonadRandom, getRandomR)
import Control.Monad.Reader.Class (MonadReader, asks)
import Control.Monad.State.Strict (MonadState(..), gets)
import Data.Bool (bool)
import Data.Has (Has(..))
import Data.Text (Text)
import EVM
import EVM.ABI (AbiValue(..), encodeAbiValue)
import EVM.Concrete (Blob(..))
import EVM.Types (Addr)
import Echidna.Exec
import Echidna.Transaction
type SolTest = (Text, Addr)
data TestConf = TestConf { classifier :: VM -> Bool
, testSender :: Addr -> Addr
}
data CallRes = ResFalse | ResTrue | ResRevert | ResOther deriving (Eq, Show)
classifyRes :: VMResult -> CallRes
classifyRes (VMSuccess (B b)) | b == encodeAbiValue (AbiBool True) = ResTrue
| b == encodeAbiValue (AbiBool False) = ResFalse
classifyRes Reversion = ResRevert
classifyRes _ = ResOther
checkETest :: (MonadReader x m, Has TestConf x, MonadState y m, Has VM y, MonadThrow m) => SolTest -> m Bool
checkETest (f, a) = asks getter >>= \(TestConf p s) -> do
og <- get
res <- execTx (Tx (Left (f, [])) (s a) a 0) >> gets (p . getter)
put og
pure res
shrinkSeq :: (MonadRandom m, MonadReader x m, Has TestConf x, MonadState y m, Has VM y, MonadThrow m)
=> SolTest -> [Tx] -> m [Tx]
shrinkSeq t xs = shorten >>= mapM shrinkTx >>= ap (fmap . flip bool xs) check where
check xs' = do {og <- get; res <- traverse execTx xs' >> checkETest t; put og; pure res}
shorten = (\i -> take i xs ++ drop (i + 1) xs) <$> getRandomR (0, length xs)

@ -0,0 +1,94 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell #-}
module Echidna.Transaction where
import Prelude hiding (Word)
import Control.Lens
import Control.Monad (liftM4)
import Control.Monad.Catch (MonadThrow)
import Control.Monad.Random.Strict (MonadRandom, getRandomR)
import Control.Monad.Reader.Class (MonadReader)
import Control.Monad.State.Strict (MonadState, State, runState)
import Data.ByteString (ByteString)
import Data.Either (either, lefts)
import Data.Has (Has(..))
import Data.Set (Set)
import EVM
import EVM.ABI (abiCalldata, abiTypeSolidity, abiValueType)
import EVM.Concrete (Blob(..), Word(..), w256)
import EVM.Types (Addr)
import qualified Control.Monad.State.Strict as S (state)
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Data.Vector as V
import Echidna.ABI
-- Note: I currently don't model gas cost, nonces, or signatures here
data Tx = Tx { _call :: Either SolCall ByteString
, _src :: Addr
, _dst :: Addr
, _value :: Word
} deriving (Eq, Ord, Show)
makeLenses ''Tx
type ContractA = (Addr, [SolSignature])
data World = World { _senders :: [Addr]
, _receivers :: [ContractA]
}
makeLenses ''World
genTxWith :: (MonadRandom m, MonadState x m, Has World x, MonadThrow m)
=> ([Addr] -> m Addr) -- Sender generator
-> ([ContractA] -> m ContractA) -- Receiver generator
-> (Addr -> ContractA -> m SolCall) -- Call generator
-> (Addr -> ContractA -> SolCall -> m Word) -- Value generator
-> m Tx
genTxWith s r c v = use hasLens >>= \case(World ss rs) -> do s' <- s ss
r' <- r rs
c' <- c s' r'
Tx (Left c') s' (fst r') <$> v s' r' c'
genTx :: (MonadRandom m, MonadState x m, Has World x, MonadThrow m) => m Tx
genTx = genTxWith (rElem "sender list") (rElem "recipient list") (const $ genInteractions . snd) (\_ _ _ -> pure 0)
genTxM :: (MonadRandom m, MonadState x m, Has World x, MonadThrow m, MonadReader y m, Has GenConf y) => m Tx
genTxM = genTxWith (rElem "sender list") (rElem "recipient list") (const $ genInteractionsM . snd) (\_ _ _ -> pure 0)
canShrinkTx :: Tx -> Bool
canShrinkTx (Tx (Right _) _ _ 0) = False
canShrinkTx (Tx (Left (_,l)) _ _ 0) = any canShrinkAbiValue l
canShrinkTx _ = True
shrinkTx :: MonadRandom m => Tx -> m Tx
shrinkTx (Tx c s d (C _ v)) = let c' = either (fmap Left . shrinkAbiCall) (fmap Right . pure) c in
liftM4 Tx c' (pure s) (pure d) $ w256 . fromIntegral <$> getRandomR (0 :: Integer, fromIntegral v)
spliceTxs :: (MonadRandom m, MonadState x m, Has World x, MonadThrow m) => Set Tx -> m Tx
spliceTxs ts = let l = S.toList ts; (cs, ss) = unzip $ (\(Tx c s _ _) -> (c,s)) <$> l in
genTxWith (const . rElem "sender list" $ ss) (rElem "recipient list")
(\_ _ -> mutateAbiCall =<< rElem "past calls" (lefts cs))
(\ _ _ (n,_) -> let valOf (Tx c _ _ v) = if elem n $ c ^? _Left . _1 then v else 0
in rElem "values" $ valOf <$> l)
liftSH :: (MonadState a m, Has b a) => State b x -> m x
liftSH = S.state . runState . zoom hasLens
setupTx :: (MonadState x m, Has VM x) => Tx -> m ()
setupTx (Tx c s r v) = liftSH . sequence_ $
[ result .= Nothing, state . pc .= 0, state . stack .= mempty, state . gas .= 0xffffffff
, env . origin .= s, state . caller .= s, state . callvalue .= v, setup] where
setup = case c of
Left cd -> loadContract r >> state . calldata .= encode cd
Right bc -> assign (env . contracts . ix r) (initialContract bc) >> loadContract r
encode (n, vs) = B . abiCalldata
(n <> "(" <> T.intercalate "," (abiTypeSolidity . abiValueType <$> vs) <> ")") $ V.fromList vs

@ -0,0 +1,121 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
module Echidna.UI where
import Brick
import Brick.BChan
import Brick.Widgets.Border
import Brick.Widgets.Center
import Control.Lens
import Control.Monad (forever)
import Control.Monad.Catch (MonadCatch(..))
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Random.Strict (MonadRandom)
import Control.Monad.Reader (MonadReader, runReader)
import Data.Bool (bool)
import Data.Either (either)
import Data.Foldable (toList)
import Data.Has (Has(..))
import Data.List (intercalate)
import Data.Map (Map)
import Data.Set (Set)
import EVM (VM)
import EVM.ABI (AbiValue(..))
import EVM.Types (Addr, W256)
import Graphics.Vty (Event(..), Key(..), Modifier(..), defaultConfig, mkVty)
import Numeric (showHex)
import UnliftIO (MonadUnliftIO)
import UnliftIO.Concurrent (forkIO, killThread)
import qualified Data.Text as T
import Echidna.Campaign
import Echidna.ABI
import Echidna.Exec
import Echidna.Test
import Echidna.Transaction
ppAbiValue :: AbiValue -> String
ppAbiValue (AbiUInt _ n) = show n
ppAbiValue (AbiInt _ n) = show n
ppAbiValue (AbiAddress n) = showHex n ""
ppAbiValue (AbiBool b) = if b then "true" else "false"
ppAbiValue (AbiBytes _ b) = show b
ppAbiValue (AbiBytesDynamic b) = show b
ppAbiValue (AbiString s) = show s
ppAbiValue (AbiArrayDynamic _ v) =
"[" ++ intercalate ", " (ppAbiValue <$> toList v) ++ "]"
ppAbiValue (AbiArray _ _ v) =
"[" ++ intercalate ", " (ppAbiValue <$> toList v) ++ "]"
ppSolCall :: SolCall -> String
ppSolCall (t, vs) = T.unpack t ++ "(" ++ intercalate "," (ppAbiValue <$> vs) ++ ")"
data Role = Sender | Receiver | Ambiguous
type Names = Role -> Addr -> String
ppTx :: (MonadReader x m, Has Names x) => Tx -> m String
ppTx (Tx c s r v) = let sOf = either ppSolCall (const "<CREATE>") in
view hasLens <&> \f -> sOf c ++ f Sender s ++ f Receiver r
++ (if v == 0 then "" else "Value: " ++ show v)
progress :: Int -> Int -> String
progress n m = "(" ++ show n ++ "/" ++ show m ++ ")"
ppFail :: (MonadReader x m, Has Names x) => Maybe (Int, Int) -> [Tx] -> m String
ppFail _ [] = pure "failed with no transactions made ⁉ "
ppFail b xs = let status = case b of
Nothing -> ""
Just (n,m) -> ", shrinking " ++ progress n m in
(("failed!💥 \n Call sequence" ++ status ++ ":\n") ++) <$> unlines . fmap (" " ++) <$> mapM ppTx xs
ppTS :: (MonadReader x m, Has CampaignConf x, Has Names x) => TestState -> m String
ppTS (Failed e) = pure $ "could not evaluate ☣\n " ++ show e
ppTS (Solved l) = ppFail Nothing l
ppTS Passed = pure $ "passed! 🎉"
ppTS (Open i) = view hasLens >>= \(CampaignConf t _ _ _) ->
if i >= t then ppTS Passed else pure $ "fuzzing " ++ progress i t
ppTS (Large n l) = view (hasLens . to shrinkLimit) >>= \m -> ppFail (if n < m then Just (n,m)
else Nothing) l
ppTests :: (MonadReader x m, Has CampaignConf x, Has Names x) => Campaign -> m String
ppTests (Campaign ts _) = unlines <$> mapM (\((n, _), s) -> ((T.unpack n ++ ": ") ++ ) <$> ppTS s) ts
ppCoverage :: Map W256 (Set Int) -> String
ppCoverage s = "Unique instructions: " ++ show (coveragePoints s)
++ "\nUnique codehashes: " ++ show (length s)
campaignStatus :: (MonadReader x m, Has CampaignConf x, Has Names x) => Campaign -> m (Widget ())
campaignStatus c = let mSection = maybe emptyWidget ((hBorder <=>) . padLeft (Pad 2) . str) in do
stats <- padLeft (Pad 2) . str <$> ppTests c <&> (<=> mSection (ppCoverage <$> c ^. coverage))
bl <- bool emptyWidget (str "Campaign complete, C-c or esc to print report") <$> isDone c
pure . hCenter . hLimit 120 . joinBorders $ (borderWithLabel (str "Echidna") stats) <=> bl
monitor :: (MonadReader x m, Has CampaignConf x, Has Names x)
=> IO a -> m (App Campaign Campaign ())
monitor cleanup = let
cs :: (CampaignConf, Names) -> Campaign -> Widget ()
cs s c = runReader (campaignStatus c) s
se s _ (AppEvent c') = continue c' & if runReader (isDone c') s then (liftIO cleanup >>) else id
se _ c (VtyEvent (EvKey KEsc _)) = liftIO cleanup >> halt c
se _ c (VtyEvent (EvKey (KChar 'c') l)) | MCtrl `elem` l = liftIO cleanup >> halt c
se _ c _ = continue c in
((,) <$> view hasLens <*> view hasLens) <&> \s ->
App (pure . cs s) neverShowCursor (se s) pure (const $ forceAttrMap mempty)
ui :: ( MonadCatch m, MonadRandom m, MonadReader x m, MonadUnliftIO m
, Has GenConf x, Has TestConf x, Has CampaignConf x, Has Names x)
=> VM -> World -> [SolTest] -> m Campaign
ui v w ts = let xfer e = use hasLens >>= \c -> (isDone c >>= ($ e c) . bool id forever) in do
bc <- liftIO $ newBChan 100
t <- forkIO $ campaign (xfer $ liftIO . writeBChan bc) v w ts >> pure ()
a <- monitor (killThread t)
c <- liftIO (customMain (mkVty defaultConfig) (Just bc) a $ Campaign mempty mempty)
(cf, tf) <- (maybe "" ppCoverage (c ^. coverage),) <$> ppTests c
liftIO (putStrLn tf >> putStrLn cf)
return c

@ -4,31 +4,36 @@ author: JP Smith
version: 0.0.0.1
ghc-options: -Wall -fno-warn-orphans -O2
ghc-options: -Wall -fno-warn-orphans -O2 -threaded +RTS -N -RTS
dependencies:
- aeson >= 1.3 && < 1.5
- base
- ansi-terminal
- brick
- bytestring >= 0.10.8 && < 0.11
- containers >= 0.5.7 && < 0.6
- data-dword >= 0.3.1 && < 0.4
- data-has
- deepseq
- directory >= 1.3 && < 1.4
- exceptions >= 0.8.1 && < 0.11
- hedgehog >= 0.6
- hashable
- hevm
- lens >= 4.15.1 && < 4.17
- MonadRandom
- mtl >= 2.2.1 && < 2.3
- multiset >= 0.3 && < 0.4
- optparse-applicative >= 0.13.0 && < 0.15
- process >= 1.4.3 && < 1.7
- stm
- temporary >= 1.2.1 && < 1.4
- text >= 1.2.2 && < 1.3
- transformers
- unliftio
- vector >= 0.11.0 && < 0.13
- vty
- wl-pprint-annotated
- word8
- yaml
- unordered-containers
@ -37,36 +42,9 @@ default-extensions:
library:
source-dirs: lib/
exposed-modules:
- Echidna.ABI
- Echidna.Config
- Echidna.Coverage
- Echidna.Exec
- Echidna.Solidity
- Echidna.Property
executables:
echidna-test:
main: Main.hs
source-dirs: src/
dependencies: echidna
echidna-diagnose:
main: Main.hs
source-dirs: diagnose
dependencies: echidna
revert-example-exe:
main: Revert.hs
source-dirs: examples/revert
dependencies: echidna
state-example-exe:
main: StateMachine.hs
source-dirs: examples/state-machine
dependencies: echidna
perprop-exe:
main: Main.hs
source-dirs: perprop
dependencies: echidna
gastest-exe:
main: Main.hs
source-dirs: gastest
dependencies: echidna

@ -5,15 +5,15 @@ contract Test {
bool private flag1=true;
function set0(int val) returns (bool){
if (val % 10 == 0) {flag0 = false;}
if (val % 100 == 0) {flag0 = false;}
}
function set1(int val) returns (bool){
if (val % 10 == 0 && flag0) {flag1 = false;}
if (val % 10 == 0 && !flag0) {flag1 = false;}
}
function echidna_alwaystrue() returns (bool){
return(true);
}
function echidna_sometimesfalse() returns (bool){
return(flag0 || flag1);
return(flag1);
}
}

@ -1,28 +1,16 @@
{-# LANGUAGE FlexibleContexts, OverloadedStrings, TupleSections, DoAndIfThenElse #-}
module Main where
import Control.Lens hiding (argument)
import Control.Concurrent.MVar (newMVar, readMVar, swapMVar)
import Control.Monad (forM, replicateM_)
import Control.Monad.Catch (MonadThrow(..))
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (runReaderT)
import Data.List (foldl')
import Data.Set (unions, size)
import Data.Text (pack)
import Data.Semigroup ((<>))
import Control.Monad.Reader (runReaderT)
import Data.Text (pack)
import Options.Applicative
import EVM
import Echidna.Config
import Echidna.Coverage (ePropertySeqCoverage, getCover)
import Echidna.Exec
import Echidna.Solidity
import Hedgehog hiding (checkParallel)
import Hedgehog.Internal.Property (GroupName(..), PropertyName(..))
import Options.Applicative
import Echidna.Campaign
import Echidna.Transaction
import Echidna.UI
data Options = Options
{ filePath :: FilePath
@ -32,62 +20,27 @@ data Options = Options
}
options :: Parser Options
options = Options
<$> argument str
( metavar "FILE"
<> help "Solidity file to analyze" )
<*> optional ( argument str
( metavar "CONTRACT"
<> help "Contract inside of file to analyze" ))
<*> switch
( long "coverage"
<> help "Turn on coverage")
<*> optional ( option str
( long "config"
<> help "Echidna config file" ))
options = Options <$> argument str (metavar "FILE"
<> help "Solidity file to analyze")
<*> optional (argument str $ metavar "CONTRACT"
<> help "Contract to analyze")
<*> switch (long "coverage"
<> help "Turn on coverage")
<*> optional (option str $ long "config"
<> help "Config file")
opts :: ParserInfo Options
opts = info (options <**> helper)
( fullDesc
<> progDesc "Fuzzing/property based testing of EVM code"
<> header "Echidna - Ethereum fuzz testing framework" )
opts = info (options <**> helper) $ fullDesc
<> progDesc "EVM property-based testing framework"
<> header "Echidna"
main :: IO ()
main = do
-- Read cmd line options and load config
(Options file contract usecov configFile) <- execParser opts
config <- maybe (pure defaultConfig) parseConfig configFile
let f = checkTest (config ^. returnType)
checkGroup = if config ^. outputJson
then
checkParallelJson
else
checkParallel
flip runReaderT config $ do
-- Load solidity contract and get VM
(v,a,ts) <- loadSolidity file (pack <$> contract)
if null ts
then throwM NoTests
else pure ()
if not $ usecov || config ^. printCoverage
-- Run without coverage
then do
let prop t = ePropertySeq (`f` t) a v >>= \x -> return (PropertyName $ show t, x)
_ <- checkGroup . Group (GroupName file) =<< mapM prop ts
return ()
-- Run with coverage
else do
tests <- liftIO $ mapM (\t -> fmap (t,) (newMVar [])) ts
let prop (cov,t,mvar) =
ePropertySeqCoverage cov mvar (`f` t) a v >>= \x -> return (PropertyName $ show t, x)
replicateM_ (config ^. epochs) $ do
xs <- liftIO $ forM tests $ \(x,y) -> swapMVar y [] <&> (, x, y) . getCover
checkGroup . Group (GroupName file) =<< mapM prop xs
ls <- liftIO $ mapM (readMVar . snd) tests
let ci = foldl' (\acc xs -> unions (acc : map snd xs)) mempty ls
liftIO . putStrLn $ "Coverage: " ++ show (size ci) ++ " unique PC's"
main = do (Options f c cov conf) <- execParser opts
cfg <- maybe (pure defaultConfig) parseConfig conf
flip runReaderT (cfg & cConf %~ (if cov then \k -> k {knownCoverage = Just mempty}
else id)) $ do
(v,a,ts) <- loadSolidity f (pack <$> c)
let r = v ^. state . contract
let w = World [0] [(r, a)]
let ts' = zip ts $ repeat r
ui v w ts' >> pure ()

@ -17,3 +17,9 @@ extra-deps:
- text-format-0.3.2
- tree-view-0.5
- Unixutils-1.54.1
extra-include-dirs:
- /usr/local/opt/readline/include
extra-lib-dirs:
- /usr/local/opt/readline/lib

Loading…
Cancel
Save