cov-loupe 3.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +329 -0
- data/docs/dev/ARCHITECTURE.md +80 -0
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
- data/docs/dev/DEVELOPMENT.md +83 -0
- data/docs/dev/README.md +10 -0
- data/docs/dev/RELEASING.md +146 -0
- data/docs/dev/arch-decisions/001-x-arch-decision.md +95 -0
- data/docs/dev/arch-decisions/002-x-arch-decision.md +159 -0
- data/docs/dev/arch-decisions/003-x-arch-decision.md +165 -0
- data/docs/dev/arch-decisions/004-x-arch-decision.md +203 -0
- data/docs/dev/arch-decisions/005-x-arch-decision.md +189 -0
- data/docs/dev/arch-decisions/README.md +60 -0
- data/docs/dev/presentations/cov-loupe-presentation.md +255 -0
- data/docs/fixtures/demo_project/README.md +9 -0
- data/docs/user/ADVANCED_USAGE.md +777 -0
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
- data/docs/user/CLI_USAGE.md +750 -0
- data/docs/user/ERROR_HANDLING.md +93 -0
- data/docs/user/EXAMPLES.md +588 -0
- data/docs/user/INSTALLATION.md +130 -0
- data/docs/user/LIBRARY_API.md +693 -0
- data/docs/user/MCP_INTEGRATION.md +490 -0
- data/docs/user/README.md +14 -0
- data/docs/user/TROUBLESHOOTING.md +197 -0
- data/docs/user/V2-BREAKING-CHANGES.md +472 -0
- data/exe/cov-loupe +23 -0
- data/lib/cov_loupe/app_config.rb +56 -0
- data/lib/cov_loupe/app_context.rb +26 -0
- data/lib/cov_loupe/base_tool.rb +102 -0
- data/lib/cov_loupe/cli.rb +178 -0
- data/lib/cov_loupe/commands/base_command.rb +67 -0
- data/lib/cov_loupe/commands/command_factory.rb +45 -0
- data/lib/cov_loupe/commands/detailed_command.rb +38 -0
- data/lib/cov_loupe/commands/list_command.rb +13 -0
- data/lib/cov_loupe/commands/raw_command.rb +38 -0
- data/lib/cov_loupe/commands/summary_command.rb +41 -0
- data/lib/cov_loupe/commands/totals_command.rb +53 -0
- data/lib/cov_loupe/commands/uncovered_command.rb +45 -0
- data/lib/cov_loupe/commands/validate_command.rb +60 -0
- data/lib/cov_loupe/commands/version_command.rb +33 -0
- data/lib/cov_loupe/config_parser.rb +32 -0
- data/lib/cov_loupe/constants.rb +22 -0
- data/lib/cov_loupe/coverage_reporter.rb +31 -0
- data/lib/cov_loupe/error_handler.rb +165 -0
- data/lib/cov_loupe/error_handler_factory.rb +31 -0
- data/lib/cov_loupe/errors.rb +191 -0
- data/lib/cov_loupe/formatters/source_formatter.rb +152 -0
- data/lib/cov_loupe/formatters.rb +51 -0
- data/lib/cov_loupe/mcp_server.rb +42 -0
- data/lib/cov_loupe/mode_detector.rb +56 -0
- data/lib/cov_loupe/model.rb +339 -0
- data/lib/cov_loupe/option_normalizers.rb +113 -0
- data/lib/cov_loupe/option_parser_builder.rb +147 -0
- data/lib/cov_loupe/option_parsers/env_options_parser.rb +48 -0
- data/lib/cov_loupe/option_parsers/error_helper.rb +110 -0
- data/lib/cov_loupe/path_relativizer.rb +64 -0
- data/lib/cov_loupe/predicate_evaluator.rb +72 -0
- data/lib/cov_loupe/presenters/base_coverage_presenter.rb +42 -0
- data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/project_coverage_presenter.rb +50 -0
- data/lib/cov_loupe/presenters/project_totals_presenter.rb +27 -0
- data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +122 -0
- data/lib/cov_loupe/resolvers/resolver_factory.rb +28 -0
- data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +76 -0
- data/lib/cov_loupe/resultset_loader.rb +131 -0
- data/lib/cov_loupe/staleness_checker.rb +247 -0
- data/lib/cov_loupe/table_formatter.rb +64 -0
- data/lib/cov_loupe/tools/all_files_coverage_tool.rb +51 -0
- data/lib/cov_loupe/tools/coverage_detailed_tool.rb +35 -0
- data/lib/cov_loupe/tools/coverage_raw_tool.rb +34 -0
- data/lib/cov_loupe/tools/coverage_summary_tool.rb +34 -0
- data/lib/cov_loupe/tools/coverage_table_tool.rb +50 -0
- data/lib/cov_loupe/tools/coverage_totals_tool.rb +44 -0
- data/lib/cov_loupe/tools/help_tool.rb +115 -0
- data/lib/cov_loupe/tools/uncovered_lines_tool.rb +34 -0
- data/lib/cov_loupe/tools/validate_tool.rb +72 -0
- data/lib/cov_loupe/tools/version_tool.rb +32 -0
- data/lib/cov_loupe/util.rb +88 -0
- data/lib/cov_loupe/version.rb +5 -0
- data/lib/cov_loupe.rb +140 -0
- data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
- data/spec/TIMESTAMPS.md +48 -0
- data/spec/all_files_coverage_tool_spec.rb +53 -0
- data/spec/app_config_spec.rb +142 -0
- data/spec/base_tool_spec.rb +62 -0
- data/spec/cli/show_default_report_spec.rb +33 -0
- data/spec/cli_enumerated_options_spec.rb +90 -0
- data/spec/cli_error_spec.rb +184 -0
- data/spec/cli_format_spec.rb +123 -0
- data/spec/cli_json_options_spec.rb +50 -0
- data/spec/cli_source_spec.rb +44 -0
- data/spec/cli_spec.rb +192 -0
- data/spec/cli_table_spec.rb +28 -0
- data/spec/cli_usage_spec.rb +42 -0
- data/spec/commands/base_command_spec.rb +107 -0
- data/spec/commands/command_factory_spec.rb +76 -0
- data/spec/commands/detailed_command_spec.rb +34 -0
- data/spec/commands/list_command_spec.rb +28 -0
- data/spec/commands/raw_command_spec.rb +69 -0
- data/spec/commands/summary_command_spec.rb +34 -0
- data/spec/commands/totals_command_spec.rb +34 -0
- data/spec/commands/uncovered_command_spec.rb +55 -0
- data/spec/commands/validate_command_spec.rb +213 -0
- data/spec/commands/version_command_spec.rb +38 -0
- data/spec/constants_spec.rb +61 -0
- data/spec/cov_loupe/formatters/source_formatter_spec.rb +267 -0
- data/spec/cov_loupe/formatters_spec.rb +76 -0
- data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +79 -0
- data/spec/cov_loupe_model_spec.rb +454 -0
- data/spec/cov_loupe_module_spec.rb +37 -0
- data/spec/cov_loupe_opts_spec.rb +185 -0
- data/spec/coverage_reporter_spec.rb +102 -0
- data/spec/coverage_table_tool_spec.rb +59 -0
- data/spec/coverage_totals_tool_spec.rb +37 -0
- data/spec/error_handler_spec.rb +197 -0
- data/spec/error_mode_spec.rb +139 -0
- data/spec/errors_edge_cases_spec.rb +312 -0
- data/spec/errors_stale_spec.rb +83 -0
- data/spec/file_based_mcp_tools_spec.rb +99 -0
- data/spec/fixtures/project1/lib/bar.rb +5 -0
- data/spec/fixtures/project1/lib/foo.rb +6 -0
- data/spec/help_tool_spec.rb +26 -0
- data/spec/integration_spec.rb +789 -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 +106 -0
- data/spec/mode_detector_spec.rb +153 -0
- data/spec/model_error_handling_spec.rb +269 -0
- data/spec/model_staleness_spec.rb +79 -0
- data/spec/option_normalizers_spec.rb +203 -0
- data/spec/option_parsers/env_options_parser_spec.rb +221 -0
- data/spec/option_parsers/error_helper_spec.rb +222 -0
- data/spec/path_relativizer_spec.rb +98 -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 +87 -0
- data/spec/presenters/project_totals_presenter_spec.rb +144 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +282 -0
- data/spec/resolvers/resolver_factory_spec.rb +61 -0
- data/spec/resolvers/resultset_path_resolver_spec.rb +60 -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 +179 -0
- data/spec/shared_examples/formatted_command_examples.rb +64 -0
- data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
- data/spec/spec_helper.rb +127 -0
- data/spec/staleness_checker_spec.rb +374 -0
- data/spec/staleness_more_spec.rb +42 -0
- 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 +66 -0
- 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 +130 -0
- data/spec/util_spec.rb +154 -0
- data/spec/version_spec.rb +123 -0
- data/spec/version_tool_spec.rb +141 -0
- metadata +290 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe::OptionParsers::EnvOptionsParser do
|
|
6
|
+
let(:parser) { described_class.new }
|
|
7
|
+
|
|
8
|
+
around do |example|
|
|
9
|
+
original_value = ENV['COV_LOUPE_OPTS']
|
|
10
|
+
example.run
|
|
11
|
+
ensure
|
|
12
|
+
ENV['COV_LOUPE_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('COV_LOUPE_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['COV_LOUPE_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['COV_LOUPE_OPTS'] = ' '
|
|
29
|
+
expect(parser.parse_env_opts).to eq([])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'parses simple options correctly' do
|
|
33
|
+
ENV['COV_LOUPE_OPTS'] = '--error-mode off --format json'
|
|
34
|
+
expect(parser.parse_env_opts).to eq(['--error-mode', 'off', '--format', 'json'])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'handles quoted strings with spaces' do
|
|
38
|
+
ENV['COV_LOUPE_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['COV_LOUPE_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['COV_LOUPE_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['COV_LOUPE_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['COV_LOUPE_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['COV_LOUPE_OPTS'] = '--resultset "unterminated string'
|
|
69
|
+
|
|
70
|
+
expect do
|
|
71
|
+
parser.parse_env_opts
|
|
72
|
+
end.to raise_error(CovLoupe::ConfigurationError, /Invalid COV_LOUPE_OPTS format/)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'raises ConfigurationError for unmatched single quotes' do
|
|
76
|
+
ENV['COV_LOUPE_OPTS'] = "--resultset 'unterminated string"
|
|
77
|
+
|
|
78
|
+
expect do
|
|
79
|
+
parser.parse_env_opts
|
|
80
|
+
end.to raise_error(CovLoupe::ConfigurationError, /Invalid COV_LOUPE_OPTS format/)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'raises ConfigurationError with descriptive message' do
|
|
84
|
+
ENV['COV_LOUPE_OPTS'] = '--option "bad quote'
|
|
85
|
+
|
|
86
|
+
expect do
|
|
87
|
+
parser.parse_env_opts
|
|
88
|
+
end.to raise_error(CovLoupe::ConfigurationError) do |error|
|
|
89
|
+
expect(error.message).to include('Invalid COV_LOUPE_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['COV_LOUPE_OPTS'] = '"first "second "third'
|
|
96
|
+
|
|
97
|
+
expect do
|
|
98
|
+
parser.parse_env_opts
|
|
99
|
+
end.to raise_error(CovLoupe::ConfigurationError, /Invalid COV_LOUPE_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', 'debug', '--other-option']
|
|
110
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: error_mode_normalizer)
|
|
111
|
+
expect(result).to eq(:debug)
|
|
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', 'log', '--error-mode', 'off']
|
|
129
|
+
result = parser.pre_scan_error_mode(argv, error_mode_normalizer: error_mode_normalizer)
|
|
130
|
+
expect(result).to eq(:log)
|
|
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 'when handling errors during pre-scan' do
|
|
149
|
+
it 'returns nil when normalizer raises an error' do
|
|
150
|
+
faulty_normalizer = ->(_) { raise StandardError, 'Intentional error' }
|
|
151
|
+
argv = ['--error-mode', 'log']
|
|
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 = ->(_) { raise ArgumentError, 'Bad argument' }
|
|
159
|
+
argv = ['--error-mode', 'log']
|
|
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 = ->(_) { raise '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 '#normalize_error_mode (private)' do
|
|
176
|
+
it 'normalizes "off" to :off' do
|
|
177
|
+
expect(parser.send(:normalize_error_mode, 'off')).to eq(:off)
|
|
178
|
+
expect(parser.send(:normalize_error_mode, 'OFF')).to eq(:off)
|
|
179
|
+
expect(parser.send(:normalize_error_mode, 'Off')).to eq(:off)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it 'normalizes "log" to :log' do
|
|
183
|
+
expect(parser.send(:normalize_error_mode, 'log')).to eq(:log)
|
|
184
|
+
expect(parser.send(:normalize_error_mode, 'LOG')).to eq(:log)
|
|
185
|
+
expect(parser.send(:normalize_error_mode, 'Log')).to eq(:log)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'normalizes "debug" to :debug' do
|
|
189
|
+
expect(parser.send(:normalize_error_mode, 'debug')).to eq(:debug)
|
|
190
|
+
expect(parser.send(:normalize_error_mode, 'DEBUG')).to eq(:debug)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'defaults unknown values to :log' do
|
|
194
|
+
expect(parser.send(:normalize_error_mode, 'unknown')).to eq(:log)
|
|
195
|
+
expect(parser.send(:normalize_error_mode, 'invalid')).to eq(:log)
|
|
196
|
+
expect(parser.send(:normalize_error_mode, '')).to eq(:log)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'handles nil by defaulting to :log' do
|
|
200
|
+
expect(parser.send(:normalize_error_mode, nil)).to eq(:log)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
describe 'custom environment variable name' do
|
|
205
|
+
it 'uses custom environment variable when specified' do
|
|
206
|
+
custom_parser = described_class.new(env_var: 'CUSTOM_OPTS')
|
|
207
|
+
ENV['CUSTOM_OPTS'] = '--error-mode off'
|
|
208
|
+
|
|
209
|
+
expect(custom_parser.parse_env_opts).to eq(['--error-mode', 'off'])
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'includes custom env var name in error messages' do
|
|
213
|
+
custom_parser = described_class.new(env_var: 'MY_CUSTOM_VAR')
|
|
214
|
+
ENV['MY_CUSTOM_VAR'] = '"bad quote'
|
|
215
|
+
|
|
216
|
+
expect do
|
|
217
|
+
custom_parser.parse_env_opts
|
|
218
|
+
end.to raise_error(CovLoupe::ConfigurationError, /Invalid MY_CUSTOM_VAR format/)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
OPTION_TESTS = {
|
|
6
|
+
staleness: {
|
|
7
|
+
long: '--staleness',
|
|
8
|
+
short: '-S',
|
|
9
|
+
pattern: /Valid values for --staleness: o\[ff\]|e\[rror\]/
|
|
10
|
+
},
|
|
11
|
+
source: {
|
|
12
|
+
long: '--source',
|
|
13
|
+
short: '-s',
|
|
14
|
+
pattern: /Valid values for --source: f\[ull\]|u\[ncovered\]/
|
|
15
|
+
},
|
|
16
|
+
error_mode: {
|
|
17
|
+
long: '--error-mode',
|
|
18
|
+
short: nil,
|
|
19
|
+
pattern: /Valid values for --error-mode: o\[ff\]|l\[og\]|d\[ebug\]/
|
|
20
|
+
},
|
|
21
|
+
sort_order: {
|
|
22
|
+
long: '--sort-order',
|
|
23
|
+
short: '-o',
|
|
24
|
+
pattern: /Valid values for --sort-order: a\[scending\]|d\[escending\]/
|
|
25
|
+
}
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
RSpec.describe CovLoupe::OptionParsers::ErrorHelper do
|
|
29
|
+
subject(:helper) { described_class.new }
|
|
30
|
+
|
|
31
|
+
# Helper method to capture stderr output
|
|
32
|
+
def capture_stderr
|
|
33
|
+
captured = StringIO.new
|
|
34
|
+
original = $stderr
|
|
35
|
+
$stderr = captured
|
|
36
|
+
begin
|
|
37
|
+
yield
|
|
38
|
+
rescue SystemExit
|
|
39
|
+
# Ignore exit calls
|
|
40
|
+
ensure
|
|
41
|
+
$stderr = original
|
|
42
|
+
end
|
|
43
|
+
captured.string
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Helper method to test error output matches expected pattern
|
|
47
|
+
def expect_error_output(error:, argv:, pattern:)
|
|
48
|
+
expect do
|
|
49
|
+
helper.handle_option_parser_error(error, argv: argv)
|
|
50
|
+
rescue SystemExit
|
|
51
|
+
# Ignore exit call
|
|
52
|
+
end.to output(pattern).to_stderr
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '#handle_option_parser_error' do
|
|
56
|
+
context 'with invalid enumerated option values' do
|
|
57
|
+
OPTION_TESTS.each_value do |config|
|
|
58
|
+
context "when parsing #{config[:long]} option" do
|
|
59
|
+
let(:error) { OptionParser::InvalidArgument.new('invalid argument: xyz') }
|
|
60
|
+
|
|
61
|
+
it 'suggests valid values for space-separated form with invalid value' do
|
|
62
|
+
expect_error_output(
|
|
63
|
+
error: error,
|
|
64
|
+
argv: [config[:long], 'xyz'],
|
|
65
|
+
pattern: config[:pattern]
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'suggests valid values for equal form with invalid value' do
|
|
70
|
+
expect_error_output(
|
|
71
|
+
error: error,
|
|
72
|
+
argv: ["#{config[:long]}=xyz"],
|
|
73
|
+
pattern: config[:pattern]
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if config[:short]
|
|
78
|
+
it 'suggests valid values for short form with invalid value' do
|
|
79
|
+
expect_error_output(
|
|
80
|
+
error: error,
|
|
81
|
+
argv: [config[:short], 'xyz'],
|
|
82
|
+
pattern: config[:pattern]
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context 'when handling --staleness option edge cases' do
|
|
90
|
+
it 'suggests valid values when value is missing' do
|
|
91
|
+
error = OptionParser::InvalidArgument.new('missing argument: --staleness')
|
|
92
|
+
expect_error_output(
|
|
93
|
+
error: error,
|
|
94
|
+
argv: ['--staleness'],
|
|
95
|
+
pattern: /Valid values for --staleness: o\[ff\]|e\[rror\]/
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'suggests valid values when next token looks like an option' do
|
|
100
|
+
error = OptionParser::InvalidArgument.new('invalid argument: --other')
|
|
101
|
+
expect_error_output(
|
|
102
|
+
error: error,
|
|
103
|
+
argv: ['--staleness', '--other-option'],
|
|
104
|
+
pattern: /Valid values for --staleness: o\[ff\]|e\[rror\]/
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
context 'with multiple options in argv' do
|
|
111
|
+
it 'correctly identifies the problematic option among valid options' do
|
|
112
|
+
error = OptionParser::InvalidArgument.new('invalid argument: bad')
|
|
113
|
+
expect_error_output(
|
|
114
|
+
error: error,
|
|
115
|
+
argv: ['--resultset', 'coverage', '--staleness', 'bad', '--format', 'json'],
|
|
116
|
+
pattern: /Valid values for --staleness: o\[ff\]|e\[rror\]/
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'handles equal form mixed with other options' do
|
|
121
|
+
error = OptionParser::InvalidArgument.new('invalid argument: invalid')
|
|
122
|
+
expect_error_output(
|
|
123
|
+
error: error,
|
|
124
|
+
argv: ['--format', 'json', '--sort-order=invalid', '--resultset', 'coverage'],
|
|
125
|
+
pattern: /Valid values for --sort-order: a\[scending\]|d\[escending\]/
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
context 'when option is not an enumerated type' do
|
|
131
|
+
it 'shows generic error message without enum hint' do
|
|
132
|
+
error = OptionParser::InvalidArgument.new('invalid option: --unknown')
|
|
133
|
+
|
|
134
|
+
stderr_output = capture_stderr do
|
|
135
|
+
helper.handle_option_parser_error(error, argv: ['--unknown'])
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
expect(stderr_output).to match(/Error:.*invalid option.*--unknown/)
|
|
139
|
+
expect(stderr_output).to match(/Run 'cov-loupe --help'/)
|
|
140
|
+
expect(stderr_output).not_to match(/Valid values/)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
context 'when invalid option matches a subcommand' do
|
|
145
|
+
it 'suggests using it as a subcommand instead' do
|
|
146
|
+
error = OptionParser::InvalidOption.new('invalid option: --summary')
|
|
147
|
+
|
|
148
|
+
stderr_output = capture_stderr do
|
|
149
|
+
helper.handle_option_parser_error(error, argv: ['--summary'])
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# NOTE: The subcommand detection logic isn't fully working as expected
|
|
153
|
+
# because extract_invalid_option doesn't properly parse the error message
|
|
154
|
+
expect(stderr_output).to match(/Error:.*--summary/)
|
|
155
|
+
expect(stderr_output).to match(/Run 'cov-loupe --help'/)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context 'when exiting after invalid option' do
|
|
160
|
+
it 'exits with status 1' do
|
|
161
|
+
error = OptionParser::InvalidArgument.new('invalid argument: xyz')
|
|
162
|
+
|
|
163
|
+
stderr_output = capture_stderr do
|
|
164
|
+
expect do
|
|
165
|
+
helper.handle_option_parser_error(error, argv: ['--staleness', 'xyz'])
|
|
166
|
+
end.to raise_error(SystemExit) do |e|
|
|
167
|
+
expect(e.status).to eq(1)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
expect(stderr_output).to include('invalid argument: xyz')
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
context 'when customizing usage hint' do
|
|
176
|
+
it 'uses custom usage hint when provided' do
|
|
177
|
+
error = OptionParser::InvalidArgument.new('invalid argument: xyz')
|
|
178
|
+
|
|
179
|
+
expect do
|
|
180
|
+
helper.handle_option_parser_error(error, argv: ['--staleness', 'xyz'],
|
|
181
|
+
usage_hint: 'Custom hint message')
|
|
182
|
+
rescue SystemExit
|
|
183
|
+
# Ignore exit call
|
|
184
|
+
end.to output(/Custom hint message/).to_stderr
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
describe 'when handling edge cases' do
|
|
190
|
+
it 'handles empty argv gracefully' do
|
|
191
|
+
error = OptionParser::InvalidArgument.new('some error')
|
|
192
|
+
expect_error_output(
|
|
193
|
+
error: error,
|
|
194
|
+
argv: [],
|
|
195
|
+
pattern: /Error: invalid argument: some error/
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'handles argv with only valid options (no problematic enum)' do
|
|
200
|
+
error = OptionParser::InvalidArgument.new('some error')
|
|
201
|
+
|
|
202
|
+
stderr_output = capture_stderr do
|
|
203
|
+
helper.handle_option_parser_error(error,
|
|
204
|
+
argv: ['--format', 'json', '--resultset', 'coverage'])
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
expect(stderr_output).to match(/Error: invalid argument: some error/)
|
|
208
|
+
expect(stderr_output).to match(/Run 'cov-loupe --help'/)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it 'does not show enum hint when all enum values are valid' do
|
|
212
|
+
error = OptionParser::MissingArgument.new('missing argument: --resultset')
|
|
213
|
+
|
|
214
|
+
stderr_output = capture_stderr do
|
|
215
|
+
helper.handle_option_parser_error(error, argv: ['--staleness', 'off', '--resultset'])
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
expect(stderr_output).to match(/Error:.*missing argument.*--resultset/)
|
|
219
|
+
expect(stderr_output).not_to match(/Valid values/)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe::PathRelativizer do
|
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
7
|
+
let(:relativizer) do
|
|
8
|
+
described_class.new(
|
|
9
|
+
root: root,
|
|
10
|
+
scalar_keys: %w[file file_path],
|
|
11
|
+
array_keys: %w[newer_files missing_files deleted_files]
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe '#relativize' do
|
|
16
|
+
it 'converts configured scalar keys to root-relative paths' do
|
|
17
|
+
payload = { 'file' => File.join(root, 'lib/foo.rb') }
|
|
18
|
+
result = relativizer.relativize(payload)
|
|
19
|
+
|
|
20
|
+
expect(result['file']).to eq('lib/foo.rb')
|
|
21
|
+
expect(payload['file']).to eq(File.join(root, 'lib/foo.rb'))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'relativizes arrays for configured keys without mutating originals' do
|
|
25
|
+
payload = {
|
|
26
|
+
'newer_files' => [File.join(root, 'lib/foo.rb'), File.join(root, 'lib/bar.rb')]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
result = relativizer.relativize(payload)
|
|
30
|
+
|
|
31
|
+
expect(result['newer_files']).to contain_exactly('lib/foo.rb', 'lib/bar.rb')
|
|
32
|
+
expect(payload['newer_files']).to all(start_with(root))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'leaves unconfigured keys untouched' do
|
|
36
|
+
payload = { 'other' => File.join(root, 'lib/foo.rb') }
|
|
37
|
+
result = relativizer.relativize(payload)
|
|
38
|
+
|
|
39
|
+
expect(result['other']).to eq(payload['other'])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'ignores paths outside the root' do
|
|
43
|
+
outside = '/tmp/external.rb'
|
|
44
|
+
payload = { 'file' => outside }
|
|
45
|
+
|
|
46
|
+
result = relativizer.relativize(payload)
|
|
47
|
+
|
|
48
|
+
expect(result['file']).to eq(outside)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'relativizes nested arrays of hashes' do
|
|
52
|
+
payload = {
|
|
53
|
+
'files' => [
|
|
54
|
+
{ 'file' => File.join(root, 'lib/foo.rb') },
|
|
55
|
+
{ 'file' => File.join(root, 'lib/bar.rb') }
|
|
56
|
+
],
|
|
57
|
+
'counts' => { 'total' => 2 }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
result = relativizer.relativize(payload)
|
|
61
|
+
|
|
62
|
+
expect(result['files'].map { |h| h['file'] }).to eq(%w[lib/foo.rb lib/bar.rb])
|
|
63
|
+
expect(result['counts']).to eq('total' => 2)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "handles paths with '..' components" do
|
|
67
|
+
payload = { 'file' => File.join(root, 'lib/../lib/foo.rb') }
|
|
68
|
+
result = relativizer.relativize(payload)
|
|
69
|
+
expect(result['file']).to eq('lib/foo.rb')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'handles paths with spaces' do
|
|
73
|
+
file_with_space = File.join(root, 'lib/file with space.rb')
|
|
74
|
+
FileUtils.touch(file_with_space)
|
|
75
|
+
|
|
76
|
+
payload = { 'file' => file_with_space }
|
|
77
|
+
result = relativizer.relativize(payload)
|
|
78
|
+
expect(result['file']).to eq('lib/file with space.rb')
|
|
79
|
+
ensure
|
|
80
|
+
FileUtils.rm_f(file_with_space)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# On Windows, relative_path_from raises ArgumentError for paths on different
|
|
84
|
+
# drives (e.g., C: vs D:). The rescue block returns the original path.
|
|
85
|
+
it 'returns original path when relative_path_from raises ArgumentError' do
|
|
86
|
+
fake_pathname = instance_double(Pathname)
|
|
87
|
+
allow(fake_pathname).to receive(:relative_path_from)
|
|
88
|
+
.and_raise(ArgumentError, 'different prefix')
|
|
89
|
+
allow(Pathname).to receive(:new).and_call_original
|
|
90
|
+
allow(Pathname).to receive(:new).with(File.absolute_path('lib/foo.rb', root))
|
|
91
|
+
.and_return(fake_pathname)
|
|
92
|
+
|
|
93
|
+
result = relativizer.relativize_path(File.join(root, 'lib/foo.rb'))
|
|
94
|
+
|
|
95
|
+
expect(result).to eq(File.join(root, 'lib/foo.rb'))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative '../shared_examples/coverage_presenter_examples'
|
|
5
|
+
|
|
6
|
+
RSpec.describe CovLoupe::Presenters::CoverageDetailedPresenter do
|
|
7
|
+
it_behaves_like 'a coverage presenter',
|
|
8
|
+
model_method: :detailed_for,
|
|
9
|
+
payload: {
|
|
10
|
+
'file' => '/abs/path/lib/foo.rb',
|
|
11
|
+
'lines' => [
|
|
12
|
+
{ 'line' => 1, 'hits' => 1, 'covered' => true },
|
|
13
|
+
{ 'line' => 2, 'hits' => 0, 'covered' => false }
|
|
14
|
+
],
|
|
15
|
+
'summary' => { 'covered' => 1, 'total' => 2, 'percentage' => 50.0 }
|
|
16
|
+
},
|
|
17
|
+
stale: 'L',
|
|
18
|
+
expected_keys: ['lines', 'summary']
|
|
19
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative '../shared_examples/coverage_presenter_examples'
|
|
5
|
+
|
|
6
|
+
RSpec.describe CovLoupe::Presenters::CoverageRawPresenter do
|
|
7
|
+
it_behaves_like 'a coverage presenter',
|
|
8
|
+
model_method: :raw_for,
|
|
9
|
+
payload: {
|
|
10
|
+
'file' => '/abs/path/lib/foo.rb',
|
|
11
|
+
'lines' => [1, 0, nil, 2]
|
|
12
|
+
},
|
|
13
|
+
stale: 'L',
|
|
14
|
+
expected_keys: ['lines']
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative '../shared_examples/coverage_presenter_examples'
|
|
5
|
+
|
|
6
|
+
RSpec.describe CovLoupe::Presenters::CoverageSummaryPresenter do
|
|
7
|
+
it_behaves_like 'a coverage presenter',
|
|
8
|
+
model_method: :summary_for,
|
|
9
|
+
payload: {
|
|
10
|
+
'file' => '/abs/path/lib/foo.rb',
|
|
11
|
+
'summary' => { 'covered' => 8, 'total' => 10, 'percentage' => 80.0 }
|
|
12
|
+
},
|
|
13
|
+
stale: false,
|
|
14
|
+
expected_keys: ['summary']
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative '../shared_examples/coverage_presenter_examples'
|
|
5
|
+
|
|
6
|
+
RSpec.describe CovLoupe::Presenters::CoverageUncoveredPresenter do
|
|
7
|
+
it_behaves_like 'a coverage presenter',
|
|
8
|
+
model_method: :uncovered_for,
|
|
9
|
+
payload: {
|
|
10
|
+
'file' => '/abs/path/lib/foo.rb',
|
|
11
|
+
'uncovered' => [2, 4],
|
|
12
|
+
'summary' => { 'covered' => 2, 'total' => 4, 'percentage' => 50.0 }
|
|
13
|
+
},
|
|
14
|
+
stale: 'M',
|
|
15
|
+
expected_keys: ['uncovered', 'summary']
|
|
16
|
+
end
|