hevm-0.24: hevm-cli/hevm-cli.hs
-- Main file of the hevm CLI program
{-# Language BangPatterns #-}
{-# Language CPP #-}
{-# Language DeriveGeneric #-}
{-# Language GeneralizedNewtypeDeriving #-}
{-# Language LambdaCase #-}
{-# Language NumDecimals #-}
{-# Language OverloadedStrings #-}
{-# Language TemplateHaskell #-}
import EVM.Concrete (w256)
import qualified EVM as EVM
import qualified EVM.FeeSchedule as FeeSchedule
import qualified EVM.Fetch
import qualified EVM.Flatten
import qualified EVM.Stepper
import qualified EVM.TTY as EVM.TTY
import qualified EVM.Emacs as EVM.Emacs
#if MIN_VERSION_aeson(1, 0, 0)
import qualified EVM.VMTest as VMTest
#endif
import EVM.Debug
import EVM.Exec
import EVM.Solidity
import EVM.Types hiding (word)
import EVM.UnitTest (UnitTestOptions, coverageReport, coverageForUnitTestContract)
import EVM.UnitTest (runUnitTestContract)
import EVM.UnitTest (getParametersFromEnvironmentVariables, testNumber)
import EVM.Dapp (findUnitTests, dappInfo)
import qualified EVM.UnitTest as EVM.UnitTest
import qualified Paths_hevm as Paths
import Control.Concurrent.Async (async, waitCatch)
import Control.Exception (evaluate)
import Control.Lens
import Control.Monad (void, when, forM_)
import Control.Monad.State.Strict (execState)
import Data.ByteString (ByteString)
import Data.List (intercalate, isSuffixOf)
import Data.Text (Text, unpack, pack)
import Data.Maybe (fromMaybe)
import System.Directory (withCurrentDirectory, listDirectory)
import System.Exit (die, exitFailure)
import System.IO (hFlush, stdout)
import System.Process (callProcess)
import System.Timeout (timeout)
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Base16 as BS16
import qualified Data.ByteString.Char8 as Char8
import qualified Data.ByteString.Lazy as LazyByteString
import qualified Data.Map as Map
import qualified Options.Generic as Options
import qualified EVM.Facts as Facts
import qualified EVM.Facts.Git as Git
import qualified Text.Regex.TDFA as Regex
import qualified Data.Sequence as Seq
-- This record defines the program's command-line options
-- automatically via the `optparse-generic` package.
data Command
= Exec
{ code :: ByteString
, calldata :: Maybe ByteString
, address :: Maybe Addr
, caller :: Maybe Addr
, origin :: Maybe Addr
, coinbase :: Maybe Addr
, value :: Maybe W256
, gas :: Maybe W256
, number :: Maybe W256
, timestamp :: Maybe W256
, gaslimit :: Maybe W256
, gasprice :: Maybe W256
, difficulty :: Maybe W256
, debug :: Bool
, state :: Maybe String
}
| DappTest
{ jsonFile :: Maybe String
, dappRoot :: Maybe String
, debug :: Bool
, rpc :: Maybe URL
, verbose :: Bool
, coverage :: Bool
, state :: Maybe String
, match :: Maybe String
}
| Interactive
{ jsonFile :: Maybe String
, dappRoot :: Maybe String
, rpc :: Maybe URL
, state :: Maybe String
}
| VmTest
{ file :: String
, test :: [String]
, debug :: Bool
}
| VmTestReport
{ tests :: String
}
| Flatten
{ sourceFile :: String
, jsonFile :: Maybe String
, dappRoot :: Maybe String
}
| Emacs
deriving (Show, Options.Generic, Eq)
type URL = Text
instance Options.ParseRecord Command where
parseRecord =
Options.parseRecordWithModifiers Options.lispCaseModifiers
optsMode :: Command -> Mode
optsMode x = if debug x then Debug else Run
unitTestOptions :: Command -> IO UnitTestOptions
unitTestOptions cmd = do
vmModifier <-
case state cmd of
Nothing ->
pure id
Just repoPath -> do
facts <- Git.loadFacts (Git.RepoAt repoPath)
pure (flip Facts.apply facts)
params <- getParametersFromEnvironmentVariables
pure EVM.UnitTest.UnitTestOptions
{ EVM.UnitTest.oracle =
case rpc cmd of
Just url -> EVM.Fetch.http (EVM.Fetch.BlockNumber (testNumber params)) url
Nothing -> EVM.Fetch.zero
, EVM.UnitTest.verbose = verbose cmd
, EVM.UnitTest.match = pack $ fromMaybe "^test" (match cmd)
, EVM.UnitTest.vmModifier = vmModifier
, EVM.UnitTest.testParams = params
}
main :: IO ()
main = do
cmd <- Options.getRecord "hevm -- Ethereum evaluator"
let
root = fromMaybe "." (dappRoot cmd)
case cmd of
Exec {} ->
launchExec cmd
VmTest {} ->
launchVMTest cmd
DappTest {} ->
withCurrentDirectory root $ do
testFile <- findJsonFile (jsonFile cmd)
testOpts <- unitTestOptions cmd
case coverage cmd of
False ->
dappTest testOpts (optsMode cmd) testFile
True ->
dappCoverage testOpts (optsMode cmd) testFile
Interactive {} ->
withCurrentDirectory root $ do
testFile <- findJsonFile (jsonFile cmd)
testOpts <- unitTestOptions cmd
EVM.TTY.main testOpts root testFile
VmTestReport {} ->
withCurrentDirectory (tests cmd) $ do
dataDir <- Paths.getDataDir
callProcess "bash" [dataDir ++ "/run-consensus-tests", "."]
Flatten {} ->
withCurrentDirectory root $ do
theJson <- findJsonFile (jsonFile cmd)
readSolc theJson >>=
\case
Just (contractMap, cache) -> do
let dapp = dappInfo "." contractMap cache
EVM.Flatten.flatten dapp (pack (sourceFile cmd))
Nothing ->
error ("Failed to read Solidity JSON for `" ++ theJson ++ "'")
Emacs ->
EVM.Emacs.main
findJsonFile :: Maybe String -> IO String
findJsonFile (Just s) = pure s
findJsonFile Nothing = do
outFiles <- listDirectory "out"
case filter (isSuffixOf ".sol.json") outFiles of
[x] -> pure ("out/" ++ x)
[] ->
error $ concat
[ "No `*.sol.json' file found in `./out'.\n"
, "Maybe you need to run `dapp build'.\n"
, "You can specify a file with `--json-file'."
]
xs ->
error $ concat
[ "Multiple `*.sol.json' files found in `./out'.\n"
, "Specify one using `--json-file'.\n"
, "Files found: "
, intercalate ", " xs
]
dappTest :: UnitTestOptions -> Mode -> String -> IO ()
dappTest opts _ solcFile = do
readSolc solcFile >>=
\case
Just (contractMap, cache) -> do
let matcher = regexMatches (EVM.UnitTest.match opts)
let unitTests = (findUnitTests matcher) (Map.elems contractMap)
results <- mapM (runUnitTestContract opts contractMap cache) unitTests
when (any (== False) results) exitFailure
Nothing ->
error ("Failed to read Solidity JSON for `" ++ solcFile ++ "'")
regexMatches :: Text -> Text -> Bool
regexMatches regexSource =
let
compOpts =
Regex.defaultCompOpt { Regex.lastStarGreedy = True }
execOpts =
Regex.defaultExecOpt { Regex.captureGroups = False }
regex = Regex.makeRegexOpts compOpts execOpts (unpack regexSource)
in
Regex.matchTest regex . Seq.fromList . unpack
dappCoverage :: UnitTestOptions -> Mode -> String -> IO ()
dappCoverage opts _ solcFile = do
readSolc solcFile >>=
\case
Just (contractMap, cache) -> do
let matcher = regexMatches (EVM.UnitTest.match opts)
let unitTests = (findUnitTests matcher) (Map.elems contractMap)
covs <- mconcat <$> mapM (coverageForUnitTestContract opts contractMap cache) unitTests
let
dapp = dappInfo "." contractMap cache
f (k, vs) = do
putStr "***** hevm coverage for "
putStrLn (unpack k)
putStrLn ""
forM_ vs $ \(n, bs) -> do
case ByteString.find (\x -> x /= 0x9 && x /= 0x20 && x /= 0x7d) bs of
Nothing -> putStr "..... "
Just _ ->
case n of
-1 -> putStr ";;;;; "
0 -> putStr "##### "
_ -> putStr " "
Char8.putStrLn bs
putStrLn ""
mapM_ f (Map.toList (coverageReport dapp covs))
Nothing ->
error ("Failed to read Solidity JSON for `" ++ solcFile ++ "'")
launchExec :: Command -> IO ()
launchExec cmd = do
let vm = vmFromCommand cmd
vm1 <- case state cmd of
Nothing -> pure vm
Just path ->
-- Note: this will load the code, so if you've specified a state
-- repository, then you effectively can't change `--code' after
-- the first run.
Facts.apply vm <$> Git.loadFacts (Git.RepoAt path)
case optsMode cmd of
Run ->
let vm' = execState exec vm1
in case view EVM.result vm' of
Nothing ->
error "internal error; no EVM result"
Just (EVM.VMFailure e) -> do
die (show e)
Just (EVM.VMSuccess x) -> do
let hex = BS16.encode x
if ByteString.null hex then pure ()
else do
ByteString.putStr hex
putStrLn ""
case state cmd of
Nothing -> pure ()
Just path ->
Git.saveFacts (Git.RepoAt path) (Facts.vmFacts vm')
Debug ->
void (EVM.TTY.runFromVM vm1)
vmFromCommand :: Command -> EVM.VM
vmFromCommand cmd =
vm1 & EVM.env . EVM.contracts . ix address' . EVM.balance +~ (w256 value')
where
value' = word value 0
address' = addr address 1
vm1 = EVM.makeVm $ EVM.VMOpts
{ EVM.vmoptCode = hexByteString "--code" (code cmd)
, EVM.vmoptCalldata = maybe "" (hexByteString "--calldata")
(calldata cmd)
, EVM.vmoptValue = value'
, EVM.vmoptAddress = address'
, EVM.vmoptCaller = addr caller 2
, EVM.vmoptOrigin = addr origin 3
, EVM.vmoptGas = word gas 0
, EVM.vmoptCoinbase = addr coinbase 0
, EVM.vmoptNumber = word number 0
, EVM.vmoptTimestamp = word timestamp 0
, EVM.vmoptGaslimit = word gaslimit 0
, EVM.vmoptGasprice = word gasprice 0
, EVM.vmoptDifficulty = word difficulty 0
, EVM.vmoptSchedule = FeeSchedule.metropolis
}
word f def = maybe def id (f cmd)
addr f def = maybe def id (f cmd)
launchVMTest :: Command -> IO ()
launchVMTest cmd =
#if MIN_VERSION_aeson(1, 0, 0)
VMTest.parseSuite <$> LazyByteString.readFile (file cmd) >>=
\case
Left err -> print err
Right allTests ->
let testFilter =
if null (test cmd)
then id
else filter (\(x, _) -> elem x (test cmd))
in
mapM_ (runVMTest (optsMode cmd)) $
testFilter (Map.toList allTests)
#else
putStrLn "Not supported"
#endif
#if MIN_VERSION_aeson(1, 0, 0)
runVMTest :: Mode -> (String, VMTest.Case) -> IO Bool
runVMTest mode (name, x) = do
let
vm0 = VMTest.vmForCase x
m = void EVM.Stepper.execFully >> EVM.Stepper.evm EVM.finalize
putStr (name ++ " ")
hFlush stdout
result <- do
action <- async $
case mode of
Run ->
timeout (1e6) . evaluate $ do
execState (VMTest.interpret m) vm0
Debug ->
Just <$> EVM.TTY.runFromVM vm0
waitCatch action
case result of
Right (Just vm1) -> do
ok <- VMTest.checkExpectation x vm1
putStrLn (if ok then "ok" else "")
return ok
Right Nothing -> do
putStrLn "timeout"
return False
Left _ -> do
putStrLn "error"
return False
#endif