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,269 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe::CoverageModel, 'error handling' do
|
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
7
|
+
let(:malformed_resultset) do
|
|
8
|
+
{
|
|
9
|
+
'RSpec' => {
|
|
10
|
+
'coverage' => 'not_a_hash' # Should be a hash, not a string
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe 'initialization error handling' do
|
|
16
|
+
let(:valid_resultset) do
|
|
17
|
+
{
|
|
18
|
+
'RSpec' => {
|
|
19
|
+
'coverage' => {
|
|
20
|
+
"lib/foo\x00bar.rb" => { 'lines' => [1, 0, 1] } # Path with NULL byte
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
'timestamp' => 1000
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
let(:malformed_resultset) do
|
|
27
|
+
{
|
|
28
|
+
'RSpec' => {
|
|
29
|
+
'coverage' => 'not_a_hash' # Should be a hash, not a string
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'raises CoverageDataError with message detail for invalid JSON format' do
|
|
35
|
+
# Mock JSON.parse to raise JSON::ParserError
|
|
36
|
+
allow(JSON).to receive(:load_file).with(anything)
|
|
37
|
+
.and_raise(JSON::ParserError.new('unexpected token'))
|
|
38
|
+
|
|
39
|
+
expect do
|
|
40
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
41
|
+
end.to raise_error(CovLoupe::CoverageDataError) do |error|
|
|
42
|
+
expect(error.message).to include('Invalid coverage data format')
|
|
43
|
+
expect(error.message).to include('unexpected token')
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'raises FilePermissionError when coverage file is not readable' do
|
|
48
|
+
# Mock File.read to raise Errno::EACCES
|
|
49
|
+
allow(JSON).to receive(:load_file).with(anything).and_raise(
|
|
50
|
+
Errno::EACCES.new('Permission denied')
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
expect do
|
|
54
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
55
|
+
end.to raise_error(CovLoupe::FilePermissionError) do |error|
|
|
56
|
+
expect(error.message).to include('Permission denied reading coverage data')
|
|
57
|
+
expect(error.message).to include('Permission denied')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
it 'raises CoverageDataError when resultset structure is invalid (TypeError)' do
|
|
63
|
+
allow(JSON).to receive(:load_file).with(anything).and_return(malformed_resultset)
|
|
64
|
+
|
|
65
|
+
expect do
|
|
66
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
67
|
+
end.to raise_error(CovLoupe::CoverageDataError) do |error|
|
|
68
|
+
expect(error.message).to include('Invalid coverage data structure')
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'raises CoverageDataError when resultset structure causes NoMethodError' do
|
|
73
|
+
# Create a resultset structure that will cause NoMethodError
|
|
74
|
+
malformed_resultset = {
|
|
75
|
+
'RSpec' => {
|
|
76
|
+
'coverage' => {
|
|
77
|
+
'file.rb' => nil # Should have 'lines' key, this will cause NoMethodError
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
allow(File).to receive(:open).and_call_original
|
|
83
|
+
allow(File).to receive(:open).with(end_with('.resultset.json'), 'r')
|
|
84
|
+
.and_return(StringIO.new(malformed_resultset.to_json))
|
|
85
|
+
|
|
86
|
+
broken_map = instance_double('CoverageMap')
|
|
87
|
+
allow(broken_map).to receive(:transform_keys)
|
|
88
|
+
.and_raise(NoMethodError.new("undefined method `upcase' for nil:NilClass"))
|
|
89
|
+
allow(CovLoupe::ResultsetLoader).to receive(:load).and_return(
|
|
90
|
+
CovLoupe::ResultsetLoader::Result.new(coverage_map: broken_map,
|
|
91
|
+
timestamp: 0, suite_names: ['RSpec'])
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
expect do
|
|
95
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
96
|
+
end.to raise_error(CovLoupe::CoverageDataError) do |error|
|
|
97
|
+
expect(error.message).to include('Invalid coverage data structure')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
it 'raises CoverageDataError when path operations raise ArgumentError' do
|
|
105
|
+
allow(JSON).to receive(:load_file).with(end_with('.resultset.json'))
|
|
106
|
+
.and_return(valid_resultset)
|
|
107
|
+
|
|
108
|
+
# Mock File.absolute_path to raise ArgumentError when called with the problematic path
|
|
109
|
+
# But allow it to work for the root initialization
|
|
110
|
+
allow(File).to receive(:absolute_path).and_call_original
|
|
111
|
+
allow(File).to receive(:absolute_path).with(include("\x00"), anything).and_raise(
|
|
112
|
+
ArgumentError.new('string contains null byte')
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
expect do
|
|
116
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
117
|
+
end.to raise_error(CovLoupe::CoverageDataError) do |error|
|
|
118
|
+
expect(error.message).to include('Invalid path in coverage data')
|
|
119
|
+
expect(error.message).to include('null byte')
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'preserves error context in JSON::ParserError messages' do
|
|
124
|
+
allow(JSON).to receive(:load_file).with(anything).and_raise(
|
|
125
|
+
JSON::ParserError.new('765: unexpected token at line 3, column 5')
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
expect do
|
|
129
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
130
|
+
end.to raise_error(CovLoupe::CoverageDataError) do |error|
|
|
131
|
+
# Verify the original error message details are preserved
|
|
132
|
+
expect(error.message).to include('765')
|
|
133
|
+
expect(error.message).to include('line 3')
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'provides helpful error for permission issues with file path' do
|
|
138
|
+
# Mock to raise permission error with actual file path
|
|
139
|
+
resultset_path = File.join(root, 'coverage', '.resultset.json')
|
|
140
|
+
allow(JSON).to receive(:load_file).with(resultset_path).and_raise(
|
|
141
|
+
Errno::EACCES.new(resultset_path)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
expect do
|
|
145
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
146
|
+
end.to raise_error(CovLoupe::FilePermissionError) do |error|
|
|
147
|
+
expect(error.message).to include('Permission denied')
|
|
148
|
+
expect(error.message).to match(/\.resultset\.json/)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
describe 'error context preservation' do
|
|
154
|
+
it 'includes original exception message for JSON::ParserError' do
|
|
155
|
+
allow(JSON).to receive(:load_file).with(anything)
|
|
156
|
+
.and_raise(JSON::ParserError.new('unexpected character at byte 42'))
|
|
157
|
+
|
|
158
|
+
expect do
|
|
159
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
160
|
+
end.to raise_error(CovLoupe::CoverageDataError) do |error|
|
|
161
|
+
expect(error.message).to include('unexpected character at byte 42')
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'includes original exception message for Errno::EACCES' do
|
|
166
|
+
resultset_path = File.join(root, 'coverage', '.resultset.json')
|
|
167
|
+
allow(JSON).to receive(:load_file).with(resultset_path).and_raise(Errno::EACCES.new(resultset_path))
|
|
168
|
+
|
|
169
|
+
expect do
|
|
170
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
171
|
+
end.to raise_error(CovLoupe::FilePermissionError) do |error|
|
|
172
|
+
expect(error.message).to include(resultset_path)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'includes original exception message for TypeError' do
|
|
177
|
+
# Mock to cause TypeError within ResultsetLoader's processing
|
|
178
|
+
allow(JSON).to receive(:load_file).with(anything).and_return(malformed_resultset)
|
|
179
|
+
|
|
180
|
+
expect do
|
|
181
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
182
|
+
end.to raise_error(CovLoupe::CoverageDataError) do |error|
|
|
183
|
+
expect(error.message).to include('Invalid coverage data structure')
|
|
184
|
+
expect(error.message).to include('suite "RSpec"')
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
describe 'RuntimeError handling from find_resultset' do
|
|
190
|
+
it 'converts RuntimeError to CoverageDataError with helpful message' do
|
|
191
|
+
# Mock find_resultset to raise RuntimeError (simulating missing resultset)
|
|
192
|
+
allow(CovLoupe::CovUtil).to receive(:find_resultset).and_raise(
|
|
193
|
+
RuntimeError.new('Specified resultset not found: /nonexistent/path/.resultset.json')
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
expect do
|
|
197
|
+
described_class.new(root: root, resultset: '/nonexistent/path')
|
|
198
|
+
end.to raise_error(CovLoupe::ResultsetNotFoundError) do |error|
|
|
199
|
+
expect(error.message).to include('Specified resultset not found')
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'handles RuntimeError with generic messages' do
|
|
204
|
+
# Test RuntimeError with any generic message that includes 'resultset'
|
|
205
|
+
allow(CovLoupe::CovUtil).to receive(:find_resultset).and_raise(
|
|
206
|
+
RuntimeError.new('Something went wrong during resultset lookup')
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
expect do
|
|
210
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
211
|
+
end.to raise_error(CovLoupe::ResultsetNotFoundError) do |error|
|
|
212
|
+
expect(error.message).to include('Something went wrong during resultset lookup')
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it 'converts RuntimeError without "resultset" in message to CoverageDataError' do
|
|
217
|
+
# Test RuntimeError that does NOT contain 'resultset' in its message
|
|
218
|
+
# This exercises the else branch in the RuntimeError rescue clause
|
|
219
|
+
allow(CovLoupe::CovUtil).to receive(:find_resultset).and_raise(
|
|
220
|
+
RuntimeError.new('Some completely unrelated runtime error')
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
expect do
|
|
224
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
225
|
+
end.to raise_error(CovLoupe::CoverageDataError) do |error|
|
|
226
|
+
expect(error.message).to include('Failed to load coverage data')
|
|
227
|
+
expect(error.message).to include('Some completely unrelated runtime error')
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
describe 'all_files error handling' do
|
|
233
|
+
it 'skips files that raise FileError during coverage lookup' do
|
|
234
|
+
# This exercises the `next` statement in the all_files loop when FileError is raised
|
|
235
|
+
model = described_class.new(root: root, resultset: 'coverage')
|
|
236
|
+
|
|
237
|
+
# Mock lookup_lines to raise FileError for one specific file
|
|
238
|
+
allow(CovLoupe::CovUtil).to receive(:lookup_lines).and_call_original
|
|
239
|
+
allow(CovLoupe::CovUtil).to receive(:lookup_lines)
|
|
240
|
+
.with(anything, include('/lib/foo.rb'))
|
|
241
|
+
.and_raise(CovLoupe::FileError.new('Corrupted coverage entry'))
|
|
242
|
+
|
|
243
|
+
# Should not raise, just skip the problematic file
|
|
244
|
+
result = model.all_files(check_stale: false)
|
|
245
|
+
|
|
246
|
+
# The result should contain bar.rb but not foo.rb
|
|
247
|
+
file_names = result.map { |r| File.basename(r['file']) }
|
|
248
|
+
expect(file_names).to include('bar.rb')
|
|
249
|
+
expect(file_names).not_to include('foo.rb')
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
describe 'resolve method error handling' do
|
|
254
|
+
it 'converts RuntimeError from lookup_lines to FileError' do
|
|
255
|
+
# This exercises the RuntimeError rescue clause in the resolve method
|
|
256
|
+
model = described_class.new(root: root, resultset: 'coverage')
|
|
257
|
+
|
|
258
|
+
# Mock lookup_lines to raise RuntimeError for a specific file
|
|
259
|
+
allow(CovLoupe::CovUtil).to receive(:lookup_lines)
|
|
260
|
+
.and_raise(RuntimeError.new('Unexpected runtime error during lookup'))
|
|
261
|
+
|
|
262
|
+
expect do
|
|
263
|
+
model.summary_for('nonexistent_file.rb')
|
|
264
|
+
end.to raise_error(CovLoupe::FileError) do |error|
|
|
265
|
+
expect(error.message).to include('No coverage data found for file')
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
RSpec.describe CovLoupe::CoverageModel do
|
|
7
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
8
|
+
|
|
9
|
+
def with_stubbed_coverage_timestamp(timestamp)
|
|
10
|
+
mock_resultset_with_timestamp(root, timestamp)
|
|
11
|
+
yield
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "raises stale error when staleness mode is 'error' and file is newer" do
|
|
15
|
+
with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
|
|
16
|
+
model = described_class.new(root: root, staleness: :error)
|
|
17
|
+
expect do
|
|
18
|
+
model.summary_for('lib/foo.rb')
|
|
19
|
+
end.to raise_error(CovLoupe::CoverageDataStaleError, /stale/i)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "does not check staleness when mode is 'off'" do
|
|
24
|
+
with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
|
|
25
|
+
model = described_class.new(root: root, staleness: :off)
|
|
26
|
+
expect { model.summary_for('lib/foo.rb') }.not_to raise_error
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'all_files raises project-level stale when any source file is newer than coverage' do
|
|
31
|
+
with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
|
|
32
|
+
model = described_class.new(root: root, staleness: :error)
|
|
33
|
+
expect { model.all_files }.to raise_error(CovLoupe::CoverageDataProjectStaleError)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'all_files detects new files via tracked_globs' do
|
|
38
|
+
with_stubbed_coverage_timestamp(Time.now.to_i) do
|
|
39
|
+
Tempfile.create(['brand_new_file', '.rb'], File.join(root, 'lib')) do |f|
|
|
40
|
+
f.write("# new file\n")
|
|
41
|
+
f.flush
|
|
42
|
+
model = described_class.new(root: root, staleness: :error)
|
|
43
|
+
expect do
|
|
44
|
+
model.all_files(tracked_globs: ['lib/**/*.rb'])
|
|
45
|
+
end.to raise_error(CovLoupe::CoverageDataProjectStaleError)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
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(CovLoupe::CoverageDataStaleError) { |error|
|
|
75
|
+
expect(error.cov_timestamp).to eq(created_at_time.to_i)
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe::OptionNormalizers do
|
|
6
|
+
describe '.normalize_sort_order' do
|
|
7
|
+
context 'with strict mode (default)' do
|
|
8
|
+
[
|
|
9
|
+
['a', :ascending],
|
|
10
|
+
['ascending', :ascending],
|
|
11
|
+
['d', :descending],
|
|
12
|
+
['descending', :descending],
|
|
13
|
+
['ASCENDING', :ascending],
|
|
14
|
+
['Descending', :descending]
|
|
15
|
+
].each do |input, expected|
|
|
16
|
+
it "normalizes '#{input}' to #{expected}" do
|
|
17
|
+
expect(described_class.normalize_sort_order(input)).to eq(expected)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'raises OptionParser::InvalidArgument for invalid values' do
|
|
22
|
+
expect { described_class.normalize_sort_order('invalid') }
|
|
23
|
+
.to raise_error(OptionParser::InvalidArgument, /invalid argument: invalid/)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context 'with strict: false' do
|
|
28
|
+
it 'returns nil for invalid values' do
|
|
29
|
+
expect(described_class.normalize_sort_order('invalid', strict: false)).to be_nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'still normalizes valid values' do
|
|
33
|
+
expect(described_class.normalize_sort_order('a', strict: false)).to eq(:ascending)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '.normalize_source_mode' do
|
|
39
|
+
context 'with strict mode (default)' do
|
|
40
|
+
[nil, ''].each do |input|
|
|
41
|
+
it "raises OptionParser::InvalidArgument for #{input.inspect}" do
|
|
42
|
+
expect { described_class.normalize_source_mode(input) }
|
|
43
|
+
.to raise_error(OptionParser::InvalidArgument, /invalid argument/)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
[
|
|
48
|
+
['f', :full],
|
|
49
|
+
['full', :full],
|
|
50
|
+
['u', :uncovered],
|
|
51
|
+
['uncovered', :uncovered],
|
|
52
|
+
['FULL', :full],
|
|
53
|
+
['Uncovered', :uncovered]
|
|
54
|
+
].each do |input, expected|
|
|
55
|
+
it "normalizes '#{input}' to #{expected}" do
|
|
56
|
+
expect(described_class.normalize_source_mode(input)).to eq(expected)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'raises OptionParser::InvalidArgument for invalid values' do
|
|
61
|
+
expect { described_class.normalize_source_mode('invalid') }
|
|
62
|
+
.to raise_error(OptionParser::InvalidArgument, /invalid argument: invalid/)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context 'with strict: false' do
|
|
67
|
+
it 'returns nil for invalid values' do
|
|
68
|
+
expect(described_class.normalize_source_mode('invalid', strict: false)).to be_nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'still normalizes valid values' do
|
|
72
|
+
expect(described_class.normalize_source_mode('u', strict: false)).to eq(:uncovered)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '.normalize_staleness' do
|
|
78
|
+
context 'with strict mode (default)' do
|
|
79
|
+
[
|
|
80
|
+
['o', :off],
|
|
81
|
+
['off', :off],
|
|
82
|
+
['e', :error],
|
|
83
|
+
['error', :error],
|
|
84
|
+
['OFF', :off],
|
|
85
|
+
['Error', :error]
|
|
86
|
+
].each do |input, expected|
|
|
87
|
+
it "normalizes '#{input}' to #{expected}" do
|
|
88
|
+
expect(described_class.normalize_staleness(input)).to eq(expected)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'raises OptionParser::InvalidArgument for invalid values' do
|
|
93
|
+
expect { described_class.normalize_staleness('invalid') }
|
|
94
|
+
.to raise_error(OptionParser::InvalidArgument, /invalid argument: invalid/)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
context 'with strict: false' do
|
|
99
|
+
it 'returns nil for invalid values' do
|
|
100
|
+
expect(described_class.normalize_staleness('invalid', strict: false)).to be_nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'still normalizes valid values' do
|
|
104
|
+
expect(described_class.normalize_staleness('e', strict: false)).to eq(:error)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe '.normalize_error_mode' do
|
|
110
|
+
context 'with strict mode (default)' do
|
|
111
|
+
[
|
|
112
|
+
['off', :off],
|
|
113
|
+
['o', :off],
|
|
114
|
+
['log', :log],
|
|
115
|
+
['l', :log],
|
|
116
|
+
['debug', :debug],
|
|
117
|
+
['d', :debug],
|
|
118
|
+
['OFF', :off],
|
|
119
|
+
['Log', :log],
|
|
120
|
+
['DEBUG', :debug]
|
|
121
|
+
].each do |input, expected|
|
|
122
|
+
it "normalizes '#{input}' to #{expected}" do
|
|
123
|
+
expect(described_class.normalize_error_mode(input)).to eq(expected)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
['invalid', 'on', 'trace'].each do |input|
|
|
128
|
+
it "raises OptionParser::InvalidArgument for '#{input}'" do
|
|
129
|
+
expect { described_class.normalize_error_mode(input) }
|
|
130
|
+
.to raise_error(OptionParser::InvalidArgument, /invalid argument: #{input}/)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
context 'with strict: false and default: :log' do
|
|
136
|
+
[['invalid', :log], [nil, :log]].each do |input, expected|
|
|
137
|
+
it "returns default #{expected} for #{input.inspect}" do
|
|
138
|
+
expect(described_class.normalize_error_mode(input, strict: false,
|
|
139
|
+
default: :log)).to eq(expected)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'still normalizes valid values' do
|
|
144
|
+
expect(described_class.normalize_error_mode('off', strict: false)).to eq(:off)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
context 'with custom default' do
|
|
149
|
+
it 'returns custom default for invalid values when not strict' do
|
|
150
|
+
expect(described_class.normalize_error_mode('invalid', strict: false,
|
|
151
|
+
default: :off)).to eq(:off)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
describe '.normalize_format' do
|
|
157
|
+
context 'with strict mode (default)' do
|
|
158
|
+
[
|
|
159
|
+
['t', :table],
|
|
160
|
+
['table', :table],
|
|
161
|
+
['j', :json],
|
|
162
|
+
['json', :json],
|
|
163
|
+
['pretty_json', :pretty_json],
|
|
164
|
+
['pretty-json', :pretty_json],
|
|
165
|
+
['y', :yaml],
|
|
166
|
+
['yaml', :yaml],
|
|
167
|
+
['a', :awesome_print],
|
|
168
|
+
['awesome_print', :awesome_print],
|
|
169
|
+
['ap', :awesome_print],
|
|
170
|
+
['TABLE', :table],
|
|
171
|
+
['Json', :json]
|
|
172
|
+
].each do |input, expected|
|
|
173
|
+
it "normalizes '#{input}' to #{expected}" do
|
|
174
|
+
expect(described_class.normalize_format(input)).to eq(expected)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'raises OptionParser::InvalidArgument for invalid values' do
|
|
179
|
+
expect { described_class.normalize_format('invalid') }
|
|
180
|
+
.to raise_error(OptionParser::InvalidArgument, /invalid argument: invalid/)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
context 'with strict: false' do
|
|
185
|
+
it 'returns nil for invalid values' do
|
|
186
|
+
expect(described_class.normalize_format('invalid', strict: false)).to be_nil
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'still normalizes valid values' do
|
|
190
|
+
expect(described_class.normalize_format('json', strict: false)).to eq(:json)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
describe 'constant maps' do
|
|
196
|
+
[:SORT_ORDER_MAP, :SOURCE_MODE_MAP, :STALENESS_MAP, :ERROR_MODE_MAP,
|
|
197
|
+
:FORMAT_MAP].each do |const|
|
|
198
|
+
it "has frozen #{const}" do
|
|
199
|
+
expect(described_class.const_get(const)).to be_frozen
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|