spicycode-rcov 0.8.1.3.0

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 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: