valo-rcov 0.8.3.4

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