Advanced Usage Guide¶
Examples use
clp, an alias pointed at the demo fixture with partial coverage:
alias clp='cov-loupe -R docs/fixtures/demo_project'# -R = --rootReplace
clpwithcov-loupeif you want to target your own project/resultset.
Table of Contents¶
- Advanced MCP Integration
- Staleness Detection & Validation
- Advanced Path Resolution
- Error Handling Strategies
- Custom Ruby Integration
- CI/CD Integration
- Advanced Filtering & Glob Patterns
- Performance Optimization
- Custom Output Processing
- Multi-Suite Coverage Merging
Advanced MCP Integration¶
MCP Error Handling¶
The MCP server uses structured error responses:
{
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "Coverage data not found at coverage/.resultset.json",
"data": {
"type": "FileError",
"context": "MCP tool execution"
}
},
"id": 1
}
MCP Server Logging¶
The MCP server logs to cov_loupe.log in the current directory by default.
To override the default log file location, specify the --log-file (or -l) argument wherever and however you configure your MCP server. For example, to log to a different file path, include -l /path/to/logfile.log in your server configuration. To log to standard error, use -l stderr.
Warning: Log files may grow unbounded in long-running or CI usage. Consider using a log rotation tool or periodically cleaning up the log file if this is a concern.
Note: Logging to stdout is not permitted in MCP mode since it would interfere with the request processing.
Testing MCP Server Manually¶
Use JSON-RPC over stdin to test the MCP server. Note: CLI flags set defaults for MCP tool calls, but per-request JSON parameters still win. Use -R/-r when you want server-wide defaults, or pass root/resultset per request.
# Get version (no parameters needed)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"version_tool","arguments":{}}}' | cov-loupe -m mcp
# Get file summary (include root parameter in JSON)
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"coverage_summary_tool","arguments":{"path":"app/models/order.rb","root":"docs/fixtures/demo_project"}}}' | cov-loupe -m mcp
# List all files with sorting (include root parameter)
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_tool","arguments":{"sort_order":"ascending","root":"docs/fixtures/demo_project"}}}' | cov-loupe -m mcp
# Get uncovered lines (include root parameter)
echo '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"uncovered_lines_tool","arguments":{"path":"app/controllers/orders_controller.rb","root":"docs/fixtures/demo_project"}}}' | cov-loupe -m mcp
Why not use clp alias here? clp is useful for CLI subcommands, but MCP calls run a long-lived server process. You can pass -R at startup to set defaults, or set root explicitly in each JSON request when you want to be explicit or override the defaults.
Sorting Coverage Lists with n/a Entries¶
When a file has no executable lines, its coverage percentage is reported as n/a. In list outputs, n/a entries are grouped separately from numeric percentages. The default descending sort order places n/a entries above 100% coverage so they appear earlier in the list, while still keeping low numeric percentages toward the bottom for attention.
If you need to treat n/a differently, post-process the JSON output from list or list_tool and apply your own sort or filtering rules.
Staleness Detection & Validation¶
Understanding Staleness Checks¶
Staleness checking prevents using outdated coverage data. The behavior is controlled by the boolean raise_on_stale setting:
raise_on_stale: false (default) - Coverage data is returned even if stale - Stale indicators are still computed - Best for exploratory reporting
raise_on_stale: true - Raises errors when stale coverage is detected - Recommended for CI/CD enforcement
File-Level Staleness¶
A file is considered stale when any of the following are true: 1. Source file modified after coverage generation (requires timestamps - see Timestamp Warnings) 2. Line count differs from coverage array length 3. File exists in coverage but deleted from filesystem
CLI Usage:
Ruby API:
model = CovLoupe::CoverageModel.new(
raise_on_stale: true
)
begin
summary = model.summary_for('app/models/order.rb')
rescue CovLoupe::CoverageDataStaleError => e
puts "File modified after coverage: #{e.file_path}"
puts "Coverage timestamp: #{e.cov_timestamp}"
puts "File mtime: #{e.file_mtime}"
puts "Source lines: #{e.src_len}, Coverage lines: #{e.cov_len}"
end
Project-Level Staleness¶
Detects system-wide staleness issues:
Conditions Checked: 1. Newer files - Any tracked file modified after coverage (requires timestamps - see Timestamp Warnings) 2. Missing files - Tracked files with no coverage data 3. Deleted files - Coverage exists for non-existent files
CLI Usage:
You can see if any files in the project are stale by running the (implicit here) list command with --raise-on-stale and checking the exit code:
$ cov-loupe -S true list
Coverage data stale (project): CovLoupe::CoverageDataProjectStaleError
Coverage - time: 2025-12-10T18:23:00Z (local 2025-12-11T02:23:00+08:00)
Newer files (1): - lib/cov_loupe/version.rb
Resultset - /path/to/project/coverage/.resultset.json
$ echo $?
1
Ruby API:
model = CovLoupe::CoverageModel.new(raise_on_stale: true)
begin
model.list(raise_on_stale: true)
rescue CovLoupe::CoverageDataProjectStaleError => e
puts "Newer files: #{e.newer_files.join(', ')}"
puts "Missing from coverage: #{e.missing_files.join(', ')}"
puts "Deleted but in coverage: #{e.deleted_files.join(', ')}"
end
Timestamp Warnings¶
When coverage data lacks timestamps (e.g., manually created resultsets or older SimpleCov versions), cov-loupe displays a warning in both CLI and MCP modes:
WARNING: Coverage timestamps are missing. Time-based staleness checks were skipped.
Files may appear "ok" even if source code is newer than the coverage data.
Check your coverage tool configuration to ensure timestamps are recorded.
What this means: - Time-based staleness checks (the "newer" indicator) cannot run without timestamps - Files modified after coverage collection won't be flagged as stale - Only line count mismatches and missing files will be detected - The timestamp_status field in JSON output will show "missing" instead of "ok"
Where it appears: - CLI table format: After the coverage table in list output - CLI JSON format: After JSON output, and in the timestamp_status field - MCP mode: In coverage_table_tool output after the exclusions summary - MCP JSON: In the timestamp_status field of list_tool responses
How to fix:
Modern SimpleCov versions automatically include timestamps in .resultset.json. If you see this warning:
- Ensure SimpleCov is up to date (
gem update simplecov) - Regenerate coverage data (
bundle exec rspec) - If using custom resultset generation, ensure timestamps are included
Example timestamp in .resultset.json:
Advanced Path Resolution¶
Path Matching Strategy¶
Path resolution uses two strategies in order:
- Exact absolute path match - Direct lookup using the full path
- Relative path resolution - Strips project root and retries with relative path
model = CovLoupe::CoverageModel.new(root: '/path/to/project')
model.summary_for('/path/to/project/app/models/order.rb') # Absolute
model.summary_for('app/models/order.rb') # Relative
Working with Multiple Projects¶
# Project A
model_a = CovLoupe::CoverageModel.new(
root: '/path/to/projects/service-a',
resultset: '/path/to/projects/service-a/coverage/.resultset.json'
)
# Project B
model_b = CovLoupe::CoverageModel.new(
root: '/path/to/projects/service-b',
resultset: '/path/to/projects/service-b/tmp/coverage/.resultset.json'
)
# Compare coverage
coverage_a = model_a.list
coverage_b = model_b.list
Error Handling Strategies¶
Context-Aware Error Handling¶
CLI Mode: user-facing messages, exit codes, optional debug mode
Library Mode: typed exceptions with full details
MCP Server Mode: JSON-RPC errors logged to file with structured data
Error Modes¶
CLI Error Modes:
# Silent mode - minimal output
clp --error-mode off summary app/models/order.rb
# Standard mode - user-friendly errors (default)
clp --error-mode log summary app/models/order.rb
# Verbose mode - full stack traces
clp --error-mode debug summary app/models/order.rb
Ruby API Error Handling:
require 'cov_loupe'
begin
model = CovLoupe::CoverageModel.new(
root: '/path/to/project',
resultset: '/nonexistent/.resultset.json'
)
rescue CovLoupe::FileError => e
# Handle missing resultset
puts "Coverage file not found: #{e.message}"
rescue CovLoupe::CoverageDataError => e
# Handle corrupt/invalid coverage data
puts "Invalid coverage data: #{e.message}"
end
Custom Error Handlers¶
Provide custom error handlers when embedding the CLI:
class CustomErrorHandler
def handle_error(error, context: nil)
# Log to custom service
ErrorTracker.notify(error, context: context)
# Re-raise or handle gracefully
raise error
end
end
cli = CovLoupe::CoverageCLI.new(error_handler: CustomErrorHandler.new)
Custom Ruby Integration¶
Building Custom Coverage Policies¶
Use the validate subcommand to enforce custom coverage policies in CI/CD. Example predicates are in examples/success_predicates/.
The predicate can be any Ruby object that responds to call and accepts a CoverageModel as its argument. This is usually a lambda (proc), but it can also be a nonlambda proc, a class, or an instance with a call method. The predicate should return a truthy value for success or false/nil for failure.
⚠️ SECURITY WARNING
Success predicates execute as arbitrary Ruby code with full system privileges. They have unrestricted access to: - File system operations (read, write, delete) - Network operations (HTTP requests, sockets) - System commands (via backticks,
system(),exec(), etc.) - Environment variables and sensitive dataOnly use predicate files from trusted sources. Treat them like any other executable code in your project. - Never use predicates from untrusted or unknown sources - Review predicates before use, especially in CI/CD environments - Store predicates in version control with code review - Be cautious when copying examples from the internet
Quick Usage:
# All files must be >= 80%
clp validate examples/success_predicates/list_above_threshold_predicate.rb
# Total project coverage >= 85%
clp validate examples/success_predicates/project_coverage_minimum_predicate.rb
# Custom predicate from file
clp validate coverage_policy.rb
# Inline string mode
clp validate -i '->(m) { m.list["files"].all? { |f| f["percentage"] >= 80 } }'
Creating a predicate:
# coverage_policy.rb
->(model) do
# All files must have >= 80% coverage
model.list['files'].all? { |f| f['percentage'] >= 80 }
end
Advanced predicate with reporting:
# coverage_policy.rb
class CoveragePolicy
def call(model)
threshold = 80
low_files = model.list['files'].select { |f| f['percentage'] < threshold }
if low_files.empty?
puts "✓ All files have >= #{threshold}% coverage"
true
else
warn "✗ Files below #{threshold}%:"
low_files.each { |f| warn " #{f['file']}: #{f['percentage']}%" }
false
end
end
end
CoveragePolicy.new
Exit codes: - 0 - Predicate returned truthy (pass) - 1 - Predicate returned falsy (fail) - 2 - Predicate raised an error
See examples/success_predicates/README.md for more examples.
Path Relativization¶
Convert absolute paths to relative for cleaner output:
model = CovLoupe::CoverageModel.new(root: '/path/to/project')
# Get data with absolute paths
data = model.summary_for('app/models/order.rb')
# => { 'file' => '/path/to/project/app/models/order.rb', ... }
# Relativize paths
relative_data = model.relativize(data)
# => { 'file' => 'app/models/order.rb', ... }
# Works with list payloads too
list_result = model.list
relative_list = model.relativize(list_result)
relative_files = relative_list['files']
CI/CD Integration¶
The CLI is designed for CI/CD use with features that integrate naturally into pipeline workflows:
Key Integration Features¶
- Exit codes: Non-zero on failure, making it suitable for pipeline failure conditions
- JSON output:
-fJformat for parsing by CI tools and custom processing - Staleness checking:
--raise-on-stale trueto fail on outdated coverage data - Success predicates: Custom Ruby policies for coverage enforcement
Basic CI Pattern¶
# 1. Run tests to generate coverage
bundle exec rspec
# 2. Validate coverage freshness (fails with exit code 1 if stale)
clp -S true -g "lib/**/*.rb"
# 3. Export data for CI artifacts or further processing
clp -fJ list > coverage.json
Using Coverage Validation¶
Enforce custom coverage policies with the validate subcommand:
# Run tests
bundle exec rspec
# Apply coverage policy (fails with exit code 1 if predicate returns false)
clp validate coverage_policy.rb
Exit codes: - 0 - Success (coverage meets requirements) - 1 - Failure (coverage policy not met or stale data detected) - 2 - Error (invalid predicate or system error)
Platform-Specific Examples¶
For platform-specific integration examples (GitHub Actions, GitLab CI, Jenkins, CircleCI, etc.), see community contributions in the GitHub Discussions.
Advanced Filtering & Glob Patterns¶
Tracked Globs Overview¶
Default behavior: By default, --tracked-globs is empty ([]), which means all files in the coverage resultset are shown. This ensures transparency—you see exactly what SimpleCov measured without any filtering.
Why opt-in filtering? - Coverage results are not hidden - Results are not excluded because their filespecs did not match default tracked globs - Meaningful validation - missing_tracked_files only flags files you explicitly expect to have coverage - Project flexibility - Different projects use different directory structures
Important: Files lacking any coverage at all (not loaded during tests) will not appear in the resultset and therefore won't be visible with the default empty array. To detect such files, you must set --tracked-globs to match the files you expect to have coverage.
Two purposes of tracked globs: 1. Exclude unwanted results - Only show files from the resultset that match the patterns 2. Include files with or without coverage - Report files that match the patterns but aren't in the resultset (reported in missing_tracked_files for list, missing_from_coverage for totals)
Best practice: Set COV_LOUPE_OPTS to match your SimpleCov track_files configuration:
# spec_helper.rb
SimpleCov.start do
add_filter '/spec/'
track_files 'lib/**/*.rb'
track_files 'app/**/*.rb'
end
# Shell config (.bashrc, .zshrc, etc.)
export COV_LOUPE_OPTS="--tracked-globs lib/**/*.rb,app/**/*.rb"
This alignment ensures: - list and totals output matches SimpleCov's scope - missing_tracked_files (in list) reports files that SimpleCov should track but hasn't measured - No surprises from default patterns that don't match your project
Pattern Syntax¶
Uses Ruby's File.fnmatch with extended glob support:
# Single directory, recursive
-g "lib/**/*.rb"
# Multiple patterns
-g "lib/payments/**/*.rb" -g "lib/ops/jobs/**/*.rb"
# Exclude patterns (use CLI filtering to exclude ops jobs)
clp -fJ list | jq '.files[] | select(.file | test("ops") | not)'
# Ruby alternative:
clp -fJ list | ruby -r json -e '
JSON.parse($stdin.read)["files"].reject { |f| f["file"].include?("ops") }.each do |f|
puts JSON.pretty_generate(f)
end
'
# Rexe alternative:
clp -fJ list | rexe -ij -mb -oJ 'self["files"].reject { |f| f["file"].include?("ops") }'
# Complex patterns
-g "lib/{models,controllers}/**/*.rb"
-g "app/**/concerns/*.rb"
Use Cases¶
1. Monitor Subsystem Coverage:
# API layer only
clp -g "lib/api/**/*.rb" list
# Core business logic
clp -g "lib/domain/**/*.rb" list
2. Ensure New Files Have Coverage:
3. Multi-tier Reporting:
# Generate separate reports per layer
for layer in models views controllers; do
clp -g "app/${layer}/**/*.rb" -fJ list > "coverage-${layer}.json"
done
Ruby API with Globs¶
model = CovLoupe::CoverageModel.new
# Filter files in output
api_files = model.list(
tracked_globs: ['lib/api/**/*.rb']
)['files']
# Multi-pattern filtering
core_files = model.list(
tracked_globs: [
'lib/core/**/*.rb',
'lib/domain/**/*.rb'
]
)['files']
# Validate specific subsystems
begin
model.list(
raise_on_stale: true,
tracked_globs: ['lib/critical/**/*.rb']
)
rescue CovLoupe::CoverageDataProjectStaleError => e
# Handle missing coverage for critical files
puts "Critical files missing coverage:"
e.missing_files.each { |f| puts " - #{f}" }
end
Performance Optimization¶
Minimizing Coverage Reads¶
The CoverageModel reads .resultset.json once at initialization:
# Good: Single model for multiple queries
model = CovLoupe::CoverageModel.new
files = model.list['files']
file1 = model.summary_for('lib/a.rb')
file2 = model.summary_for('lib/b.rb')
# Bad: Re-reads coverage for each operation
model1 = CovLoupe::CoverageModel.new
files = model1.list['files']
model2 = CovLoupe::CoverageModel.new
file1 = model2.summary_for('lib/a.rb')
Batch Processing¶
# Process multiple files in one pass
files_to_analyze = ['lib/a.rb', 'lib/b.rb', 'lib/c.rb']
model = CovLoupe::CoverageModel.new
results = files_to_analyze.each_with_object({}) do |file, hash|
hash[file] = {
summary: model.summary_for(file),
uncovered: model.uncovered_for(file)
}
rescue CovLoupe::FileError
hash[file] = { error: 'No coverage' }
end
Filtering Early¶
Use tracked_globs to reduce data processing:
# Bad: Filter after loading all data
list = model.list['files']
api_files = list.select { |f| f['file'].include?('api') }
# Good: Filter during query
api_files = model.list(
tracked_globs: ['lib/api/**/*.rb']
)['files']
Caching Coverage Models¶
For long-running processes:
class CoverageCache
def initialize(ttl: 300) # 5 minute cache
@cache = {}
@ttl = ttl
end
def model_for(root)
key = root.to_s
now = Time.now
if @cache[key] && (now - @cache[key][:time] < @ttl)
@cache[key][:model]
else
@cache[key] = {
model: CovLoupe::CoverageModel.new(root: root),
time: now
}
@cache[key][:model]
end
end
end
cache = CoverageCache.new
model = cache.model_for('/path/to/project')
Custom Output Processing¶
Format Conversion¶
CSV Export:
require 'csv'
model = CovLoupe::CoverageModel.new
files = model.list['files']
CSV.open('coverage.csv', 'w') do |csv|
csv << ['File', 'Coverage %', 'Lines Covered', 'Total Lines', 'Stale']
files.each do |f|
csv << [
model.relativize(f)['file'],
f['percentage'],
f['covered'],
f['total'],
f['stale']
]
end
end
HTML Report:
require 'erb'
template = ERB.new(<<~HTML)
<html>
<head><title>Coverage Report</title></head>
<body>
<h1>Coverage Report</h1>
<table>
<tr>
<th>File</th><th>Coverage</th><th>Covered</th><th>Total</th>
</tr>
<% files.each do |f| %>
<tr class="<%= f['percentage'] < 80 ? 'low' : 'ok' %>">
<td><%= f['file'] %></td>
<td><%= f['percentage'].round(2) %>%</td>
<td><%= f['covered'] %></td>
<td><%= f['total'] %></td>
</tr>
<% end %>
</table>
</body>
</html>
HTML
model = CovLoupe::CoverageModel.new
list_result = model.list
relative_list = model.relativize(list_result)
files = relative_list['files']
File.write('coverage.html', template.result(binding))
Annotated Source Output¶
The CLI supports annotated source viewing:
# Show uncovered lines with context
clp uncovered app/models/order.rb \
-s uncovered \
-c 3 # -s = --source, -c = --context-lines
# Show full file with coverage annotations
clp uncovered app/models/order.rb \
-s full \
-c 0
Programmatic Source Annotation:
def annotate_source(file_path)
model = CovLoupe::CoverageModel.new
details = model.detailed_for(file_path)
source_lines = File.readlines(file_path)
output = []
details['lines'].each do |line_data|
line_num = line_data['line']
hits = line_data['hits']
source = source_lines[line_num - 1]
marker = case hits
when nil then ' '
when 0 then ' ✗ '
else " #{hits} "
end
output << "#{marker}#{line_num.to_s.rjust(4)}: #{source}"
end
output.join
end
puts annotate_source('app/models/order.rb')
Integration with Coverage Trackers¶
Send to Codecov:
#!/bin/bash
bundle exec rspec
clp -fJ list > coverage.json
# Transform to Codecov format (example)
jq '{
coverage: [
.files[] | {
name: .file,
coverage: .percentage
}
]
}' coverage.json | curl -X POST \
-H "Authorization: token $CODECOV_TOKEN" \
-d @- https://codecov.io/upload
# Ruby alternative:
ruby -r json -e '
data = JSON.parse(File.read("coverage.json"))
transformed = {
coverage: data["files"].map { |f|
{name: f["file"], coverage: f["percentage"]}
}
}
puts JSON.pretty_generate(transformed)
' | curl -X POST \
-H "Authorization: token $CODECOV_TOKEN" \
-d @- https://codecov.io/upload
# Rexe alternative:
rexe -f coverage.json -oJ '
{
coverage: self["files"].map { |f|
{name: f["file"], coverage: f["percentage"]}
}
}
' | curl -X POST \
-H "Authorization: token $CODECOV_TOKEN" \
-d @- https://codecov.io/upload
Send to Coveralls:
require 'cov_loupe'
require 'net/http'
require 'json'
model = CovLoupe::CoverageModel.new
files = model.list['files']
coveralls_data = {
repo_token: ENV['COVERALLS_REPO_TOKEN'],
source_files: files.map { |f|
{
name: f['file'],
coverage: model.raw_for(f['file'])['lines']
}
}
}
uri = URI('https://coveralls.io/api/v1/jobs')
Net::HTTP.post(uri, coveralls_data.to_json, {
'Content-Type' => 'application/json'
})
Multi-Suite Coverage Merging¶
How It Works¶
When a .resultset.json file contains multiple test suites (e.g., RSpec + Cucumber), cov-loupe automatically merges them using SimpleCov's combine logic. All covered files from every suite become available to the CLI, library, and MCP tools.
Performance: Single-suite projects avoid loading SimpleCov at runtime. Multi-suite resultsets trigger a lazy SimpleCov load only when needed, keeping the tool fast for the simpler coverage configurations.
Current Limitations¶
Staleness checks: When suites are merged, we keep a single "latest suite" timestamp. This matches prior behavior but may under-report stale files if only some suites were re-run after a change. Use --raise-on-stale (or -S) on the CLI, raise_on_stale: true via the Ruby API, or the MCP tool parameter to turn these warnings into hard failures. A per-file timestamp refinement is planned; until then, treat multi-suite staleness flags as advisory rather than definitive.
Multiple resultset files: Only suites stored inside a single .resultset.json are merged automatically. If your project produces separate resultset files (e.g., different CI jobs writing coverage/job1/.resultset.json, coverage/job2/.resultset.json), you must merge them yourself before pointing cov-loupe at the combined file.