spicycode-rcov 0.8.1.3.0

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