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(), - ) }