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

coverage: count number of executions per line #1265

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/Echidna.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.TLS.GHC (mkTLS)
import System.FilePath ((</>))

import EVM (cheatCode)
Expand Down Expand Up @@ -119,6 +120,7 @@ mkEnv cfg buildOutput tests world = do
chainId <- maybe (pure Nothing) EVM.Fetch.fetchChainIdFrom cfg.rpcUrl
eventQueue <- newChan
coverageRef <- newIORef mempty
statsRef <- mkTLS $ newIORef mempty
corpusRef <- newIORef mempty
testRefs <- traverse newIORef tests
(contractCache, slotCache) <- Onchain.loadRpcCache cfg
Expand All @@ -127,5 +129,5 @@ mkEnv cfg buildOutput tests world = do
-- TODO put in real path
let dapp = dappInfo "/" buildOutput
pure $ Env { cfg, dapp, codehashMap, fetchContractCache, fetchSlotCache
, chainId, eventQueue, coverageRef, corpusRef, testRefs, world
, chainId, eventQueue, coverageRef, statsRef, corpusRef, testRefs, world
}
16 changes: 13 additions & 3 deletions lib/Echidna/Exec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Data.IORef (readIORef, atomicWriteIORef, newIORef, writeIORef, modifyIORe
import Data.Map qualified as Map
import Data.Maybe (fromMaybe, fromJust)
import Data.Text qualified as T
import Data.TLS.GHC (getTLS)
import Data.Vector qualified as V
import Data.Vector.Unboxed.Mutable qualified as VMut
import System.Process (readProcessWithExitCode)
Expand Down Expand Up @@ -296,6 +297,15 @@ execTxWithCov tx = do
forM_ [0..size-1] $ \i -> VMut.write vec i (-1, 0, 0)
pure $ Just vec

statsRef <- getTLS env.statsRef
maybeStatsVec <- lookupUsingCodehashOrInsert env.codehashMap contract env.dapp statsRef $ do
let size = BS.length . forceBuf . fromJust . view bytecode $ contract
if size == 0 then pure Nothing else do
-- IO for making a new vec
vec <- VMut.new size
elopez marked this conversation as resolved.
Show resolved Hide resolved
forM_ [0..size-1] $ \i -> VMut.write vec i (0, 0)
pure $ Just vec

case maybeCovVec of
Nothing -> pure ()
Just vec -> do
Expand All @@ -304,13 +314,13 @@ execTxWithCov tx = do
-- bug in another place, investigate.
-- ... this should be fixed now, since we use `codeContract` instead
-- of `contract` for everything; it may be safe to remove this check.
when (pc < VMut.length vec) $
when (pc < VMut.length vec) $ do
VMut.modify (fromJust maybeStatsVec) (\(execQty, revertQty) -> (execQty + 1, revertQty)) opIx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see anywhere where we increment revertQty

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I have not implemented counting of reverts. I meant to do it at first but then it wasn't clear to me where I should be counting them. If you have any pointers there I'd appreciate them, otherwise I may remove the extra var at this time.

VMut.read vec pc >>= \case
(_, depths, results) | depth < 64 && not (depths `testBit` depth) -> do
VMut.write vec pc (opIx, depths `setBit` depth, results `setBit` fromEnum Stop)
writeIORef covContextRef (True, Just (vec, pc))
_ ->
modifyIORef' covContextRef $ \(new, _) -> (new, Just (vec, pc))
_ -> modifyIORef' covContextRef $ \(new, _) -> (new, Just (vec, pc))

-- | Get the VM's current execution location
currentCovLoc vm = (vm.state.pc, fromMaybe 0 $ vmOpIx vm, length vm.frames)
Expand Down
67 changes: 50 additions & 17 deletions lib/Echidna/Output/Source.hs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE ParallelListComp #-}

module Echidna.Output.Source where

import Prelude hiding (writeFile)

import Data.ByteString qualified as BS
import Data.Foldable
import Data.IORef (readIORef)
import Data.IORef (readIORef, IORef)
import Data.List (nub, sort)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Maybe (fromMaybe, mapMaybe, isJust, fromJust)
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Sequence qualified as Seq
Expand All @@ -17,7 +18,9 @@ import Data.Text (Text, pack)
import Data.Text qualified as T
import Data.Text.Encoding (decodeUtf8)
import Data.Text.IO (writeFile)
import Data.TLS.GHC (allTLS, TLS)
import Data.Vector qualified as V
import qualified Data.Vector.Unboxed as U
import Data.Vector.Unboxed.Mutable qualified as VU
import HTMLEntities.Text qualified as HTML
import System.Directory (createDirectoryIfMissing)
Expand All @@ -29,8 +32,25 @@ import EVM.Solidity (SourceCache(..), SrcMap, SolcContract(..))

import Echidna.Types.Campaign (CampaignConf(..))
import Echidna.Types.Config (Env(..), EConfig(..))
import Echidna.Types.Coverage (OpIx, unpackTxResults, CoverageMap, CoverageFileType (..))
import Echidna.Types.Coverage (OpIx, unpackTxResults, CoverageMap, CoverageFileType (..), ExecQty, StatsMap, StatsMapV, StatsInfo)
import Echidna.Types.Tx (TxResult(..))
import EVM.Types (W256)

zipSumStats :: IO [StatsInfo] -> IO [StatsInfo] -> IO [StatsInfo]
zipSumStats v1 v2 = do
vec1 <- v1
vec2 <- v2
return [(exec1 + exec2, revert1 + revert2) | (exec1, revert1) <- vec1 | (exec2, revert2) <- vec2]

mvToList :: (VU.Unbox a) => VU.IOVector a -> IO [a]
mvToList = fmap U.toList . U.freeze

combineStats :: TLS (IORef StatsMap) -> IO StatsMapV
combineStats statsRef = do
threadStats' <- allTLS statsRef
threadStats <- mapM readIORef threadStats' :: IO [StatsMap]
statsLists <- pure $ map (\(m :: StatsMap) -> Map.map (\(x :: VU.IOVector StatsInfo) -> mvToList x) m) threadStats :: IO [Map EVM.Types.W256 (IO [StatsInfo])]
elopez marked this conversation as resolved.
Show resolved Hide resolved
traverse (U.fromList <$>) $ Map.unionsWith zipSumStats statsLists

saveCoverages
:: Env
Expand All @@ -42,7 +62,8 @@ saveCoverages
saveCoverages env seed d sc cs = do
let fileTypes = env.cfg.campaignConf.coverageFormats
coverage <- readIORef env.coverageRef
mapM_ (\ty -> saveCoverage ty seed d sc cs coverage) fileTypes
stats <- combineStats env.statsRef
mapM_ (\ty -> saveCoverage ty seed d sc cs coverage stats) fileTypes

saveCoverage
:: CoverageFileType
Expand All @@ -51,11 +72,12 @@ saveCoverage
-> SourceCache
-> [SolcContract]
-> CoverageMap
-> StatsMapV
-> IO ()
saveCoverage fileType seed d sc cs covMap = do
saveCoverage fileType seed d sc cs covMap statMap = do
let extension = coverageFileExtension fileType
fn = d </> "covered." <> show seed <> extension
cc <- ppCoveredCode fileType sc cs covMap
cc <- ppCoveredCode fileType sc cs covMap statMap
createDirectoryIfMissing True d
writeFile fn cc

Expand All @@ -65,11 +87,11 @@ coverageFileExtension Html = ".html"
coverageFileExtension Txt = ".txt"

-- | Pretty-print the covered code
ppCoveredCode :: CoverageFileType -> SourceCache -> [SolcContract] -> CoverageMap -> IO Text
ppCoveredCode fileType sc cs s | null s = pure "Coverage map is empty"
ppCoveredCode :: CoverageFileType -> SourceCache -> [SolcContract] -> CoverageMap -> StatsMapV -> IO Text
ppCoveredCode fileType sc cs s sm | null s = pure "Coverage map is empty"
| otherwise = do
-- List of covered lines during the fuzzing campaign
covLines <- srcMapCov sc s cs
covLines <- srcMapCov sc s sm cs
let
-- Collect all the possible lines from all the files
allFiles = (\(path, src) -> (path, V.fromList (decodeUtf8 <$> BS.split 0xa src))) <$> Map.elems sc.files
Expand Down Expand Up @@ -103,7 +125,7 @@ ppCoveredCode fileType sc cs s | null s = pure "Coverage map is empty"
pure $ topHeader <> T.unlines (map ppFile allFiles)

-- | Mark one particular line, from a list of lines, keeping the order of them
markLines :: CoverageFileType -> V.Vector Text -> S.Set Int -> Map Int [TxResult] -> V.Vector Text
markLines :: CoverageFileType -> V.Vector Text -> S.Set Int -> Map Int ([TxResult], ExecQty) -> V.Vector Text
elopez marked this conversation as resolved.
Show resolved Hide resolved
markLines fileType codeLines runtimeLines resultMap =
V.map markLine . V.filter shouldUseLine $ V.indexed codeLines
where
Expand All @@ -112,7 +134,7 @@ markLines fileType codeLines runtimeLines resultMap =
_ -> True
markLine (i, codeLine) =
let n = i + 1
results = fromMaybe [] (Map.lookup n resultMap)
(results, execs) = fromMaybe ([], 0) (Map.lookup n resultMap)
markers = sort $ nub $ getMarker <$> results
wrapLine :: Text -> Text
wrapLine line = case fileType of
Expand All @@ -123,11 +145,16 @@ markLines fileType codeLines runtimeLines resultMap =
where
cssClass = if n `elem` runtimeLines then getCSSClass markers else "neutral"
result = case fileType of
Lcov -> pack $ printf "DA:%d,%d" n (length results)
_ -> pack $ printf " %*d | %-4s| %s" lineNrSpan n markers (wrapLine codeLine)
Lcov -> pack $ printf "DA:%d,%d" n execs
Html -> pack $ printf "%*d | %4s | %-4s| %s" lineNrSpan n (prettyExecs execs) markers (wrapLine codeLine)
_ -> pack $ printf "%*d | %-4s| %s" lineNrSpan n markers (wrapLine codeLine)

in result
lineNrSpan = length . show $ V.length codeLines + 1
prettyExecs x = prettyExecs' x 0
prettyExecs' x n | x >= 1000 = prettyExecs' (x `div` 1000) (n + 1)
| x < 1000 && n == 0 = show x
| otherwise = show x <> [" kMGTPEZY" !! n]

getCSSClass :: String -> Text
getCSSClass markers =
Expand All @@ -146,11 +173,11 @@ getMarker ErrorOutOfGas = 'o'
getMarker _ = 'e'

-- | Given a source cache, a coverage map, a contract returns a list of covered lines
srcMapCov :: SourceCache -> CoverageMap -> [SolcContract] -> IO (Map FilePath (Map Int [TxResult]))
srcMapCov sc covMap contracts = do
srcMapCov :: SourceCache -> CoverageMap -> StatsMapV -> [SolcContract] -> IO (Map FilePath (Map Int ([TxResult], ExecQty)))
srcMapCov sc covMap statMap contracts = do
Map.unionsWith Map.union <$> mapM linesCovered contracts
where
linesCovered :: SolcContract -> IO (Map FilePath (Map Int [TxResult]))
linesCovered :: SolcContract -> IO (Map FilePath (Map Int ([TxResult], ExecQty)))
linesCovered c =
case Map.lookup c.runtimeCodehash covMap of
Just vec -> VU.foldl' (\acc covInfo -> case covInfo of
Expand All @@ -167,8 +194,14 @@ srcMapCov sc covMap contracts = do
where
innerUpdate =
Map.alter
(Just . (<> unpackTxResults txResults) . fromMaybe mempty)
updateLine
line
updateLine (Just (r, q)) = Just ((<> unpackTxResults txResults) r, max q execQty)
updateLine Nothing = Just (unpackTxResults txResults, execQty)
fileStats = Map.lookup c.runtimeCodehash statMap
idxStats | isJust fileStats = fromJust fileStats U.! opIx
| otherwise = (0, 0)
elopez marked this conversation as resolved.
Show resolved Hide resolved
execQty = fst idxStats
Nothing -> acc
Nothing -> acc
) mempty vec
Expand Down
4 changes: 3 additions & 1 deletion lib/Echidna/Types/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import Data.Set (Set)
import Data.Text (Text)
import Data.Time (LocalTime)
import Data.Word (Word64)
import Data.TLS.GHC

