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
@@ -3,9 +3,29 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
RSpec.describe SimpleCovMcp::CoverageModel do
|
6
|
-
let(:root)
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
7
7
|
subject(:model) { described_class.new(root: root) }
|
8
8
|
|
9
|
+
describe 'initialization error handling' do
|
10
|
+
it 'raises FileError when File.read raises Errno::ENOENT directly' do
|
11
|
+
# Stub find_resultset to return a path, but File.read to raise ENOENT
|
12
|
+
allow(SimpleCovMcp::CovUtil).to receive(:find_resultset)
|
13
|
+
.and_return('/some/path/.resultset.json')
|
14
|
+
allow(File).to receive(:read).with('/some/path/.resultset.json')
|
15
|
+
.and_raise(Errno::ENOENT, 'No such file')
|
16
|
+
|
17
|
+
expect do
|
18
|
+
described_class.new(root: root, resultset: '/some/path/.resultset.json')
|
19
|
+
end.to raise_error(SimpleCovMcp::FileError, /Coverage data not found/)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'raises CoverageDataError when resultset file does not exist' do
|
23
|
+
expect do
|
24
|
+
described_class.new(root: root, resultset: '/nonexistent/path/.resultset.json')
|
25
|
+
end.to raise_error(SimpleCovMcp::CoverageDataError, /Failed to load coverage data/)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
9
29
|
describe 'raw_for' do
|
10
30
|
it 'returns absolute file and lines array' do
|
11
31
|
data = model.raw_for('lib/foo.rb')
|
@@ -23,6 +43,17 @@ RSpec.describe SimpleCovMcp::CoverageModel do
|
|
23
43
|
end
|
24
44
|
end
|
25
45
|
|
46
|
+
describe '#relativize' do
|
47
|
+
it 'returns a copy with file paths relative to the root' do
|
48
|
+
data = model.summary_for('lib/foo.rb')
|
49
|
+
relative = model.relativize(data)
|
50
|
+
|
51
|
+
expect(relative['file']).to eq('lib/foo.rb')
|
52
|
+
expect(data['file']).not_to eq(relative['file'])
|
53
|
+
expect(relative).not_to equal(data)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
26
57
|
describe 'uncovered_for' do
|
27
58
|
it 'lists uncovered executable line numbers' do
|
28
59
|
data = model.uncovered_for('lib/foo.rb')
|
@@ -42,6 +73,36 @@ RSpec.describe SimpleCovMcp::CoverageModel do
|
|
42
73
|
end
|
43
74
|
end
|
44
75
|
|
76
|
+
describe 'staleness_for' do
|
77
|
+
it 'returns the staleness character for a file' do
|
78
|
+
allow_any_instance_of(SimpleCovMcp::StalenessChecker)
|
79
|
+
.to receive(:stale_for_file?) do |_, file_abs, _|
|
80
|
+
if file_abs == File.expand_path('lib/foo.rb', root)
|
81
|
+
'T'
|
82
|
+
else
|
83
|
+
false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
expect(model.staleness_for('lib/foo.rb')).to eq('T')
|
88
|
+
expect(model.staleness_for('lib/bar.rb')).to eq(false)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'returns false when an exception occurs during staleness check' do
|
92
|
+
# Stub the checker to raise an error
|
93
|
+
allow_any_instance_of(SimpleCovMcp::StalenessChecker).to receive(:stale_for_file?)
|
94
|
+
.and_raise(StandardError, 'Something went wrong')
|
95
|
+
|
96
|
+
# The rescue clause should catch the error and return false
|
97
|
+
expect(model.staleness_for('lib/foo.rb')).to eq(false)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'returns false when coverage data is not found for the file' do
|
101
|
+
# Try to get staleness for a file that doesn't exist in coverage
|
102
|
+
expect(model.staleness_for('lib/nonexistent.rb')).to eq(false)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
45
106
|
describe 'all_files' do
|
46
107
|
it 'sorts ascending by percentage then by file path' do
|
47
108
|
files = model.all_files(sort_order: :ascending)
|
@@ -49,6 +110,70 @@ RSpec.describe SimpleCovMcp::CoverageModel do
|
|
49
110
|
expect(files.first['percentage']).to be_within(0.01).of(33.33)
|
50
111
|
expect(files.last['file']).to eq(File.expand_path('lib/foo.rb', root))
|
51
112
|
end
|
113
|
+
|
114
|
+
it 'filters rows when tracked_globs are provided' do
|
115
|
+
files = model.all_files(tracked_globs: ['lib/foo.rb'])
|
116
|
+
|
117
|
+
expect(files.length).to eq(1)
|
118
|
+
expect(files.first['file']).to eq(File.expand_path('lib/foo.rb', root))
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'combines results from multiple tracked_globs patterns' do
|
122
|
+
abs_bar = File.expand_path('lib/bar.rb', root)
|
123
|
+
|
124
|
+
files = model.all_files(tracked_globs: ['lib/foo.rb', abs_bar])
|
125
|
+
|
126
|
+
expect(files.map { |f| f['file'] }).to contain_exactly(
|
127
|
+
File.expand_path('lib/foo.rb', root),
|
128
|
+
abs_bar
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'handles files with paths that cannot be relativized' do
|
133
|
+
# Create a custom row with a path from a Windows-style drive (C:/) that will cause ArgumentError
|
134
|
+
# when trying to make it relative to a Unix-style root
|
135
|
+
custom_rows = [
|
136
|
+
{
|
137
|
+
'file' => 'C:/Windows/system32/file.rb',
|
138
|
+
'percentage' => 100.0,
|
139
|
+
'covered' => 10,
|
140
|
+
'total' => 10,
|
141
|
+
'stale' => false
|
142
|
+
}
|
143
|
+
]
|
144
|
+
|
145
|
+
# This should trigger the ArgumentError rescue in filter_rows_by_globs
|
146
|
+
# When the path cannot be made relative (different path types), it falls back to using the absolute path
|
147
|
+
output = model.format_table(custom_rows, tracked_globs: ['C:/Windows/**/*.rb'])
|
148
|
+
|
149
|
+
# The file should be included because the absolute path fallback matches the glob
|
150
|
+
expect(output).to include('C:/Windows/system32/file.rb')
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe 'resolve method error handling' do
|
155
|
+
it 'raises FileError when coverage_lines is nil after lookup' do
|
156
|
+
# Stub lookup_lines to return nil without raising
|
157
|
+
allow(SimpleCovMcp::CovUtil).to receive(:lookup_lines).and_return(nil)
|
158
|
+
|
159
|
+
expect do
|
160
|
+
model.summary_for('lib/nonexistent.rb')
|
161
|
+
end.to raise_error(SimpleCovMcp::FileError, /No coverage data found for file/)
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'converts Errno::ENOENT to FileNotFoundError during resolve' do
|
165
|
+
# We need to trigger Errno::ENOENT inside the resolve method
|
166
|
+
# Stub the checker's check_file! method to raise Errno::ENOENT
|
167
|
+
allow_any_instance_of(SimpleCovMcp::StalenessChecker).to receive(:check_file!)
|
168
|
+
.and_raise(Errno::ENOENT, 'No such file or directory')
|
169
|
+
|
170
|
+
# Create a model with staleness checking enabled to trigger the check_file! call
|
171
|
+
stale_model = described_class.new(root: root, staleness: 'error')
|
172
|
+
|
173
|
+
expect do
|
174
|
+
stale_model.summary_for('lib/foo.rb')
|
175
|
+
end.to raise_error(SimpleCovMcp::FileNotFoundError, /File not found/)
|
176
|
+
end
|
52
177
|
end
|
53
178
|
|
54
179
|
describe 'resultset directory handling' do
|
@@ -58,16 +183,222 @@ RSpec.describe SimpleCovMcp::CoverageModel do
|
|
58
183
|
expect(data['summary']['total']).to eq(3)
|
59
184
|
expect(data['summary']['covered']).to eq(2)
|
60
185
|
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe 'branch-only coverage resultsets' do
|
189
|
+
let(:branch_root) { (FIXTURES_DIR / 'branch_only_project').to_s }
|
190
|
+
let(:branch_model) { described_class.new(root: branch_root) }
|
191
|
+
|
192
|
+
it 'computes summaries by synthesizing branch data' do
|
193
|
+
data = branch_model.summary_for('lib/branch_only.rb')
|
194
|
+
|
195
|
+
expect(data['summary']['total']).to eq(5)
|
196
|
+
expect(data['summary']['covered']).to eq(3)
|
197
|
+
expect(data['summary']['pct']).to be_within(0.01).of(60.0)
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'returns detailed data using branch-derived hits' do
|
201
|
+
data = branch_model.detailed_for('lib/branch_only.rb')
|
202
|
+
|
203
|
+
expect(data['lines']).to eq([
|
204
|
+
{ 'line' => 6, 'hits' => 3, 'covered' => true },
|
205
|
+
{ 'line' => 7, 'hits' => 0, 'covered' => false },
|
206
|
+
{ 'line' => 13, 'hits' => 0, 'covered' => false },
|
207
|
+
{ 'line' => 14, 'hits' => 2, 'covered' => true },
|
208
|
+
{ 'line' => 16, 'hits' => 2, 'covered' => true }
|
209
|
+
])
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'identifies uncovered lines based on branch hits' do
|
213
|
+
data = branch_model.uncovered_for('lib/branch_only.rb')
|
214
|
+
|
215
|
+
expect(data['uncovered']).to eq([7, 13])
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'includes branch-only files in all_files results' do
|
219
|
+
files = branch_model.all_files(sort_order: :ascending)
|
220
|
+
branch_path = File.expand_path('lib/branch_only.rb', branch_root)
|
221
|
+
another_path = File.expand_path('lib/another.rb', branch_root)
|
222
|
+
|
223
|
+
expect(files.map { |f| f['file'] }).to contain_exactly(branch_path, another_path)
|
224
|
+
|
225
|
+
branch_entry = files.find { |f| f['file'] == branch_path }
|
226
|
+
another_entry = files.find { |f| f['file'] == another_path }
|
227
|
+
|
228
|
+
expect(branch_entry['total']).to eq(5)
|
229
|
+
expect(branch_entry['covered']).to eq(3)
|
230
|
+
expect(another_entry['total']).to eq(1)
|
231
|
+
expect(another_entry['covered']).to eq(0)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
describe 'multiple suites in resultset' do
|
236
|
+
let(:resultset_path) { '/tmp/multi_suite_resultset.json' }
|
237
|
+
|
238
|
+
before do
|
239
|
+
allow(SimpleCovMcp::CovUtil).to receive(:find_resultset).and_wrap_original do
|
240
|
+
|original, search_root, resultset: nil|
|
241
|
+
root_match = File.absolute_path(search_root) == File.absolute_path(root)
|
242
|
+
resultset_empty = resultset.nil? || resultset.to_s.empty?
|
243
|
+
if root_match && resultset_empty
|
244
|
+
resultset_path
|
245
|
+
else
|
246
|
+
original.call(search_root, resultset: resultset)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
allow(File).to receive(:read).and_call_original
|
250
|
+
end
|
251
|
+
|
252
|
+
it 'merges coverage data from multiple suites while keeping latest timestamp' do
|
253
|
+
suite_a_cov = {
|
254
|
+
File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, nil, 2] }
|
255
|
+
}
|
256
|
+
suite_b_cov = {
|
257
|
+
File.join(root, 'lib', 'bar.rb') => { 'lines' => [0, 1, 1] }
|
258
|
+
}
|
259
|
+
|
260
|
+
resultset = {
|
261
|
+
'RSpec' => { 'timestamp' => 100, 'coverage' => suite_a_cov },
|
262
|
+
'Cucumber' => { 'timestamp' => 200, 'coverage' => suite_b_cov }
|
263
|
+
}
|
264
|
+
|
265
|
+
allow(File).to receive(:read).with(resultset_path).and_return(resultset.to_json)
|
266
|
+
|
267
|
+
model = described_class.new(root: root)
|
268
|
+
files = model.all_files(sort_order: :ascending)
|
269
|
+
|
270
|
+
expect(files.map { |f| File.basename(f['file']) }).to include('foo.rb', 'bar.rb')
|
271
|
+
|
272
|
+
timestamp = model.instance_variable_get(:@cov_timestamp)
|
273
|
+
expect(timestamp).to eq(200)
|
274
|
+
end
|
275
|
+
|
276
|
+
it 'combines coverage arrays when the same file appears in multiple suites' do
|
277
|
+
shared_file = File.join(root, 'lib', 'foo.rb')
|
278
|
+
suite_a_cov = {
|
279
|
+
shared_file => { 'lines' => [1, 0, nil, 0] }
|
280
|
+
}
|
281
|
+
suite_b_cov = {
|
282
|
+
shared_file => { 'lines' => [0, 3, nil, 1] }
|
283
|
+
}
|
284
|
+
|
285
|
+
resultset = {
|
286
|
+
'RSpec' => { 'timestamp' => 100, 'coverage' => suite_a_cov },
|
287
|
+
'Cucumber' => { 'timestamp' => 150, 'coverage' => suite_b_cov }
|
288
|
+
}
|
61
289
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
ensure
|
69
|
-
ENV.delete('SIMPLECOV_RESULTSET')
|
290
|
+
allow(File).to receive(:read).with(resultset_path).and_return(resultset.to_json)
|
291
|
+
|
292
|
+
model = described_class.new(root: root)
|
293
|
+
detailed = model.detailed_for('lib/foo.rb')
|
294
|
+
hits_by_line = detailed['lines'].each_with_object({}) do |row, acc|
|
295
|
+
acc[row['line']] = row['hits']
|
70
296
|
end
|
297
|
+
|
298
|
+
expect(hits_by_line[1]).to eq(1)
|
299
|
+
expect(hits_by_line[2]).to eq(3)
|
300
|
+
expect(hits_by_line[4]).to eq(1)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
describe 'format_table' do
|
305
|
+
it 'returns a formatted table string with all files coverage data' do
|
306
|
+
output = model.format_table
|
307
|
+
|
308
|
+
# Should contain table structure
|
309
|
+
expect(output).to include('┌', '┬', '┐', '│', '├', '┼', '┤', '└', '┴', '┘')
|
310
|
+
|
311
|
+
# Should contain headers
|
312
|
+
expect(output).to include('File', '%', 'Covered', 'Total', 'Stale')
|
313
|
+
|
314
|
+
# Should contain file data
|
315
|
+
expect(output).to include('lib/foo.rb', 'lib/bar.rb')
|
316
|
+
|
317
|
+
# Should contain summary
|
318
|
+
expect(output).to include('Files: total', ', ok ', ', stale ')
|
319
|
+
end
|
320
|
+
|
321
|
+
it 'returns "No coverage data found" when rows is empty' do
|
322
|
+
rows = []
|
323
|
+
output = model.format_table(rows)
|
324
|
+
expect(output).to eq('No coverage data found')
|
325
|
+
end
|
326
|
+
|
327
|
+
it 'accepts custom rows parameter' do
|
328
|
+
custom_rows = [
|
329
|
+
{
|
330
|
+
'file' => '/path/to/file1.rb',
|
331
|
+
'percentage' => 100.0,
|
332
|
+
'covered' => 10,
|
333
|
+
'total' => 10,
|
334
|
+
'stale' => false
|
335
|
+
},
|
336
|
+
{
|
337
|
+
'file' => '/path/to/file2.rb',
|
338
|
+
'percentage' => 50.0,
|
339
|
+
'covered' => 5,
|
340
|
+
'total' => 10,
|
341
|
+
'stale' => 'M'
|
342
|
+
},
|
343
|
+
{
|
344
|
+
'file' => '/path/to/file3.rb',
|
345
|
+
'percentage' => 75.0,
|
346
|
+
'covered' => 15,
|
347
|
+
'total' => 20,
|
348
|
+
'stale' => 'T'
|
349
|
+
}
|
350
|
+
]
|
351
|
+
|
352
|
+
output = model.format_table(custom_rows)
|
353
|
+
|
354
|
+
expect(output).to include('file1.rb')
|
355
|
+
expect(output).to include('file2.rb')
|
356
|
+
expect(output).to include('file3.rb')
|
357
|
+
expect(output).to include('100.00')
|
358
|
+
expect(output).to include('50.00')
|
359
|
+
expect(output).to include('75.00')
|
360
|
+
expect(output).to include('M')
|
361
|
+
expect(output).to include('T')
|
362
|
+
expect(output).not_to include('!')
|
363
|
+
staleness_msg = 'Staleness: M = Missing file, T = Timestamp (source newer), ' \
|
364
|
+
'L = Line count mismatch'
|
365
|
+
expect(output).to include(staleness_msg)
|
366
|
+
end
|
367
|
+
|
368
|
+
it 'accepts sort_order parameter' do
|
369
|
+
# Test that sort_order parameter is passed through correctly
|
370
|
+
rows_desc = model.all_files(sort_order: :descending)
|
371
|
+
output_asc = model.format_table(sort_order: :ascending)
|
372
|
+
output_desc = model.format_table(sort_order: :descending)
|
373
|
+
|
374
|
+
# Both should be valid table outputs
|
375
|
+
expect(output_asc).to include('┌')
|
376
|
+
expect(output_desc).to include('┌')
|
377
|
+
expect(output_asc).to include('Files: total')
|
378
|
+
expect(output_desc).to include('Files: total')
|
379
|
+
end
|
380
|
+
|
381
|
+
it 'sorts table output correctly when provided with custom rows' do
|
382
|
+
# Get all files data to use as custom rows
|
383
|
+
all_files_data = model.all_files
|
384
|
+
|
385
|
+
# Test ascending sort with custom rows
|
386
|
+
output_asc = model.format_table(all_files_data, sort_order: :ascending)
|
387
|
+
lines_asc = output_asc.split("\n")
|
388
|
+
bar_line_asc = lines_asc.find { |line| line.include?('bar.rb') }
|
389
|
+
foo_line_asc = lines_asc.find { |line| line.include?('foo.rb') }
|
390
|
+
|
391
|
+
# In ascending order, bar.rb (33.33%) should come before foo.rb (66.67%)
|
392
|
+
expect(lines_asc.index(bar_line_asc)).to be < lines_asc.index(foo_line_asc)
|
393
|
+
|
394
|
+
# Test descending sort with custom rows
|
395
|
+
output_desc = model.format_table(all_files_data, sort_order: :descending)
|
396
|
+
lines_desc = output_desc.split("\n")
|
397
|
+
bar_line_desc = lines_desc.find { |line| line.include?('bar.rb') }
|
398
|
+
foo_line_desc = lines_desc.find { |line| line.include?('foo.rb') }
|
399
|
+
|
400
|
+
# In descending order, foo.rb (66.67%) should come before bar.rb (33.33%)
|
401
|
+
expect(lines_desc.index(foo_line_desc)).to be < lines_desc.index(bar_line_desc)
|
71
402
|
end
|
72
403
|
end
|
73
404
|
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'SIMPLECOV_MCP_OPTS Environment Variable' do
|
6
|
+
let(:cli) { SimpleCovMcp::CoverageCLI.new }
|
7
|
+
|
8
|
+
around do |example|
|
9
|
+
original_value = ENV['SIMPLECOV_MCP_OPTS']
|
10
|
+
example.run
|
11
|
+
ensure
|
12
|
+
ENV['SIMPLECOV_MCP_OPTS'] = original_value
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'CLI option parsing from environment' do
|
16
|
+
it 'parses simple options from SIMPLECOV_MCP_OPTS' do
|
17
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--error-mode off --json'
|
18
|
+
|
19
|
+
begin
|
20
|
+
silence_output { cli.send(:run, ['summary', 'lib/foo.rb']) }
|
21
|
+
rescue Exception => e
|
22
|
+
# Expected to fail due to missing file, but options should be parsed
|
23
|
+
puts "DEBUG: Caught exception: #{e.class}: #{e.message}" if ENV['DEBUG']
|
24
|
+
end
|
25
|
+
|
26
|
+
expect(cli.config.error_mode).to eq(:off)
|
27
|
+
expect(cli.config.json).to be true
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'handles quoted options with spaces' do
|
31
|
+
test_path = File.join(Dir.tmpdir, 'test path with spaces', '.resultset.json')
|
32
|
+
ENV['SIMPLECOV_MCP_OPTS'] = "--resultset \"#{test_path}\""
|
33
|
+
|
34
|
+
# Stub exit method to prevent process termination
|
35
|
+
allow_any_instance_of(Object).to receive(:exit)
|
36
|
+
|
37
|
+
# silence_output captures the expected error message from the CLI trying to
|
38
|
+
# load the (non-existent) resultset, preventing it from leaking to the console.
|
39
|
+
silence_output do
|
40
|
+
cli.send(:run, ['--help'])
|
41
|
+
end
|
42
|
+
|
43
|
+
expect(cli.config.resultset).to eq(test_path)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'supports setting log-file to stdout from environment' do
|
47
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--log-file stdout'
|
48
|
+
|
49
|
+
allow_any_instance_of(Object).to receive(:exit)
|
50
|
+
|
51
|
+
silence_output do
|
52
|
+
cli.send(:run, ['--help'])
|
53
|
+
end
|
54
|
+
|
55
|
+
expect(cli.config.log_file).to eq('stdout')
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'command line arguments override environment options' do
|
59
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--error-mode off'
|
60
|
+
|
61
|
+
begin
|
62
|
+
silence_output { cli.send(:run, ['--error-mode', 'trace', 'summary', 'lib/foo.rb']) }
|
63
|
+
rescue SystemExit, SimpleCovMcp::Error
|
64
|
+
# Expected to fail, but options should be parsed
|
65
|
+
end
|
66
|
+
|
67
|
+
# Command line should override environment
|
68
|
+
expect(cli.config.error_mode).to eq(:trace)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'handles malformed SIMPLECOV_MCP_OPTS gracefully' do
|
72
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--option "unclosed quote'
|
73
|
+
|
74
|
+
# Should catch the ConfigurationError and exit cleanly
|
75
|
+
_out, _err, status = run_cli_with_status('summary', 'lib/foo.rb')
|
76
|
+
expect(status).not_to eq(0)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'returns empty array when SIMPLECOV_MCP_OPTS is not set' do
|
80
|
+
# ENV is already cleared by around block
|
81
|
+
opts = cli.send(:parse_env_opts)
|
82
|
+
expect(opts).to eq([])
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'returns empty array when SIMPLECOV_MCP_OPTS is empty' do
|
86
|
+
ENV['SIMPLECOV_MCP_OPTS'] = ''
|
87
|
+
opts = cli.send(:parse_env_opts)
|
88
|
+
expect(opts).to eq([])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe 'CLI mode detection with SIMPLECOV_MCP_OPTS' do
|
93
|
+
it 'respects --force-cli from environment variable' do
|
94
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--force-cli'
|
95
|
+
|
96
|
+
# This would normally be MCP mode (no TTY, no subcommand)
|
97
|
+
stdin = double('stdin', tty?: false)
|
98
|
+
|
99
|
+
env_opts = SimpleCovMcp.send(:parse_env_opts_for_mode_detection)
|
100
|
+
full_argv = env_opts + []
|
101
|
+
|
102
|
+
expect(SimpleCovMcp::ModeDetector.cli_mode?(full_argv, stdin: stdin)).to be true
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'handles parse errors gracefully in mode detection' do
|
106
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--option "unclosed quote'
|
107
|
+
|
108
|
+
# Should return empty array and not crash
|
109
|
+
opts = SimpleCovMcp.send(:parse_env_opts_for_mode_detection)
|
110
|
+
expect(opts).to eq([])
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'actually runs CLI when --force-cli is in SIMPLECOV_MCP_OPTS' do
|
114
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--force-cli'
|
115
|
+
|
116
|
+
# Mock STDIN to not be a TTY (would normally trigger MCP server mode)
|
117
|
+
allow(STDIN).to receive(:tty?).and_return(false)
|
118
|
+
|
119
|
+
# Stub exit to prevent process termination
|
120
|
+
allow_any_instance_of(Object).to receive(:exit)
|
121
|
+
|
122
|
+
# Run with --help which should produce help output
|
123
|
+
output = nil
|
124
|
+
silence_output do |out, err|
|
125
|
+
SimpleCovMcp.run(['--help'])
|
126
|
+
output = out.string + err.string
|
127
|
+
end
|
128
|
+
|
129
|
+
# Verify CLI actually ran by checking for help text
|
130
|
+
expect(output).to include('Usage:')
|
131
|
+
expect(output).to include('simplecov-mcp')
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'actually runs MCP server mode when no CLI indicators present' do
|
135
|
+
ENV['SIMPLECOV_MCP_OPTS'] = ''
|
136
|
+
|
137
|
+
# Mock STDIN to not be a TTY and to provide valid JSON-RPC
|
138
|
+
allow(STDIN).to receive(:tty?).and_return(false)
|
139
|
+
|
140
|
+
# Provide a minimal JSON-RPC request that the server can handle
|
141
|
+
json_request = JSON.generate({
|
142
|
+
jsonrpc: '2.0',
|
143
|
+
id: 1,
|
144
|
+
method: 'initialize',
|
145
|
+
params: {
|
146
|
+
protocolVersion: '2024-11-05',
|
147
|
+
capabilities: {},
|
148
|
+
clientInfo: { name: 'test', version: '1.0' }
|
149
|
+
}
|
150
|
+
})
|
151
|
+
|
152
|
+
allow(STDIN).to receive(:gets).and_return(json_request, nil)
|
153
|
+
|
154
|
+
# Capture output to verify MCP server response
|
155
|
+
output = nil
|
156
|
+
silence_output do |out, err|
|
157
|
+
SimpleCovMcp.run([])
|
158
|
+
output = out.string + err.string
|
159
|
+
end
|
160
|
+
|
161
|
+
# Verify MCP server ran by checking for JSON-RPC response
|
162
|
+
expect(output).to include('"jsonrpc"')
|
163
|
+
expect(output).to include('"result"')
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe 'integration with actual CLI usage' do
|
168
|
+
it 'works end-to-end with --resultset option' do
|
169
|
+
test_resultset = File.join(Dir.tmpdir, 'test_coverage', '.resultset.json')
|
170
|
+
ENV['SIMPLECOV_MCP_OPTS'] = "--resultset #{test_resultset} --json"
|
171
|
+
|
172
|
+
allow_any_instance_of(Object).to receive(:exit)
|
173
|
+
|
174
|
+
expect do
|
175
|
+
silence_output { cli.send(:run, ['--help']) }
|
176
|
+
end.not_to raise_error
|
177
|
+
|
178
|
+
expect(cli.config.resultset).to eq(test_resultset)
|
179
|
+
expect(cli.config.json).to be true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|