simplecov-mcp 1.0.0 → 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 +32 -20
- 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 -83
- 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 +114 -170
- 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 +141 -82
- 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 +99 -49
- 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
|
|
@@ -63,7 +61,26 @@ module SimpleCovMcp
|
|
|
63
61
|
class FileNotFoundError < FileError; end
|
|
64
62
|
class FilePermissionError < FileError; end
|
|
65
63
|
class NotAFileError < FileError; end
|
|
66
|
-
|
|
64
|
+
|
|
65
|
+
class ResultsetNotFoundError < FileError
|
|
66
|
+
def user_friendly_message
|
|
67
|
+
base = "File error: #{message}"
|
|
68
|
+
|
|
69
|
+
# Only add helpful tips in CLI and library modes, not MCP mode
|
|
70
|
+
unless SimpleCovMcp.context.mcp_mode?
|
|
71
|
+
base += <<~HELP
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
Try one of the following:
|
|
75
|
+
- cd to a directory containing coverage/.resultset.json
|
|
76
|
+
- Specify a resultset: simplecov-mcp -r PATH
|
|
77
|
+
- Use -h for help: simplecov-mcp -h
|
|
78
|
+
HELP
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
base
|
|
82
|
+
end
|
|
83
|
+
end
|
|
67
84
|
|
|
68
85
|
# Coverage data related errors
|
|
69
86
|
class CoverageDataError < Error
|
|
@@ -78,28 +95,25 @@ module SimpleCovMcp
|
|
|
78
95
|
|
|
79
96
|
def initialize(message = nil, original_error = nil, file_path: nil, file_mtime: nil,
|
|
80
97
|
cov_timestamp: nil, src_len: nil, cov_len: nil, resultset_path: nil)
|
|
81
|
-
super(message, original_error)
|
|
82
98
|
@file_path = file_path
|
|
83
99
|
@file_mtime = file_mtime
|
|
84
100
|
@cov_timestamp = cov_timestamp
|
|
85
101
|
@src_len = src_len
|
|
86
102
|
@cov_len = cov_len
|
|
87
103
|
@resultset_path = resultset_path
|
|
104
|
+
super(message || default_message, original_error)
|
|
88
105
|
end
|
|
89
106
|
|
|
90
107
|
def user_friendly_message
|
|
91
|
-
|
|
92
|
-
base + build_details
|
|
108
|
+
"Coverage data stale: #{message}" + build_details
|
|
93
109
|
end
|
|
94
110
|
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
def default_message
|
|
111
|
+
private def default_message
|
|
98
112
|
fp = file_path || 'file'
|
|
99
113
|
"Coverage data appears stale for #{fp}"
|
|
100
114
|
end
|
|
101
115
|
|
|
102
|
-
def build_details
|
|
116
|
+
private def build_details
|
|
103
117
|
file_utc, file_local = format_time_both(@file_mtime)
|
|
104
118
|
cov_utc, cov_local = format_epoch_both(@cov_timestamp)
|
|
105
119
|
delta_str = format_delta_seconds(@file_mtime, @cov_timestamp)
|
|
@@ -136,13 +150,11 @@ module SimpleCovMcp
|
|
|
136
150
|
base + build_details
|
|
137
151
|
end
|
|
138
152
|
|
|
139
|
-
private
|
|
140
|
-
|
|
141
|
-
def default_message
|
|
153
|
+
private def default_message
|
|
142
154
|
'Coverage data appears stale for project'
|
|
143
155
|
end
|
|
144
156
|
|
|
145
|
-
def build_details
|
|
157
|
+
private def build_details
|
|
146
158
|
cov_utc, cov_local = format_epoch_both(@cov_timestamp)
|
|
147
159
|
parts = []
|
|
148
160
|
parts << "\nCoverage - time: #{cov_utc || 'not found'} (local #{cov_local || 'n/a'})"
|