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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base_command'
|
|
4
4
|
require_relative '../presenters/coverage_raw_presenter'
|
|
5
|
+
require_relative '../table_formatter'
|
|
5
6
|
|
|
6
7
|
module SimpleCovMcp
|
|
7
8
|
module Commands
|
|
@@ -10,11 +11,26 @@ module SimpleCovMcp
|
|
|
10
11
|
handle_with_path(args, 'raw') do |path|
|
|
11
12
|
presenter = Presenters::CoverageRawPresenter.new(model: model, path: path)
|
|
12
13
|
data = presenter.absolute_payload
|
|
13
|
-
break if
|
|
14
|
+
break if maybe_output_structured_format?(data, model)
|
|
14
15
|
|
|
15
16
|
relative_path = presenter.relative_path
|
|
16
17
|
puts "File: #{relative_path}"
|
|
17
|
-
puts
|
|
18
|
+
puts
|
|
19
|
+
|
|
20
|
+
# Table format for raw coverage data
|
|
21
|
+
headers = ['Line', 'Coverage']
|
|
22
|
+
rows = data['lines'].each_with_index.map do |coverage, index|
|
|
23
|
+
[
|
|
24
|
+
(index + 1).to_s,
|
|
25
|
+
coverage.nil? ? 'nil' : coverage.to_s
|
|
26
|
+
]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
puts TableFormatter.format(
|
|
30
|
+
headers: headers,
|
|
31
|
+
rows: rows,
|
|
32
|
+
alignments: [:right, :right]
|
|
33
|
+
)
|
|
18
34
|
end
|
|
19
35
|
end
|
|
20
36
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base_command'
|
|
4
4
|
require_relative '../presenters/coverage_summary_presenter'
|
|
5
|
+
require_relative '../table_formatter'
|
|
5
6
|
|
|
6
7
|
module SimpleCovMcp
|
|
7
8
|
module Commands
|
|
@@ -10,12 +11,28 @@ module SimpleCovMcp
|
|
|
10
11
|
handle_with_path(args, 'summary') do |path|
|
|
11
12
|
presenter = Presenters::CoverageSummaryPresenter.new(model: model, path: path)
|
|
12
13
|
data = presenter.absolute_payload
|
|
13
|
-
break if
|
|
14
|
+
break if emit_structured_format_with_optional_source?(data, model, path)
|
|
14
15
|
|
|
15
16
|
relative_path = presenter.relative_path
|
|
16
17
|
summary = data['summary']
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
# Table format with box-drawing
|
|
20
|
+
headers = ['File', '%', 'Covered', 'Total', 'Stale']
|
|
21
|
+
stale_marker = data['stale'] ? 'Yes' : ''
|
|
22
|
+
rows = [[
|
|
23
|
+
relative_path,
|
|
24
|
+
format('%.2f%%', summary['percentage']),
|
|
25
|
+
summary['covered'].to_s,
|
|
26
|
+
summary['total'].to_s,
|
|
27
|
+
stale_marker
|
|
28
|
+
]]
|
|
29
|
+
|
|
30
|
+
puts TableFormatter.format(
|
|
31
|
+
headers: headers,
|
|
32
|
+
rows: rows,
|
|
33
|
+
alignments: [:left, :right, :right, :right, :center]
|
|
34
|
+
)
|
|
35
|
+
puts
|
|
19
36
|
print_source_for(model, path) if config.source_mode
|
|
20
37
|
end
|
|
21
38
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_command'
|
|
4
|
+
require_relative '../presenters/project_totals_presenter'
|
|
5
|
+
require_relative '../table_formatter'
|
|
6
|
+
|
|
7
|
+
module SimpleCovMcp
|
|
8
|
+
module Commands
|
|
9
|
+
class TotalsCommand < BaseCommand
|
|
10
|
+
def execute(args)
|
|
11
|
+
unless args.empty?
|
|
12
|
+
raise UsageError.for_subcommand('totals')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
presenter = Presenters::ProjectTotalsPresenter.new(
|
|
16
|
+
model: model,
|
|
17
|
+
check_stale: (config.staleness == :error),
|
|
18
|
+
tracked_globs: config.tracked_globs
|
|
19
|
+
)
|
|
20
|
+
payload = presenter.absolute_payload
|
|
21
|
+
return if maybe_output_structured_format?(payload, model)
|
|
22
|
+
|
|
23
|
+
lines = payload['lines']
|
|
24
|
+
files = payload['files']
|
|
25
|
+
|
|
26
|
+
# Table format
|
|
27
|
+
headers = ['Metric', 'Total', 'Covered', 'Uncovered', '%']
|
|
28
|
+
rows = [
|
|
29
|
+
[
|
|
30
|
+
'Lines',
|
|
31
|
+
lines['total'].to_s,
|
|
32
|
+
lines['covered'].to_s,
|
|
33
|
+
lines['uncovered'].to_s,
|
|
34
|
+
format('%.2f%%', payload['percentage'])
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
'Files',
|
|
38
|
+
files['total'].to_s,
|
|
39
|
+
files['ok'].to_s,
|
|
40
|
+
files['stale'].to_s,
|
|
41
|
+
''
|
|
42
|
+
]
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
puts TableFormatter.format(
|
|
46
|
+
headers: headers,
|
|
47
|
+
rows: rows,
|
|
48
|
+
alignments: [:left, :right, :right, :right, :right]
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base_command'
|
|
4
4
|
require_relative '../presenters/coverage_uncovered_presenter'
|
|
5
|
+
require_relative '../table_formatter'
|
|
5
6
|
|
|
6
7
|
module SimpleCovMcp
|
|
7
8
|
module Commands
|
|
@@ -10,14 +11,32 @@ module SimpleCovMcp
|
|
|
10
11
|
handle_with_path(args, 'uncovered') do |path|
|
|
11
12
|
presenter = Presenters::CoverageUncoveredPresenter.new(model: model, path: path)
|
|
12
13
|
data = presenter.absolute_payload
|
|
13
|
-
break if
|
|
14
|
+
break if emit_structured_format_with_optional_source?(data, model, path)
|
|
14
15
|
|
|
15
16
|
relative_path = presenter.relative_path
|
|
16
|
-
puts "File: #{relative_path}"
|
|
17
|
-
puts "Uncovered lines: #{data['uncovered'].join(', ')}"
|
|
18
17
|
summary = data['summary']
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
|
|
19
|
+
puts "File: #{relative_path}"
|
|
20
|
+
puts "Coverage: #{format('%.2f%%', summary['percentage'])} " \
|
|
21
|
+
"(#{summary['covered']}/#{summary['total']} lines)"
|
|
22
|
+
puts
|
|
23
|
+
|
|
24
|
+
# Table format for uncovered lines
|
|
25
|
+
uncovered_lines = data['uncovered']
|
|
26
|
+
if uncovered_lines.empty?
|
|
27
|
+
puts 'All lines covered!'
|
|
28
|
+
else
|
|
29
|
+
headers = ['Line']
|
|
30
|
+
rows = uncovered_lines.map { |line| [line.to_s] }
|
|
31
|
+
|
|
32
|
+
puts TableFormatter.format(
|
|
33
|
+
headers: headers,
|
|
34
|
+
rows: rows,
|
|
35
|
+
alignments: [:right]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts
|
|
21
40
|
print_source_for(model, path) if config.source_mode
|
|
22
41
|
end
|
|
23
42
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_command'
|
|
4
|
+
require_relative '../predicate_evaluator'
|
|
5
|
+
|
|
6
|
+
module SimpleCovMcp
|
|
7
|
+
module Commands
|
|
8
|
+
# Validates coverage data against a predicate.
|
|
9
|
+
# Exits with code 0 (pass), 1 (fail), or 2 (error).
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# simplecov-mcp validate policy.rb # File mode
|
|
13
|
+
# simplecov-mcp validate -i '->(m) { ... }' # Inline mode
|
|
14
|
+
class ValidateCommand < BaseCommand
|
|
15
|
+
def execute(args)
|
|
16
|
+
# Parse command-specific options
|
|
17
|
+
inline_mode = false
|
|
18
|
+
code = nil
|
|
19
|
+
|
|
20
|
+
# Simple option parsing for -i/--inline flag
|
|
21
|
+
while args.first&.start_with?('-')
|
|
22
|
+
case args.first
|
|
23
|
+
when '-i', '--inline'
|
|
24
|
+
inline_mode = true
|
|
25
|
+
args.shift
|
|
26
|
+
code = args.shift or raise UsageError.for_subcommand('validate -i <code>')
|
|
27
|
+
else
|
|
28
|
+
raise UsageError, "Unknown option for validate: #{args.first}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# If not inline mode, expect a file path as positional argument
|
|
33
|
+
unless inline_mode
|
|
34
|
+
file_path = args.shift or raise UsageError.for_subcommand('validate <file> | -i <code>')
|
|
35
|
+
code = file_path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Evaluate the predicate
|
|
39
|
+
result = if inline_mode
|
|
40
|
+
PredicateEvaluator.evaluate_code(code, model)
|
|
41
|
+
else
|
|
42
|
+
PredicateEvaluator.evaluate_file(code, model)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
exit(result ? 0 : 1)
|
|
46
|
+
rescue UsageError
|
|
47
|
+
# Usage errors should exit with code 1, not 2
|
|
48
|
+
raise
|
|
49
|
+
rescue => e
|
|
50
|
+
handle_predicate_error(e)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private def handle_predicate_error(error)
|
|
54
|
+
warn "Predicate error: #{error.message}"
|
|
55
|
+
warn error.backtrace.first(5).join("\n") if config.error_mode == :debug
|
|
56
|
+
exit 2
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -2,17 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require_relative 'base_command'
|
|
5
|
+
require_relative '../table_formatter'
|
|
5
6
|
|
|
6
7
|
module SimpleCovMcp
|
|
7
8
|
module Commands
|
|
8
9
|
class VersionCommand < BaseCommand
|
|
9
|
-
def execute(
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
def execute(_args)
|
|
11
|
+
@gem_root = File.expand_path('../../..', __dir__)
|
|
12
|
+
|
|
13
|
+
if config.format == :table
|
|
14
|
+
data = {
|
|
15
|
+
'Version' => SimpleCovMcp::VERSION,
|
|
16
|
+
'Gem Root' => @gem_root,
|
|
17
|
+
'Documentation' => 'README.md and docs/user/**/*.md in gem root'
|
|
18
|
+
}
|
|
19
|
+
puts TableFormatter.format_vertical(data)
|
|
12
20
|
else
|
|
13
|
-
puts
|
|
21
|
+
puts SimpleCovMcp::Formatters.format(version_info, config.format)
|
|
14
22
|
end
|
|
15
23
|
end
|
|
24
|
+
|
|
25
|
+
private def version_info
|
|
26
|
+
{
|
|
27
|
+
version: SimpleCovMcp::VERSION,
|
|
28
|
+
gem_root: @gem_root
|
|
29
|
+
}
|
|
30
|
+
end
|
|
16
31
|
end
|
|
17
32
|
end
|
|
18
33
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'app_config'
|
|
4
|
+
require_relative 'option_parser_builder'
|
|
5
|
+
|
|
6
|
+
module SimpleCovMcp
|
|
7
|
+
# Centralized configuration parser for both CLI and MCP modes
|
|
8
|
+
# Parses argv (which should already include environment options merged by caller)
|
|
9
|
+
class ConfigParser
|
|
10
|
+
attr_reader :config, :argv
|
|
11
|
+
|
|
12
|
+
def initialize(argv)
|
|
13
|
+
@argv = argv
|
|
14
|
+
@config = AppConfig.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Parse argv (with env opts already merged) and return config
|
|
18
|
+
# @param argv [Array<String>] command-line arguments (should include env opts if needed)
|
|
19
|
+
# @return [AppConfig] populated configuration object
|
|
20
|
+
def self.parse(argv)
|
|
21
|
+
new(argv).parse
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse
|
|
25
|
+
# Build and execute the option parser
|
|
26
|
+
parser = OptionParserBuilder.new(config).build_option_parser
|
|
27
|
+
parser.parse!(argv)
|
|
28
|
+
|
|
29
|
+
config
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -9,14 +9,14 @@ module SimpleCovMcp
|
|
|
9
9
|
OPTIONS_EXPECTING_ARGUMENT = %w[
|
|
10
10
|
-r --resultset
|
|
11
11
|
-R --root
|
|
12
|
+
-f --format
|
|
12
13
|
-o --sort-order
|
|
13
14
|
-s --source
|
|
14
|
-
-c --
|
|
15
|
-
-S --
|
|
15
|
+
-c --context-lines
|
|
16
|
+
-S --staleness
|
|
16
17
|
-g --tracked-globs
|
|
17
18
|
-l --log-file
|
|
18
19
|
--error-mode
|
|
19
|
-
--success-predicate
|
|
20
20
|
].freeze
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCovMcp
|
|
4
|
+
# Reports files with coverage below a specified threshold.
|
|
5
|
+
# Useful for displaying low coverage files after test runs.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage in spec_helper.rb
|
|
8
|
+
# SimpleCov.at_exit do
|
|
9
|
+
# SimpleCov.result.format!
|
|
10
|
+
# report = SimpleCovMcp::CoverageReporter.report(threshold: 80, count: 5)
|
|
11
|
+
# puts report if report
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
module CoverageReporter
|
|
15
|
+
module_function def report(threshold: 80, count: 5, model: nil)
|
|
16
|
+
model ||= CoverageModel.new
|
|
17
|
+
file_list = model.all_files(sort_order: :ascending)
|
|
18
|
+
.select { |f| f['percentage'] < threshold }
|
|
19
|
+
.first(count)
|
|
20
|
+
file_list = model.relativize(file_list)
|
|
21
|
+
|
|
22
|
+
return nil if file_list.empty?
|
|
23
|
+
|
|
24
|
+
lines = ["\nLowest coverage files (< #{threshold}%):"]
|
|
25
|
+
file_list.each do |f|
|
|
26
|
+
lines << format(' %5.1f%% %s', f['percentage'], f['file'])
|
|
27
|
+
end
|
|
28
|
+
lines.join("\n")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -9,9 +9,9 @@ module SimpleCovMcp
|
|
|
9
9
|
class ErrorHandler
|
|
10
10
|
attr_accessor :error_mode, :logger
|
|
11
11
|
|
|
12
|
-
VALID_ERROR_MODES = [:off, :
|
|
12
|
+
VALID_ERROR_MODES = [:off, :log, :debug].freeze
|
|
13
13
|
|
|
14
|
-
def initialize(error_mode: :
|
|
14
|
+
def initialize(error_mode: :log, logger: nil)
|
|
15
15
|
unless VALID_ERROR_MODES.include?(error_mode)
|
|
16
16
|
raise ArgumentError, "Invalid error_mode: #{error_mode.inspect}. Valid modes: #{VALID_ERROR_MODES.inspect}"
|
|
17
17
|
end
|
|
@@ -25,7 +25,7 @@ module SimpleCovMcp
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def show_stack_traces?
|
|
28
|
-
error_mode == :
|
|
28
|
+
error_mode == :debug
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# Handle an error with appropriate logging and re-raising behavior
|
|
@@ -36,54 +36,94 @@ module SimpleCovMcp
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# Convert standard Ruby errors to user-friendly custom errors
|
|
40
|
-
|
|
39
|
+
# Convert standard Ruby errors to user-friendly custom errors.
|
|
40
|
+
# @param error [Exception] the error to convert
|
|
41
|
+
# @param context [Symbol] :general (default) or :coverage_loading for context-specific messages
|
|
42
|
+
def convert_standard_error(error, context: :general)
|
|
41
43
|
case error
|
|
42
44
|
when Errno::ENOENT
|
|
43
|
-
|
|
44
|
-
FileNotFoundError.new("File not found: #{filename}", error)
|
|
45
|
+
convert_enoent(error, context)
|
|
45
46
|
when Errno::EACCES
|
|
46
|
-
|
|
47
|
-
FilePermissionError.new("Permission denied accessing file: #{filename}", error)
|
|
47
|
+
convert_eacces(error, context)
|
|
48
48
|
when Errno::EISDIR
|
|
49
49
|
filename = extract_filename(error.message)
|
|
50
50
|
NotAFileError.new("Expected file but found directory: #{filename}", error)
|
|
51
51
|
when JSON::ParserError
|
|
52
|
-
CoverageDataError.new("Invalid coverage data format
|
|
52
|
+
CoverageDataError.new("Invalid coverage data format: #{error.message}", error)
|
|
53
|
+
when TypeError
|
|
54
|
+
CoverageDataError.new("Invalid coverage data structure: #{error.message}", error)
|
|
53
55
|
when ArgumentError
|
|
54
|
-
|
|
55
|
-
UsageError.new("Invalid number of arguments: #{error.message}", error)
|
|
56
|
-
else
|
|
57
|
-
ConfigurationError.new("Invalid configuration: #{error.message}", error)
|
|
58
|
-
end
|
|
56
|
+
convert_argument_error(error, context)
|
|
59
57
|
when NoMethodError
|
|
58
|
+
convert_no_method_error(error, context)
|
|
59
|
+
when RuntimeError
|
|
60
|
+
convert_runtime_error(error, context)
|
|
61
|
+
else
|
|
62
|
+
Error.new("An unexpected error occurred: #{error.message}", error)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private def convert_enoent(error, context)
|
|
67
|
+
if context == :coverage_loading
|
|
68
|
+
ResultsetNotFoundError.new('Coverage data not found', error)
|
|
69
|
+
else
|
|
70
|
+
filename = extract_filename(error.message)
|
|
71
|
+
FileNotFoundError.new("File not found: #{filename}", error)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private def convert_eacces(error, context)
|
|
76
|
+
if context == :coverage_loading
|
|
77
|
+
FilePermissionError.new("Permission denied reading coverage data: #{error.message}", error)
|
|
78
|
+
else
|
|
79
|
+
filename = extract_filename(error.message)
|
|
80
|
+
FilePermissionError.new("Permission denied accessing file: #{filename}", error)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private def convert_argument_error(error, context)
|
|
85
|
+
if context == :coverage_loading
|
|
86
|
+
CoverageDataError.new("Invalid path in coverage data: #{error.message}", error)
|
|
87
|
+
elsif error.message.include?('wrong number of arguments')
|
|
88
|
+
UsageError.new("Invalid number of arguments: #{error.message}", error)
|
|
89
|
+
else
|
|
90
|
+
ConfigurationError.new("Invalid configuration: #{error.message}", error)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private def convert_no_method_error(error, context)
|
|
95
|
+
if context == :coverage_loading
|
|
96
|
+
CoverageDataError.new("Invalid coverage data structure: #{error.message}", error)
|
|
97
|
+
else
|
|
60
98
|
method_info = extract_method_info(error.message)
|
|
61
99
|
CoverageDataError.new("Invalid coverage data structure - #{method_info}", error)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
dir_info
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private def convert_runtime_error(error, context)
|
|
104
|
+
message = error.message
|
|
105
|
+
if message.include?('Could not find .resultset.json')
|
|
106
|
+
dir_info = message.match(/under (.+?)(?:;|$)/)&.[](1) || 'project directory'
|
|
107
|
+
CoverageDataError.new(
|
|
108
|
+
"Coverage data not found in #{dir_info} - please run your tests first", error)
|
|
109
|
+
elsif message.include?('No .resultset.json found in directory')
|
|
110
|
+
dir_info = message.match(/directory: (.+)$/)&.[](1) || 'specified directory'
|
|
111
|
+
CoverageDataError.new("Coverage data not found in directory: #{dir_info}", error)
|
|
112
|
+
elsif message.include?('Specified resultset not found')
|
|
113
|
+
# Preserve the original message format for consistency with existing tests
|
|
114
|
+
ResultsetNotFoundError.new(message, error)
|
|
115
|
+
elsif context == :coverage_loading
|
|
116
|
+
if message.downcase.include?('resultset')
|
|
117
|
+
ResultsetNotFoundError.new(message, error)
|
|
76
118
|
else
|
|
77
|
-
|
|
119
|
+
CoverageDataError.new("Failed to load coverage data: #{message}", error)
|
|
78
120
|
end
|
|
79
121
|
else
|
|
80
|
-
Error.new("An unexpected error occurred: #{
|
|
122
|
+
Error.new("An unexpected error occurred: #{message}", error)
|
|
81
123
|
end
|
|
82
124
|
end
|
|
83
125
|
|
|
84
|
-
private
|
|
85
|
-
|
|
86
|
-
def log_error(error, context)
|
|
126
|
+
private def log_error(error, context)
|
|
87
127
|
return unless log_errors?
|
|
88
128
|
|
|
89
129
|
message = build_log_message(error, context)
|
|
@@ -94,8 +134,9 @@ module SimpleCovMcp
|
|
|
94
134
|
end
|
|
95
135
|
end
|
|
96
136
|
|
|
97
|
-
def build_log_message(error, context)
|
|
98
|
-
|
|
137
|
+
private def build_log_message(error, context)
|
|
138
|
+
context_suffix = context ? " in #{context}" : ''
|
|
139
|
+
parts = ["Error#{context_suffix}: #{error.class}: #{error.message}"]
|
|
99
140
|
|
|
100
141
|
if show_stack_traces? && error.backtrace
|
|
101
142
|
parts << error.backtrace.join("\n")
|
|
@@ -104,15 +145,15 @@ module SimpleCovMcp
|
|
|
104
145
|
parts.join("\n")
|
|
105
146
|
end
|
|
106
147
|
|
|
107
|
-
def extract_filename(message)
|
|
148
|
+
private def extract_filename(message)
|
|
108
149
|
# Extract filename from "No such file or directory @ rb_sysopen - filename"
|
|
109
150
|
match = message.match(/@ \w+ - (.+)$/)
|
|
110
151
|
match ? match[1] : 'unknown file'
|
|
111
152
|
end
|
|
112
153
|
|
|
113
|
-
def extract_method_info(message)
|
|
114
|
-
|
|
115
|
-
if match
|
|
154
|
+
private def extract_method_info(message)
|
|
155
|
+
match = message.match(/undefined method `(.+?)' for (.+)$/)
|
|
156
|
+
if match
|
|
116
157
|
method_name = match[1]
|
|
117
158
|
object_info = match[2].gsub(/#<.*?>/, 'object')
|
|
118
159
|
"missing method '#{method_name}' on #{object_info}"
|
|
@@ -8,7 +8,7 @@ module SimpleCovMcp
|
|
|
8
8
|
# - Logs errors for debugging
|
|
9
9
|
# - Shows stack traces only when explicitly requested
|
|
10
10
|
# - Suitable for user-facing command line interface
|
|
11
|
-
def self.for_cli(error_mode: :
|
|
11
|
+
def self.for_cli(error_mode: :log)
|
|
12
12
|
ErrorHandler.new(error_mode: error_mode)
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -24,7 +24,7 @@ module SimpleCovMcp
|
|
|
24
24
|
# - Logs errors for server debugging
|
|
25
25
|
# - Shows stack traces only when explicitly requested
|
|
26
26
|
# - Suitable for long-running server processes
|
|
27
|
-
def self.for_mcp_server(error_mode: :
|
|
27
|
+
def self.for_mcp_server(error_mode: :log)
|
|
28
28
|
ErrorHandler.new(error_mode: error_mode)
|
|
29
29
|
end
|
|
30
30
|
end
|
data/lib/simplecov_mcp/errors.rb
CHANGED
|
@@ -14,33 +14,31 @@ module SimpleCovMcp
|
|
|
14
14
|
message
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
protected
|
|
18
|
-
|
|
19
|
-
def format_epoch_both(epoch_seconds)
|
|
17
|
+
protected def format_epoch_both(epoch_seconds)
|
|
20
18
|
return [nil, nil] unless epoch_seconds
|
|
21
19
|
|
|
22
20
|
t = Time.at(epoch_seconds.to_i)
|
|
23
21
|
[t.utc.iso8601, t.getlocal.iso8601]
|
|
24
|
-
rescue
|
|
22
|
+
rescue
|
|
25
23
|
[epoch_seconds.to_s, epoch_seconds.to_s]
|
|
26
24
|
end
|
|
27
25
|
|
|
28
|
-
def format_time_both(time)
|
|
26
|
+
protected def format_time_both(time)
|
|
29
27
|
return [nil, nil] unless time
|
|
30
28
|
|
|
31
29
|
t = time.is_a?(Time) ? time : Time.parse(time.to_s)
|
|
32
30
|
[t.utc.iso8601, t.getlocal.iso8601]
|
|
33
|
-
rescue
|
|
31
|
+
rescue
|
|
34
32
|
[time.to_s, time.to_s]
|
|
35
33
|
end
|
|
36
34
|
|
|
37
|
-
def format_delta_seconds(file_mtime, cov_timestamp)
|
|
35
|
+
protected def format_delta_seconds(file_mtime, cov_timestamp)
|
|
38
36
|
return nil unless file_mtime && cov_timestamp
|
|
39
37
|
|
|
40
38
|
seconds = file_mtime.to_i - cov_timestamp.to_i
|
|
41
39
|
sign = seconds >= 0 ? '+' : '-'
|
|
42
40
|
"#{sign}#{seconds.abs}s"
|
|
43
|
-
rescue
|
|
41
|
+
rescue
|
|
44
42
|
nil
|
|
45
43
|
end
|
|
46
44
|
end
|
|
@@ -97,28 +95,25 @@ module SimpleCovMcp
|
|
|
97
95
|
|
|
98
96
|
def initialize(message = nil, original_error = nil, file_path: nil, file_mtime: nil,
|
|
99
97
|
cov_timestamp: nil, src_len: nil, cov_len: nil, resultset_path: nil)
|
|
100
|
-
super(message, original_error)
|
|
101
98
|
@file_path = file_path
|
|
102
99
|
@file_mtime = file_mtime
|
|
103
100
|
@cov_timestamp = cov_timestamp
|
|
104
101
|
@src_len = src_len
|
|
105
102
|
@cov_len = cov_len
|
|
106
103
|
@resultset_path = resultset_path
|
|
104
|
+
super(message || default_message, original_error)
|
|
107
105
|
end
|
|
108
106
|
|
|
109
107
|
def user_friendly_message
|
|
110
|
-
|
|
111
|
-
base + build_details
|
|
108
|
+
"Coverage data stale: #{message}" + build_details
|
|
112
109
|
end
|
|
113
110
|
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
def default_message
|
|
111
|
+
private def default_message
|
|
117
112
|
fp = file_path || 'file'
|
|
118
113
|
"Coverage data appears stale for #{fp}"
|
|
119
114
|
end
|
|
120
115
|
|
|
121
|
-
def build_details
|
|
116
|
+
private def build_details
|
|
122
117
|
file_utc, file_local = format_time_both(@file_mtime)
|
|
123
118
|
cov_utc, cov_local = format_epoch_both(@cov_timestamp)
|
|
124
119
|
delta_str = format_delta_seconds(@file_mtime, @cov_timestamp)
|
|
@@ -155,13 +150,11 @@ module SimpleCovMcp
|
|
|
155
150
|
base + build_details
|
|
156
151
|
end
|
|
157
152
|
|
|
158
|
-
private
|
|
159
|
-
|
|
160
|
-
def default_message
|
|
153
|
+
private def default_message
|
|
161
154
|
'Coverage data appears stale for project'
|
|
162
155
|
end
|
|
163
156
|
|
|
164
|
-
def build_details
|
|
157
|
+
private def build_details
|
|
165
158
|
cov_utc, cov_local = format_epoch_both(@cov_timestamp)
|
|
166
159
|
parts = []
|
|
167
160
|
parts << "\nCoverage - time: #{cov_utc || 'not found'} (local #{cov_local || 'n/a'})"
|