class SimpleCov::SourceFile
source lines and featuring helpers to interpret that data.
Representation of a source file including it’s coverage data, source code,
def branches
def branches @branches ||= build_branches end
def branches_coverage_percent
def branches_coverage_percent return 100.0 if no_branches? return 0.0 if covered_branches.empty? Float(covered_branches.size * 100.0 / total_branches.size.to_f) end
def branches_for_line(line_number)
def branches_for_line(line_number) branches_report.fetch(line_number, []) end
def branches_report
def branches_report @branches_report ||= build_branches_report end
def build_branch(branch_data, hit_count, condition_start_line, condition_id)
def build_branch(branch_data, hit_count, condition_start_line, condition_id) type, id, start_line, _start_col, end_line, _end_col = branch_data SourceFile::Branch.new( # rubocop these are keyword args please let me keep them, thank you # rubocop:disable Style/HashSyntax start_line: start_line, end_line: end_line, coverage: hit_count, inline: start_line == condition_start_line, positive: positive_branch?(condition_id, id, type) # rubocop:enable Style/HashSyntax ) end
def build_branches
-
(Array)
-
def build_branches coverage_branch_data = coverage.fetch(:branches, {}) branches = coverage_branch_data.flat_map do |condition, coverage_branches| build_branches_from(condition, coverage_branches) end process_skipped_branches(branches) end
def build_branches_from(condition, branches)
def build_branches_from(condition, branches) # the format handed in from the coverage data is like this: # # [:then, 4, 6, 6, 6, 10] # # which is [type, id, start_line, start_col, end_line, end_col] condition_type, condition_id, condition_start_line, * = restore_ruby_data_structure(condition) branches .map { |branch_data, hit_count| [restore_ruby_data_structure(branch_data), hit_count] } .reject { |branch_data, _hit_count| ignore_branch?(branch_data, condition_type, condition_start_line) } .map { |branch_data, hit_count| build_branch(branch_data, hit_count, condition_start_line, condition_id) } end
def build_branches_report
-
(Hash)
-
def build_branches_report branches.reject(&:skipped?).each_with_object({}) do |branch, coverage_statistics| coverage_statistics[branch.report_line] ||= [] coverage_statistics[branch.report_line] << branch.report end end
def build_lines
def build_lines coverage_exceeding_source_warn if coverage[:lines].size > src.size lines = src.map.with_index(1) do |src, i| SimpleCov::SourceFile::Line.new(src, i, coverage[:lines][i - 1]) end process_skipped_lines(lines) end
def build_no_cov_chunks
def build_no_cov_chunks no_cov_lines = src.map.with_index(1).select { |line, _index| LinesClassifier.no_cov_line?(line) } warn "uneven number of nocov comments detected" if no_cov_lines.size.odd? no_cov_lines.each_slice(2).map do |(_line_start, index_start), (_line_end, index_end)| index_start..index_end end end
def coverage_exceeding_source_warn
def coverage_exceeding_source_warn warn "Warning: coverage data provided by Coverage [#{coverage[:lines].size}] exceeds number of lines in #{filename} [#{src.size}]" end
def covered_branches
-
(Array)
-
def covered_branches @covered_branches ||= branches.select(&:covered?) end
def covered_lines
def covered_lines @covered_lines ||= lines.select(&:covered?) end
def covered_percent
def covered_percent return 100.0 if no_lines? return 0.0 if relevant_lines.zero? Float(covered_lines.size * 100.0 / relevant_lines.to_f) end
def covered_strength
def covered_strength return 0.0 if relevant_lines.zero? (lines_strength / relevant_lines.to_f).round(1) end
def ignore_branch?(branch_data, condition_type, condition_start_line)
def ignore_branch?(branch_data, condition_type, condition_start_line) branch_type = branch_data[0] branch_start_line = branch_data[2] # branch coverage always reports case to be with an else branch even when # there is no else branch to be covered, it's noticable by the reported start # line being the same as that of the condition/case condition_type == :case && branch_type == :else && condition_start_line == branch_start_line end
def initialize(filename, coverage)
def initialize(filename, coverage) @filename = filename.to_s @coverage = coverage end
def line(number)
def line(number) lines[number - 1] end
def line_with_missed_branch?(line_number)
-
(Boolean)
-
Parameters:
-
line_number
(Integer
) --
def line_with_missed_branch?(line_number) branches_for_line(line_number).select { |count, _sign| count.zero? }.any? end
def lines
Returns all source lines for this file as instances of SimpleCov::SourceFile::Line,
def lines @lines ||= build_lines end
def lines_of_code
def lines_of_code covered_lines.size + missed_lines.size end
def lines_strength
def lines_strength lines.map(&:coverage).compact.reduce(:+) end
def missed_branches
-
(Array)
-
def missed_branches @missed_branches ||= branches.select(&:missed?) end
def missed_lines
Returns all lines that should have been, but were not covered
def missed_lines @missed_lines ||= lines.select(&:missed?) end
def never_lines
Returns all lines that are not relevant for coverage as
def never_lines @never_lines ||= lines.select(&:never?) end
def no_branches?
def no_branches? total_branches.empty? end
def no_cov_chunks
def no_cov_chunks @no_cov_chunks ||= build_no_cov_chunks end
def no_lines?
def no_lines? lines.length.zero? || (lines.length == never_lines.size) end
def positive_branch?(condition_id, branch_id, branch_type)
-
(Boolean)
-
def positive_branch?(condition_id, branch_id, branch_type) return true if branch_type == :when branch_id == (1 + condition_id) end
def process_skipped_branches(branches)
def process_skipped_branches(branches) return branches if no_cov_chunks.empty? branches.each do |branch| branch.skipped! if no_cov_chunks.any? { |no_cov_chunk| branch.overlaps_with?(no_cov_chunk) } end branches end
def process_skipped_lines(lines)
def process_skipped_lines(lines) # the array the lines are kept in is 0-based whereas the line numbers in the nocov # chunks are 1-based and are expected to be like this in other parts (and it's also # arguably more understandable) no_cov_chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) } lines end
def project_filename
def project_filename @filename.sub(Regexp.new("^#{Regexp.escape(SimpleCov.root)}"), "") end
def relevant_lines
def relevant_lines lines.size - never_lines.size - skipped_lines.size end
def restore_ruby_data_structure(structure)
See #801
our data structures that we write so eval isn't _too_ bad.
We should probably do something different here, but as it stands these are
don't make their way back to us intact e.g. just as a string or a symbol (currently keys are symbolized).
Since we are dumping to and loading from JSON, and we have arrays as keys those
def restore_ruby_data_structure(structure) # Tests use the real data structures (except for integration tests) so no need to # put them through here. return structure if structure.is_a?(Array) # as of right now the keys are still symbolized # rubocop:disable Security/Eval eval structure.to_s # rubocop:enable Security/Eval end
def skipped_lines
def skipped_lines @skipped_lines ||= lines.select(&:skipped?) end
def src
def src # We intentionally read source code lazily to # suppress reading unused source code. @src ||= File.open(filename, "rb", &:readlines) end
def total_branches
def total_branches covered_branches + missed_branches end