Parent

Rcov::FileStatistics

A FileStatistics object associates a filename to:

  1. its source code

  2. the per-line coverage information after correction using rcov’s heuristics

  3. the per-line execution counts

A FileStatistics object can be therefore be built given the filename, the associated source code, and an array holding execution counts (i.e. how many times each line has been executed).

FileStatistics is relatively intelligent: it handles normal comments, =begin/=end, heredocs, many multiline-expressions... It uses a number of heuristics to determine what is code and what is a comment, and to refine the initial (incomplete) coverage information.

Basic usage is as follows:

 sf = FileStatistics.new("foo.rb", ["puts 1", "if true &&", "   false", 
                                "puts 2", "end"],  [1, 1, 0, 0, 0])
 sf.num_lines        # => 5
 sf.num_code_lines   # => 5
 sf.coverage[2]      # => true
 sf.coverage[3]      # => :inferred
 sf.code_coverage    # => 0.6
                   

The array of strings representing the source code and the array of execution counts would normally be obtained from a Rcov::CodeCoverageAnalyzer.

Attributes

name[R]
lines[R]
coverage[R]
counts[R]

Public Class Methods

new(name, lines, counts, comments_run_by_default = false) click to toggle source
    # File lib/rcov/file_statistics.rb, line 29
29:     def initialize(name, lines, counts, comments_run_by_default = false)
30:       @name = name
31:       @lines = lines
32:       initial_coverage = counts.map{|x| (x || 0) > 0 ? true : false }
33:       @coverage = CoverageInfo.new initial_coverage
34:       @counts = counts
35:       @is_begin_comment = nil
36:       # points to the line defining the heredoc identifier
37:       # but only if it was marked (we don't care otherwise)
38:       @heredoc_start = Array.new(lines.size, false)
39:       @multiline_string_start = Array.new(lines.size, false)
40:       extend_heredocs
41:       find_multiline_strings
42:       precompute_coverage comments_run_by_default
43:     end

Public Instance Methods

code_coverage() click to toggle source

Code coverage rate: fraction of lines of code executed, relative to the total amount of lines of code (loc). Returns a float from 0 to 1.0.

    # File lib/rcov/file_statistics.rb, line 79
79:     def code_coverage
80:       indices = (0...@lines.size).select{|i| is_code? i }
81:       return 0 if indices.size == 0
82:       count = 0
83:       indices.each {|i| count += 1 if @coverage[i] }
84:       1.0 * count / indices.size
85:     end
code_coverage_for_report() click to toggle source
    # File lib/rcov/file_statistics.rb, line 87
87:     def code_coverage_for_report
88:       code_coverage * 100
89:     end
is_code?(lineno) click to toggle source

Returns true if the given line number corresponds to code, as opposed to a comment (either # or =begin/=end blocks).

     # File lib/rcov/file_statistics.rb, line 107
107:     def is_code?(lineno)
108:       unless @is_begin_comment
109:         @is_begin_comment = Array.new(@lines.size, false)
110:         pending = []
111:         state = :code
112:         @lines.each_with_index do |line, index|
113:           case state
114:           when :code
115:             if /^=begin\b/ =~ line
116:               state = :comment
117:               pending << index
118:             end
119:           when :comment
120:             pending << index
121:             if /^=end\b/ =~ line
122:               state = :code
123:               pending.each{|idx| @is_begin_comment[idx] = true}
124:               pending.clear
125:             end
126:           end
127:         end
128:       end
129:       @lines[lineno] && !@is_begin_comment[lineno] && @lines[lineno] !~ /^\s*(#|$)/ 
130:     end
merge(lines, coverage, counts) click to toggle source

Merge code coverage and execution count information. As for code coverage, a line will be considered

  • covered for sure (true) if it is covered in either self or in the coverage array

  • considered :inferred if the neither self nor the coverage array indicate that it was definitely executed, but it was inferred in either one

  • not covered (false) if it was uncovered in both

Execution counts are just summated on a per-line basis.

    # File lib/rcov/file_statistics.rb, line 55
55:     def merge(lines, coverage, counts)
56:       coverage.each_with_index do |v, idx|
57:         case @coverage[idx]
58:         when :inferred 
59:           @coverage[idx] = v || @coverage[idx]
60:         when false 
61:           @coverage[idx] ||= v
62:         end
63:       end
64:       counts.each_with_index{|v, idx| @counts[idx] += v }
65:       precompute_coverage false
66:     end
num_code_lines() click to toggle source

Number of lines of code (loc).

    # File lib/rcov/file_statistics.rb, line 96
96:     def num_code_lines
97:       (0...@lines.size).select{|i| is_code? i}.size
98:     end
num_lines() click to toggle source

Total number of lines.

     # File lib/rcov/file_statistics.rb, line 101
101:     def num_lines
102:       @lines.size
103:     end
total_coverage() click to toggle source

Total coverage rate if comments are also considered “executable”, given as a fraction, i.e. from 0 to 1.0. A comment is attached to the code following it (RDoc-style): it will be considered executed if the the next statement was executed.

    # File lib/rcov/file_statistics.rb, line 72
72:     def total_coverage
73:       return 0 if @coverage.size == 0
74:       @coverage.inject(0.0) {|s,a| s + (a ? 1:0) } / @coverage.size
75:     end
total_coverage_for_report() click to toggle source
    # File lib/rcov/file_statistics.rb, line 91
91:     def total_coverage_for_report
92:       total_coverage * 100
93:     end

Private Instance Methods

extend_heredocs() click to toggle source
     # File lib/rcov/file_statistics.rb, line 231
231:     def extend_heredocs
232:       i = 0
233:       while i < @lines.size
234:         unless is_code? i
235:           i += 1
236:           next
237:         end
238:         #FIXME: using a restrictive regexp so that only <<[A-Z_a-z]\w*
239:         # matches when unquoted, so as to avoid problems with 1<<2
240:         # (keep in mind that whereas puts <<2 is valid, puts 1<<2 is a
241:         # parse error, but  a = 1<<2  is of course fine)
242:         scanner = StringScanner.new(@lines[i])
243:         j = k = i
244:         loop do
245:           scanned_text = scanner.search_full(/<<(-?)(?:(['"`])((?:(?!\22)).)+)\22||([A-Z_a-z]\w*))/, true, true)
246:           # k is the first line after the end delimiter for the last heredoc
247:           # scanned so far
248:           unless scanner.matched?
249:             i = k
250:             break
251:           end
252:           term = scanner[3] || scanner[4]
253:           # try to ignore symbolic bitshifts like  1<<LSHIFT
254:           ident_text = "<<#{scanner[1]}#{scanner[2]}#{term}#{scanner[2]}"
255:           if scanned_text[/\d+\s*#{Regexp.escape(ident_text)}/]
256:             # it was preceded by a number, ignore
257:             i = k
258:             break
259:           end
260:           must_mark = []
261:           end_of_heredoc = (scanner[1] == "-") ? /^\s*#{Regexp.escape(term)}$/ : /^#{Regexp.escape(term)}$/
262:           loop do
263:             break if j == @lines.size
264:             must_mark << j
265:             if end_of_heredoc =~ @lines[j]
266:               must_mark.each do |n|
267:                 @heredoc_start[n] = i
268:               end
269:               if (must_mark + [i]).any?{|lineidx| @coverage[lineidx]}
270:                 @coverage[i] ||= :inferred
271:                 must_mark.each{|lineidx| @coverage[lineidx] ||= :inferred}
272:               end
273:               # move the "first line after heredocs" index
274:               if @lines[j+=1] =~ /^\s*\n$/
275:                 k = j
276:               end
277:               break
278:             end
279:             j += 1
280:           end
281:         end
282: 
283:         i += 1
284:       end
285:     end
find_multiline_strings() click to toggle source
     # File lib/rcov/file_statistics.rb, line 134
134:     def find_multiline_strings
135:       state = :awaiting_string
136:       wanted_delimiter = nil
137:       string_begin_line = 0
138:       @lines.each_with_index do |line, i|
139:         matching_delimiters = Hash.new{|h,k| k} 
140:         matching_delimiters.update("{" => "}", "[" => "]", "(" => ")")
141:         case state
142:         when :awaiting_string
143:           # very conservative, doesn't consider the last delimited string but
144:           # only the very first one
145:           if md = /^[^#]*%(?:[qQ])?(.)/.match(line)
146:             if !/"%"/.match(line)
147:               wanted_delimiter = /(?!\\).#{Regexp.escape(matching_delimiters[md[1]])}/
148:               # check if closed on the very same line
149:               # conservative again, we might have several quoted strings with the
150:               # same delimiter on the same line, leaving the last one open
151:               unless wanted_delimiter.match(md.post_match)
152:                 state = :want_end_delimiter
153:                 string_begin_line = i
154:               end
155:             end
156:           end
157:         when :want_end_delimiter
158:           @multiline_string_start[i] = string_begin_line
159:           if wanted_delimiter.match(line)
160:             state = :awaiting_string
161:           end
162:         end
163:       end
164:     end
is_nocov?(line) click to toggle source
     # File lib/rcov/file_statistics.rb, line 166
166:     def is_nocov?(line)
167:       line =~ /#:nocov:/
168:     end
mark_nocov_regions(nocov_line_numbers, coverage) click to toggle source
     # File lib/rcov/file_statistics.rb, line 170
170:     def mark_nocov_regions(nocov_line_numbers, coverage)
171:       while nocov_line_numbers.size > 0
172:         begin_line, end_line = nocov_line_numbers.shift, nocov_line_numbers.shift
173:         next unless begin_line && end_line
174:         (begin_line..end_line).each do |line_num|
175:           coverage[line_num] ||= :inferred
176:         end
177:       end
178:     end
next_expr_marked?(lineno) click to toggle source
     # File lib/rcov/file_statistics.rb, line 287
287:     def next_expr_marked?(lineno)
288:       return false if lineno >= @lines.size
289:       found = false
290:       idx = (lineno+1).upto(@lines.size-1) do |i|
291:         next unless is_code? i
292:         found = true
293:         break i
294:       end
295:       return false unless found
296:       @coverage[idx]
297:     end
precompute_coverage(comments_run_by_default = true) click to toggle source
     # File lib/rcov/file_statistics.rb, line 180
180:     def precompute_coverage(comments_run_by_default = true)
181:       changed = false
182:       lastidx = lines.size - 1
183:       if (!is_code?(lastidx) || /^__END__$/ =~ @lines[1]) && !@coverage[lastidx]
184:         # mark the last block of comments
185:         @coverage[lastidx] ||= :inferred
186:         (lastidx-1).downto(0) do |i|
187:           break if is_code?(i)
188:           @coverage[i] ||= :inferred
189:         end
190:       end
191:       nocov_line_numbers = []
192:       
193:       (0...lines.size).each do |i|
194:         nocov_line_numbers << i if is_nocov?(@lines[i])
195: 
196:         next if @coverage[i]
197:         line = @lines[i]
198:         if /^\s*(begin|ensure|else|case)\s*(?:#.*)?$/ =~ line && next_expr_marked?(i) or
199:           /^\s*(?:end|\})\s*(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
200:           /^\s*(?:end\b|\})/ =~ line && prev_expr_marked?(i) && next_expr_marked?(i) or
201:           /^\s*rescue\b/ =~ line && next_expr_marked?(i) or
202:           /(do|\{)\s*(\|[^|]*\|\s*)?(?:#.*)?$/ =~ line && next_expr_marked?(i) or
203:           prev_expr_continued?(i) && prev_expr_marked?(i) or
204:           comments_run_by_default && !is_code?(i) or 
205:           /^\s*((\)|\]|\})\s*)+(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
206:           prev_expr_continued?(i+1) && next_expr_marked?(i)
207:           @coverage[i] ||= :inferred
208:           changed = true
209:         end
210:         
211:       end
212: 
213:       mark_nocov_regions(nocov_line_numbers, @coverage)
214:       
215:       (@lines.size-1).downto(0) do |i|
216:         next if @coverage[i]
217:         if !is_code?(i) and @coverage[i+1] 
218:           @coverage[i] = :inferred
219:           changed = true
220:         end
221:       end
222: 
223:       extend_heredocs if changed
224: 
225:       # if there was any change, we have to recompute; we'll eventually
226:       # reach a fixed point and stop there
227:       precompute_coverage(comments_run_by_default) if changed
228:     end
prev_expr_continued?(lineno) click to toggle source
     # File lib/rcov/file_statistics.rb, line 311
311:     def prev_expr_continued?(lineno)
312:       return false if lineno <= 0
313:       return false if lineno >= @lines.size
314:       found = false
315:       if @multiline_string_start[lineno] && 
316:         @multiline_string_start[lineno] < lineno
317:         return true
318:       end
319:       # find index of previous code line
320:       idx = (lineno-1).downto(0) do |i|
321:         if @heredoc_start[i]
322:           found = true
323:           break @heredoc_start[i] 
324:         end
325:         next unless is_code? i
326:         found = true
327:         break i
328:       end
329:       return false unless found
330:       #TODO: write a comprehensive list
331:       if is_code?(lineno) && /^\s*((\)|\]|\})\s*)+(?:#.*)?$/.match(@lines[lineno])
332:         return true
333:       end
334:       #FIXME: / matches regexps too
335:       # the following regexp tries to reject #{interpolation}
336:       r = /(,|\.|\+|-|\*|\/|<|>|%|&&|\|\||<<|\(|\[|\{|=|and|or|\\)\s*(?:#(?![{$@]).*)?$/.match @lines[idx]
337:       # try to see if a multi-line expression with opening, closing delimiters
338:       # started on that line
339:       [%( )!].each do |opening_str, closing_str| 
340:         # conservative: only consider nesting levels opened in that line, not
341:         # previous ones too.
342:         # next regexp considers interpolation too
343:         line = @lines[idx].gsub(/#(?![{$@]).*$/, "")
344:         opened = line.scan(/#{Regexp.escape(opening_str)}/).size
345:         closed = line.scan(/#{Regexp.escape(closing_str)}/).size
346:         return true if opened - closed > 0
347:       end
348:       if /(do|\{)\s*\|[^|]*\|\s*(?:#.*)?$/.match @lines[idx]
349:         return false
350:       end
351: 
352:       r
353:     end
prev_expr_marked?(lineno) click to toggle source
     # File lib/rcov/file_statistics.rb, line 299
299:     def prev_expr_marked?(lineno)
300:       return false if lineno <= 0
301:       found = false
302:       idx = (lineno-1).downto(0) do |i|
303:         next unless is_code? i
304:         found = true
305:         break i
306:       end
307:       return false unless found
308:       @coverage[idx]
309:     end

Disabled; run with --debug to generate this.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.