simplecov-mcp 0.3.0 → 1.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 +4 -4
- data/LICENSE +21 -0
- data/README.md +173 -356
- data/docs/ADVANCED_USAGE.md +967 -0
- data/docs/ARCHITECTURE.md +79 -0
- data/docs/BRANCH_ONLY_COVERAGE.md +81 -0
- data/docs/CLI_USAGE.md +637 -0
- data/docs/DEVELOPMENT.md +82 -0
- data/docs/ERROR_HANDLING.md +93 -0
- data/docs/EXAMPLES.md +430 -0
- data/docs/INSTALLATION.md +352 -0
- data/docs/LIBRARY_API.md +635 -0
- data/docs/MCP_INTEGRATION.md +488 -0
- data/docs/TROUBLESHOOTING.md +276 -0
- data/docs/arch-decisions/001-x-arch-decision.md +93 -0
- data/docs/arch-decisions/002-x-arch-decision.md +157 -0
- data/docs/arch-decisions/003-x-arch-decision.md +163 -0
- data/docs/arch-decisions/004-x-arch-decision.md +199 -0
- data/docs/arch-decisions/005-x-arch-decision.md +187 -0
- data/docs/arch-decisions/README.md +60 -0
- data/docs/presentations/simplecov-mcp-presentation.md +249 -0
- data/exe/simplecov-mcp +4 -4
- data/lib/simplecov_mcp/app_context.rb +26 -0
- data/lib/simplecov_mcp/base_tool.rb +74 -0
- data/lib/simplecov_mcp/cli.rb +234 -0
- data/lib/simplecov_mcp/cli_config.rb +56 -0
- data/lib/simplecov_mcp/commands/base_command.rb +78 -0
- data/lib/simplecov_mcp/commands/command_factory.rb +39 -0
- data/lib/simplecov_mcp/commands/detailed_command.rb +24 -0
- data/lib/simplecov_mcp/commands/list_command.rb +13 -0
- data/lib/simplecov_mcp/commands/raw_command.rb +22 -0
- data/lib/simplecov_mcp/commands/summary_command.rb +24 -0
- data/lib/simplecov_mcp/commands/uncovered_command.rb +26 -0
- data/lib/simplecov_mcp/commands/version_command.rb +18 -0
- data/lib/simplecov_mcp/constants.rb +22 -0
- data/lib/simplecov_mcp/error_handler.rb +124 -0
- data/lib/simplecov_mcp/error_handler_factory.rb +31 -0
- data/lib/simplecov_mcp/errors.rb +179 -0
- data/lib/simplecov_mcp/formatters/source_formatter.rb +148 -0
- data/lib/simplecov_mcp/mcp_server.rb +40 -0
- data/lib/simplecov_mcp/mode_detector.rb +55 -0
- data/lib/simplecov_mcp/model.rb +300 -0
- data/lib/simplecov_mcp/option_normalizers.rb +92 -0
- data/lib/simplecov_mcp/option_parser_builder.rb +134 -0
- data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +50 -0
- data/lib/simplecov_mcp/option_parsers/error_helper.rb +109 -0
- data/lib/simplecov_mcp/path_relativizer.rb +61 -0
- data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +44 -0
- data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +52 -0
- data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +126 -0
- data/lib/simplecov_mcp/resolvers/resolver_factory.rb +28 -0
- data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +78 -0
- data/lib/simplecov_mcp/resultset_loader.rb +136 -0
- data/lib/simplecov_mcp/staleness_checker.rb +243 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/all_files_coverage_tool.rb +31 -13
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_detailed_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_raw_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_summary_tool.rb +7 -7
- data/lib/simplecov_mcp/tools/coverage_table_tool.rb +90 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/help_tool.rb +13 -4
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/uncovered_lines_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/version_tool.rb +11 -3
- data/lib/simplecov_mcp/util.rb +82 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/version.rb +1 -1
- data/lib/simplecov_mcp.rb +144 -2
- data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
- data/spec/TIMESTAMPS.md +48 -0
- data/spec/all_files_coverage_tool_spec.rb +29 -25
- data/spec/base_tool_spec.rb +11 -10
- data/spec/cli/show_default_report_spec.rb +33 -0
- data/spec/cli_config_spec.rb +137 -0
- data/spec/cli_enumerated_options_spec.rb +68 -0
- data/spec/cli_error_spec.rb +105 -47
- data/spec/cli_source_spec.rb +82 -23
- data/spec/cli_spec.rb +140 -5
- data/spec/cli_success_predicate_spec.rb +141 -0
- data/spec/cli_table_spec.rb +1 -1
- data/spec/cli_usage_spec.rb +10 -26
- data/spec/commands/base_command_spec.rb +187 -0
- data/spec/commands/command_factory_spec.rb +72 -0
- data/spec/commands/detailed_command_spec.rb +48 -0
- data/spec/commands/raw_command_spec.rb +46 -0
- data/spec/commands/summary_command_spec.rb +47 -0
- data/spec/commands/uncovered_command_spec.rb +49 -0
- data/spec/constants_spec.rb +61 -0
- data/spec/coverage_table_tool_spec.rb +17 -33
- data/spec/error_handler_spec.rb +22 -13
- data/spec/error_mode_spec.rb +143 -0
- data/spec/errors_edge_cases_spec.rb +239 -0
- data/spec/errors_stale_spec.rb +2 -2
- data/spec/file_based_mcp_tools_spec.rb +99 -0
- data/spec/fixtures/project1/lib/bar.rb +0 -1
- data/spec/fixtures/project1/lib/foo.rb +0 -1
- data/spec/help_tool_spec.rb +11 -17
- data/spec/integration_spec.rb +845 -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 +15 -4
- data/spec/mode_detector_spec.rb +148 -0
- data/spec/model_error_handling_spec.rb +210 -0
- data/spec/model_staleness_spec.rb +40 -10
- data/spec/option_normalizers_spec.rb +204 -0
- data/spec/option_parsers/env_options_parser_spec.rb +233 -0
- data/spec/option_parsers/error_helper_spec.rb +222 -0
- data/spec/path_relativizer_spec.rb +83 -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 +86 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +57 -0
- data/spec/resolvers/resolver_factory_spec.rb +61 -0
- data/spec/resolvers/resultset_path_resolver_spec.rb +55 -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 +174 -0
- data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
- data/spec/simple_cov_mcp_module_spec.rb +16 -0
- data/spec/simplecov_mcp_model_spec.rb +340 -9
- data/spec/simplecov_mcp_opts_spec.rb +182 -0
- data/spec/spec_helper.rb +147 -4
- data/spec/staleness_checker_spec.rb +373 -0
- data/spec/staleness_more_spec.rb +16 -13
- data/spec/support/mcp_runner.rb +64 -0
- data/spec/tools_error_handling_spec.rb +144 -0
- data/spec/util_spec.rb +109 -34
- data/spec/version_spec.rb +117 -9
- data/spec/version_tool_spec.rb +131 -10
- metadata +120 -63
- data/lib/simple_cov/mcp.rb +0 -9
- data/lib/simple_cov_mcp/base_tool.rb +0 -70
- data/lib/simple_cov_mcp/cli.rb +0 -390
- data/lib/simple_cov_mcp/error_handler.rb +0 -131
- data/lib/simple_cov_mcp/error_handler_factory.rb +0 -38
- data/lib/simple_cov_mcp/errors.rb +0 -176
- data/lib/simple_cov_mcp/mcp_server.rb +0 -30
- data/lib/simple_cov_mcp/model.rb +0 -104
- data/lib/simple_cov_mcp/staleness_checker.rb +0 -125
- data/lib/simple_cov_mcp/tools/coverage_table_tool.rb +0 -61
- data/lib/simple_cov_mcp/util.rb +0 -122
- data/lib/simple_cov_mcp.rb +0 -102
- data/spec/coverage_detailed_tool_spec.rb +0 -36
- data/spec/coverage_raw_tool_spec.rb +0 -32
- data/spec/coverage_summary_tool_spec.rb +0 -39
- data/spec/legacy_shim_spec.rb +0 -13
- data/spec/uncovered_lines_tool_spec.rb +0 -33
data/docs/LIBRARY_API.md
ADDED
@@ -0,0 +1,635 @@
|
|
1
|
+
# Library API Guide
|
2
|
+
|
3
|
+
Use this gem programmatically to inspect coverage without running the CLI or MCP server. The primary entry point is `SimpleCovMcp::CoverageModel`.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Quick Start](#quick-start)
|
8
|
+
- [Method Reference](#method-reference)
|
9
|
+
- [Return Types](#return-types)
|
10
|
+
- [Error Handling](#error-handling)
|
11
|
+
- [Advanced Recipes](#advanced-recipes)
|
12
|
+
- [API Stability](#api-stability)
|
13
|
+
|
14
|
+
## Quick Start
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
require "simplecov_mcp"
|
18
|
+
|
19
|
+
# Defaults (omit args; shown here with comments):
|
20
|
+
# - root: "."
|
21
|
+
# - resultset: resolved from common paths under root
|
22
|
+
# - staleness: "off" (no stale checks)
|
23
|
+
# - tracked_globs: nil (no project-level file-set checks)
|
24
|
+
model = SimpleCovMcp::CoverageModel.new
|
25
|
+
|
26
|
+
# Custom configuration (non-default values):
|
27
|
+
model = SimpleCovMcp::CoverageModel.new(
|
28
|
+
root: "/path/to/project", # non-default project root
|
29
|
+
resultset: "build/coverage", # file or directory containing .resultset.json
|
30
|
+
staleness: "error", # enable stale checks (raise on stale)
|
31
|
+
tracked_globs: ["lib/**/*.rb"] # for 'all_files' staleness: flag new/missing files
|
32
|
+
)
|
33
|
+
|
34
|
+
# List all files with coverage summary
|
35
|
+
files = model.all_files
|
36
|
+
# Per-file queries
|
37
|
+
summary = model.summary_for("lib/foo.rb")
|
38
|
+
uncovered = model.uncovered_for("lib/foo.rb")
|
39
|
+
detailed = model.detailed_for("lib/foo.rb")
|
40
|
+
raw = model.raw_for("lib/foo.rb")
|
41
|
+
```
|
42
|
+
|
43
|
+
## Method Reference
|
44
|
+
|
45
|
+
### `all_files(sort_order: :ascending)`
|
46
|
+
|
47
|
+
Returns coverage summary for all files in the resultset.
|
48
|
+
|
49
|
+
**Parameters:**
|
50
|
+
- `sort_order` (Symbol, optional): `:ascending` (default) or `:descending` by coverage percentage
|
51
|
+
|
52
|
+
**Returns:** `Array<Hash>` - See [all_files return type](#all_files)
|
53
|
+
|
54
|
+
**Example:**
|
55
|
+
```ruby
|
56
|
+
files = model.all_files
|
57
|
+
# => [ { 'file' => '/abs/path/lib/foo.rb', 'covered' => 12, 'total' => 14, 'percentage' => 85.71, 'stale' => false }, ... ]
|
58
|
+
|
59
|
+
# Get worst coverage first
|
60
|
+
worst_files = model.all_files(sort_order: :ascending).first(10)
|
61
|
+
```
|
62
|
+
|
63
|
+
### `summary_for(path)`
|
64
|
+
|
65
|
+
Returns coverage summary for a specific file.
|
66
|
+
|
67
|
+
**Parameters:**
|
68
|
+
- `path` (String): File path (absolute, relative to root, or basename)
|
69
|
+
|
70
|
+
**Returns:** `Hash` - See [summary_for return type](#summary_for)
|
71
|
+
|
72
|
+
**Raises:** `SimpleCovMcp::FileError` if file not in coverage data
|
73
|
+
|
74
|
+
**Example:**
|
75
|
+
```ruby
|
76
|
+
summary = model.summary_for("lib/foo.rb")
|
77
|
+
# => { 'file' => '/abs/.../lib/foo.rb', 'summary' => {'covered'=>12, 'total'=>14, 'pct'=>85.71}, 'stale' => false }
|
78
|
+
```
|
79
|
+
|
80
|
+
### `uncovered_for(path)`
|
81
|
+
|
82
|
+
Returns list of uncovered line numbers for a specific file.
|
83
|
+
|
84
|
+
**Parameters:**
|
85
|
+
- `path` (String): File path (absolute, relative to root, or basename)
|
86
|
+
|
87
|
+
**Returns:** `Hash` - See [uncovered_for return type](#uncovered_for)
|
88
|
+
|
89
|
+
**Raises:** `SimpleCovMcp::FileError` if file not in coverage data
|
90
|
+
|
91
|
+
**Example:**
|
92
|
+
```ruby
|
93
|
+
uncovered = model.uncovered_for("lib/foo.rb")
|
94
|
+
# => { 'file' => '/abs/.../lib/foo.rb', 'uncovered' => [5, 9, 12], 'summary' => { ... }, 'stale' => false }
|
95
|
+
```
|
96
|
+
|
97
|
+
### `detailed_for(path)`
|
98
|
+
|
99
|
+
Returns per-line coverage details with hit counts.
|
100
|
+
|
101
|
+
**Parameters:**
|
102
|
+
- `path` (String): File path (absolute, relative to root, or basename)
|
103
|
+
|
104
|
+
**Returns:** `Hash` - See [detailed_for return type](#detailed_for)
|
105
|
+
|
106
|
+
**Raises:** `SimpleCovMcp::FileError` if file not in coverage data
|
107
|
+
|
108
|
+
**Example:**
|
109
|
+
```ruby
|
110
|
+
detailed = model.detailed_for("lib/foo.rb")
|
111
|
+
# => { 'file' => '/abs/.../lib/foo.rb', 'lines' => [{'line' => 1, 'hits' => 1, 'covered' => true}, ...], 'summary' => { ... }, 'stale' => false }
|
112
|
+
```
|
113
|
+
|
114
|
+
### `raw_for(path)`
|
115
|
+
|
116
|
+
Returns raw SimpleCov lines array for a specific file.
|
117
|
+
|
118
|
+
**Parameters:**
|
119
|
+
- `path` (String): File path (absolute, relative to root, or basename)
|
120
|
+
|
121
|
+
**Returns:** `Hash` - See [raw_for return type](#raw_for)
|
122
|
+
|
123
|
+
**Raises:** `SimpleCovMcp::FileError` if file not in coverage data
|
124
|
+
|
125
|
+
**Example:**
|
126
|
+
```ruby
|
127
|
+
raw = model.raw_for("lib/foo.rb")
|
128
|
+
# => { 'file' => '/abs/.../lib/foo.rb', 'lines' => [nil, 1, 0, 3, ...], 'stale' => false }
|
129
|
+
```
|
130
|
+
|
131
|
+
### `format_table(rows = nil, sort_order: :ascending)`
|
132
|
+
|
133
|
+
Generates formatted ASCII table string.
|
134
|
+
|
135
|
+
**Parameters:**
|
136
|
+
- `rows` (Array<Hash>, optional): Custom row data; defaults to `all_files`
|
137
|
+
- `sort_order` (Symbol, optional): `:ascending` (default) or `:descending`
|
138
|
+
|
139
|
+
**Returns:** `String` - Formatted table with Unicode borders
|
140
|
+
|
141
|
+
**Example:**
|
142
|
+
```ruby
|
143
|
+
# Default: all files
|
144
|
+
table = model.format_table
|
145
|
+
puts table
|
146
|
+
|
147
|
+
# Custom rows
|
148
|
+
lib_files = model.all_files.select { |f| f['file'].include?('/lib/') }
|
149
|
+
lib_table = model.format_table(lib_files, sort_order: :descending)
|
150
|
+
puts lib_table
|
151
|
+
```
|
152
|
+
|
153
|
+
## Return Types
|
154
|
+
|
155
|
+
### `all_files`
|
156
|
+
|
157
|
+
Returns `Array<Hash>` where each hash contains:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
{
|
161
|
+
'file' => String, # Absolute file path
|
162
|
+
'covered' => Integer, # Number of covered lines
|
163
|
+
'total' => Integer, # Total relevant lines
|
164
|
+
'percentage' => Float, # Coverage percentage (0.00-100.00)
|
165
|
+
'stale' => false | String # Staleness indicator: false, 'M', 'T', or 'L'
|
166
|
+
}
|
167
|
+
```
|
168
|
+
|
169
|
+
### `summary_for`
|
170
|
+
|
171
|
+
Returns `Hash`:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
{
|
175
|
+
'file' => String, # Absolute file path
|
176
|
+
'summary' => {
|
177
|
+
'covered' => Integer, # Number of covered lines
|
178
|
+
'total' => Integer, # Total relevant lines
|
179
|
+
'pct' => Float # Coverage percentage (0.00-100.00)
|
180
|
+
}
|
181
|
+
}
|
182
|
+
```
|
183
|
+
|
184
|
+
### `uncovered_for`
|
185
|
+
|
186
|
+
Returns `Hash`:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
{
|
190
|
+
'file' => String, # Absolute file path
|
191
|
+
'uncovered' => Array<Integer>, # Line numbers that are not covered
|
192
|
+
'summary' => {
|
193
|
+
'covered' => Integer,
|
194
|
+
'total' => Integer,
|
195
|
+
'pct' => Float
|
196
|
+
}
|
197
|
+
}
|
198
|
+
```
|
199
|
+
|
200
|
+
### `detailed_for`
|
201
|
+
|
202
|
+
Returns `Hash`:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
{
|
206
|
+
'file' => String, # Absolute file path
|
207
|
+
'lines' => Array<Hash>, # Per-line coverage details
|
208
|
+
'summary' => {
|
209
|
+
'covered' => Integer,
|
210
|
+
'total' => Integer,
|
211
|
+
'pct' => Float
|
212
|
+
}
|
213
|
+
}
|
214
|
+
```
|
215
|
+
|
216
|
+
Each element in `lines` array:
|
217
|
+
```ruby
|
218
|
+
{
|
219
|
+
'line' => Integer, # Line number (1-indexed)
|
220
|
+
'hits' => Integer, # Execution count (0 means not covered)
|
221
|
+
'covered' => Boolean # true if hits > 0
|
222
|
+
}
|
223
|
+
```
|
224
|
+
|
225
|
+
### `raw_for`
|
226
|
+
|
227
|
+
Returns `Hash`:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
{
|
231
|
+
'file' => String, # Absolute file path
|
232
|
+
'lines' => Array<Integer | nil> # SimpleCov lines array (nil = irrelevant, 0 = uncovered, >0 = hit count)
|
233
|
+
}
|
234
|
+
```
|
235
|
+
|
236
|
+
## Error Handling
|
237
|
+
|
238
|
+
### Exception Types
|
239
|
+
|
240
|
+
The library raises these custom exceptions:
|
241
|
+
|
242
|
+
- **`SimpleCovMcp::ResultsetNotFoundError`** - Coverage data file not found
|
243
|
+
- **`SimpleCovMcp::FileError`** - Requested file not in coverage data
|
244
|
+
- **`SimpleCovMcp::CoverageDataStaleError`** - Coverage data is stale (only when `staleness: 'error'`)
|
245
|
+
- **`SimpleCovMcp::CoverageDataError`** - Invalid coverage data format or structure
|
246
|
+
|
247
|
+
All exceptions inherit from `SimpleCovMcp::Error`.
|
248
|
+
|
249
|
+
### Basic Error Handling
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
require "simplecov_mcp"
|
253
|
+
|
254
|
+
begin
|
255
|
+
model = SimpleCovMcp::CoverageModel.new
|
256
|
+
summary = model.summary_for("lib/foo.rb")
|
257
|
+
puts "Coverage: #{summary['summary']['pct']}%"
|
258
|
+
rescue SimpleCovMcp::FileError => e
|
259
|
+
puts "File not in coverage data: #{e.message}"
|
260
|
+
rescue SimpleCovMcp::ResultsetNotFoundError => e
|
261
|
+
puts "Coverage data not found: #{e.message}"
|
262
|
+
puts "Run your tests first: bundle exec rspec"
|
263
|
+
rescue SimpleCovMcp::Error => e
|
264
|
+
puts "Coverage error: #{e.message}"
|
265
|
+
end
|
266
|
+
```
|
267
|
+
|
268
|
+
### Handling Stale Coverage
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
# Option 1: Check staleness without raising
|
272
|
+
model = SimpleCovMcp::CoverageModel.new(staleness: "off")
|
273
|
+
files = model.all_files
|
274
|
+
|
275
|
+
stale_files = files.select { |f| f['stale'] }
|
276
|
+
if stale_files.any?
|
277
|
+
puts "Warning: #{stale_files.length} files have stale coverage"
|
278
|
+
stale_files.each do |f|
|
279
|
+
puts " #{f['file']}: #{f['stale']}"
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Option 2: Raise on staleness
|
284
|
+
begin
|
285
|
+
model = SimpleCovMcp::CoverageModel.new(staleness: "error")
|
286
|
+
files = model.all_files
|
287
|
+
rescue SimpleCovMcp::CoverageDataStaleError => e
|
288
|
+
puts "Stale coverage detected: #{e.message}"
|
289
|
+
puts "Re-run tests: bundle exec rspec"
|
290
|
+
exit 1
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
### Graceful Degradation
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
# Try multiple file paths
|
298
|
+
def find_coverage(model, possible_paths)
|
299
|
+
possible_paths.each do |path|
|
300
|
+
begin
|
301
|
+
return model.summary_for(path)
|
302
|
+
rescue SimpleCovMcp::FileError
|
303
|
+
next
|
304
|
+
end
|
305
|
+
end
|
306
|
+
nil
|
307
|
+
end
|
308
|
+
|
309
|
+
summary = find_coverage(model, [
|
310
|
+
"lib/services/auth_service.rb",
|
311
|
+
"app/services/auth_service.rb",
|
312
|
+
"services/auth_service.rb"
|
313
|
+
])
|
314
|
+
|
315
|
+
if summary
|
316
|
+
puts "Coverage: #{summary['summary']['pct']}%"
|
317
|
+
else
|
318
|
+
puts "File not found in coverage data"
|
319
|
+
end
|
320
|
+
```
|
321
|
+
|
322
|
+
## Advanced Recipes
|
323
|
+
|
324
|
+
### Batch File Analysis
|
325
|
+
|
326
|
+
```ruby
|
327
|
+
require "simplecov_mcp"
|
328
|
+
|
329
|
+
model = SimpleCovMcp::CoverageModel.new
|
330
|
+
|
331
|
+
# Analyze multiple files efficiently
|
332
|
+
files_to_check = [
|
333
|
+
"lib/auth_service.rb",
|
334
|
+
"lib/payment_processor.rb",
|
335
|
+
"lib/user_manager.rb"
|
336
|
+
]
|
337
|
+
|
338
|
+
results = files_to_check.map do |path|
|
339
|
+
begin
|
340
|
+
summary = model.summary_for(path)
|
341
|
+
{
|
342
|
+
file: path,
|
343
|
+
coverage: summary['summary']['pct'],
|
344
|
+
status: summary['summary']['pct'] >= 80 ? :ok : :low
|
345
|
+
}
|
346
|
+
rescue SimpleCovMcp::FileError
|
347
|
+
{
|
348
|
+
file: path,
|
349
|
+
coverage: nil,
|
350
|
+
status: :missing
|
351
|
+
}
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Report
|
356
|
+
results.each do |r|
|
357
|
+
status_icon = { ok: '✓', low: '⚠', missing: '✗' }[r[:status]]
|
358
|
+
puts "#{status_icon} #{r[:file]}: #{r[:coverage] || 'N/A'}%"
|
359
|
+
end
|
360
|
+
```
|
361
|
+
|
362
|
+
### Coverage Threshold Validation
|
363
|
+
|
364
|
+
```ruby
|
365
|
+
require "simplecov_mcp"
|
366
|
+
|
367
|
+
class CoverageValidator
|
368
|
+
THRESHOLDS = {
|
369
|
+
'lib/' => 90.0, # Core library needs 90%+
|
370
|
+
'app/' => 80.0, # Application code needs 80%+
|
371
|
+
'spec/' => 70.0, # Test helpers need 70%+
|
372
|
+
}
|
373
|
+
|
374
|
+
def initialize(model)
|
375
|
+
@model = model
|
376
|
+
end
|
377
|
+
|
378
|
+
def validate!
|
379
|
+
files = @model.all_files
|
380
|
+
failures = []
|
381
|
+
|
382
|
+
files.each do |file|
|
383
|
+
threshold = threshold_for(file['file'])
|
384
|
+
next unless threshold
|
385
|
+
|
386
|
+
if file['percentage'] < threshold
|
387
|
+
failures << {
|
388
|
+
file: file['file'],
|
389
|
+
actual: file['percentage'],
|
390
|
+
required: threshold,
|
391
|
+
gap: threshold - file['percentage']
|
392
|
+
}
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
if failures.any?
|
397
|
+
puts "❌ #{failures.length} files below coverage threshold:"
|
398
|
+
failures.sort_by { |f| -f[:gap] }.each do |f|
|
399
|
+
puts " #{f[:file]}: #{f[:actual]}% (need #{f[:required]}%)"
|
400
|
+
end
|
401
|
+
exit 1
|
402
|
+
else
|
403
|
+
puts "✓ All files meet coverage thresholds"
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
private
|
408
|
+
|
409
|
+
def threshold_for(path)
|
410
|
+
THRESHOLDS.each do |prefix, threshold|
|
411
|
+
return threshold if path.include?(prefix)
|
412
|
+
end
|
413
|
+
nil
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
model = SimpleCovMcp::CoverageModel.new
|
418
|
+
validator = CoverageValidator.new(model)
|
419
|
+
validator.validate!
|
420
|
+
```
|
421
|
+
|
422
|
+
### Directory-Level Aggregation
|
423
|
+
|
424
|
+
```ruby
|
425
|
+
require "simplecov_mcp"
|
426
|
+
|
427
|
+
model = SimpleCovMcp::CoverageModel.new
|
428
|
+
files = model.all_files
|
429
|
+
|
430
|
+
# Calculate coverage by directory
|
431
|
+
by_directory = files.group_by do |file|
|
432
|
+
# Get first two path components (e.g., "lib/services")
|
433
|
+
file['file'].split('/')[0..1].join('/')
|
434
|
+
end
|
435
|
+
|
436
|
+
directory_stats = by_directory.map do |dir, dir_files|
|
437
|
+
total_lines = dir_files.sum { |f| f['total'] }
|
438
|
+
covered_lines = dir_files.sum { |f| f['covered'] }
|
439
|
+
percentage = (covered_lines.to_f / total_lines * 100).round(2)
|
440
|
+
|
441
|
+
{
|
442
|
+
directory: dir,
|
443
|
+
files: dir_files.length,
|
444
|
+
coverage: percentage,
|
445
|
+
covered: covered_lines,
|
446
|
+
total: total_lines
|
447
|
+
}
|
448
|
+
end
|
449
|
+
|
450
|
+
# Display sorted by coverage
|
451
|
+
directory_stats.sort_by { |s| s[:coverage] }.each do |stat|
|
452
|
+
puts "#{stat[:directory]}: #{stat[:coverage]}% (#{stat[:files]} files)"
|
453
|
+
end
|
454
|
+
```
|
455
|
+
|
456
|
+
### Coverage Delta Tracking
|
457
|
+
|
458
|
+
```ruby
|
459
|
+
require "simplecov_mcp"
|
460
|
+
require "json"
|
461
|
+
|
462
|
+
class CoverageDeltaTracker
|
463
|
+
def initialize(baseline_path: "coverage_baseline.json")
|
464
|
+
@baseline_path = baseline_path
|
465
|
+
@model = SimpleCovMcp::CoverageModel.new
|
466
|
+
end
|
467
|
+
|
468
|
+
def save_baseline
|
469
|
+
current = @model.all_files
|
470
|
+
File.write(@baseline_path, JSON.pretty_generate(current))
|
471
|
+
puts "Saved coverage baseline (#{current.length} files)"
|
472
|
+
end
|
473
|
+
|
474
|
+
def compare
|
475
|
+
unless File.exist?(@baseline_path)
|
476
|
+
puts "No baseline found. Run save_baseline first."
|
477
|
+
return
|
478
|
+
end
|
479
|
+
|
480
|
+
baseline = JSON.parse(File.read(@baseline_path))
|
481
|
+
current = @model.all_files
|
482
|
+
|
483
|
+
improved = []
|
484
|
+
regressed = []
|
485
|
+
|
486
|
+
current.each do |file|
|
487
|
+
baseline_file = baseline.find { |f| f['file'] == file['file'] }
|
488
|
+
next unless baseline_file
|
489
|
+
|
490
|
+
delta = file['percentage'] - baseline_file['percentage']
|
491
|
+
|
492
|
+
if delta > 0.1
|
493
|
+
improved << {
|
494
|
+
file: file['file'],
|
495
|
+
before: baseline_file['percentage'],
|
496
|
+
after: file['percentage'],
|
497
|
+
delta: delta
|
498
|
+
}
|
499
|
+
elsif delta < -0.1
|
500
|
+
regressed << {
|
501
|
+
file: file['file'],
|
502
|
+
before: baseline_file['percentage'],
|
503
|
+
after: file['percentage'],
|
504
|
+
delta: delta
|
505
|
+
}
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
if improved.any?
|
510
|
+
puts "\n✓ Coverage Improvements:"
|
511
|
+
improved.sort_by { |f| -f[:delta] }.each do |f|
|
512
|
+
puts " #{f[:file]}: #{f[:before]}% → #{f[:after]}% (+#{f[:delta].round(2)}%)"
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
if regressed.any?
|
517
|
+
puts "\n⚠ Coverage Regressions:"
|
518
|
+
regressed.sort_by { |f| f[:delta] }.each do |f|
|
519
|
+
puts " #{f[:file]}: #{f[:before]}% → #{f[:after]}% (#{f[:delta].round(2)}%)"
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
if improved.empty? && regressed.empty?
|
524
|
+
puts "No significant coverage changes"
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
# Usage
|
530
|
+
tracker = CoverageDeltaTracker.new
|
531
|
+
tracker.save_baseline # Run before making changes
|
532
|
+
# ... make code changes and re-run tests ...
|
533
|
+
tracker.compare # See what changed
|
534
|
+
```
|
535
|
+
|
536
|
+
### Custom Reporting
|
537
|
+
|
538
|
+
```ruby
|
539
|
+
require "simplecov_mcp"
|
540
|
+
|
541
|
+
class CoverageReporter
|
542
|
+
def initialize(model)
|
543
|
+
@model = model
|
544
|
+
end
|
545
|
+
|
546
|
+
def generate_markdown_report(output_path)
|
547
|
+
files = @model.all_files
|
548
|
+
|
549
|
+
File.open(output_path, 'w') do |f|
|
550
|
+
f.puts "# Coverage Report"
|
551
|
+
f.puts
|
552
|
+
f.puts "Generated: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
553
|
+
f.puts
|
554
|
+
|
555
|
+
# Overall stats
|
556
|
+
total_lines = files.sum { |file| file['total'] }
|
557
|
+
covered_lines = files.sum { |file| file['covered'] }
|
558
|
+
overall_pct = (covered_lines.to_f / total_lines * 100).round(2)
|
559
|
+
|
560
|
+
f.puts "## Overall Coverage: #{overall_pct}%"
|
561
|
+
f.puts
|
562
|
+
f.puts "- Total Files: #{files.length}"
|
563
|
+
f.puts "- Total Lines: #{total_lines}"
|
564
|
+
f.puts "- Covered Lines: #{covered_lines}"
|
565
|
+
f.puts
|
566
|
+
|
567
|
+
# Files below threshold
|
568
|
+
threshold = 80.0
|
569
|
+
low_coverage = files.select { |file| file['percentage'] < threshold }
|
570
|
+
|
571
|
+
if low_coverage.any?
|
572
|
+
f.puts "## Files Below #{threshold}% Coverage"
|
573
|
+
f.puts
|
574
|
+
f.puts "| File | Coverage | Missing Lines |"
|
575
|
+
f.puts "|------|----------|---------------|"
|
576
|
+
|
577
|
+
low_coverage.sort_by { |file| file['percentage'] }.each do |file|
|
578
|
+
uncovered = @model.uncovered_for(file['file'])
|
579
|
+
missing_count = uncovered['uncovered'].length
|
580
|
+
f.puts "| #{file['file']} | #{file['percentage']}% | #{missing_count} |"
|
581
|
+
end
|
582
|
+
f.puts
|
583
|
+
end
|
584
|
+
|
585
|
+
# Top performers
|
586
|
+
f.puts "## Top 10 Best Covered Files"
|
587
|
+
f.puts
|
588
|
+
f.puts "| File | Coverage |"
|
589
|
+
f.puts "|------|----------|"
|
590
|
+
|
591
|
+
files.sort_by { |file| -file['percentage'] }.take(10).each do |file|
|
592
|
+
f.puts "| #{file['file']} | #{file['percentage']}% |"
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
puts "Report saved to #{output_path}"
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
model = SimpleCovMcp::CoverageModel.new
|
601
|
+
reporter = CoverageReporter.new(model)
|
602
|
+
reporter.generate_markdown_report("coverage_report.md")
|
603
|
+
```
|
604
|
+
|
605
|
+
## Staleness Detection
|
606
|
+
|
607
|
+
The `all_files` method returns a `'stale'` field for each file with one of these values:
|
608
|
+
|
609
|
+
- `false` - Coverage data is current
|
610
|
+
- `'M'` - **Missing**: File no longer exists on disk
|
611
|
+
- `'T'` - **Timestamp**: File modified more recently than coverage data
|
612
|
+
- `'L'` - **Length**: Source file line count differs from coverage data
|
613
|
+
|
614
|
+
**Note:** Per-file methods (`summary_for`, `uncovered_for`, `detailed_for`, `raw_for`) do not include staleness information in their return values. To check staleness for individual files, use `all_files` and filter the results.
|
615
|
+
|
616
|
+
When `staleness: 'error'` mode is enabled in `CoverageModel.new`, the model will raise `SimpleCovMcp::CoverageDataStaleError` exceptions when stale files are detected during method calls.
|
617
|
+
|
618
|
+
## API Stability
|
619
|
+
|
620
|
+
Consider the following public and stable under SemVer:
|
621
|
+
- `SimpleCovMcp::CoverageModel.new(root:, resultset:, staleness: 'off', tracked_globs: nil)`
|
622
|
+
- `#raw_for(path)`, `#summary_for(path)`, `#uncovered_for(path)`, `#detailed_for(path)`, `#all_files(sort_order:)`, `#format_table(rows: nil, sort_order:, check_stale:, tracked_globs:)`
|
623
|
+
- Return shapes shown in the [Return Types](#return-types) section
|
624
|
+
- Exception types documented in [Error Handling](#error-handling)
|
625
|
+
|
626
|
+
**Note:**
|
627
|
+
- CLI (`SimpleCovMcp.run(argv)`) and MCP tools remain stable but are separate surfaces
|
628
|
+
- Internal helpers under `SimpleCovMcp::CovUtil` may change; prefer `CoverageModel` unless you need low-level access
|
629
|
+
|
630
|
+
## Related Documentation
|
631
|
+
|
632
|
+
- [Examples](EXAMPLES.md) - Practical cookbook-style examples
|
633
|
+
- [CLI Usage](CLI_USAGE.md) - Command-line interface reference
|
634
|
+
- [Error Handling](ERROR_HANDLING.md) - Detailed error handling documentation
|
635
|
+
- [MCP Integration](MCP_INTEGRATION.md) - AI assistant integration
|