-
Notifications
You must be signed in to change notification settings - Fork 393
/
Copy pathSource.hs
221 lines (203 loc) · 8.72 KB
/
Source.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
{-# LANGUAGE ViewPatterns #-}
module Echidna.Output.Source where
import Prelude hiding (writeFile)
import Control.Monad (unless)
import Data.ByteString qualified as BS
import Data.Foldable
import Data.List (nub, sort)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Sequence qualified as Seq
import Data.Set qualified as S
import Data.Text (Text, pack)
import Data.Text qualified as T
import Data.Text.Lazy (toStrict)
import Data.Text.Lazy.Builder (fromString, fromText, singleton, toLazyText)
import Data.Text.Encoding (decodeUtf8)
import Data.Text.IO (writeFile)
import Data.Vector qualified as V
import Data.Vector.Unboxed qualified as VU
import HTMLEntities.Text qualified as HTML
import System.Directory (createDirectoryIfMissing)
import System.FilePath ((</>))
import Text.Printf (printf)
import EVM.Dapp (srcMapCodePos, DappInfo(..))
import EVM.Solidity (SourceCache(..), SrcMap, SolcContract(..))
import Echidna.Types.Campaign (CampaignConf(..))
import Echidna.Types.Config (Env(..), EConfig(..))
import Echidna.Types.Coverage (OpIx, unpackTxResults, FrozenCoverageMap, CoverageFileType (..), mergeCoverageMaps)
import Echidna.Types.Tx (TxResult(..))
import Echidna.SourceAnalysis.Slither (AssertLocation(..), assertLocationList, SlitherInfo(..))
saveCoverages
:: Env
-> Int
-> FilePath
-> SourceCache
-> [SolcContract]
-> IO ()
saveCoverages env seed d sc cs = do
let fileTypes = env.cfg.campaignConf.coverageFormats
coverage <- mergeCoverageMaps env.dapp env.coverageRefInit env.coverageRefRuntime
mapM_ (\ty -> saveCoverage ty seed d sc cs coverage) fileTypes
saveCoverage
:: CoverageFileType
-> Int
-> FilePath
-> SourceCache
-> [SolcContract]
-> FrozenCoverageMap
-> IO ()
saveCoverage fileType seed d sc cs covMap = do
let extension = coverageFileExtension fileType
fn = d </> "covered." <> show seed <> extension
cc = ppCoveredCode fileType sc cs covMap
createDirectoryIfMissing True d
writeFile fn cc
coverageFileExtension :: CoverageFileType -> String
coverageFileExtension Lcov = ".lcov"
coverageFileExtension Html = ".html"
coverageFileExtension Txt = ".txt"
-- | Pretty-print the covered code
ppCoveredCode :: CoverageFileType -> SourceCache -> [SolcContract] -> FrozenCoverageMap -> Text
ppCoveredCode fileType sc cs s | null s = "Coverage map is empty"
| otherwise =
let
-- List of covered lines during the fuzzing campaign
covLines = srcMapCov sc s cs
-- Collect all the possible lines from all the files
allFiles = (\(path, src) -> (path, V.fromList (decodeUtf8 <$> BS.split 0xa src))) <$> Map.elems sc.files
-- Excludes lines such as comments or blanks
runtimeLinesMap = buildRuntimeLinesMap sc cs
-- Pretty print individual file coverage
ppFile (srcPath, srcLines) =
let runtimeLines = fromMaybe mempty $ Map.lookup srcPath runtimeLinesMap
marked = markLines fileType srcLines runtimeLines (fromMaybe Map.empty (Map.lookup srcPath covLines))
in changeFileName srcPath <> lazyunlines fromText (changeFileLines (V.toList marked))
topHeader = case fileType of
Lcov -> "TN:\n"
Html -> "<style> code { white-space: pre-wrap; display: block; background-color: #eee; }" <>
".executed { background-color: #afa; }" <>
".reverted { background-color: #ffa; }" <>
".unexecuted { background-color: #faa; }" <>
".neutral { background-color: #eee; }" <>
"</style>"
Txt -> ""
-- ^ Text to add to top of the file
changeFileName (T.pack -> fn) = case fileType of
Lcov -> fromString "SF:" <> fromText fn
Html -> fromString "<b>" <> fromText (HTML.text fn) <> fromString "</b>"
Txt -> fromText fn
-- ^ Alter file name, in the case of html turning it into bold text
changeFileLines ls = case fileType of
Lcov -> ls ++ ["end_of_record"]
Html -> "<code>" : ls ++ ["", "</code>","<br />"]
Txt -> ls
-- ^ Alter file contents, in the case of html encasing it in <code> and adding a line break
lazyunlines conv l = foldr (\x r -> conv x <> singleton '\n' <> r) mempty l <> singleton '\n'
in toStrict $ toLazyText $ fromString topHeader <> lazyunlines id (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 fileType codeLines runtimeLines resultMap =
V.map markLine . V.filter shouldUseLine $ V.indexed codeLines
where
shouldUseLine (i, _) = case fileType of
Lcov -> i + 1 `elem` runtimeLines
_ -> True
markLine (i, codeLine) =
let n = i + 1
results = fromMaybe [] (Map.lookup n resultMap)
markers = sort $ nub $ getMarker <$> results
wrapLine :: Text -> Text
wrapLine line = case fileType of
Html -> "<span class='" <> cssClass <> "'>" <>
HTML.text line <>
"</span>"
_ -> line
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)
in result
lineNrSpan = length . show $ V.length codeLines + 1
getCSSClass :: String -> Text
getCSSClass markers =
case markers of
[] -> "unexecuted"
_ | '*' `elem` markers -> "executed"
_ -> "reverted"
-- | Select the proper marker, according to the result of the transaction
getMarker :: TxResult -> Char
getMarker ReturnTrue = '*'
getMarker ReturnFalse = '*'
getMarker Stop = '*'
getMarker ErrorRevert = 'r'
getMarker ErrorOutOfGas = 'o'
getMarker _ = 'e'
-- | Given a source cache, a coverage map, a contract returns a list of covered lines
srcMapCov :: SourceCache -> FrozenCoverageMap -> [SolcContract] -> Map FilePath (Map Int [TxResult])
srcMapCov sc covMap contracts =
Map.unionsWith Map.union $ linesCovered <$> contracts
where
linesCovered :: SolcContract -> Map FilePath (Map Int [TxResult])
linesCovered c =
case Map.lookup c.runtimeCodehash covMap of
Just vec -> VU.foldl' (\acc covInfo -> case covInfo of
(-1, _, _) -> acc -- not covered
(opIx, _stackDepths, txResults) ->
case srcMapForOpLocation c opIx of
Just srcMap ->
case srcMapCodePos sc srcMap of
Just (file, line) ->
Map.alter
(Just . innerUpdate . fromMaybe mempty)
file
acc
where
innerUpdate =
Map.alter
(Just . (<> unpackTxResults txResults) . fromMaybe mempty)
line
Nothing -> acc
Nothing -> acc
) mempty vec
Nothing -> mempty
-- | Given a contract, and tuple as coverage, return the corresponding mapped line (if any)
srcMapForOpLocation :: SolcContract -> OpIx -> Maybe SrcMap
srcMapForOpLocation contract opIx =
Seq.lookup opIx (contract.runtimeSrcmap <> contract.creationSrcmap)
-- | Builds a Map from file paths to lines that can be executed, this excludes
-- for example lines with comments
buildRuntimeLinesMap :: SourceCache -> [SolcContract] -> Map FilePath (S.Set Int)
buildRuntimeLinesMap sc contracts =
Map.fromListWith (<>)
[(k, S.singleton v) | (k, v) <- mapMaybe (srcMapCodePos sc) srcMaps]
where
srcMaps = concatMap
(\c -> toList $ c.runtimeSrcmap <> c.creationSrcmap) contracts
-- | Check that all assertions were hit, and log a warning if they weren't
checkAssertionsCoverage
:: SourceCache
-> Env
-> IO ()
checkAssertionsCoverage sc env = do
covMap <- mergeCoverageMaps env.dapp env.coverageRefInit env.coverageRefRuntime
let
cs = Map.elems env.dapp.solcByName
asserts = maybe [] (concatMap assertLocationList . Map.elems . (.asserts)) env.slitherInfo
covLines = srcMapCov sc covMap cs
mapM_ (checkAssertionReached covLines) asserts
-- | Helper function for `checkAssertionsCoverage` which checks a single assertion
-- and logs a warning if it wasn't hit
checkAssertionReached :: Map String (Map Int [TxResult]) -> AssertLocation -> IO ()
checkAssertionReached covLines assert =
maybe
warnAssertNotReached checkCoverage
(Map.lookup assert.filenameAbsolute covLines)
where
checkCoverage coverage = let lineNumbers = Map.keys coverage in
unless ((head assert.assertLines) `elem` lineNumbers) warnAssertNotReached
warnAssertNotReached =
putStrLn $ "WARNING: assertion at file: " ++ assert.filenameRelative
++ " starting at line: " ++ show (head assert.assertLines) ++ " was never reached"