cov-loupe 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +329 -0
- data/docs/dev/ARCHITECTURE.md +80 -0
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
- data/docs/dev/DEVELOPMENT.md +83 -0
- data/docs/dev/README.md +10 -0
- data/docs/dev/RELEASING.md +146 -0
- data/docs/dev/arch-decisions/001-x-arch-decision.md +95 -0
- data/docs/dev/arch-decisions/002-x-arch-decision.md +159 -0
- data/docs/dev/arch-decisions/003-x-arch-decision.md +165 -0
- data/docs/dev/arch-decisions/004-x-arch-decision.md +203 -0
- data/docs/dev/arch-decisions/005-x-arch-decision.md +189 -0
- data/docs/dev/arch-decisions/README.md +60 -0
- data/docs/dev/presentations/cov-loupe-presentation.md +255 -0
- data/docs/fixtures/demo_project/README.md +9 -0
- data/docs/user/ADVANCED_USAGE.md +777 -0
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
- data/docs/user/CLI_USAGE.md +750 -0
- data/docs/user/ERROR_HANDLING.md +93 -0
- data/docs/user/EXAMPLES.md +588 -0
- data/docs/user/INSTALLATION.md +130 -0
- data/docs/user/LIBRARY_API.md +693 -0
- data/docs/user/MCP_INTEGRATION.md +490 -0
- data/docs/user/README.md +14 -0
- data/docs/user/TROUBLESHOOTING.md +197 -0
- data/docs/user/V2-BREAKING-CHANGES.md +472 -0
- data/exe/cov-loupe +23 -0
- data/lib/cov_loupe/app_config.rb +56 -0
- data/lib/cov_loupe/app_context.rb +26 -0
- data/lib/cov_loupe/base_tool.rb +102 -0
- data/lib/cov_loupe/cli.rb +178 -0
- data/lib/cov_loupe/commands/base_command.rb +67 -0
- data/lib/cov_loupe/commands/command_factory.rb +45 -0
- data/lib/cov_loupe/commands/detailed_command.rb +38 -0
- data/lib/cov_loupe/commands/list_command.rb +13 -0
- data/lib/cov_loupe/commands/raw_command.rb +38 -0
- data/lib/cov_loupe/commands/summary_command.rb +41 -0
- data/lib/cov_loupe/commands/totals_command.rb +53 -0
- data/lib/cov_loupe/commands/uncovered_command.rb +45 -0
- data/lib/cov_loupe/commands/validate_command.rb +60 -0
- data/lib/cov_loupe/commands/version_command.rb +33 -0
- data/lib/cov_loupe/config_parser.rb +32 -0
- data/lib/cov_loupe/constants.rb +22 -0
- data/lib/cov_loupe/coverage_reporter.rb +31 -0
- data/lib/cov_loupe/error_handler.rb +165 -0
- data/lib/cov_loupe/error_handler_factory.rb +31 -0
- data/lib/cov_loupe/errors.rb +191 -0
- data/lib/cov_loupe/formatters/source_formatter.rb +152 -0
- data/lib/cov_loupe/formatters.rb +51 -0
- data/lib/cov_loupe/mcp_server.rb +42 -0
- data/lib/cov_loupe/mode_detector.rb +56 -0
- data/lib/cov_loupe/model.rb +339 -0
- data/lib/cov_loupe/option_normalizers.rb +113 -0
- data/lib/cov_loupe/option_parser_builder.rb +147 -0
- data/lib/cov_loupe/option_parsers/env_options_parser.rb +48 -0
- data/lib/cov_loupe/option_parsers/error_helper.rb +110 -0
- data/lib/cov_loupe/path_relativizer.rb +64 -0
- data/lib/cov_loupe/predicate_evaluator.rb +72 -0
- data/lib/cov_loupe/presenters/base_coverage_presenter.rb +42 -0
- data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/project_coverage_presenter.rb +50 -0
- data/lib/cov_loupe/presenters/project_totals_presenter.rb +27 -0
- data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +122 -0
- data/lib/cov_loupe/resolvers/resolver_factory.rb +28 -0
- data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +76 -0
- data/lib/cov_loupe/resultset_loader.rb +131 -0
- data/lib/cov_loupe/staleness_checker.rb +247 -0
- data/lib/cov_loupe/table_formatter.rb +64 -0
- data/lib/cov_loupe/tools/all_files_coverage_tool.rb +51 -0
- data/lib/cov_loupe/tools/coverage_detailed_tool.rb +35 -0
- data/lib/cov_loupe/tools/coverage_raw_tool.rb +34 -0
- data/lib/cov_loupe/tools/coverage_summary_tool.rb +34 -0
- data/lib/cov_loupe/tools/coverage_table_tool.rb +50 -0
- data/lib/cov_loupe/tools/coverage_totals_tool.rb +44 -0
- data/lib/cov_loupe/tools/help_tool.rb +115 -0
- data/lib/cov_loupe/tools/uncovered_lines_tool.rb +34 -0
- data/lib/cov_loupe/tools/validate_tool.rb +72 -0
- data/lib/cov_loupe/tools/version_tool.rb +32 -0
- data/lib/cov_loupe/util.rb +88 -0
- data/lib/cov_loupe/version.rb +5 -0
- data/lib/cov_loupe.rb +140 -0
- data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
- data/spec/TIMESTAMPS.md +48 -0
- data/spec/all_files_coverage_tool_spec.rb +53 -0
- data/spec/app_config_spec.rb +142 -0
- data/spec/base_tool_spec.rb +62 -0
- data/spec/cli/show_default_report_spec.rb +33 -0
- data/spec/cli_enumerated_options_spec.rb +90 -0
- data/spec/cli_error_spec.rb +184 -0
- data/spec/cli_format_spec.rb +123 -0
- data/spec/cli_json_options_spec.rb +50 -0
- data/spec/cli_source_spec.rb +44 -0
- data/spec/cli_spec.rb +192 -0
- data/spec/cli_table_spec.rb +28 -0
- data/spec/cli_usage_spec.rb +42 -0
- data/spec/commands/base_command_spec.rb +107 -0
- data/spec/commands/command_factory_spec.rb +76 -0
- data/spec/commands/detailed_command_spec.rb +34 -0
- data/spec/commands/list_command_spec.rb +28 -0
- data/spec/commands/raw_command_spec.rb +69 -0
- data/spec/commands/summary_command_spec.rb +34 -0
- data/spec/commands/totals_command_spec.rb +34 -0
- data/spec/commands/uncovered_command_spec.rb +55 -0
- data/spec/commands/validate_command_spec.rb +213 -0
- data/spec/commands/version_command_spec.rb +38 -0
- data/spec/constants_spec.rb +61 -0
- data/spec/cov_loupe/formatters/source_formatter_spec.rb +267 -0
- data/spec/cov_loupe/formatters_spec.rb +76 -0
- data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +79 -0
- data/spec/cov_loupe_model_spec.rb +454 -0
- data/spec/cov_loupe_module_spec.rb +37 -0
- data/spec/cov_loupe_opts_spec.rb +185 -0
- data/spec/coverage_reporter_spec.rb +102 -0
- data/spec/coverage_table_tool_spec.rb +59 -0
- data/spec/coverage_totals_tool_spec.rb +37 -0
- data/spec/error_handler_spec.rb +197 -0
- data/spec/error_mode_spec.rb +139 -0
- data/spec/errors_edge_cases_spec.rb +312 -0
- data/spec/errors_stale_spec.rb +83 -0
- data/spec/file_based_mcp_tools_spec.rb +99 -0
- data/spec/fixtures/project1/lib/bar.rb +5 -0
- data/spec/fixtures/project1/lib/foo.rb +6 -0
- data/spec/help_tool_spec.rb +26 -0
- data/spec/integration_spec.rb +789 -0
- data/spec/logging_fallback_spec.rb +128 -0
- data/spec/mcp_logging_spec.rb +44 -0
- data/spec/mcp_server_integration_spec.rb +23 -0
- data/spec/mcp_server_spec.rb +106 -0
- data/spec/mode_detector_spec.rb +153 -0
- data/spec/model_error_handling_spec.rb +269 -0
- data/spec/model_staleness_spec.rb +79 -0
- data/spec/option_normalizers_spec.rb +203 -0
- data/spec/option_parsers/env_options_parser_spec.rb +221 -0
- data/spec/option_parsers/error_helper_spec.rb +222 -0
- data/spec/path_relativizer_spec.rb +98 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
- data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
- data/spec/presenters/project_coverage_presenter_spec.rb +87 -0
- data/spec/presenters/project_totals_presenter_spec.rb +144 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +282 -0
- data/spec/resolvers/resolver_factory_spec.rb +61 -0
- data/spec/resolvers/resultset_path_resolver_spec.rb +60 -0
- data/spec/resultset_loader_spec.rb +167 -0
- data/spec/shared_examples/README.md +115 -0
- data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
- data/spec/shared_examples/file_based_mcp_tools.rb +179 -0
- data/spec/shared_examples/formatted_command_examples.rb +64 -0
- data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
- data/spec/spec_helper.rb +127 -0
- data/spec/staleness_checker_spec.rb +374 -0
- data/spec/staleness_more_spec.rb +42 -0
- data/spec/support/cli_helpers.rb +22 -0
- data/spec/support/control_flow_helpers.rb +20 -0
- data/spec/support/fake_mcp.rb +40 -0
- data/spec/support/io_helpers.rb +29 -0
- data/spec/support/mcp_helpers.rb +35 -0
- data/spec/support/mcp_runner.rb +66 -0
- data/spec/support/mocking_helpers.rb +30 -0
- data/spec/table_format_spec.rb +70 -0
- data/spec/tools/validate_tool_spec.rb +132 -0
- data/spec/tools_error_handling_spec.rb +130 -0
- data/spec/util_spec.rb +154 -0
- data/spec/version_spec.rb +123 -0
- data/spec/version_tool_spec.rb +141 -0
- metadata +290 -0
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
# Advanced Usage Guide
|
|
2
|
+
|
|
3
|
+
[Back to main README](../README.md)
|
|
4
|
+
|
|
5
|
+
> Examples use `clp`, an alias pointed at the demo fixture with partial coverage:
|
|
6
|
+
> `alias clp='cov-loupe --root docs/fixtures/demo_project'`
|
|
7
|
+
> Swap `clp` with `cov-loupe` if you want to target your own project/resultset.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Advanced MCP Integration](#advanced-mcp-integration)
|
|
12
|
+
- [Staleness Detection & Validation](#staleness-detection--validation)
|
|
13
|
+
- [Advanced Path Resolution](#advanced-path-resolution)
|
|
14
|
+
- [Error Handling Strategies](#error-handling-strategies)
|
|
15
|
+
- [Custom Ruby Integration](#custom-ruby-integration)
|
|
16
|
+
- [CI/CD Integration Patterns](#cicd-integration-patterns)
|
|
17
|
+
- [Advanced Filtering & Glob Patterns](#advanced-filtering--glob-patterns)
|
|
18
|
+
- [Performance Optimization](#performance-optimization)
|
|
19
|
+
- [Custom Output Processing](#custom-output-processing)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Advanced MCP Integration
|
|
24
|
+
|
|
25
|
+
### MCP Error Handling
|
|
26
|
+
|
|
27
|
+
The MCP server uses structured error responses:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"jsonrpc": "2.0",
|
|
32
|
+
"error": {
|
|
33
|
+
"code": -32603,
|
|
34
|
+
"message": "Coverage data not found at coverage/.resultset.json",
|
|
35
|
+
"data": {
|
|
36
|
+
"type": "FileError",
|
|
37
|
+
"context": "MCP tool execution"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"id": 1
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### MCP Server Logging
|
|
45
|
+
|
|
46
|
+
The MCP server logs to `cov_loupe.log` in the current directory by default.
|
|
47
|
+
|
|
48
|
+
To override the default log file location, specify the `--log-file` argument wherever and however you configure your MCP server. For example, to log to a different file path, include `--log-file /path/to/logfile.log` in your server configuration. To log to standard error, use `--log-file stderr`.
|
|
49
|
+
|
|
50
|
+
**Note:** Logging to `stdout` is not permitted in MCP mode.
|
|
51
|
+
|
|
52
|
+
### Testing MCP Server Manually
|
|
53
|
+
|
|
54
|
+
Use JSON-RPC over stdin to test the MCP server:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
# Get version
|
|
58
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"version_tool","arguments":{}}}' | clp
|
|
59
|
+
|
|
60
|
+
# Get file summary
|
|
61
|
+
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"coverage_summary_tool","arguments":{"path":"app/models/order.rb"}}}' | clp
|
|
62
|
+
|
|
63
|
+
# List all files with sorting
|
|
64
|
+
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"all_files_coverage_tool","arguments":{"sort_order":"ascending"}}}' | clp
|
|
65
|
+
|
|
66
|
+
# Get uncovered lines
|
|
67
|
+
echo '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"uncovered_lines_tool","arguments":{"path":"app/controllers/orders_controller.rb"}}}' | clp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Staleness Detection & Validation
|
|
73
|
+
|
|
74
|
+
### Understanding Staleness Modes
|
|
75
|
+
|
|
76
|
+
Staleness checking prevents using outdated coverage data. Two modes are available:
|
|
77
|
+
|
|
78
|
+
**Mode: `off` (default)**
|
|
79
|
+
- No validation, fastest operation
|
|
80
|
+
- Coverage data used as-is
|
|
81
|
+
- Stale indicators still computed but don't block operations
|
|
82
|
+
|
|
83
|
+
**Mode: `error`**
|
|
84
|
+
- Strict validation enabled
|
|
85
|
+
- Raises errors if coverage is outdated
|
|
86
|
+
- Perfect for CI/CD pipelines
|
|
87
|
+
|
|
88
|
+
### File-Level Staleness
|
|
89
|
+
|
|
90
|
+
A file is considered stale when any of the following are true:
|
|
91
|
+
1. Source file modified after coverage generation
|
|
92
|
+
2. Line count differs from coverage array length
|
|
93
|
+
3. File exists in coverage but deleted from filesystem
|
|
94
|
+
|
|
95
|
+
**CLI Usage:**
|
|
96
|
+
```sh
|
|
97
|
+
# Fail if any file is stale (option before subcommand)
|
|
98
|
+
clp --staleness error summary app/models/order.rb
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Ruby API:**
|
|
102
|
+
```ruby
|
|
103
|
+
model = CovLoupe::CoverageModel.new(
|
|
104
|
+
staleness: 'error'
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
begin
|
|
108
|
+
summary = model.summary_for('app/models/order.rb')
|
|
109
|
+
rescue CovLoupe::CoverageDataStaleError => e
|
|
110
|
+
puts "File modified after coverage: #{e.file_path}"
|
|
111
|
+
puts "Coverage timestamp: #{e.cov_timestamp}"
|
|
112
|
+
puts "File mtime: #{e.file_mtime}"
|
|
113
|
+
puts "Source lines: #{e.src_len}, Coverage lines: #{e.cov_len}"
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Project-Level Staleness
|
|
118
|
+
|
|
119
|
+
Detects system-wide staleness issues:
|
|
120
|
+
|
|
121
|
+
**Conditions Checked:**
|
|
122
|
+
1. **Newer files** - Any tracked file modified after coverage
|
|
123
|
+
2. **Missing files** - Tracked files with no coverage data
|
|
124
|
+
3. **Deleted files** - Coverage exists for non-existent files
|
|
125
|
+
|
|
126
|
+
**CLI Usage:**
|
|
127
|
+
```sh
|
|
128
|
+
# Track specific patterns
|
|
129
|
+
clp --staleness error \
|
|
130
|
+
-g "lib/payments/**/*.rb" \
|
|
131
|
+
-g "lib/ops/jobs/**/*.rb" # -g = --tracked-globs
|
|
132
|
+
|
|
133
|
+
# Combine with JSON output for parsing
|
|
134
|
+
clp --staleness error -fJ list > stale-check.json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Ruby API:**
|
|
138
|
+
```ruby
|
|
139
|
+
model = CovLoupe::CoverageModel.new(
|
|
140
|
+
staleness: 'error',
|
|
141
|
+
tracked_globs: ['lib/payments/**/*.rb', 'lib/ops/jobs/**/*.rb']
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
begin
|
|
145
|
+
files = model.all_files(check_stale: true)
|
|
146
|
+
rescue CovLoupe::CoverageDataProjectStaleError => e
|
|
147
|
+
puts "Newer files: #{e.newer_files.join(', ')}"
|
|
148
|
+
puts "Missing from coverage: #{e.missing_files.join(', ')}"
|
|
149
|
+
puts "Deleted but in coverage: #{e.deleted_files.join(', ')}"
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Staleness in CI/CD
|
|
154
|
+
|
|
155
|
+
Staleness checking is particularly useful in CI/CD pipelines to ensure coverage data is fresh:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
# Run tests to generate coverage
|
|
159
|
+
bundle exec rspec
|
|
160
|
+
|
|
161
|
+
# Validate coverage freshness (fails with exit code 1 if stale)
|
|
162
|
+
clp --staleness error -g "lib/**/*.rb"
|
|
163
|
+
|
|
164
|
+
# Export validated data for CI artifacts
|
|
165
|
+
clp -fJ list > coverage.json
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The `--staleness error` flag causes the command to exit with a non-zero status when coverage is outdated, making it suitable for pipeline failure conditions.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Advanced Path Resolution
|
|
173
|
+
|
|
174
|
+
### Multi-Strategy Path Matching
|
|
175
|
+
|
|
176
|
+
Path resolution order:
|
|
177
|
+
|
|
178
|
+
1. **Exact absolute path match**
|
|
179
|
+
2. **Relative path resolution from root**
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
model = CovLoupe::CoverageModel.new(root: '/project')
|
|
183
|
+
|
|
184
|
+
model.summary_for('/project/app/models/order.rb') # Absolute
|
|
185
|
+
model.summary_for('app/models/order.rb') # Relative
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Working with Multiple Projects
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# Project A
|
|
192
|
+
model_a = CovLoupe::CoverageModel.new(
|
|
193
|
+
root: '/projects/service-a',
|
|
194
|
+
resultset: '/projects/service-a/coverage/.resultset.json'
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Project B
|
|
198
|
+
model_b = CovLoupe::CoverageModel.new(
|
|
199
|
+
root: '/projects/service-b',
|
|
200
|
+
resultset: '/projects/service-b/tmp/coverage/.resultset.json'
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Compare coverage
|
|
204
|
+
coverage_a = model_a.all_files
|
|
205
|
+
coverage_b = model_b.all_files
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Error Handling Strategies
|
|
214
|
+
|
|
215
|
+
### Context-Aware Error Handling
|
|
216
|
+
|
|
217
|
+
**CLI Mode:** user-facing messages, exit codes, optional debug mode
|
|
218
|
+
|
|
219
|
+
**Library Mode:** typed exceptions with full details
|
|
220
|
+
|
|
221
|
+
**MCP Server Mode:** JSON-RPC errors logged to file with structured data
|
|
222
|
+
|
|
223
|
+
### Error Modes
|
|
224
|
+
|
|
225
|
+
**CLI Error Modes:**
|
|
226
|
+
```sh
|
|
227
|
+
# Silent mode - minimal output
|
|
228
|
+
clp --error-mode off summary app/models/order.rb
|
|
229
|
+
|
|
230
|
+
# Standard mode - user-friendly errors (default)
|
|
231
|
+
clp --error-mode log summary app/models/order.rb
|
|
232
|
+
|
|
233
|
+
# Verbose mode - full stack traces
|
|
234
|
+
clp --error-mode debug summary app/models/order.rb
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Ruby API Error Handling:**
|
|
238
|
+
```ruby
|
|
239
|
+
require 'cov_loupe'
|
|
240
|
+
|
|
241
|
+
begin
|
|
242
|
+
model = CovLoupe::CoverageModel.new(
|
|
243
|
+
root: '/project',
|
|
244
|
+
resultset: '/nonexistent/.resultset.json'
|
|
245
|
+
)
|
|
246
|
+
rescue CovLoupe::FileError => e
|
|
247
|
+
# Handle missing resultset
|
|
248
|
+
puts "Coverage file not found: #{e.message}"
|
|
249
|
+
rescue CovLoupe::CoverageDataError => e
|
|
250
|
+
# Handle corrupt/invalid coverage data
|
|
251
|
+
puts "Invalid coverage data: #{e.message}"
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Custom Error Handlers
|
|
256
|
+
|
|
257
|
+
Provide custom error handlers when embedding the CLI:
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
class CustomErrorHandler
|
|
261
|
+
def handle_error(error, context: nil)
|
|
262
|
+
# Log to custom service
|
|
263
|
+
ErrorTracker.notify(error, context: context)
|
|
264
|
+
|
|
265
|
+
# Re-raise or handle gracefully
|
|
266
|
+
raise error
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
cli = CovLoupe::CoverageCLI.new(error_handler: CustomErrorHandler.new)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Custom Ruby Integration
|
|
276
|
+
|
|
277
|
+
### Building Custom Coverage Policies
|
|
278
|
+
|
|
279
|
+
Use the `validate` subcommand to enforce custom coverage policies in CI/CD. Example predicates are in [`examples/success_predicates/`](../../examples/success_predicates/).
|
|
280
|
+
|
|
281
|
+
> **⚠️ SECURITY WARNING**
|
|
282
|
+
>
|
|
283
|
+
> Success predicates execute as **arbitrary Ruby code with full system privileges**. They have unrestricted access to:
|
|
284
|
+
> - File system operations (read, write, delete)
|
|
285
|
+
> - Network operations (HTTP requests, sockets)
|
|
286
|
+
> - System commands (via backticks, `system()`, `exec()`, etc.)
|
|
287
|
+
> - Environment variables and sensitive data
|
|
288
|
+
>
|
|
289
|
+
> **Only use predicate files from trusted sources.** Treat them like any other executable code in your project.
|
|
290
|
+
> - Never use predicates from untrusted or unknown sources
|
|
291
|
+
> - Review predicates before use, especially in CI/CD environments
|
|
292
|
+
> - Store predicates in version control with code review
|
|
293
|
+
> - Be cautious when copying examples from the internet
|
|
294
|
+
|
|
295
|
+
**Quick Usage:**
|
|
296
|
+
```sh
|
|
297
|
+
# All files must be >= 80%
|
|
298
|
+
clp validate examples/success_predicates/all_files_above_threshold_predicate.rb
|
|
299
|
+
|
|
300
|
+
# Total project coverage >= 85%
|
|
301
|
+
clp validate examples/success_predicates/project_coverage_minimum_predicate.rb
|
|
302
|
+
|
|
303
|
+
# Custom predicate from file
|
|
304
|
+
clp validate coverage_policy.rb
|
|
305
|
+
|
|
306
|
+
# Inline string mode
|
|
307
|
+
clp validate -i '->(m) { m.all_files.all? { |f| f["percentage"] >= 80 } }'
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Creating a predicate:**
|
|
311
|
+
```ruby
|
|
312
|
+
# coverage_policy.rb
|
|
313
|
+
->(model) do
|
|
314
|
+
# All files must have >= 80% coverage
|
|
315
|
+
model.all_files.all? { |f| f['percentage'] >= 80 }
|
|
316
|
+
end
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Advanced predicate with reporting:**
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
# coverage_policy.rb
|
|
323
|
+
class CoveragePolicy
|
|
324
|
+
def call(model)
|
|
325
|
+
threshold = 80
|
|
326
|
+
low_files = model.all_files.select { |f| f['percentage'] < threshold }
|
|
327
|
+
|
|
328
|
+
if low_files.empty?
|
|
329
|
+
puts "✓ All files have >= #{threshold}% coverage"
|
|
330
|
+
true
|
|
331
|
+
else
|
|
332
|
+
warn "✗ Files below #{threshold}%:"
|
|
333
|
+
low_files.each { |f| warn " #{f['file']}: #{f['percentage']}%" }
|
|
334
|
+
false
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
CoveragePolicy.new
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Exit codes:**
|
|
343
|
+
- `0` - Predicate returned truthy (pass)
|
|
344
|
+
- `1` - Predicate returned falsy (fail)
|
|
345
|
+
- `2` - Predicate raised an error
|
|
346
|
+
|
|
347
|
+
See [examples/success_predicates/README.md](../../examples/success_predicates/README.md) for more examples.
|
|
348
|
+
|
|
349
|
+
### Path Relativization
|
|
350
|
+
|
|
351
|
+
Convert absolute paths to relative for cleaner output:
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
model = CovLoupe::CoverageModel.new(root: '/project')
|
|
355
|
+
|
|
356
|
+
# Get data with absolute paths
|
|
357
|
+
data = model.summary_for('app/models/order.rb')
|
|
358
|
+
# => { 'file' => '/project/app/models/order.rb', ... }
|
|
359
|
+
|
|
360
|
+
# Relativize paths
|
|
361
|
+
relative_data = model.relativize(data)
|
|
362
|
+
# => { 'file' => 'app/models/order.rb', ... }
|
|
363
|
+
|
|
364
|
+
# Works with arrays too
|
|
365
|
+
files = model.all_files
|
|
366
|
+
relative_files = model.relativize(files)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## CI/CD Integration
|
|
372
|
+
|
|
373
|
+
The CLI is designed for CI/CD use with features that integrate naturally into pipeline workflows:
|
|
374
|
+
|
|
375
|
+
### Key Integration Features
|
|
376
|
+
|
|
377
|
+
- **Exit codes**: Non-zero on failure, making it suitable for pipeline failure conditions
|
|
378
|
+
- **JSON output**: `-fJ` format for parsing by CI tools and custom processing
|
|
379
|
+
- **Staleness checking**: `--stale error` to fail on outdated coverage data
|
|
380
|
+
- **Success predicates**: Custom Ruby policies for coverage enforcement
|
|
381
|
+
|
|
382
|
+
### Basic CI Pattern
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# 1. Run tests to generate coverage
|
|
386
|
+
bundle exec rspec
|
|
387
|
+
|
|
388
|
+
# 2. Validate coverage freshness (fails with exit code 1 if stale)
|
|
389
|
+
clp --staleness error -g "lib/**/*.rb"
|
|
390
|
+
|
|
391
|
+
# 3. Export data for CI artifacts or further processing
|
|
392
|
+
clp -fJ list > coverage.json
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Using Coverage Validation
|
|
396
|
+
|
|
397
|
+
Enforce custom coverage policies with the `validate` subcommand:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
# Run tests
|
|
401
|
+
bundle exec rspec
|
|
402
|
+
|
|
403
|
+
# Apply coverage policy (fails with exit code 1 if predicate returns false)
|
|
404
|
+
clp validate coverage_policy.rb
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Exit codes:
|
|
408
|
+
- `0` - Success (coverage meets requirements)
|
|
409
|
+
- `1` - Failure (coverage policy not met or stale data detected)
|
|
410
|
+
- `2` - Error (invalid predicate or system error)
|
|
411
|
+
|
|
412
|
+
### Platform-Specific Examples
|
|
413
|
+
|
|
414
|
+
For platform-specific integration examples (GitHub Actions, GitLab CI, Jenkins, CircleCI, etc.), see community contributions in the [GitHub Discussions](https://github.com/keithrbennett/cov-loupe/discussions).
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Advanced Filtering & Glob Patterns
|
|
419
|
+
|
|
420
|
+
### Tracked Globs Overview
|
|
421
|
+
|
|
422
|
+
Tracked globs serve two purposes:
|
|
423
|
+
1. **Filter output** - Only show matching files
|
|
424
|
+
2. **Validate coverage** - Ensure new files have coverage
|
|
425
|
+
|
|
426
|
+
### Pattern Syntax
|
|
427
|
+
|
|
428
|
+
Uses Ruby's `File.fnmatch` with extended glob support:
|
|
429
|
+
|
|
430
|
+
```sh
|
|
431
|
+
# Single directory
|
|
432
|
+
--tracked-globs "lib/**/*.rb"
|
|
433
|
+
|
|
434
|
+
# Multiple patterns
|
|
435
|
+
--tracked-globs "lib/payments/**/*.rb" --tracked-globs "lib/ops/jobs/**/*.rb"
|
|
436
|
+
|
|
437
|
+
# Exclude patterns (use CLI filtering)
|
|
438
|
+
clp -fJ list | jq '.files[] | select(.file | test("spec") | not)'
|
|
439
|
+
|
|
440
|
+
# Ruby alternative:
|
|
441
|
+
clp -fJ list | ruby -r json -e '
|
|
442
|
+
JSON.parse($stdin.read)["files"].reject { |f| f["file"].include?("spec") }.each do |f|
|
|
443
|
+
puts JSON.pretty_generate(f)
|
|
444
|
+
end
|
|
445
|
+
'
|
|
446
|
+
|
|
447
|
+
# Rexe alternative:
|
|
448
|
+
clp -fJ list | rexe -ij -mb -oJ 'self["files"].reject { |f| f["file"].include?("spec") }'
|
|
449
|
+
|
|
450
|
+
# Complex patterns
|
|
451
|
+
--tracked-globs "lib/{models,controllers}/**/*.rb"
|
|
452
|
+
--tracked-globs "app/**/concerns/*.rb"
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Use Cases
|
|
456
|
+
|
|
457
|
+
**1. Monitor Subsystem Coverage:**
|
|
458
|
+
```sh
|
|
459
|
+
# API layer only
|
|
460
|
+
clp -g "lib/api/**/*.rb" list
|
|
461
|
+
|
|
462
|
+
# Core business logic
|
|
463
|
+
clp -g "lib/domain/**/*.rb" list
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**2. Ensure New Files Have Coverage:**
|
|
467
|
+
```sh
|
|
468
|
+
# Fail if any tracked file lacks coverage
|
|
469
|
+
clp --staleness error -g "lib/features/**/*.rb"
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**3. Multi-tier Reporting:**
|
|
473
|
+
```sh
|
|
474
|
+
# Generate separate reports per layer
|
|
475
|
+
for layer in models views controllers; do
|
|
476
|
+
clp -g "app/${layer}/**/*.rb" -fJ list > "coverage-${layer}.json"
|
|
477
|
+
done
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Ruby API with Globs
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
model = CovLoupe::CoverageModel.new
|
|
484
|
+
|
|
485
|
+
# Filter files in output
|
|
486
|
+
api_files = model.all_files(
|
|
487
|
+
tracked_globs: ['lib/api/**/*.rb']
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Multi-pattern filtering
|
|
491
|
+
core_files = model.all_files(
|
|
492
|
+
tracked_globs: [
|
|
493
|
+
'lib/core/**/*.rb',
|
|
494
|
+
'lib/domain/**/*.rb'
|
|
495
|
+
]
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Validate specific subsystems
|
|
499
|
+
begin
|
|
500
|
+
model.all_files(
|
|
501
|
+
check_stale: true,
|
|
502
|
+
tracked_globs: ['lib/critical/**/*.rb']
|
|
503
|
+
)
|
|
504
|
+
rescue CovLoupe::CoverageDataProjectStaleError => e
|
|
505
|
+
# Handle missing coverage for critical files
|
|
506
|
+
puts "Critical files missing coverage:"
|
|
507
|
+
e.missing_files.each { |f| puts " - #{f}" }
|
|
508
|
+
end
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## Performance Optimization
|
|
514
|
+
|
|
515
|
+
### Minimizing Coverage Reads
|
|
516
|
+
|
|
517
|
+
The `CoverageModel` reads `.resultset.json` once at initialization:
|
|
518
|
+
|
|
519
|
+
```ruby
|
|
520
|
+
# Good: Single model for multiple queries
|
|
521
|
+
model = CovLoupe::CoverageModel.new
|
|
522
|
+
files = model.all_files
|
|
523
|
+
file1 = model.summary_for('lib/a.rb')
|
|
524
|
+
file2 = model.summary_for('lib/b.rb')
|
|
525
|
+
|
|
526
|
+
# Bad: Re-reads coverage for each operation
|
|
527
|
+
model1 = CovLoupe::CoverageModel.new
|
|
528
|
+
files = model1.all_files
|
|
529
|
+
|
|
530
|
+
model2 = CovLoupe::CoverageModel.new
|
|
531
|
+
file1 = model2.summary_for('lib/a.rb')
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Batch Processing
|
|
535
|
+
|
|
536
|
+
```ruby
|
|
537
|
+
# Process multiple files in one pass
|
|
538
|
+
files_to_analyze = ['lib/a.rb', 'lib/b.rb', 'lib/c.rb']
|
|
539
|
+
model = CovLoupe::CoverageModel.new
|
|
540
|
+
|
|
541
|
+
results = files_to_analyze.each_with_object({}) do |file, hash|
|
|
542
|
+
hash[file] = {
|
|
543
|
+
summary: model.summary_for(file),
|
|
544
|
+
uncovered: model.uncovered_for(file)
|
|
545
|
+
}
|
|
546
|
+
rescue CovLoupe::FileError
|
|
547
|
+
hash[file] = { error: 'No coverage' }
|
|
548
|
+
end
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Filtering Early
|
|
552
|
+
|
|
553
|
+
Use `tracked_globs` to reduce data processing:
|
|
554
|
+
|
|
555
|
+
```ruby
|
|
556
|
+
# Bad: Filter after loading all data
|
|
557
|
+
all_files = model.all_files
|
|
558
|
+
api_files = all_files.select { |f| f['file'].include?('api') }
|
|
559
|
+
|
|
560
|
+
# Good: Filter during query
|
|
561
|
+
api_files = model.all_files(
|
|
562
|
+
tracked_globs: ['lib/api/**/*.rb']
|
|
563
|
+
)
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Caching Coverage Models
|
|
567
|
+
|
|
568
|
+
For long-running processes:
|
|
569
|
+
|
|
570
|
+
```ruby
|
|
571
|
+
class CoverageCache
|
|
572
|
+
def initialize(ttl: 300) # 5 minute cache
|
|
573
|
+
@cache = {}
|
|
574
|
+
@ttl = ttl
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def model_for(root)
|
|
578
|
+
key = root.to_s
|
|
579
|
+
now = Time.now
|
|
580
|
+
|
|
581
|
+
if @cache[key] && (now - @cache[key][:time] < @ttl)
|
|
582
|
+
@cache[key][:model]
|
|
583
|
+
else
|
|
584
|
+
@cache[key] = {
|
|
585
|
+
model: CovLoupe::CoverageModel.new(root: root),
|
|
586
|
+
time: now
|
|
587
|
+
}
|
|
588
|
+
@cache[key][:model]
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
cache = CoverageCache.new
|
|
594
|
+
model = cache.model_for('/project')
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## Custom Output Processing
|
|
600
|
+
|
|
601
|
+
### Format Conversion
|
|
602
|
+
|
|
603
|
+
**CSV Export:**
|
|
604
|
+
```ruby
|
|
605
|
+
require 'csv'
|
|
606
|
+
|
|
607
|
+
model = CovLoupe::CoverageModel.new
|
|
608
|
+
files = model.all_files
|
|
609
|
+
|
|
610
|
+
CSV.open('coverage.csv', 'w') do |csv|
|
|
611
|
+
csv << ['File', 'Coverage %', 'Lines Covered', 'Total Lines', 'Stale']
|
|
612
|
+
files.each do |f|
|
|
613
|
+
csv << [
|
|
614
|
+
model.relativize(f)['file'],
|
|
615
|
+
f['percentage'],
|
|
616
|
+
f['covered'],
|
|
617
|
+
f['total'],
|
|
618
|
+
f['stale']
|
|
619
|
+
]
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**HTML Report:**
|
|
625
|
+
```ruby
|
|
626
|
+
require 'erb'
|
|
627
|
+
|
|
628
|
+
template = ERB.new(<<~HTML)
|
|
629
|
+
<html>
|
|
630
|
+
<head><title>Coverage Report</title></head>
|
|
631
|
+
<body>
|
|
632
|
+
<h1>Coverage Report</h1>
|
|
633
|
+
<table>
|
|
634
|
+
<tr>
|
|
635
|
+
<th>File</th><th>Coverage</th><th>Covered</th><th>Total</th>
|
|
636
|
+
</tr>
|
|
637
|
+
<% files.each do |f| %>
|
|
638
|
+
<tr class="<%= f['percentage'] < 80 ? 'low' : 'ok' %>">
|
|
639
|
+
<td><%= f['file'] %></td>
|
|
640
|
+
<td><%= f['percentage'].round(2) %>%</td>
|
|
641
|
+
<td><%= f['covered'] %></td>
|
|
642
|
+
<td><%= f['total'] %></td>
|
|
643
|
+
</tr>
|
|
644
|
+
<% end %>
|
|
645
|
+
</table>
|
|
646
|
+
</body>
|
|
647
|
+
</html>
|
|
648
|
+
HTML
|
|
649
|
+
|
|
650
|
+
model = CovLoupe::CoverageModel.new
|
|
651
|
+
files = model.relativize(model.all_files)
|
|
652
|
+
File.write('coverage.html', template.result(binding))
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Annotated Source Output
|
|
656
|
+
|
|
657
|
+
The CLI supports annotated source viewing:
|
|
658
|
+
|
|
659
|
+
```sh
|
|
660
|
+
# Show uncovered lines with context
|
|
661
|
+
clp uncovered app/models/order.rb \
|
|
662
|
+
--source uncovered \
|
|
663
|
+
--source-context 3
|
|
664
|
+
|
|
665
|
+
# Show full file with coverage annotations
|
|
666
|
+
clp uncovered app/models/order.rb \
|
|
667
|
+
--source full \
|
|
668
|
+
--source-context 0
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**Programmatic Source Annotation:**
|
|
672
|
+
```ruby
|
|
673
|
+
def annotate_source(file_path)
|
|
674
|
+
model = CovLoupe::CoverageModel.new
|
|
675
|
+
details = model.detailed_for(file_path)
|
|
676
|
+
source_lines = File.readlines(file_path)
|
|
677
|
+
|
|
678
|
+
output = []
|
|
679
|
+
details['lines'].each do |line_data|
|
|
680
|
+
line_num = line_data['line']
|
|
681
|
+
hits = line_data['hits']
|
|
682
|
+
source = source_lines[line_num - 1]
|
|
683
|
+
|
|
684
|
+
marker = case hits
|
|
685
|
+
when nil then ' '
|
|
686
|
+
when 0 then ' ✗ '
|
|
687
|
+
else " #{hits} "
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
output << "#{marker}#{line_num.to_s.rjust(4)}: #{source}"
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
output.join
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
puts annotate_source('app/models/order.rb')
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Integration with Coverage Trackers
|
|
700
|
+
|
|
701
|
+
**Send to Codecov:**
|
|
702
|
+
```sh
|
|
703
|
+
#!/bin/bash
|
|
704
|
+
bundle exec rspec
|
|
705
|
+
clp -fJ list > coverage.json
|
|
706
|
+
|
|
707
|
+
# Transform to Codecov format (example)
|
|
708
|
+
jq '{
|
|
709
|
+
coverage: [
|
|
710
|
+
.files[] | {
|
|
711
|
+
name: .file,
|
|
712
|
+
coverage: .percentage
|
|
713
|
+
}
|
|
714
|
+
]
|
|
715
|
+
}' coverage.json | curl -X POST \
|
|
716
|
+
-H "Authorization: token $CODECOV_TOKEN" \
|
|
717
|
+
-d @- https://codecov.io/upload
|
|
718
|
+
|
|
719
|
+
# Ruby alternative:
|
|
720
|
+
ruby -r json -e '
|
|
721
|
+
data = JSON.parse(File.read("coverage.json"))
|
|
722
|
+
transformed = {
|
|
723
|
+
coverage: data["files"].map { |f|
|
|
724
|
+
{name: f["file"], coverage: f["percentage"]}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
puts JSON.pretty_generate(transformed)
|
|
728
|
+
' | curl -X POST \
|
|
729
|
+
-H "Authorization: token $CODECOV_TOKEN" \
|
|
730
|
+
-d @- https://codecov.io/upload
|
|
731
|
+
|
|
732
|
+
# Rexe alternative:
|
|
733
|
+
rexe -f coverage.json -oJ '
|
|
734
|
+
{
|
|
735
|
+
coverage: self["files"].map { |f|
|
|
736
|
+
{name: f["file"], coverage: f["percentage"]}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
' | curl -X POST \
|
|
740
|
+
-H "Authorization: token $CODECOV_TOKEN" \
|
|
741
|
+
-d @- https://codecov.io/upload
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
**Send to Coveralls:**
|
|
745
|
+
```ruby
|
|
746
|
+
require 'cov_loupe'
|
|
747
|
+
require 'net/http'
|
|
748
|
+
require 'json'
|
|
749
|
+
|
|
750
|
+
model = CovLoupe::CoverageModel.new
|
|
751
|
+
files = model.all_files
|
|
752
|
+
|
|
753
|
+
coveralls_data = {
|
|
754
|
+
repo_token: ENV['COVERALLS_REPO_TOKEN'],
|
|
755
|
+
source_files: files.map { |f|
|
|
756
|
+
{
|
|
757
|
+
name: f['file'],
|
|
758
|
+
coverage: model.raw_for(f['file'])['lines']
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
uri = URI('https://coveralls.io/api/v1/jobs')
|
|
764
|
+
Net::HTTP.post(uri, coveralls_data.to_json, {
|
|
765
|
+
'Content-Type' => 'application/json'
|
|
766
|
+
})
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
---
|
|
770
|
+
|
|
771
|
+
## Additional Resources
|
|
772
|
+
|
|
773
|
+
- [CLI Usage Guide](CLI_USAGE.md)
|
|
774
|
+
- [Library API Reference](LIBRARY_API.md)
|
|
775
|
+
- [MCP Integration Guide](MCP_INTEGRATION.md)
|
|
776
|
+
- [Error Handling Details](ERROR_HANDLING.md)
|
|
777
|
+
- [Troubleshooting](TROUBLESHOOTING.md)
|