Merge pull request #3616 from nspcc-dev/coverage-enh

neotest: coverage collection polishing
This commit is contained in:
Roman Khimov 2024-10-16 16:14:04 +03:00 committed by GitHub
commit 86ed214e8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 49 additions and 21 deletions

View file

@ -3,7 +3,7 @@ module github.com/nspcc-dev/neo-go/examples/nft-nd-nns
go 1.22 go 1.22
require ( require (
github.com/nspcc-dev/neo-go v0.106.4-0.20241007161539-0968c3a81fb0 github.com/nspcc-dev/neo-go v0.106.4-0.20241016130346-d8e945978af6
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
) )

View file

@ -74,8 +74,8 @@ github.com/nspcc-dev/go-ordered-json v0.0.0-20240830112754-291b000d1f3b h1:DRG4c
github.com/nspcc-dev/go-ordered-json v0.0.0-20240830112754-291b000d1f3b/go.mod h1:d3cUseu4Asxfo9/QA/w4TtGjM0AbC9ynyab+PfH+Bso= github.com/nspcc-dev/go-ordered-json v0.0.0-20240830112754-291b000d1f3b/go.mod h1:d3cUseu4Asxfo9/QA/w4TtGjM0AbC9ynyab+PfH+Bso=
github.com/nspcc-dev/hrw/v2 v2.0.1 h1:CxYUkBeJvNfMEn2lHhrV6FjY8pZPceSxXUtMVq0BUOU= github.com/nspcc-dev/hrw/v2 v2.0.1 h1:CxYUkBeJvNfMEn2lHhrV6FjY8pZPceSxXUtMVq0BUOU=
github.com/nspcc-dev/hrw/v2 v2.0.1/go.mod h1:iZAs5hT2q47EGq6AZ0FjaUI6ggntOi7vrY4utfzk5VA= github.com/nspcc-dev/hrw/v2 v2.0.1/go.mod h1:iZAs5hT2q47EGq6AZ0FjaUI6ggntOi7vrY4utfzk5VA=
github.com/nspcc-dev/neo-go v0.106.4-0.20241007161539-0968c3a81fb0 h1:8IYGFnZQ+wyH5Qdtq4oxBEyiWSNMdi6AXSHQunyZ9VU= github.com/nspcc-dev/neo-go v0.106.4-0.20241016130346-d8e945978af6 h1:3/V1U2ZgbFZQ5xGjTD9NMsz4NHzPy/nYJzcaHDVDu88=
github.com/nspcc-dev/neo-go v0.106.4-0.20241007161539-0968c3a81fb0/go.mod h1:ds91T4WJwtk7eWUo0fuVC36HpTQKkkdj5AjNxbjXAR0= github.com/nspcc-dev/neo-go v0.106.4-0.20241016130346-d8e945978af6/go.mod h1:ds91T4WJwtk7eWUo0fuVC36HpTQKkkdj5AjNxbjXAR0=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec h1:vDrbVXF2+2uP0RlkZmem3QYATcXCu9BzzGGCNsNcK7Q= github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec h1:vDrbVXF2+2uP0RlkZmem3QYATcXCu9BzzGGCNsNcK7Q=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec/go.mod h1:/vrbWSHc7YS1KSYhVOyyeucXW/e+1DkVBOgnBEXUCeY= github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec/go.mod h1:/vrbWSHc7YS1KSYhVOyyeucXW/e+1DkVBOgnBEXUCeY=
github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240305074711-35bc78d84dc4 h1:arN0Ypn+jawZpu1BND7TGRn44InAVIqKygndsx0y2no= github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240305074711-35bc78d84dc4 h1:arN0Ypn+jawZpu1BND7TGRn44InAVIqKygndsx0y2no=

View file

