Small standalone fixes: coverage attribution, generic log events (#1599)

* feat: name the function that found new coverage

ppWorkerEvent already receives the transaction sequence behind a
NewCoverage event but only reported the aggregate counters. Append the
last transaction's call to the log line so it is visible which function
is making progress.

Co-authored-by: gustavo-grieco <[email protected]>

* feat: add generic log events

Add a WorkerEvent Log constructor for free-form worker messages and a
CampaignEvent ServerLog for messages that are not attributable to any
worker, reported with a [Server] prefix in the text UI and as a
server_log SSE event.

All four exhaustive matches over these types (the ToJSON instances, the
pretty printers and the SSE event names) land together so the compiler
stays happy.

Co-authored-by: gustavo-grieco <[email protected]>

* fix(tests): return the flag from flags.sol set0/set1

Both functions are declared 'returns (bool)' but fall off the end, so
solc warns and they silently return false.

Co-authored-by: gustavo-grieco <[email protected]>

* chore: gitignore Python artifacts

Co-authored-by: gustavo-grieco <[email protected]>

* fix(worker): avoid hlint false positive on record-dot access

CI runs bare `hlint lib src`, which does not read echidna.cabal and so
does not know OverloadedRecordDot is on. It parsed `(last txs).call` as
function composition and flagged the parens as a redundant bracket.

Taking that suggestion would have broken the build: under
OverloadedRecordDot, `last txs.call` parses as `last (txs.call)`.

Bind the last transaction first so the expression needs no parens and
reads identically to hlint and to GHC.

---------

Co-authored-by: gustavo-grieco <[email protected]>
This commit is contained in:
Emilio López
2026-08-14 19:54:20 +02:00
committed by GitHub
co-authored by gustavo-grieco
parent 07282dac29
commit d87e720342
6 changed files with 36 additions and 5 deletions
+6
View File
@@ -22,3 +22,9 @@ crytic-export/
echidna.cabal
*.swp
profiles/
# Python helper scripts and test suites
__pycache__/
*.py[cod]
.pytest_cache/
.venv/
+6
View File
@@ -36,6 +36,10 @@ instance ToJSON SSE where
object [ "timestamp" .= time
, "filename" .= filename
]
toJSON (SSE (time, ServerLog msg)) =
object [ "timestamp" .= time
, "data" .= msg
]
runSSEServer :: MVar () -> Env -> Word16 -> Int -> IO ()
runSSEServer serverStopVar env port nworkers = do
@@ -53,11 +57,13 @@ runSSEServer serverStopVar env port nworkers = do
NewCoverage {} -> "new_coverage"
SymExecLog _ -> "sym_exec_log"
SymExecError _ -> "sym_exec_error"
Log _ -> "log"
TxSequenceReplayed {} -> "tx_sequence_replayed"
TxSequenceReplayFailed {} -> "tx_sequence_replay_failed"
WorkerStopped _ -> "worker_stopped"
Failure _err -> "failure"
ReproducerSaved _ -> "saved_reproducer"
ServerLog _ -> "server_log"
let serverEvent = ServerEvent
{ eventName = Just (eventName campaignEvent)
+2
View File
@@ -11,6 +11,7 @@ data CampaignEvent
= WorkerEvent WorkerId WorkerType WorkerEvent
| Failure String
| ReproducerSaved String -- filename
| ServerLog String
data WorkerEvent
= TestFalsified !EchidnaTest
@@ -18,6 +19,7 @@ data WorkerEvent
| NewCoverage { points :: !Int, numCodehashes :: !Int, corpusSize :: !Int, transactions :: [Tx] }
| SymExecError !String
| SymExecLog !String
| Log !String
| TxSequenceReplayed FilePath !Int !Int
| TxSequenceReplayFailed FilePath Tx
| WorkerStopped WorkerStopReason
+2
View File
@@ -34,6 +34,8 @@ ppLogLine vm (time, event@(WorkerEvent workerId FuzzWorker _)) =
((timePrefix time <> "[Worker " <> show workerId <> "] ") <>) <$> ppCampaignEventLog vm event
ppLogLine vm (time, event@(WorkerEvent workerId SymbolicWorker _)) =
((timePrefix time <> "[Worker " <> show workerId <> ", symbolic] ") <>) <$> ppCampaignEventLog vm event
ppLogLine vm (time, event@(ServerLog _)) =
((timePrefix time <> "[Server] ") <>) <$> ppCampaignEventLog vm event
ppLogLine vm (time, event) =
((timePrefix time <> " ") <>) <$> ppCampaignEventLog vm event
+16 -3
View File
@@ -12,6 +12,7 @@ import Echidna.ABI (encodeSig)
import Echidna.Types.Campaign
import Echidna.Types.Config (Env(..), EConfig(..))
import Echidna.Types.Test
import Echidna.Types.Tx (Tx(..), TxCall(..))
import Echidna.Types.Worker
import Echidna.Utility (getTimestamp)
@@ -30,6 +31,7 @@ instance ToJSON WorkerEvent where
object [ "coverage" .= points, "contracts" .= numCodehashes, "corpus_size" .= corpusSize]
SymExecError msg -> object [ "msg" .= msg ]
SymExecLog msg -> object [ "msg" .= msg ]
Log msg -> object [ "msg" .= msg ]
TxSequenceReplayed file current total ->
object [ "file" .= file, "current" .= current, "total" .= total ]
TxSequenceReplayFailed file tx ->
@@ -97,6 +99,7 @@ ppCampaignEvent = \case
WorkerEvent _ _ e -> ppWorkerEvent e
Failure err -> err
ReproducerSaved f -> "Saved reproducer to " <> f
ServerLog msg -> msg
ppWorkerEvent :: WorkerEvent -> String
ppWorkerEvent = \case
@@ -105,14 +108,24 @@ ppWorkerEvent = \case
TestOptimized test ->
let name = case test.testType of OptimizationTest n _ -> n; _ -> error "fixme"
in "New maximum value of " <> unpack name <> ": " <> show test.value
NewCoverage { points, numCodehashes, corpusSize } ->
"New coverage: " <> show points <> " instr, "
NewCoverage { points, numCodehashes, corpusSize, transactions } ->
let -- the coverage is credited to the last transaction of the sequence
culprit = case transactions of
[] -> "init"
txs -> let tx = last txs in case tx.call of
SolCall (name, _) -> unpack name
SolCreate _ -> "constructor"
SolCalldata _ -> "fallback"
NoCall -> "no call"
in "New coverage: " <> show points <> " instr, "
<> show numCodehashes <> " contracts, "
<> show corpusSize <> " seqs in corpus"
<> show corpusSize <> " seqs in corpus (" <> culprit <> ")"
SymExecError err ->
"Symbolic execution failed: " <> err
SymExecLog msg ->
"Symbolic execution log: " <> msg
Log msg ->
msg
TxSequenceReplayed file current total ->
"Sequence replayed from corpus file " <> file <> " (" <> show current <> "/" <> show total <> ")"
TxSequenceReplayFailed file tx ->
+4 -2
View File
@@ -5,13 +5,15 @@ contract Test {
bool private flag1 = true;
function set0(int val) public returns (bool){
if (val % 100 == 0)
if (val % 100 == 0)
flag0 = false;
return flag0;
}
function set1(int val) public returns (bool){
if (val % 10 == 0 && !flag0)
if (val % 10 == 0 && !flag0)
flag1 = false;
return flag1;
}
function echidna_alwaystrue() public returns (bool){