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
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/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/hrw/v2 v2.0.1 h1:CxYUkBeJvNfMEn2lHhrV6FjY8pZPceSxXUtMVq0BUOU=
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.20241007161539-0968c3a81fb0/go.mod h1:ds91T4WJwtk7eWUo0fuVC36HpTQKkkdj5AjNxbjXAR0=
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.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/go.mod h1:/vrbWSHc7YS1KSYhVOyyeucXW/e+1DkVBOgnBEXUCeY=
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) {}`),
&compiler.Options{Name: "foo"})
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)
cFrom.Invoke(t, 1, "totalSupply")
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,
Committee: committee,
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.
func (e *Executor) EnableCoverage() {
e.collectCoverage = isCoverageEnabled()
func (e *Executor) EnableCoverage(t testing.TB) {
e.collectCoverage = isCoverageEnabled(t)
}
// DisableCoverage disables coverage collection for this executor until enabled explicitly through EnableCoverage.

View file

@ -1,10 +1,12 @@
package neotest
import (
"cmp"
"flag"
"fmt"
"io"
"os"
"slices"
"strconv"
"sync"
"testing"
@ -16,13 +18,22 @@ import (
)
const (
// goCoverProfileFlag specifies the name of `go test` flag that tells it where to save coverage data.
// Neotest coverage can be enabled only when this flag is set.
// goCoverProfileFlag specifies the name of `go test` command flag `coverprofile`
// that tells it where to save coverage data. Neotest coverage can be enabled
// only when this flag is set.
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 = "DISABLE_NEOTEST_COVER"
)
const (
// goCoverModeSet is the name of "set" go test coverage mode.
goCoverModeSet = "set"
)
var (
// coverageLock protects all vars below from concurrent modification when tests are run in parallel.
coverageLock sync.Mutex
@ -34,6 +45,8 @@ var (
coverageEnabled bool
// coverProfile specifies the file all coverage data is written to, unless empty.
coverProfile = ""
// coverMode is the mode of go coverage collection.
coverMode = goCoverModeSet
)
type scriptRawCoverage struct {
@ -59,7 +72,7 @@ type coverBlock struct {
// documentName makes it clear when a `string` maps path to the script file.
type documentName = string
func isCoverageEnabled() bool {
func isCoverageEnabled(t testing.TB) bool {
coverageLock.Lock()
defer coverageLock.Unlock()
@ -72,7 +85,7 @@ func isCoverageEnabled() bool {
if v, ok := os.LookupEnv(disableNeotestCover); ok {
disabled, err := strconv.ParseBool(v)
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
}
@ -83,16 +96,22 @@ func isCoverageEnabled() bool {
goToolCoverageEnabled = true
coverProfile = f.Value.String()
}
if f.Name == goCoverModeFlag && f.Value != nil && f.Value.String() != "" {
coverMode = f.Value.String()
}
})
coverageEnabled = !disabledByEnvironment && goToolCoverageEnabled
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
// the file with our coverage when all tests are done.
err := flag.Set(goCoverProfileFlag, "")
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) {
fmt.Fprintf(w, "mode: set\n")
fmt.Fprintf(w, "mode: %s\n", coverMode)
cover := processCover()
for name, blocks := range cover {
for _, b := range blocks {
c := 0
if b.counts > 0 {
c = 1
var counts = b.counts
if coverMode == goCoverModeSet && counts > 0 {
counts = 1
}
fmt.Fprintf(w, "%s:%d.%d,%d.%d %d %d\n", name,
b.startLine, b.startCol,
b.endLine, b.endCol,
b.stmts,
c,
counts,
)
}
}
}
func processCover() map[documentName][]coverBlock {
func processCover() map[documentName][]*coverBlock {
documents := make(map[documentName]struct{})
for _, scriptRawCoverage := range rawCoverage {
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 {
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 {
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
}