Skip to content

Commit

Permalink
add coverage report config to select between lcov and html
Browse files Browse the repository at this point in the history
  • Loading branch information
0xalpharush committed Sep 9, 2024
1 parent be7d773 commit 37c0f38
Show file tree
Hide file tree
Showing 11 changed files with 125 additions and 87 deletions.
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
67 changes: 34 additions & 33 deletions compilation/types/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (

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

Expand All @@ -36,34 +37,6 @@ func (s FunctionDefinition) GetNodeType() string {
return s.NodeType
}

func (s FunctionDefinition) GetStart() int {
// 95:42:0 returns 95
re := regexp.MustCompile(`([0-9]*):[0-9]*:[0-9]*`)
startCandidates := re.FindStringSubmatch(s.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
}

func (s FunctionDefinition) GetLength() int {
// 95:42:0 returns 42
re := regexp.MustCompile(`[0-9]*:([0-9]*):[0-9]*`)
endCandidates := re.FindStringSubmatch(s.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
}

// ContractDefinition is the contract definition node
type ContractDefinition struct {
// NodeType represents the node type (currently we only evaluate source unit node types)
Expand All @@ -78,7 +51,6 @@ type ContractDefinition struct {
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
}
Expand Down Expand Up @@ -136,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 Down Expand Up @@ -188,10 +159,10 @@ func (a *AST) UnmarshalJSON(data []byte) error {
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 @@ -201,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
}
17 changes: 13 additions & 4 deletions docs/src/coverage_reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@

## Generating HTML Report from LCOV

Enable coverage reporting by passing a directory over the CLI (`--corpus-dir`) or by setting the `corpusDirectory` key in the configuration file.
Enable coverage reporting by setting the `corpusDirectory` key in the configuration file and setting the `coverageReports` key to `["lcov", "html"]`.

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

### Install lcov and genhtml

Linux:

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

MacOS:

Expand All @@ -23,9 +29,12 @@ brew install lcov

```bash

genhtml corpus/coverage/lcov.info --output-dir corpus
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
Expand Down
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**: ""

### `coverageReports`

- **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 `corpusDirectory`.
- **Default**: `["lcov", "html"]`

### `targetContracts`

- **Type**: [String] (e.g. `[FirstContract, SecondContract, ThirdContract]`)
Expand Down
16 changes: 16 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"`

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

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

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

// The coverage report format must be either "lcov" or "html"
if p.Fuzzing.CoverageReports != nil {
if p.Fuzzing.CorpusDirectory == "" {
return errors.New("project configuration must specify a corpus directory if coverage reports are enabled")
}
for _, report := range p.Fuzzing.CoverageReports {
if report != "lcov" && report != "html" {
return errors.New(fmt.Sprintf("project configuration must specify only valid coverage reports (lcov, html): %s", report))

Check failure on line 405 in fuzzing/config/config.go

View workflow job for this annotation

GitHub Actions / lint

S1028: should use fmt.Errorf(...) instead of errors.New(fmt.Sprintf(...)) (gosimple)
}
}
}

// 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,
CoverageReports: []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.

46 changes: 11 additions & 35 deletions fuzzing/coverage/report_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"strconv"
"time"

"github.com/crytic/medusa/compilation/types"
"github.com/crytic/medusa/utils"
)

Expand All @@ -19,31 +18,8 @@ var (
htmlReportTemplate []byte
)

// GenerateReports takes a set of CoverageMaps and compilations, and produces a coverage report using them, detailing
// all source mapped ranges of the source files which were covered or not.
// Returns an error if one occurred.
func GenerateReports(compilations []types.Compilation, coverageMaps *CoverageMaps, reportDir string) error {
// Perform source analysis.
sourceAnalysis, err := AnalyzeSourceCoverage(compilations, coverageMaps)
if err != nil {
return err
}

if reportDir != "" {
// Save the LCOV report.
err = saveLCOVReport(sourceAnalysis, reportDir)
if err != nil {
return err
}

// Save the HTML report.
err = saveHTMLReport(sourceAnalysis, reportDir)
}
return err
}

// saveHTMLReport takes a previously performed source analysis and generates an HTML coverage report from it.
func saveHTMLReport(sourceAnalysis *SourceAnalysis, reportDir string) error {
// WriteHTMLReport takes a previously performed source analysis and generates an HTML coverage report from it.
func WriteHTMLReport(sourceAnalysis *SourceAnalysis, reportDir string) (string, error) {
// Define mappings onto some useful variables/functions.
functionMap := template.FuncMap{
"timeNow": time.Now,
Expand Down Expand Up @@ -85,21 +61,21 @@ func saveHTMLReport(sourceAnalysis *SourceAnalysis, reportDir string) error {
// Parse our HTML template
tmpl, err := template.New("coverage_report.html").Funcs(functionMap).Parse(string(htmlReportTemplate))
if err != nil {
return fmt.Errorf("could not export report, failed to parse report template: %v", err)
return "", fmt.Errorf("could not export report, failed to parse report template: %v", err)
}

// If the directory doesn't exist, create it.
err = utils.MakeDirectory(reportDir)
if err != nil {
return err
return "", err
}

// Create our report file
htmlReportPath := filepath.Join(reportDir, "coverage_report.html")
file, err := os.Create(htmlReportPath)
if err != nil {
_ = file.Close()
return fmt.Errorf("could not export report, failed to open file for writing: %v", err)
return "", fmt.Errorf("could not export report, failed to open file for writing: %v", err)
}

// Execute the template and write it back to file.
Expand All @@ -108,26 +84,26 @@ func saveHTMLReport(sourceAnalysis *SourceAnalysis, reportDir string) error {
if err == nil {
err = fileCloseErr
}
return err
return htmlReportPath, err
}

// saveLCOVReport takes a previously performed source analysis and generates an LCOV report from it.
func saveLCOVReport(sourceAnalysis *SourceAnalysis, reportDir string) error {
// WriteLCOVReport takes a previously performed source analysis and generates an LCOV report from it.
func WriteLCOVReport(sourceAnalysis *SourceAnalysis, reportDir string) (string, error) {
// Generate the LCOV report.
lcovReport := sourceAnalysis.GenerateLCOVReport()

// If the directory doesn't exist, create it.
err := utils.MakeDirectory(reportDir)
if err != nil {
return err
return "", err
}

// Write the LCOV report to a file.
lcovReportPath := filepath.Join(reportDir, "lcov.info")
err = os.WriteFile(lcovReportPath, []byte(lcovReport), 0644)
if err != nil {
return fmt.Errorf("could not export LCOV report: %v", err)
return "", fmt.Errorf("could not export LCOV report: %v", err)
}

return nil
return lcovReportPath, nil
}
Loading

0 comments on commit 37c0f38

Please sign in to comment.