Skip to content

Instantly share code, notes, and snippets.

@ericboehs
Created June 9, 2025 02:01
Show Gist options
  • Save ericboehs/928d5bf593a73b5ddab4083d7c93bd45 to your computer and use it in GitHub Desktop.
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
#!/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
@oddboehs
Copy link

oddboehs commented Jun 9, 2025

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

  • πŸ“Š Overall Coverage Summary: Shows line and branch coverage percentages with totals
  • πŸ•’ Timestamp: Displays when the coverage report was generated
  • πŸ“‹ Detailed File Breakdown: Lists files with incomplete coverage, showing:
    • Specific line and branch coverage percentages
    • Exact line numbers that are missed (e.g., L25, L30-32)
    • Missed branch locations
    • Per-file line counts (covered/total)
  • βœ… Error Handling: Gracefully handles missing coverage reports

Usage

# Make executable
chmod +x bin/coverage

# Run after tests generate coverage
bin/coverage

Sample Output

πŸ“Š SimpleCov Coverage Report
Generated: 2025-06-08T20:36:33-05:00

πŸ“ˆ Line Coverage: 98.86%
  βœ… 174/176 lines covered
  ❌ 2 lines missed

🌳 Branch Coverage: 97.06%
  βœ… 33/34 branches covered
  ❌ 1 branches missed

πŸ“‹ Files with missing coverage:

  app/controllers/account_controller.rb (Line: 92.31 %, Branch: 50.00 %):
         πŸ“ Lines: 12/13 (missed: L25)
         🌿 Branches: 1/2 (missed: L30)

  app/controllers/concerns/authentication.rb (Line: 96.88 %, Branch: 100.00 %):
         πŸ“ Lines: 31/32 (missed: L45-47)

Requirements

  • Ruby with Nokogiri gem
  • SimpleCov HTML reports (generated in coverage/index.html)

Integration with Rails Testing

Add to your test/test_helper.rb to automatically show detailed coverage after test runs:

# In parallelize_teardown block
parallelize_teardown do |worker|
  SimpleCov.result
  
  # Show detailed coverage only from the main process after all workers finish
  if worker == 1
    sleep 0.1 # Give a moment for coverage to be written
    if File.exist?("coverage/index.html") && File.exist?("bin/coverage")
      puts ""
      system("bin/coverage")
    end
  end
end

This provides immediate visibility into which specific lines and branches need more test coverage!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment