Skip to content

Commit 1bd1ba4

Browse files
authored
Merge pull request #645 from ethereum/better-results-printing
Better results printing, WARNING-s make the check FAIL now.
2 parents 5f48aaa + 4d82711 commit 1bd1ba4

File tree

8 files changed

+152
-116
lines changed

8 files changed

+152
-116
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
- Dumping of END states (.prop) files is now default for `--debug`
3232
- When cheatcode is missing, we produce a partial execution warning
3333

34+
## Changed
35+
- Warnings now lead printing FAIL. This way, users don't accidentally think that
36+
their contract is correct when there were cases/branches that hevm could not
37+
fully explore. Printing of issues is also now much more organized
38+
3439
## [0.54.2] - 2024-12-12
3540

3641
## Fixed

cli/cli.hs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ main = withUtf8 $ do
243243
-- TODO: which functions here actually require a BuildOutput, and which can take it as a Maybe?
244244
testOpts <- liftIO $ unitTestOptions cmd solvers (Just out)
245245
res <- unitTest testOpts out.contracts
246-
liftIO $ unless res exitFailure
246+
liftIO $ unless (uncurry (&&) res) exitFailure
247247

248248
equivalence :: App m => Command Options.Unwrapped -> m ()
249249
equivalence cmd = do
@@ -269,15 +269,16 @@ equivalence cmd = do
269269
cores <- liftIO $ unsafeInto <$> getNumProcessors
270270
let solverCount = fromMaybe cores cmd.numSolvers
271271
withSolvers solver solverCount (fromMaybe 1 cmd.solverThreads) cmd.smttimeout $ \s -> do
272-
res <- equivalenceCheck s bytecodeA bytecodeB veriOpts calldata
272+
(res, e) <- equivalenceCheck s bytecodeA bytecodeB veriOpts calldata
273+
liftIO $ case (any isCex res, any Expr.isPartial e || any isUnknown res) of
274+
(False, False) -> putStrLn " \x1b[32m[PASS]\x1b[0m Contracts behave equivalently"
275+
(True, _) -> putStrLn " \x1b[31m[FAIL]\x1b[0m Contracts do not behave equivalently"
276+
(_, True) -> putStrLn " \x1b[31m[FAIL]\x1b[0m Contracts may not behave equivalently"
277+
liftIO $ printWarnings e res "the contracts under test"
273278
case any isCex res of
274279
False -> liftIO $ do
280+
when (any isUnknown res || any isError res || any isPartial e) exitFailure
275281
putStrLn "No discrepancies found"
276-
when (any isUnknown res || any isError res) $ do
277-
putStrLn "But the following issues occurred:"
278-
forM_ (groupIssues (filter isError res)) $ \(num, str) -> putStrLn $ " " <> show num <> "x -> " <> str
279-
forM_ (groupIssues (filter isUnknown res)) $ \(num, str) -> putStrLn $ " " <> show num <> "x -> " <> str
280-
exitFailure
281282
True -> liftIO $ do
282283
let cexs = mapMaybe getCex res
283284
T.putStrLn . T.unlines $

src/EVM/Format.hs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module EVM.Format
44
( formatExpr
55
, formatSomeExpr
66
, formatPartial
7+
, formatPartialShort
78
, formatProp
89
, contractNamePart
910
, contractPathPart
@@ -483,6 +484,13 @@ formatPartial = \case
483484
, "function selector: " <> T.pack (show selector)
484485
]
485486

487+
formatPartialShort :: PartialExec -> Text
488+
formatPartialShort = \case
489+
UnexpectedSymbolicArg _ opcode _ _ -> "Unexpected symbolic arguments to opcode: " <> T.pack opcode
490+
MaxIterationsReached {} -> "Max iterations reached"
491+
JumpIntoSymbolicCode {} -> "Encountered a jump into a potentially symbolic code region while executing initcode"
492+
CheatCodeMissing _ selector -> "Cheat code not recognized: " <> T.pack (show selector)
493+
486494
formatSomeExpr :: SomeExpr -> Text
487495
formatSomeExpr (SomeExpr e) = formatExpr $ Expr.simplify e
488496

