Skip to content

Commit

Permalink
Extract statement coverage when executing repositories
Browse files Browse the repository at this point in the history
  • Loading branch information
bauersimon committed Apr 3, 2024
1 parent 244bb2c commit c98b469
Show file tree
Hide file tree
Showing 10 changed files with 60 additions and 15 deletions.
11 changes: 9 additions & 2 deletions evaluate/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"golang.org/x/exp/maps"
"gonum.org/v1/gonum/stat"
)

// Metrics holds numerical benchmarking metrics.
Expand All @@ -15,31 +16,37 @@ type Metrics struct {
Total uint
// Executed is the number of benchmarking candidates with successful execution.
Executed uint

// Coverage holds the coverage of the benchmarking candidates.
Coverage []float64
}

// Add sums two metrics objects.
func (m Metrics) Add(o Metrics) Metrics {
return Metrics{
Total: m.Total + o.Total,
Executed: m.Executed + o.Total,

Coverage: append(m.Coverage, o.Coverage...),
}
}

// String returns a string representation of the metrics.
func (m Metrics) String() string {
return fmt.Sprintf("#executed=%d/%d", m.Executed, m.Total)
return fmt.Sprintf("#executed=%d/%d, coverage=%v", m.Executed, m.Total, m.Coverage)
}

// String returns a string representation of the metrics.
func (m Metrics) StringPercentage() string {
return fmt.Sprintf("#executed=%3.0f%%", float64(m.Executed)/float64(m.Total)*100.0)
return fmt.Sprintf("#executed=%3.0f%%, average coverage=%3.0f%%", float64(m.Executed)/float64(m.Total)*100.0, stat.Mean(m.Coverage, nil))
}

// StringCSV returns a CSV row string representation of the metrics.
func (m Metrics) StringCSV() []string {
return []string{
fmt.Sprintf("%d", m.Total),
fmt.Sprintf("%d", m.Executed),
fmt.Sprintf("%.0f", stat.Mean(m.Coverage, nil)),
}
}

Expand Down
8 changes: 5 additions & 3 deletions evaluate/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestFormatStringCSV(t *testing.T) {
},

