Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save traces for every transaction in reproducer #1180

Merged
merged 1 commit into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/Echidna/Campaign.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Control.Monad.Trans (lift)
import Data.Binary.Get (runGetOrFail)
import Data.ByteString.Lazy qualified as LBS
import Data.IORef (readIORef, atomicModifyIORef')
import Data.List qualified as List
import Data.Map qualified as Map
import Data.Map (Map, (\\))
import Data.Maybe (isJust, mapMaybe, fromMaybe)
Expand Down Expand Up @@ -47,7 +48,6 @@ import Echidna.Types.Test qualified as Test
import Echidna.Types.Tx (TxCall(..), Tx(..), call)
import Echidna.Types.World (World)
import Echidna.Utility (getTimestamp)
import qualified Data.List as List

instance MonadThrow m => MonadThrow (RandT g m) where
throwM = lift . throwM
Expand Down
20 changes: 20 additions & 0 deletions lib/Echidna/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,23 @@ checkOverflowTest :: DappInfo -> VM RealWorld-> TestValue
checkOverflowTest dappInfo vm =
let es = extractEvents False dappInfo vm
in BoolValue $ null es || not (checkPanicEvent "17" es)

-- | Reproduce a test saving VM snapshot after every transaction
reproduceTest
:: (MonadIO m, MonadThrow m, MonadReader Env m)
=> VM RealWorld -- ^ Initial VM
-> EchidnaTest
-> m ([(Tx, VM RealWorld)], VM RealWorld)
reproduceTest vm0 test = do
let txs = test.reproducer
(results, vm) <- go vm0 [] txs
(_, vm') <- checkETest test vm
pure (results, vm')
where
go vm executedSoFar toExecute =
case toExecute of
[] -> pure ([], vm)
tx:remainingTxs -> do
(_, vm') <- execTx vm tx
(remaining, _) <- go vm' (tx:executedSoFar) remainingTxs
pure ((tx, vm') : remaining, vm')
27 changes: 27 additions & 0 deletions lib/Echidna/UI/Report.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Echidna.UI.Report where

import Control.Monad (forM)
import Control.Monad.Reader (MonadReader, MonadIO (liftIO), asks)
import Control.Monad.ST (RealWorld)
import Data.IORef (readIORef)
Expand Down Expand Up @@ -111,6 +112,23 @@ ppFail b vm xs = do
<> unlines ((" " <>) <$> prettyTxs) <> "\n"
<> "Traces: \n" <> T.unpack (showTraceTree dappInfo vm)

-- | Pretty-print the status of a solved test.
ppFailWithTraces :: MonadReader Env m => Maybe (Int, Int) -> VM RealWorld -> [(Tx, VM RealWorld)] -> m String
ppFailWithTraces _ _ [] = pure "failed with no transactions made ⁉️ "
ppFailWithTraces b finalVM results = do
dappInfo <- asks (.dapp)
let xs = fst <$> results
let status = case b of
Nothing -> ""
Just (n,m) -> ", shrinking " <> progress n m
let printName = length (nub $ (.src) <$> xs) /= 1
prettyTxs <- forM results $ \(tx, vm) -> do
txPrinted <- ppTx printName tx
pure $ txPrinted <> "\nTraces:\n" <> T.unpack (showTraceTree dappInfo vm)
pure $ "failed!💥 \n Call sequence" <> status <> ":\n"
<> unlines ((" " <>) <$> prettyTxs) <> "\n"
<> "Test traces: \n" <> T.unpack (showTraceTree dappInfo finalVM)

-- | Pretty-print the status of a test.

ppTS :: MonadReader Env m => TestState -> VM RealWorld -> [Tx] -> m String
Expand Down Expand Up @@ -166,6 +184,15 @@ ppTests tests = do
pure $ Just (T.unpack n <> ": max value: " <> show t.value <> "\n" <> status)
Exploration -> pure Nothing

ppTestName :: EchidnaTest -> String
ppTestName t =
case t.testType of
PropertyTest n _ -> T.unpack n
CallTest n _ -> T.unpack n
AssertionTest _ s _ -> T.unpack (encodeSig s)
OptimizationTest n _ -> T.unpack n <> ": max value: " <> show t.value
Exploration -> "<exploration>"

-- | Given a number of boxes checked and a number of total boxes, pretty-print
-- progress in box-checking.
progress :: Int -> Int -> String
Expand Down
28 changes: 23 additions & 5 deletions src/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

module Main where

import Control.Monad (unless, forM_)
import Control.Monad.Reader (runReaderT)
import Control.Monad (unless, forM_, when)
import Control.Monad.Reader (runReaderT, liftIO)
import Control.Monad.Random (getRandomR)
import Data.Aeson.Key qualified as Aeson.Key
import Data.Function ((&))
import Data.Hashable (hash)
import Data.IORef (readIORef)
import Data.List.NonEmpty qualified as NE
import Data.Map qualified as Map
import Data.Maybe (fromMaybe)
import Data.Maybe (fromMaybe, isJust)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Time.Clock.System (getSystemTime, systemSeconds)
Expand All @@ -19,8 +20,10 @@ import Data.Word (Word8, Word16)
import Main.Utf8 (withUtf8)
import Options.Applicative
import Paths_echidna (version)
import System.Directory (createDirectoryIfMissing)
import System.Environment (lookupEnv)
import System.Exit (exitWith, exitSuccess, ExitCode(..))
import System.FilePath ((</>))
import System.FilePath ((</>), (<.>))
import System.IO (hPutStrLn, stderr)
import System.IO.CodePage (withCP65001)

Expand All @@ -35,12 +38,13 @@ import Echidna.Onchain qualified as Onchain
import Echidna.Output.Corpus
import Echidna.Output.Source
import Echidna.Solidity (compileContracts)
import Echidna.Test (validateTestMode)
import Echidna.Test (reproduceTest, validateTestMode)
import Echidna.Types.Campaign
import Echidna.Types.Config
import Echidna.Types.Solidity
import Echidna.Types.Test (TestMode, EchidnaTest(..))
import Echidna.UI
import Echidna.UI.Report (ppFailWithTraces, ppTestName)
import Echidna.Utility (measureIO)

main :: IO ()
Expand Down Expand Up @@ -76,6 +80,20 @@ main = withUtf8 $ withCP65001 $ do
Just dir -> do
measureIO cfg.solConf.quiet "Saving test reproducers" $
saveTxs (dir </> "reproducers") (filter (not . null) $ (.reproducer) <$> tests)

saveTracesEnabled <- lookupEnv "ECHIDNA_SAVE_TRACES"
when (isJust saveTracesEnabled) $ do
measureIO cfg.solConf.quiet "Saving test reproducers-traces" $ do
flip runReaderT env $ do
forM_ tests $ \test ->
unless (null test.reproducer) $ do
(results, finalVM) <- reproduceTest vm test
let subdir = dir </> "reproducers-traces"
liftIO $ createDirectoryIfMissing True subdir
let file = subdir </> (show . abs . hash . show) test.reproducer <.> "txt"
txsPrinted <- ppFailWithTraces Nothing finalVM results
liftIO $ writeFile file (ppTestName test <> ": " <> txsPrinted)

measureIO cfg.solConf.quiet "Saving corpus" $ do
corpus <- readIORef env.corpusRef
saveTxs (dir </> "coverage") (snd <$> Set.toList corpus)
Expand Down