Created
June 9, 2025 02:01
-
-
Save ericboehs/928d5bf593a73b5ddab4083d7c93bd45 to your computer and use it in GitHub Desktop.
SimpleCov Coverage Report Parser - Extract and display line and branch coverage from SimpleCov HTML reports with detailed per-file breakdown
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'nokogiri' | |
require 'set' | |
# Path to the SimpleCov HTML report | |
coverage_file = File.join(Dir.pwd, 'coverage', 'index.html') | |
unless File.exist?(coverage_file) | |
puts "β No coverage report found at #{coverage_file}" | |
puts "Run tests first: bin/rails test" | |
exit 1 | |
end | |
# Parse the HTML file | |
doc = Nokogiri::HTML(File.read(coverage_file)) | |
# Extract overall line coverage | |
line_percent = doc.css('.covered_percent .green').first&.text&.strip | |
total_lines = doc.css('.t-line-summary b').first&.text&.strip | |
covered_lines = doc.css('.t-line-summary .green b').first&.text&.strip | |
missed_lines = doc.css('.t-line-summary .red b').first&.text&.strip | |
# Extract overall branch coverage (first t-branch-summary is the overall stats) | |
overall_branch_summary = doc.css('.t-branch-summary').first | |
branch_percent = overall_branch_summary.css('span').last.text.strip.gsub(/[()]/,'') | |
branch_summary_spans = overall_branch_summary.css('span b') | |
total_branches = branch_summary_spans[0]&.text&.strip | |
covered_branches = branch_summary_spans[1]&.text&.strip | |
missed_branches = branch_summary_spans[2]&.text&.strip | |
# Extract timestamp | |
timestamp = doc.css('.timestamp .timeago').first&.attr('title') | |
puts "π SimpleCov Coverage Report" | |
puts "Generated: #{timestamp}" | |
puts "" | |
puts "π Line Coverage: #{line_percent}" | |
puts " β #{covered_lines}/#{total_lines} lines covered" | |
puts " β #{missed_lines} lines missed" | |
puts "" | |
puts "π³ Branch Coverage: #{branch_percent}" | |
puts " β #{covered_branches}/#{total_branches} branches covered" | |
puts " β #{missed_branches} branches missed" | |
puts "" | |
# Show file-by-file breakdown if there are missed lines or branches | |
if missed_lines.to_i > 0 || missed_branches.to_i > 0 | |
puts "π Files with missing coverage:" | |
puts "" | |
files_shown = Set.new | |
doc.css('tbody .t-file').each do |row| | |
file_name = row.css('.t-file__name a').first&.text&.strip | |
line_coverage = row.css('.t-file__coverage').first&.text&.strip | |
branch_coverage = row.css('.t-file__branch-coverage').first&.text&.strip | |
file_link = row.css('.t-file__name a').first&.attr('href') | |
# Only show files that aren't 100% covered and haven't been shown yet | |
if !files_shown.include?(file_name) && (line_coverage != "100.00 %" || branch_coverage != "100.00 %") | |
files_shown.add(file_name) | |
# Extract detailed line information for this file | |
missed_lines = [] | |
missed_branches = [] | |
total_file_lines = 0 | |
covered_file_lines = 0 | |
if file_link | |
file_id = file_link.gsub('#', '') | |
file_section = doc.css("##{file_id}") | |
if file_section.any? | |
# Get the actual counts from SimpleCov's summary | |
line_summary = file_section.css('.t-line-summary') | |
if line_summary.any? | |
summary_text = line_summary.text | |
# Extract numbers from text like "13 relevant lines. 12 lines covered and 1 lines missed." | |
if summary_text.match(/(\d+)\s+relevant\s+lines/) | |
total_file_lines = $1.to_i | |
end | |
if summary_text.match(/(\d+)\s+lines\s+covered/) | |
covered_file_lines = $1.to_i | |
end | |
end | |
# Find missed lines and branches | |
file_section.css('li').each do |line_item| | |
line_number = line_item.attr('data-linenumber') | |
line_class = line_item.attr('class') | |
if line_class&.include?('missed') && !line_class.include?('missed-branch') | |
missed_lines << line_number | |
elsif line_class&.include?('missed-branch') | |
missed_branches << line_number | |
end | |
end | |
end | |
end | |
# Format the line ranges more clearly | |
def format_line_ranges(lines) | |
return "" if lines.empty? | |
ranges = [] | |
current_range = [lines.first.to_i] | |
lines.map(&:to_i).sort[1..-1]&.each do |line| | |
if line == current_range.last + 1 | |
current_range << line | |
else | |
ranges << format_range(current_range) | |
current_range = [line] | |
end | |
end | |
ranges << format_range(current_range) | |
"L#{ranges.join(', L')}" | |
end | |
def format_range(range) | |
if range.length == 1 | |
range.first.to_s | |
else | |
"#{range.first}-#{range.last}" | |
end | |
end | |
files_shown.add(file_name) | |
# Get branch counts from the file section | |
covered_branches = 0 | |
total_branches = 0 | |
if file_link | |
file_id = file_link.gsub('#', '') | |
file_section = doc.css("##{file_id}") | |
branch_summary = file_section.css('.t-branch-summary') | |
if branch_summary.any? | |
branch_spans = branch_summary.css('span b') | |
total_branches = branch_spans[0]&.text&.to_i || 0 | |
covered_branches = branch_spans[1]&.text&.to_i || 0 | |
end | |
end | |
puts " #{file_name} (Line: #{line_coverage}, Branch: #{branch_coverage}):" | |
line_info = "π Lines: #{covered_file_lines}/#{total_file_lines}" | |
unless missed_lines.empty? | |
line_info += " (missed: #{format_line_ranges(missed_lines)})" | |
end | |
puts " #{line_info}" | |
if total_branches > 0 | |
branch_info = "πΏ Branches: #{covered_branches}/#{total_branches}" | |
unless missed_branches.empty? | |
branch_info += " (missed: #{format_line_ranges(missed_branches)})" | |
end | |
puts " #{branch_info}" | |
end | |
puts "" | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
SimpleCov Coverage Report Parser
A Ruby script that extracts and displays SimpleCov coverage data in a clean, human-readable format with detailed per-file breakdown.
Features
Usage
Sample Output
Requirements
coverage/index.html
)Integration with Rails Testing
Add to your
test/test_helper.rb
to automatically show detailed coverage after test runs:This provides immediate visibility into which specific lines and branches need more test coverage!