Skip to content
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

Merged
merged 11 commits into from Jun 28, 2022
Merged

Conversation

onbjerg
Copy link
Member

@onbjerg onbjerg commented Jun 27, 2022

Some small refactoring and solution for #1967

Motivation

We want good branch coverage in forge coverage, which means detecting the following scenarios:

  • Calls to assert and require should result in branching (one branch reverts, the other one doesn't)
  • If statements with no else block should have an implicit else
  • Else if statements should also be considered a branch

Solution

assert and require require us to inspect the bytecode that the source map identifies as representing them. Generally, this bytecode follows the format:

PUSH [PC if condition is true]
JUMPI
[eventually revert]

That is, we need to find bytecode that identifies an assert or a require 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:

PUSH [PC if condition is false]
JUMPI
[true branch]

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

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.
@onbjerg onbjerg added the T-feature Type: feature label Jun 27, 2022
@onbjerg onbjerg changed the title Onbjerg/coverage branches feat(coverage): better branch handling Jun 27, 2022
Comment on lines 393 to 385
.unwrap_or(loc.start);
.ok_or_else(|| {
eyre::eyre!("could not find anchor: no matching instruction in range")
})?;
Copy link
Member Author

@onbjerg onbjerg Jun 27, 2022

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

@onbjerg
Copy link
Member Author

onbjerg commented Jun 28, 2022

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 :)

@onbjerg onbjerg marked this pull request as ready for review June 28, 2022 07:22
Comment on lines +488 to +494
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
};
Copy link
Member Author

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?

/// 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>>,
Copy link
Member

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

Copy link
Member Author

@onbjerg onbjerg Jun 28, 2022

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

Copy link
Member

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...

@onbjerg onbjerg merged commit 5279f69 into master Jun 28, 2022
@onbjerg onbjerg deleted the onbjerg/coverage-branches branch June 28, 2022 15:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-feature Type: feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Coverage: Better branch handling (require/assert)
2 participants