@ -381,7 +381,7 @@ func TestTransfer(t *testing.T) {
func OnNEP11Payment(from interop.Hash160, amount int, token []byte, data any) {}`), func OnNEP11Payment(from interop.Hash160, amount int, token []byte, data any) {}`),
&compiler.Options{Name: "foo"}) &compiler.Options{Name: "foo"})
e.DeployContract(t, ctr, nil) e.DeployContract(t, ctr, nil)
e.EnableCoverage() // contracts above have no source files which leads to unprocessable coverage data e.EnableCoverage(t) // contracts above have no source files which leads to unprocessable coverage data
cTo.Invoke(t, true, "transfer", ctr.Hash, []byte("neo.com"), nil) cTo.Invoke(t, true, "transfer", ctr.Hash, []byte("neo.com"), nil)
cFrom.Invoke(t, 1, "totalSupply") cFrom.Invoke(t, 1, "totalSupply")
cFrom.Invoke(t, ctr.Hash.BytesBE(), "ownerOf", []byte("neo.com")) cFrom.Invoke(t, ctr.Hash.BytesBE(), "ownerOf", []byte("neo.com"))

View file

@ -48,7 +48,7 @@ func NewExecutor(t testing.TB, bc *core.Blockchain, validator, committee Signer)
Validator: validator, Validator: validator,
Committee: committee, Committee: committee,
CommitteeHash: committee.ScriptHash(), CommitteeHash: committee.ScriptHash(),
collectCoverage: isCoverageEnabled(), collectCoverage: isCoverageEnabled(t),
} }
} }
@ -452,8 +452,8 @@ func (e *Executor) GetTxExecResult(t testing.TB, h util.Uint256) *state.AppExecR
} }
// EnableCoverage enables coverage collection for this executor, but only when `go test` is running with coverage enabled. // EnableCoverage enables coverage collection for this executor, but only when `go test` is running with coverage enabled.
func (e *Executor) EnableCoverage() { func (e *Executor) EnableCoverage(t testing.TB) {
e.collectCoverage = isCoverageEnabled() e.collectCoverage = isCoverageEnabled(t)
} }
// DisableCoverage disables coverage collection for this executor until enabled explicitly through EnableCoverage. // DisableCoverage disables coverage collection for this executor until enabled explicitly through EnableCoverage.

View file

