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
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'CLI enumerated option parsing' do
|
6
|
+
def parse!(argv)
|
7
|
+
cli = SimpleCovMcp::CoverageCLI.new
|
8
|
+
cli.send(:parse_options!, argv.dup)
|
9
|
+
cli
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'accepts short and long forms' do
|
13
|
+
cases = [
|
14
|
+
{ argv: ['--sort-order', 'a', 'list'], accessor: :sort_order, expected: :ascending },
|
15
|
+
{ argv: ['--sort-order', 'd', 'list'], accessor: :sort_order, expected: :descending },
|
16
|
+
{ argv: ['--sort-order', 'ascending', 'list'], accessor: :sort_order, expected: :ascending },
|
17
|
+
{ argv: ['--sort-order', 'descending', 'list'], accessor: :sort_order,
|
18
|
+
expected: :descending },
|
19
|
+
|
20
|
+
{ argv: ['--source', 'f', 'summary', 'lib/foo.rb'], accessor: :source_mode, expected: :full },
|
21
|
+
{ argv: ['--source=u', 'summary', 'lib/foo.rb'], accessor: :source_mode,
|
22
|
+
expected: :uncovered },
|
23
|
+
{ argv: ['--source=full', 'summary', 'lib/foo.rb'], accessor: :source_mode, expected: :full },
|
24
|
+
{ argv: ['--source=uncovered', 'summary', 'lib/foo.rb'], accessor: :source_mode,
|
25
|
+
expected: :uncovered },
|
26
|
+
|
27
|
+
{ argv: ['-S', 'e', 'list'], accessor: :stale_mode, expected: :error },
|
28
|
+
{ argv: ['-S', 'o', 'list'], accessor: :stale_mode, expected: :off },
|
29
|
+
|
30
|
+
{ argv: ['--error-mode', 'off', 'list'], accessor: :error_mode, expected: :off },
|
31
|
+
{ argv: ['--error-mode', 'on', 'list'], accessor: :error_mode, expected: :on },
|
32
|
+
{ argv: ['--error-mode', 't', 'list'], accessor: :error_mode, expected: :trace }
|
33
|
+
]
|
34
|
+
|
35
|
+
cases.each do |c|
|
36
|
+
it "parses #{c[:argv].join(' ')}" do
|
37
|
+
cli = parse!(c[:argv])
|
38
|
+
expect(cli.config.public_send(c[:accessor])).to eq(c[:expected])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'rejects invalid values' do
|
44
|
+
invalid_cases = [
|
45
|
+
{ argv: ['--sort-order', 'asc', 'list'] },
|
46
|
+
{ argv: ['--source=x', 'summary', 'lib/foo.rb'] },
|
47
|
+
{ argv: ['-S', 'x', 'list'] },
|
48
|
+
{ argv: ['--error-mode', 'bad', 'list'] }
|
49
|
+
]
|
50
|
+
|
51
|
+
invalid_cases.each do |c|
|
52
|
+
it "exits 1 for #{c[:argv].join(' ')}" do
|
53
|
+
_out, err, status = run_cli_with_status(*c[:argv])
|
54
|
+
expect(status).to eq(1)
|
55
|
+
expect(err).to include('Error:')
|
56
|
+
expect(err).to include('invalid argument')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'missing value hints' do
|
62
|
+
it 'exits 1 when -S is provided without a value' do
|
63
|
+
_out, err, status = run_cli_with_status('-S', 'list')
|
64
|
+
expect(status).to eq(1)
|
65
|
+
expect(err).to include('invalid argument')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/spec/cli_error_spec.rb
CHANGED
@@ -3,30 +3,16 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
RSpec.describe SimpleCovMcp::CoverageCLI do
|
6
|
-
let(:root) { (
|
7
|
-
|
8
|
-
def run_cli_with_status(*argv)
|
9
|
-
cli = described_class.new
|
10
|
-
status = nil
|
11
|
-
out_str = err_str = nil
|
12
|
-
silence_output do |out, err|
|
13
|
-
begin
|
14
|
-
cli.run(argv.flatten)
|
15
|
-
status = 0
|
16
|
-
rescue SystemExit => e
|
17
|
-
status = e.status
|
18
|
-
end
|
19
|
-
out_str = out.string
|
20
|
-
err_str = err.string
|
21
|
-
end
|
22
|
-
[out_str, err_str, status]
|
23
|
-
end
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
24
7
|
|
25
8
|
it 'shows help and exits 0' do
|
26
9
|
out, err, status = run_cli_with_status('--help')
|
27
10
|
expect(status).to eq(0)
|
28
|
-
expect(out).to
|
29
|
-
expect(
|
11
|
+
expect(out).to match(/Usage:.*simplecov-mcp/)
|
12
|
+
expect(out).to include('Repository: https://github.com/keithrbennett/simplecov-mcp')
|
13
|
+
expect(out).to match(/Version:.*#{SimpleCovMcp::VERSION}/)
|
14
|
+
expect(out).to include('Subcommands:')
|
15
|
+
expect(err).to eq('')
|
30
16
|
end
|
31
17
|
|
32
18
|
shared_examples 'maps error to exit 1 with message' do
|
@@ -64,40 +50,112 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
64
50
|
end
|
65
51
|
|
66
52
|
it 'emits detailed stale coverage info and exits 1' do
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
53
|
+
mock_resultset_with_timestamp(root, VERY_OLD_TIMESTAMP, coverage: {
|
54
|
+
File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, 1] }
|
55
|
+
})
|
56
|
+
|
57
|
+
_out, err, status = run_cli_with_status('summary', 'lib/foo.rb', '--root', root, '--resultset',
|
58
|
+
'coverage', '--stale', 'error')
|
59
|
+
expect(status).to eq(1)
|
60
|
+
expect(err).to include('Coverage data stale:')
|
61
|
+
expect(err).to match(/File\s+- time:/)
|
62
|
+
expect(err).to match('Coverage\s+- time:')
|
63
|
+
expect(err).to match(/Delta\s+- file is [+-]?\d+s newer than coverage/)
|
64
|
+
expect(err).to match('Resultset\s+-')
|
78
65
|
end
|
79
66
|
|
80
67
|
it 'honors --no-strict-staleness to disable checks' do
|
68
|
+
mock_resultset_with_timestamp(root, VERY_OLD_TIMESTAMP, coverage: {
|
69
|
+
File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, 1] }
|
70
|
+
})
|
71
|
+
|
72
|
+
_out, err, status = run_cli_with_status('summary', 'lib/foo.rb', '--root', root, '--resultset',
|
73
|
+
'coverage', '--stale', 'off')
|
74
|
+
expect(status).to eq(0)
|
75
|
+
expect(err).to eq('')
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'handles source rendering errors gracefully with fallback message' do
|
79
|
+
# Test that source rendering with problematic coverage data doesn't crash
|
80
|
+
# This is a regression test for the "can't convert nil into Integer" crash
|
81
|
+
# that was previously mentioned in comments
|
82
|
+
out, err, status = run_cli_with_status(
|
83
|
+
'uncovered', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
|
84
|
+
'--source=uncovered', '--source-context', '2', '--no-color'
|
85
|
+
)
|
86
|
+
|
87
|
+
expect(status).to eq(0)
|
88
|
+
expect(err).to eq('')
|
89
|
+
expect(out).to match(/File:\s+lib\/foo\.rb/)
|
90
|
+
expect(out).to include('Uncovered lines: 2')
|
91
|
+
expect(out).to show_source_table_or_fallback
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'renders source with full mode without crashing' do
|
95
|
+
# Additional regression test for source rendering with full mode
|
96
|
+
out, err, status = run_cli_with_status(
|
97
|
+
'summary', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
|
98
|
+
'--source=full', '--no-color'
|
99
|
+
)
|
100
|
+
|
101
|
+
expect(status).to eq(0)
|
102
|
+
expect(err).to eq('')
|
103
|
+
expect(out).to include('lib/foo.rb')
|
104
|
+
expect(out).to include('66.67%')
|
105
|
+
expect(out).to show_source_table_or_fallback
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'shows fallback message when source file is unreadable' do
|
109
|
+
# Test the fallback path when source files can't be read
|
110
|
+
# Temporarily rename the source file to make it unreadable
|
111
|
+
foo_path = File.join(root, 'lib', 'foo.rb')
|
112
|
+
temp_path = "#{foo_path}.hidden"
|
113
|
+
|
81
114
|
begin
|
82
|
-
|
83
|
-
|
115
|
+
File.rename(foo_path, temp_path) if File.exist?(foo_path)
|
116
|
+
|
117
|
+
out, err, status = run_cli_with_status(
|
118
|
+
'summary', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
|
119
|
+
'--source=full', '--no-color'
|
120
|
+
)
|
121
|
+
|
84
122
|
expect(status).to eq(0)
|
85
|
-
expect(err).to eq(
|
123
|
+
expect(err).to eq('')
|
124
|
+
expect(out).to include('lib/foo.rb')
|
125
|
+
expect(out).to include('66.67%')
|
126
|
+
expect(out).to include('[source not available]')
|
86
127
|
ensure
|
128
|
+
# Restore the file
|
129
|
+
File.rename(temp_path, foo_path) if File.exist?(temp_path)
|
87
130
|
end
|
88
131
|
end
|
89
132
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
133
|
+
describe 'invalid option handling' do
|
134
|
+
it 'suggests subcommand for --subcommand-like option' do
|
135
|
+
_out, err, status = run_cli_with_status('--summary')
|
136
|
+
expect(status).to eq(1)
|
137
|
+
expect(err).to include(
|
138
|
+
"Error: '--summary' is not a valid option. Did you mean the 'summary' subcommand?"
|
139
|
+
)
|
140
|
+
expect(err).to include('Try: simplecov-mcp summary [args]')
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'reports invalid enum value for --opt=value' do
|
144
|
+
_out, err, status = run_cli_with_status('list', '--stale=bogus')
|
145
|
+
expect(status).to eq(1)
|
146
|
+
expect(err).to include('invalid argument: --stale=bogus')
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'reports invalid enum value for --opt value' do
|
150
|
+
_out, err, status = run_cli_with_status('list', '--stale', 'bogus')
|
151
|
+
expect(status).to eq(1)
|
152
|
+
expect(err).to include('invalid argument: bogus')
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'handles generic invalid options' do
|
156
|
+
_out, err, status = run_cli_with_status('--no-such-option')
|
157
|
+
expect(status).to eq(1)
|
158
|
+
expect(err).to include('Error: invalid option: --no-such-option')
|
159
|
+
end
|
160
|
+
end
|
103
161
|
end
|
data/spec/cli_source_spec.rb
CHANGED
@@ -3,24 +3,7 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
RSpec.describe SimpleCovMcp::CoverageCLI do
|
6
|
-
let(:root) { (
|
7
|
-
|
8
|
-
def run_cli_with_status(*argv)
|
9
|
-
cli = described_class.new
|
10
|
-
status = nil
|
11
|
-
out_str = err_str = nil
|
12
|
-
silence_output do |out, err|
|
13
|
-
begin
|
14
|
-
cli.run(argv.flatten)
|
15
|
-
status = 0
|
16
|
-
rescue SystemExit => e
|
17
|
-
status = e.status
|
18
|
-
end
|
19
|
-
out_str = out.string
|
20
|
-
err_str = err.string
|
21
|
-
end
|
22
|
-
[out_str, err_str, status]
|
23
|
-
end
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
24
7
|
|
25
8
|
it 'renders uncovered source without error for fixture file' do
|
26
9
|
out, err, status = run_cli_with_status(
|
@@ -28,10 +11,86 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
28
11
|
'--source=uncovered', '--source-context', '1', '--no-color'
|
29
12
|
)
|
30
13
|
expect(status).to eq(0)
|
31
|
-
expect(err).to eq(
|
32
|
-
expect(out).to
|
33
|
-
expect(out).to
|
34
|
-
|
35
|
-
|
14
|
+
expect(err).to eq('')
|
15
|
+
expect(out).to match(/File:\s+lib\/foo\.rb/)
|
16
|
+
expect(out).to match(/Uncovered lines:\s*2\b/)
|
17
|
+
expect(out).to show_source_table_or_fallback
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'renders full source for uncovered command without brittle spacing' do
|
21
|
+
out, err, status = run_cli_with_status(
|
22
|
+
'uncovered', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
|
23
|
+
'--source=full', '--no-color'
|
24
|
+
)
|
25
|
+
expect(status).to eq(0)
|
26
|
+
expect(err).to eq('')
|
27
|
+
# Summary line with flexible spacing
|
28
|
+
expect(out).to match(/Summary:\s*\d+\.\d{2}%\s*\d+\/\d+/)
|
29
|
+
expect(out).to show_source_table_or_fallback
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'renders source for summary with uncovered mode without crashing' do
|
33
|
+
out, err, status = run_cli_with_status(
|
34
|
+
'summary', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
|
35
|
+
'--source=uncovered', '--source-context', '1', '--no-color'
|
36
|
+
)
|
37
|
+
expect(status).to eq(0)
|
38
|
+
expect(err).to eq('')
|
39
|
+
expect(out).to include('lib/foo.rb')
|
40
|
+
# Presence of percentage and counts, spacing-agnostic
|
41
|
+
expect(out).to match(/66\.67%/)
|
42
|
+
expect(out).to match(/\b2\/3\b/)
|
43
|
+
expect(out).to show_source_table_or_fallback
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'source option without equals sign' do
|
47
|
+
it 'parses --source uncovered correctly (space-separated argument)' do
|
48
|
+
out, err, status = run_cli_with_status(
|
49
|
+
'summary', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
|
50
|
+
'--source', 'uncovered', '--source-context', '1', '--no-color'
|
51
|
+
)
|
52
|
+
expect(status).to eq(0)
|
53
|
+
expect(err).to eq('')
|
54
|
+
expect(out).to include('lib/foo.rb')
|
55
|
+
expect(out).to match(/66\.67%/)
|
56
|
+
expect(out).to match(/\b2\/3\b/)
|
57
|
+
expect(out).to show_source_table_or_fallback
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'parses -s full correctly (short form with space-separated argument)' do
|
61
|
+
out, err, status = run_cli_with_status(
|
62
|
+
'uncovered', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
|
63
|
+
'-s', 'full', '--no-color'
|
64
|
+
)
|
65
|
+
expect(status).to eq(0)
|
66
|
+
expect(err).to eq('')
|
67
|
+
expect(out).to match(/Summary:\s*\d+\.\d{2}%\s*\d+\/\d+/)
|
68
|
+
expect(out).to show_source_table_or_fallback
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'handles --source uncovered in default report (no subcommand)' do
|
72
|
+
out, err, status = run_cli_with_status(
|
73
|
+
'--root', root, '--resultset', 'coverage',
|
74
|
+
'--source', 'uncovered', '--no-color'
|
75
|
+
)
|
76
|
+
expect(status).to eq(0)
|
77
|
+
expect(err).to eq('')
|
78
|
+
expect(out).to match(/66\.67%/)
|
79
|
+
# Default report doesn't show source tables, that's OK - just check it parses correctly
|
80
|
+
expect(out).not_to include('Unknown subcommand')
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'does not misinterpret following token as subcommand when using --source' do
|
84
|
+
# This test specifically addresses the bug where --source uncovered
|
85
|
+
# was interpreting 'uncovered' as a subcommand
|
86
|
+
out, err, status = run_cli_with_status(
|
87
|
+
'--root', root, '--resultset', 'coverage',
|
88
|
+
'--source', 'uncovered'
|
89
|
+
)
|
90
|
+
expect(status).to eq(0)
|
91
|
+
expect(err).to eq('')
|
92
|
+
expect(out).not_to include('Unknown subcommand')
|
93
|
+
expect(out).to match(/66\.67%/)
|
94
|
+
end
|
36
95
|
end
|
37
96
|
end
|
data/spec/cli_spec.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'spec_helper'
|
4
|
+
require 'tempfile'
|
4
5
|
|
5
6
|
RSpec.describe SimpleCovMcp::CoverageCLI do
|
6
|
-
let(:root) { (
|
7
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
7
8
|
|
8
9
|
def run_cli(*argv)
|
9
10
|
cli = described_class.new
|
@@ -27,6 +28,12 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
27
28
|
expect(data['lines']).to eq([1, 0, nil, 2])
|
28
29
|
end
|
29
30
|
|
31
|
+
it 'prints raw lines as text' do
|
32
|
+
output = run_cli('raw', 'lib/foo.rb', '--root', root, '--resultset', 'coverage')
|
33
|
+
expect(output).to include('File: lib/foo.rb')
|
34
|
+
expect(output).to include('[1, 0, nil, 2]')
|
35
|
+
end
|
36
|
+
|
30
37
|
it 'prints uncovered lines as JSON' do
|
31
38
|
output = run_cli('uncovered', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage')
|
32
39
|
data = JSON.parse(output)
|
@@ -41,8 +48,9 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
41
48
|
expect(data['lines'].first).to include('line', 'hits', 'covered')
|
42
49
|
end
|
43
50
|
|
44
|
-
it '
|
45
|
-
output = run_cli('list', '--json', '--root', root, '--resultset', 'coverage', '--sort-order',
|
51
|
+
it 'list subcommand with --json outputs JSON with sort order' do
|
52
|
+
output = run_cli('list', '--json', '--root', root, '--resultset', 'coverage', '--sort-order',
|
53
|
+
'a')
|
46
54
|
asc = JSON.parse(output)
|
47
55
|
expect(asc['files']).to be_an(Array)
|
48
56
|
expect(asc['files'].first['file']).to end_with('lib/bar.rb')
|
@@ -55,18 +63,145 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
55
63
|
expect(total).to eq(asc['files'].length)
|
56
64
|
expect(ok + stale).to eq(total)
|
57
65
|
|
58
|
-
output = run_cli('list', '--json', '--root', root, '--resultset', 'coverage', '--sort-order',
|
66
|
+
output = run_cli('list', '--json', '--root', root, '--resultset', 'coverage', '--sort-order',
|
67
|
+
'd')
|
59
68
|
desc = JSON.parse(output)
|
60
69
|
expect(desc['files'].first['file']).to end_with('lib/foo.rb')
|
61
70
|
end
|
62
71
|
|
72
|
+
it 'list subcommand outputs formatted table' do
|
73
|
+
output = run_cli('list', '--root', root, '--resultset', 'coverage')
|
74
|
+
expect(output).to include('File')
|
75
|
+
expect(output).to include('lib/foo.rb')
|
76
|
+
expect(output).to include('lib/bar.rb')
|
77
|
+
expect(output).to match(/Files: total \d+/)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'list subcommand retains rows when using an absolute tracked glob' do
|
81
|
+
absolute_glob = File.join(root, 'lib', '**', '*.rb')
|
82
|
+
output = run_cli('list', '--root', root, '--resultset', 'coverage', '--tracked-globs',
|
83
|
+
absolute_glob)
|
84
|
+
expect(output).not_to include('No coverage data found')
|
85
|
+
expect(output).to include('lib/foo.rb')
|
86
|
+
expect(output).to include('lib/bar.rb')
|
87
|
+
end
|
88
|
+
|
63
89
|
it 'exposes expected subcommands via constant' do
|
64
90
|
expect(described_class::SUBCOMMANDS).to eq(%w[list summary raw uncovered detailed version])
|
65
91
|
end
|
66
92
|
|
67
93
|
it 'can include source in JSON payload (nil if file missing)' do
|
68
|
-
output = run_cli('summary', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage',
|
94
|
+
output = run_cli('summary', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage',
|
95
|
+
'--source')
|
69
96
|
data = JSON.parse(output)
|
70
97
|
expect(data).to have_key('source')
|
71
98
|
end
|
99
|
+
|
100
|
+
describe 'log file configuration' do
|
101
|
+
it 'passes --log-file path into the CLI execution context' do
|
102
|
+
Dir.mktmpdir do |dir|
|
103
|
+
log_path = File.join(dir, 'custom.log')
|
104
|
+
expect(SimpleCovMcp).to receive(:create_context)
|
105
|
+
.and_wrap_original do |m, error_handler:, log_target:, mode:|
|
106
|
+
# Ensure CLI forwards the requested log path into the context without changing other fields.
|
107
|
+
expect(log_target).to eq(log_path)
|
108
|
+
m.call(error_handler: error_handler, log_target: log_target, mode: mode)
|
109
|
+
end
|
110
|
+
original_target = SimpleCovMcp.active_log_file
|
111
|
+
run_cli('summary', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage',
|
112
|
+
'--log-file', log_path)
|
113
|
+
expect(SimpleCovMcp.active_log_file).to eq(original_target)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'supports stdout logging within the CLI context' do
|
118
|
+
expect(SimpleCovMcp).to receive(:create_context)
|
119
|
+
.and_wrap_original do |m, error_handler:, log_target:, mode:|
|
120
|
+
# For stdout logging, verify the context is still constructed with the expected value.
|
121
|
+
expect(log_target).to eq('stdout')
|
122
|
+
m.call(error_handler: error_handler, log_target: log_target, mode: mode)
|
123
|
+
end
|
124
|
+
original_target = SimpleCovMcp.active_log_file
|
125
|
+
run_cli('summary', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage',
|
126
|
+
'--log-file', 'stdout')
|
127
|
+
expect(SimpleCovMcp.active_log_file).to eq(original_target)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe '#load_success_predicate' do
|
132
|
+
let(:cli) { described_class.new }
|
133
|
+
|
134
|
+
def with_temp_predicate(content)
|
135
|
+
Tempfile.create(['predicate', '.rb']) do |file|
|
136
|
+
file.write(content)
|
137
|
+
file.flush
|
138
|
+
yield file.path
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'loads a callable predicate from file' do
|
143
|
+
with_temp_predicate("->(model) { model }\n") do |path|
|
144
|
+
predicate = cli.send(:load_success_predicate, path)
|
145
|
+
expect(predicate).to respond_to(:call)
|
146
|
+
expect(predicate.call(:ok)).to eq(:ok)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'raises when file does not return callable' do
|
151
|
+
with_temp_predicate(":not_callable\n") do |path|
|
152
|
+
expect { cli.send(:load_success_predicate, path) }
|
153
|
+
.to raise_error(RuntimeError, include('Success predicate must be callable'))
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'wraps syntax errors with friendly message' do
|
158
|
+
with_temp_predicate("->(model) {\n") do |path|
|
159
|
+
expect { cli.send(:load_success_predicate, path) }
|
160
|
+
.to raise_error(RuntimeError, include('Syntax error in success predicate file'))
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe '#extract_subcommand!' do
|
166
|
+
let(:cli) { described_class.new }
|
167
|
+
|
168
|
+
around do |example|
|
169
|
+
original = ENV['SIMPLECOV_MCP_OPTS']
|
170
|
+
example.run
|
171
|
+
ensure
|
172
|
+
ENV['SIMPLECOV_MCP_OPTS'] = original
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'picks up subcommands that appear after env-provided options' do
|
176
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--resultset coverage'
|
177
|
+
argv = cli.send(:parse_env_opts) + ['summary', 'lib/foo.rb']
|
178
|
+
|
179
|
+
expect do
|
180
|
+
cli.send(:extract_subcommand!, argv)
|
181
|
+
end.to change { cli.instance_variable_get(:@cmd) }.from(nil).to('summary')
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
describe 'version command' do
|
186
|
+
it 'prints version as plain text by default' do
|
187
|
+
output = run_cli('version')
|
188
|
+
expect(output).to include('SimpleCovMcp version')
|
189
|
+
expect(output).to include(SimpleCovMcp::VERSION)
|
190
|
+
expect(output).not_to include('{')
|
191
|
+
expect(output).not_to include('}')
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'prints version as JSON when --json flag is used' do
|
195
|
+
output = run_cli('version', '--json')
|
196
|
+
data = JSON.parse(output)
|
197
|
+
expect(data).to have_key('version')
|
198
|
+
expect(data['version']).to eq(SimpleCovMcp::VERSION)
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'works with version command and other flags' do
|
202
|
+
output = run_cli('version', '--root', root)
|
203
|
+
expect(output).to include('SimpleCovMcp version')
|
204
|
+
expect(output).to include(SimpleCovMcp::VERSION)
|
205
|
+
end
|
206
|
+
end
|
72
207
|
end
|