relevance-rcov 0.8.5.2 → 0.8.6

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.
@@ -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