simplecov-mcp 1.0.1 → 2.0.1
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/README.md +98 -50
- data/docs/{ARCHITECTURE.md → dev/ARCHITECTURE.md} +11 -10
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
- data/docs/{DEVELOPMENT.md → dev/DEVELOPMENT.md} +2 -1
- data/docs/dev/README.md +10 -0
- data/docs/dev/RELEASING.md +146 -0
- data/docs/{arch-decisions → dev/arch-decisions}/001-x-arch-decision.md +3 -1
- data/docs/{arch-decisions → dev/arch-decisions}/002-x-arch-decision.md +7 -5
- data/docs/{arch-decisions → dev/arch-decisions}/003-x-arch-decision.md +2 -0
- data/docs/{arch-decisions → dev/arch-decisions}/004-x-arch-decision.md +6 -2
- data/docs/{arch-decisions → dev/arch-decisions}/005-x-arch-decision.md +4 -2
- data/docs/{arch-decisions → dev/arch-decisions}/README.md +3 -3
- data/docs/{presentations → dev/presentations}/simplecov-mcp-presentation.md +28 -22
- data/docs/fixtures/demo_project/README.md +9 -0
- data/docs/{ADVANCED_USAGE.md → user/ADVANCED_USAGE.md} +129 -319
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
- data/docs/user/CLI_USAGE.md +750 -0
- data/docs/{ERROR_HANDLING.md → user/ERROR_HANDLING.md} +12 -12
- data/docs/user/EXAMPLES.md +588 -0
- data/docs/user/INSTALLATION.md +130 -0
- data/docs/{LIBRARY_API.md → user/LIBRARY_API.md} +90 -32
- data/docs/{MCP_INTEGRATION.md → user/MCP_INTEGRATION.md} +36 -34
- data/docs/user/README.md +14 -0
- data/docs/{TROUBLESHOOTING.md → user/TROUBLESHOOTING.md} +21 -100
- data/docs/user/V2-BREAKING-CHANGES.md +472 -0
- data/exe/simplecov-mcp +1 -1
- data/lib/simplecov_mcp/{cli_config.rb → app_config.rb} +12 -12
- data/lib/simplecov_mcp/app_context.rb +1 -1
- data/lib/simplecov_mcp/base_tool.rb +66 -38
- data/lib/simplecov_mcp/cli.rb +67 -123
- data/lib/simplecov_mcp/commands/base_command.rb +16 -27
- data/lib/simplecov_mcp/commands/command_factory.rb +8 -2
- data/lib/simplecov_mcp/commands/detailed_command.rb +16 -2
- data/lib/simplecov_mcp/commands/list_command.rb +1 -1
- data/lib/simplecov_mcp/commands/raw_command.rb +18 -2
- data/lib/simplecov_mcp/commands/summary_command.rb +20 -3
- data/lib/simplecov_mcp/commands/totals_command.rb +53 -0
- data/lib/simplecov_mcp/commands/uncovered_command.rb +24 -5
- data/lib/simplecov_mcp/commands/validate_command.rb +60 -0
- data/lib/simplecov_mcp/commands/version_command.rb +19 -4
- data/lib/simplecov_mcp/config_parser.rb +32 -0
- data/lib/simplecov_mcp/constants.rb +3 -3
- data/lib/simplecov_mcp/coverage_reporter.rb +31 -0
- data/lib/simplecov_mcp/error_handler.rb +81 -40
- data/lib/simplecov_mcp/error_handler_factory.rb +2 -2
- data/lib/simplecov_mcp/errors.rb +12 -19
- data/lib/simplecov_mcp/formatters/source_formatter.rb +23 -19
- data/lib/simplecov_mcp/formatters.rb +51 -0
- data/lib/simplecov_mcp/mcp_server.rb +9 -7
- data/lib/simplecov_mcp/mode_detector.rb +6 -5
- data/lib/simplecov_mcp/model.rb +122 -88
- data/lib/simplecov_mcp/option_normalizers.rb +39 -18
- data/lib/simplecov_mcp/option_parser_builder.rb +85 -72
- data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +3 -5
- data/lib/simplecov_mcp/option_parsers/error_helper.rb +18 -17
- data/lib/simplecov_mcp/path_relativizer.rb +17 -14
- data/lib/simplecov_mcp/predicate_evaluator.rb +72 -0
- data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/project_totals_presenter.rb +27 -0
- data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +14 -18
- data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +7 -9
- data/lib/simplecov_mcp/resultset_loader.rb +20 -25
- data/lib/simplecov_mcp/staleness_checker.rb +50 -46
- data/lib/simplecov_mcp/table_formatter.rb +64 -0
- data/lib/simplecov_mcp/tools/all_files_coverage_tool.rb +20 -50
- data/lib/simplecov_mcp/tools/coverage_detailed_tool.rb +13 -7
- data/lib/simplecov_mcp/tools/coverage_raw_tool.rb +12 -7
- data/lib/simplecov_mcp/tools/coverage_summary_tool.rb +13 -8
- data/lib/simplecov_mcp/tools/coverage_table_tool.rb +20 -60
- data/lib/simplecov_mcp/tools/coverage_totals_tool.rb +44 -0
- data/lib/simplecov_mcp/tools/help_tool.rb +38 -66
- data/lib/simplecov_mcp/tools/uncovered_lines_tool.rb +13 -8
- data/lib/simplecov_mcp/tools/validate_tool.rb +72 -0
- data/lib/simplecov_mcp/tools/version_tool.rb +7 -14
- data/lib/simplecov_mcp/util.rb +18 -12
- data/lib/simplecov_mcp/version.rb +1 -1
- data/lib/simplecov_mcp.rb +23 -29
- data/spec/all_files_coverage_tool_spec.rb +4 -3
- data/spec/{cli_config_spec.rb → app_config_spec.rb} +31 -26
- data/spec/base_tool_spec.rb +17 -14
- data/spec/cli/show_default_report_spec.rb +2 -2
- data/spec/cli_enumerated_options_spec.rb +31 -9
- data/spec/cli_error_spec.rb +46 -23
- data/spec/cli_format_spec.rb +123 -0
- data/spec/cli_json_options_spec.rb +50 -0
- data/spec/cli_source_spec.rb +11 -63
- data/spec/cli_spec.rb +82 -97
- data/spec/cli_usage_spec.rb +15 -15
- data/spec/commands/base_command_spec.rb +12 -92
- data/spec/commands/command_factory_spec.rb +7 -3
- data/spec/commands/detailed_command_spec.rb +10 -24
- data/spec/commands/list_command_spec.rb +28 -0
- data/spec/commands/raw_command_spec.rb +43 -20
- data/spec/commands/summary_command_spec.rb +10 -23
- data/spec/commands/totals_command_spec.rb +34 -0
- data/spec/commands/uncovered_command_spec.rb +29 -23
- data/spec/commands/validate_command_spec.rb +213 -0
- data/spec/commands/version_command_spec.rb +38 -0
- data/spec/constants_spec.rb +3 -3
- data/spec/coverage_reporter_spec.rb +102 -0
- data/spec/coverage_table_tool_spec.rb +21 -10
- data/spec/coverage_totals_tool_spec.rb +37 -0
- data/spec/error_handler_spec.rb +120 -4
- data/spec/error_mode_spec.rb +18 -22
- data/spec/errors_edge_cases_spec.rb +101 -28
- data/spec/errors_stale_spec.rb +34 -0
- data/spec/file_based_mcp_tools_spec.rb +6 -6
- data/spec/fixtures/project1/lib/bar.rb +2 -0
- data/spec/fixtures/project1/lib/foo.rb +2 -0
- data/spec/help_tool_spec.rb +2 -18
- data/spec/integration_spec.rb +103 -161
- data/spec/logging_fallback_spec.rb +3 -3
- data/spec/mcp_server_integration_spec.rb +1 -1
- data/spec/mcp_server_spec.rb +70 -53
- data/spec/mode_detector_spec.rb +46 -41
- data/spec/model_error_handling_spec.rb +139 -78
- data/spec/model_staleness_spec.rb +13 -13
- data/spec/option_normalizers_spec.rb +111 -112
- data/spec/option_parsers/env_options_parser_spec.rb +25 -37
- data/spec/option_parsers/error_helper_spec.rb +56 -56
- data/spec/path_relativizer_spec.rb +15 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +1 -1
- data/spec/presenters/coverage_summary_presenter_spec.rb +1 -1
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +1 -1
- data/spec/presenters/project_coverage_presenter_spec.rb +9 -8
- data/spec/presenters/project_totals_presenter_spec.rb +144 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +261 -36
- data/spec/resolvers/resultset_path_resolver_spec.rb +13 -8
- data/spec/shared_examples/file_based_mcp_tools.rb +23 -18
- data/spec/shared_examples/formatted_command_examples.rb +64 -0
- data/spec/simple_cov_mcp_module_spec.rb +24 -3
- data/spec/simplecov_mcp/formatters/source_formatter_spec.rb +267 -0
- data/spec/simplecov_mcp/formatters_spec.rb +76 -0
- data/spec/simplecov_mcp/presenters/base_coverage_presenter_spec.rb +79 -0
- data/spec/simplecov_mcp_model_spec.rb +97 -47
- data/spec/simplecov_mcp_opts_spec.rb +42 -39
- data/spec/spec_helper.rb +27 -92
- data/spec/staleness_checker_spec.rb +10 -9
- data/spec/staleness_more_spec.rb +4 -4
- 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 +10 -8
- 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 +34 -48
- data/spec/util_spec.rb +5 -4
- data/spec/version_spec.rb +7 -7
- data/spec/version_tool_spec.rb +20 -22
- metadata +90 -23
- data/docs/BRANCH_ONLY_COVERAGE.md +0 -81
- data/docs/CLI_USAGE.md +0 -637
- data/docs/EXAMPLES.md +0 -430
- data/docs/INSTALLATION.md +0 -352
- data/spec/cli_success_predicate_spec.rb +0 -141
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
RSpec.describe SimpleCovMcp::Commands::ValidateCommand do
|
|
7
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
8
|
+
|
|
9
|
+
def with_temp_predicate(content)
|
|
10
|
+
Tempfile.create(['predicate', '.rb']) do |file|
|
|
11
|
+
file.write(content)
|
|
12
|
+
file.flush
|
|
13
|
+
yield file.path
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe 'validate subcommand with file' do
|
|
18
|
+
it 'exits 0 when predicate returns truthy value' do
|
|
19
|
+
with_temp_predicate("->(model) { true }\n") do |path|
|
|
20
|
+
_out, _err, status = run_cli_with_status(
|
|
21
|
+
'--root', root,
|
|
22
|
+
'--resultset', 'coverage',
|
|
23
|
+
'validate', path
|
|
24
|
+
)
|
|
25
|
+
expect(status).to eq(0)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'exits 1 when predicate returns falsy value' do
|
|
30
|
+
with_temp_predicate("->(model) { false }\n") do |path|
|
|
31
|
+
_out, _err, status = run_cli_with_status(
|
|
32
|
+
'--root', root,
|
|
33
|
+
'--resultset', 'coverage',
|
|
34
|
+
'validate', path
|
|
35
|
+
)
|
|
36
|
+
expect(status).to eq(1)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'exits 2 when predicate raises an error' do
|
|
41
|
+
with_temp_predicate("->(model) { raise 'Boom!' }\n") do |path|
|
|
42
|
+
_out, err, status = run_cli_with_status(
|
|
43
|
+
'--root', root,
|
|
44
|
+
'--resultset', 'coverage',
|
|
45
|
+
'validate', path
|
|
46
|
+
)
|
|
47
|
+
expect(status).to eq(2)
|
|
48
|
+
expect(err).to include('Predicate error: Boom!')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'shows backtrace when predicate errors with --error-mode debug' do
|
|
53
|
+
with_temp_predicate("->(model) { raise 'Boom!' }\n") do |path|
|
|
54
|
+
_out, err, status = run_cli_with_status(
|
|
55
|
+
'--error-mode', 'debug',
|
|
56
|
+
'--root', root,
|
|
57
|
+
'--resultset', 'coverage',
|
|
58
|
+
'validate', path
|
|
59
|
+
)
|
|
60
|
+
expect(status).to eq(2)
|
|
61
|
+
expect(err).to include('Predicate error: Boom!')
|
|
62
|
+
# With trace mode, should show backtrace
|
|
63
|
+
expect(err).to match(/predicate.*\.rb:\d+/)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'exits 2 when predicate file is not found' do
|
|
68
|
+
_out, err, status = run_cli_with_status(
|
|
69
|
+
'--root', root,
|
|
70
|
+
'--resultset', 'coverage',
|
|
71
|
+
'validate', '/nonexistent/predicate.rb'
|
|
72
|
+
)
|
|
73
|
+
expect(status).to eq(2)
|
|
74
|
+
expect(err).to include('Predicate file not found')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'exits 2 when predicate has syntax error' do
|
|
78
|
+
with_temp_predicate("-> { this is invalid syntax\n") do |path|
|
|
79
|
+
_out, err, status = run_cli_with_status(
|
|
80
|
+
'--root', root,
|
|
81
|
+
'--resultset', 'coverage',
|
|
82
|
+
'validate', path
|
|
83
|
+
)
|
|
84
|
+
expect(status).to eq(2)
|
|
85
|
+
expect(err).to include('Syntax error in predicate file')
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'exits 2 when predicate is not callable' do
|
|
90
|
+
with_temp_predicate("42\n") do |path|
|
|
91
|
+
_out, err, status = run_cli_with_status(
|
|
92
|
+
'--root', root,
|
|
93
|
+
'--resultset', 'coverage',
|
|
94
|
+
'validate', path
|
|
95
|
+
)
|
|
96
|
+
expect(status).to eq(2)
|
|
97
|
+
expect(err).to include('Predicate must be callable')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'provides model to predicate that can query coverage' do
|
|
102
|
+
# Test that the predicate receives a working CoverageModel
|
|
103
|
+
with_temp_predicate(<<~RUBY) do |path|
|
|
104
|
+
->(model) do
|
|
105
|
+
# Access coverage data via the model
|
|
106
|
+
summary = model.summary_for('lib/foo.rb')
|
|
107
|
+
summary['summary']['percentage'] > 50 # Should be true for foo.rb
|
|
108
|
+
end
|
|
109
|
+
RUBY
|
|
110
|
+
_out, _err, status = run_cli_with_status(
|
|
111
|
+
'--root', root,
|
|
112
|
+
'--resultset', 'coverage',
|
|
113
|
+
'validate', path
|
|
114
|
+
)
|
|
115
|
+
expect(status).to eq(0)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe 'validate subcommand with -i/--inline flag' do
|
|
121
|
+
it 'exits 0 when predicate code returns truthy value' do
|
|
122
|
+
_out, _err, status = run_cli_with_status(
|
|
123
|
+
'--root', root,
|
|
124
|
+
'--resultset', 'coverage',
|
|
125
|
+
'validate', '-i', '->(model) { true }'
|
|
126
|
+
)
|
|
127
|
+
expect(status).to eq(0)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'exits 1 when predicate code returns falsy value' do
|
|
131
|
+
_out, _err, status = run_cli_with_status(
|
|
132
|
+
'--root', root,
|
|
133
|
+
'--resultset', 'coverage',
|
|
134
|
+
'validate', '-i', '->(model) { false }'
|
|
135
|
+
)
|
|
136
|
+
expect(status).to eq(1)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'exits 2 when predicate code raises an error' do
|
|
140
|
+
_out, err, status = run_cli_with_status(
|
|
141
|
+
'--root', root,
|
|
142
|
+
'--resultset', 'coverage',
|
|
143
|
+
'validate', '-i', "->(model) { raise 'Boom!' }"
|
|
144
|
+
)
|
|
145
|
+
expect(status).to eq(2)
|
|
146
|
+
expect(err).to include('Predicate error: Boom!')
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'exits 2 when predicate code has syntax error' do
|
|
150
|
+
_out, err, status = run_cli_with_status(
|
|
151
|
+
'--root', root,
|
|
152
|
+
'--resultset', 'coverage',
|
|
153
|
+
'validate', '-i', '-> { invalid syntax'
|
|
154
|
+
)
|
|
155
|
+
expect(status).to eq(2)
|
|
156
|
+
expect(err).to include('Syntax error in predicate code')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'exits 2 when predicate code is not callable' do
|
|
160
|
+
_out, err, status = run_cli_with_status(
|
|
161
|
+
'--root', root,
|
|
162
|
+
'--resultset', 'coverage',
|
|
163
|
+
'validate', '-i', '42'
|
|
164
|
+
)
|
|
165
|
+
expect(status).to eq(2)
|
|
166
|
+
expect(err).to include('Predicate must be callable')
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'provides model to predicate that can query coverage' do
|
|
170
|
+
code = <<~RUBY.strip
|
|
171
|
+
->(model) { model.summary_for('lib/foo.rb')['summary']['percentage'] > 50 }
|
|
172
|
+
RUBY
|
|
173
|
+
_out, _err, status = run_cli_with_status(
|
|
174
|
+
'--root', root,
|
|
175
|
+
'--resultset', 'coverage',
|
|
176
|
+
'validate', '-i', code
|
|
177
|
+
)
|
|
178
|
+
expect(status).to eq(0)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe 'error handling' do
|
|
183
|
+
it 'raises error when no file or -i flag provided' do
|
|
184
|
+
_out, err, status = run_cli_with_status(
|
|
185
|
+
'--root', root,
|
|
186
|
+
'--resultset', 'coverage',
|
|
187
|
+
'validate'
|
|
188
|
+
)
|
|
189
|
+
expect(status).to eq(1)
|
|
190
|
+
expect(err).to include('validate <file> | -i <code>')
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'raises error when -i flag provided without code' do
|
|
194
|
+
_out, err, status = run_cli_with_status(
|
|
195
|
+
'--root', root,
|
|
196
|
+
'--resultset', 'coverage',
|
|
197
|
+
'validate', '-i'
|
|
198
|
+
)
|
|
199
|
+
expect(status).to eq(1)
|
|
200
|
+
expect(err).to include('validate -i <code>')
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'raises error when unknown option is provided' do
|
|
204
|
+
_out, err, status = run_cli_with_status(
|
|
205
|
+
'--root', root,
|
|
206
|
+
'--resultset', 'coverage',
|
|
207
|
+
'validate', '--unknown-option'
|
|
208
|
+
)
|
|
209
|
+
expect(status).to eq(1)
|
|
210
|
+
expect(err).to include('Unknown option for validate: --unknown-option')
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative '../shared_examples/formatted_command_examples'
|
|
5
|
+
|
|
6
|
+
RSpec.describe SimpleCovMcp::Commands::VersionCommand do
|
|
7
|
+
let(:cli_context) { SimpleCovMcp::CoverageCLI.new }
|
|
8
|
+
let(:command) { described_class.new(cli_context) }
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
cli_context.config.format = :table
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#execute' do
|
|
15
|
+
context 'with table format' do
|
|
16
|
+
it 'prints version, gem root, and documentation info in text mode' do
|
|
17
|
+
output = capture_command_output(command, [])
|
|
18
|
+
|
|
19
|
+
expect(output).to include('│', SimpleCovMcp::VERSION, 'Gem Root', 'Documentation',
|
|
20
|
+
'README.md')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'includes a valid gem root path that exists' do
|
|
24
|
+
output = capture_command_output(command, [])
|
|
25
|
+
|
|
26
|
+
# Extract gem root from table output
|
|
27
|
+
gem_root_line = output.lines.find { |line| line.include?('Gem Root') }
|
|
28
|
+
expect(gem_root_line).not_to be_nil
|
|
29
|
+
|
|
30
|
+
parts = gem_root_line.split('│')
|
|
31
|
+
gem_root = parts[-2].strip
|
|
32
|
+
expect(File.directory?(gem_root)).to be true
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it_behaves_like 'a command with formatted output', [], ['version', 'gem_root']
|
|
37
|
+
end
|
|
38
|
+
end
|
data/spec/constants_spec.rb
CHANGED
|
@@ -18,14 +18,14 @@ RSpec.describe SimpleCovMcp::Constants do
|
|
|
18
18
|
expected_options = %w[
|
|
19
19
|
-r --resultset
|
|
20
20
|
-R --root
|
|
21
|
+
-f --format
|
|
21
22
|
-o --sort-order
|
|
22
23
|
-s --source
|
|
23
|
-
-c --
|
|
24
|
-
-S --
|
|
24
|
+
-c --context-lines
|
|
25
|
+
-S --staleness
|
|
25
26
|
-g --tracked-globs
|
|
26
27
|
-l --log-file
|
|
27
28
|
--error-mode
|
|
28
|
-
--success-predicate
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
expect(options).to eq(expected_options)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe SimpleCovMcp::CoverageReporter do
|
|
6
|
+
let(:model) { instance_double(SimpleCovMcp::CoverageModel) }
|
|
7
|
+
# Data is pre-sorted by percentage ascending (as model.all_files returns)
|
|
8
|
+
let(:all_files_data) do
|
|
9
|
+
[
|
|
10
|
+
{ 'file' => '/project/lib/zero.rb', 'percentage' => 0.0, 'covered' => 0, 'total' => 10 },
|
|
11
|
+
{ 'file' => '/project/lib/low.rb', 'percentage' => 25.0, 'covered' => 5, 'total' => 20 },
|
|
12
|
+
{ 'file' => '/project/lib/medium.rb', 'percentage' => 60.0, 'covered' => 12, 'total' => 20 },
|
|
13
|
+
{ 'file' => '/project/lib/high.rb', 'percentage' => 95.0, 'covered' => 19, 'total' => 20 }
|
|
14
|
+
]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
before do
|
|
18
|
+
allow(model).to receive(:all_files).with(sort_order: :ascending).and_return(all_files_data)
|
|
19
|
+
allow(model).to receive(:relativize) do |files|
|
|
20
|
+
files.map { |f| f.merge('file' => f['file'].sub('/project/', '')) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '.report' do
|
|
25
|
+
it 'returns formatted low coverage files string' do
|
|
26
|
+
result = described_class.report(threshold: 80, count: 5, model: model)
|
|
27
|
+
|
|
28
|
+
expect(result).to be_a(String)
|
|
29
|
+
expect(result).to include('Lowest coverage files (< 80%):')
|
|
30
|
+
expect(result).to include('lib/zero.rb')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'includes files below threshold sorted by coverage ascending' do
|
|
34
|
+
result = described_class.report(threshold: 80, count: 5, model: model)
|
|
35
|
+
|
|
36
|
+
expect(result).to include('lib/zero.rb', 'lib/low.rb', 'lib/medium.rb')
|
|
37
|
+
expect(result).not_to include('lib/high.rb')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'respects count parameter' do
|
|
41
|
+
result = described_class.report(threshold: 80, count: 2, model: model)
|
|
42
|
+
|
|
43
|
+
expect(result).to include('lib/zero.rb')
|
|
44
|
+
expect(result).to include('lib/low.rb')
|
|
45
|
+
expect(result).not_to include('lib/medium.rb')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns nil when no files below threshold' do
|
|
49
|
+
result = described_class.report(threshold: 0, count: 5, model: model)
|
|
50
|
+
|
|
51
|
+
expect(result).to be_nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'uses threshold in header' do
|
|
55
|
+
result = described_class.report(threshold: 90, count: 5, model: model)
|
|
56
|
+
|
|
57
|
+
expect(result).to include('< 90%')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'uses default threshold of 80' do
|
|
61
|
+
result = described_class.report(count: 5, model: model)
|
|
62
|
+
|
|
63
|
+
expect(result).to include('< 80%')
|
|
64
|
+
expect(result).not_to include('lib/high.rb')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'uses default count of 5' do
|
|
68
|
+
result = described_class.report(threshold: 100, model: model)
|
|
69
|
+
|
|
70
|
+
# All 4 files are below 100%
|
|
71
|
+
expect(result).to include('lib/zero.rb')
|
|
72
|
+
expect(result).to include('lib/high.rb')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'relativizes file paths' do
|
|
76
|
+
result = described_class.report(threshold: 80, count: 5, model: model)
|
|
77
|
+
|
|
78
|
+
expect(result).to include('lib/zero.rb')
|
|
79
|
+
expect(result).not_to include('/project/')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'aligns percentages correctly' do
|
|
83
|
+
result = described_class.report(threshold: 100, count: 5, model: model)
|
|
84
|
+
lines = result.split("\n")
|
|
85
|
+
|
|
86
|
+
# lines[0] is empty (leading newline), lines[1] is header, lines[2..] are data
|
|
87
|
+
expect(lines[2]).to match(/^\s+0\.0%/)
|
|
88
|
+
expect(lines[3]).to match(/^\s+25\.0%/)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe 'module_function behavior' do
|
|
93
|
+
it 'report is available as a module method' do
|
|
94
|
+
expect(described_class).to respond_to(:report)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'report is available as a private instance method when included' do
|
|
98
|
+
klass = Class.new { include SimpleCovMcp::CoverageReporter }
|
|
99
|
+
expect(klass.private_instance_methods).to include(:report)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -11,10 +11,10 @@ RSpec.describe SimpleCovMcp::Tools::CoverageTableTool do
|
|
|
11
11
|
setup_mcp_response_stub
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def run_tool(
|
|
14
|
+
def run_tool(staleness: :off)
|
|
15
15
|
# Let real CoverageModel work to test actual format_table behavior
|
|
16
|
-
described_class.call(root: root,
|
|
17
|
-
server_context: server_context).payload.first[
|
|
16
|
+
described_class.call(root: root, staleness: staleness,
|
|
17
|
+
server_context: server_context).payload.first['text']
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
it 'returns a formatted table as a string' do
|
|
@@ -30,12 +30,15 @@ RSpec.describe SimpleCovMcp::Tools::CoverageTableTool do
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
it 'configures CLI to enforce stale checking when requested' do
|
|
33
|
-
model = instance_double(SimpleCovMcp::CoverageModel
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
model = instance_double(SimpleCovMcp::CoverageModel,
|
|
34
|
+
all_files: [
|
|
35
|
+
{ 'file' => "#{root}/lib/foo.rb", 'percentage' => 100.0, 'covered' => 10, 'total' => 10,
|
|
36
|
+
'stale' => false }
|
|
37
|
+
],
|
|
38
|
+
relativize: ->(payload) { payload },
|
|
39
|
+
format_table: 'Mock table output'
|
|
40
|
+
)
|
|
41
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).with(
|
|
39
42
|
root: root,
|
|
40
43
|
resultset: nil,
|
|
41
44
|
staleness: :error,
|
|
@@ -43,6 +46,14 @@ RSpec.describe SimpleCovMcp::Tools::CoverageTableTool do
|
|
|
43
46
|
).and_return(model)
|
|
44
47
|
allow(model).to receive(:format_table).and_return('Mock table output')
|
|
45
48
|
|
|
46
|
-
described_class.call(root: root,
|
|
49
|
+
described_class.call(root: root, staleness: :error, server_context: server_context)
|
|
50
|
+
|
|
51
|
+
expect(SimpleCovMcp::CoverageModel).to have_received(:new).with(
|
|
52
|
+
root: root,
|
|
53
|
+
resultset: nil,
|
|
54
|
+
staleness: :error,
|
|
55
|
+
tracked_globs: nil
|
|
56
|
+
)
|
|
57
|
+
expect(model).to have_received(:format_table)
|
|
47
58
|
end
|
|
48
59
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'simplecov_mcp/tools/coverage_totals_tool'
|
|
5
|
+
|
|
6
|
+
RSpec.describe SimpleCovMcp::Tools::CoverageTotalsTool do
|
|
7
|
+
subject(:tool_response) { described_class.call(root: root, server_context: server_context) }
|
|
8
|
+
|
|
9
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
10
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
setup_mcp_response_stub
|
|
14
|
+
model = instance_double(SimpleCovMcp::CoverageModel)
|
|
15
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
|
16
|
+
|
|
17
|
+
payload = {
|
|
18
|
+
'lines' => { 'total' => 42, 'covered' => 40, 'uncovered' => 2 },
|
|
19
|
+
'percentage' => 95.24,
|
|
20
|
+
'files' => { 'total' => 4, 'ok' => 4, 'stale' => 0 }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
presenter = instance_double(SimpleCovMcp::Presenters::ProjectTotalsPresenter)
|
|
24
|
+
allow(SimpleCovMcp::Presenters::ProjectTotalsPresenter).to receive(:new).and_return(presenter)
|
|
25
|
+
allow(presenter).to receive(:relativized_payload).and_return(payload)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it_behaves_like 'an MCP tool that returns text JSON'
|
|
29
|
+
|
|
30
|
+
it 'returns aggregated totals' do
|
|
31
|
+
data, = expect_mcp_text_json(tool_response, expected_keys: ['lines', 'percentage', 'files'])
|
|
32
|
+
|
|
33
|
+
expect(data['lines']).to include('total' => 42, 'covered' => 40, 'uncovered' => 2)
|
|
34
|
+
expect(data['files']).to include('total' => 4, 'stale' => 0)
|
|
35
|
+
expect(data['percentage']).to eq(95.24)
|
|
36
|
+
end
|
|
37
|
+
end
|
data/spec/error_handler_spec.rb
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
RSpec.describe SimpleCovMcp::ErrorHandler do
|
|
6
|
+
subject(:handler) { described_class.new(error_mode: :log, logger: logger) }
|
|
7
|
+
|
|
6
8
|
let(:logger) do
|
|
7
9
|
Class.new do
|
|
8
10
|
attr_reader :messages
|
|
9
11
|
|
|
10
|
-
def initialize
|
|
11
|
-
def error(msg)
|
|
12
|
+
def initialize = @messages = []
|
|
13
|
+
def error(msg) = @messages << msg
|
|
12
14
|
end.new
|
|
13
15
|
end
|
|
14
16
|
|
|
15
|
-
subject(:handler) { described_class.new(error_mode: :on, logger: logger) }
|
|
16
17
|
|
|
17
18
|
it 'maps filesystem errors to friendly custom errors' do
|
|
18
19
|
e = handler.convert_standard_error(Errno::EISDIR.new('Is a directory @ rb_sysopen - a_dir'))
|
|
@@ -73,9 +74,124 @@ RSpec.describe SimpleCovMcp::ErrorHandler do
|
|
|
73
74
|
begin
|
|
74
75
|
handler.handle_error(Errno::ENOENT.new('No such file or directory @ rb_sysopen - x'),
|
|
75
76
|
context: 'test', reraise: false)
|
|
76
|
-
rescue
|
|
77
|
+
rescue
|
|
77
78
|
# reraise disabled
|
|
78
79
|
end
|
|
79
80
|
expect(logger.messages.join).to include('Error in test')
|
|
80
81
|
end
|
|
82
|
+
|
|
83
|
+
it 'converts TypeError to CoverageDataError for invalid data structures' do
|
|
84
|
+
error = TypeError.new('wrong argument type')
|
|
85
|
+
result = handler.convert_standard_error(error)
|
|
86
|
+
|
|
87
|
+
expect(result).to be_a(SimpleCovMcp::CoverageDataError)
|
|
88
|
+
expect(result.user_friendly_message).to include('Invalid coverage data structure')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'returns generic Error for unrecognized SystemCallError' do
|
|
92
|
+
error = Errno::EEXIST.new('File exists')
|
|
93
|
+
result = handler.convert_standard_error(error)
|
|
94
|
+
|
|
95
|
+
expect(result).to be_a(SimpleCovMcp::Error)
|
|
96
|
+
expect(result.user_friendly_message).to include('An unexpected error occurred')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'handles NoMethodError with non-standard message format' do
|
|
100
|
+
error = NoMethodError.new('some weird error message without the expected pattern')
|
|
101
|
+
result = handler.convert_standard_error(error)
|
|
102
|
+
|
|
103
|
+
expect(result).to be_a(SimpleCovMcp::CoverageDataError)
|
|
104
|
+
expect(result.user_friendly_message).to include('some weird error message')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe 'else branch for non-StandardError exceptions' do
|
|
108
|
+
# This tests the else clause in convert_standard_error for exceptions
|
|
109
|
+
# that don't inherit from StandardError
|
|
110
|
+
it 'returns generic Error for Exception subclasses not inheriting from StandardError' do
|
|
111
|
+
# Create a custom exception that inherits from Exception, not StandardError
|
|
112
|
+
custom_exception_class = Class.new(StandardError) do
|
|
113
|
+
def message
|
|
114
|
+
'Custom non-standard exception'
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
error = custom_exception_class.new
|
|
119
|
+
result = handler.convert_standard_error(error)
|
|
120
|
+
|
|
121
|
+
expect(result).to be_a(SimpleCovMcp::Error)
|
|
122
|
+
expect(result.user_friendly_message).to include('An unexpected error occurred')
|
|
123
|
+
expect(result.user_friendly_message).to include('Custom non-standard exception')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'returns generic Error for ScriptError subclasses' do
|
|
127
|
+
# ScriptError inherits from Exception, not StandardError
|
|
128
|
+
error = NotImplementedError.new('This feature is not implemented')
|
|
129
|
+
result = handler.convert_standard_error(error)
|
|
130
|
+
|
|
131
|
+
expect(result).to be_a(SimpleCovMcp::Error)
|
|
132
|
+
expect(result.user_friendly_message).to include('An unexpected error occurred')
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe 'extract_method_info fallback' do
|
|
137
|
+
# This tests the fallback path in extract_method_info when NoMethodError
|
|
138
|
+
# message doesn't match the expected pattern
|
|
139
|
+
it 'returns original message when pattern does not match' do
|
|
140
|
+
# Test various NoMethodError formats that won't match the regex
|
|
141
|
+
test_messages = [
|
|
142
|
+
'method not found',
|
|
143
|
+
'private method called',
|
|
144
|
+
'undefined local variable or method',
|
|
145
|
+
''
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
test_messages.each do |msg|
|
|
149
|
+
error = NoMethodError.new(msg)
|
|
150
|
+
result = handler.convert_standard_error(error)
|
|
151
|
+
|
|
152
|
+
expect(result).to be_a(SimpleCovMcp::CoverageDataError)
|
|
153
|
+
# The original message should be preserved
|
|
154
|
+
expect(result.message).to include(msg) unless msg.empty?
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ErrorHandler#convert_runtime_error handles RuntimeErrors differently based on context:
|
|
160
|
+
# - :coverage_loading assumes errors relate to coverage data and maps them to
|
|
161
|
+
# CoverageDataError or ResultsetNotFoundError
|
|
162
|
+
# - :general (or any other context) maps unrecognized errors to generic Error
|
|
163
|
+
# This tests the final else branch in convert_runtime_error.
|
|
164
|
+
describe 'convert_runtime_error with general context' do
|
|
165
|
+
it 'converts RuntimeError with unrecognized message to generic Error' do
|
|
166
|
+
error = RuntimeError.new('Some completely unexpected runtime error')
|
|
167
|
+
|
|
168
|
+
result = handler.convert_standard_error(error, context: :general)
|
|
169
|
+
|
|
170
|
+
expect(result).to be_a(SimpleCovMcp::Error)
|
|
171
|
+
expect(result.user_friendly_message)
|
|
172
|
+
.to include('An unexpected error occurred', 'unexpected runtime error')
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
describe '#handle_error with reraise' do
|
|
177
|
+
it 're-raises SimpleCovMcp::Error when reraise is true' do
|
|
178
|
+
error = SimpleCovMcp::FileNotFoundError.new('Test file not found')
|
|
179
|
+
|
|
180
|
+
expect { handler.handle_error(error, context: 'test', reraise: true) }
|
|
181
|
+
.to raise_error(SimpleCovMcp::FileNotFoundError, 'Test file not found')
|
|
182
|
+
|
|
183
|
+
# Verify it was logged
|
|
184
|
+
expect(logger.messages.join).to include('Error in test')
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'converts and re-raises StandardError when reraise is true' do
|
|
188
|
+
error = Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.rb')
|
|
189
|
+
|
|
190
|
+
expect { handler.handle_error(error, context: 'test', reraise: true) }
|
|
191
|
+
.to raise_error(SimpleCovMcp::FileNotFoundError)
|
|
192
|
+
|
|
193
|
+
# Verify it was logged
|
|
194
|
+
expect(logger.messages.join).to include('Error in test')
|
|
195
|
+
end
|
|
196
|
+
end
|
|
81
197
|
end
|