covered 0.28.0 → 0.28.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +4 -3
- data/bake/covered/policy.rb +2 -22
- data/bake/covered/validate.rb +4 -4
- data/context/configuration.md +10 -10
- data/context/getting-started.md +6 -6
- data/context/usage.md +6 -6
- data/lib/covered/autostart.rb +3 -0
- data/lib/covered/brief_summary.rb +6 -0
- data/lib/covered/capture.rb +10 -1
- data/lib/covered/config.rb +51 -2
- data/lib/covered/coverage.rb +73 -0
- data/lib/covered/files.rb +63 -1
- data/lib/covered/forks.rb +12 -0
- data/lib/covered/markdown_summary.rb +23 -1
- data/lib/covered/minitest.rb +3 -0
- data/lib/covered/partial_summary.rb +16 -3
- data/lib/covered/persist.rb +29 -0
- data/lib/covered/policy.rb +33 -0
- data/lib/covered/rspec.rb +8 -0
- data/lib/covered/source.rb +32 -0
- data/lib/covered/statistics.rb +42 -0
- data/lib/covered/summary.rb +40 -1
- data/lib/covered/sus.rb +8 -1
- data/lib/covered/version.rb +2 -1
- data/lib/covered/wrapper.rb +51 -0
- data/license.md +1 -1
- data/readme.md +20 -0
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +3 -3
- metadata.gz.sig +3 -3
data/lib/covered/coverage.rb
CHANGED
|
@@ -6,53 +6,83 @@
|
|
|
6
6
|
require_relative "source"
|
|
7
7
|
|
|
8
8
|
module Covered
|
|
9
|
+
# Computes common coverage ratios from executed and executable line counts.
|
|
9
10
|
module Ratio
|
|
11
|
+
# The fraction of executable lines that were executed.
|
|
12
|
+
# @returns [Rational | Float] The executed-to-executable line ratio, or `1.0` when there are no executable lines.
|
|
10
13
|
def ratio
|
|
11
14
|
return 1.0 if executable_count.zero?
|
|
12
15
|
|
|
13
16
|
Rational(executed_count, executable_count)
|
|
14
17
|
end
|
|
15
18
|
|
|
19
|
+
# Whether all executable lines were executed.
|
|
20
|
+
# @returns [Boolean] Whether `executed_count` equals `executable_count`.
|
|
16
21
|
def complete?
|
|
17
22
|
executed_count == executable_count
|
|
18
23
|
end
|
|
19
24
|
|
|
25
|
+
# The coverage ratio as a percentage.
|
|
26
|
+
# @returns [Numeric] The coverage ratio multiplied by `100`.
|
|
20
27
|
def percentage
|
|
21
28
|
ratio * 100
|
|
22
29
|
end
|
|
23
30
|
end
|
|
24
31
|
|
|
32
|
+
# Stores line execution counts and source metadata for a single file.
|
|
25
33
|
class Coverage
|
|
26
34
|
include Ratio
|
|
27
35
|
|
|
36
|
+
# Build coverage for the given source path.
|
|
37
|
+
# @parameter path [String] The source path.
|
|
38
|
+
# @parameter options [Hash] Options forwarded to {Covered::Source.for}.
|
|
39
|
+
# @returns [Covered::Coverage] The coverage object for the source path.
|
|
28
40
|
def self.for(path, **options)
|
|
29
41
|
self.new(Source.for(path, **options))
|
|
30
42
|
end
|
|
31
43
|
|
|
44
|
+
# Initialize coverage with the given source, line counts and annotations.
|
|
45
|
+
# @parameter source [Covered::Source] The covered source metadata.
|
|
46
|
+
# @parameter counts [Array(Integer | Nil)] Line execution counts indexed by line number.
|
|
47
|
+
# @parameter annotations [Hash(Integer, Array(String))] Line annotations indexed by line number.
|
|
32
48
|
def initialize(source, counts = [], annotations = {})
|
|
33
49
|
@source = source
|
|
34
50
|
@counts = counts
|
|
35
51
|
@annotations = annotations
|
|
36
52
|
end
|
|
37
53
|
|
|
54
|
+
# @attribute [Covered::Source] The covered source metadata.
|
|
38
55
|
attr_accessor :source
|
|
56
|
+
|
|
57
|
+
# @attribute [Array(Integer | Nil)] Line execution counts indexed by line number.
|
|
39
58
|
attr :counts
|
|
59
|
+
|
|
60
|
+
# @attribute [Hash(Integer, Array(String))] Line annotations indexed by line number.
|
|
40
61
|
attr :annotations
|
|
41
62
|
|
|
63
|
+
# The total number of executions across all tracked lines.
|
|
64
|
+
# @returns [Integer] The sum of all non-`nil` execution counts.
|
|
42
65
|
def total
|
|
43
66
|
counts.sum{|count| count || 0}
|
|
44
67
|
end
|
|
45
68
|
|
|
46
69
|
# Create an empty coverage with the same source.
|
|
70
|
+
# @returns [Covered::Coverage] Coverage with the same source and `nil` counts.
|
|
47
71
|
def empty
|
|
48
72
|
self.class.new(@source, [nil] * @counts.size)
|
|
49
73
|
end
|
|
50
74
|
|
|
75
|
+
# Add an annotation to the given line number.
|
|
76
|
+
# @parameter line_number [Integer] The line number to annotate.
|
|
77
|
+
# @parameter annotation [String] The annotation text.
|
|
51
78
|
def annotate(line_number, annotation)
|
|
52
79
|
@annotations[line_number] ||= []
|
|
53
80
|
@annotations[line_number] << annotation
|
|
54
81
|
end
|
|
55
82
|
|
|
83
|
+
# Add the given execution count to one or more line numbers.
|
|
84
|
+
# @parameter line_number [Integer] The first line number to mark.
|
|
85
|
+
# @parameter value [Integer | Array(Integer)] The execution count or counts to add.
|
|
56
86
|
def mark(line_number, value = 1)
|
|
57
87
|
# As currently implemented, @counts is base-zero rather than base-one.
|
|
58
88
|
# Line numbers generally start at line 1, so the first line, line 1, is at index 1. This means that index[0] is usually nil.
|
|
@@ -66,6 +96,8 @@ module Covered
|
|
|
66
96
|
end
|
|
67
97
|
end
|
|
68
98
|
|
|
99
|
+
# Merge another coverage object into this coverage object.
|
|
100
|
+
# @parameter other [Covered::Coverage] The coverage object to merge.
|
|
69
101
|
def merge!(other)
|
|
70
102
|
# If the counts are non-zero and don't match, that can indicate a problem.
|
|
71
103
|
|
|
@@ -83,6 +115,7 @@ module Covered
|
|
|
83
115
|
|
|
84
116
|
# Construct a new coverage object for the given line numbers. Only the given line numbers will be considered for the purposes of computing coverage.
|
|
85
117
|
# @parameter line_numbers [Array(Integer)] The line numbers to include in the new coverage object.
|
|
118
|
+
# @returns [Covered::Coverage] A coverage object containing counts for the selected lines.
|
|
86
119
|
def for_lines(line_numbers)
|
|
87
120
|
counts = [nil] * @counts.size
|
|
88
121
|
line_numbers.each do |line_number|
|
|
@@ -92,14 +125,20 @@ module Covered
|
|
|
92
125
|
self.class.new(@source, counts, @annotations)
|
|
93
126
|
end
|
|
94
127
|
|
|
128
|
+
# The covered source path.
|
|
129
|
+
# @returns [String] The covered source path.
|
|
95
130
|
def path
|
|
96
131
|
@source.path
|
|
97
132
|
end
|
|
98
133
|
|
|
134
|
+
# Assign the covered source path.
|
|
135
|
+
# @parameter value [String] The new covered source path.
|
|
99
136
|
def path= value
|
|
100
137
|
@source.path = value
|
|
101
138
|
end
|
|
102
139
|
|
|
140
|
+
# Whether the source file has not changed since this coverage was recorded.
|
|
141
|
+
# @returns [Boolean] Whether the source exists and its modification time is not newer than the recorded time.
|
|
103
142
|
def fresh?
|
|
104
143
|
if @source.modified_time.nil?
|
|
105
144
|
# We don't know when the file was last modified, so we assume it is stale:
|
|
@@ -119,10 +158,16 @@ module Covered
|
|
|
119
158
|
return false
|
|
120
159
|
end
|
|
121
160
|
|
|
161
|
+
# Read the covered source.
|
|
162
|
+
# @yields {|file| ...} If a block is given, yields an open source file.
|
|
163
|
+
# @parameter file [File] The open source file.
|
|
164
|
+
# @returns [String | Object] The source contents without a block, or the block result with a block.
|
|
122
165
|
def read(&block)
|
|
123
166
|
@source.read(&block)
|
|
124
167
|
end
|
|
125
168
|
|
|
169
|
+
# Freeze this coverage and its mutable collections.
|
|
170
|
+
# @returns [Covered::Coverage] This frozen coverage object.
|
|
126
171
|
def freeze
|
|
127
172
|
return self if frozen?
|
|
128
173
|
|
|
@@ -132,46 +177,69 @@ module Covered
|
|
|
132
177
|
super
|
|
133
178
|
end
|
|
134
179
|
|
|
180
|
+
# The raw coverage counts array.
|
|
181
|
+
# @returns [Array(Integer | Nil)] The raw coverage counts.
|
|
135
182
|
def to_a
|
|
136
183
|
@counts
|
|
137
184
|
end
|
|
138
185
|
|
|
186
|
+
# Whether this coverage has no executions.
|
|
187
|
+
# @returns [Boolean] Whether the total execution count is zero.
|
|
139
188
|
def zero?
|
|
140
189
|
total.zero?
|
|
141
190
|
end
|
|
142
191
|
|
|
192
|
+
# The raw coverage count for the given line number.
|
|
193
|
+
# @parameter line_number [Integer] The line number to query.
|
|
194
|
+
# @returns [Integer | Nil] The execution count for the line.
|
|
143
195
|
def [] line_number
|
|
144
196
|
@counts[line_number]
|
|
145
197
|
end
|
|
146
198
|
|
|
199
|
+
# Counts for lines that are executable.
|
|
200
|
+
# @returns [Array(Integer)] Execution counts for executable lines.
|
|
147
201
|
def executable_lines
|
|
148
202
|
@counts.compact
|
|
149
203
|
end
|
|
150
204
|
|
|
205
|
+
# The number of executable lines.
|
|
206
|
+
# @returns [Integer] The number of executable lines.
|
|
151
207
|
def executable_count
|
|
152
208
|
executable_lines.count
|
|
153
209
|
end
|
|
154
210
|
|
|
211
|
+
# Counts for executable lines that were executed.
|
|
212
|
+
# @returns [Array(Integer)] Non-zero execution counts for executable lines.
|
|
155
213
|
def executed_lines
|
|
156
214
|
executable_lines.reject(&:zero?)
|
|
157
215
|
end
|
|
158
216
|
|
|
217
|
+
# The number of executable lines that were executed.
|
|
218
|
+
# @returns [Integer] The number of executed lines.
|
|
159
219
|
def executed_count
|
|
160
220
|
executed_lines.count
|
|
161
221
|
end
|
|
162
222
|
|
|
223
|
+
# The number of executable lines that were not executed.
|
|
224
|
+
# @returns [Integer] The number of missing executable lines.
|
|
163
225
|
def missing_count
|
|
164
226
|
executable_count - executed_count
|
|
165
227
|
end
|
|
166
228
|
|
|
229
|
+
# Print a human-readable coverage summary.
|
|
230
|
+
# @parameter output [IO] The output stream.
|
|
167
231
|
def print(output)
|
|
168
232
|
output.puts "** #{executed_count}/#{executable_count} lines executed; #{percentage.to_f.round(2)}% covered."
|
|
169
233
|
end
|
|
170
234
|
|
|
235
|
+
# A human-readable representation of this coverage object.
|
|
236
|
+
# @returns [String] A summary including the source path and percentage.
|
|
171
237
|
def to_s
|
|
172
238
|
"\#<#{self.class} path=#{self.path} #{self.percentage.to_f.round(2)}% covered>"
|
|
173
239
|
end
|
|
174
240
|
|
|
241
|
+
# A JSON-compatible representation of this coverage object.
|
|
242
|
+
# @returns [Hash] The coverage counts and summary statistics.
|
|
175
243
|
def as_json
|
|
176
244
|
{
|
|
177
245
|
counts: counts,
|
|
@@ -181,12 +249,17 @@ module Covered
|
|
|
181
249
|
}
|
|
182
250
|
end
|
|
183
251
|
|
|
252
|
+
# Serialize this coverage object with the given packer.
|
|
253
|
+
# @parameter packer [Object] The MessagePack-compatible packer.
|
|
184
254
|
def serialize(packer)
|
|
185
255
|
packer.write(@source)
|
|
186
256
|
packer.write(@counts)
|
|
187
257
|
packer.write(@annotations)
|
|
188
258
|
end
|
|
189
259
|
|
|
260
|
+
# Deserialize a coverage object from the given unpacker.
|
|
261
|
+
# @parameter unpacker [Object] The MessagePack-compatible unpacker.
|
|
262
|
+
# @returns [Covered::Coverage] The deserialized coverage object.
|
|
190
263
|
def self.deserialize(unpacker)
|
|
191
264
|
source = unpacker.read
|
|
192
265
|
counts = unpacker.read
|
data/lib/covered/files.rb
CHANGED
|
@@ -9,35 +9,57 @@ require_relative "wrapper"
|
|
|
9
9
|
require "set"
|
|
10
10
|
|
|
11
11
|
module Covered
|
|
12
|
+
# Collects coverage information keyed by source path.
|
|
12
13
|
class Files < Base
|
|
14
|
+
# Initialize an empty coverage collection.
|
|
13
15
|
def initialize(*)
|
|
14
16
|
super
|
|
15
17
|
|
|
16
18
|
@paths = {}
|
|
17
19
|
end
|
|
18
20
|
|
|
21
|
+
# @attribute [Hash(String, Covered::Coverage)] Coverage indexed by expanded source path.
|
|
19
22
|
attr_accessor :paths
|
|
20
23
|
|
|
24
|
+
# Get or create coverage for the given path.
|
|
25
|
+
# @parameter path [String] The source path.
|
|
26
|
+
# @returns [Covered::Coverage] The coverage object for the path.
|
|
21
27
|
def [](path)
|
|
22
28
|
@paths[path] ||= Coverage.for(path)
|
|
23
29
|
end
|
|
24
30
|
|
|
31
|
+
# Whether there are no tracked paths.
|
|
32
|
+
# @returns [Boolean] Whether no coverage paths are tracked.
|
|
25
33
|
def empty?
|
|
26
34
|
@paths.empty?
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
# Mark a line in the given path as executed.
|
|
38
|
+
# @parameter path [String] The source path.
|
|
39
|
+
# @parameter line_number [Integer] The line number to mark.
|
|
40
|
+
# @parameter value [Integer | Array(Integer)] The execution count or counts to add.
|
|
29
41
|
def mark(path, line_number, value)
|
|
30
42
|
self[path].mark(line_number, value)
|
|
31
43
|
end
|
|
32
44
|
|
|
45
|
+
# Add an annotation to a line in the given path.
|
|
46
|
+
# @parameter path [String] The source path.
|
|
47
|
+
# @parameter line_number [Integer] The line number to annotate.
|
|
48
|
+
# @parameter value [String] The annotation text.
|
|
33
49
|
def annotate(path, line_number, value)
|
|
34
50
|
self[path].annotate(line_number, value)
|
|
35
51
|
end
|
|
36
52
|
|
|
53
|
+
# Merge coverage for the given path into this collection.
|
|
54
|
+
# @parameter coverage [Covered::Coverage] The coverage object to merge.
|
|
37
55
|
def add(coverage)
|
|
38
56
|
self[coverage.path].merge!(coverage)
|
|
39
57
|
end
|
|
40
58
|
|
|
59
|
+
# Enumerate tracked coverage objects.
|
|
60
|
+
# @yields {|coverage| ...} Each tracked coverage object.
|
|
61
|
+
# @parameter coverage [Covered::Coverage] The current coverage object.
|
|
62
|
+
# @returns [Enumerator | Nil] An enumerator without a block.
|
|
41
63
|
def each
|
|
42
64
|
return to_enum unless block_given?
|
|
43
65
|
|
|
@@ -46,12 +68,19 @@ module Covered
|
|
|
46
68
|
end
|
|
47
69
|
end
|
|
48
70
|
|
|
71
|
+
# Remove all tracked coverage data.
|
|
72
|
+
# @returns [Hash] The cleared path map.
|
|
49
73
|
def clear
|
|
50
74
|
@paths.clear
|
|
51
75
|
end
|
|
52
76
|
end
|
|
53
77
|
|
|
78
|
+
# Includes coverage for files matching a glob pattern.
|
|
54
79
|
class Include < Wrapper
|
|
80
|
+
# Initialize an include filter for the given glob pattern.
|
|
81
|
+
# @parameter output [Covered::Base] The output to wrap.
|
|
82
|
+
# @parameter pattern [String] The glob pattern to include.
|
|
83
|
+
# @parameter base [String] The base path used to expand the glob.
|
|
55
84
|
def initialize(output, pattern, base = "")
|
|
56
85
|
super(output)
|
|
57
86
|
|
|
@@ -59,8 +88,11 @@ module Covered
|
|
|
59
88
|
@base = base
|
|
60
89
|
end
|
|
61
90
|
|
|
91
|
+
# @attribute [String] The glob pattern to include.
|
|
62
92
|
attr :pattern
|
|
63
93
|
|
|
94
|
+
# Resolve the include pattern to real file paths.
|
|
95
|
+
# @returns [Set(String)] The real paths matched by the include pattern.
|
|
64
96
|
def glob
|
|
65
97
|
paths = Set.new
|
|
66
98
|
root = self.expand_path(@base)
|
|
@@ -75,6 +107,9 @@ module Covered
|
|
|
75
107
|
return paths
|
|
76
108
|
end
|
|
77
109
|
|
|
110
|
+
# Enumerate existing coverage and synthesize empty coverage for unmatched included files.
|
|
111
|
+
# @yields {|coverage| ...} Each existing or synthesized coverage object.
|
|
112
|
+
# @parameter coverage [Covered::Coverage] The current coverage object.
|
|
78
113
|
def each(&block)
|
|
79
114
|
paths = glob
|
|
80
115
|
|
|
@@ -90,17 +125,22 @@ module Covered
|
|
|
90
125
|
end
|
|
91
126
|
end
|
|
92
127
|
|
|
128
|
+
# Excludes coverage for paths matching a pattern.
|
|
93
129
|
class Skip < Filter
|
|
130
|
+
# Initialize a skip filter for the given pattern.
|
|
131
|
+
# @parameter output [Covered::Base] The output to wrap.
|
|
132
|
+
# @parameter pattern [Regexp] The pattern to exclude.
|
|
94
133
|
def initialize(output, pattern)
|
|
95
134
|
super(output)
|
|
96
135
|
|
|
97
136
|
@pattern = pattern
|
|
98
137
|
end
|
|
99
138
|
|
|
139
|
+
# @attribute [Regexp] The pattern to exclude.
|
|
100
140
|
attr :pattern
|
|
101
141
|
|
|
102
142
|
if Regexp.instance_methods.include? :match?
|
|
103
|
-
# This is better as it doesn't allocate a MatchData instance which is essentially useless
|
|
143
|
+
# This is better as it doesn't allocate a MatchData instance which is essentially useless:
|
|
104
144
|
def match? path
|
|
105
145
|
!@pattern.match?(path)
|
|
106
146
|
end
|
|
@@ -111,33 +151,52 @@ module Covered
|
|
|
111
151
|
end
|
|
112
152
|
end
|
|
113
153
|
|
|
154
|
+
# Only includes coverage for paths matching a pattern.
|
|
114
155
|
class Only < Filter
|
|
156
|
+
# Initialize an only filter for the given pattern.
|
|
157
|
+
# @parameter output [Covered::Base] The output to wrap.
|
|
158
|
+
# @parameter pattern [Object] The pattern matched with `===`.
|
|
115
159
|
def initialize(output, pattern)
|
|
116
160
|
super(output)
|
|
117
161
|
|
|
118
162
|
@pattern = pattern
|
|
119
163
|
end
|
|
120
164
|
|
|
165
|
+
# @attribute [Object] The pattern matched with `===`.
|
|
121
166
|
attr :pattern
|
|
122
167
|
|
|
168
|
+
# Whether the given path matches the only pattern.
|
|
169
|
+
# @parameter path [String] The source path.
|
|
170
|
+
# @returns [Boolean] Whether the path matches the pattern.
|
|
123
171
|
def match?(path)
|
|
124
172
|
@pattern === path
|
|
125
173
|
end
|
|
126
174
|
end
|
|
127
175
|
|
|
176
|
+
# Restricts coverage to a project root and converts paths relative to it.
|
|
128
177
|
class Root < Filter
|
|
178
|
+
# Initialize a root filter for the given path.
|
|
179
|
+
# @parameter output [Covered::Base] The output to wrap.
|
|
180
|
+
# @parameter path [String] The root path.
|
|
129
181
|
def initialize(output, path)
|
|
130
182
|
super(output)
|
|
131
183
|
|
|
132
184
|
@path = path
|
|
133
185
|
end
|
|
134
186
|
|
|
187
|
+
# @attribute [String] The root path.
|
|
135
188
|
attr :path
|
|
136
189
|
|
|
190
|
+
# Expand a path relative to this root.
|
|
191
|
+
# @parameter path [String] The path to expand.
|
|
192
|
+
# @returns [String] The expanded path.
|
|
137
193
|
def expand_path(path)
|
|
138
194
|
File.expand_path(super, @path)
|
|
139
195
|
end
|
|
140
196
|
|
|
197
|
+
# Convert a path under this root to a relative path.
|
|
198
|
+
# @parameter path [String] The path to relativize.
|
|
199
|
+
# @returns [String] The relative path when under this root, otherwise the wrapped output result.
|
|
141
200
|
def relative_path(path)
|
|
142
201
|
if path.start_with?(@path)
|
|
143
202
|
path.slice(@path.size+1, path.size)
|
|
@@ -146,6 +205,9 @@ module Covered
|
|
|
146
205
|
end
|
|
147
206
|
end
|
|
148
207
|
|
|
208
|
+
# Whether the given path is under this root.
|
|
209
|
+
# @parameter path [String] The source path.
|
|
210
|
+
# @returns [Boolean] Whether the path starts with this root.
|
|
149
211
|
def match?(path)
|
|
150
212
|
path.start_with?(@path)
|
|
151
213
|
end
|
data/lib/covered/forks.rb
CHANGED
|
@@ -6,25 +6,33 @@
|
|
|
6
6
|
require_relative "wrapper"
|
|
7
7
|
|
|
8
8
|
module Covered
|
|
9
|
+
# Tracks coverage across forked child processes.
|
|
9
10
|
class Forks < Wrapper
|
|
11
|
+
# Start tracking coverage and install the fork handler.
|
|
10
12
|
def start
|
|
11
13
|
super
|
|
12
14
|
|
|
13
15
|
Handler.start(self)
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
# Stop tracking coverage and remove the fork handler state.
|
|
16
19
|
def finish
|
|
17
20
|
Handler.finish
|
|
18
21
|
|
|
19
22
|
super
|
|
20
23
|
end
|
|
21
24
|
|
|
25
|
+
# Handles `Process.fork` so child processes record coverage independently.
|
|
22
26
|
module Handler
|
|
23
27
|
LOCK = Mutex.new
|
|
24
28
|
|
|
25
29
|
class << self
|
|
30
|
+
# @attribute [Covered::Forks | Nil] The currently registered coverage wrapper.
|
|
26
31
|
attr :coverage
|
|
27
32
|
|
|
33
|
+
# Register coverage for fork handling.
|
|
34
|
+
# @parameter coverage [Covered::Forks] The coverage wrapper to use in forked children.
|
|
35
|
+
# @raises [ArgumentError] If coverage is already registered.
|
|
28
36
|
def start(coverage)
|
|
29
37
|
LOCK.synchronize do
|
|
30
38
|
if @coverage
|
|
@@ -35,12 +43,14 @@ module Covered
|
|
|
35
43
|
end
|
|
36
44
|
end
|
|
37
45
|
|
|
46
|
+
# Clear the registered coverage.
|
|
38
47
|
def finish
|
|
39
48
|
LOCK.synchronize do
|
|
40
49
|
@coverage = nil
|
|
41
50
|
end
|
|
42
51
|
end
|
|
43
52
|
|
|
53
|
+
# Reset coverage in a forked child and save it at exit.
|
|
44
54
|
def after_fork
|
|
45
55
|
return unless coverage = Handler.coverage
|
|
46
56
|
pid = Process.pid
|
|
@@ -57,6 +67,8 @@ module Covered
|
|
|
57
67
|
end
|
|
58
68
|
end
|
|
59
69
|
|
|
70
|
+
# Intercept `Process.fork` and initialize coverage in the child.
|
|
71
|
+
# @returns [Integer | Nil] The process ID in the parent, and `0` or `nil` in the child depending on Ruby's fork semantics.
|
|
60
72
|
def _fork
|
|
61
73
|
pid = super
|
|
62
74
|
|
|
@@ -9,11 +9,19 @@ require_relative "wrapper"
|
|
|
9
9
|
require "console/output"
|
|
10
10
|
|
|
11
11
|
module Covered
|
|
12
|
+
# Generates a Markdown coverage summary.
|
|
12
13
|
class MarkdownSummary
|
|
14
|
+
# Initialize the report with an optional coverage threshold.
|
|
15
|
+
# @parameter threshold [Numeric | Nil] The minimum ratio a file must meet to be omitted from the detailed output.
|
|
13
16
|
def initialize(threshold: 1.0)
|
|
14
17
|
@threshold = threshold
|
|
15
18
|
end
|
|
16
19
|
|
|
20
|
+
# Enumerate coverage below the threshold and return aggregate statistics.
|
|
21
|
+
# @parameter wrapper [Covered::Base] The coverage wrapper to enumerate.
|
|
22
|
+
# @yields {|coverage| ...} Coverage whose ratio is below the configured threshold.
|
|
23
|
+
# @parameter coverage [Covered::Coverage] The coverage object below the threshold.
|
|
24
|
+
# @returns [Covered::Statistics] Statistics for all coverage objects, including omitted ones.
|
|
17
25
|
def each(wrapper)
|
|
18
26
|
statistics = Statistics.new
|
|
19
27
|
|
|
@@ -28,6 +36,11 @@ module Covered
|
|
|
28
36
|
return statistics
|
|
29
37
|
end
|
|
30
38
|
|
|
39
|
+
# Print any annotations for the given line.
|
|
40
|
+
# @parameter output [IO] The output stream.
|
|
41
|
+
# @parameter coverage [Covered::Coverage] The coverage being rendered.
|
|
42
|
+
# @parameter line [String] The source line.
|
|
43
|
+
# @parameter line_offset [Integer] The current line number.
|
|
31
44
|
def print_annotations(output, coverage, line, line_offset)
|
|
32
45
|
if annotations = coverage.annotations[line_offset]
|
|
33
46
|
prefix = "#{line_offset}|".rjust(8) + "*|".rjust(8)
|
|
@@ -38,10 +51,17 @@ module Covered
|
|
|
38
51
|
end
|
|
39
52
|
end
|
|
40
53
|
|
|
54
|
+
# Print the line and hit-count header.
|
|
55
|
+
# @parameter output [IO] The output stream.
|
|
41
56
|
def print_line_header(output)
|
|
42
57
|
output.puts "Line|".rjust(8) + "Hits|".rjust(8)
|
|
43
58
|
end
|
|
44
59
|
|
|
60
|
+
# Print a single source line.
|
|
61
|
+
# @parameter output [IO] The output stream.
|
|
62
|
+
# @parameter line [String] The source line.
|
|
63
|
+
# @parameter line_offset [Integer] The current line number.
|
|
64
|
+
# @parameter count [Integer | Nil] The execution count for the line.
|
|
45
65
|
def print_line(output, line, line_offset, count)
|
|
46
66
|
prefix = "#{line_offset}|".rjust(8) + "#{count}|".rjust(8)
|
|
47
67
|
|
|
@@ -54,7 +74,9 @@ module Covered
|
|
|
54
74
|
end
|
|
55
75
|
end
|
|
56
76
|
|
|
57
|
-
# A coverage array gives, for each line, the number of line
|
|
77
|
+
# A coverage array gives, for each line, the number of line executions by the interpreter. A `nil` value means coverage is finished for this line (lines like `else` and `end`).
|
|
78
|
+
# @parameter wrapper [Covered::Base] The coverage wrapper to report.
|
|
79
|
+
# @parameter output [IO] The output stream.
|
|
58
80
|
def call(wrapper, output = $stdout)
|
|
59
81
|
output.puts "# Coverage Report"
|
|
60
82
|
output.puts
|
data/lib/covered/minitest.rb
CHANGED
|
@@ -11,7 +11,10 @@ require "minitest"
|
|
|
11
11
|
$covered = Covered::Config.load
|
|
12
12
|
|
|
13
13
|
module Covered
|
|
14
|
+
# Integrates coverage tracking with Minitest.
|
|
14
15
|
module Minitest
|
|
16
|
+
# Start coverage before running the Minitest suite.
|
|
17
|
+
# @returns [Object] The result of Minitest's original `run` method.
|
|
15
18
|
def run(*)
|
|
16
19
|
$covered.start
|
|
17
20
|
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
require_relative "summary"
|
|
7
7
|
|
|
8
8
|
module Covered
|
|
9
|
+
# Generates a report containing only lines near missing coverage.
|
|
9
10
|
class PartialSummary < Summary
|
|
11
|
+
# Print coverage snippets around uncovered lines.
|
|
12
|
+
# @parameter terminal [Console::Terminal] The terminal to write to.
|
|
13
|
+
# @parameter coverage [Covered::Coverage] The coverage to render.
|
|
14
|
+
# @parameter before [Integer] The number of lines before an uncovered line to show.
|
|
15
|
+
# @parameter after [Integer] The number of lines after an uncovered line to show.
|
|
10
16
|
def print_coverage(terminal, coverage, before: 4, after: 4)
|
|
11
17
|
return if coverage.zero?
|
|
12
18
|
|
|
@@ -38,11 +44,18 @@ module Covered
|
|
|
38
44
|
end
|
|
39
45
|
end
|
|
40
46
|
|
|
47
|
+
# Print the partial coverage report.
|
|
48
|
+
# @parameter wrapper [Covered::Base] The coverage wrapper to report.
|
|
49
|
+
# @parameter output [IO] The output stream.
|
|
50
|
+
# @parameter options [Hash] Options forwarded to {print_coverage}.
|
|
41
51
|
def call(wrapper, output = $stdout, **options)
|
|
42
52
|
terminal = self.terminal(output)
|
|
43
53
|
complete_files = []
|
|
54
|
+
partial_files_count = 0
|
|
44
55
|
|
|
45
56
|
statistics = self.each(wrapper) do |coverage|
|
|
57
|
+
partial_files_count += 1
|
|
58
|
+
|
|
46
59
|
path = wrapper.relative_path(coverage.path)
|
|
47
60
|
terminal.puts ""
|
|
48
61
|
terminal.puts path, style: :path
|
|
@@ -56,7 +69,7 @@ module Covered
|
|
|
56
69
|
coverage.print(output)
|
|
57
70
|
end
|
|
58
71
|
|
|
59
|
-
# Collect files with 100% coverage that were not shown
|
|
72
|
+
# Collect files with 100% coverage that were not shown:
|
|
60
73
|
wrapper.each do |coverage|
|
|
61
74
|
if coverage.ratio >= 1.0
|
|
62
75
|
complete_files << wrapper.relative_path(coverage.path)
|
|
@@ -66,8 +79,8 @@ module Covered
|
|
|
66
79
|
terminal.puts
|
|
67
80
|
statistics.print(output)
|
|
68
81
|
|
|
69
|
-
#
|
|
70
|
-
if complete_files.any?
|
|
82
|
+
# Only show information about files with 100% coverage if there were files with partial coverage shown above:
|
|
83
|
+
if complete_files.any? && partial_files_count > 0
|
|
71
84
|
terminal.puts ""
|
|
72
85
|
if complete_files.size == 1
|
|
73
86
|
terminal.puts "1 file has 100% coverage and is not shown above:"
|
data/lib/covered/persist.rb
CHANGED
|
@@ -10,15 +10,24 @@ require "msgpack"
|
|
|
10
10
|
require "time"
|
|
11
11
|
|
|
12
12
|
module Covered
|
|
13
|
+
# Persists coverage records to a MessagePack database.
|
|
13
14
|
class Persist < Wrapper
|
|
14
15
|
DEFAULT_PATH = ".covered.db"
|
|
15
16
|
|
|
17
|
+
# Initialize persistence for the given output and database path.
|
|
18
|
+
# @parameter output [Covered::Base] The output to wrap.
|
|
19
|
+
# @parameter path [String] The coverage database path.
|
|
16
20
|
def initialize(output, path = DEFAULT_PATH)
|
|
17
21
|
super(output)
|
|
18
22
|
|
|
19
23
|
@path = self.expand_path(path)
|
|
20
24
|
end
|
|
21
25
|
|
|
26
|
+
# Apply a persisted record to the output.
|
|
27
|
+
# Records with stale source modification times are ignored unless `ignore_mtime` is true.
|
|
28
|
+
# @parameter record [Hash] The persisted coverage record.
|
|
29
|
+
# @parameter ignore_mtime [Boolean] Whether to apply records even if their source appears stale.
|
|
30
|
+
# @returns [Boolean] Whether the record was applied.
|
|
22
31
|
def apply(record, ignore_mtime: false)
|
|
23
32
|
if coverage = record[:coverage]
|
|
24
33
|
if path = record[:path]
|
|
@@ -35,6 +44,9 @@ module Covered
|
|
|
35
44
|
return false
|
|
36
45
|
end
|
|
37
46
|
|
|
47
|
+
# Convert coverage into a database record.
|
|
48
|
+
# @parameter coverage [Covered::Coverage] The coverage object to serialize.
|
|
49
|
+
# @returns [Hash] A MessagePack-compatible record.
|
|
38
50
|
def serialize(coverage)
|
|
39
51
|
{
|
|
40
52
|
# We want to use relative paths so that moving the repo won't break everything:
|
|
@@ -45,6 +57,9 @@ module Covered
|
|
|
45
57
|
}
|
|
46
58
|
end
|
|
47
59
|
|
|
60
|
+
# Load persisted coverage records into the output.
|
|
61
|
+
# @parameter options [Hash] Options forwarded to {apply}.
|
|
62
|
+
# @raises [LoadError] If the database exists but cannot be decoded.
|
|
48
63
|
def load!(**options)
|
|
49
64
|
return unless File.exist?(@path)
|
|
50
65
|
|
|
@@ -61,6 +76,7 @@ module Covered
|
|
|
61
76
|
raise LoadError, "Failed to load coverage from #{@path}, maybe old format or corrupt!"
|
|
62
77
|
end
|
|
63
78
|
|
|
79
|
+
# Save all output coverage records to the database.
|
|
64
80
|
def save!
|
|
65
81
|
# Dump all coverage:
|
|
66
82
|
File.open(@path, "ab") do |file|
|
|
@@ -77,12 +93,17 @@ module Covered
|
|
|
77
93
|
end
|
|
78
94
|
end
|
|
79
95
|
|
|
96
|
+
# Finish the wrapped output and save the coverage database.
|
|
80
97
|
def finish
|
|
81
98
|
super
|
|
82
99
|
|
|
83
100
|
self.save!
|
|
84
101
|
end
|
|
85
102
|
|
|
103
|
+
# Reload persisted coverage and enumerate the wrapped output.
|
|
104
|
+
# @yields {|coverage| ...} Each coverage object from the reloaded output.
|
|
105
|
+
# @parameter coverage [Covered::Coverage] The current coverage object.
|
|
106
|
+
# @returns [Enumerator | Nil] An enumerator without a block.
|
|
86
107
|
def each(&block)
|
|
87
108
|
return to_enum unless block_given?
|
|
88
109
|
|
|
@@ -92,6 +113,8 @@ module Covered
|
|
|
92
113
|
super
|
|
93
114
|
end
|
|
94
115
|
|
|
116
|
+
# Build the MessagePack factory used for coverage records.
|
|
117
|
+
# @returns [MessagePack::Factory] The configured MessagePack factory.
|
|
95
118
|
def make_factory
|
|
96
119
|
factory = MessagePack::Factory.new
|
|
97
120
|
|
|
@@ -117,10 +140,16 @@ module Covered
|
|
|
117
140
|
return factory
|
|
118
141
|
end
|
|
119
142
|
|
|
143
|
+
# Build a MessagePack packer for the given IO.
|
|
144
|
+
# @parameter io [IO] The IO to write packed records to.
|
|
145
|
+
# @returns [MessagePack::Packer] A packer configured for coverage records.
|
|
120
146
|
def make_packer(io)
|
|
121
147
|
return make_factory.packer(io)
|
|
122
148
|
end
|
|
123
149
|
|
|
150
|
+
# Build a MessagePack unpacker for the given IO.
|
|
151
|
+
# @parameter io [IO] The IO to read packed records from.
|
|
152
|
+
# @returns [MessagePack::Unpacker] An unpacker configured for coverage records.
|
|
124
153
|
def make_unpacker(io)
|
|
125
154
|
return make_factory.unpacker(io)
|
|
126
155
|
end
|