Name | Total Lines | Lines of Code | Total Coverage | Code Coverage |
---|---|---|---|---|
rcov/ruby/1.8/gems/rcov-0.9.9/lib/rcov/code_coverage_analyzer.rb | 271 | 156 | 7.01%
|
5.13%
|
Code reported as executed by Ruby looks like this...and this: this line is also marked as covered.Lines considered as run by rcov, but not reported by Ruby, look like this,and this: these lines were inferred by rcov (using simple heuristics).Finally, here's a line marked as not executed.
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 do |x,i| |
161 dest[i] ||= 0 |
162 dest[i] += x.to_i |
163 end |
164 end |
165 end |
166 |
167 def compute_raw_data_difference(first, last) |
168 difference = {} |
169 last.each_pair do |fname, cov_arr| |
170 unless first.has_key?(fname) |
171 difference[fname] = cov_arr.clone |
172 else |
173 orig_arr = first[fname] |
174 diff_arr = Array.new(cov_arr.size, 0) |
175 changed = false |
176 cov_arr.each_with_index do |x, i| |
177 diff_arr[i] = diff = (x || 0) - (orig_arr[i] || 0) |
178 changed = true if diff != 0 |
179 end |
180 difference[fname] = diff_arr if changed |
181 end |
182 end |
183 difference |
184 end |
185 |
186 def refine_coverage_info(lines, covers) |
187 marked_info = [] |
188 count_info = [] |
189 lines.size.times do |i| |
190 c = covers[i] |
191 marked_info << ((c && c > 0) ? true : false) |
192 count_info << (c || 0) |
193 end |
194 |
195 script_lines_workaround(lines, marked_info, count_info) |
196 end |
197 |
198 # Try to detect repeated data, based on observed repetitions in line_info: |
199 # this is a workaround for SCRIPT_LINES__[filename] including as many copies |
200 # of the file as the number of times it was parsed. |
201 def script_lines_workaround(line_info, coverage_info, count_info) |
202 is_repeated = lambda do |div| |
203 n = line_info.size / div |
204 break false unless line_info.size % div == 0 && n > 1 |
205 different = false |
206 n.times do |i| |
207 |
208 things = (0...div).map { |j| line_info[i + j * n] } |
209 if things.uniq.size != 1 |
210 different = true |
211 break |
212 end |
213 end |
214 |
215 ! different |
216 end |
217 |
218 factors = braindead_factorize(line_info.size) |
219 factors.each do |n| |
220 if is_repeated[n] |
221 line_info = line_info[0, line_info.size / n] |
222 coverage_info = coverage_info[0, coverage_info.size / n] |
223 count_info = count_info[0, count_info.size / n] |
224 end |
225 end if factors.size > 1 # don't even try if it's prime |
226 |
227 [line_info, coverage_info, count_info] |
228 end |
229 |
230 def braindead_factorize(num) |
231 return [0] if num == 0 |
232 return [-1] + braindead_factorize(-num) if num < 0 |
233 factors = [] |
234 while num % 2 == 0 |
235 factors << 2 |
236 num /= 2 |
237 end |
238 size = num |
239 n = 3 |
240 max = Math.sqrt(num) |
241 while n <= max && n <= size |
242 while size % n == 0 |
243 size /= n |
244 factors << n |
245 end |
246 n += 2 |
247 end |
248 factors << size if size != 1 |
249 factors |
250 end |
251 |
252 def update_script_lines__ |
253 @script_lines__ = @script_lines__.merge(SCRIPT_LINES__) |
254 end |
255 |
256 public |
257 |
258 def marshal_dump # :nodoc: |
259 # @script_lines__ is updated just before serialization so as to avoid |
260 # missing files in SCRIPT_LINES__ |
261 ivs = {} |
262 update_script_lines__ |
263 instance_variables.each{|iv| ivs[iv] = instance_variable_get(iv)} |
264 ivs |
265 end |
266 |
267 def marshal_load(ivs) # :nodoc: |
268 ivs.each_pair{|iv, val| instance_variable_set(iv, val)} |
269 end |
270 end # CodeCoverageAnalyzer |
271 end |
Generated on Fri Apr 22 17:22:41 -0700 2011 with rcov 0.9.8