relevance-rcov 0.8.5.2 → 0.8.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,225 @@
1
+ module Rcov
2
+ # A CallSiteAnalyzer can be used to obtain information about:
3
+ # * where a method is defined ("+defsite+")
4
+ # * where a method was called from ("+callsite+")
5
+ #
6
+ # == Example
7
+ # <tt>example.rb</tt>:
8
+ # class X
9
+ # def f1; f2 end
10
+ # def f2; 1 + 1 end
11
+ # def f3; f1 end
12
+ # end
13
+ #
14
+ # analyzer = Rcov::CallSiteAnalyzer.new
15
+ # x = X.new
16
+ # analyzer.run_hooked do
17
+ # x.f1
18
+ # end
19
+ # # ....
20
+ #
21
+ # analyzer.run_hooked do
22
+ # x.f3
23
+ # # the information generated in this run is aggregated
24
+ # # to the previously recorded one
25
+ # end
26
+ #
27
+ # analyzer.analyzed_classes # => ["X", ... ]
28
+ # analyzer.methods_for_class("X") # => ["f1", "f2", "f3"]
29
+ # analyzer.defsite("X#f1") # => DefSite object
30
+ # analyzer.callsites("X#f2") # => hash with CallSite => count
31
+ # # associations
32
+ # defsite = analyzer.defsite("X#f1")
33
+ # defsite.file # => "example.rb"
34
+ # defsite.line # => 2
35
+ #
36
+ # You can have several CallSiteAnalyzer objects at a time, and it is
37
+ # possible to nest the #run_hooked / #install_hook/#remove_hook blocks: each
38
+ # analyzer will manage its data separately. Note however that no special
39
+ # provision is taken to ignore code executed "inside" the CallSiteAnalyzer
40
+ # class.
41
+ #
42
+ # +defsite+ information is only available for methods that were called under
43
+ # the inspection of the CallSiteAnalyzer, i.o.w. you will only have +defsite+
44
+ # information for those methods for which callsite information is
45
+ # available.
46
+ class CallSiteAnalyzer < DifferentialAnalyzer
47
+ # A method definition site.
48
+ class DefSite < Struct.new(:file, :line)
49
+ end
50
+
51
+ # Object representing a method call site.
52
+ # It corresponds to a part of the callstack starting from the context that
53
+ # called the method.
54
+ class CallSite < Struct.new(:backtrace)
55
+ # The depth of a CallSite is the number of stack frames
56
+ # whose information is included in the CallSite object.
57
+ def depth
58
+ backtrace.size
59
+ end
60
+
61
+ # File where the method call originated.
62
+ # Might return +nil+ or "" if it is not meaningful (C extensions, etc).
63
+ def file(level = 0)
64
+ stack_frame = backtrace[level]
65
+ stack_frame ? stack_frame[2] : nil
66
+ end
67
+
68
+ # Line where the method call originated.
69
+ # Might return +nil+ or 0 if it is not meaningful (C extensions, etc).
70
+ def line(level = 0)
71
+ stack_frame = backtrace[level]
72
+ stack_frame ? stack_frame[3] : nil
73
+ end
74
+
75
+ # Name of the method where the call originated.
76
+ # Returns +nil+ if the call originated in +toplevel+.
77
+ # Might return +nil+ if it could not be determined.
78
+ def calling_method(level = 0)
79
+ stack_frame = backtrace[level]
80
+ stack_frame ? stack_frame[1] : nil
81
+ end
82
+
83
+ # Name of the class holding the method where the call originated.
84
+ # Might return +nil+ if it could not be determined.
85
+ def calling_class(level = 0)
86
+ stack_frame = backtrace[level]
87
+ stack_frame ? stack_frame[0] : nil
88
+ end
89
+ end
90
+
91
+ @hook_level = 0
92
+ # defined this way instead of attr_accessor so that it's covered
93
+ def self.hook_level # :nodoc:
94
+ @hook_level
95
+ end
96
+
97
+ def self.hook_level=(x) # :nodoc:
98
+ @hook_level = x
99
+ end
100
+
101
+ def initialize
102
+ super(:install_callsite_hook, :remove_callsite_hook,
103
+ :reset_callsite)
104
+ end
105
+
106
+ # Classes whose methods have been called.
107
+ # Returns an array of strings describing the classes (just klass.to_s for
108
+ # each of them). Singleton classes are rendered as:
109
+ # #<Class:MyNamespace::MyClass>
110
+ def analyzed_classes
111
+ raw_data_relative.first.keys.map{|klass, meth| klass}.uniq.sort
112
+ end
113
+
114
+ # Methods that were called for the given class. See #analyzed_classes for
115
+ # the notation used for singleton classes.
116
+ # Returns an array of strings or +nil+
117
+ def methods_for_class(classname)
118
+ a = raw_data_relative.first.keys.select{|kl,_| kl == classname}.map{|_,meth| meth}.sort
119
+ a.empty? ? nil : a
120
+ end
121
+ alias_method :analyzed_methods, :methods_for_class
122
+
123
+ # Returns a hash with <tt>CallSite => call count</tt> associations or +nil+
124
+ # Can be called in two ways:
125
+ # analyzer.callsites("Foo#f1") # instance method
126
+ # analyzer.callsites("Foo.g1") # singleton method of the class
127
+ # or
128
+ # analyzer.callsites("Foo", "f1")
129
+ # analyzer.callsites("#<class:Foo>", "g1")
130
+ def callsites(classname_or_fullname, methodname = nil)
131
+ rawsites = raw_data_relative.first[expand_name(classname_or_fullname, methodname)]
132
+ return nil unless rawsites
133
+ ret = {}
134
+ # could be a job for inject but it's slow and I don't mind the extra loc
135
+ rawsites.each_pair do |backtrace, count|
136
+ ret[CallSite.new(backtrace)] = count
137
+ end
138
+ ret
139
+ end
140
+
141
+ # Returns a DefSite object corresponding to the given method
142
+ # Can be called in two ways:
143
+ # analyzer.defsite("Foo#f1") # instance method
144
+ # analyzer.defsite("Foo.g1") # singleton method of the class
145
+ # or
146
+ # analyzer.defsite("Foo", "f1")
147
+ # analyzer.defsite("#<class:Foo>", "g1")
148
+ def defsite(classname_or_fullname, methodname = nil)
149
+ file, line = raw_data_relative[1][expand_name(classname_or_fullname, methodname)]
150
+ return nil unless file && line
151
+ DefSite.new(file, line)
152
+ end
153
+
154
+ private
155
+
156
+ def expand_name(classname_or_fullname, methodname = nil)
157
+ if methodname.nil?
158
+ case classname_or_fullname
159
+ when /(.*)#(.*)/ then classname, methodname = $1, $2
160
+ when /(.*)\.(.*)/ then classname, methodname = "#<Class:#{$1}>", $2
161
+ else
162
+ raise ArgumentError, "Incorrect method name"
163
+ end
164
+
165
+ return [classname, methodname]
166
+ end
167
+
168
+ [classname_or_fullname, methodname]
169
+ end
170
+
171
+ def data_default; [{}, {}] end
172
+
173
+ def raw_data_absolute
174
+ raw, method_def_site = RCOV__.generate_callsite_info
175
+ ret1 = {}
176
+ ret2 = {}
177
+ raw.each_pair do |(klass, method), hash|
178
+ begin
179
+ key = [klass.to_s, method.to_s]
180
+ ret1[key] = hash.clone #Marshal.load(Marshal.dump(hash))
181
+ ret2[key] = method_def_site[[klass, method]]
182
+ #rescue Exception
183
+ end
184
+ end
185
+
186
+ [ret1, ret2]
187
+ end
188
+
189
+ def aggregate_data(aggregated_data, delta)
190
+ callsites1, defsites1 = aggregated_data
191
+ callsites2, defsites2 = delta
192
+
193
+ callsites2.each_pair do |(klass, method), hash|
194
+ dest_hash = (callsites1[[klass, method]] ||= {})
195
+ hash.each_pair do |callsite, count|
196
+ dest_hash[callsite] ||= 0
197
+ dest_hash[callsite] += count
198
+ end
199
+ end
200
+
201
+ defsites1.update(defsites2)
202
+ end
203
+
204
+ def compute_raw_data_difference(first, last)
205
+ difference = {}
206
+ default = Hash.new(0)
207
+
208
+ callsites1, defsites1 = *first
209
+ callsites2, defsites2 = *last
210
+
211
+ callsites2.each_pair do |(klass, method), hash|
212
+ old_hash = callsites1[[klass, method]] || default
213
+ hash.each_pair do |callsite, count|
214
+ diff = hash[callsite] - (old_hash[callsite] || 0)
215
+ if diff > 0
216
+ difference[[klass, method]] ||= {}
217
+ difference[[klass, method]][callsite] = diff
218
+ end
219
+ end
220
+ end
221
+
222
+ [difference, defsites1.update(defsites2)]
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,268 @@
1
+ module Rcov
2
+ # A CodeCoverageAnalyzer is responsible for tracing code execution and
3
+ # returning code coverage and execution count information.
4
+ #
5
+ # Note that you must <tt>require 'rcov'</tt> before the code you want to
6
+ # analyze is parsed (i.e. before it gets loaded or required). You can do that
7
+ # by either invoking ruby with the <tt>-rrcov</tt> command-line option or
8
+ # just:
9
+ # require 'rcov'
10
+ # require 'mycode'
11
+ # # ....
12
+ #
13
+ # == Example
14
+ #
15
+ # analyzer = Rcov::CodeCoverageAnalyzer.new
16
+ # analyzer.run_hooked do
17
+ # do_foo
18
+ # # all the code executed as a result of this method call is traced
19
+ # end
20
+ # # ....
21
+ #
22
+ # analyzer.run_hooked do
23
+ # do_bar
24
+ # # the code coverage information generated in this run is aggregated
25
+ # # to the previously recorded one
26
+ # end
27
+ #
28
+ # analyzer.analyzed_files # => ["foo.rb", "bar.rb", ... ]
29
+ # lines, marked_info, count_info = analyzer.data("foo.rb")
30
+ #
31
+ # In this example, two pieces of code are monitored, and the data generated in
32
+ # both runs are aggregated. +lines+ is an array of strings representing the
33
+ # source code of <tt>foo.rb</tt>. +marked_info+ is an array holding false,
34
+ # true values indicating whether the corresponding lines of code were reported
35
+ # as executed by Ruby. +count_info+ is an array of integers representing how
36
+ # many times each line of code has been executed (more precisely, how many
37
+ # events where reported by Ruby --- a single line might correspond to several
38
+ # events, e.g. many method calls).
39
+ #
40
+ # You can have several CodeCoverageAnalyzer objects at a time, and it is
41
+ # possible to nest the #run_hooked / #install_hook/#remove_hook blocks: each
42
+ # analyzer will manage its data separately. Note however that no special
43
+ # provision is taken to ignore code executed "inside" the CodeCoverageAnalyzer
44
+ # class. At any rate this will not pose a problem since it's easy to ignore it
45
+ # manually: just don't do
46
+ # lines, coverage, counts = analyzer.data("/path/to/lib/rcov.rb")
47
+ # if you're not interested in that information.
48
+ class CodeCoverageAnalyzer < DifferentialAnalyzer
49
+ @hook_level = 0
50
+ # defined this way instead of attr_accessor so that it's covered
51
+ def self.hook_level # :nodoc:
52
+ @hook_level
53
+ end
54
+
55
+ def self.hook_level=(x) # :nodoc:
56
+ @hook_level = x
57
+ end
58
+
59
+ def initialize
60
+ @script_lines__ = SCRIPT_LINES__
61
+ super(:install_coverage_hook, :remove_coverage_hook,
62
+ :reset_coverage)
63
+ end
64
+
65
+ # Return an array with the names of the files whose code was executed inside
66
+ # the block given to #run_hooked or between #install_hook and #remove_hook.
67
+ def analyzed_files
68
+ update_script_lines__
69
+ raw_data_relative.select do |file, lines|
70
+ @script_lines__.has_key?(file)
71
+ end.map{|fname,| fname}
72
+ end
73
+
74
+ # Return the available data about the requested file, or nil if none of its
75
+ # code was executed or it cannot be found.
76
+ # The return value is an array with three elements:
77
+ # lines, marked_info, count_info = analyzer.data("foo.rb")
78
+ # +lines+ is an array of strings representing the
79
+ # source code of <tt>foo.rb</tt>. +marked_info+ is an array holding false,
80
+ # true values indicating whether the corresponding lines of code were reported
81
+ # as executed by Ruby. +count_info+ is an array of integers representing how
82
+ # many times each line of code has been executed (more precisely, how many
83
+ # events where reported by Ruby --- a single line might correspond to several
84
+ # events, e.g. many method calls).
85
+ #
86
+ # The returned data corresponds to the aggregation of all the statistics
87
+ # collected in each #run_hooked or #install_hook/#remove_hook runs. You can
88
+ # reset the data at any time with #reset to start from scratch.
89
+ def data(filename)
90
+ raw_data = raw_data_relative
91
+ update_script_lines__
92
+ unless @script_lines__.has_key?(filename) &&
93
+ raw_data.has_key?(filename)
94
+ return nil
95
+ end
96
+ refine_coverage_info(@script_lines__[filename], raw_data[filename])
97
+ end
98
+
99
+ # Data for the first file matching the given regexp.
100
+ # See #data.
101
+ def data_matching(filename_re)
102
+ raw_data = raw_data_relative
103
+ update_script_lines__
104
+
105
+ match = raw_data.keys.sort.grep(filename_re).first
106
+ return nil unless match
107
+
108
+ refine_coverage_info(@script_lines__[match], raw_data[match])
109
+ end
110
+
111
+ # Execute the code in the given block, monitoring it in order to gather
112
+ # information about which code was executed.
113
+ def run_hooked; super end
114
+
115
+ # Start monitoring execution to gather code coverage and execution count
116
+ # information. Such data will be collected until #remove_hook is called.
117
+ #
118
+ # Use #run_hooked instead if possible.
119
+ def install_hook; super end
120
+
121
+ # Stop collecting code coverage and execution count information.
122
+ # #remove_hook will also stop collecting info if it is run inside a
123
+ # #run_hooked block.
124
+ def remove_hook; super end
125
+
126
+ # Remove the data collected so far. The coverage and execution count
127
+ # "history" will be erased, and further collection will start from scratch:
128
+ # no code is considered executed, and therefore all execution counts are 0.
129
+ # Right after #reset, #analyzed_files will return an empty array, and
130
+ # #data(filename) will return nil.
131
+ def reset; super end
132
+
133
+ def dump_coverage_info(formatters) # :nodoc:
134
+ update_script_lines__
135
+ raw_data_relative.each do |file, lines|
136
+ next if @script_lines__.has_key?(file) == false
137
+ lines = @script_lines__[file]
138
+ raw_coverage_array = raw_data_relative[file]
139
+
140
+ line_info, marked_info,
141
+ count_info = refine_coverage_info(lines, raw_coverage_array)
142
+ formatters.each do |formatter|
143
+ formatter.add_file(file, line_info, marked_info, count_info)
144
+ end
145
+ end
146
+ formatters.each{|formatter| formatter.execute}
147
+ end
148
+
149
+ private
150
+
151
+ def data_default; {} end
152
+
153
+ def raw_data_absolute
154
+ Rcov::RCOV__.generate_coverage_info
155
+ end
156
+
157
+ def aggregate_data(aggregated_data, delta)
158
+ delta.each_pair do |file, cov_arr|
159
+ dest = (aggregated_data[file] ||= Array.new(cov_arr.size, 0))
160
+ cov_arr.each_with_index{|x,i| dest[i] += x}
161
+ end
162
+ end
163
+
164
+ def compute_raw_data_difference(first, last)
165
+ difference = {}
166
+ last.each_pair do |fname, cov_arr|
167
+ unless first.has_key?(fname)
168
+ difference[fname] = cov_arr.clone
169
+ else
170
+ orig_arr = first[fname]
171
+ diff_arr = Array.new(cov_arr.size, 0)
172
+ changed = false
173
+ cov_arr.each_with_index do |x, i|
174
+ diff_arr[i] = diff = (x || 0) - (orig_arr[i] || 0)
175
+ changed = true if diff != 0
176
+ end
177
+ difference[fname] = diff_arr if changed
178
+ end
179
+ end
180
+ difference
181
+ end
182
+
183
+ def refine_coverage_info(lines, covers)
184
+ marked_info = []
185
+ count_info = []
186
+ lines.size.times do |i|
187
+ c = covers[i]
188
+ marked_info << ((c && c > 0) ? true : false)
189
+ count_info << (c || 0)
190
+ end
191
+
192
+ script_lines_workaround(lines, marked_info, count_info)
193
+ end
194
+
195
+ # Try to detect repeated data, based on observed repetitions in line_info:
196
+ # this is a workaround for SCRIPT_LINES__[filename] including as many copies
197
+ # of the file as the number of times it was parsed.
198
+ def script_lines_workaround(line_info, coverage_info, count_info)
199
+ is_repeated = lambda do |div|
200
+ n = line_info.size / div
201
+ break false unless line_info.size % div == 0 && n > 1
202
+ different = false
203
+ n.times do |i|
204
+
205
+ things = (0...div).map { |j| line_info[i + j * n] }
206
+ if things.uniq.size != 1
207
+ different = true
208
+ break
209
+ end
210
+ end
211
+
212
+ ! different
213
+ end
214
+
215
+ factors = braindead_factorize(line_info.size)
216
+ factors.each do |n|
217
+ if is_repeated[n]
218
+ line_info = line_info[0, line_info.size / n]
219
+ coverage_info = coverage_info[0, coverage_info.size / n]
220
+ count_info = count_info[0, count_info.size / n]
221
+ end
222
+ end if factors.size > 1 # don't even try if it's prime
223
+
224
+ [line_info, coverage_info, count_info]
225
+ end
226
+
227
+ def braindead_factorize(num)
228
+ return [0] if num == 0
229
+ return [-1] + braindead_factorize(-num) if num < 0
230
+ factors = []
231
+ while num % 2 == 0
232
+ factors << 2
233
+ num /= 2
234
+ end
235
+ size = num
236
+ n = 3
237
+ max = Math.sqrt(num)
238
+ while n <= max && n <= size
239
+ while size % n == 0
240
+ size /= n
241
+ factors << n
242
+ end
243
+ n += 2
244
+ end
245
+ factors << size if size != 1
246
+ factors
247
+ end
248
+
249
+ def update_script_lines__
250
+ @script_lines__ = @script_lines__.merge(SCRIPT_LINES__)
251
+ end
252
+
253
+ public
254
+
255
+ def marshal_dump # :nodoc:
256
+ # @script_lines__ is updated just before serialization so as to avoid
257
+ # missing files in SCRIPT_LINES__
258
+ ivs = {}
259
+ update_script_lines__
260
+ instance_variables.each{|iv| ivs[iv] = instance_variable_get(iv)}
261
+ ivs
262
+ end
263
+
264
+ def marshal_load(ivs) # :nodoc:
265
+ ivs.each_pair{|iv, val| instance_variable_set(iv, val)}
266
+ end
267
+ end # CodeCoverageAnalyzer
268
+ end