relevance-rcov 0.8.5.2 → 0.8.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ # Rcov::CoverageInfo is but a wrapper for an array, with some additional
2
+ # checks. It is returned by FileStatistics#coverage.
3
+ class CoverageInfo
4
+ def initialize(coverage_array)
5
+ @cover = coverage_array.clone
6
+ end
7
+
8
+ # Return the coverage status for the requested line. There are four possible
9
+ # return values:
10
+ # * nil if there's no information for the requested line (i.e. it doesn't exist)
11
+ # * true if the line was reported by Ruby as executed
12
+ # * :inferred if rcov inferred it was executed, despite not being reported
13
+ # by Ruby.
14
+ # * false otherwise, i.e. if it was not reported by Ruby and rcov's
15
+ # heuristics indicated that it was not executed
16
+ def [](line)
17
+ @cover[line]
18
+ end
19
+
20
+ def []=(line, val) # :nodoc:
21
+ unless [true, false, :inferred].include? val
22
+ raise RuntimeError, "What does #{val} mean?"
23
+ end
24
+ return if line < 0 || line >= @cover.size
25
+ @cover[line] = val
26
+ end
27
+
28
+ # Return an Array holding the code coverage information.
29
+ def to_a
30
+ @cover.clone
31
+ end
32
+
33
+ def method_missing(meth, *a, &b) # :nodoc:
34
+ @cover.send(meth, *a, &b)
35
+ end
36
+ end
@@ -0,0 +1,116 @@
1
+ module Rcov
2
+ class DifferentialAnalyzer
3
+ require 'thread'
4
+ @@mutex = Mutex.new
5
+
6
+ def initialize(install_hook_meth, remove_hook_meth, reset_meth)
7
+ @cache_state = :wait
8
+ @start_raw_data = data_default
9
+ @end_raw_data = data_default
10
+ @aggregated_data = data_default
11
+ @install_hook_meth = install_hook_meth
12
+ @remove_hook_meth= remove_hook_meth
13
+ @reset_meth= reset_meth
14
+ end
15
+
16
+ # Execute the code in the given block, monitoring it in order to gather
17
+ # information about which code was executed.
18
+ def run_hooked
19
+ install_hook
20
+ yield
21
+ ensure
22
+ remove_hook
23
+ end
24
+
25
+ # Start monitoring execution to gather information. Such data will be
26
+ # collected until #remove_hook is called.
27
+ #
28
+ # Use #run_hooked instead if possible.
29
+ def install_hook
30
+ @start_raw_data = raw_data_absolute
31
+ Rcov::RCOV__.send(@install_hook_meth)
32
+ @cache_state = :hooked
33
+ @@mutex.synchronize{ self.class.hook_level += 1 }
34
+ end
35
+
36
+ # Stop collecting information.
37
+ # #remove_hook will also stop collecting info if it is run inside a
38
+ # #run_hooked block.
39
+ def remove_hook
40
+ @@mutex.synchronize do
41
+ self.class.hook_level -= 1
42
+ Rcov::RCOV__.send(@remove_hook_meth) if self.class.hook_level == 0
43
+ end
44
+ @end_raw_data = raw_data_absolute
45
+ @cache_state = :done
46
+ # force computation of the stats for the traced code in this run;
47
+ # we cannot simply let it be if self.class.hook_level == 0 because
48
+ # some other analyzer could install a hook, causing the raw_data_absolute
49
+ # to change again.
50
+ # TODO: lazy computation of raw_data_relative, only when the hook gets
51
+ # activated again.
52
+ raw_data_relative
53
+ end
54
+
55
+ # Remove the data collected so far. Further collection will start from
56
+ # scratch.
57
+ def reset
58
+ @@mutex.synchronize do
59
+ if self.class.hook_level == 0
60
+ # Unfortunately there's no way to report this as covered with rcov:
61
+ # if we run the tests under rcov self.class.hook_level will be >= 1 !
62
+ # It is however executed when we run the tests normally.
63
+ Rcov::RCOV__.send(@reset_meth)
64
+ @start_raw_data = data_default
65
+ @end_raw_data = data_default
66
+ else
67
+ @start_raw_data = @end_raw_data = raw_data_absolute
68
+ end
69
+ @raw_data_relative = data_default
70
+ @aggregated_data = data_default
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ def data_default
77
+ raise "must be implemented by the subclass"
78
+ end
79
+
80
+ def self.hook_level
81
+ raise "must be implemented by the subclass"
82
+ end
83
+
84
+ def raw_data_absolute
85
+ raise "must be implemented by the subclass"
86
+ end
87
+
88
+ def aggregate_data(aggregated_data, delta)
89
+ raise "must be implemented by the subclass"
90
+ end
91
+
92
+ def compute_raw_data_difference(first, last)
93
+ raise "must be implemented by the subclass"
94
+ end
95
+
96
+ private
97
+
98
+ def raw_data_relative
99
+ case @cache_state
100
+ when :wait
101
+ return @aggregated_data
102
+ when :hooked
103
+ new_start = raw_data_absolute
104
+ new_diff = compute_raw_data_difference(@start_raw_data, new_start)
105
+ @start_raw_data = new_start
106
+ when :done
107
+ @cache_state = :wait
108
+ new_diff = compute_raw_data_difference(@start_raw_data,
109
+ @end_raw_data)
110
+ end
111
+
112
+ aggregate_data(@aggregated_data, new_diff)
113
+ @aggregated_data
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,334 @@
1
+ module Rcov
2
+ # A FileStatistics object associates a filename to:
3
+ # 1. its source code
4
+ # 2. the per-line coverage information after correction using rcov's heuristics
5
+ # 3. the per-line execution counts
6
+ #
7
+ # A FileStatistics object can be therefore be built given the filename, the
8
+ # associated source code, and an array holding execution counts (i.e. how many
9
+ # times each line has been executed).
10
+ #
11
+ # FileStatistics is relatively intelligent: it handles normal comments,
12
+ # <tt>=begin/=end</tt>, heredocs, many multiline-expressions... It uses a
13
+ # number of heuristics to determine what is code and what is a comment, and to
14
+ # refine the initial (incomplete) coverage information.
15
+ #
16
+ # Basic usage is as follows:
17
+ # sf = FileStatistics.new("foo.rb", ["puts 1", "if true &&", " false",
18
+ # "puts 2", "end"], [1, 1, 0, 0, 0])
19
+ # sf.num_lines # => 5
20
+ # sf.num_code_lines # => 5
21
+ # sf.coverage[2] # => true
22
+ # sf.coverage[3] # => :inferred
23
+ # sf.code_coverage # => 0.6
24
+ #
25
+ # The array of strings representing the source code and the array of execution
26
+ # counts would normally be obtained from a Rcov::CodeCoverageAnalyzer.
27
+ class FileStatistics
28
+ attr_reader :name, :lines, :coverage, :counts
29
+ def initialize(name, lines, counts, comments_run_by_default = false)
30
+ @name = name
31
+ @lines = lines
32
+ initial_coverage = counts.map{|x| (x || 0) > 0 ? true : false }
33
+ @coverage = CoverageInfo.new initial_coverage
34
+ @counts = counts
35
+ @is_begin_comment = nil
36
+ # points to the line defining the heredoc identifier
37
+ # but only if it was marked (we don't care otherwise)
38
+ @heredoc_start = Array.new(lines.size, false)
39
+ @multiline_string_start = Array.new(lines.size, false)
40
+ extend_heredocs
41
+ find_multiline_strings
42
+ precompute_coverage comments_run_by_default
43
+ end
44
+
45
+ # Merge code coverage and execution count information.
46
+ # As for code coverage, a line will be considered
47
+ # * covered for sure (true) if it is covered in either +self+ or in the
48
+ # +coverage+ array
49
+ # * considered <tt>:inferred</tt> if the neither +self+ nor the +coverage+ array
50
+ # indicate that it was definitely executed, but it was <tt>inferred</tt>
51
+ # in either one
52
+ # * not covered (<tt>false</tt>) if it was uncovered in both
53
+ #
54
+ # Execution counts are just summated on a per-line basis.
55
+ def merge(lines, coverage, counts)
56
+ coverage.each_with_index do |v, idx|
57
+ case @coverage[idx]
58
+ when :inferred
59
+ @coverage[idx] = v || @coverage[idx]
60
+ when false
61
+ @coverage[idx] ||= v
62
+ end
63
+ end
64
+ counts.each_with_index{|v, idx| @counts[idx] += v }
65
+ precompute_coverage false
66
+ end
67
+
68
+ # Total coverage rate if comments are also considered "executable", given as
69
+ # a fraction, i.e. from 0 to 1.0.
70
+ # A comment is attached to the code following it (RDoc-style): it will be
71
+ # considered executed if the the next statement was executed.
72
+ def total_coverage
73
+ return 0 if @coverage.size == 0
74
+ @coverage.inject(0.0) {|s,a| s + (a ? 1:0) } / @coverage.size
75
+ end
76
+
77
+ # Code coverage rate: fraction of lines of code executed, relative to the
78
+ # total amount of lines of code (loc). Returns a float from 0 to 1.0.
79
+ def code_coverage
80
+ indices = (0...@lines.size).select{|i| is_code? i }
81
+ return 0 if indices.size == 0
82
+ count = 0
83
+ indices.each {|i| count += 1 if @coverage[i] }
84
+ 1.0 * count / indices.size
85
+ end
86
+
87
+ def code_coverage_for_report
88
+ code_coverage * 100
89
+ end
90
+
91
+ def total_coverage_for_report
92
+ total_coverage * 100
93
+ end
94
+
95
+ # Number of lines of code (loc).
96
+ def num_code_lines
97
+ (0...@lines.size).select{|i| is_code? i}.size
98
+ end
99
+
100
+ # Total number of lines.
101
+ def num_lines
102
+ @lines.size
103
+ end
104
+
105
+ # Returns true if the given line number corresponds to code, as opposed to a
106
+ # comment (either # or =begin/=end blocks).
107
+ def is_code?(lineno)
108
+ unless @is_begin_comment
109
+ @is_begin_comment = Array.new(@lines.size, false)
110
+ pending = []
111
+ state = :code
112
+ @lines.each_with_index do |line, index|
113
+ case state
114
+ when :code
115
+ if /^=begin\b/ =~ line
116
+ state = :comment
117
+ pending << index
118
+ end
119
+ when :comment
120
+ pending << index
121
+ if /^=end\b/ =~ line
122
+ state = :code
123
+ pending.each{|idx| @is_begin_comment[idx] = true}
124
+ pending.clear
125
+ end
126
+ end
127
+ end
128
+ end
129
+ @lines[lineno] && !@is_begin_comment[lineno] &&
130
+ @lines[lineno] !~ /^\s*(#|$)/
131
+ end
132
+
133
+ private
134
+
135
+ def find_multiline_strings
136
+ state = :awaiting_string
137
+ wanted_delimiter = nil
138
+ string_begin_line = 0
139
+ @lines.each_with_index do |line, i|
140
+ matching_delimiters = Hash.new{|h,k| k}
141
+ matching_delimiters.update("{" => "}", "[" => "]", "(" => ")")
142
+ case state
143
+ when :awaiting_string
144
+ # very conservative, doesn't consider the last delimited string but
145
+ # only the very first one
146
+ if md = /^[^#]*%(?:[qQ])?(.)/.match(line)
147
+ if !/"%"/.match(line)
148
+ wanted_delimiter = /(?!\\).#{Regexp.escape(matching_delimiters[md[1]])}/
149
+ # check if closed on the very same line
150
+ # conservative again, we might have several quoted strings with the
151
+ # same delimiter on the same line, leaving the last one open
152
+ unless wanted_delimiter.match(md.post_match)
153
+ state = :want_end_delimiter
154
+ string_begin_line = i
155
+ end
156
+ end
157
+ end
158
+ when :want_end_delimiter
159
+ @multiline_string_start[i] = string_begin_line
160
+ if wanted_delimiter.match(line)
161
+ state = :awaiting_string
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ def precompute_coverage(comments_run_by_default = true)
168
+ changed = false
169
+ lastidx = lines.size - 1
170
+ if (!is_code?(lastidx) || /^__END__$/ =~ @lines[-1]) && !@coverage[lastidx]
171
+ # mark the last block of comments
172
+ @coverage[lastidx] ||= :inferred
173
+ (lastidx-1).downto(0) do |i|
174
+ break if is_code?(i)
175
+ @coverage[i] ||= :inferred
176
+ end
177
+ end
178
+ (0...lines.size).each do |i|
179
+ next if @coverage[i]
180
+ line = @lines[i]
181
+ if /^\s*(begin|ensure|else|case)\s*(?:#.*)?$/ =~ line && next_expr_marked?(i) or
182
+ /^\s*(?:end|\})\s*(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
183
+ /^\s*(?:end\b|\})/ =~ line && prev_expr_marked?(i) && next_expr_marked?(i) or
184
+ /^\s*rescue\b/ =~ line && next_expr_marked?(i) or
185
+ /(do|\{)\s*(\|[^|]*\|\s*)?(?:#.*)?$/ =~ line && next_expr_marked?(i) or
186
+ prev_expr_continued?(i) && prev_expr_marked?(i) or
187
+ comments_run_by_default && !is_code?(i) or
188
+ /^\s*((\)|\]|\})\s*)+(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
189
+ prev_expr_continued?(i+1) && next_expr_marked?(i)
190
+ @coverage[i] ||= :inferred
191
+ changed = true
192
+ end
193
+ end
194
+ (@lines.size-1).downto(0) do |i|
195
+ next if @coverage[i]
196
+ if !is_code?(i) and @coverage[i+1]
197
+ @coverage[i] = :inferred
198
+ changed = true
199
+ end
200
+ end
201
+
202
+ extend_heredocs if changed
203
+
204
+ # if there was any change, we have to recompute; we'll eventually
205
+ # reach a fixed point and stop there
206
+ precompute_coverage(comments_run_by_default) if changed
207
+ end
208
+
209
+ require 'strscan'
210
+ def extend_heredocs
211
+ i = 0
212
+ while i < @lines.size
213
+ unless is_code? i
214
+ i += 1
215
+ next
216
+ end
217
+ #FIXME: using a restrictive regexp so that only <<[A-Z_a-z]\w*
218
+ # matches when unquoted, so as to avoid problems with 1<<2
219
+ # (keep in mind that whereas puts <<2 is valid, puts 1<<2 is a
220
+ # parse error, but a = 1<<2 is of course fine)
221
+ scanner = StringScanner.new(@lines[i])
222
+ j = k = i
223
+ loop do
224
+ scanned_text = scanner.search_full(/<<(-?)(?:(['"`])((?:(?!\2).)+)\2|([A-Z_a-z]\w*))/, true, true)
225
+ # k is the first line after the end delimiter for the last heredoc
226
+ # scanned so far
227
+ unless scanner.matched?
228
+ i = k
229
+ break
230
+ end
231
+ term = scanner[3] || scanner[4]
232
+ # try to ignore symbolic bitshifts like 1<<LSHIFT
233
+ ident_text = "<<#{scanner[1]}#{scanner[2]}#{term}#{scanner[2]}"
234
+ if scanned_text[/\d+\s*#{Regexp.escape(ident_text)}/]
235
+ # it was preceded by a number, ignore
236
+ i = k
237
+ break
238
+ end
239
+ must_mark = []
240
+ end_of_heredoc = (scanner[1] == "-") ? /^\s*#{Regexp.escape(term)}$/ : /^#{Regexp.escape(term)}$/
241
+ loop do
242
+ break if j == @lines.size
243
+ must_mark << j
244
+ if end_of_heredoc =~ @lines[j]
245
+ must_mark.each do |n|
246
+ @heredoc_start[n] = i
247
+ end
248
+ if (must_mark + [i]).any?{|lineidx| @coverage[lineidx]}
249
+ @coverage[i] ||= :inferred
250
+ must_mark.each{|lineidx| @coverage[lineidx] ||= :inferred}
251
+ end
252
+ # move the "first line after heredocs" index
253
+ if @lines[j+=1] =~ /^\s*\n$/
254
+ k = j
255
+ end
256
+ break
257
+ end
258
+ j += 1
259
+ end
260
+ end
261
+
262
+ i += 1
263
+ end
264
+ end
265
+
266
+ def next_expr_marked?(lineno)
267
+ return false if lineno >= @lines.size
268
+ found = false
269
+ idx = (lineno+1).upto(@lines.size-1) do |i|
270
+ next unless is_code? i
271
+ found = true
272
+ break i
273
+ end
274
+ return false unless found
275
+ @coverage[idx]
276
+ end
277
+
278
+ def prev_expr_marked?(lineno)
279
+ return false if lineno <= 0
280
+ found = false
281
+ idx = (lineno-1).downto(0) do |i|
282
+ next unless is_code? i
283
+ found = true
284
+ break i
285
+ end
286
+ return false unless found
287
+ @coverage[idx]
288
+ end
289
+
290
+ def prev_expr_continued?(lineno)
291
+ return false if lineno <= 0
292
+ return false if lineno >= @lines.size
293
+ found = false
294
+ if @multiline_string_start[lineno] &&
295
+ @multiline_string_start[lineno] < lineno
296
+ return true
297
+ end
298
+ # find index of previous code line
299
+ idx = (lineno-1).downto(0) do |i|
300
+ if @heredoc_start[i]
301
+ found = true
302
+ break @heredoc_start[i]
303
+ end
304
+ next unless is_code? i
305
+ found = true
306
+ break i
307
+ end
308
+ return false unless found
309
+ #TODO: write a comprehensive list
310
+ if is_code?(lineno) && /^\s*((\)|\]|\})\s*)+(?:#.*)?$/.match(@lines[lineno])
311
+ return true
312
+ end
313
+ #FIXME: / matches regexps too
314
+ # the following regexp tries to reject #{interpolation}
315
+ r = /(,|\.|\+|-|\*|\/|<|>|%|&&|\|\||<<|\(|\[|\{|=|and|or|\\)\s*(?:#(?![{$@]).*)?$/.match @lines[idx]
316
+ # try to see if a multi-line expression with opening, closing delimiters
317
+ # started on that line
318
+ [%w!( )!].each do |opening_str, closing_str|
319
+ # conservative: only consider nesting levels opened in that line, not
320
+ # previous ones too.
321
+ # next regexp considers interpolation too
322
+ line = @lines[idx].gsub(/#(?![{$@]).*$/, "")
323
+ opened = line.scan(/#{Regexp.escape(opening_str)}/).size
324
+ closed = line.scan(/#{Regexp.escape(closing_str)}/).size
325
+ return true if opened - closed > 0
326
+ end
327
+ if /(do|\{)\s*\|[^|]*\|\s*(?:#.*)?$/.match @lines[idx]
328
+ return false
329
+ end
330
+
331
+ r
332
+ end
333
+ end
334
+ end