rcov 0.5.0.1-mswin32

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