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: LCOV report #442

Merged
merged 12 commits into from
Sep 25, 2024
2 changes: 1 addition & 1 deletion compilation/platforms/crytic_compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error)
}

// Retrieve the source unit ID
sourceUnitId := ast.GetSourceUnitID()
sourceUnitId := types.GetSrcMapSourceUnitID(ast.Src)
compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{
// TODO: Our types.AST is not the same as the original AST but we could parse it and avoid using "any"
Ast: source.AST,
Expand Down
2 changes: 1 addition & 1 deletion compilation/platforms/solc.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func (s *SolcCompilationConfig) Compile() ([]types.Compilation, string, error) {
}

// Get the source unit ID
sourceUnitId := ast.GetSourceUnitID()
sourceUnitId := types.GetSrcMapSourceUnitID(ast.Src)
// Construct our compiled source object
compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{
// TODO our types.AST is not the same as the original AST but we could parse it and avoid using "any"
Expand Down
120 changes: 105 additions & 15 deletions compilation/types/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,84 @@ const (

// Node interface represents a generic AST node
type Node interface {
// GetNodeType returns solc's node type e.g. FunctionDefinition, ContractDefinition.
GetNodeType() string
}

// FunctionDefinition is the function definition node
type FunctionDefinition struct {
// NodeType represents the node type (currently we only evaluate source unit node types)
NodeType string `json:"nodeType"`
// Src is the source file for this AST
Src string `json:"src"`
Name string `json:"name,omitempty"`
}

func (s FunctionDefinition) GetNodeType() string {
return s.NodeType
}

// ContractDefinition is the contract definition node
type ContractDefinition struct {
// NodeType represents the AST node type (note that it will always be a contract definition)
// NodeType represents the node type (currently we only evaluate source unit node types)
NodeType string `json:"nodeType"`
// Nodes is a list of Nodes within the AST
Nodes []Node `json:"nodes"`
// Src is the source file for this AST
Src string `json:"src"`
// CanonicalName is the name of the contract definition
CanonicalName string `json:"canonicalName,omitempty"`
// Kind is a ContractKind that represents what type of contract definition this is (contract, interface, or library)
Kind ContractKind `json:"contractKind,omitempty"`
}

// GetNodeType implements the Node interface and returns the node type for the contract definition
func (s ContractDefinition) GetNodeType() string {
return s.NodeType
}

func (c *ContractDefinition) UnmarshalJSON(data []byte) error {
// Unmarshal the top-level AST into our own representation. Defer the unmarshaling of all the individual nodes until later
type Alias ContractDefinition
aux := &struct {
Nodes []json.RawMessage `json:"nodes"`

*Alias
}{
Alias: (*Alias)(c),
}

if err := json.Unmarshal(data, &aux); err != nil {
return err
}

// Iterate through all the nodes of the contract definition
for _, nodeData := range aux.Nodes {
// Unmarshal the node data to retrieve the node type
var nodeType struct {
NodeType string `json:"nodeType"`
}
if err := json.Unmarshal(nodeData, &nodeType); err != nil {
return err
}

// Unmarshal the contents of the node based on the node type
switch nodeType.NodeType {
case "FunctionDefinition":
// If this is a function definition, unmarshal it
var functionDefinition FunctionDefinition
if err := json.Unmarshal(nodeData, &functionDefinition); err != nil {
return err
}
c.Nodes = append(c.Nodes, functionDefinition)
default:
continue
}
}

return nil

}

// AST is the abstract syntax tree
type AST struct {
// NodeType represents the node type (currently we only evaluate source unit node types)
Expand All @@ -48,7 +108,6 @@ type AST struct {
Src string `json:"src"`
}

// UnmarshalJSON unmarshals from JSON
func (a *AST) UnmarshalJSON(data []byte) error {
// Unmarshal the top-level AST into our own representation. Defer the unmarshaling of all the individual nodes until later
type Alias AST
Expand All @@ -62,11 +121,6 @@ func (a *AST) UnmarshalJSON(data []byte) error {
return err
}

// Check if nodeType is "SourceUnit". Return early otherwise
if aux.NodeType != "SourceUnit" {
return nil
}

// Iterate through all the nodes of the source unit
for _, nodeData := range aux.Nodes {
// Unmarshal the node data to retrieve the node type
Expand All @@ -78,31 +132,37 @@ func (a *AST) UnmarshalJSON(data []byte) error {
}

// Unmarshal the contents of the node based on the node type
var node Node
switch nodeType.NodeType {
case "ContractDefinition":
// If this is a contract definition, unmarshal it
var contractDefinition ContractDefinition
if err := json.Unmarshal(nodeData, &contractDefinition); err != nil {
return err
}
node = contractDefinition
a.Nodes = append(a.Nodes, contractDefinition)

case "FunctionDefinition":
// If this is a function definition, unmarshal it
var functionDefinition FunctionDefinition
if err := json.Unmarshal(nodeData, &functionDefinition); err != nil {
return err
}
a.Nodes = append(a.Nodes, functionDefinition)

// TODO: Add cases for other node types as needed
default:
continue
}

// Append the node
a.Nodes = append(a.Nodes, node)
}

return nil
}

// GetSourceUnitID returns the source unit ID based on the source of the AST
func (a *AST) GetSourceUnitID() int {
// GetSrcMapSourceUnitID returns the source unit ID based on the source of the AST
func GetSrcMapSourceUnitID(src string) int {
re := regexp.MustCompile(`[0-9]*:[0-9]*:([0-9]*)`)
sourceUnitCandidates := re.FindStringSubmatch(a.Src)
sourceUnitCandidates := re.FindStringSubmatch(src)

if len(sourceUnitCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element
sourceUnit, err := strconv.Atoi(sourceUnitCandidates[1])
Expand All @@ -112,3 +172,33 @@ func (a *AST) GetSourceUnitID() int {
}
return -1
}

// GetSrcMapStart returns the byte offset where the function definition starts in the source file
func GetSrcMapStart(src string) int {
// 95:42:0 returns 95
re := regexp.MustCompile(`([0-9]*):[0-9]*:[0-9]*`)
startCandidates := re.FindStringSubmatch(src)

if len(startCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element
start, err := strconv.Atoi(startCandidates[1])
if err == nil {
return start
}
}
return -1
}

// GetSrcMapLength returns the length of the function definition in bytes
func GetSrcMapLength(src string) int {
// 95:42:0 returns 42
re := regexp.MustCompile(`[0-9]*:([0-9]*):[0-9]*`)
endCandidates := re.FindStringSubmatch(src)

if len(endCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element
end, err := strconv.Atoi(endCandidates[1])
if err == nil {
return end
}
}
return -1
}
43 changes: 42 additions & 1 deletion docs/src/coverage_reports.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
# Coverage Reports

WIP
## Generating HTML Report from LCOV

Enable coverage reporting by setting the `corpusDirectory` key in the configuration file and setting the `coverageReports` key to `["lcov", "html"]`.

```json
{
"corpusDirectory": "corpus",
"coverageReports": ["lcov", "html"]
}
```

### Install lcov and genhtml

Linux:

```bash
apt-get install lcov
```

MacOS:

```bash
brew install lcov
```

### Generate LCOV Report

```bash

genhtml corpus/coverage/lcov.info --output-dir corpus --rc derive_function_end_line=0
```

> [!WARNING]
> ** The `derive_function_end_line` flag is required to prevent the `genhtml` tool from crashing when processing the Solidity source code. **

Open the `corpus/index.html` file in your browser or follow the steps to use VSCode below.

### View Coverage Report in VSCode with Coverage Gutters

Install the [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) extension.

Then, right click in a project file and select `Coverage Gutters: Display Coverage`.
7 changes: 7 additions & 0 deletions docs/src/project_configuration/fuzzing_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ The fuzzing configuration defines the parameters for the fuzzing campaign.
can then be re-used/mutated by the fuzzer during the next fuzzing campaign.
- **Default**: ""

### `coverageFormats`

- **Type**: [String] (e.g. `["lcov"]`)
- **Description**: The coverage reports to generate after the fuzzing campaign has completed. The coverage reports are saved
in the `coverage` directory within `crytic-export/` or `corpusDirectory` if configured.
- **Default**: `["lcov", "html"]`

### `targetContracts`

- **Type**: [String] (e.g. `[FirstContract, SecondContract, ThirdContract]`)
Expand Down
13 changes: 13 additions & 0 deletions fuzzing/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"encoding/json"
"errors"
"fmt"
"math/big"
"os"

Expand Down Expand Up @@ -60,6 +61,9 @@ type FuzzingConfig struct {
// CoverageEnabled describes whether to use coverage-guided fuzzing
CoverageEnabled bool `json:"coverageEnabled"`

// CoverageFormats indicate which reports to generate: "lcov" and "html" are supported.
CoverageFormats []string `json:"coverageFormats"`

// TargetContracts are the target contracts for fuzz testing
TargetContracts []string `json:"targetContracts"`

Expand Down Expand Up @@ -391,6 +395,15 @@ func (p *ProjectConfig) Validate() error {
}
}

// The coverage report format must be either "lcov" or "html"
if p.Fuzzing.CoverageFormats != nil {
for _, report := range p.Fuzzing.CoverageFormats {
if report != "lcov" && report != "html" {
return fmt.Errorf("project configuration must specify only valid coverage reports (lcov, html): %s", report)
}
}
}

// Ensure that the log level is a valid one
level, err := zerolog.ParseLevel(p.Logging.Level.String())
if err != nil || level == zerolog.FatalLevel {
Expand Down
1 change: 1 addition & 0 deletions fuzzing/config/config_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) {
ConstructorArgs: map[string]map[string]any{},
CorpusDirectory: "",
CoverageEnabled: true,
CoverageFormats: []string{"html", "lcov"},
SenderAddresses: []string{
"0x10000",
"0x20000",
Expand Down
6 changes: 6 additions & 0 deletions fuzzing/config/gen_fuzzing_config.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading