diff --git a/runtime/coverage.go b/runtime/coverage.go index a39e560c23..b20241da05 100644 --- a/runtime/coverage.go +++ b/runtime/coverage.go @@ -112,14 +112,17 @@ 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 `json:"-"` + locationFilter LocationFilter + // Contains a mapping with source paths for each + // location. + locationMappings map[string]string } // WithLocationFilter sets the LocationFilter for the current @@ -127,7 +130,15 @@ type CoverageReport struct { func (r *CoverageReport) WithLocationFilter( locationFilter LocationFilter, ) { - r.LocationFilter = locationFilter + 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 @@ -167,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) { @@ -405,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. @@ -423,7 +432,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.sourcePathForLocation(location) + coverage[locationSource] = lcAlias{ LineHits: locationCoverage.LineHits, MissedLines: locationCoverage.MissedLines(), Statements: locationCoverage.Statements, @@ -433,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), }) } @@ -448,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 @@ -505,7 +510,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.sourcePathForLocation(location) + _, err := fmt.Fprintf(buf, "TN:\nSF:%s\n", locationSource) if err != nil { return nil, err } @@ -539,3 +545,27 @@ 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) sourcePathForLocation(location common.Location) string { + var locationIdentifier string + + switch loc := location.(type) { + case common.AddressLocation: + locationIdentifier = loc.Name + case common.StringLocation: + locationIdentifier = loc.String() + case common.IdentifierLocation: + 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..8a5e780e0b 100644 --- a/runtime/coverage_test.go +++ b/runtime/coverage_test.go @@ -621,6 +621,117 @@ 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)) + }) + + 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) { t.Parallel() @@ -1790,43 +1901,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 +2027,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(), - ) }