ADR 005: No 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.
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
Decision¶
We chose to make SimpleCov a development dependency only and read .resultset.json files directly using Ruby's standard library JSON parser.
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 = CovUtil.find_resultset(@root, resultset: resultset)
raw = JSON.parse(File.read(rs))
# SimpleCov typically writes a single test suite entry to .resultset.json
# Find the first entry that has coverage data (skip comment entries)
_suite, data = raw.find { |k, v| v.is_a?(Hash) && v.key?('coverage') }
raise "No test suite with coverage data found in resultset file: #{rs}" unless data
cov = data['coverage'] or raise "No 'coverage' key found in resultset file: #{rs}"
@cov = cov.transform_keys { |k| File.absolute_path(k, @root) }
@cov_timestamp = (data['timestamp'] || data['created_at'] || 0).to_i
Coverage calculations use simple algorithms in CovLoupe::CovUtil (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 CovUtil::RESULTSET_CANDIDATES:
RESULTSET_CANDIDATES = [
'.resultset.json',
'coverage/.resultset.json',
'tmp/.resultset.json'
].freeze
This supports common SimpleCov configurations without requiring SimpleCov to be loaded.
Consequences¶
Positive¶
- 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¶
- 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¶
- Versus runtime dependency: Lighter weight but less resilient to format changes
- Versus multi-format support: Simpler implementation but locked to SimpleCov ecosystem
- Versus using SimpleCov API: More flexible deployment but requires understanding the file format
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: CLAUDE.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 - No support for merged coverage from multiple test runs (SimpleCov handles this before writing JSON)
- 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/model.rb(CoverageModel#load_coverage_data) - Coverage calculations:
lib/cov_loupe/util.rb(CovUtil.summary,.uncovered,.detailed) - Resultset discovery:
lib/cov_loupe/util.rb(CovUtil::RESULTSET_CANDIDATESand helpers) - SimpleCov format documentation: https://github.com/simplecov-ruby/simplecov
- Development usage: Uses SimpleCov in
spec/spec_helper.rbto test itself