From 38640c081d277918d842221a9131ebbe6a1010d3 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 9 Oct 2023 15:44:19 +0300 Subject: [PATCH 1/3] Allow specifying a mapping with human-friendly aliases for locations in coverage reports Having AddressLocation or StringLocation in coverage reports, is not usable when trying to use these reports with standard tools, such as LCOV. --- runtime/coverage.go | 39 ++++++- runtime/coverage_test.go | 236 ++++++++++++++++++++++++++++++++------- 2 files changed, 233 insertions(+), 42 deletions(-) diff --git a/runtime/coverage.go b/runtime/coverage.go index a39e560c23..599b2975c7 100644 --- a/runtime/coverage.go +++ b/runtime/coverage.go @@ -120,6 +120,9 @@ type CoverageReport struct { // This filter can be used to inject custom logic on // each location/program inspection. LocationFilter LocationFilter `json:"-"` + // Contains a mapping with human-friendly names for + // locations. This could be the filepath of a location. + LocationMappings map[string]string `json:"-"` } // WithLocationFilter sets the LocationFilter for the current @@ -130,6 +133,14 @@ func (r *CoverageReport) WithLocationFilter( r.LocationFilter = locationFilter } +// WithLocationMappings sets the LocationMappings for the current +// CoverageReport. +func (r *CoverageReport) WithLocationMappings( + locationMappings map[string]string, +) { + r.LocationMappings = locationMappings +} + // ExcludeLocation adds the given location to the map of excluded // locations. func (r *CoverageReport) ExcludeLocation(location Location) { @@ -423,7 +434,8 @@ type lcAlias struct { func (r *CoverageReport) MarshalJSON() ([]byte, error) { coverage := make(map[string]lcAlias, len(r.Coverage)) for location, locationCoverage := range r.Coverage { // nolint:maprange - coverage[location.ID()] = lcAlias{ + locationSource := r.locationSource(location) + coverage[locationSource] = lcAlias{ LineHits: locationCoverage.LineHits, MissedLines: locationCoverage.MissedLines(), Statements: locationCoverage.Statements, @@ -505,7 +517,8 @@ func (r *CoverageReport) MarshalLCOV() ([]byte, error) { buf := new(bytes.Buffer) for _, location := range locations { coverage := r.Coverage[location] - _, err := fmt.Fprintf(buf, "TN:\nSF:%s\n", location.ID()) + locationSource := r.locationSource(location) + _, err := fmt.Fprintf(buf, "TN:\nSF:%s\n", locationSource) if err != nil { return nil, err } @@ -539,3 +552,25 @@ func (r *CoverageReport) MarshalLCOV() ([]byte, error) { return buf.Bytes(), nil } + +// Given a common.Location, returns its mapped source, if any. +// Defaults to the location's ID(). +func (r *CoverageReport) locationSource(location common.Location) string { + var locationIdentifier string + + switch loc := location.(type) { + case common.AddressLocation: + locationIdentifier = loc.Name + case common.StringLocation: + locationIdentifier = loc.String() + default: + locationIdentifier = loc.ID() + } + + locationSource, ok := r.LocationMappings[locationIdentifier] + if !ok { + locationSource = location.ID() + } + + return locationSource +} diff --git a/runtime/coverage_test.go b/runtime/coverage_test.go index 951aa16033..3fcc47119c 100644 --- a/runtime/coverage_test.go +++ b/runtime/coverage_test.go @@ -621,6 +621,89 @@ func TestCoverageReportWithAddressLocation(t *testing.T) { require.JSONEq(t, expected, string(actual)) } +func TestCoverageReportWithLocationMappings(t *testing.T) { + + t.Parallel() + + script := []byte(` + pub fun answer(): Int { + var i = 0 + while i < 42 { + i = i + 1 + } + return i + } + `) + + program, err := parser.ParseProgram(nil, script, parser.Config{}) + require.NoError(t, err) + + locationMappings := map[string]string{ + "Answer": "cadence/scripts/answer.cdc", + } + coverageReport := NewCoverageReport() + coverageReport.WithLocationMappings(locationMappings) + + t.Run("with AddressLocation", func(t *testing.T) { + location := common.AddressLocation{ + Address: common.MustBytesToAddress([]byte{1, 2}), + Name: "Answer", + } + coverageReport.InspectProgram(location, program) + + actual, err := json.Marshal(coverageReport) + require.NoError(t, err) + + expected := ` + { + "coverage": { + "cadence/scripts/answer.cdc": { + "line_hits": { + "3": 0, + "4": 0, + "5": 0, + "7": 0 + }, + "missed_lines": [3, 4, 5, 7], + "statements": 4, + "percentage": "0.0%" + } + }, + "excluded_locations": [] + } + ` + require.JSONEq(t, expected, string(actual)) + }) + + t.Run("with StringLocation", func(t *testing.T) { + location := common.StringLocation("Answer") + coverageReport.InspectProgram(location, program) + + actual, err := json.Marshal(coverageReport) + require.NoError(t, err) + + expected := ` + { + "coverage": { + "cadence/scripts/answer.cdc": { + "line_hits": { + "3": 0, + "4": 0, + "5": 0, + "7": 0 + }, + "missed_lines": [3, 4, 5, 7], + "statements": 4, + "percentage": "0.0%" + } + }, + "excluded_locations": [] + } + ` + require.JSONEq(t, expected, string(actual)) + }) +} + func TestCoverageReportReset(t *testing.T) { t.Parallel() @@ -1790,43 +1873,114 @@ func TestCoverageReportLCOVFormat(t *testing.T) { } `) - coverageReport := NewCoverageReport() - scriptlocation := common.ScriptLocation{} - coverageReport.ExcludeLocation(scriptlocation) - - runtimeInterface := &testRuntimeInterface{ - getCode: func(location Location) (bytes []byte, err error) { - switch location { - case common.StringLocation("IntegerTraits"): - return integerTraits, nil - default: - return nil, fmt.Errorf("unknown import location: %s", location) - } - }, - } - - runtime := newTestInterpreterRuntime() - runtime.defaultConfig.CoverageReport = coverageReport - - value, err := runtime.ExecuteScript( - Script{ - Source: script, - }, - Context{ - Interface: runtimeInterface, - Location: scriptlocation, - CoverageReport: coverageReport, - }, - ) - require.NoError(t, err) + t.Run("without location mappings", func(t *testing.T) { + coverageReport := NewCoverageReport() + scriptlocation := common.ScriptLocation{} + coverageReport.ExcludeLocation(scriptlocation) + + runtimeInterface := &testRuntimeInterface{ + getCode: func(location Location) (bytes []byte, err error) { + switch location { + case common.StringLocation("IntegerTraits"): + return integerTraits, nil + default: + return nil, fmt.Errorf("unknown import location: %s", location) + } + }, + } + + runtime := newTestInterpreterRuntime() + runtime.defaultConfig.CoverageReport = coverageReport + + value, err := runtime.ExecuteScript( + Script{ + Source: script, + }, + Context{ + Interface: runtimeInterface, + Location: scriptlocation, + CoverageReport: coverageReport, + }, + ) + require.NoError(t, err) + + assert.Equal(t, cadence.NewInt(42), value) + + actual, err := coverageReport.MarshalLCOV() + require.NoError(t, err) + + expected := `TN: +SF:S.IntegerTraits +DA:9,1 +DA:13,10 +DA:14,1 +DA:15,9 +DA:16,1 +DA:17,8 +DA:18,1 +DA:19,7 +DA:20,1 +DA:21,6 +DA:22,1 +DA:25,5 +DA:26,4 +DA:29,1 +LF:14 +LH:14 +end_of_record +` - assert.Equal(t, cadence.NewInt(42), value) + require.Equal(t, expected, string(actual)) - actual, err := coverageReport.MarshalLCOV() - require.NoError(t, err) + assert.Equal( + t, + "Coverage: 100.0% of statements", + coverageReport.String(), + ) + }) - expected := `TN: -SF:S.IntegerTraits + t.Run("with location mappings", func(t *testing.T) { + locationMappings := map[string]string{ + "IntegerTraits": "cadence/contracts/IntegerTraits.cdc", + } + coverageReport := NewCoverageReport() + coverageReport.WithLocationMappings(locationMappings) + scriptlocation := common.ScriptLocation{} + coverageReport.ExcludeLocation(scriptlocation) + + runtimeInterface := &testRuntimeInterface{ + getCode: func(location Location) (bytes []byte, err error) { + switch location { + case common.StringLocation("IntegerTraits"): + return integerTraits, nil + default: + return nil, fmt.Errorf("unknown import location: %s", location) + } + }, + } + + runtime := newTestInterpreterRuntime() + runtime.defaultConfig.CoverageReport = coverageReport + + value, err := runtime.ExecuteScript( + Script{ + Source: script, + }, + Context{ + Interface: runtimeInterface, + Location: scriptlocation, + CoverageReport: coverageReport, + }, + ) + require.NoError(t, err) + + assert.Equal(t, cadence.NewInt(42), value) + + actual, err := coverageReport.MarshalLCOV() + require.NoError(t, err) + + expected := `TN: +SF:cadence/contracts/IntegerTraits.cdc DA:9,1 DA:13,10 DA:14,1 @@ -1845,11 +1999,13 @@ LF:14 LH:14 end_of_record ` - require.Equal(t, expected, string(actual)) + require.Equal(t, expected, string(actual)) + + assert.Equal( + t, + "Coverage: 100.0% of statements", + coverageReport.String(), + ) + }) - assert.Equal( - t, - "Coverage: 100.0% of statements", - coverageReport.String(), - ) } From 4871d50d108d715a582746143ff09d72e78c525b Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Sun, 15 Oct 2023 18:03:44 +0300 Subject: [PATCH 2/3] Handle identifier locations and make some fields non-exported --- runtime/coverage.go | 24 +++++++++++++----------- runtime/coverage_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/runtime/coverage.go b/runtime/coverage.go index 599b2975c7..999207fdce 100644 --- a/runtime/coverage.go +++ b/runtime/coverage.go @@ -119,10 +119,10 @@ type CoverageReport struct { ExcludedLocations map[common.Location]struct{} `json:"-"` // This filter can be used to inject custom logic on // each location/program inspection. - LocationFilter LocationFilter `json:"-"` - // Contains a mapping with human-friendly names for - // locations. This could be the filepath of a location. - LocationMappings map[string]string `json:"-"` + locationFilter LocationFilter + // Contains a mapping with source paths for each + // location. + locationMappings map[string]string } // WithLocationFilter sets the LocationFilter for the current @@ -130,7 +130,7 @@ type CoverageReport struct { func (r *CoverageReport) WithLocationFilter( locationFilter LocationFilter, ) { - r.LocationFilter = locationFilter + r.locationFilter = locationFilter } // WithLocationMappings sets the LocationMappings for the current @@ -138,7 +138,7 @@ func (r *CoverageReport) WithLocationFilter( func (r *CoverageReport) WithLocationMappings( locationMappings map[string]string, ) { - r.LocationMappings = locationMappings + r.locationMappings = locationMappings } // ExcludeLocation adds the given location to the map of excluded @@ -178,7 +178,7 @@ func (r *CoverageReport) AddLineHit(location Location, line int) { // If the CoverageReport.LocationFilter is present, and calling it with the given // location results to false, the method call also results in a NO-OP. func (r *CoverageReport) InspectProgram(location Location, program *ast.Program) { - if r.LocationFilter != nil && !r.LocationFilter(location) { + if r.locationFilter != nil && !r.locationFilter(location) { return } if r.IsLocationExcluded(location) { @@ -434,7 +434,7 @@ type lcAlias struct { func (r *CoverageReport) MarshalJSON() ([]byte, error) { coverage := make(map[string]lcAlias, len(r.Coverage)) for location, locationCoverage := range r.Coverage { // nolint:maprange - locationSource := r.locationSource(location) + locationSource := r.sourcePathForLocation(location) coverage[locationSource] = lcAlias{ LineHits: locationCoverage.LineHits, MissedLines: locationCoverage.MissedLines(), @@ -517,7 +517,7 @@ func (r *CoverageReport) MarshalLCOV() ([]byte, error) { buf := new(bytes.Buffer) for _, location := range locations { coverage := r.Coverage[location] - locationSource := r.locationSource(location) + locationSource := r.sourcePathForLocation(location) _, err := fmt.Fprintf(buf, "TN:\nSF:%s\n", locationSource) if err != nil { return nil, err @@ -555,7 +555,7 @@ func (r *CoverageReport) MarshalLCOV() ([]byte, error) { // Given a common.Location, returns its mapped source, if any. // Defaults to the location's ID(). -func (r *CoverageReport) locationSource(location common.Location) string { +func (r *CoverageReport) sourcePathForLocation(location common.Location) string { var locationIdentifier string switch loc := location.(type) { @@ -563,11 +563,13 @@ func (r *CoverageReport) locationSource(location common.Location) string { locationIdentifier = loc.Name case common.StringLocation: locationIdentifier = loc.String() + case common.IdentifierLocation: + locationIdentifier = loc.String() default: locationIdentifier = loc.ID() } - locationSource, ok := r.LocationMappings[locationIdentifier] + locationSource, ok := r.locationMappings[locationIdentifier] if !ok { locationSource = location.ID() } diff --git a/runtime/coverage_test.go b/runtime/coverage_test.go index 3fcc47119c..8a5e780e0b 100644 --- a/runtime/coverage_test.go +++ b/runtime/coverage_test.go @@ -702,6 +702,34 @@ func TestCoverageReportWithLocationMappings(t *testing.T) { ` require.JSONEq(t, expected, string(actual)) }) + + t.Run("with IdentifierLocation", func(t *testing.T) { + location := common.IdentifierLocation("Answer") + coverageReport.InspectProgram(location, program) + + actual, err := json.Marshal(coverageReport) + require.NoError(t, err) + + expected := ` + { + "coverage": { + "cadence/scripts/answer.cdc": { + "line_hits": { + "3": 0, + "4": 0, + "5": 0, + "7": 0 + }, + "missed_lines": [3, 4, 5, 7], + "statements": 4, + "percentage": "0.0%" + } + }, + "excluded_locations": [] + } + ` + require.JSONEq(t, expected, string(actual)) + }) } func TestCoverageReportReset(t *testing.T) { From 6b9d9f2fcd558ec316794b345bb8d9a9aadd0525 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 16 Oct 2023 19:37:44 +0300 Subject: [PATCH 3/3] Remove crAlias relic type and update JSON tags for CoverageReport --- runtime/coverage.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/runtime/coverage.go b/runtime/coverage.go index 999207fdce..b20241da05 100644 --- a/runtime/coverage.go +++ b/runtime/coverage.go @@ -112,11 +112,11 @@ type LocationFilter func(location Location) bool // locations from coverage collection. type CoverageReport struct { // Contains a *LocationCoverage per location. - Coverage map[common.Location]*LocationCoverage `json:"-"` + Coverage map[common.Location]*LocationCoverage // Contains locations whose programs are already inspected. - Locations map[common.Location]struct{} `json:"-"` + Locations map[common.Location]struct{} // Contains locations excluded from coverage collection. - ExcludedLocations map[common.Location]struct{} `json:"-"` + ExcludedLocations map[common.Location]struct{} // This filter can be used to inject custom logic on // each location/program inspection. locationFilter LocationFilter @@ -416,8 +416,6 @@ func NewCoverageReport() *CoverageReport { } } -type crAlias CoverageReport - // To avoid the overhead of having the Percentage & MissedLines // as fields in the LocationCoverage struct, we simply populate // this lcAlias struct, with the corresponding methods, upon marshalling. @@ -445,11 +443,9 @@ func (r *CoverageReport) MarshalJSON() ([]byte, error) { return json.Marshal(&struct { Coverage map[string]lcAlias `json:"coverage"` ExcludedLocations []string `json:"excluded_locations"` - *crAlias }{ Coverage: coverage, ExcludedLocations: r.ExcludedLocationIDs(), - crAlias: (*crAlias)(r), }) } @@ -460,10 +456,7 @@ func (r *CoverageReport) UnmarshalJSON(data []byte) error { cr := &struct { Coverage map[string]lcAlias `json:"coverage"` ExcludedLocations []string `json:"excluded_locations"` - *crAlias - }{ - crAlias: (*crAlias)(r), - } + }{} if err := json.Unmarshal(data, cr); err != nil { return err