src/EVM/SymExec.hs

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import EVM.ABI
3737
import EVM.Effects
3838
import EVM.Expr qualified as Expr
3939
import EVM.FeeSchedule (feeSchedule)
40-
import EVM.Format (formatExpr, formatPartial, showVal, bsToHex, indent, formatBinary)
40+
import EVM.Format (formatExpr, formatPartial, formatPartialShort, showVal, bsToHex, indent, formatBinary)
4141
import EVM.SMT (SMTCex(..), SMT2(..), assertProps)
4242
import EVM.SMT qualified as SMT
4343
import EVM.Solvers
@@ -89,6 +89,15 @@ groupIssues results = map (\g -> (into (length g), NE.head g)) grouped
8989
sorted = sort $ map getErr results
9090
grouped = NE.group sorted
9191

92+
groupPartials :: [Expr End] -> [(Integer, String)]
93+
groupPartials e = map (\g -> (into (length g), NE.head g)) grouped
94+
where
95+
getErr :: Expr End -> String
96+
getErr (Partial _ _ reason) = T.unpack $ formatPartialShort reason
97+
getErr _ = internalError "shouldn't happen"
98+
sorted = sort $ map getErr (filter isPartial e)
99+
grouped = NE.group sorted
100+
92101
data VeriOpts = VeriOpts
93102
{ simp :: Bool
94103
, maxIter :: Maybe Integer
@@ -575,6 +584,13 @@ isPartial :: Expr a -> Bool
575584
isPartial (Partial _ _ _) = True
576585
isPartial _ = False
577586

587+
printPartialIssues :: [Expr End] -> String -> IO ()
588+
printPartialIssues flattened call =
589+
when (any isPartial flattened) $ do
590+
T.putStrLn $ indent 3 "\x1b[33m[WARNING]\x1b[0m: hevm was only able to partially explore "
591+
<> T.pack call <> " due to the following issue(s):"
592+
T.putStr . T.unlines . fmap (indent 5 . ("- " <>)) . fmap formatPartial . getPartials $ flattened
593+
578594
getPartials :: [Expr End] -> [PartialExec]
579595
getPartials = mapMaybe go
580596
where
@@ -603,14 +619,10 @@ verify solvers opts preState maybepost = do
603619
when conf.debug $ putStrLn " Simplifying expression"
604620
let expr = if opts.simp then (Expr.simplify exprInter) else exprInter
605621
when conf.dumpExprs $ T.writeFile "simplified.expr" (formatExpr expr)
606-
607-
when conf.debug $ putStrLn $ " Exploration finished, " <> show (Expr.numBranches expr) <> " branch(es) to check in call " <> call
608-
609622
let flattened = flattenExpr expr
610-
when (any isPartial flattened) $ do
611-
T.putStrLn $ indent 3 "\x1b[33mWARNING\x1b[0m: hevm was only able to partially explore the call "
612-
<> T.pack call <> " due to the following issue(s):"
613-
T.putStr . T.unlines . fmap (indent 5 . ("- " <>)) . fmap formatPartial . getPartials $ flattened
623+
when conf.debug $ do
624+
printPartialIssues flattened ("the call " <> call)
625+
putStrLn $ " Exploration finished, " <> show (Expr.numBranches expr) <> " branch(es) to check in call " <> call
614626

615627
case maybepost of
616628
Nothing -> pure (expr, [Qed ()])
@@ -675,19 +687,20 @@ equivalenceCheck
675687
-> ByteString
676688
-> VeriOpts
677689
-> (Expr Buf, [Prop])
678-
-> m [EquivResult]
690+
-> m ([EquivResult], [Expr End])
679691
equivalenceCheck solvers bytecodeA bytecodeB opts calldata = do
680692
conf <- readConfig
681693
case bytecodeA == bytecodeB of
682694
True -> liftIO $ do
683695
putStrLn "bytecodeA and bytecodeB are identical"
684-
pure [Qed ()]
696+
pure ([Qed ()], mempty)
685697
False -> do
686-
branchesA <- getBranches bytecodeA
687-
branchesB <- getBranches bytecodeB
688698
when conf.debug $ liftIO $ do
689699
putStrLn "bytecodeA and bytecodeB are different, checking for equivalence"
690-
equivalenceCheck' solvers branchesA branchesB
700+
branchesA <- getBranches bytecodeA
701+
branchesB <- getBranches bytecodeB
702+
res <- equivalenceCheck' solvers branchesA branchesB
703+
pure (res, branchesA <> branchesB)
691704
where
692705
-- decompiles the given bytecode into a list of branches
693706
getBranches :: ByteString -> m [Expr End]
@@ -703,14 +716,14 @@ equivalenceCheck'
703716
:: forall m . App m
704717
=> SolverGroup -> [Expr End] -> [Expr End] -> m [EquivResult]
705718
equivalenceCheck' solvers branchesA branchesB = do
706-
when (any isPartial branchesA || any isPartial branchesB) $ liftIO $ do
707-
putStrLn "\x1b[33mWARNING\x1b[0m: hevm was only able to partially explore the given contract due to the following issue(s):"
708-
T.putStr . T.unlines . fmap (indent 2 . ("- " <>)) . fmap formatPartial . nubOrd $ ((getPartials branchesA) <> (getPartials branchesB))
719+
conf <- readConfig
720+
when conf.debug $ do
721+
liftIO $ printPartialIssues branchesA "codeA"
722+
liftIO $ printPartialIssues branchesB "codeB"
709723

710724
let allPairs = [(a,b) | a <- branchesA, b <- branchesB]
711725
liftIO $ putStrLn $ "Found " <> show (length allPairs) <> " total pairs of endstates"
712726

713-
conf <- readConfig
714727
when conf.dumpEndStates $ liftIO $
715728
putStrLn $ "endstates in bytecodeA: " <> show (length branchesA)
716729
<> "\nendstates in bytecodeB: " <> show (length branchesB)
@@ -726,9 +739,10 @@ equivalenceCheck' solvers branchesA branchesB = do
726739

727740
let useful = foldr (\(_, b) n -> if b then n+1 else n) (0::Integer) results
728741
liftIO $ putStrLn $ "Reuse of previous queries was Useful in " <> (show useful) <> " cases"
742+
729743
case all (isQed . fst) results of
730744
True -> pure [Qed ()]
731-
False -> pure $ filter (/= Qed ()) . fmap fst $ results
745+
False -> pure $ filter (not . isQed) . fmap fst $ results
732746
where
733747
-- we order the sets by size because this gives us more cache hits when
734748
-- running our queries later on (since we rely on a subset check)

src/EVM/UnitTest.hs

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import EVM.FeeSchedule (feeSchedule)
1414
import EVM.Fetch qualified as Fetch
1515
import EVM.Format
1616
import EVM.Solidity
17-
import EVM.SymExec (defaultVeriOpts, symCalldata, verify, isQed, extractCex, prettyCalldata, panicMsg, VeriOpts(..), flattenExpr, isUnknown, isError, groupIssues)
17+
import EVM.SymExec (defaultVeriOpts, symCalldata, verify, isCex, extractCex, prettyCalldata, panicMsg, VeriOpts(..), flattenExpr, isUnknown, isError, groupIssues, groupPartials, ProofResult(..))
1818
import EVM.Types
1919
import EVM.Transaction (initTx)
2020
import EVM.Stepper (Stepper)
@@ -113,7 +113,8 @@ makeVeriOpts opts =
113113
}
114114

