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
feat(coverage): better branch handling #2133
Conversation
It's possible to have multiple functions defined in the same source file with the same name. For example, you could define two tokens in the same file - each would have a transfer function, so we need to ensure the function names are unique by prepending the contract name.
evm/src/coverage/visitor.rs
Outdated
.unwrap_or(loc.start); | ||
.ok_or_else(|| { | ||
eyre::eyre!("could not find anchor: no matching instruction in range") | ||
})?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more "brittle" in the sense that we don't have a fallback, but the prior fallback made 0 sense so it's better this way
f18c406
to
a71e740
Compare
a71e740
to
d982559
Compare
765a39b
to
34080de
Compare
I've run this on Lil Web3 and my coverage testing repo and it seems to find some uncovered branches, and those seem to line up with what is actually uncovered. For example, in Lil Web3 it found some branches that have not been tested :) |
let element = if let Some(element) = source_map.get(pc - cumulative_push_size) { | ||
element | ||
} else { | ||
// NOTE(onbjerg): For some reason the last few bytes of the bytecode do not have | ||
// a source map associated, so at that point we just stop searching | ||
break | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit weird, but I'm unsure if it's intentional?
evm/src/coverage/visitor.rs
Outdated
/// The source code that contains the AST being walked. | ||
source: String, | ||
/// Source maps for this specific source file, keyed by the contract name. | ||
source_maps: HashMap<String, SourceMap>, | ||
/// Bytecodes for this specific source file, keyed by the contract name. | ||
bytecodes: HashMap<String, Cow<'a, Bytes>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bytes is cheaply cloneable because it's basically just an AtomicPointer and a * u8
pointer to the data, so no need to use cow, I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the cow is from ethers-rs 😄 but yes i agree
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, maybe I should change that, I probably introduced this by copy-pasting the other functions...
Some small refactoring and solution for #1967
Motivation
We want good branch coverage in
forge coverage
, which means detecting the following scenarios:assert
andrequire
should result in branching (one branch reverts, the other one doesn't)Solution
assert
andrequire
require us to inspect the bytecode that the source map identifies as representing them. Generally, this bytecode follows the format:That is, we need to find bytecode that identifies an
assert
or arequire
and look for a JUMPI preceded by a PUSH. The first branch is the instruction directly after JUMPI, because this branch will eventually end in a REVERT. The second branch is the instruction at the PC we pushed before JUMPI. Both of these locations need to be translated to instruction counters (i.e. program counters minus any push bytes leading up to that program counter) because this is how the source maps are indexed.For if statements, the general format is somewhat the same:
There is a special case though: if you have more complex conditions for
assert
,require
or your if statement, then there might be multiple sections of bytecode that match the same template. The last piece of bytecode that matches this template seems to be the "main" jump, so we always need to find the last JUMPI to find the branches.Closes #1967