simplecov-mcp 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +98 -50
- data/docs/{ARCHITECTURE.md → dev/ARCHITECTURE.md} +11 -10
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
- data/docs/{DEVELOPMENT.md → dev/DEVELOPMENT.md} +2 -1
- data/docs/dev/README.md +10 -0
- data/docs/dev/RELEASING.md +146 -0
- data/docs/{arch-decisions → dev/arch-decisions}/001-x-arch-decision.md +3 -1
- data/docs/{arch-decisions → dev/arch-decisions}/002-x-arch-decision.md +7 -5
- data/docs/{arch-decisions → dev/arch-decisions}/003-x-arch-decision.md +2 -0
- data/docs/{arch-decisions → dev/arch-decisions}/004-x-arch-decision.md +6 -2
- data/docs/{arch-decisions → dev/arch-decisions}/005-x-arch-decision.md +4 -2
- data/docs/{arch-decisions → dev/arch-decisions}/README.md +3 -3
- data/docs/{presentations → dev/presentations}/simplecov-mcp-presentation.md +28 -22
- data/docs/fixtures/demo_project/README.md +9 -0
- data/docs/{ADVANCED_USAGE.md → user/ADVANCED_USAGE.md} +129 -319
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
- data/docs/user/CLI_USAGE.md +750 -0
- data/docs/{ERROR_HANDLING.md → user/ERROR_HANDLING.md} +12 -12
- data/docs/user/EXAMPLES.md +588 -0
- data/docs/user/INSTALLATION.md +130 -0
- data/docs/{LIBRARY_API.md → user/LIBRARY_API.md} +90 -32
- data/docs/{MCP_INTEGRATION.md → user/MCP_INTEGRATION.md} +36 -34
- data/docs/user/README.md +14 -0
- data/docs/{TROUBLESHOOTING.md → user/TROUBLESHOOTING.md} +21 -100
- data/docs/user/V2-BREAKING-CHANGES.md +472 -0
- data/exe/simplecov-mcp +1 -1
- data/lib/simplecov_mcp/{cli_config.rb → app_config.rb} +12 -12
- data/lib/simplecov_mcp/app_context.rb +1 -1
- data/lib/simplecov_mcp/base_tool.rb +66 -38
- data/lib/simplecov_mcp/cli.rb +67 -123
- data/lib/simplecov_mcp/commands/base_command.rb +16 -27
- data/lib/simplecov_mcp/commands/command_factory.rb +8 -2
- data/lib/simplecov_mcp/commands/detailed_command.rb +16 -2
- data/lib/simplecov_mcp/commands/list_command.rb +1 -1
- data/lib/simplecov_mcp/commands/raw_command.rb +18 -2
- data/lib/simplecov_mcp/commands/summary_command.rb +20 -3
- data/lib/simplecov_mcp/commands/totals_command.rb +53 -0
- data/lib/simplecov_mcp/commands/uncovered_command.rb +24 -5
- data/lib/simplecov_mcp/commands/validate_command.rb +60 -0
- data/lib/simplecov_mcp/commands/version_command.rb +19 -4
- data/lib/simplecov_mcp/config_parser.rb +32 -0
- data/lib/simplecov_mcp/constants.rb +3 -3
- data/lib/simplecov_mcp/coverage_reporter.rb +31 -0
- data/lib/simplecov_mcp/error_handler.rb +81 -40
- data/lib/simplecov_mcp/error_handler_factory.rb +2 -2
- data/lib/simplecov_mcp/errors.rb +12 -19
- data/lib/simplecov_mcp/formatters/source_formatter.rb +23 -19
- data/lib/simplecov_mcp/formatters.rb +51 -0
- data/lib/simplecov_mcp/mcp_server.rb +9 -7
- data/lib/simplecov_mcp/mode_detector.rb +6 -5
- data/lib/simplecov_mcp/model.rb +122 -88
- data/lib/simplecov_mcp/option_normalizers.rb +39 -18
- data/lib/simplecov_mcp/option_parser_builder.rb +82 -65
- data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +3 -5
- data/lib/simplecov_mcp/option_parsers/error_helper.rb +18 -17
- data/lib/simplecov_mcp/path_relativizer.rb +17 -14
- data/lib/simplecov_mcp/predicate_evaluator.rb +72 -0
- data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/project_totals_presenter.rb +27 -0
- data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +14 -18
- data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +7 -9
- data/lib/simplecov_mcp/resultset_loader.rb +20 -25
- data/lib/simplecov_mcp/staleness_checker.rb +50 -46
- data/lib/simplecov_mcp/table_formatter.rb +64 -0
- data/lib/simplecov_mcp/tools/all_files_coverage_tool.rb +20 -50
- data/lib/simplecov_mcp/tools/coverage_detailed_tool.rb +13 -7
- data/lib/simplecov_mcp/tools/coverage_raw_tool.rb +12 -7
- data/lib/simplecov_mcp/tools/coverage_summary_tool.rb +13 -8
- data/lib/simplecov_mcp/tools/coverage_table_tool.rb +20 -60
- data/lib/simplecov_mcp/tools/coverage_totals_tool.rb +44 -0
- data/lib/simplecov_mcp/tools/help_tool.rb +38 -66
- data/lib/simplecov_mcp/tools/uncovered_lines_tool.rb +13 -8
- data/lib/simplecov_mcp/tools/validate_tool.rb +72 -0
- data/lib/simplecov_mcp/tools/version_tool.rb +7 -14
- data/lib/simplecov_mcp/util.rb +18 -12
- data/lib/simplecov_mcp/version.rb +1 -1
- data/lib/simplecov_mcp.rb +23 -29
- data/spec/all_files_coverage_tool_spec.rb +4 -3
- data/spec/{cli_config_spec.rb → app_config_spec.rb} +31 -26
- data/spec/base_tool_spec.rb +17 -14
- data/spec/cli/show_default_report_spec.rb +2 -2
- data/spec/cli_enumerated_options_spec.rb +31 -9
- data/spec/cli_error_spec.rb +46 -23
- data/spec/cli_format_spec.rb +123 -0
- data/spec/cli_json_options_spec.rb +50 -0
- data/spec/cli_source_spec.rb +11 -63
- data/spec/cli_spec.rb +82 -97
- data/spec/cli_usage_spec.rb +15 -15
- data/spec/commands/base_command_spec.rb +12 -92
- data/spec/commands/command_factory_spec.rb +7 -3
- data/spec/commands/detailed_command_spec.rb +10 -24
- data/spec/commands/list_command_spec.rb +28 -0
- data/spec/commands/raw_command_spec.rb +43 -20
- data/spec/commands/summary_command_spec.rb +10 -23
- data/spec/commands/totals_command_spec.rb +34 -0
- data/spec/commands/uncovered_command_spec.rb +29 -23
- data/spec/commands/validate_command_spec.rb +213 -0
- data/spec/commands/version_command_spec.rb +38 -0
- data/spec/constants_spec.rb +3 -3
- data/spec/coverage_reporter_spec.rb +102 -0
- data/spec/coverage_table_tool_spec.rb +21 -10
- data/spec/coverage_totals_tool_spec.rb +37 -0
- data/spec/error_handler_spec.rb +120 -4
- data/spec/error_mode_spec.rb +18 -22
- data/spec/errors_edge_cases_spec.rb +101 -28
- data/spec/errors_stale_spec.rb +34 -0
- data/spec/file_based_mcp_tools_spec.rb +6 -6
- data/spec/fixtures/project1/lib/bar.rb +2 -0
- data/spec/fixtures/project1/lib/foo.rb +2 -0
- data/spec/help_tool_spec.rb +2 -18
- data/spec/integration_spec.rb +103 -161
- data/spec/logging_fallback_spec.rb +3 -3
- data/spec/mcp_server_integration_spec.rb +1 -1
- data/spec/mcp_server_spec.rb +70 -53
- data/spec/mode_detector_spec.rb +46 -41
- data/spec/model_error_handling_spec.rb +139 -78
- data/spec/model_staleness_spec.rb +13 -13
- data/spec/option_normalizers_spec.rb +111 -112
- data/spec/option_parsers/env_options_parser_spec.rb +25 -37
- data/spec/option_parsers/error_helper_spec.rb +56 -56
- data/spec/path_relativizer_spec.rb +15 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +1 -1
- data/spec/presenters/coverage_summary_presenter_spec.rb +1 -1
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +1 -1
- data/spec/presenters/project_coverage_presenter_spec.rb +9 -8
- data/spec/presenters/project_totals_presenter_spec.rb +144 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +261 -36
- data/spec/resolvers/resultset_path_resolver_spec.rb +13 -8
- data/spec/shared_examples/file_based_mcp_tools.rb +23 -18
- data/spec/shared_examples/formatted_command_examples.rb +64 -0
- data/spec/simple_cov_mcp_module_spec.rb +24 -3
- data/spec/simplecov_mcp/formatters/source_formatter_spec.rb +267 -0
- data/spec/simplecov_mcp/formatters_spec.rb +76 -0
- data/spec/simplecov_mcp/presenters/base_coverage_presenter_spec.rb +79 -0
- data/spec/simplecov_mcp_model_spec.rb +97 -47
- data/spec/simplecov_mcp_opts_spec.rb +42 -39
- data/spec/spec_helper.rb +27 -92
- data/spec/staleness_checker_spec.rb +10 -9
- data/spec/staleness_more_spec.rb +4 -4
- 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 +10 -8
- 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 +34 -48
- data/spec/util_spec.rb +5 -4
- data/spec/version_spec.rb +7 -7
- data/spec/version_tool_spec.rb +20 -22
- metadata +90 -23
- data/docs/BRANCH_ONLY_COVERAGE.md +0 -81
- data/docs/CLI_USAGE.md +0 -637
- data/docs/EXAMPLES.md +0 -430
- data/docs/INSTALLATION.md +0 -352
- data/spec/cli_success_predicate_spec.rb +0 -141
data/spec/mode_detector_spec.rb
CHANGED
|
@@ -2,37 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
|
+
# Array-driven test cases for comprehensive coverage
|
|
6
|
+
# Format: [argv, tty?, expected_result, description]
|
|
7
|
+
CLI_MODE_SCENARIOS = [
|
|
8
|
+
# Priority 1: --force-cli flag (highest priority)
|
|
9
|
+
[['--force-cli'], false, true, '--force-cli with piped input'],
|
|
10
|
+
[['--force-cli', '--format', 'json'], false, true, '--force-cli with other flags'],
|
|
11
|
+
|
|
12
|
+
# Priority 2: Valid subcommands (must be first arg)
|
|
13
|
+
[['list'], false, true, 'list subcommand'],
|
|
14
|
+
[['summary', 'lib/foo.rb'], false, true, 'summary with path'],
|
|
15
|
+
[['version'], false, true, 'version subcommand'],
|
|
16
|
+
[['total'], false, true, 'total subcommand'],
|
|
17
|
+
[['list', '--format', 'json'], false, true, 'subcommand with trailing flags'],
|
|
18
|
+
|
|
19
|
+
# Priority 3: Invalid subcommand attempts (must be first non-flag arg)
|
|
20
|
+
[['invalid-command'], false, true, 'invalid subcommand (shows error)'],
|
|
21
|
+
[['lib/foo.rb'], false, true, 'file path (shows error)'],
|
|
22
|
+
|
|
23
|
+
# Priority 4: TTY determines mode when no subcommand/force-cli
|
|
24
|
+
[[], true, true, 'empty args with TTY'],
|
|
25
|
+
[[], false, false, 'empty args with piped input'],
|
|
26
|
+
[['--format', 'json'], true, true, 'flags only with TTY'],
|
|
27
|
+
[['--format', 'json'], false, false, 'flags only with piped input'],
|
|
28
|
+
[['-r', 'foo', '--format', 'json'], false, false, 'multiple flags with piped input'],
|
|
29
|
+
|
|
30
|
+
# Edge cases: flags before subcommands should now be detected as CLI mode
|
|
31
|
+
[['--format', 'json', 'list'], false, true, 'flag first = CLI mode'],
|
|
32
|
+
[['-r', 'foo', 'summary'], false, true, 'option first = CLI mode'],
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# Simpler test cases for the inverse method
|
|
36
|
+
MCP_SCENARIOS = [
|
|
37
|
+
[[], false, true, 'piped input, no args'],
|
|
38
|
+
[['--format', 'json'], false, true, 'piped input with flags'],
|
|
39
|
+
[[], true, false, 'TTY, no args'],
|
|
40
|
+
[['--force-cli'], false, false, '--force-cli flag'],
|
|
41
|
+
[['list'], false, false, 'subcommand'],
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
5
44
|
RSpec.describe SimpleCovMcp::ModeDetector do
|
|
6
45
|
describe '.cli_mode?' do
|
|
7
|
-
# Array-driven test cases for comprehensive coverage
|
|
8
|
-
# Format: [argv, tty?, expected_result, description]
|
|
9
|
-
CLI_MODE_SCENARIOS = [
|
|
10
|
-
# Priority 1: --force-cli flag (highest priority)
|
|
11
|
-
[['--force-cli'], false, true, '--force-cli with piped input'],
|
|
12
|
-
[['--force-cli', '--json'], false, true, '--force-cli with other flags'],
|
|
13
|
-
|
|
14
|
-
# Priority 2: Valid subcommands (must be first arg)
|
|
15
|
-
[['list'], false, true, 'list subcommand'],
|
|
16
|
-
[['summary', 'lib/foo.rb'], false, true, 'summary with path'],
|
|
17
|
-
[['version'], false, true, 'version subcommand'],
|
|
18
|
-
[['list', '--json'], false, true, 'subcommand with trailing flags'],
|
|
19
|
-
|
|
20
|
-
# Priority 3: Invalid subcommand attempts (must be first non-flag arg)
|
|
21
|
-
[['invalid-command'], false, true, 'invalid subcommand (shows error)'],
|
|
22
|
-
[['lib/foo.rb'], false, true, 'file path (shows error)'],
|
|
23
|
-
|
|
24
|
-
# Priority 4: TTY determines mode when no subcommand/force-cli
|
|
25
|
-
[[], true, true, 'empty args with TTY'],
|
|
26
|
-
[[], false, false, 'empty args with piped input'],
|
|
27
|
-
[['--json'], true, true, 'flags only with TTY'],
|
|
28
|
-
[['--json'], false, false, 'flags only with piped input'],
|
|
29
|
-
[['-r', 'foo', '--json'], false, false, 'multiple flags with piped input'],
|
|
30
|
-
|
|
31
|
-
# Edge cases: flags before subcommands should now be detected as CLI mode
|
|
32
|
-
[['--json', 'list'], false, true, 'flag first = CLI mode'],
|
|
33
|
-
[['-r', 'foo', 'summary'], false, true, 'option first = CLI mode'],
|
|
34
|
-
].freeze
|
|
35
|
-
|
|
36
46
|
CLI_MODE_SCENARIOS.each do |argv, is_tty, expected, description|
|
|
37
47
|
it "#{expected ? 'CLI' : 'MCP'}: #{description}" do
|
|
38
48
|
stdin = double('stdin', tty?: is_tty)
|
|
@@ -53,21 +63,12 @@ RSpec.describe SimpleCovMcp::ModeDetector do
|
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
it 'uses STDIN by default when no stdin parameter given' do
|
|
56
|
-
allow(
|
|
66
|
+
allow($stdin).to receive(:tty?).and_return(true)
|
|
57
67
|
expect(described_class.cli_mode?([])).to be true
|
|
58
68
|
end
|
|
59
69
|
end
|
|
60
70
|
|
|
61
71
|
describe '.mcp_server_mode?' do
|
|
62
|
-
# Simpler test cases for the inverse method
|
|
63
|
-
MCP_SCENARIOS = [
|
|
64
|
-
[[], false, true, 'piped input, no args'],
|
|
65
|
-
[['--json'], false, true, 'piped input with flags'],
|
|
66
|
-
[[], true, false, 'TTY, no args'],
|
|
67
|
-
[['--force-cli'], false, false, '--force-cli flag'],
|
|
68
|
-
[['list'], false, false, 'subcommand'],
|
|
69
|
-
].freeze
|
|
70
|
-
|
|
71
72
|
MCP_SCENARIOS.each do |argv, is_tty, expected, description|
|
|
72
73
|
it "#{expected ? 'MCP' : 'CLI'}: #{description}" do
|
|
73
74
|
stdin = double('stdin', tty?: is_tty)
|
|
@@ -137,12 +138,16 @@ RSpec.describe SimpleCovMcp::ModeDetector do
|
|
|
137
138
|
expect(described_class.cli_mode?(['--version'], stdin: stdin)).to be true
|
|
138
139
|
end
|
|
139
140
|
|
|
141
|
+
it 'chooses CLI mode for -v' do
|
|
142
|
+
expect(described_class.cli_mode?(['-v'], stdin: stdin)).to be true
|
|
143
|
+
end
|
|
144
|
+
|
|
140
145
|
it 'chooses CLI mode for --json list' do
|
|
141
|
-
expect(described_class.cli_mode?(['--json', 'list'], stdin: stdin)).to be true
|
|
146
|
+
expect(described_class.cli_mode?(['--format', 'json', 'list'], stdin: stdin)).to be true
|
|
142
147
|
end
|
|
143
148
|
|
|
144
149
|
it 'chooses MCP mode for flags without a subcommand' do
|
|
145
|
-
expect(described_class.cli_mode?(['--json'], stdin: stdin)).to be false
|
|
150
|
+
expect(described_class.cli_mode?(['--format', 'json'], stdin: stdin)).to be false
|
|
146
151
|
end
|
|
147
152
|
end
|
|
148
153
|
end
|
|
@@ -4,14 +4,40 @@ require 'spec_helper'
|
|
|
4
4
|
|
|
5
5
|
RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
|
|
6
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
|
|
7
14
|
|
|
8
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
|
+
|
|
9
34
|
it 'raises CoverageDataError with message detail for invalid JSON format' do
|
|
10
35
|
# Mock JSON.parse to raise JSON::ParserError
|
|
11
|
-
allow(JSON).to receive(:
|
|
36
|
+
allow(JSON).to receive(:load_file).with(anything)
|
|
37
|
+
.and_raise(JSON::ParserError.new('unexpected token'))
|
|
12
38
|
|
|
13
39
|
expect do
|
|
14
|
-
|
|
40
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
15
41
|
end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
|
|
16
42
|
expect(error.message).to include('Invalid coverage data format')
|
|
17
43
|
expect(error.message).to include('unexpected token')
|
|
@@ -20,33 +46,24 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
|
|
|
20
46
|
|
|
21
47
|
it 'raises FilePermissionError when coverage file is not readable' do
|
|
22
48
|
# Mock File.read to raise Errno::EACCES
|
|
23
|
-
allow(
|
|
24
|
-
allow(File).to receive(:read).with(end_with('.resultset.json')).and_raise(
|
|
49
|
+
allow(JSON).to receive(:load_file).with(anything).and_raise(
|
|
25
50
|
Errno::EACCES.new('Permission denied')
|
|
26
51
|
)
|
|
27
52
|
|
|
28
53
|
expect do
|
|
29
|
-
|
|
54
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
30
55
|
end.to raise_error(SimpleCovMcp::FilePermissionError) do |error|
|
|
31
56
|
expect(error.message).to include('Permission denied reading coverage data')
|
|
32
57
|
expect(error.message).to include('Permission denied')
|
|
33
58
|
end
|
|
34
59
|
end
|
|
35
60
|
|
|
36
|
-
it 'raises CoverageDataError when resultset structure is invalid (TypeError)' do
|
|
37
|
-
# Create a malformed resultset that will cause TypeError
|
|
38
|
-
malformed_resultset = {
|
|
39
|
-
'RSpec' => {
|
|
40
|
-
'coverage' => 'not_a_hash' # Should be a hash, not a string
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
allow(
|
|
46
|
-
.and_return(malformed_resultset.to_json)
|
|
62
|
+
it 'raises CoverageDataError when resultset structure is invalid (TypeError)' do
|
|
63
|
+
allow(JSON).to receive(:load_file).with(anything).and_return(malformed_resultset)
|
|
47
64
|
|
|
48
65
|
expect do
|
|
49
|
-
|
|
66
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
50
67
|
end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
|
|
51
68
|
expect(error.message).to include('Invalid coverage data structure')
|
|
52
69
|
end
|
|
@@ -62,36 +79,31 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
|
|
|
62
79
|
}
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
allow(File).to receive(:
|
|
66
|
-
allow(File).to receive(:
|
|
67
|
-
.and_return(malformed_resultset.to_json)
|
|
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))
|
|
68
85
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
allow_any_instance_of(Hash).to receive(:transform_keys)
|
|
86
|
+
broken_map = instance_double('CoverageMap')
|
|
87
|
+
allow(broken_map).to receive(:transform_keys)
|
|
72
88
|
.and_raise(NoMethodError.new("undefined method `upcase' for nil:NilClass"))
|
|
89
|
+
allow(SimpleCovMcp::ResultsetLoader).to receive(:load).and_return(
|
|
90
|
+
SimpleCovMcp::ResultsetLoader::Result.new(coverage_map: broken_map,
|
|
91
|
+
timestamp: 0, suite_names: ['RSpec'])
|
|
92
|
+
)
|
|
73
93
|
|
|
74
94
|
expect do
|
|
75
|
-
|
|
95
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
76
96
|
end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
|
|
77
97
|
expect(error.message).to include('Invalid coverage data structure')
|
|
78
98
|
end
|
|
79
99
|
end
|
|
80
100
|
|
|
81
|
-
it 'raises CoverageDataError when path operations raise ArgumentError' do
|
|
82
|
-
# Create a valid resultset structure with a problematic path
|
|
83
|
-
valid_resultset = {
|
|
84
|
-
'RSpec' => {
|
|
85
|
-
'coverage' => {
|
|
86
|
-
"lib/foo\x00bar.rb" => { 'lines' => [1, 0, 1] } # Path with NULL byte
|
|
87
|
-
},
|
|
88
|
-
'timestamp' => 1000
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
101
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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)
|
|
95
107
|
|
|
96
108
|
# Mock File.absolute_path to raise ArgumentError when called with the problematic path
|
|
97
109
|
# But allow it to work for the root initialization
|
|
@@ -101,7 +113,7 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
|
|
|
101
113
|
)
|
|
102
114
|
|
|
103
115
|
expect do
|
|
104
|
-
|
|
116
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
105
117
|
end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
|
|
106
118
|
expect(error.message).to include('Invalid path in coverage data')
|
|
107
119
|
expect(error.message).to include('null byte')
|
|
@@ -109,13 +121,12 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
|
|
|
109
121
|
end
|
|
110
122
|
|
|
111
123
|
it 'preserves error context in JSON::ParserError messages' do
|
|
112
|
-
|
|
113
|
-
allow(JSON).to receive(:parse).and_raise(
|
|
124
|
+
allow(JSON).to receive(:load_file).with(anything).and_raise(
|
|
114
125
|
JSON::ParserError.new('765: unexpected token at line 3, column 5')
|
|
115
126
|
)
|
|
116
127
|
|
|
117
128
|
expect do
|
|
118
|
-
|
|
129
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
119
130
|
end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
|
|
120
131
|
# Verify the original error message details are preserved
|
|
121
132
|
expect(error.message).to include('765')
|
|
@@ -126,13 +137,12 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
|
|
|
126
137
|
it 'provides helpful error for permission issues with file path' do
|
|
127
138
|
# Mock to raise permission error with actual file path
|
|
128
139
|
resultset_path = File.join(root, 'coverage', '.resultset.json')
|
|
129
|
-
allow(
|
|
130
|
-
allow(File).to receive(:read).with(resultset_path).and_raise(
|
|
140
|
+
allow(JSON).to receive(:load_file).with(resultset_path).and_raise(
|
|
131
141
|
Errno::EACCES.new(resultset_path)
|
|
132
142
|
)
|
|
133
143
|
|
|
134
144
|
expect do
|
|
135
|
-
|
|
145
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
136
146
|
end.to raise_error(SimpleCovMcp::FilePermissionError) do |error|
|
|
137
147
|
expect(error.message).to include('Permission denied')
|
|
138
148
|
expect(error.message).to match(/\.resultset\.json/)
|
|
@@ -141,39 +151,37 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
|
|
|
141
151
|
end
|
|
142
152
|
|
|
143
153
|
describe 'error context preservation' do
|
|
144
|
-
it 'includes original exception message
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
expect(error.message).to include(test_case[:expected_content])
|
|
176
|
-
end
|
|
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(SimpleCovMcp::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(SimpleCovMcp::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(SimpleCovMcp::CoverageDataError) do |error|
|
|
183
|
+
expect(error.message).to include('Invalid coverage data structure')
|
|
184
|
+
expect(error.message).to include('suite "RSpec"')
|
|
177
185
|
end
|
|
178
186
|
end
|
|
179
187
|
end
|
|
@@ -186,7 +194,7 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
|
|
|
186
194
|
)
|
|
187
195
|
|
|
188
196
|
expect do
|
|
189
|
-
|
|
197
|
+
described_class.new(root: root, resultset: '/nonexistent/path')
|
|
190
198
|
end.to raise_error(SimpleCovMcp::ResultsetNotFoundError) do |error|
|
|
191
199
|
expect(error.message).to include('Specified resultset not found')
|
|
192
200
|
end
|
|
@@ -199,10 +207,63 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
|
|
|
199
207
|
)
|
|
200
208
|
|
|
201
209
|
expect do
|
|
202
|
-
|
|
210
|
+
described_class.new(root: root, resultset: 'coverage')
|
|
203
211
|
end.to raise_error(SimpleCovMcp::ResultsetNotFoundError) do |error|
|
|
204
212
|
expect(error.message).to include('Something went wrong during resultset lookup')
|
|
205
213
|
end
|
|
206
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(SimpleCovMcp::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(SimpleCovMcp::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(SimpleCovMcp::CovUtil).to receive(:lookup_lines).and_call_original
|
|
239
|
+
allow(SimpleCovMcp::CovUtil).to receive(:lookup_lines)
|
|
240
|
+
.with(anything, include('/lib/foo.rb'))
|
|
241
|
+
.and_raise(SimpleCovMcp::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(SimpleCovMcp::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(SimpleCovMcp::FileError) do |error|
|
|
265
|
+
expect(error.message).to include('No coverage data found for file')
|
|
266
|
+
end
|
|
267
|
+
end
|
|
207
268
|
end
|
|
208
269
|
end
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'tempfile'
|
|
4
5
|
|
|
5
6
|
RSpec.describe SimpleCovMcp::CoverageModel do
|
|
6
7
|
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
7
8
|
|
|
8
|
-
def with_stubbed_coverage_timestamp(
|
|
9
|
-
mock_resultset_with_timestamp(root,
|
|
9
|
+
def with_stubbed_coverage_timestamp(timestamp)
|
|
10
|
+
mock_resultset_with_timestamp(root, timestamp)
|
|
10
11
|
yield
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
it "raises stale error when staleness mode is 'error' and file is newer" do
|
|
14
15
|
with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
|
|
15
|
-
model = described_class.new(root: root, staleness:
|
|
16
|
+
model = described_class.new(root: root, staleness: :error)
|
|
16
17
|
expect do
|
|
17
18
|
model.summary_for('lib/foo.rb')
|
|
18
19
|
end.to raise_error(SimpleCovMcp::CoverageDataStaleError, /stale/i)
|
|
@@ -21,28 +22,27 @@ RSpec.describe SimpleCovMcp::CoverageModel do
|
|
|
21
22
|
|
|
22
23
|
it "does not check staleness when mode is 'off'" do
|
|
23
24
|
with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
|
|
24
|
-
model = described_class.new(root: root, staleness:
|
|
25
|
+
model = described_class.new(root: root, staleness: :off)
|
|
25
26
|
expect { model.summary_for('lib/foo.rb') }.not_to raise_error
|
|
26
27
|
end
|
|
27
28
|
end
|
|
29
|
+
|
|
28
30
|
it 'all_files raises project-level stale when any source file is newer than coverage' do
|
|
29
31
|
with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
|
|
30
|
-
model = described_class.new(root: root, staleness:
|
|
32
|
+
model = described_class.new(root: root, staleness: :error)
|
|
31
33
|
expect { model.all_files }.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
it 'all_files detects new files via tracked_globs' do
|
|
36
38
|
with_stubbed_coverage_timestamp(Time.now.to_i) do
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
model = described_class.new(root: root, staleness:
|
|
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)
|
|
41
43
|
expect do
|
|
42
44
|
model.all_files(tracked_globs: ['lib/**/*.rb'])
|
|
43
45
|
end.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
|
|
44
|
-
ensure
|
|
45
|
-
File.delete(tmp) if File.exist?(tmp)
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -52,7 +52,7 @@ RSpec.describe SimpleCovMcp::CoverageModel do
|
|
|
52
52
|
created_at = Time.new(2024, 7, 3, 16, 26, 40, '-07:00')
|
|
53
53
|
mock_resultset_with_created_at(root, created_at.strftime('%Y-%m-%d %H:%M:%S %z'))
|
|
54
54
|
|
|
55
|
-
model = described_class.new(root: root, staleness:
|
|
55
|
+
model = described_class.new(root: root, staleness: :off)
|
|
56
56
|
|
|
57
57
|
expect(model.instance_variable_get(:@cov_timestamp)).to eq(created_at.to_i)
|
|
58
58
|
end
|
|
@@ -66,7 +66,7 @@ RSpec.describe SimpleCovMcp::CoverageModel do
|
|
|
66
66
|
}
|
|
67
67
|
mock_resultset_with_created_at(root, created_at_time.iso8601, coverage: mismatched_coverage)
|
|
68
68
|
|
|
69
|
-
model = described_class.new(root: root, staleness:
|
|
69
|
+
model = described_class.new(root: root, staleness: :error)
|
|
70
70
|
|
|
71
71
|
expect(model.instance_variable_get(:@cov_timestamp)).to eq(created_at_time.to_i)
|
|
72
72
|
expect do
|