@ -1,10 +1,12 @@
package neotest package neotest
import ( import (
"cmp"
"flag" "flag"
"fmt" "fmt"
"io" "io"
"os" "os"
"slices"
"strconv" "strconv"
"sync" "sync"
"testing" "testing"
@ -16,13 +18,22 @@ import (
) )
const ( const (
// goCoverProfileFlag specifies the name of `go test` flag that tells it where to save coverage data. // goCoverProfileFlag specifies the name of `go test` command flag `coverprofile`
// Neotest coverage can be enabled only when this flag is set. // that tells it where to save coverage data. Neotest coverage can be enabled
// only when this flag is set.
goCoverProfileFlag = "test.coverprofile" goCoverProfileFlag = "test.coverprofile"
// goCoverModeFlag specifies the name of `go test` command flag `covermode` that
// specifies the coverage calculation mode.
goCoverModeFlag = "test.covermode"
// disableNeotestCover is name of the environmental variable used to explicitly disable neotest coverage. // disableNeotestCover is name of the environmental variable used to explicitly disable neotest coverage.
disableNeotestCover = "DISABLE_NEOTEST_COVER" disableNeotestCover = "DISABLE_NEOTEST_COVER"
) )
const (
// goCoverModeSet is the name of "set" go test coverage mode.
goCoverModeSet = "set"
)
var ( var (
// coverageLock protects all vars below from concurrent modification when tests are run in parallel. // coverageLock protects all vars below from concurrent modification when tests are run in parallel.
coverageLock sync.Mutex coverageLock sync.Mutex
@ -34,6 +45,8 @@ var (
coverageEnabled bool coverageEnabled bool
// coverProfile specifies the file all coverage data is written to, unless empty. // coverProfile specifies the file all coverage data is written to, unless empty.
coverProfile = "" coverProfile = ""
// coverMode is the mode of go coverage collection.
coverMode = goCoverModeSet
) )
type scriptRawCoverage struct { type scriptRawCoverage struct {
@ -59,7 +72,7 @@ type coverBlock struct {
// documentName makes it clear when a `string` maps path to the script file. // documentName makes it clear when a `string` maps path to the script file.
type documentName = string type documentName = string
func isCoverageEnabled() bool { func isCoverageEnabled(t testing.TB) bool {
coverageLock.Lock() coverageLock.Lock()
defer coverageLock.Unlock() defer coverageLock.Unlock()
@ -72,7 +85,7 @@ func isCoverageEnabled() bool {
if v, ok := os.LookupEnv(disableNeotestCover); ok { if v, ok := os.LookupEnv(disableNeotestCover); ok {
disabled, err := strconv.ParseBool(v) disabled, err := strconv.ParseBool(v)
if err != nil { if err != nil {
panic(fmt.Sprintf("coverage: error when parsing environment variable '%s', expected bool, but got '%s'", disableNeotestCover, v)) t.Fatalf("coverage: error when parsing environment variable '%s', expected bool, but got '%s'", disableNeotestCover, v)
} }
disabledByEnvironment = disabled disabledByEnvironment = disabled
} }
@ -83,16 +96,22 @@ func isCoverageEnabled() bool {
goToolCoverageEnabled = true goToolCoverageEnabled = true
coverProfile = f.Value.String() coverProfile = f.Value.String()
} }
if f.Name == goCoverModeFlag && f.Value != nil && f.Value.String() != "" {
coverMode = f.Value.String()
}
}) })
coverageEnabled = !disabledByEnvironment && goToolCoverageEnabled coverageEnabled = !disabledByEnvironment && goToolCoverageEnabled
if coverageEnabled { if coverageEnabled {
if coverMode != goCoverModeSet {
t.Fatalf("coverage: only '%s' cover mode is currently supported (#3587), got '%s'", goCoverModeSet, coverMode)
}
// This is needed so go cover tool doesn't overwrite // This is needed so go cover tool doesn't overwrite
// the file with our coverage when all tests are done. // the file with our coverage when all tests are done.
err := flag.Set(goCoverProfileFlag, "") err := flag.Set(goCoverProfileFlag, "")
if err != nil { if err != nil {
panic(err) t.Fatalf("coverage: failed to overwrite coverprofile flag: %v", err)
} }
} }
@ -119,25 +138,25 @@ func reportCoverage(t testing.TB) {
} }
func writeCoverageReport(w io.Writer) { func writeCoverageReport(w io.Writer) {
fmt.Fprintf(w, "mode: set\n") fmt.Fprintf(w, "mode: %s\n", coverMode)
cover := processCover() cover := processCover()
for name, blocks := range cover { for name, blocks := range cover {
for _, b := range blocks { for _, b := range blocks {
c := 0 var counts = b.counts
if b.counts > 0 { if coverMode == goCoverModeSet && counts > 0 {
c = 1 counts = 1
} }
fmt.Fprintf(w, "%s:%d.%d,%d.%d %d %d\n", name, fmt.Fprintf(w, "%s:%d.%d,%d.%d %d %d\n", name,
b.startLine, b.startCol, b.startLine, b.startCol,
b.endLine, b.endCol, b.endLine, b.endCol,
b.stmts, b.stmts,
c, counts,
) )
} }
} }
} }
func processCover() map[documentName][]coverBlock { func processCover() map[documentName][]*coverBlock {
documents := make(map[documentName]struct{}) documents := make(map[documentName]struct{})
for _, scriptRawCoverage := range rawCoverage { for _, scriptRawCoverage := range rawCoverage {
for _, documentName := range scriptRawCoverage.debugInfo.Documents { for _, documentName := range scriptRawCoverage.debugInfo.Documents {
@ -145,7 +164,7 @@ func processCover() map[documentName][]coverBlock {
} }
} }
cover := make(map[documentName][]coverBlock) cover := make(map[documentName][]*coverBlock)
for documentName := range documents { for documentName := range documents {
mappedBlocks := make(map[int]*coverBlock) mappedBlocks := make(map[int]*coverBlock)
@ -180,10 +199,19 @@ func processCover() map[documentName][]coverBlock {
} }
} }
var blocks []coverBlock var blocks = make([]*coverBlock, 0, len(mappedBlocks))
for _, b := range mappedBlocks { for _, b := range mappedBlocks {
blocks = append(blocks, *b) blocks = append(blocks, b)
} }
slices.SortFunc(blocks, func(a, b *coverBlock) int {
return cmp.Or(
cmp.Compare(a.startLine, b.startLine),
cmp.Compare(a.endLine, b.endLine),
cmp.Compare(a.startCol, b.startCol),
cmp.Compare(a.endCol, b.endCol),
cmp.Compare(a.stmts, b.stmts),
cmp.Compare(a.counts, b.counts))
})
cover[documentName] = blocks cover[documentName] = blocks
} }