relevance-rcov 0.8.5.2 → 0.8.6

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.
data/lib/rcov.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # rcov Copyright (c) 2004-2006 Mauricio Fernandez <mfp@acm.org>
2
2
  #
3
- # See LEGAL and LICENSE for licensing information.
3
+ # See LICENSE for licensing information.
4
4
 
5
5
  # NOTE: if you're reading this in the XHTML code coverage report generated by
6
6
  # rcov, you'll notice that only code inside methods is reported as covered,
@@ -11,1001 +11,23 @@
11
11
 
12
12
  require 'rcov/version'
13
13
  require 'rcov/formatters'
14
+ require 'rcov/coverage_info'
15
+ require 'rcov/file_statistics'
16
+ require 'rcov/differential_analyzer'
17
+ require 'rcov/code_coverage_analyzer'
18
+ require 'rcov/call_site_analyzer'
14
19
 
15
20
  SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__
16
21
 
17
22
  module Rcov
18
-
19
- # TODO: Move to Ruby 1.8.6 Backport module
20
- unless RUBY_VERSION =~ /1.9/
21
-
22
- class ::String
23
-
24
- def lines
25
- map
26
- end
27
-
28
- end
29
-
30
- end
31
- # Rcov::CoverageInfo is but a wrapper for an array, with some additional
32
- # checks. It is returned by FileStatistics#coverage.
33
- class CoverageInfo
34
- def initialize(coverage_array)
35
- @cover = coverage_array.clone
36
- end
37
-
38
- # Return the coverage status for the requested line. There are four possible
39
- # return values:
40
- # * nil if there's no information for the requested line (i.e. it doesn't exist)
41
- # * true if the line was reported by Ruby as executed
42
- # * :inferred if rcov inferred it was executed, despite not being reported
43
- # by Ruby.
44
- # * false otherwise, i.e. if it was not reported by Ruby and rcov's
45
- # heuristics indicated that it was not executed
46
- def [](line)
47
- @cover[line]
48
- end
49
-
50
- def []=(line, val) # :nodoc:
51
- unless [true, false, :inferred].include? val
52
- raise RuntimeError, "What does #{val} mean?"
53
- end
54
- return if line < 0 || line >= @cover.size
55
- @cover[line] = val
56
- end
57
-
58
- # Return an Array holding the code coverage information.
59
- def to_a
60
- @cover.clone
61
- end
62
-
63
- def method_missing(meth, *a, &b) # :nodoc:
64
- @cover.send(meth, *a, &b)
65
- end
66
- end
67
-
68
- # A FileStatistics object associates a filename to:
69
- # 1. its source code
70
- # 2. the per-line coverage information after correction using rcov's heuristics
71
- # 3. the per-line execution counts
72
- #
73
- # A FileStatistics object can be therefore be built given the filename, the
74
- # associated source code, and an array holding execution counts (i.e. how many
75
- # times each line has been executed).
76
- #
77
- # FileStatistics is relatively intelligent: it handles normal comments,
78
- # <tt>=begin/=end</tt>, heredocs, many multiline-expressions... It uses a
79
- # number of heuristics to determine what is code and what is a comment, and to
80
- # refine the initial (incomplete) coverage information.
81
- #
82
- # Basic usage is as follows:
83
- # sf = FileStatistics.new("foo.rb", ["puts 1", "if true &&", " false",
84
- # "puts 2", "end"], [1, 1, 0, 0, 0])
85
- # sf.num_lines # => 5
86
- # sf.num_code_lines # => 5
87
- # sf.coverage[2] # => true
88
- # sf.coverage[3] # => :inferred
89
- # sf.code_coverage # => 0.6
90
- #
91
- # The array of strings representing the source code and the array of execution
92
- # counts would normally be obtained from a Rcov::CodeCoverageAnalyzer.
93
- class FileStatistics
94
- attr_reader :name, :lines, :coverage, :counts
95
- def initialize(name, lines, counts, comments_run_by_default = false)
96
- @name = name
97
- @lines = lines
98
- initial_coverage = counts.map{|x| (x || 0) > 0 ? true : false }
99
- @coverage = CoverageInfo.new initial_coverage
100
- @counts = counts
101
- @is_begin_comment = nil
102
- # points to the line defining the heredoc identifier
103
- # but only if it was marked (we don't care otherwise)
104
- @heredoc_start = Array.new(lines.size, false)
105
- @multiline_string_start = Array.new(lines.size, false)
106
- extend_heredocs
107
- find_multiline_strings
108
- precompute_coverage comments_run_by_default
109
- end
110
-
111
- # Merge code coverage and execution count information.
112
- # As for code coverage, a line will be considered
113
- # * covered for sure (true) if it is covered in either +self+ or in the
114
- # +coverage+ array
115
- # * considered <tt>:inferred</tt> if the neither +self+ nor the +coverage+ array
116
- # indicate that it was definitely executed, but it was <tt>inferred</tt>
117
- # in either one
118
- # * not covered (<tt>false</tt>) if it was uncovered in both
119
- #
120
- # Execution counts are just summated on a per-line basis.
121
- def merge(lines, coverage, counts)
122
- coverage.each_with_index do |v, idx|
123
- case @coverage[idx]
124
- when :inferred
125
- @coverage[idx] = v || @coverage[idx]
126
- when false
127
- @coverage[idx] ||= v
128
- end
129
- end
130
- counts.each_with_index{|v, idx| @counts[idx] += v }
131
- precompute_coverage false
132
- end
133
-
134
- # Total coverage rate if comments are also considered "executable", given as
135
- # a fraction, i.e. from 0 to 1.0.
136
- # A comment is attached to the code following it (RDoc-style): it will be
137
- # considered executed if the the next statement was executed.
138
- def total_coverage
139
- return 0 if @coverage.size == 0
140
- @coverage.inject(0.0) {|s,a| s + (a ? 1:0) } / @coverage.size
141
- end
142
-
143
- # Code coverage rate: fraction of lines of code executed, relative to the
144
- # total amount of lines of code (loc). Returns a float from 0 to 1.0.
145
- def code_coverage
146
- indices = (0...@lines.size).select{|i| is_code? i }
147
- return 0 if indices.size == 0
148
- count = 0
149
- indices.each {|i| count += 1 if @coverage[i] }
150
- 1.0 * count / indices.size
151
- end
152
-
153
- def code_coverage_for_report
154
- code_coverage * 100
155
- end
156
-
157
- def total_coverage_for_report
158
- total_coverage * 100
159
- end
160
-
161
- # Number of lines of code (loc).
162
- def num_code_lines
163
- (0...@lines.size).select{|i| is_code? i}.size
164
- end
165
-
166
- # Total number of lines.
167
- def num_lines
168
- @lines.size
169
- end
170
-
171
- # Returns true if the given line number corresponds to code, as opposed to a
172
- # comment (either # or =begin/=end blocks).
173
- def is_code?(lineno)
174
- unless @is_begin_comment
175
- @is_begin_comment = Array.new(@lines.size, false)
176
- pending = []
177
- state = :code
178
- @lines.each_with_index do |line, index|
179
- case state
180
- when :code
181
- if /^=begin\b/ =~ line
182
- state = :comment
183
- pending << index
184
- end
185
- when :comment
186
- pending << index
187
- if /^=end\b/ =~ line
188
- state = :code
189
- pending.each{|idx| @is_begin_comment[idx] = true}
190
- pending.clear
191
- end
192
- end
193
- end
194
- end
195
- @lines[lineno] && !@is_begin_comment[lineno] &&
196
- @lines[lineno] !~ /^\s*(#|$)/
197
- end
198
-
199
- private
200
-
201
- def find_multiline_strings
202
- state = :awaiting_string
203
- wanted_delimiter = nil
204
- string_begin_line = 0
205
- @lines.each_with_index do |line, i|
206
- matching_delimiters = Hash.new{|h,k| k}
207
- matching_delimiters.update("{" => "}", "[" => "]", "(" => ")")
208
- case state
209
- when :awaiting_string
210
- # very conservative, doesn't consider the last delimited string but
211
- # only the very first one
212
- if md = /^[^#]*%(?:[qQ])?(.)/.match(line)
213
- if !/"%"/.match(line)
214
- wanted_delimiter = /(?!\\).#{Regexp.escape(matching_delimiters[md[1]])}/
215
- # check if closed on the very same line
216
- # conservative again, we might have several quoted strings with the
217
- # same delimiter on the same line, leaving the last one open
218
- unless wanted_delimiter.match(md.post_match)
219
- state = :want_end_delimiter
220
- string_begin_line = i
221
- end
222
- end
223
- end
224
- when :want_end_delimiter
225
- @multiline_string_start[i] = string_begin_line
226
- if wanted_delimiter.match(line)
227
- state = :awaiting_string
228
- end
229
- end
230
- end
231
- end
232
-
233
- def precompute_coverage(comments_run_by_default = true)
234
- changed = false
235
- lastidx = lines.size - 1
236
- if (!is_code?(lastidx) || /^__END__$/ =~ @lines[-1]) && !@coverage[lastidx]
237
- # mark the last block of comments
238
- @coverage[lastidx] ||= :inferred
239
- (lastidx-1).downto(0) do |i|
240
- break if is_code?(i)
241
- @coverage[i] ||= :inferred
242
- end
243
- end
244
- (0...lines.size).each do |i|
245
- next if @coverage[i]
246
- line = @lines[i]
247
- if /^\s*(begin|ensure|else|case)\s*(?:#.*)?$/ =~ line && next_expr_marked?(i) or
248
- /^\s*(?:end|\})\s*(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
249
- /^\s*(?:end\b|\})/ =~ line && prev_expr_marked?(i) && next_expr_marked?(i) or
250
- /^\s*rescue\b/ =~ line && next_expr_marked?(i) or
251
- /(do|\{)\s*(\|[^|]*\|\s*)?(?:#.*)?$/ =~ line && next_expr_marked?(i) or
252
- prev_expr_continued?(i) && prev_expr_marked?(i) or
253
- comments_run_by_default && !is_code?(i) or
254
- /^\s*((\)|\]|\})\s*)+(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
255
- prev_expr_continued?(i+1) && next_expr_marked?(i)
256
- @coverage[i] ||= :inferred
257
- changed = true
258
- end
259
- end
260
- (@lines.size-1).downto(0) do |i|
261
- next if @coverage[i]
262
- if !is_code?(i) and @coverage[i+1]
263
- @coverage[i] = :inferred
264
- changed = true
23
+ # TODO: Move to Ruby 1.8.6 Backport module
24
+ unless RUBY_VERSION =~ /1.9/
25
+ class ::String
26
+ def lines
27
+ map
265
28
  end
266
29
  end
267
-
268
- extend_heredocs if changed
269
-
270
- # if there was any change, we have to recompute; we'll eventually
271
- # reach a fixed point and stop there
272
- precompute_coverage(comments_run_by_default) if changed
273
30
  end
274
31
 
275
- require 'strscan'
276
- def extend_heredocs
277
- i = 0
278
- while i < @lines.size
279
- unless is_code? i
280
- i += 1
281
- next
282
- end
283
- #FIXME: using a restrictive regexp so that only <<[A-Z_a-z]\w*
284
- # matches when unquoted, so as to avoid problems with 1<<2
285
- # (keep in mind that whereas puts <<2 is valid, puts 1<<2 is a
286
- # parse error, but a = 1<<2 is of course fine)
287
- scanner = StringScanner.new(@lines[i])
288
- j = k = i
289
- loop do
290
- scanned_text = scanner.search_full(/<<(-?)(?:(['"`])((?:(?!\2).)+)\2|([A-Z_a-z]\w*))/, true, true)
291
- # k is the first line after the end delimiter for the last heredoc
292
- # scanned so far
293
- unless scanner.matched?
294
- i = k
295
- break
296
- end
297
- term = scanner[3] || scanner[4]
298
- # try to ignore symbolic bitshifts like 1<<LSHIFT
299
- ident_text = "<<#{scanner[1]}#{scanner[2]}#{term}#{scanner[2]}"
300
- if scanned_text[/\d+\s*#{Regexp.escape(ident_text)}/]
301
- # it was preceded by a number, ignore
302
- i = k
303
- break
304
- end
305
- must_mark = []
306
- end_of_heredoc = (scanner[1] == "-") ? /^\s*#{Regexp.escape(term)}$/ : /^#{Regexp.escape(term)}$/
307
- loop do
308
- break if j == @lines.size
309
- must_mark << j
310
- if end_of_heredoc =~ @lines[j]
311
- must_mark.each do |n|
312
- @heredoc_start[n] = i
313
- end
314
- if (must_mark + [i]).any?{|lineidx| @coverage[lineidx]}
315
- @coverage[i] ||= :inferred
316
- must_mark.each{|lineidx| @coverage[lineidx] ||= :inferred}
317
- end
318
- # move the "first line after heredocs" index
319
- if @lines[j+=1] =~ /^\s*\n$/
320
- k = j
321
- end
322
- break
323
- end
324
- j += 1
325
- end
326
- end
327
-
328
- i += 1
329
- end
330
- end
331
-
332
- def next_expr_marked?(lineno)
333
- return false if lineno >= @lines.size
334
- found = false
335
- idx = (lineno+1).upto(@lines.size-1) do |i|
336
- next unless is_code? i
337
- found = true
338
- break i
339
- end
340
- return false unless found
341
- @coverage[idx]
342
- end
343
-
344
- def prev_expr_marked?(lineno)
345
- return false if lineno <= 0
346
- found = false
347
- idx = (lineno-1).downto(0) do |i|
348
- next unless is_code? i
349
- found = true
350
- break i
351
- end
352
- return false unless found
353
- @coverage[idx]
354
- end
355
-
356
- def prev_expr_continued?(lineno)
357
- return false if lineno <= 0
358
- return false if lineno >= @lines.size
359
- found = false
360
- if @multiline_string_start[lineno] &&
361
- @multiline_string_start[lineno] < lineno
362
- return true
363
- end
364
- # find index of previous code line
365
- idx = (lineno-1).downto(0) do |i|
366
- if @heredoc_start[i]
367
- found = true
368
- break @heredoc_start[i]
369
- end
370
- next unless is_code? i
371
- found = true
372
- break i
373
- end
374
- return false unless found
375
- #TODO: write a comprehensive list
376
- if is_code?(lineno) && /^\s*((\)|\]|\})\s*)+(?:#.*)?$/.match(@lines[lineno])
377
- return true
378
- end
379
- #FIXME: / matches regexps too
380
- # the following regexp tries to reject #{interpolation}
381
- r = /(,|\.|\+|-|\*|\/|<|>|%|&&|\|\||<<|\(|\[|\{|=|and|or|\\)\s*(?:#(?![{$@]).*)?$/.match @lines[idx]
382
- # try to see if a multi-line expression with opening, closing delimiters
383
- # started on that line
384
- [%w!( )!].each do |opening_str, closing_str|
385
- # conservative: only consider nesting levels opened in that line, not
386
- # previous ones too.
387
- # next regexp considers interpolation too
388
- line = @lines[idx].gsub(/#(?![{$@]).*$/, "")
389
- opened = line.scan(/#{Regexp.escape(opening_str)}/).size
390
- closed = line.scan(/#{Regexp.escape(closing_str)}/).size
391
- return true if opened - closed > 0
392
- end
393
- if /(do|\{)\s*\|[^|]*\|\s*(?:#.*)?$/.match @lines[idx]
394
- return false
395
- end
396
-
397
- r
398
- end
399
- end
400
-
401
-
402
- autoload :RCOV__, "rcov/lowlevel.rb"
403
-
404
- class DifferentialAnalyzer
405
- require 'thread'
406
- @@mutex = Mutex.new
407
-
408
- def initialize(install_hook_meth, remove_hook_meth, reset_meth)
409
- @cache_state = :wait
410
- @start_raw_data = data_default
411
- @end_raw_data = data_default
412
- @aggregated_data = data_default
413
- @install_hook_meth = install_hook_meth
414
- @remove_hook_meth= remove_hook_meth
415
- @reset_meth= reset_meth
416
- end
417
-
418
- # Execute the code in the given block, monitoring it in order to gather
419
- # information about which code was executed.
420
- def run_hooked
421
- install_hook
422
- yield
423
- ensure
424
- remove_hook
425
- end
426
-
427
- # Start monitoring execution to gather information. Such data will be
428
- # collected until #remove_hook is called.
429
- #
430
- # Use #run_hooked instead if possible.
431
- def install_hook
432
- @start_raw_data = raw_data_absolute
433
- Rcov::RCOV__.send(@install_hook_meth)
434
- @cache_state = :hooked
435
- @@mutex.synchronize{ self.class.hook_level += 1 }
436
- end
437
-
438
- # Stop collecting information.
439
- # #remove_hook will also stop collecting info if it is run inside a
440
- # #run_hooked block.
441
- def remove_hook
442
- @@mutex.synchronize do
443
- self.class.hook_level -= 1
444
- Rcov::RCOV__.send(@remove_hook_meth) if self.class.hook_level == 0
445
- end
446
- @end_raw_data = raw_data_absolute
447
- @cache_state = :done
448
- # force computation of the stats for the traced code in this run;
449
- # we cannot simply let it be if self.class.hook_level == 0 because
450
- # some other analyzer could install a hook, causing the raw_data_absolute
451
- # to change again.
452
- # TODO: lazy computation of raw_data_relative, only when the hook gets
453
- # activated again.
454
- raw_data_relative
455
- end
456
-
457
- # Remove the data collected so far. Further collection will start from
458
- # scratch.
459
- def reset
460
- @@mutex.synchronize do
461
- if self.class.hook_level == 0
462
- # Unfortunately there's no way to report this as covered with rcov:
463
- # if we run the tests under rcov self.class.hook_level will be >= 1 !
464
- # It is however executed when we run the tests normally.
465
- Rcov::RCOV__.send(@reset_meth)
466
- @start_raw_data = data_default
467
- @end_raw_data = data_default
468
- else
469
- @start_raw_data = @end_raw_data = raw_data_absolute
470
- end
471
- @raw_data_relative = data_default
472
- @aggregated_data = data_default
473
- end
474
- end
475
-
476
- protected
477
-
478
- def data_default
479
- raise "must be implemented by the subclass"
480
- end
481
-
482
- def self.hook_level
483
- raise "must be implemented by the subclass"
484
- end
485
-
486
- def raw_data_absolute
487
- raise "must be implemented by the subclass"
488
- end
489
-
490
- def aggregate_data(aggregated_data, delta)
491
- raise "must be implemented by the subclass"
492
- end
493
-
494
- def compute_raw_data_difference(first, last)
495
- raise "must be implemented by the subclass"
496
- end
497
-
498
- private
499
- def raw_data_relative
500
- case @cache_state
501
- when :wait
502
- return @aggregated_data
503
- when :hooked
504
- new_start = raw_data_absolute
505
- new_diff = compute_raw_data_difference(@start_raw_data, new_start)
506
- @start_raw_data = new_start
507
- when :done
508
- @cache_state = :wait
509
- new_diff = compute_raw_data_difference(@start_raw_data,
510
- @end_raw_data)
511
- end
512
-
513
- aggregate_data(@aggregated_data, new_diff)
514
-
515
- @aggregated_data
516
- end
517
-
518
- end
519
-
520
- # A CodeCoverageAnalyzer is responsible for tracing code execution and
521
- # returning code coverage and execution count information.
522
- #
523
- # Note that you must <tt>require 'rcov'</tt> before the code you want to
524
- # analyze is parsed (i.e. before it gets loaded or required). You can do that
525
- # by either invoking ruby with the <tt>-rrcov</tt> command-line option or
526
- # just:
527
- # require 'rcov'
528
- # require 'mycode'
529
- # # ....
530
- #
531
- # == Example
532
- #
533
- # analyzer = Rcov::CodeCoverageAnalyzer.new
534
- # analyzer.run_hooked do
535
- # do_foo
536
- # # all the code executed as a result of this method call is traced
537
- # end
538
- # # ....
539
- #
540
- # analyzer.run_hooked do
541
- # do_bar
542
- # # the code coverage information generated in this run is aggregated
543
- # # to the previously recorded one
544
- # end
545
- #
546
- # analyzer.analyzed_files # => ["foo.rb", "bar.rb", ... ]
547
- # lines, marked_info, count_info = analyzer.data("foo.rb")
548
- #
549
- # In this example, two pieces of code are monitored, and the data generated in
550
- # both runs are aggregated. +lines+ is an array of strings representing the
551
- # source code of <tt>foo.rb</tt>. +marked_info+ is an array holding false,
552
- # true values indicating whether the corresponding lines of code were reported
553
- # as executed by Ruby. +count_info+ is an array of integers representing how
554
- # many times each line of code has been executed (more precisely, how many
555
- # events where reported by Ruby --- a single line might correspond to several
556
- # events, e.g. many method calls).
557
- #
558
- # You can have several CodeCoverageAnalyzer objects at a time, and it is
559
- # possible to nest the #run_hooked / #install_hook/#remove_hook blocks: each
560
- # analyzer will manage its data separately. Note however that no special
561
- # provision is taken to ignore code executed "inside" the CodeCoverageAnalyzer
562
- # class. At any rate this will not pose a problem since it's easy to ignore it
563
- # manually: just don't do
564
- # lines, coverage, counts = analyzer.data("/path/to/lib/rcov.rb")
565
- # if you're not interested in that information.
566
- class CodeCoverageAnalyzer < DifferentialAnalyzer
567
- @hook_level = 0
568
- # defined this way instead of attr_accessor so that it's covered
569
- def self.hook_level # :nodoc:
570
- @hook_level
571
- end
572
- def self.hook_level=(x) # :nodoc:
573
- @hook_level = x
574
- end
575
-
576
- def initialize
577
- @script_lines__ = SCRIPT_LINES__
578
- super(:install_coverage_hook, :remove_coverage_hook,
579
- :reset_coverage)
580
- end
581
-
582
- # Return an array with the names of the files whose code was executed inside
583
- # the block given to #run_hooked or between #install_hook and #remove_hook.
584
- def analyzed_files
585
- update_script_lines__
586
- raw_data_relative.select do |file, lines|
587
- @script_lines__.has_key?(file)
588
- end.map{|fname,| fname}
589
- end
590
-
591
- # Return the available data about the requested file, or nil if none of its
592
- # code was executed or it cannot be found.
593
- # The return value is an array with three elements:
594
- # lines, marked_info, count_info = analyzer.data("foo.rb")
595
- # +lines+ is an array of strings representing the
596
- # source code of <tt>foo.rb</tt>. +marked_info+ is an array holding false,
597
- # true values indicating whether the corresponding lines of code were reported
598
- # as executed by Ruby. +count_info+ is an array of integers representing how
599
- # many times each line of code has been executed (more precisely, how many
600
- # events where reported by Ruby --- a single line might correspond to several
601
- # events, e.g. many method calls).
602
- #
603
- # The returned data corresponds to the aggregation of all the statistics
604
- # collected in each #run_hooked or #install_hook/#remove_hook runs. You can
605
- # reset the data at any time with #reset to start from scratch.
606
- def data(filename)
607
- raw_data = raw_data_relative
608
- update_script_lines__
609
- unless @script_lines__.has_key?(filename) &&
610
- raw_data.has_key?(filename)
611
- return nil
612
- end
613
- refine_coverage_info(@script_lines__[filename], raw_data[filename])
614
- end
615
-
616
- # Data for the first file matching the given regexp.
617
- # See #data.
618
- def data_matching(filename_re)
619
- raw_data = raw_data_relative
620
- update_script_lines__
621
-
622
- match = raw_data.keys.sort.grep(filename_re).first
623
- return nil unless match
624
-
625
- refine_coverage_info(@script_lines__[match], raw_data[match])
626
- end
627
-
628
- # Execute the code in the given block, monitoring it in order to gather
629
- # information about which code was executed.
630
- def run_hooked; super end
631
-
632
- # Start monitoring execution to gather code coverage and execution count
633
- # information. Such data will be collected until #remove_hook is called.
634
- #
635
- # Use #run_hooked instead if possible.
636
- def install_hook; super end
637
-
638
- # Stop collecting code coverage and execution count information.
639
- # #remove_hook will also stop collecting info if it is run inside a
640
- # #run_hooked block.
641
- def remove_hook; super end
642
-
643
- # Remove the data collected so far. The coverage and execution count
644
- # "history" will be erased, and further collection will start from scratch:
645
- # no code is considered executed, and therefore all execution counts are 0.
646
- # Right after #reset, #analyzed_files will return an empty array, and
647
- # #data(filename) will return nil.
648
- def reset; super end
649
-
650
- def dump_coverage_info(formatters) # :nodoc:
651
- update_script_lines__
652
- raw_data_relative.each do |file, lines|
653
- next if @script_lines__.has_key?(file) == false
654
- lines = @script_lines__[file]
655
- raw_coverage_array = raw_data_relative[file]
656
-
657
- line_info, marked_info,
658
- count_info = refine_coverage_info(lines, raw_coverage_array)
659
- formatters.each do |formatter|
660
- formatter.add_file(file, line_info, marked_info, count_info)
661
- end
662
- end
663
- formatters.each{|formatter| formatter.execute}
664
- end
665
-
666
- private
667
-
668
- def data_default; {} end
669
-
670
- def raw_data_absolute
671
- Rcov::RCOV__.generate_coverage_info
672
- end
673
-
674
- def aggregate_data(aggregated_data, delta)
675
- delta.each_pair do |file, cov_arr|
676
- dest = (aggregated_data[file] ||= Array.new(cov_arr.size, 0))
677
- cov_arr.each_with_index{|x,i| dest[i] += x}
678
- end
679
- end
680
-
681
- def compute_raw_data_difference(first, last)
682
- difference = {}
683
- last.each_pair do |fname, cov_arr|
684
- unless first.has_key?(fname)
685
- difference[fname] = cov_arr.clone
686
- else
687
- orig_arr = first[fname]
688
- diff_arr = Array.new(cov_arr.size, 0)
689
- changed = false
690
- cov_arr.each_with_index do |x, i|
691
- diff_arr[i] = diff = (x || 0) - (orig_arr[i] || 0)
692
- changed = true if diff != 0
693
- end
694
- difference[fname] = diff_arr if changed
695
- end
696
- end
697
- difference
698
- end
699
-
700
-
701
- def refine_coverage_info(lines, covers)
702
- marked_info = []
703
- count_info = []
704
- lines.size.times do |i|
705
- c = covers[i]
706
- marked_info << ((c && c > 0) ? true : false)
707
- count_info << (c || 0)
708
- end
709
-
710
- script_lines_workaround(lines, marked_info, count_info)
711
- end
712
-
713
- # Try to detect repeated data, based on observed repetitions in line_info:
714
- # this is a workaround for SCRIPT_LINES__[filename] including as many copies
715
- # of the file as the number of times it was parsed.
716
- def script_lines_workaround(line_info, coverage_info, count_info)
717
- is_repeated = lambda do |div|
718
- n = line_info.size / div
719
- break false unless line_info.size % div == 0 && n > 1
720
- different = false
721
- n.times do |i|
722
-
723
- things = (0...div).map { |j| line_info[i + j * n] }
724
- if things.uniq.size != 1
725
- different = true
726
- break
727
- end
728
- end
729
-
730
- ! different
731
- end
732
-
733
- factors = braindead_factorize(line_info.size)
734
- factors.each do |n|
735
- if is_repeated[n]
736
- line_info = line_info[0, line_info.size / n]
737
- coverage_info = coverage_info[0, coverage_info.size / n]
738
- count_info = count_info[0, count_info.size / n]
739
- end
740
- end if factors.size > 1 # don't even try if it's prime
741
-
742
- [line_info, coverage_info, count_info]
743
- end
744
-
745
- def braindead_factorize(num)
746
- return [0] if num == 0
747
- return [-1] + braindead_factorize(-num) if num < 0
748
- factors = []
749
- while num % 2 == 0
750
- factors << 2
751
- num /= 2
752
- end
753
- size = num
754
- n = 3
755
- max = Math.sqrt(num)
756
- while n <= max && n <= size
757
- while size % n == 0
758
- size /= n
759
- factors << n
760
- end
761
- n += 2
762
- end
763
- factors << size if size != 1
764
- factors
765
- end
766
-
767
- def update_script_lines__
768
- @script_lines__ = @script_lines__.merge(SCRIPT_LINES__)
769
- end
770
-
771
- public
772
- def marshal_dump # :nodoc:
773
- # @script_lines__ is updated just before serialization so as to avoid
774
- # missing files in SCRIPT_LINES__
775
- ivs = {}
776
- update_script_lines__
777
- instance_variables.each{|iv| ivs[iv] = instance_variable_get(iv)}
778
- ivs
779
- end
780
-
781
- def marshal_load(ivs) # :nodoc:
782
- ivs.each_pair{|iv, val| instance_variable_set(iv, val)}
783
- end
784
-
785
- end # CodeCoverageAnalyzer
786
-
787
- # A CallSiteAnalyzer can be used to obtain information about:
788
- # * where a method is defined ("+defsite+")
789
- # * where a method was called from ("+callsite+")
790
- #
791
- # == Example
792
- # <tt>example.rb</tt>:
793
- # class X
794
- # def f1; f2 end
795
- # def f2; 1 + 1 end
796
- # def f3; f1 end
797
- # end
798
- #
799
- # analyzer = Rcov::CallSiteAnalyzer.new
800
- # x = X.new
801
- # analyzer.run_hooked do
802
- # x.f1
803
- # end
804
- # # ....
805
- #
806
- # analyzer.run_hooked do
807
- # x.f3
808
- # # the information generated in this run is aggregated
809
- # # to the previously recorded one
810
- # end
811
- #
812
- # analyzer.analyzed_classes # => ["X", ... ]
813
- # analyzer.methods_for_class("X") # => ["f1", "f2", "f3"]
814
- # analyzer.defsite("X#f1") # => DefSite object
815
- # analyzer.callsites("X#f2") # => hash with CallSite => count
816
- # # associations
817
- # defsite = analyzer.defsite("X#f1")
818
- # defsite.file # => "example.rb"
819
- # defsite.line # => 2
820
- #
821
- # You can have several CallSiteAnalyzer objects at a time, and it is
822
- # possible to nest the #run_hooked / #install_hook/#remove_hook blocks: each
823
- # analyzer will manage its data separately. Note however that no special
824
- # provision is taken to ignore code executed "inside" the CallSiteAnalyzer
825
- # class.
826
- #
827
- # +defsite+ information is only available for methods that were called under
828
- # the inspection of the CallSiteAnalyzer, i.o.w. you will only have +defsite+
829
- # information for those methods for which callsite information is
830
- # available.
831
- class CallSiteAnalyzer < DifferentialAnalyzer
832
- # A method definition site.
833
- class DefSite < Struct.new(:file, :line)
834
- end
835
-
836
- # Object representing a method call site.
837
- # It corresponds to a part of the callstack starting from the context that
838
- # called the method.
839
- class CallSite < Struct.new(:backtrace)
840
- # The depth of a CallSite is the number of stack frames
841
- # whose information is included in the CallSite object.
842
- def depth
843
- backtrace.size
844
- end
845
-
846
- # File where the method call originated.
847
- # Might return +nil+ or "" if it is not meaningful (C extensions, etc).
848
- def file(level = 0)
849
- stack_frame = backtrace[level]
850
- stack_frame ? stack_frame[2] : nil
851
- end
852
-
853
- # Line where the method call originated.
854
- # Might return +nil+ or 0 if it is not meaningful (C extensions, etc).
855
- def line(level = 0)
856
- stack_frame = backtrace[level]
857
- stack_frame ? stack_frame[3] : nil
858
- end
859
-
860
- # Name of the method where the call originated.
861
- # Returns +nil+ if the call originated in +toplevel+.
862
- # Might return +nil+ if it could not be determined.
863
- def calling_method(level = 0)
864
- stack_frame = backtrace[level]
865
- stack_frame ? stack_frame[1] : nil
866
- end
867
-
868
- # Name of the class holding the method where the call originated.
869
- # Might return +nil+ if it could not be determined.
870
- def calling_class(level = 0)
871
- stack_frame = backtrace[level]
872
- stack_frame ? stack_frame[0] : nil
873
- end
874
- end
875
-
876
- @hook_level = 0
877
- # defined this way instead of attr_accessor so that it's covered
878
- def self.hook_level # :nodoc:
879
- @hook_level
880
- end
881
- def self.hook_level=(x) # :nodoc:
882
- @hook_level = x
883
- end
884
-
885
- def initialize
886
- super(:install_callsite_hook, :remove_callsite_hook,
887
- :reset_callsite)
888
- end
889
-
890
- # Classes whose methods have been called.
891
- # Returns an array of strings describing the classes (just klass.to_s for
892
- # each of them). Singleton classes are rendered as:
893
- # #<Class:MyNamespace::MyClass>
894
- def analyzed_classes
895
- raw_data_relative.first.keys.map{|klass, meth| klass}.uniq.sort
896
- end
897
-
898
- # Methods that were called for the given class. See #analyzed_classes for
899
- # the notation used for singleton classes.
900
- # Returns an array of strings or +nil+
901
- def methods_for_class(classname)
902
- a = raw_data_relative.first.keys.select{|kl,_| kl == classname}.map{|_,meth| meth}.sort
903
- a.empty? ? nil : a
904
- end
905
- alias_method :analyzed_methods, :methods_for_class
906
-
907
- # Returns a hash with <tt>CallSite => call count</tt> associations or +nil+
908
- # Can be called in two ways:
909
- # analyzer.callsites("Foo#f1") # instance method
910
- # analyzer.callsites("Foo.g1") # singleton method of the class
911
- # or
912
- # analyzer.callsites("Foo", "f1")
913
- # analyzer.callsites("#<class:Foo>", "g1")
914
- def callsites(classname_or_fullname, methodname = nil)
915
- rawsites = raw_data_relative.first[expand_name(classname_or_fullname, methodname)]
916
- return nil unless rawsites
917
- ret = {}
918
- # could be a job for inject but it's slow and I don't mind the extra loc
919
- rawsites.each_pair do |backtrace, count|
920
- ret[CallSite.new(backtrace)] = count
921
- end
922
- ret
923
- end
924
-
925
- # Returns a DefSite object corresponding to the given method
926
- # Can be called in two ways:
927
- # analyzer.defsite("Foo#f1") # instance method
928
- # analyzer.defsite("Foo.g1") # singleton method of the class
929
- # or
930
- # analyzer.defsite("Foo", "f1")
931
- # analyzer.defsite("#<class:Foo>", "g1")
932
- def defsite(classname_or_fullname, methodname = nil)
933
- file, line = raw_data_relative[1][expand_name(classname_or_fullname, methodname)]
934
- return nil unless file && line
935
- DefSite.new(file, line)
936
- end
937
-
938
- private
939
-
940
- def expand_name(classname_or_fullname, methodname = nil)
941
- if methodname.nil?
942
- case classname_or_fullname
943
- when /(.*)#(.*)/ then classname, methodname = $1, $2
944
- when /(.*)\.(.*)/ then classname, methodname = "#<Class:#{$1}>", $2
945
- else
946
- raise ArgumentError, "Incorrect method name"
947
- end
948
-
949
- return [classname, methodname]
950
- end
951
-
952
- [classname_or_fullname, methodname]
953
- end
954
-
955
- def data_default; [{}, {}] end
956
-
957
- def raw_data_absolute
958
- raw, method_def_site = RCOV__.generate_callsite_info
959
- ret1 = {}
960
- ret2 = {}
961
- raw.each_pair do |(klass, method), hash|
962
- begin
963
- key = [klass.to_s, method.to_s]
964
- ret1[key] = hash.clone #Marshal.load(Marshal.dump(hash))
965
- ret2[key] = method_def_site[[klass, method]]
966
- #rescue Exception
967
- end
968
- end
969
-
970
- [ret1, ret2]
971
- end
972
-
973
- def aggregate_data(aggregated_data, delta)
974
- callsites1, defsites1 = aggregated_data
975
- callsites2, defsites2 = delta
976
-
977
- callsites2.each_pair do |(klass, method), hash|
978
- dest_hash = (callsites1[[klass, method]] ||= {})
979
- hash.each_pair do |callsite, count|
980
- dest_hash[callsite] ||= 0
981
- dest_hash[callsite] += count
982
- end
983
- end
984
-
985
- defsites1.update(defsites2)
986
- end
987
-
988
- def compute_raw_data_difference(first, last)
989
- difference = {}
990
- default = Hash.new(0)
991
-
992
- callsites1, defsites1 = *first
993
- callsites2, defsites2 = *last
994
-
995
- callsites2.each_pair do |(klass, method), hash|
996
- old_hash = callsites1[[klass, method]] || default
997
- hash.each_pair do |callsite, count|
998
- diff = hash[callsite] - (old_hash[callsite] || 0)
999
- if diff > 0
1000
- difference[[klass, method]] ||= {}
1001
- difference[[klass, method]][callsite] = diff
1002
- end
1003
- end
1004
- end
1005
-
1006
- [difference, defsites1.update(defsites2)]
1007
- end
1008
-
1009
- end
1010
-
32
+ autoload :RCOV__, "rcov/lowlevel.rb"
1011
33
  end