-
Notifications
You must be signed in to change notification settings - Fork 572
Open
Description
Single File Coverage Reporting for SimpleCov
Problem
Currently, SimpleCov doesn't provide a way to check test coverage for individual files. This makes it difficult to:
- Validate coverage for new files in CI
- Get quick feedback during TDD
- Support AI tools that need file-specific coverage metrics
Real-world Example
In our project, we use AI to help write tests. Our .cursorrules require specific coverage checks:
roles:
- name: "RSpec Testing Guide"
rules:
- Check that tests pass and coverage is at least 80% using `bin/rspec_coverage spec/<folder>/<file>.rb`
# ...other rulesSolution
I've implemented a proof of concept that works with RSpec (ready to implement for Minitest if approved):
$ bin/rspec_coverage spec/models/user_spec.rb
Coverage for:
spec/models/user_spec.rb
app/models/user.rb:
Line coverage: 92.5%
Branch coverage: 87.3%Implementation
Here are the files needed to implement this feature:
# bin/rspec_coverage
#!/usr/bin/env ruby
if ARGV.empty?
puts "Usage: bin/rspec_coverage <spec_file1> [spec_file2 ...]"
puts "Example: bin/rspec_coverage spec/models/user_spec.rb spec/models/post_spec.rb"
exit 1
end
spec_files = ARGV
invalid_files = spec_files.reject { |file| file.end_with?('_spec.rb') }
unless invalid_files.empty?
puts "Please provide only spec files (ending with _spec.rb):"
invalid_files.each { |file| puts " #{file}" }
exit 1
end
puts "Coverage for:"
spec_files.each { |file| puts " #{file}" }
puts
exit_code = 0
spec_files.each do |spec_file|
puts "\nRunning #{spec_file}..."
env = {
'COVERAGE' => 'true',
'COVERAGE_SPEC_FILE_PATH' => spec_file
}
system(env, "bundle exec rspec #{spec_file}")
exit_code = 1 unless $?.success?
end
exit exit_code # lib/simplecov/single_file_reporter.rb
module SimpleCov
module SingleFileReporter
module_function
LOGGER = Logger.new(STDOUT)
LOGGER.level = Logger::INFO
LOGGER.formatter = ->(_, _, _, msg) { "#{msg}\n" }
ReportResult = Struct.new(:success?, :messages, :coverage, keyword_init: true) do
def self.failure(message)
new(success?: false, messages: [message], coverage: nil)
end
def self.success(coverage_stats)
new(success?: true, messages: [], coverage: coverage_stats)
end
end
def report_for(spec_file_path)
example_group = find_example_group(spec_file_path)
return ReportResult.failure("No example group found for #{spec_file_path}") unless example_group
described_class = example_group.metadata[:described_class]
return ReportResult.failure("Cannot determine described_class for #{spec_file_path}") unless described_class
source_file_path = find_source_file(described_class)
return ReportResult.failure("Cannot find source file for #{described_class}") unless source_file_path
target_result = find_coverage_result(source_file_path)
if target_result
coverage_stats = log_coverage_stats(target_result)
ReportResult.success(coverage_stats)
else
ReportResult.failure("File #{source_file_path} not found in coverage results")
end
end
def find_example_group(spec_file_path)
RSpec.world.example_groups.find do |g|
g.metadata[:absolute_file_path] == File.expand_path(spec_file_path)
end
end
def find_source_file(klass)
Object.const_source_location(klass.to_s)&.first
end
def find_coverage_result(source_file_path)
SimpleCov.result.files.find { |file| file.filename == source_file_path }
end
def log_coverage_stats(target_result, precision = 1)
coverage_stats = target_result.coverage_statistics
line_coverage = coverage_stats[:line].percent.round(precision)
branch_coverage = coverage_stats[:branch].percent.round(precision)
stats = {
line_coverage: line_coverage,
branch_coverage: branch_coverage
}
LOGGER.info("#{target_result.filename}:")
LOGGER.info(" Line coverage: #{line_coverage}%")
LOGGER.info(" Branch coverage: #{branch_coverage}%")
stats
end
end
end# spec/spec_helper.rb
if ENV['COVERAGE']
require 'simplecov'
if ENV['COVERAGE_SPEC_FILE_PATH']
SimpleCov.start 'rails' do
enable_coverage :branch
primary_coverage :branch
coverage_dir 'tmp/coverage'
target_file = ENV['COVERAGE_SPEC_FILE_PATH']
next unless target_file
at_exit do
SimpleCov::SingleFileReporter.report_for(target_file)
end
end
end
endjdufresne
Metadata
Metadata
Assignees
Labels
No labels