Object
A FileStatistics object associates a filename to:
its source code
the per-line coverage information after correction using rcov’s heuristics
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.
# 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
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
# File lib/rcov/file_statistics.rb, line 87 87: def code_coverage_for_report 88: code_coverage * 100 89: end
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 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
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
Total number of lines.
# File lib/rcov/file_statistics.rb, line 101 101: def num_lines 102: @lines.size 103: end
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
# 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
# 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
# File lib/rcov/file_statistics.rb, line 166 166: def is_nocov?(line) 167: line =~ /#:nocov:/ 168: end
# 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
# 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
# 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
# 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
# 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.
Generated with the Darkfish Rdoc Generator 1.1.6.