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,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe::CoverageReporter do
|
|
6
|
+
let(:model) { instance_double(CovLoupe::CoverageModel) }
|
|
7
|
+
# Data is pre-sorted by percentage ascending (as model.all_files returns)
|
|
8
|
+
let(:all_files_data) do
|
|
9
|
+
[
|
|
10
|
+
{ 'file' => '/project/lib/zero.rb', 'percentage' => 0.0, 'covered' => 0, 'total' => 10 },
|
|
11
|
+
{ 'file' => '/project/lib/low.rb', 'percentage' => 25.0, 'covered' => 5, 'total' => 20 },
|
|
12
|
+
{ 'file' => '/project/lib/medium.rb', 'percentage' => 60.0, 'covered' => 12, 'total' => 20 },
|
|
13
|
+
{ 'file' => '/project/lib/high.rb', 'percentage' => 95.0, 'covered' => 19, 'total' => 20 }
|
|
14
|
+
]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
before do
|
|
18
|
+
allow(model).to receive(:all_files).with(sort_order: :ascending).and_return(all_files_data)
|
|
19
|
+
allow(model).to receive(:relativize) do |files|
|
|
20
|
+
files.map { |f| f.merge('file' => f['file'].sub('/project/', '')) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '.report' do
|
|
25
|
+
it 'returns formatted low coverage files string' do
|
|
26
|
+
result = described_class.report(threshold: 80, count: 5, model: model)
|
|
27
|
+
|
|
28
|
+
expect(result).to be_a(String)
|
|
29
|
+
expect(result).to include('Lowest coverage files (< 80%):')
|
|
30
|
+
expect(result).to include('lib/zero.rb')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'includes files below threshold sorted by coverage ascending' do
|
|
34
|
+
result = described_class.report(threshold: 80, count: 5, model: model)
|
|
35
|
+
|
|
36
|
+
expect(result).to include('lib/zero.rb', 'lib/low.rb', 'lib/medium.rb')
|
|
37
|
+
expect(result).not_to include('lib/high.rb')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'respects count parameter' do
|
|
41
|
+
result = described_class.report(threshold: 80, count: 2, model: model)
|
|
42
|
+
|
|
43
|
+
expect(result).to include('lib/zero.rb')
|
|
44
|
+
expect(result).to include('lib/low.rb')
|
|
45
|
+
expect(result).not_to include('lib/medium.rb')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns nil when no files below threshold' do
|
|
49
|
+
result = described_class.report(threshold: 0, count: 5, model: model)
|
|
50
|
+
|
|
51
|
+
expect(result).to be_nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'uses threshold in header' do
|
|
55
|
+
result = described_class.report(threshold: 90, count: 5, model: model)
|
|
56
|
+
|
|
57
|
+
expect(result).to include('< 90%')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'uses default threshold of 80' do
|
|
61
|
+
result = described_class.report(count: 5, model: model)
|
|
62
|
+
|
|
63
|
+
expect(result).to include('< 80%')
|
|
64
|
+
expect(result).not_to include('lib/high.rb')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'uses default count of 5' do
|
|
68
|
+
result = described_class.report(threshold: 100, model: model)
|
|
69
|
+
|
|
70
|
+
# All 4 files are below 100%
|
|
71
|
+
expect(result).to include('lib/zero.rb')
|
|
72
|
+
expect(result).to include('lib/high.rb')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'relativizes file paths' do
|
|
76
|
+
result = described_class.report(threshold: 80, count: 5, model: model)
|
|
77
|
+
|
|
78
|
+
expect(result).to include('lib/zero.rb')
|
|
79
|
+
expect(result).not_to include('/project/')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'aligns percentages correctly' do
|
|
83
|
+
result = described_class.report(threshold: 100, count: 5, model: model)
|
|
84
|
+
lines = result.split("\n")
|
|
85
|
+
|
|
86
|
+
# lines[0] is empty (leading newline), lines[1] is header, lines[2..] are data
|
|
87
|
+
expect(lines[2]).to match(/^\s+0\.0%/)
|
|
88
|
+
expect(lines[3]).to match(/^\s+25\.0%/)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe 'module_function behavior' do
|
|
93
|
+
it 'report is available as a module method' do
|
|
94
|
+
expect(described_class).to respond_to(:report)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'report is available as a private instance method when included' do
|
|
98
|
+
klass = Class.new { include CovLoupe::CoverageReporter }
|
|
99
|
+
expect(klass.private_instance_methods).to include(:report)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'cov_loupe/tools/coverage_table_tool'
|
|
5
|
+
|
|
6
|
+
RSpec.describe CovLoupe::Tools::CoverageTableTool do
|
|
7
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
8
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
setup_mcp_response_stub
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run_tool(staleness: :off)
|
|
15
|
+
# Let real CoverageModel work to test actual format_table behavior
|
|
16
|
+
described_class.call(root: root, staleness: staleness,
|
|
17
|
+
server_context: server_context).payload.first['text']
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'returns a formatted table as a string' do
|
|
21
|
+
output = run_tool
|
|
22
|
+
|
|
23
|
+
# Contains table structure, headers, and file data
|
|
24
|
+
expect(output).to include(
|
|
25
|
+
'┌', '┬', '┐', '│', '├', '┼', '┤', '└', '┴', '┘',
|
|
26
|
+
'File', 'Covered', 'Total', ' │ Stale │',
|
|
27
|
+
'lib/foo.rb', 'lib/bar.rb',
|
|
28
|
+
'Files: total 2, ok 0, stale 2'
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'configures CLI to enforce stale checking when requested' do
|
|
33
|
+
model = instance_double(CovLoupe::CoverageModel,
|
|
34
|
+
all_files: [
|
|
35
|
+
{ 'file' => "#{root}/lib/foo.rb", 'percentage' => 100.0, 'covered' => 10, 'total' => 10,
|
|
36
|
+
'stale' => false }
|
|
37
|
+
],
|
|
38
|
+
relativize: ->(payload) { payload },
|
|
39
|
+
format_table: 'Mock table output'
|
|
40
|
+
)
|
|
41
|
+
allow(CovLoupe::CoverageModel).to receive(:new).with(
|
|
42
|
+
root: root,
|
|
43
|
+
resultset: nil,
|
|
44
|
+
staleness: :error,
|
|
45
|
+
tracked_globs: nil
|
|
46
|
+
).and_return(model)
|
|
47
|
+
allow(model).to receive(:format_table).and_return('Mock table output')
|
|
48
|
+
|
|
49
|
+
described_class.call(root: root, staleness: :error, server_context: server_context)
|
|
50
|
+
|
|
51
|
+
expect(CovLoupe::CoverageModel).to have_received(:new).with(
|
|
52
|
+
root: root,
|
|
53
|
+
resultset: nil,
|
|
54
|
+
staleness: :error,
|
|
55
|
+
tracked_globs: nil
|
|
56
|
+
)
|
|
57
|
+
expect(model).to have_received(:format_table)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'cov_loupe/tools/coverage_totals_tool'
|
|
5
|
+
|
|
6
|
+
RSpec.describe CovLoupe::Tools::CoverageTotalsTool do
|
|
7
|
+
subject(:tool_response) { described_class.call(root: root, server_context: server_context) }
|
|
8
|
+
|
|
9
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
10
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
setup_mcp_response_stub
|
|
14
|
+
model = instance_double(CovLoupe::CoverageModel)
|
|
15
|
+
allow(CovLoupe::CoverageModel).to receive(:new).and_return(model)
|
|
16
|
+
|
|
17
|
+
payload = {
|
|
18
|
+
'lines' => { 'total' => 42, 'covered' => 40, 'uncovered' => 2 },
|
|
19
|
+
'percentage' => 95.24,
|
|
20
|
+
'files' => { 'total' => 4, 'ok' => 4, 'stale' => 0 }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
presenter = instance_double(CovLoupe::Presenters::ProjectTotalsPresenter)
|
|
24
|
+
allow(CovLoupe::Presenters::ProjectTotalsPresenter).to receive(:new).and_return(presenter)
|
|
25
|
+
allow(presenter).to receive(:relativized_payload).and_return(payload)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it_behaves_like 'an MCP tool that returns text JSON'
|
|
29
|
+
|
|
30
|
+
it 'returns aggregated totals' do
|
|
31
|
+
data, = expect_mcp_text_json(tool_response, expected_keys: ['lines', 'percentage', 'files'])
|
|
32
|
+
|
|
33
|
+
expect(data['lines']).to include('total' => 42, 'covered' => 40, 'uncovered' => 2)
|
|
34
|
+
expect(data['files']).to include('total' => 4, 'stale' => 0)
|
|
35
|
+
expect(data['percentage']).to eq(95.24)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe::ErrorHandler do
|
|
6
|
+
subject(:handler) { described_class.new(error_mode: :log, logger: logger) }
|
|
7
|
+
|
|
8
|
+
let(:logger) do
|
|
9
|
+
Class.new do
|
|
10
|
+
attr_reader :messages
|
|
11
|
+
|
|
12
|
+
def initialize = @messages = []
|
|
13
|
+
def error(msg) = @messages << msg
|
|
14
|
+
end.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
it 'maps filesystem errors to friendly custom errors' do
|
|
19
|
+
e = handler.convert_standard_error(Errno::EISDIR.new('Is a directory @ rb_sysopen - a_dir'))
|
|
20
|
+
expect(e).to be_a(CovLoupe::NotAFileError)
|
|
21
|
+
|
|
22
|
+
e = handler.convert_standard_error(
|
|
23
|
+
Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.txt')
|
|
24
|
+
)
|
|
25
|
+
expect(e).to be_a(CovLoupe::FileNotFoundError)
|
|
26
|
+
|
|
27
|
+
e = handler.convert_standard_error(Errno::EACCES.new('Permission denied @ rb_sysopen - secret'))
|
|
28
|
+
expect(e).to be_a(CovLoupe::FilePermissionError)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'maps JSON::ParserError to CoverageDataError' do
|
|
32
|
+
e = handler.convert_standard_error(JSON::ParserError.new('unexpected token'))
|
|
33
|
+
expect(e).to be_a(CovLoupe::CoverageDataError)
|
|
34
|
+
expect(e.user_friendly_message).to include('Invalid coverage data format')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'maps ArgumentError by message' do
|
|
38
|
+
e = handler.convert_standard_error(
|
|
39
|
+
ArgumentError.new('wrong number of arguments (given 1, expected 2)')
|
|
40
|
+
)
|
|
41
|
+
expect(e).to be_a(CovLoupe::UsageError)
|
|
42
|
+
|
|
43
|
+
e = handler.convert_standard_error(ArgumentError.new('invalid option'))
|
|
44
|
+
expect(e).to be_a(CovLoupe::ConfigurationError)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'maps NoMethodError to CoverageDataError with helpful info' do
|
|
48
|
+
e = handler.convert_standard_error(
|
|
49
|
+
NoMethodError.new("undefined method `fetch' for #<Hash:0x123>")
|
|
50
|
+
)
|
|
51
|
+
expect(e).to be_a(CovLoupe::CoverageDataError)
|
|
52
|
+
expect(e.user_friendly_message).to include('Invalid coverage data structure')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'maps runtime strings from util to friendly errors' do
|
|
56
|
+
e = handler.convert_standard_error(
|
|
57
|
+
RuntimeError.new('Could not find .resultset.json under /path; run tests')
|
|
58
|
+
)
|
|
59
|
+
expect(e).to be_a(CovLoupe::CoverageDataError)
|
|
60
|
+
expect(e.user_friendly_message).to include('run your tests first')
|
|
61
|
+
|
|
62
|
+
e = handler.convert_standard_error(
|
|
63
|
+
RuntimeError.new('No .resultset.json found in directory: /path')
|
|
64
|
+
)
|
|
65
|
+
expect(e).to be_a(CovLoupe::CoverageDataError)
|
|
66
|
+
|
|
67
|
+
e = handler.convert_standard_error(
|
|
68
|
+
RuntimeError.new('Specified resultset not found: /nowhere/file.json')
|
|
69
|
+
)
|
|
70
|
+
expect(e).to be_a(CovLoupe::ResultsetNotFoundError)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'logs via provided logger' do
|
|
74
|
+
begin
|
|
75
|
+
handler.handle_error(Errno::ENOENT.new('No such file or directory @ rb_sysopen - x'),
|
|
76
|
+
context: 'test', reraise: false)
|
|
77
|
+
rescue
|
|
78
|
+
# reraise disabled
|
|
79
|
+
end
|
|
80
|
+
expect(logger.messages.join).to include('Error in test')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'converts TypeError to CoverageDataError for invalid data structures' do
|
|
84
|
+
error = TypeError.new('wrong argument type')
|
|
85
|
+
result = handler.convert_standard_error(error)
|
|
86
|
+
|
|
87
|
+
expect(result).to be_a(CovLoupe::CoverageDataError)
|
|
88
|
+
expect(result.user_friendly_message).to include('Invalid coverage data structure')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'returns generic Error for unrecognized SystemCallError' do
|
|
92
|
+
error = Errno::EEXIST.new('File exists')
|
|
93
|
+
result = handler.convert_standard_error(error)
|
|
94
|
+
|
|
95
|
+
expect(result).to be_a(CovLoupe::Error)
|
|
96
|
+
expect(result.user_friendly_message).to include('An unexpected error occurred')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'handles NoMethodError with non-standard message format' do
|
|
100
|
+
error = NoMethodError.new('some weird error message without the expected pattern')
|
|
101
|
+
result = handler.convert_standard_error(error)
|
|
102
|
+
|
|
103
|
+
expect(result).to be_a(CovLoupe::CoverageDataError)
|
|
104
|
+
expect(result.user_friendly_message).to include('some weird error message')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe 'else branch for non-StandardError exceptions' do
|
|
108
|
+
# This tests the else clause in convert_standard_error for exceptions
|
|
109
|
+
# that don't inherit from StandardError
|
|
110
|
+
it 'returns generic Error for Exception subclasses not inheriting from StandardError' do
|
|
111
|
+
# Create a custom exception that inherits from Exception, not StandardError
|
|
112
|
+
custom_exception_class = Class.new(StandardError) do
|
|
113
|
+
def message
|
|
114
|
+
'Custom non-standard exception'
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
error = custom_exception_class.new
|
|
119
|
+
result = handler.convert_standard_error(error)
|
|
120
|
+
|
|
121
|
+
expect(result).to be_a(CovLoupe::Error)
|
|
122
|
+
expect(result.user_friendly_message).to include('An unexpected error occurred')
|
|
123
|
+
expect(result.user_friendly_message).to include('Custom non-standard exception')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'returns generic Error for ScriptError subclasses' do
|
|
127
|
+
# ScriptError inherits from Exception, not StandardError
|
|
128
|
+
error = NotImplementedError.new('This feature is not implemented')
|
|
129
|
+
result = handler.convert_standard_error(error)
|
|
130
|
+
|
|
131
|
+
expect(result).to be_a(CovLoupe::Error)
|
|
132
|
+
expect(result.user_friendly_message).to include('An unexpected error occurred')
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe 'extract_method_info fallback' do
|
|
137
|
+
# This tests the fallback path in extract_method_info when NoMethodError
|
|
138
|
+
# message doesn't match the expected pattern
|
|
139
|
+
it 'returns original message when pattern does not match' do
|
|
140
|
+
# Test various NoMethodError formats that won't match the regex
|
|
141
|
+
test_messages = [
|
|
142
|
+
'method not found',
|
|
143
|
+
'private method called',
|
|
144
|
+
'undefined local variable or method',
|
|
145
|
+
''
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
test_messages.each do |msg|
|
|
149
|
+
error = NoMethodError.new(msg)
|
|
150
|
+
result = handler.convert_standard_error(error)
|
|
151
|
+
|
|
152
|
+
expect(result).to be_a(CovLoupe::CoverageDataError)
|
|
153
|
+
# The original message should be preserved
|
|
154
|
+
expect(result.message).to include(msg) unless msg.empty?
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ErrorHandler#convert_runtime_error handles RuntimeErrors differently based on context:
|
|
160
|
+
# - :coverage_loading assumes errors relate to coverage data and maps them to
|
|
161
|
+
# CoverageDataError or ResultsetNotFoundError
|
|
162
|
+
# - :general (or any other context) maps unrecognized errors to generic Error
|
|
163
|
+
# This tests the final else branch in convert_runtime_error.
|
|
164
|
+
describe 'convert_runtime_error with general context' do
|
|
165
|
+
it 'converts RuntimeError with unrecognized message to generic Error' do
|
|
166
|
+
error = RuntimeError.new('Some completely unexpected runtime error')
|
|
167
|
+
|
|
168
|
+
result = handler.convert_standard_error(error, context: :general)
|
|
169
|
+
|
|
170
|
+
expect(result).to be_a(CovLoupe::Error)
|
|
171
|
+
expect(result.user_friendly_message)
|
|
172
|
+
.to include('An unexpected error occurred', 'unexpected runtime error')
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
describe '#handle_error with reraise' do
|
|
177
|
+
it 're-raises CovLoupe::Error when reraise is true' do
|
|
178
|
+
error = CovLoupe::FileNotFoundError.new('Test file not found')
|
|
179
|
+
|
|
180
|
+
expect { handler.handle_error(error, context: 'test', reraise: true) }
|
|
181
|
+
.to raise_error(CovLoupe::FileNotFoundError, 'Test file not found')
|
|
182
|
+
|
|
183
|
+
# Verify it was logged
|
|
184
|
+
expect(logger.messages.join).to include('Error in test')
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'converts and re-raises StandardError when reraise is true' do
|
|
188
|
+
error = Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.rb')
|
|
189
|
+
|
|
190
|
+
expect { handler.handle_error(error, context: 'test', reraise: true) }
|
|
191
|
+
.to raise_error(CovLoupe::FileNotFoundError)
|
|
192
|
+
|
|
193
|
+
# Verify it was logged
|
|
194
|
+
expect(logger.messages.join).to include('Error in test')
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Error Mode System' do
|
|
6
|
+
let(:test_logger) do
|
|
7
|
+
Class.new do
|
|
8
|
+
attr_reader :messages
|
|
9
|
+
|
|
10
|
+
def initialize = @messages = []
|
|
11
|
+
def error(msg) = @messages << msg
|
|
12
|
+
end.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
let(:test_error) { StandardError.new('Test error message') }
|
|
16
|
+
|
|
17
|
+
describe 'ErrorHandler error modes' do
|
|
18
|
+
context 'with error_mode: :off' do
|
|
19
|
+
subject(:handler) { CovLoupe::ErrorHandler.new(error_mode: :off, logger: test_logger) }
|
|
20
|
+
|
|
21
|
+
it 'does not log errors' do
|
|
22
|
+
expect(handler.log_errors?).to be false
|
|
23
|
+
expect(handler.show_stack_traces?).to be false
|
|
24
|
+
|
|
25
|
+
handler.handle_error(test_error, context: 'test', reraise: false)
|
|
26
|
+
expect(test_logger.messages).to be_empty
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context 'with error_mode: :log' do
|
|
31
|
+
subject(:handler) { CovLoupe::ErrorHandler.new(error_mode: :log, logger: test_logger) }
|
|
32
|
+
|
|
33
|
+
it 'logs errors but not stack traces' do
|
|
34
|
+
expect(handler.log_errors?).to be true
|
|
35
|
+
expect(handler.show_stack_traces?).to be false
|
|
36
|
+
|
|
37
|
+
handler.handle_error(test_error, context: 'test', reraise: false)
|
|
38
|
+
logged_message = test_logger.messages.join
|
|
39
|
+
expect(logged_message).to include('Error in test: StandardError: Test error message')
|
|
40
|
+
expect(logged_message).not_to include('spec/error_mode_spec.rb') # No stack trace
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context 'with error_mode: :debug' do
|
|
45
|
+
subject(:handler) { CovLoupe::ErrorHandler.new(error_mode: :debug, logger: test_logger) }
|
|
46
|
+
|
|
47
|
+
it 'logs errors with stack traces' do
|
|
48
|
+
expect(handler.log_errors?).to be true
|
|
49
|
+
expect(handler.show_stack_traces?).to be true
|
|
50
|
+
|
|
51
|
+
# Create an error with a proper backtrace
|
|
52
|
+
begin
|
|
53
|
+
raise StandardError, 'Test error message'
|
|
54
|
+
rescue => e
|
|
55
|
+
handler.handle_error(e, context: 'test', reraise: false)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
logged_message = test_logger.messages.join
|
|
59
|
+
expect(logged_message).to include('Error in test: StandardError: Test error message')
|
|
60
|
+
expect(logged_message).to include('spec/error_mode_spec.rb') # Stack trace included
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe 'ErrorHandlerFactory' do
|
|
66
|
+
it 'creates handlers with correct modes' do
|
|
67
|
+
cli_handler = CovLoupe::ErrorHandlerFactory.for_cli(error_mode: :debug)
|
|
68
|
+
expect(cli_handler.error_mode).to eq(:debug)
|
|
69
|
+
|
|
70
|
+
lib_handler = CovLoupe::ErrorHandlerFactory.for_library(error_mode: :off)
|
|
71
|
+
expect(lib_handler.error_mode).to eq(:off)
|
|
72
|
+
|
|
73
|
+
mcp_handler = CovLoupe::ErrorHandlerFactory.for_mcp_server(error_mode: :log)
|
|
74
|
+
expect(mcp_handler.error_mode).to eq(:log)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe 'MCP Tools error mode support' do
|
|
79
|
+
before { setup_mcp_response_stub }
|
|
80
|
+
|
|
81
|
+
it 'BaseTool.handle_mcp_error respects error modes' do
|
|
82
|
+
test_error = StandardError.new('Test MCP error')
|
|
83
|
+
|
|
84
|
+
# Test different error modes
|
|
85
|
+
[:off, :log, :debug].each do |mode|
|
|
86
|
+
expect(CovLoupe::ErrorHandlerFactory)
|
|
87
|
+
.to receive(:for_mcp_server).with(error_mode: mode).and_call_original
|
|
88
|
+
|
|
89
|
+
response = CovLoupe::BaseTool.handle_mcp_error(test_error, 'TestTool', error_mode: mode)
|
|
90
|
+
expect(response).to be_a(MCP::Tool::Response)
|
|
91
|
+
expect(response.payload.first['text']).to include('Error:')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe 'CLI error mode support' do
|
|
97
|
+
let(:project_dir) { File.join(__dir__, 'fixtures', 'project1') }
|
|
98
|
+
|
|
99
|
+
it 'accepts --error-mode flag' do
|
|
100
|
+
cli = CovLoupe::CoverageCLI.new
|
|
101
|
+
|
|
102
|
+
# Test that the option parser accepts the flag
|
|
103
|
+
expect do
|
|
104
|
+
cli.send(:parse_options!, ['--error-mode', 'debug', 'summary', 'lib/foo.rb'])
|
|
105
|
+
end.not_to raise_error
|
|
106
|
+
|
|
107
|
+
expect(cli.config.error_mode).to eq(:debug)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'creates error handler with specified mode' do
|
|
111
|
+
cli = CovLoupe::CoverageCLI.new
|
|
112
|
+
cli.send(:parse_options!, ['--error-mode', 'off', 'summary', 'lib/foo.rb'])
|
|
113
|
+
|
|
114
|
+
expect(cli.send(:error_handler).error_mode).to eq(:off)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'validates error mode values' do
|
|
118
|
+
cli = CovLoupe::CoverageCLI.new
|
|
119
|
+
|
|
120
|
+
expect do
|
|
121
|
+
cli.send(:parse_options!, ['--error-mode', 'invalid', 'summary', 'lib/foo.rb'])
|
|
122
|
+
end.to raise_error(OptionParser::InvalidArgument)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe 'Error mode validation' do
|
|
127
|
+
it 'raises ArgumentError for invalid error modes' do
|
|
128
|
+
expect do
|
|
129
|
+
CovLoupe::ErrorHandler.new(error_mode: :invalid)
|
|
130
|
+
end.to raise_error(ArgumentError, /Invalid error_mode: :invalid/)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'accepts all valid error modes' do
|
|
134
|
+
expect { CovLoupe::ErrorHandler.new(error_mode: :off) }.not_to raise_error
|
|
135
|
+
expect { CovLoupe::ErrorHandler.new(error_mode: :log) }.not_to raise_error
|
|
136
|
+
expect { CovLoupe::ErrorHandler.new(error_mode: :debug) }.not_to raise_error
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|