import EVM.Dapp (DappInfo)
import EVM.Types (Addr, Contract, W256)

import Echidna.SourceMapping (CodehashMap)
import Echidna.Types.Campaign (CampaignConf, CampaignEvent)
import Echidna.Types.Corpus (Corpus)
import Echidna.Types.Coverage (CoverageMap)
import Echidna.Types.Coverage (CoverageMap, StatsMap)
import Echidna.Types.Solidity (SolConf)
import Echidna.Types.Test (TestConf, EchidnaTest)
import Echidna.Types.Tx (TxConf)
Expand Down Expand Up @@ -71,6 +72,7 @@ data Env = Env

, testRefs :: [IORef EchidnaTest]
, coverageRef :: IORef CoverageMap
, statsRef :: TLS (IORef StatsMap)
, corpusRef :: IORef Corpus

, codehashMap :: CodehashMap
Expand Down
18 changes: 18 additions & 0 deletions lib/Echidna/Types/Coverage.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Data.List (foldl')
import Data.Map qualified as Map
import Data.Map.Strict (Map)
import Data.Text (toLower)
import Data.Vector.Unboxed (Vector)
import Data.Vector.Unboxed.Mutable (IOVector)
import Data.Vector.Unboxed.Mutable qualified as V
import Data.Word (Word64)
Expand All @@ -17,9 +18,20 @@ import Echidna.Types.Tx (TxResult)
-- Indexed by contracts' compile-time codehash; see `CodehashMap`.
type CoverageMap = Map W256 (IOVector CoverageInfo)

-- | Map with the statistic information needed for source code printing.
-- Indexed by contracts' compile-time codehash; see `CodehashMap`.
type StatsMap = Map W256 (IOVector StatsInfo)

-- | Map with the statistic information needed for source code printing.
-- Indexed by contracts' compile-time codehash; see `CodehashMap`.
elopez marked this conversation as resolved.
Show resolved Hide resolved
type StatsMapV = Map W256 (Vector StatsInfo)

-- | Basic coverage information
type CoverageInfo = (OpIx, StackDepths, TxResults)

-- | Basic stats information
type StatsInfo = (ExecQty, RevertQty)

-- | Index per operation in the source code, obtained from the source mapping
type OpIx = Int

Expand All @@ -29,6 +41,12 @@ type StackDepths = Word64
-- | Packed TxResults used for coverage, corresponding bits are set
type TxResults = Word64

-- | Hit count
type ExecQty = Word64

-- | Revert count
type RevertQty = Word64

-- | Given good point coverage, count the number of unique points but
-- only considering the different instruction PCs (discarding the TxResult).
-- This is useful to report a coverage measure to the user
Expand Down
1 change: 1 addition & 0 deletions package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies:
- semver
- split
- text
- thread-local-storage
- transformers
- time
- unliftio
Expand Down
Loading