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,30 +3,30 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
RSpec.describe SimpleCovMcp::CoverageModel do
|
6
|
-
let(:root) { (
|
7
|
-
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
7
|
+
|
8
8
|
def with_stubbed_coverage_timestamp(ts) = begin
|
9
|
-
|
9
|
+
mock_resultset_with_timestamp(root, ts)
|
10
10
|
yield
|
11
11
|
end
|
12
12
|
|
13
13
|
it "raises stale error when staleness mode is 'error' and file is newer" do
|
14
|
-
with_stubbed_coverage_timestamp(
|
14
|
+
with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
|
15
15
|
model = described_class.new(root: root, staleness: 'error')
|
16
|
-
expect
|
16
|
+
expect do
|
17
17
|
model.summary_for('lib/foo.rb')
|
18
|
-
|
18
|
+
end.to raise_error(SimpleCovMcp::CoverageDataStaleError, /stale/i)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
it "does not check staleness when mode is 'off'" do
|
23
|
-
with_stubbed_coverage_timestamp(
|
23
|
+
with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
|
24
24
|
model = described_class.new(root: root, staleness: 'off')
|
25
25
|
expect { model.summary_for('lib/foo.rb') }.not_to raise_error
|
26
26
|
end
|
27
27
|
end
|
28
28
|
it 'all_files raises project-level stale when any source file is newer than coverage' do
|
29
|
-
with_stubbed_coverage_timestamp(
|
29
|
+
with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
|
30
30
|
model = described_class.new(root: root, staleness: 'error')
|
31
31
|
expect { model.all_files }.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
|
32
32
|
end
|
@@ -38,12 +38,42 @@ RSpec.describe SimpleCovMcp::CoverageModel do
|
|
38
38
|
begin
|
39
39
|
File.write(tmp, "# new file\n")
|
40
40
|
model = described_class.new(root: root, staleness: 'error')
|
41
|
-
expect
|
41
|
+
expect do
|
42
42
|
model.all_files(tracked_globs: ['lib/**/*.rb'])
|
43
|
-
|
43
|
+
end.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
|
44
44
|
ensure
|
45
45
|
File.delete(tmp) if File.exist?(tmp)
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
49
|
+
|
50
|
+
describe 'timestamp normalization' do
|
51
|
+
it 'parses created_at strings to epoch seconds' do
|
52
|
+
created_at = Time.new(2024, 7, 3, 16, 26, 40, '-07:00')
|
53
|
+
mock_resultset_with_created_at(root, created_at.strftime('%Y-%m-%d %H:%M:%S %z'))
|
54
|
+
|
55
|
+
model = described_class.new(root: root, staleness: 'off')
|
56
|
+
|
57
|
+
expect(model.instance_variable_get(:@cov_timestamp)).to eq(created_at.to_i)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'propagates parsed created_at timestamps into stale errors' do
|
61
|
+
file_mtime = File.mtime(File.join(root, 'lib', 'foo.rb'))
|
62
|
+
created_at_time = (file_mtime + 3600).utc
|
63
|
+
# Use mismatched coverage (3 lines instead of 4) to trigger staleness
|
64
|
+
mismatched_coverage = {
|
65
|
+
File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, nil] }
|
66
|
+
}
|
67
|
+
mock_resultset_with_created_at(root, created_at_time.iso8601, coverage: mismatched_coverage)
|
68
|
+
|
69
|
+
model = described_class.new(root: root, staleness: 'error')
|
70
|
+
|
71
|
+
expect(model.instance_variable_get(:@cov_timestamp)).to eq(created_at_time.to_i)
|
72
|
+
expect do
|
73
|
+
model.summary_for('lib/foo.rb')
|
74
|
+
end.to raise_error(SimpleCovMcp::CoverageDataStaleError) { |error|
|
75
|
+
expect(error.cov_timestamp).to eq(created_at_time.to_i)
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
49
79
|
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::OptionNormalizers do
|
6
|
+
describe '.normalize_sort_order' do
|
7
|
+
context 'with strict mode (default)' do
|
8
|
+
it 'normalizes "a" to :ascending' do
|
9
|
+
expect(described_class.normalize_sort_order('a')).to eq(:ascending)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'normalizes "ascending" to :ascending' do
|
13
|
+
expect(described_class.normalize_sort_order('ascending')).to eq(:ascending)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'normalizes "d" to :descending' do
|
17
|
+
expect(described_class.normalize_sort_order('d')).to eq(:descending)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'normalizes "descending" to :descending' do
|
21
|
+
expect(described_class.normalize_sort_order('descending')).to eq(:descending)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'is case-insensitive' do
|
25
|
+
expect(described_class.normalize_sort_order('ASCENDING')).to eq(:ascending)
|
26
|
+
expect(described_class.normalize_sort_order('Descending')).to eq(:descending)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'raises OptionParser::InvalidArgument for invalid values' do
|
30
|
+
expect { described_class.normalize_sort_order('invalid') }
|
31
|
+
.to raise_error(OptionParser::InvalidArgument, /invalid argument: invalid/)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'with strict: false' do
|
36
|
+
it 'returns nil for invalid values' do
|
37
|
+
expect(described_class.normalize_sort_order('invalid', strict: false)).to be_nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'still normalizes valid values' do
|
41
|
+
expect(described_class.normalize_sort_order('a', strict: false)).to eq(:ascending)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '.normalize_source_mode' do
|
47
|
+
context 'with strict mode (default)' do
|
48
|
+
it 'normalizes nil to :full' do
|
49
|
+
expect(described_class.normalize_source_mode(nil)).to eq(:full)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'normalizes empty string to :full' do
|
53
|
+
expect(described_class.normalize_source_mode('')).to eq(:full)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'normalizes "f" to :full' do
|
57
|
+
expect(described_class.normalize_source_mode('f')).to eq(:full)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'normalizes "full" to :full' do
|
61
|
+
expect(described_class.normalize_source_mode('full')).to eq(:full)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'normalizes "u" to :uncovered' do
|
65
|
+
expect(described_class.normalize_source_mode('u')).to eq(:uncovered)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'normalizes "uncovered" to :uncovered' do
|
69
|
+
expect(described_class.normalize_source_mode('uncovered')).to eq(:uncovered)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'is case-insensitive' do
|
73
|
+
expect(described_class.normalize_source_mode('FULL')).to eq(:full)
|
74
|
+
expect(described_class.normalize_source_mode('Uncovered')).to eq(:uncovered)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'raises OptionParser::InvalidArgument for invalid values' do
|
78
|
+
expect { described_class.normalize_source_mode('invalid') }
|
79
|
+
.to raise_error(OptionParser::InvalidArgument, /invalid argument: invalid/)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'with strict: false' do
|
84
|
+
it 'returns nil for invalid values' do
|
85
|
+
expect(described_class.normalize_source_mode('invalid', strict: false)).to be_nil
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'still normalizes valid values' do
|
89
|
+
expect(described_class.normalize_source_mode('u', strict: false)).to eq(:uncovered)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '.normalize_stale_mode' do
|
95
|
+
context 'with strict mode (default)' do
|
96
|
+
it 'normalizes "o" to :off' do
|
97
|
+
expect(described_class.normalize_stale_mode('o')).to eq(:off)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'normalizes "off" to :off' do
|
101
|
+
expect(described_class.normalize_stale_mode('off')).to eq(:off)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'normalizes "e" to :error' do
|
105
|
+
expect(described_class.normalize_stale_mode('e')).to eq(:error)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'normalizes "error" to :error' do
|
109
|
+
expect(described_class.normalize_stale_mode('error')).to eq(:error)
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'is case-insensitive' do
|
113
|
+
expect(described_class.normalize_stale_mode('OFF')).to eq(:off)
|
114
|
+
expect(described_class.normalize_stale_mode('Error')).to eq(:error)
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'raises OptionParser::InvalidArgument for invalid values' do
|
118
|
+
expect { described_class.normalize_stale_mode('invalid') }
|
119
|
+
.to raise_error(OptionParser::InvalidArgument, /invalid argument: invalid/)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'with strict: false' do
|
124
|
+
it 'returns nil for invalid values' do
|
125
|
+
expect(described_class.normalize_stale_mode('invalid', strict: false)).to be_nil
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'still normalizes valid values' do
|
129
|
+
expect(described_class.normalize_stale_mode('e', strict: false)).to eq(:error)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe '.normalize_error_mode' do
|
135
|
+
context 'with strict mode (default)' do
|
136
|
+
it 'normalizes "off" to :off' do
|
137
|
+
expect(described_class.normalize_error_mode('off')).to eq(:off)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'normalizes "on" to :on' do
|
141
|
+
expect(described_class.normalize_error_mode('on')).to eq(:on)
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'normalizes "trace" to :trace' do
|
145
|
+
expect(described_class.normalize_error_mode('trace')).to eq(:trace)
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'normalizes "t" to :trace' do
|
149
|
+
expect(described_class.normalize_error_mode('t')).to eq(:trace)
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'is case-insensitive' do
|
153
|
+
expect(described_class.normalize_error_mode('OFF')).to eq(:off)
|
154
|
+
expect(described_class.normalize_error_mode('On')).to eq(:on)
|
155
|
+
expect(described_class.normalize_error_mode('TRACE')).to eq(:trace)
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'raises OptionParser::InvalidArgument for invalid values' do
|
159
|
+
expect { described_class.normalize_error_mode('invalid') }
|
160
|
+
.to raise_error(OptionParser::InvalidArgument, /invalid argument: invalid/)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
context 'with strict: false and default: :on' do
|
165
|
+
it 'returns default for invalid values' do
|
166
|
+
expect(described_class.normalize_error_mode('invalid', strict: false,
|
167
|
+
default: :on)).to eq(:on)
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'returns default for nil' do
|
171
|
+
expect(described_class.normalize_error_mode(nil, strict: false, default: :on)).to eq(:on)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'still normalizes valid values' do
|
175
|
+
expect(described_class.normalize_error_mode('off', strict: false)).to eq(:off)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'with custom default' do
|
180
|
+
it 'returns custom default for invalid values when not strict' do
|
181
|
+
expect(described_class.normalize_error_mode('invalid', strict: false,
|
182
|
+
default: :off)).to eq(:off)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe 'constant maps' do
|
188
|
+
it 'has frozen SORT_ORDER_MAP' do
|
189
|
+
expect(described_class::SORT_ORDER_MAP).to be_frozen
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'has frozen SOURCE_MODE_MAP' do
|
193
|
+
expect(described_class::SOURCE_MODE_MAP).to be_frozen
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'has frozen STALE_MODE_MAP' do
|
197
|
+
expect(described_class::STALE_MODE_MAP).to be_frozen
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'has frozen ERROR_MODE_MAP' do
|
201
|
+
expect(described_class::ERROR_MODE_MAP).to be_frozen
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::OptionParsers::EnvOptionsParser do
|
6
|
+
let(:parser) { described_class.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 '#parse_env_opts' do
|
16
|
+
context 'with valid inputs' do
|
17
|
+
it 'returns empty array when environment variable is not set' do
|
18
|
+
ENV.delete('SIMPLECOV_MCP_OPTS')
|
19
|
+
expect(parser.parse_env_opts).to eq([])
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'returns empty array when environment variable is empty string' do
|
23
|
+
ENV['SIMPLECOV_MCP_OPTS'] = ''
|
24
|
+
expect(parser.parse_env_opts).to eq([])
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns empty array when environment variable contains only whitespace' do
|
28
|
+
ENV['SIMPLECOV_MCP_OPTS'] = ' '
|
29
|
+
expect(parser.parse_env_opts).to eq([])
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'parses simple options correctly' do
|
33
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--error-mode off --json'
|
34
|
+
expect(parser.parse_env_opts).to eq(['--error-mode', 'off', '--json'])
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'handles quoted strings with spaces' do
|
38
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--resultset "/path/to/my file.json"'
|
39
|
+
expect(parser.parse_env_opts).to eq(['--resultset', '/path/to/my file.json'])
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'handles complex shell escaping scenarios' do
|
43
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--resultset "/path/with spaces/file.json" --error-mode on'
|
44
|
+
expect(parser.parse_env_opts)
|
45
|
+
.to eq(['--resultset', '/path/with spaces/file.json', '--error-mode', 'on'])
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'handles single quotes' do
|
49
|
+
ENV['SIMPLECOV_MCP_OPTS'] = "--resultset '/path/with spaces/file.json'"
|
50
|
+
expect(parser.parse_env_opts).to eq(['--resultset', '/path/with spaces/file.json'])
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'handles escaped characters' do
|
54
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--resultset /path/with\\ spaces/file.json'
|
55
|
+
expect(parser.parse_env_opts).to eq(['--resultset', '/path/with spaces/file.json'])
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'handles mixed quoting styles' do
|
59
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--option1 "value with spaces" --option2 \'another value\''
|
60
|
+
expect(parser.parse_env_opts).to eq(
|
61
|
+
['--option1', 'value with spaces', '--option2', 'another value']
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'with malformed inputs' do
|
67
|
+
it 'raises ConfigurationError for unmatched double quotes' do
|
68
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--resultset "unterminated string'
|
69
|
+
|
70
|
+
expect do
|
71
|
+
parser.parse_env_opts
|
72
|
+
end.to raise_error(SimpleCovMcp::ConfigurationError, /Invalid SIMPLECOV_MCP_OPTS format/)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'raises ConfigurationError for unmatched single quotes' do
|
76
|
+
ENV['SIMPLECOV_MCP_OPTS'] = "--resultset 'unterminated string"
|
77
|
+
|
78
|
+
expect do
|
79
|
+
parser.parse_env_opts
|
80
|
+
end.to raise_error(SimpleCovMcp::ConfigurationError, /Invalid SIMPLECOV_MCP_OPTS format/)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'raises ConfigurationError with descriptive message' do
|
84
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '--option "bad quote'
|
85
|
+
|
86
|
+
expect do
|
87
|
+
parser.parse_env_opts
|
88
|
+
end.to raise_error(SimpleCovMcp::ConfigurationError) do |error|
|
89
|
+
expect(error.message).to include('Invalid SIMPLECOV_MCP_OPTS format')
|
90
|
+
expect(error.message).to include('Unmatched') # from Shellwords error
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'handles multiple quoting errors' do
|
95
|
+
ENV['SIMPLECOV_MCP_OPTS'] = '"first "second "third'
|
96
|
+
|
97
|
+
expect do
|
98
|
+
parser.parse_env_opts
|
99
|
+
end.to raise_error(SimpleCovMcp::ConfigurationError, /Invalid SIMPLECOV_MCP_OPTS format/)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#pre_scan_error_mode' do
|
105
|
+
let(:error_mode_normalizer) { parser.send(:method, :normalize_error_mode) }
|
106
|
+
|
107
|
+
context 'when error-mode is found' do
|
108
|
+
it 'extracts error-mode with space separator' do
|
109
|
+
argv = ['--error-mode', 'trace', '--other-option']
|
110
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: error_mode_normalizer)
|
111
|
+
expect(result).to eq(:trace)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'extracts error-mode with equals separator' do
|
115
|
+
argv = ['--error-mode=off', '--other-option']
|
116
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: error_mode_normalizer)
|
117
|
+
expect(result).to eq(:off)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'handles error-mode with equals but empty value' do
|
121
|
+
argv = ['--error-mode=', '--other-option']
|
122
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: error_mode_normalizer)
|
123
|
+
# Empty value after = explicitly returns nil (line 32)
|
124
|
+
expect(result).to be_nil
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'returns first error-mode when multiple are present' do
|
128
|
+
argv = ['--error-mode', 'on', '--error-mode', 'off']
|
129
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: error_mode_normalizer)
|
130
|
+
expect(result).to eq(:on)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'when error-mode is not found' do
|
135
|
+
it 'returns nil when no error-mode is present' do
|
136
|
+
argv = ['--other-option', 'value']
|
137
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: error_mode_normalizer)
|
138
|
+
expect(result).to be_nil
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'returns nil for empty argv' do
|
142
|
+
argv = []
|
143
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: error_mode_normalizer)
|
144
|
+
expect(result).to be_nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'error handling during pre-scan' do
|
149
|
+
it 'returns nil when normalizer raises an error' do
|
150
|
+
faulty_normalizer = ->(value) { raise StandardError, 'Intentional error' }
|
151
|
+
argv = ['--error-mode', 'on']
|
152
|
+
|
153
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: faulty_normalizer)
|
154
|
+
expect(result).to be_nil
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'returns nil when normalizer raises ArgumentError' do
|
158
|
+
faulty_normalizer = ->(value) { raise ArgumentError, 'Bad argument' }
|
159
|
+
argv = ['--error-mode', 'on']
|
160
|
+
|
161
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: faulty_normalizer)
|
162
|
+
expect(result).to be_nil
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'returns nil when normalizer raises RuntimeError' do
|
166
|
+
faulty_normalizer = ->(value) { raise RuntimeError, 'Runtime problem' }
|
167
|
+
argv = ['--error-mode=off']
|
168
|
+
|
169
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: faulty_normalizer)
|
170
|
+
expect(result).to be_nil
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe 'integration with ErrorHandlerFactory' do
|
176
|
+
it 'maps trace alias to an accepted error_mode' do
|
177
|
+
mode = parser.pre_scan_error_mode(['--error-mode', 'trace'])
|
178
|
+
expect { SimpleCovMcp::ErrorHandlerFactory.for_cli(error_mode: mode) }.not_to raise_error
|
179
|
+
expect(mode).to eq(:trace)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe '#normalize_error_mode (private)' do
|
184
|
+
it 'normalizes "off" to :off' do
|
185
|
+
expect(parser.send(:normalize_error_mode, 'off')).to eq(:off)
|
186
|
+
expect(parser.send(:normalize_error_mode, 'OFF')).to eq(:off)
|
187
|
+
expect(parser.send(:normalize_error_mode, 'Off')).to eq(:off)
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'normalizes "on" to :on' do
|
191
|
+
expect(parser.send(:normalize_error_mode, 'on')).to eq(:on)
|
192
|
+
expect(parser.send(:normalize_error_mode, 'ON')).to eq(:on)
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'normalizes "trace" to :trace' do
|
196
|
+
expect(parser.send(:normalize_error_mode, 'trace')).to eq(:trace)
|
197
|
+
expect(parser.send(:normalize_error_mode, 'TRACE')).to eq(:trace)
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'normalizes "t" to :trace' do
|
201
|
+
expect(parser.send(:normalize_error_mode, 't')).to eq(:trace)
|
202
|
+
expect(parser.send(:normalize_error_mode, 'T')).to eq(:trace)
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'defaults unknown values to :on' do
|
206
|
+
expect(parser.send(:normalize_error_mode, 'unknown')).to eq(:on)
|
207
|
+
expect(parser.send(:normalize_error_mode, 'invalid')).to eq(:on)
|
208
|
+
expect(parser.send(:normalize_error_mode, '')).to eq(:on)
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'handles nil by defaulting to :on' do
|
212
|
+
expect(parser.send(:normalize_error_mode, nil)).to eq(:on)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
describe 'custom environment variable name' do
|
217
|
+
it 'uses custom environment variable when specified' do
|
218
|
+
custom_parser = described_class.new(env_var: 'CUSTOM_OPTS')
|
219
|
+
ENV['CUSTOM_OPTS'] = '--error-mode off'
|
220
|
+
|
221
|
+
expect(custom_parser.parse_env_opts).to eq(['--error-mode', 'off'])
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'includes custom env var name in error messages' do
|
225
|
+
custom_parser = described_class.new(env_var: 'MY_CUSTOM_VAR')
|
226
|
+
ENV['MY_CUSTOM_VAR'] = '"bad quote'
|
227
|
+
|
228
|
+
expect do
|
229
|
+
custom_parser.parse_env_opts
|
230
|
+
end.to raise_error(SimpleCovMcp::ConfigurationError, /Invalid MY_CUSTOM_VAR format/)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|