From 794f2048f1eadb32fa1b66d47d097183e5a71355 Mon Sep 17 00:00:00 2001 From: Slava0135 Date: Thu, 12 Sep 2024 11:08:45 +0300 Subject: [PATCH] neotest: fix coverage blocks overlapping Signed-off-by: Slava0135 --- pkg/neotest/coverage.go | 77 +++++++-- pkg/neotest/coverage_test.go | 301 +++++++++++++++++++++++++++++++++++ 2 files changed, 361 insertions(+), 17 deletions(-) create mode 100644 pkg/neotest/coverage_test.go diff --git a/pkg/neotest/coverage.go b/pkg/neotest/coverage.go index c832b0a43e..1cfd8c4152 100644 --- a/pkg/neotest/coverage.go +++ b/pkg/neotest/coverage.go @@ -59,6 +59,11 @@ type coverBlock struct { // documentName makes it clear when a `string` maps path to the script file. type documentName = string +type interval struct { + compiler.DebugSeqPoint + remove bool +} + func isCoverageEnabled() bool { coverageLock.Lock() defer coverageLock.Unlock() @@ -150,31 +155,33 @@ func processCover() map[documentName][]coverBlock { for documentName := range documents { mappedBlocks := make(map[int]*coverBlock) + var allDocumentSeqPoints []compiler.DebugSeqPoint for _, scriptRawCoverage := range rawCoverage { - di := scriptRawCoverage.debugInfo - documentSeqPoints := documentSeqPoints(di, documentName) - - for _, point := range documentSeqPoints { - b := coverBlock{ - startLine: uint(point.StartLine), - startCol: uint(point.StartCol), - endLine: uint(point.EndLine), - endCol: uint(point.EndCol), - stmts: 1 + uint(point.EndLine) - uint(point.StartLine), - counts: 0, - } - mappedBlocks[point.Opcode] = &b + documentSeqPoints := documentSeqPoints(scriptRawCoverage.debugInfo, documentName) + allDocumentSeqPoints = append(allDocumentSeqPoints, documentSeqPoints...) + } + allDocumentSeqPoints = resolveOverlaps(allDocumentSeqPoints) + + for _, point := range allDocumentSeqPoints { + b := coverBlock{ + startLine: uint(point.StartLine), + startCol: uint(point.StartCol), + endLine: uint(point.EndLine), + endCol: uint(point.EndCol), + stmts: 1 + uint(point.EndLine) - uint(point.StartLine), + counts: 0, } + mappedBlocks[point.Opcode] = &b } for _, scriptRawCoverage := range rawCoverage { - di := scriptRawCoverage.debugInfo - documentSeqPoints := documentSeqPoints(di, documentName) - + documentSeqPoints := documentSeqPoints(scriptRawCoverage.debugInfo, documentName) for _, offset := range scriptRawCoverage.offsetsVisited { for _, point := range documentSeqPoints { if point.Opcode == offset { - mappedBlocks[point.Opcode].counts++ + if _, ok := mappedBlocks[offset]; ok { + mappedBlocks[offset].counts++ + } } } } @@ -202,6 +209,42 @@ func documentSeqPoints(di *compiler.DebugInfo, doc documentName) []compiler.Debu return res } +// resolveOverlaps removes overlaps from debug points. +// Its assumed that intervals can never overlap partially. +func resolveOverlaps(points []compiler.DebugSeqPoint) []compiler.DebugSeqPoint { + var intervals []interval + for _, p := range points { + intervals = append(intervals, interval{DebugSeqPoint: p}) + } + for i := range intervals { + for j := range intervals { + inner := &intervals[i] + outer := &intervals[j] + // If interval 'i' is already removed than there exists an even smaller interval that is also included by 'j'. + // This also ensures that if there are 2 equal intervals then at least 1 will remain. + if i == j || inner.remove { + continue + } + // outer interval start must be before inner interval start. + if !(outer.StartLine < inner.StartLine || outer.StartLine == inner.StartLine && outer.StartCol <= inner.StartCol) { + continue + } + // outer interval end must be after inner interval end. + if !(outer.EndLine > inner.EndLine || outer.EndLine == inner.EndLine && outer.EndCol >= inner.EndCol) { + continue + } + outer.remove = true + } + } + var res []compiler.DebugSeqPoint + for i, v := range intervals { + if !v.remove { + res = append(res, points[i]) + } + } + return res +} + func addScriptToCoverage(c *Contract) { coverageLock.Lock() defer coverageLock.Unlock() diff --git a/pkg/neotest/coverage_test.go b/pkg/neotest/coverage_test.go new file mode 100644 index 0000000000..506b8a02a2 --- /dev/null +++ b/pkg/neotest/coverage_test.go @@ -0,0 +1,301 @@ +package neotest + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/compiler" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/stretchr/testify/require" +) + +func resetCoverage() { + rawCoverage = make(map[util.Uint160]*scriptRawCoverage) +} + +func TestProcessCover_OneMethodOneDocument(t *testing.T) { + t.Cleanup(resetCoverage) + + scriptHash := util.Uint160{1} + doc := "foobar.go" + mdi := compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartLine: 0, EndLine: 0}, + {Opcode: 1, Document: 0, StartLine: 1, EndLine: 1}, + {Opcode: 2, Document: 0, StartLine: 2, EndLine: 2}, + }, + } + di := &compiler.DebugInfo{ + Documents: []string{doc}, + Methods: []compiler.MethodDebugInfo{mdi}, + } + contract := &Contract{Hash: scriptHash, DebugInfo: di} + + addScriptToCoverage(contract) + coverageHook(scriptHash, 1, opcode.NOP) + coverageHook(scriptHash, 2, opcode.NOP) + coverageHook(scriptHash, 2, opcode.NOP) + cover := processCover() + + require.Contains(t, cover, doc) + documentCover := cover[doc] + require.Equal(t, 3, len(documentCover)) + require.Contains(t, documentCover, coverBlock{startLine: 0, endLine: 0, stmts: 1, counts: 0}) + require.Contains(t, documentCover, coverBlock{startLine: 1, endLine: 1, stmts: 1, counts: 1}) + require.Contains(t, documentCover, coverBlock{startLine: 2, endLine: 2, stmts: 1, counts: 2}) +} + +func TestProcessCover_TwoMethodsTwoDocuments(t *testing.T) { + t.Cleanup(resetCoverage) + + scriptHash1 := util.Uint160{1} + scriptHash2 := util.Uint160{2} + doc1 := "contract1.go" + doc2 := "contract2.go" + mdi1 := compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartLine: 0, EndLine: 0}, + {Opcode: 1, Document: 0, StartLine: 1, EndLine: 1}, + {Opcode: 2, Document: 0, StartLine: 2, EndLine: 2}, + }, + } + mdi2 := compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 1, StartLine: 0, EndLine: 0}, + {Opcode: 1, Document: 1, StartLine: 1, EndLine: 1}, + {Opcode: 2, Document: 1, StartLine: 2, EndLine: 2}, + }, + } + di1 := &compiler.DebugInfo{ + Documents: []string{doc1, doc2}, + Methods: []compiler.MethodDebugInfo{mdi1}, + } + di2 := &compiler.DebugInfo{ + Documents: []string{doc1, doc2}, + Methods: []compiler.MethodDebugInfo{mdi2}, + } + + addScriptToCoverage(&Contract{Hash: scriptHash1, DebugInfo: di1}) + addScriptToCoverage(&Contract{Hash: scriptHash2, DebugInfo: di2}) + coverageHook(scriptHash1, 1, opcode.NOP) + coverageHook(scriptHash1, 2, opcode.NOP) + coverageHook(scriptHash1, 2, opcode.NOP) + coverageHook(scriptHash2, 0, opcode.NOP) + coverageHook(scriptHash2, 1, opcode.NOP) + coverageHook(scriptHash2, 2, opcode.NOP) + cover := processCover() + + require.Contains(t, cover, doc1) + require.Contains(t, cover, doc2) + documentCover1 := cover[doc1] + require.Equal(t, 3, len(documentCover1)) + require.Contains(t, documentCover1, coverBlock{startLine: 0, endLine: 0, stmts: 1, counts: 0}) + require.Contains(t, documentCover1, coverBlock{startLine: 1, endLine: 1, stmts: 1, counts: 1}) + require.Contains(t, documentCover1, coverBlock{startLine: 2, endLine: 2, stmts: 1, counts: 2}) + documentCover2 := cover[doc2] + require.Equal(t, 3, len(documentCover2)) + require.Contains(t, documentCover2, coverBlock{startLine: 0, endLine: 0, stmts: 1, counts: 1}) + require.Contains(t, documentCover2, coverBlock{startLine: 1, endLine: 1, stmts: 1, counts: 1}) + require.Contains(t, documentCover2, coverBlock{startLine: 2, endLine: 2, stmts: 1, counts: 1}) +} + +func TestProcessCover_BlockOverlap(t *testing.T) { + testCases := []struct { + name string + mdi compiler.MethodDebugInfo + }{ + { + name: "Different lines", + mdi: compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartLine: 2, EndLine: 9}, + {Opcode: 1, Document: 0, StartLine: 1, EndLine: 10}, + }, + }, + }, + { + name: "Different start lines", + mdi: compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartLine: 2, EndLine: 9}, + {Opcode: 1, Document: 0, StartLine: 1, EndLine: 9}, + }, + }, + }, + { + name: "Different end lines", + mdi: compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartLine: 2, EndLine: 9}, + {Opcode: 1, Document: 0, StartLine: 2, EndLine: 10}, + }, + }, + }, + { + name: "Different columns", + mdi: compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartCol: 2, EndCol: 9}, + {Opcode: 1, Document: 0, StartCol: 1, EndCol: 10}, + }, + }, + }, + { + name: "Different start columns", + mdi: compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartCol: 2, EndCol: 9}, + {Opcode: 1, Document: 0, StartCol: 1, EndCol: 9}, + }, + }, + }, + { + name: "Different end columns", + mdi: compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartCol: 2, EndCol: 9}, + {Opcode: 1, Document: 0, StartCol: 2, EndCol: 10}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Cleanup(resetCoverage) + + scriptHash := util.Uint160{1} + doc := "foobar.go" + di := &compiler.DebugInfo{ + Documents: []string{doc}, + Methods: []compiler.MethodDebugInfo{tc.mdi}, + } + contract := &Contract{Hash: scriptHash, DebugInfo: di} + + addScriptToCoverage(contract) + cover := processCover() + + require.Contains(t, cover, doc) + documentCover := cover[doc] + require.Equal(t, 1, len(documentCover)) + expectedBlock := tc.mdi.SeqPoints[0] + actualBlock := documentCover[0] + require.Equal(t, actualBlock.startLine, uint(expectedBlock.StartLine)) + require.Equal(t, actualBlock.endLine, uint(expectedBlock.EndLine)) + require.Equal(t, actualBlock.startCol, uint(expectedBlock.StartCol)) + require.Equal(t, actualBlock.endCol, uint(expectedBlock.EndCol)) + }) + } + + t.Run("Nested", func(t *testing.T) { + t.Cleanup(resetCoverage) + + scriptHash := util.Uint160{1} + doc := "foobar.go" + mdi := compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartLine: 0, EndLine: 4}, + {Opcode: 1, Document: 0, StartLine: 1, EndLine: 3}, + {Opcode: 2, Document: 0, StartLine: 2, EndLine: 2}, + }, + } + di := &compiler.DebugInfo{ + Documents: []string{doc}, + Methods: []compiler.MethodDebugInfo{mdi}, + } + contract := &Contract{Hash: scriptHash, DebugInfo: di} + + addScriptToCoverage(contract) + cover := processCover() + + require.Contains(t, cover, doc) + documentCover := cover[doc] + require.Equal(t, 1, len(documentCover)) + require.Contains(t, documentCover, coverBlock{startLine: 2, endLine: 2, stmts: 1}) + }) + + t.Run("Complex", func(t *testing.T) { + t.Cleanup(resetCoverage) + + scriptHash := util.Uint160{1} + doc := "foobar.go" + mdi := compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartLine: 0, EndLine: 0}, + {Opcode: 1, Document: 0, StartLine: 1, EndLine: 1}, + {Opcode: 2, Document: 0, StartLine: 2, EndLine: 2}, + {Opcode: 3, Document: 0, StartLine: 3, EndLine: 3}, + {Opcode: 4, Document: 0, StartLine: 4, EndLine: 4}, + {Opcode: 5, Document: 0, StartLine: 5, EndLine: 5}, // pick smaller (more specific). + {Opcode: 6, Document: 0, StartLine: 5, EndLine: 6}, // overlap. + {Opcode: 7, Document: 0, StartLine: 7, EndLine: 7}, + {Opcode: 8, Document: 0, StartLine: 8, EndLine: 8}, + {Opcode: 9, Document: 0, StartLine: 9, EndLine: 9}, + }, + } + di := &compiler.DebugInfo{ + Documents: []string{doc}, + Methods: []compiler.MethodDebugInfo{mdi}, + } + contract := &Contract{Hash: scriptHash, DebugInfo: di} + + addScriptToCoverage(contract) + cover := processCover() + + require.Contains(t, cover, doc) + documentCover := cover[doc] + require.Equal(t, 9, len(documentCover)) + require.Contains(t, documentCover, coverBlock{startLine: 5, endLine: 5, stmts: 1}) + require.NotContains(t, documentCover, coverBlock{startLine: 5, endLine: 6, stmts: 2}) + }) + + t.Run("No overlap on same line", func(t *testing.T) { + t.Cleanup(resetCoverage) + + scriptHash := util.Uint160{1} + doc := "foobar.go" + mdi := compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0, StartLine: 0, EndLine: 0, StartCol: 1, EndCol: 2}, + {Opcode: 1, Document: 0, StartLine: 0, EndLine: 0, StartCol: 3, EndCol: 4}, + }, + } + di := &compiler.DebugInfo{ + Documents: []string{doc}, + Methods: []compiler.MethodDebugInfo{mdi}, + } + contract := &Contract{Hash: scriptHash, DebugInfo: di} + + addScriptToCoverage(contract) + cover := processCover() + + require.Contains(t, cover, doc) + documentCover := cover[doc] + require.Equal(t, 2, len(documentCover)) + }) + + t.Run("Same intervals", func(t *testing.T) { + t.Cleanup(resetCoverage) + + scriptHash := util.Uint160{1} + doc := "foobar.go" + mdi := compiler.MethodDebugInfo{ + SeqPoints: []compiler.DebugSeqPoint{ + {Opcode: 0, Document: 0}, + {Opcode: 1, Document: 0}, + }, + } + di := &compiler.DebugInfo{ + Documents: []string{doc}, + Methods: []compiler.MethodDebugInfo{mdi}, + } + contract := &Contract{Hash: scriptHash, DebugInfo: di} + + addScriptToCoverage(contract) + cover := processCover() + + require.Contains(t, cover, doc) + documentCover := cover[doc] + require.Equal(t, 1, len(documentCover)) + }) +}