lib/undercover/lcov_parser.rb
# frozen_string_literal: true
module Undercover
LcovParseError = Class.new(StandardError)
class LcovParser
attr_reader :io, :source_files
def initialize(lcov_io)
@io = lcov_io
@source_files = {}
end
def self.parse(lcov_report_path)
lcov_io = File.open(lcov_report_path)
new(lcov_io).parse
end
def parse
io.each(&method(:parse_line))
io.close
self
end
def coverage(filepath)
_filename, coverage = source_files.find do |relative_path, _|
relative_path == filepath
end
coverage || []
end
def total_coverage
all_lines = source_files.values.flatten(1)
return 0 if all_lines.empty?
all_lines = all_lines.select { _1.size == 2 }
total_f = all_lines.select { |_line_no, hits| hits.positive? }.size.to_f / all_lines.size
total_f.round(3)
end
def total_branch_coverage
all_lines = source_files.values.flatten(1)
return 0 if all_lines.empty?
all_branches = all_lines.select { _1.size == 4 }
total_f = all_branches.select { |_l_no, _block_no, _br_no, hits| hits.positive? }.size.to_f / all_branches.size
total_f.round(3)
end
private
# rubocop:disable Metrics/MethodLength, Style/SpecialGlobalVars, Metrics/AbcSize
def parse_line(line)
case line
when /^SF:(.+)/
@current_filename = $~[1].gsub(/^\.\//, '')
source_files[@current_filename] = []
when /^DA:(\d+),(\d+)/
line_no = $~[1]
covered = $~[2]
source_files[@current_filename] << [line_no.to_i, covered.to_i]
when /^(BRF|BRH):(\d+)/
# branches found/hit; no-op
when /^BRDA:(\d+),(\d+),(\d+),(-|\d+)/
line_no = $~[1]
block_no = $~[2]
branch_no = $~[3]
covered = ($~[4] == '-' ? '0' : $~[4])
source_files[@current_filename] << [line_no.to_i, block_no.to_i, branch_no.to_i, covered.to_i]
when /^end_of_record$/, /^$/
@current_filename = nil
else
raise LcovParseError, "could not recognise '#{line}' as valid LCOV"
end
end
# rubocop:enable Metrics/MethodLength, Style/SpecialGlobalVars, Metrics/AbcSize
end
end