ExpectedString: `
Category,0,0
Category,0,0,NaN
`,
})
validate(t, &testCase{
Expand All @@ -43,16 +43,18 @@ func TestFormatStringCSV(t *testing.T) {
"CategoryA": Metrics{
Total: 5,
Executed: 3,
Coverage: []float64{100.0},
},
"CategoryB": Metrics{
Total: 4,
Executed: 2,
Coverage: []float64{70.0},
},
},

ExpectedString: `
CategoryA,5,3
CategoryB,4,2
CategoryA,5,3,100
CategoryB,4,2,70
`,
})
}
7 changes: 6 additions & 1 deletion evaluate/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package evaluate
import (
"errors"
"log"
"math"
"os"
"path/filepath"

Expand Down Expand Up @@ -51,12 +52,16 @@ func EvaluateRepository(model model.Model, language language.Language, repositor
continue
}

if err := language.Execute(temporaryRepositoryPath); err != nil {
coverage, err := language.Execute(temporaryRepositoryPath)
if err != nil {
problems = append(problems, pkgerrors.WithMessage(err, filePath))

continue
}
metrics.Total++
if !math.IsNaN(coverage) {
metrics.Coverage = append(metrics.Coverage, coverage)
}
}

return metrics, problems, nil
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/stretchr/testify v1.9.0
github.com/zimmski/osutil v1.1.0
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
gonum.org/v1/gonum v0.15.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=
gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
30 changes: 24 additions & 6 deletions language/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package language

import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -53,15 +54,18 @@ func (language *LanguageGolang) Files(repositoryPath string) (filePaths []string
}

var languageGoNoTestsMatch = regexp.MustCompile(`(?m)^DONE (\d+) tests.*in (.+?)$`)
var languageGoCoverageMatch = regexp.MustCompile(`(?m)^coverage: (\d+\.?\d+)% of statements`)
var languageGoNoCoverageMatch = regexp.MustCompile(`(?m)^coverage: \[no statements\]$`)

// Execute invokes the language specific testing on the given repository.
func (language *LanguageGolang) Execute(repositoryPath string) (err error) {
func (language *LanguageGolang) Execute(repositoryPath string) (coverage float64, err error) {
stdout, _, err := util.CommandWithResult(&util.Command{
Command: []string{
"gotestsum",
"--format", "standard-verbose", // Keep formatting consistent.
"--hide-summary", "skipped", // We are not interested in skipped tests, because they are the same as no tests at all.
"--", // Let the real Go "test" tool options begin.
"-cover", // Enable statement coverage.
"-v", // Output with the maximum information for easier debugging.
"-vet=off", // Disable all linter checks, because those should be part of a different task.
"./...", // Always execute all tests of the repository in case multiple test files have been generated.
Expand All @@ -70,19 +74,33 @@ func (language *LanguageGolang) Execute(repositoryPath string) (err error) {
Directory: repositoryPath,
})
if err != nil {
return pkgerrors.WithStack(err)
return 0.0, pkgerrors.WithStack(err)
}

ms := languageGoNoTestsMatch.FindStringSubmatch(stdout)
if ms == nil {
return pkgerrors.WithStack(errors.New("could not find Go test summary"))
return 0.0, pkgerrors.WithStack(errors.New("could not find Go test summary"))
}
testCount, err := strconv.ParseUint(ms[1], 10, 64)
if err != nil {
return pkgerrors.WithStack(err)
return 0.0, pkgerrors.WithStack(err)
} else if testCount == 0 {
return pkgerrors.WithStack(ErrNoTestFound)
return 0.0, pkgerrors.WithStack(ErrNoTestFound)
}

return nil
if languageGoNoCoverageMatch.MatchString(stdout) {
return 0.0, nil
}

mc := languageGoCoverageMatch.FindStringSubmatch(stdout)
if mc == nil {
fmt.Println(mc, stdout)
return 0.0, pkgerrors.WithStack(errors.New("could not find Go coverage report"))
}
coverage, err = strconv.ParseFloat(mc[1], 64)
if err != nil {
return 0.0, pkgerrors.WithStack(err)
}

return coverage, nil
}
6 changes: 5 additions & 1 deletion language/golang_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func TestLanguageGolangExecute(t *testing.T) {
RepositoryPath string
RepositoryChange func(t *testing.T, repositoryPath string)

ExpectedCoverage float64
ExpectedError error
ExpectedErrorText string
}
Expand All @@ -72,14 +73,15 @@ func TestLanguageGolangExecute(t *testing.T) {
if tc.LanguageGolang == nil {
tc.LanguageGolang = &LanguageGolang{}
}
actualError := tc.LanguageGolang.Execute(repositoryPath)
actualCoverage, actualError := tc.LanguageGolang.Execute(repositoryPath)

if tc.ExpectedError != nil {
assert.ErrorIs(t, actualError, tc.ExpectedError)
} else if actualError != nil && tc.ExpectedErrorText != "" {
assert.ErrorContains(t, actualError, tc.ExpectedErrorText)
} else {
assert.NoError(t, actualError)
assert.Equal(t, tc.ExpectedCoverage, actualCoverage)
}
})
}
Expand Down Expand Up @@ -110,6 +112,8 @@ func TestLanguageGolangExecute(t *testing.T) {
}
`)), 0660))
},

ExpectedCoverage: 100,
})

validate(t, &testCase{
Expand Down
2 changes: 1 addition & 1 deletion language/language.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Language interface {
Files(repositoryPath string) (filePaths []string, err error)

// Execute invokes the language specific testing on the given repository.
Execute(repositoryPath string) (err error)
Execute(repositoryPath string) (coverage float64, err error)
}

// Languages holds a register of all languages.
Expand Down
7 changes: 6 additions & 1 deletion model/symflower_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func TestModelSymflowerGenerateTestsForFile(t *testing.T) {
RepositoryChange func(t *testing.T, repositoryPath string)
FilePath string

ExpectedCoverage float64
ExpectedError error
ExpectedErrorText string
}
Expand All @@ -47,7 +48,9 @@ func TestModelSymflowerGenerateTestsForFile(t *testing.T) {
assert.ErrorContains(t, actualErr, tc.ExpectedErrorText)
}

require.NoError(t, tc.Language.Execute(repositoryPath))
actualCoverage, err := tc.Language.Execute(repositoryPath)
require.NoError(t, err)
assert.Equal(t, tc.ExpectedCoverage, actualCoverage)
})
}

Expand All @@ -58,5 +61,7 @@ func TestModelSymflowerGenerateTestsForFile(t *testing.T) {

RepositoryPath: "../testdata/golang/plain/",
FilePath: "plain.go",

ExpectedCoverage: 100,
})
}
1 change: 1 addition & 0 deletions testdata/golang/plain/plain.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package native

func plain() {
var _ int = 0
}

0 comments on commit c98b469

Please sign in to comment.