SimpleCov Integration¶
This document describes how cov-loupe integrates with SimpleCov and manages its dependency on the SimpleCov gem.
SimpleCov Runtime Dependency¶
Status¶
Replaced – cov-loupe now requires SimpleCov at runtime so that multi-suite resultsets can be merged using SimpleCov's combine helpers.
Original Context¶
cov-loupe provides tooling for inspecting SimpleCov coverage reports. When designing the gem, we had to decide whether to depend on SimpleCov as a runtime dependency.
Alternative Approaches¶
- Runtime dependency on SimpleCov: Use SimpleCov's API to read and process coverage data
- Development-only dependency: Read SimpleCov's
.resultset.jsonfiles directly without requiring SimpleCov at runtime - Support multiple coverage formats: Parse coverage data from multiple tools (SimpleCov, Coverage, etc.)
Key Considerations¶
Dependency weight: SimpleCov itself has dependencies: - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1)
Use case separation: - SimpleCov is needed when running tests to collect coverage - cov-loupe is needed when inspecting coverage after tests complete - These are temporally separated activities
Deployment contexts: - CI/CD: Coverage collection happens in test job, inspection might happen in a separate analysis job - Production: Some teams want to analyze coverage data without installing test dependencies - Developer machines: May want to inspect coverage without full test suite dependencies
Format stability: - SimpleCov's .resultset.json format is stable and well-documented - The format is simple JSON with predictable structure - Breaking changes would affect all SimpleCov users, so the format is unlikely to change
Original Decision¶
We initially chose to make SimpleCov a development dependency only and read .resultset.json files directly using Ruby's standard library JSON parser.
Revision: SimpleCov as Runtime Dependency¶
cov-loupe now depends on SimpleCov at runtime for the following reasons:
- Multi-suite merging: Projects using multiple test suites (e.g., RSpec + Minitest) produce separate coverage results that must be merged using SimpleCov's
SimpleCov::ResultMerger.merge_results - Consistent calculations: SimpleCov's coverage percentage algorithms handle edge cases that are difficult to replicate correctly
- Format compatibility: Changes to SimpleCov's internal data structures are automatically handled by using its API
Current Implementation¶
cov-loupe currently depends on amazing_print, mcp, and simplecov at runtime.
Coverage data is read directly from JSON files by CovLoupe::CoverageModel#load_coverage_data:
rs = Resolvers::ResolverHelpers.find_resultset(@root, resultset: resultset)
loaded = ResultsetLoader.load(resultset_path: rs)
coverage_map = loaded.coverage_map or raise(CoverageDataError, "No 'coverage' key found in resultset file: #{rs}")
@cov = coverage_map.transform_keys { |k| File.absolute_path(k, @root) }
@cov_timestamp = loaded.timestamp
Coverage calculations use simple algorithms in CovLoupe::CoverageCalculator (summary, uncovered, detailed):
def summary(arr)
total = 0
covered = 0
arr.compact.each do |hits|
total += 1
covered += 1 if hits.to_i > 0
end
percentage = total.zero? ? 100.0 : ((covered.to_f * 100.0 / total) * 100).round / 100.0
{ 'covered' => covered, 'total' => total, 'percentage' => percentage }
end
def uncovered(arr)
out = []
arr.each_with_index do |hits, i|
next if hits.nil?
out << (i + 1) if hits.to_i.zero?
end
out
end
def detailed(arr)
rows = []
arr.each_with_index do |hits, i|
h = hits&.to_i
rows << { 'line' => i + 1, 'hits' => h, 'covered' => h.positive? } if h
end
rows
end
SimpleCov .resultset.json Format¶
The format we parse has this structure:
{
"RSpec": {
"coverage": {
"/absolute/path/to/file.rb": {
"lines": [null, 1, 3, 0, null, 5, ...]
}
},
"timestamp": 1633072800
}
}
Where: - Top level keys are test suite names (e.g., "RSpec", "Minitest") - coverage contains file paths mapped to coverage data - lines is an array where each index represents a line number (0-indexed) - Array values: null = not executable, 0 = not covered, >0 = hit count - timestamp is Unix timestamp when coverage was collected
Resultset Discovery¶
We implement flexible discovery of .resultset.json files via Resolvers::ResultsetPathResolver::DEFAULT_CANDIDATES:
DEFAULT_CANDIDATES = [
'.resultset.json',
'coverage/.resultset.json',
'tmp/.resultset.json'
].freeze
This supports common SimpleCov configurations without requiring SimpleCov to be loaded.
Consequences¶
Positive (Original Development-Only Approach)¶
- Lightweight installation: No transitive dependencies beyond
mcpgem - Deployment flexibility: Can analyze coverage in environments without test dependencies
- Faster installation: Fewer gems to download and install
- Clear separation of concerns: Coverage collection vs. coverage analysis are independent
- CI/CD optimization: Analysis jobs don't need full test suite dependencies
- Production-safe: Can be deployed to production environments if needed (e.g., for monitoring)
Negative (Original Development-Only Approach)¶
- Format dependency: Tightly coupled to SimpleCov's JSON format
- Breaking changes risk: If SimpleCov changes
.resultset.jsonstructure, we must adapt - Limited to SimpleCov: Cannot read coverage data from other Ruby coverage tools
- Duplicate logic: Coverage percentage calculations reimplemented (though simple)
- Maintenance: Must track SimpleCov format changes manually
Trade-offs (Current Runtime Dependency Approach)¶
- Versus development-only dependency: Heavier installation footprint, but better multi-suite support and format compatibility
- Versus multi-format support: Simpler implementation but locked to SimpleCov ecosystem
- Versus custom merging logic: More reliable but requires SimpleCov at runtime
Risk Mitigation¶
- Format stability: SimpleCov has maintained
.resultset.jsoncompatibility for years - Simple format: JSON structure is straightforward and unlikely to change dramatically
- Development dependency: We still use SimpleCov in our own tests, so format changes would be detected immediately
- Documentation: AGENTS.md documents the format dependency explicitly
- Error handling: Robust error messages when format doesn't match expectations
Format Evolution Strategy¶
If SimpleCov's format changes: 1. Minor additions (new keys): Ignore unknown keys, only parse what we need 2. Breaking changes (structure changes): Version detection logic to support multiple formats 3. Alternative formats: Could add support for other coverage tools' JSON formats if needed
Current Limitations Accepted¶
- Only supports SimpleCov (not Coverage gem, other tools)
- Assumes standard
.resultset.jsonlocations - Multi-suite merging requires SimpleCov runtime dependency
- No support for branch coverage (SimpleCov feature not widely used yet)
References¶
- Gemspec dependencies:
cov-loupe.gemspec(spec.add_dependencyentries) - JSON parsing:
lib/cov_loupe/resultset_loader.rb(ResultsetLoader.load) - Coverage calculations:
lib/cov_loupe/coverage_calculator.rb(CoverageCalculator.summary,.uncovered,.detailed) - Resultset discovery:
lib/cov_loupe/resolvers/resultset_path_resolver.rb(ResultsetPathResolver::DEFAULT_CANDIDATES) - SimpleCov format documentation: https://github.com/simplecov-ruby/simplecov
- Development usage: Uses SimpleCov in
spec/spec_helper.rbto test itself