-
Notifications
You must be signed in to change notification settings - Fork 2
/
coverage.go
161 lines (138 loc) · 4.58 KB
/
coverage.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package solcover
import (
"bytes"
"fmt"
"math/big"
"sort"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
)
// Collector returns an EVMLogger that can be used to trace EVM operations for
// code coverage. The returned function returns an LCOV trace file at any time
// coverage is not being actively collected (i.e. it is not thread safe with
// respect to VM computation).
//
// Coverage will only be collected for contracts registered with
// RegisterContract() before they are deployed; their respective code also must
// have been registered with RegisterSourceCode(…, isExternal = false).
func Collector() (vm.EVMLogger, func() []byte) {
lineHits := make(map[string]map[int]int)
for file := range sourceCode {
lineHits[file] = make(map[int]int)
}
// TODO(aschlosberg) this is only a proxy for all "possible" lines in the
// code (i.e. not blank lines and comments). It's imperfect because the
// lines are derived from the op codes so "else lines" are missed. This will
// need to be dealt with when tracing branches.
var contracts []*compiledContract
for _, c := range contractsByHash {
contracts = append(contracts, c)
}
for _, m := range contractMatchers {
contracts = append(contracts, m.compiledContract)
}
for _, c := range contracts {
for _, l := range c.locations {
if l.FileIdx >= len(c.sourceList) {
continue
}
f := c.sourceList[l.FileIdx]
lineHits[f][l.Line] = 0
}
}
cc := &coverageCollector{
lineHits: lineHits,
last: new(Location),
}
return cc, cc.lcovTraceFile
}
type coverageCollector struct {
// contracts are a stack of contract addresses with the last entry of the
// slice being the current contract, against which the pc is compared when
// inspecting the source map. Without a stack (i.e. always using the
// "bottom" contract, to which the tx is initiated) the returned source will
// function incorrectly on library calls.
contracts []common.Address
lineHits map[string]map[int]int
last *Location
}
func (cc *coverageCollector) lcovTraceFile() []byte {
var out bytes.Buffer
linef := func(format string, a ...interface{}) {
out.WriteString(fmt.Sprintf(format, a...))
out.WriteRune('\n')
}
line := func(a ...interface{}) {
out.WriteString(fmt.Sprint(a...))
out.WriteRune('\n')
}
// It's important to range over a slice here and not the cc.lineHits map
// because the map lacks a guaranteed order.
var files []string
for f := range sourceCode {
files = append(files, f)
}
sort.Strings(files)
for _, file := range files {
c := sourceCode[file]
if c.isExternal {
continue
}
mapper := c.mapper
if mapper.Len() == 0 {
continue
}
linef("SF:%s", file)
// TODO(aschlosberg) extend coverage to include functions and branches
line("FNF:0")
line("FNH:0")
type count struct{ line, n int }
lh := cc.lineHits[file]
counts := make([]count, 0, len(lh))
for l, n := range lh {
counts = append(counts, count{l, n})
}
sort.Slice(counts, func(i, j int) bool {
return counts[i].line < counts[j].line
})
var nonZero int
for _, c := range counts {
linef("DA:%d,%d", c.line, c.n)
if c.n > 0 {
nonZero++
}
}
linef("LH:%d", nonZero)
linef("LF:%d", len(counts))
line("end_of_record")
}
return out.Bytes()
}
func (cc *coverageCollector) CaptureStart(evm *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
if create {
RegisterDeployedContract(to, input)
}
cc.contracts = []common.Address{to}
}
func (cc *coverageCollector) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
// TODO(aschlosberg) this is adding spurious extra counts to lines.
loc, ok := Source(cc.contracts[len(cc.contracts)-1], pc)
if !ok || loc.Source == "" || loc.Line == 0 {
return
}
if loc.FileIdx != cc.last.FileIdx || loc.Line != cc.last.Line {
cc.lineHits[loc.Source][loc.Line]++
}
cc.last = loc
}
func (*coverageCollector) CaptureTxStart(gasLimit uint64) {}
func (*coverageCollector) CaptureTxEnd(restGas uint64) {}
func (*coverageCollector) CaptureEnd(output []byte, gasUsed uint64, err error) {}
func (cc *coverageCollector) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
cc.contracts = append(cc.contracts, to)
}
func (cc *coverageCollector) CaptureExit(output []byte, gasUsed uint64, err error) {
cc.contracts = cc.contracts[:len(cc.contracts)-1]
}
func (*coverageCollector) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) {
}