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 = --root_dir)Replace
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
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.
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 like -R do NOT affect MCP tool calls—you must pass configuration parameters (like root) in each JSON request.
# Get version (no parameters needed)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"version_tool","arguments":{}}}' | cov-loupe
# 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
# 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
# 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
Why not use clp alias here? The clp alias (cov-loupe -R docs/fixtures/demo_project) sets the root for CLI subcommands, but the -R flag is ignored in MCP mode. Instead, pass root explicitly in the JSON arguments for each tool call.
Staleness Detection & Validation¶
Understanding Staleness Modes¶
Staleness checking prevents using outdated coverage data. Two modes are available:
Mode: off (default) - No validation, fastest operation - Coverage data used as-is - Stale indicators still computed but don't block operations
Mode: error - Strict validation enabled - Raises errors if coverage is outdated - Perfect for CI/CD pipelines
File-Level Staleness¶
A file is considered stale when any of the following are true: 1. Source file modified after coverage generation 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 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
Advanced Path Resolution¶
Multi-Strategy Path Matching¶
Path resolution order:
- Exact absolute path match
- Relative path resolution from root
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.all? { |f| f["percentage"] >= 80 } }'
Creating a predicate:
# coverage_policy.rb
->(model) do
# All files must have >= 80% coverage
model.list.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 arrays too
files = model.list
relative_files = model.relativize(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:
--stale errorto 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¶
Tracked globs serve two purposes: 1. Filter output - Only show matching files 2. Validate coverage - Ensure new files have coverage
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']
)
# Multi-pattern filtering
core_files = model.list(
tracked_globs: [
'lib/core/**/*.rb',
'lib/domain/**/*.rb'
]
)
# Validate specific subsystems
begin
model.list(
check_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
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
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
api_files = list.select { |f| f['file'].include?('api') }
# Good: Filter during query
api_files = model.list(
tracked_globs: ['lib/api/**/*.rb']
)
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
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
files = model.relativize(model.list)
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
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'
})