relevance-rcov 0.8.2.1

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