115115
-- | Top level CLI endpoint for hevm test
116-
unitTest :: App m => UnitTestOptions RealWorld -> Contracts -> m Bool
116+
-- | Returns tuple of (No Cex, No warnings)
117+
unitTest :: App m => UnitTestOptions RealWorld -> Contracts -> m (Bool, Bool)
117118
unitTest opts (Contracts cs) = do
118119
let unitTestContrs = findUnitTests opts.match $ Map.elems cs
119120
conf <- readConfig
@@ -122,7 +123,9 @@ unitTest opts (Contracts cs) = do
122123
let x = map (\(a,b) -> " --> " <> a <> " --- functions: " <> (Text.pack $ show b)) unitTestContrs
123124
putStrLn $ unlines $ map Text.unpack x
124125
results <- concatMapM (runUnitTestContract opts cs) unitTestContrs
125-
pure $ and results
126+
when conf.debug $ liftIO $ putStrLn $ "unitTest individual results: " <> show results
127+
let (firsts, seconds) = unzip results
128+
pure (and firsts, and seconds)
126129

127130
-- | Assuming a constructor is loaded, this stepper will run the constructor
128131
-- to create the test contract, give it an initial balance, and run `setUp()'.
@@ -155,24 +158,20 @@ initializeUnitTest opts theContract = do
155158
Left e -> pushTrace (ErrorTrace e)
156159
_ -> popTrace
157160

161+
-- Returns tuple of (No Cex, No warnings)
158162
runUnitTestContract
159163
:: App m
160164
=> UnitTestOptions RealWorld
161165
-> Map Text SolcContract
162166
-> (Text, [Sig])
163-
-> m [Bool]
167+
-> m [(Bool, Bool)]
164168
runUnitTestContract
165169
opts@(UnitTestOptions {..}) contractMap (name, testSigs) = do
166-
167-
-- Print a header
168170
liftIO $ putStrLn $ "Checking " ++ show (length testSigs) ++ " function(s) in contract " ++ unpack name
169171

170172
-- Look for the wanted contract by name from the Solidity info
171173
case Map.lookup name contractMap of
172-
Nothing ->
173-
-- Fail if there's no such contract
174-
internalError $ "Contract " ++ unpack name ++ " not found"
175-
174+
Nothing -> internalError $ "Contract " ++ unpack name ++ " not found"
176175
Just theContract -> do
177176
-- Construct the initial VM and begin the contract's constructor
178177
vm0 :: VM Concrete RealWorld <- liftIO $ stToIO $ initialUnitTestVm opts theContract
@@ -184,15 +183,16 @@ runUnitTestContract
184183
writeTraceDapp dapp vm1
185184
case vm1.result of
186185
Just (VMFailure _) -> liftIO $ do
187-
Text.putStrLn "\x1b[31m[BAIL]\x1b[0m setUp() "
188-
tick $ failOutput vm1 opts "setUp()"
189-
pure [False]
186+
Text.putStrLn " \x1b[31m[BAIL]\x1b[0m setUp() "
187+
tick $ indentLines 3 $ failOutput vm1 opts "setUp()"
188+
pure [(True, False)]
190189
Just (VMSuccess _) -> do
191190
forM testSigs $ \s -> symRun opts vm1 s
192191
_ -> internalError "setUp() did not end with a result"
193192

194-
-- | Define the thread spawner for symbolic tests
195-
symRun :: App m => UnitTestOptions RealWorld -> VM Concrete RealWorld -> Sig -> m Bool
193+
-- Define the thread spawner for symbolic tests
194+
-- Returns tuple of (No Cex, No warnings)
195+
symRun :: App m => UnitTestOptions RealWorld -> VM Concrete RealWorld -> Sig -> m (Bool, Bool)
196196
symRun opts@UnitTestOptions{..} vm (Sig testName types) = do
197197
let callSig = testName <> "(" <> (Text.intercalate "," (map abiTypeSolidity types)) <> ")"
198198
liftIO $ putStrLn $ "\x1b[96m[RUNNING]\x1b[0m " <> Text.unpack callSig
@@ -227,42 +227,47 @@ symRun opts@UnitTestOptions{..} vm (Sig testName types) = do
227227
writeTraceDapp dapp vm'
228228

229229
-- check postconditions against vm
230-
(e, results) <- verify solvers (makeVeriOpts opts) (symbolify vm') (Just postcondition)
231-
let allReverts = not . (any Expr.isSuccess) . flattenExpr $ e
232-
230+
(end, results) <- verify solvers (makeVeriOpts opts) (symbolify vm') (Just postcondition)
231+
let ends = flattenExpr end
233232
conf <- readConfig
234-
when conf.debug $ liftIO $ forM_ (filter Expr.isFailure (flattenExpr e)) $ \case
233+
when conf.debug $ liftIO $ forM_ (filter Expr.isFailure ends) $ \case
235234
(Failure _ _ a) -> putStrLn $ " -> debug of func: " <> Text.unpack testName <> " Failure at the end of expr: " <> show a;
236235
_ -> internalError "cannot be, filtered for failure"
237-
when (any isUnknown results || any isError results) $ liftIO $ do
238-
putStrLn $ " \x1b[33mWARNING\x1b[0m: hevm was only able to partially explore the test " <> Text.unpack testName <> " due to: ";
239-
forM_ (groupIssues (filter isError results)) $ \(num, str) -> putStrLn $ " " <> show num <> "x -> " <> str
240-
forM_ (groupIssues (filter isUnknown results)) $ \(num, str) -> putStrLn $ " " <> show num <> "x -> " <> str
241236

242237
-- display results
243-
if all isQed results
244-
then if allReverts && (not shouldFail)
245-
then do
246-
liftIO $ putStr $ " \x1b[31m[FAIL]\x1b[0m " <> Text.unpack testName <> "\n" <> Text.unpack allBranchRev
247-
pure False
248-
else do
249-
liftIO $ putStr $ " \x1b[32m[PASS]\x1b[0m " <> Text.unpack testName <> "\n"
250-
pure True
251-
else do
252-
-- not all is Qed
253-
let x = mapMaybe extractCex results
254-
let y = symFailure opts testName (fst cd) types x
255-
liftIO $ putStr $ " \x1b[31m[FAIL]\x1b[0m " <> Text.unpack testName <> "\n" <> Text.unpack y
256-
pure False
257-
258-
allBranchRev :: Text
259-
allBranchRev = intercalate "\n"
260-
[ Text.concat $ indentLines 3 <$>
261-
[ "Reason:"
262-
, " No reachable assertion violations, but all branches reverted"
263-
, " Prefix this testname with `proveFail` if this is expected"
264-
]
265-
]
238+
let t = "the test " <> Text.unpack testName
239+
let warnings = any Expr.isPartial ends || any isUnknown results || any isError results
240+
let allReverts = not . (any Expr.isSuccess) $ ends
241+
let unexpectedAllRevert = allReverts && not shouldFail
242+
when conf.debug $ liftIO $ putStrLn $ "symRun -- (cex,warnings,unexpectedAllRevert): " <> show (any isCex results, warnings, unexpectedAllRevert)
243+
liftIO $ case (any isCex results, warnings, unexpectedAllRevert) of
244+
(False, False, False) -> do
245+
-- happy case
246+
putStr $ " \x1b[32m[PASS]\x1b[0m " <> Text.unpack testName <> "\n"
247+
(True, _, _) -> do
248+
-- there are counterexamples (and maybe other things, but Cex is most important)
249+
let x = mapMaybe extractCex results
250+
y = symFailure opts testName (fst cd) types x
251+
putStr $ " \x1b[31m[FAIL]\x1b[0m " <> Text.unpack testName <> "\n" <> Text.unpack y
252+
(_, True, _) -> do
253+
-- There are errors/unknowns/partials, we fail them
254+
putStr $ " \x1b[31m[FAIL]\x1b[0m " <> Text.unpack testName <> "\n"
255+
(_, _, True) -> do
256+
-- No cexes/errors/unknowns/partials, but all branches reverted
257+
putStr $ " \x1b[31m[FAIL]\x1b[0m " <> Text.unpack testName <> "\n"
258+
<> " No reachable assertion violations, but all branches reverted\n"
259+
liftIO $ printWarnings ends results t
260+
pure (not (any isCex results), not (warnings || unexpectedAllRevert))
261+
262+
printWarnings :: [Expr 'End] -> [ProofResult a b c String] -> String -> IO ()
263+
printWarnings e results testName = do
264+
when (any isUnknown results || any isError results || any Expr.isPartial e) $ do
265+
putStrLn $ " \x1b[33m[WARNING]\x1b[0m hevm was only able to partially explore " <> testName <> " due to: ";
266+
forM_ (groupIssues (filter isError results)) $ \(num, str) -> putStrLn $ " " <> show num <> "x -> " <> str
267+
forM_ (groupIssues (filter isUnknown results)) $ \(num, str) -> putStrLn $ " " <> show num <> "x -> " <> str
268+
forM_ (groupPartials e) $ \(num, str) -> putStrLn $ " " <> show num <> "x -> " <> str
269+
putStrLn ""
270+
266271
symFailure :: UnitTestOptions RealWorld -> Text -> Expr Buf -> [AbiType] -> [(Expr End, SMTCex)] -> Text
267272
symFailure UnitTestOptions {..} testName cd types failures' =
268273
mconcat
@@ -278,9 +283,9 @@ symFailure UnitTestOptions {..} testName cd types failures' =
278283
in Text.pack $ prettyvmresult res
279284
mkMsg (leaf, cex) = intercalate "\n" $
280285
["Counterexample:"
281-
," result: " <> showRes leaf
282286
," calldata: " <> let ?context = dappContext (traceContext leaf)
283287
in prettyCalldata cex cd testName types
288+
," result: " <> showRes leaf
284289
] <> verbText leaf
285290
verbText leaf = case verbose of
286291
Just _ -> [Text.unlines [ indentLines 2 (showTraceTree' dapp leaf)]]
@@ -323,7 +328,6 @@ failOutput vm UnitTestOptions { .. } testName =
323328
Just _ -> indentLines 2 (showTraceTree dapp vm)
324329
_ -> ""
325330
, indentLines 2 (formatTestLogs dapp.eventMap vm.logs)
326-
, "\n"
327331
]
328332

329333
formatTestLogs :: (?context :: DappContext) => Map W256 Event -> [Expr Log] -> Text

test/EVM/Test/Utils.hs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ import Data.Maybe (fromMaybe)
2424
import EVM.Types (internalError)
2525
import System.Environment (lookupEnv)
2626

27+
-- Returns tuple of (No cex, No warnings)
2728
runSolidityTestCustom
2829
:: (MonadMask m, App m)
29-
=> FilePath -> Text -> Maybe Natural -> Maybe Integer -> Bool -> RpcInfo -> ProjectType -> m Bool
30+
=> FilePath -> Text -> Maybe Natural -> Maybe Integer -> Bool -> RpcInfo -> ProjectType -> m (Bool, Bool)
3031
runSolidityTestCustom testFile match timeout maxIter ffiAllowed rpcinfo projectType = do
3132
withSystemTempDirectory "dapp-test" $ \root -> do
3233
(compile projectType root testFile) >>= \case
@@ -39,9 +40,10 @@ runSolidityTestCustom testFile match timeout maxIter ffiAllowed rpcinfo projectT
3940
opts <- liftIO $ testOpts solvers root (Just bo) match maxIter ffiAllowed rpcinfo
4041
unitTest opts contracts
4142

43+
-- Returns tuple of (No cex, No warnings)
4244
runSolidityTest
4345
:: (MonadMask m, App m)
44-
=> FilePath -> Text -> m Bool
46+
=> FilePath -> Text -> m (Bool, Bool)
4547
runSolidityTest testFile match = runSolidityTestCustom testFile match Nothing Nothing True Nothing Foundry
4648

4749
testOpts :: SolverGroup -> FilePath -> Maybe BuildOutput -> Text -> Maybe Integer -> Bool -> RpcInfo -> IO (UnitTestOptions RealWorld)

test/rpc.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ tests = testGroup "rpc"
7272
[ test "dapp-test" $ do
7373
let testFile = "test/contracts/pass/rpc.sol"
7474
res <- runSolidityTestCustom testFile ".*" Nothing Nothing False testRpcInfo Foundry
75-
liftIO $ assertEqual "test result" True res
75+
liftIO $ assertEqual "test result" (True, True) res
7676

7777
-- concretely exec "transfer" on WETH9 using remote rpc
7878
-- https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code

0 commit comments

Comments
 (0)