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
|
@@ -18,7 +18,7 @@ module SimpleCovMcp
|
|
|
18
18
|
'uncovered' => :uncovered
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
STALENESS_MAP = {
|
|
22
22
|
'o' => :off,
|
|
23
23
|
'off' => :off,
|
|
24
24
|
'e' => :error,
|
|
@@ -27,19 +27,29 @@ module SimpleCovMcp
|
|
|
27
27
|
|
|
28
28
|
ERROR_MODE_MAP = {
|
|
29
29
|
'off' => :off,
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
30
|
+
'o' => :off,
|
|
31
|
+
'log' => :log,
|
|
32
|
+
'l' => :log,
|
|
33
|
+
'debug' => :debug,
|
|
34
|
+
'd' => :debug
|
|
33
35
|
}.freeze
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
FORMAT_MAP = {
|
|
38
|
+
't' => :table,
|
|
39
|
+
'table' => :table,
|
|
40
|
+
'j' => :json,
|
|
41
|
+
'json' => :json,
|
|
42
|
+
'J' => :pretty_json,
|
|
43
|
+
'pretty_json' => :pretty_json,
|
|
44
|
+
'pretty-json' => :pretty_json,
|
|
45
|
+
'y' => :yaml,
|
|
46
|
+
'yaml' => :yaml,
|
|
47
|
+
'a' => :awesome_print,
|
|
48
|
+
'awesome_print' => :awesome_print,
|
|
49
|
+
'ap' => :awesome_print
|
|
50
|
+
}.freeze
|
|
36
51
|
|
|
37
|
-
|
|
38
|
-
# @param value [String, Symbol] The value to normalize
|
|
39
|
-
# @param strict [Boolean] If true, raises on invalid value; if false, returns nil
|
|
40
|
-
# @return [Symbol, nil] The normalized symbol or nil if invalid and not strict
|
|
41
|
-
# @raise [OptionParser::InvalidArgument] If strict and value is invalid
|
|
42
|
-
def normalize_sort_order(value, strict: true)
|
|
52
|
+
module_function def normalize_sort_order(value, strict: true)
|
|
43
53
|
normalized = SORT_ORDER_MAP[value.to_s.downcase]
|
|
44
54
|
return normalized if normalized
|
|
45
55
|
raise OptionParser::InvalidArgument, "invalid argument: #{value}" if strict
|
|
@@ -52,9 +62,7 @@ module SimpleCovMcp
|
|
|
52
62
|
# @param strict [Boolean] If true, raises on invalid value; if false, returns nil
|
|
53
63
|
# @return [Symbol, nil] The normalized symbol or nil if invalid and not strict
|
|
54
64
|
# @raise [OptionParser::InvalidArgument] If strict and value is invalid
|
|
55
|
-
def normalize_source_mode(value, strict: true)
|
|
56
|
-
return :full if value.nil? || value == ''
|
|
57
|
-
|
|
65
|
+
module_function def normalize_source_mode(value, strict: true)
|
|
58
66
|
normalized = SOURCE_MODE_MAP[value.to_s.downcase]
|
|
59
67
|
return normalized if normalized
|
|
60
68
|
raise OptionParser::InvalidArgument, "invalid argument: #{value}" if strict
|
|
@@ -67,8 +75,8 @@ module SimpleCovMcp
|
|
|
67
75
|
# @param strict [Boolean] If true, raises on invalid value; if false, returns nil
|
|
68
76
|
# @return [Symbol, nil] The normalized symbol or nil if invalid and not strict
|
|
69
77
|
# @raise [OptionParser::InvalidArgument] If strict and value is invalid
|
|
70
|
-
def
|
|
71
|
-
normalized =
|
|
78
|
+
module_function def normalize_staleness(value, strict: true)
|
|
79
|
+
normalized = STALENESS_MAP[value.to_s.downcase]
|
|
72
80
|
return normalized if normalized
|
|
73
81
|
raise OptionParser::InvalidArgument, "invalid argument: #{value}" if strict
|
|
74
82
|
|
|
@@ -81,12 +89,25 @@ module SimpleCovMcp
|
|
|
81
89
|
# @param default [Symbol] The default value to return if invalid and not strict
|
|
82
90
|
# @return [Symbol] The normalized symbol or default if invalid and not strict
|
|
83
91
|
# @raise [OptionParser::InvalidArgument] If strict and value is invalid
|
|
84
|
-
def normalize_error_mode(value, strict: true, default: :
|
|
85
|
-
normalized = ERROR_MODE_MAP[value
|
|
92
|
+
module_function def normalize_error_mode(value, strict: true, default: :log)
|
|
93
|
+
normalized = ERROR_MODE_MAP[value.to_s.downcase]
|
|
86
94
|
return normalized if normalized
|
|
87
95
|
raise OptionParser::InvalidArgument, "invalid argument: #{value}" if strict
|
|
88
96
|
|
|
89
97
|
default
|
|
90
98
|
end
|
|
99
|
+
|
|
100
|
+
# Normalize format value.
|
|
101
|
+
# @param value [String, Symbol] The value to normalize
|
|
102
|
+
# @param strict [Boolean] If true, raises on invalid value; if false, returns nil
|
|
103
|
+
# @return [Symbol, nil] The normalized symbol or nil if invalid and not strict
|
|
104
|
+
# @raise [OptionParser::InvalidArgument] If strict and value is invalid
|
|
105
|
+
module_function def normalize_format(value, strict: true)
|
|
106
|
+
normalized = FORMAT_MAP[value.to_s.downcase]
|
|
107
|
+
return normalized if normalized
|
|
108
|
+
raise OptionParser::InvalidArgument, "invalid argument: #{value}" if strict
|
|
109
|
+
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
91
112
|
end
|
|
92
113
|
end
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'option_normalizers'
|
|
4
|
+
require_relative 'version'
|
|
4
5
|
|
|
5
6
|
module SimpleCovMcp
|
|
6
7
|
class OptionParserBuilder
|
|
7
8
|
HORIZONTAL_RULE = '-' * 79
|
|
8
|
-
SUBCOMMANDS = %w[list summary raw uncovered detailed version].freeze
|
|
9
|
+
SUBCOMMANDS = %w[list summary raw uncovered detailed totals validate version].freeze
|
|
9
10
|
|
|
10
11
|
attr_reader :config
|
|
11
12
|
|
|
@@ -15,21 +16,19 @@ module SimpleCovMcp
|
|
|
15
16
|
|
|
16
17
|
def build_option_parser
|
|
17
18
|
require 'optparse'
|
|
18
|
-
OptionParser.new do |
|
|
19
|
-
configure_banner(
|
|
20
|
-
define_subcommands_help(
|
|
21
|
-
define_options(
|
|
22
|
-
define_examples(
|
|
23
|
-
add_help_handler(
|
|
19
|
+
OptionParser.new do |parser|
|
|
20
|
+
configure_banner(parser)
|
|
21
|
+
define_subcommands_help(parser)
|
|
22
|
+
define_options(parser)
|
|
23
|
+
define_examples(parser)
|
|
24
|
+
add_help_handler(parser)
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def configure_banner(o)
|
|
30
|
-
o.banner = <<~BANNER
|
|
28
|
+
private def configure_banner(parser)
|
|
29
|
+
parser.banner = <<~BANNER
|
|
31
30
|
#{HORIZONTAL_RULE}
|
|
32
|
-
Usage: simplecov-mcp [
|
|
31
|
+
Usage: simplecov-mcp [options] [subcommand] [args]
|
|
33
32
|
Repository: https://github.com/keithrbennett/simplecov-mcp
|
|
34
33
|
Version: #{SimpleCovMcp::VERSION}
|
|
35
34
|
#{HORIZONTAL_RULE}
|
|
@@ -37,98 +36,116 @@ module SimpleCovMcp
|
|
|
37
36
|
BANNER
|
|
38
37
|
end
|
|
39
38
|
|
|
40
|
-
def define_subcommands_help(
|
|
41
|
-
|
|
39
|
+
private def define_subcommands_help(parser)
|
|
40
|
+
parser.separator <<~SUBCOMMANDS
|
|
42
41
|
Subcommands:
|
|
43
|
-
list Show files coverage (table or --
|
|
42
|
+
list Show files coverage (default: table, or use --format)
|
|
44
43
|
summary <path> Show covered/total/% for a file
|
|
45
44
|
raw <path> Show the SimpleCov 'lines' array
|
|
46
45
|
uncovered <path> Show uncovered lines and a summary
|
|
47
46
|
detailed <path> Show per-line rows with hits/covered
|
|
47
|
+
totals Show aggregated line totals and average %
|
|
48
|
+
validate <file> Evaluate coverage policy from file (exit 0=pass, 1=fail, 2=error)
|
|
49
|
+
validate -e <code> Evaluate coverage policy from code string
|
|
48
50
|
version Show version information
|
|
49
51
|
|
|
50
52
|
SUBCOMMANDS
|
|
51
53
|
end
|
|
52
54
|
|
|
53
|
-
def define_options(
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
private def define_options(parser)
|
|
56
|
+
parser.separator 'Options:'
|
|
57
|
+
parser.on('-r', '--resultset PATH', String,
|
|
56
58
|
'Path or directory that contains .resultset.json (default: coverage/.resultset.json)') \
|
|
57
|
-
do |
|
|
58
|
-
config.resultset =
|
|
59
|
+
do |value|
|
|
60
|
+
config.resultset = value
|
|
61
|
+
end
|
|
62
|
+
parser.on('-R', '--root PATH', String, 'Project root (default: .)') do |value|
|
|
63
|
+
config.root = value
|
|
64
|
+
end
|
|
65
|
+
parser.on(
|
|
66
|
+
'-f', '--format FORMAT', String,
|
|
67
|
+
'Output format: t[able]|j[son]|pretty-json|y[aml]|a[wesome-print] (default: table)'
|
|
68
|
+
) do |value|
|
|
69
|
+
config.format = normalize_format(value)
|
|
59
70
|
end
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'Sort order for list: a[scending]|d[escending] (default ascending)') do |v|
|
|
64
|
-
config.sort_order = normalize_sort_order(v)
|
|
71
|
+
parser.on('-o', '--sort-order ORDER', String,
|
|
72
|
+
'Sort order for list: a[scending]|d[escending] (default descending)') do |value|
|
|
73
|
+
config.sort_order = normalize_sort_order(value)
|
|
65
74
|
end
|
|
66
|
-
|
|
67
|
-
'
|
|
68
|
-
config.source_mode = normalize_source_mode(
|
|
75
|
+
parser.on('-s', '--source MODE', String,
|
|
76
|
+
'Source display: f[ull]|u[ncovered]') do |value|
|
|
77
|
+
config.source_mode = normalize_source_mode(value)
|
|
69
78
|
end
|
|
70
|
-
|
|
71
|
-
'
|
|
72
|
-
config.source_context =
|
|
79
|
+
parser.on('-c', '--context-lines N', Integer,
|
|
80
|
+
'Context lines around uncovered lines (non-negative, default: 2)') do |value|
|
|
81
|
+
config.source_context = value
|
|
73
82
|
end
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
'Staleness
|
|
78
|
-
config.
|
|
83
|
+
parser.on('--color', 'Enable ANSI colors for source output') { config.color = true }
|
|
84
|
+
parser.on('--no-color', 'Disable ANSI colors') { config.color = false }
|
|
85
|
+
parser.on('-S', '--staleness MODE', String,
|
|
86
|
+
'Staleness detection: o[ff]|e[rror] (default off)') do |value|
|
|
87
|
+
config.staleness = normalize_staleness(value)
|
|
79
88
|
end
|
|
80
|
-
|
|
81
|
-
'Globs for filtering files (list
|
|
82
|
-
config.tracked_globs =
|
|
89
|
+
parser.on('-g', '--tracked-globs x,y,z', Array,
|
|
90
|
+
'Globs for filtering files (list/totals subcommands)') do |value|
|
|
91
|
+
config.tracked_globs = value
|
|
83
92
|
end
|
|
84
|
-
|
|
85
|
-
'Log file path (default ./simplecov_mcp.log, use stdout/stderr for streams)') do |
|
|
86
|
-
config.log_file =
|
|
93
|
+
parser.on('-l', '--log-file PATH', String,
|
|
94
|
+
'Log file path (default ./simplecov_mcp.log, use stdout/stderr for streams)') do |value|
|
|
95
|
+
config.log_file = value
|
|
87
96
|
end
|
|
88
|
-
|
|
89
|
-
'Error handling mode:
|
|
90
|
-
|
|
97
|
+
parser.on('--error-mode MODE', String,
|
|
98
|
+
'Error handling mode: o[ff]|l[og]|d[ebug] (default log). ' \
|
|
99
|
+
'off (silent), log (log errors to file), debug (verbose with backtraces)') do |value|
|
|
100
|
+
config.error_mode = normalize_error_mode(value)
|
|
91
101
|
end
|
|
92
|
-
|
|
102
|
+
parser.on('--force-cli', 'Force CLI mode (useful in scripts where auto-detection fails)') do
|
|
93
103
|
# This flag is mainly for mode detection - no action needed here
|
|
94
104
|
end
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
config.success_predicate = v
|
|
105
|
+
parser.on('-v', '--version', 'Show version information and exit') do
|
|
106
|
+
config.show_version = true
|
|
98
107
|
end
|
|
99
108
|
end
|
|
100
109
|
|
|
101
|
-
def define_examples(
|
|
102
|
-
|
|
110
|
+
private def define_examples(parser)
|
|
111
|
+
parser.separator <<~EXAMPLES
|
|
103
112
|
|
|
104
113
|
Examples:
|
|
105
|
-
simplecov-mcp
|
|
106
|
-
simplecov-mcp summary lib/foo.rb
|
|
107
|
-
simplecov-mcp
|
|
114
|
+
simplecov-mcp --resultset coverage list
|
|
115
|
+
simplecov-mcp --format json --resultset coverage summary lib/foo.rb
|
|
116
|
+
simplecov-mcp --source uncovered --context-lines 2 uncovered lib/foo.rb
|
|
117
|
+
simplecov-mcp totals --format json
|
|
108
118
|
EXAMPLES
|
|
109
119
|
end
|
|
110
120
|
|
|
111
|
-
def add_help_handler(
|
|
112
|
-
|
|
113
|
-
puts
|
|
121
|
+
private def add_help_handler(parser)
|
|
122
|
+
parser.on('-h', '--help', 'Show help') do
|
|
123
|
+
puts parser
|
|
124
|
+
gem_root = File.expand_path('../..', __dir__)
|
|
125
|
+
puts "\nFor more detailed help, consult README.md and docs/user/**/*.md"
|
|
126
|
+
puts "in the installed gem at: #{gem_root}"
|
|
114
127
|
exit 0
|
|
115
128
|
end
|
|
116
129
|
end
|
|
117
130
|
|
|
118
|
-
def normalize_sort_order(
|
|
119
|
-
OptionNormalizers.normalize_sort_order(
|
|
131
|
+
private def normalize_sort_order(value)
|
|
132
|
+
OptionNormalizers.normalize_sort_order(value, strict: true)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private def normalize_source_mode(value)
|
|
136
|
+
OptionNormalizers.normalize_source_mode(value, strict: true)
|
|
120
137
|
end
|
|
121
138
|
|
|
122
|
-
def
|
|
123
|
-
OptionNormalizers.
|
|
139
|
+
private def normalize_staleness(value)
|
|
140
|
+
OptionNormalizers.normalize_staleness(value, strict: true)
|
|
124
141
|
end
|
|
125
142
|
|
|
126
|
-
def
|
|
127
|
-
OptionNormalizers.
|
|
143
|
+
private def normalize_error_mode(value)
|
|
144
|
+
OptionNormalizers.normalize_error_mode(value, strict: true)
|
|
128
145
|
end
|
|
129
146
|
|
|
130
|
-
def
|
|
131
|
-
OptionNormalizers.
|
|
147
|
+
private def normalize_format(value)
|
|
148
|
+
OptionNormalizers.normalize_format(value, strict: true)
|
|
132
149
|
end
|
|
133
150
|
end
|
|
134
151
|
end
|
|
@@ -35,15 +35,13 @@ module SimpleCovMcp
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
nil
|
|
38
|
-
rescue
|
|
38
|
+
rescue
|
|
39
39
|
# Ignore errors during pre-scan; they'll be caught during actual parsing
|
|
40
40
|
nil
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def normalize_error_mode(value)
|
|
46
|
-
OptionNormalizers.normalize_error_mode(value, strict: false, default: :on)
|
|
43
|
+
private def normalize_error_mode(value)
|
|
44
|
+
OptionNormalizers.normalize_error_mode(value, strict: false, default: :log)
|
|
47
45
|
end
|
|
48
46
|
end
|
|
49
47
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module SimpleCovMcp
|
|
4
4
|
module OptionParsers
|
|
5
5
|
class ErrorHelper
|
|
6
|
-
SUBCOMMANDS = %w[list summary raw uncovered detailed version].freeze
|
|
6
|
+
SUBCOMMANDS = %w[list summary raw uncovered detailed totals version].freeze
|
|
7
7
|
|
|
8
8
|
def initialize(subcommands = SUBCOMMANDS)
|
|
9
9
|
@subcommands = subcommands
|
|
@@ -14,7 +14,7 @@ module SimpleCovMcp
|
|
|
14
14
|
# Suggest a subcommand when an invalid option matches a known subcommand
|
|
15
15
|
option = extract_invalid_option(message)
|
|
16
16
|
|
|
17
|
-
if option
|
|
17
|
+
if option&.start_with?('--') && @subcommands.include?(option[2..])
|
|
18
18
|
suggest_subcommand(option)
|
|
19
19
|
else
|
|
20
20
|
# Generic message from OptionParser
|
|
@@ -28,19 +28,19 @@ module SimpleCovMcp
|
|
|
28
28
|
exit 1
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
private def extract_invalid_option(message)
|
|
32
|
+
message.match(/invalid option: (.+)/)[1]
|
|
33
|
+
rescue
|
|
34
|
+
nil
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def suggest_subcommand(option)
|
|
38
|
-
subcommand = option[2
|
|
37
|
+
private def suggest_subcommand(option)
|
|
38
|
+
subcommand = option[2..]
|
|
39
39
|
warn "Error: '#{option}' is not a valid option. Did you mean the '#{subcommand}' subcommand?"
|
|
40
40
|
warn "Try: #{program_name} #{subcommand} [args]"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def build_enum_value_hint(argv)
|
|
43
|
+
private def build_enum_value_hint(argv)
|
|
44
44
|
rules = enumerated_option_rules
|
|
45
45
|
tokens = Array(argv)
|
|
46
46
|
rules.each do |rule|
|
|
@@ -50,7 +50,7 @@ module SimpleCovMcp
|
|
|
50
50
|
nil
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def build_hint_for_rule(rule, tokens)
|
|
53
|
+
private def build_hint_for_rule(rule, tokens)
|
|
54
54
|
switches = rule[:switches]
|
|
55
55
|
allowed = rule[:values]
|
|
56
56
|
display = rule[:display] || allowed.join(', ')
|
|
@@ -72,17 +72,17 @@ module SimpleCovMcp
|
|
|
72
72
|
nil
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
def equal_form_match?(token, switches, preferred)
|
|
75
|
+
private def equal_form_match?(token, switches, preferred)
|
|
76
76
|
token.start_with?(preferred + '=') || switches.any? { |s| token.start_with?(s + '=') }
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
def handle_equal_form(token, switches, preferred, display, allowed)
|
|
79
|
+
private def handle_equal_form(token, switches, preferred, display, allowed)
|
|
80
80
|
sw = switches.find { |s| token.start_with?(s + '=') } || preferred
|
|
81
81
|
val = token.split('=', 2)[1]
|
|
82
82
|
"Valid values for #{sw}: #{display}" if val && !allowed.include?(val)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
def handle_space_form(tokens, index, preferred, display, allowed)
|
|
85
|
+
private def handle_space_form(tokens, index, preferred, display, allowed)
|
|
86
86
|
val = tokens[index + 1]
|
|
87
87
|
# If missing value, provide hint; if present and invalid, also hint
|
|
88
88
|
if val.nil? || val.start_with?('-') || !allowed.include?(val)
|
|
@@ -90,18 +90,19 @@ module SimpleCovMcp
|
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
def enumerated_option_rules
|
|
93
|
+
private def enumerated_option_rules
|
|
94
94
|
[
|
|
95
|
-
{ switches: ['-S', '--
|
|
95
|
+
{ switches: ['-S', '--staleness'], values: %w[off o error e], display: 'o[ff]|e[rror]' },
|
|
96
96
|
{ switches: ['-s', '--source'], values: %w[full f uncovered u],
|
|
97
97
|
display: 'f[ull]|u[ncovered]' },
|
|
98
|
-
{ switches: ['--error-mode'], values: %w[off
|
|
98
|
+
{ switches: ['--error-mode'], values: %w[off o log l debug d],
|
|
99
|
+
display: 'o[ff]|l[og]|d[ebug]' },
|
|
99
100
|
{ switches: ['-o', '--sort-order'], values: %w[a d ascending descending],
|
|
100
101
|
display: 'a[scending]|d[escending]' }
|
|
101
102
|
]
|
|
102
103
|
end
|
|
103
104
|
|
|
104
|
-
def program_name
|
|
105
|
+
private def program_name
|
|
105
106
|
'simplecov-mcp'
|
|
106
107
|
end
|
|
107
108
|
end
|
|
@@ -16,9 +16,22 @@ module SimpleCovMcp
|
|
|
16
16
|
deep_copy_and_relativize(obj)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# Converts an absolute path to a path relative to the root.
|
|
20
|
+
# Falls back to the original path if conversion fails (e.g., different drive on Windows).
|
|
21
|
+
#
|
|
22
|
+
# @param path [String] file path (absolute or relative)
|
|
23
|
+
# @return [String] relative path or original path on failure
|
|
24
|
+
def relativize_path(path)
|
|
25
|
+
root_str = @root.to_s
|
|
26
|
+
abs = File.absolute_path(path, root_str)
|
|
27
|
+
return path unless abs.start_with?(root_prefix(root_str)) || abs == root_str
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
Pathname.new(abs).relative_path_from(@root).to_s
|
|
30
|
+
rescue ArgumentError
|
|
31
|
+
path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private def deep_copy_and_relativize(obj)
|
|
22
35
|
case obj
|
|
23
36
|
when Hash
|
|
24
37
|
obj.each_with_object({}) do |(k, v), acc|
|
|
@@ -31,7 +44,7 @@ module SimpleCovMcp
|
|
|
31
44
|
end
|
|
32
45
|
end
|
|
33
46
|
|
|
34
|
-
def relativize_value(key, value)
|
|
47
|
+
private def relativize_value(key, value)
|
|
35
48
|
key_str = key.to_s
|
|
36
49
|
if @scalar_keys.include?(key_str) && value.is_a?(String)
|
|
37
50
|
relativize_path(value)
|
|
@@ -44,17 +57,7 @@ module SimpleCovMcp
|
|
|
44
57
|
end
|
|
45
58
|
end
|
|
46
59
|
|
|
47
|
-
def
|
|
48
|
-
abs = File.absolute_path(path, @root.to_s)
|
|
49
|
-
root_str = @root.to_s
|
|
50
|
-
return path unless abs.start_with?(root_prefix(root_str)) || abs == root_str
|
|
51
|
-
|
|
52
|
-
Pathname.new(abs).relative_path_from(@root).to_s
|
|
53
|
-
rescue ArgumentError
|
|
54
|
-
path
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def root_prefix(root_str)
|
|
60
|
+
private def root_prefix(root_str)
|
|
58
61
|
root_str.end_with?(File::SEPARATOR) ? root_str : root_str + File::SEPARATOR
|
|
59
62
|
end
|
|
60
63
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCovMcp
|
|
4
|
+
# Evaluates coverage predicates from either Ruby code strings or files.
|
|
5
|
+
# Used by the validate subcommand, validate MCP tool, and library API.
|
|
6
|
+
#
|
|
7
|
+
# Security Warning:
|
|
8
|
+
# Predicates execute as arbitrary Ruby code with full system privileges.
|
|
9
|
+
# Only use predicates from trusted sources.
|
|
10
|
+
class PredicateEvaluator
|
|
11
|
+
# Evaluate a predicate from a code string
|
|
12
|
+
#
|
|
13
|
+
# @param code [String] Ruby code that returns a callable (lambda, proc, or object with #call)
|
|
14
|
+
# @param model [CoverageModel] The coverage model to pass to the predicate
|
|
15
|
+
# @return [Boolean] The result of calling the predicate with the model
|
|
16
|
+
# @raise [RuntimeError] If the code doesn't return a callable or has syntax errors
|
|
17
|
+
def self.evaluate_code(code, model)
|
|
18
|
+
# WARNING: The predicate code executes with full Ruby privileges.
|
|
19
|
+
# It has unrestricted access to the file system, network, and system commands.
|
|
20
|
+
# Only use predicate code from trusted sources.
|
|
21
|
+
#
|
|
22
|
+
# We evaluate in a fresh Object context to prevent accidental access to
|
|
23
|
+
# internals, but this provides NO security isolation.
|
|
24
|
+
evaluation_context = Object.new
|
|
25
|
+
predicate = evaluation_context.instance_eval(code, '<predicate>', 1)
|
|
26
|
+
|
|
27
|
+
validate_callable(predicate)
|
|
28
|
+
predicate.call(model)
|
|
29
|
+
rescue SyntaxError => e
|
|
30
|
+
raise "Syntax error in predicate code: #{e.message}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Evaluate a predicate from a file
|
|
34
|
+
#
|
|
35
|
+
# @param path [String] Path to Ruby file containing predicate code
|
|
36
|
+
# @param model [CoverageModel] The coverage model to pass to the predicate
|
|
37
|
+
# @return [Boolean] The result of calling the predicate with the model
|
|
38
|
+
# @raise [RuntimeError] If the file doesn't exist, doesn't return a callable, or has syntax errors
|
|
39
|
+
def self.evaluate_file(path, model)
|
|
40
|
+
unless File.exist?(path)
|
|
41
|
+
raise "Predicate file not found: #{path}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
content = File.read(path)
|
|
45
|
+
|
|
46
|
+
# WARNING: The predicate code executes with full Ruby privileges.
|
|
47
|
+
# It has unrestricted access to the file system, network, and system commands.
|
|
48
|
+
# Only use predicate files from trusted sources.
|
|
49
|
+
#
|
|
50
|
+
# We evaluate in a fresh Object context to prevent accidental access to
|
|
51
|
+
# internals, but this provides NO security isolation.
|
|
52
|
+
evaluation_context = Object.new
|
|
53
|
+
predicate = evaluation_context.instance_eval(content, path, 1)
|
|
54
|
+
|
|
55
|
+
validate_callable(predicate)
|
|
56
|
+
predicate.call(model)
|
|
57
|
+
rescue SyntaxError => e
|
|
58
|
+
raise "Syntax error in predicate file: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validate that an object is callable
|
|
62
|
+
#
|
|
63
|
+
# @param predicate [Object] The object to check
|
|
64
|
+
# @raise [RuntimeError] If the object doesn't respond to #call
|
|
65
|
+
def self.validate_callable(predicate)
|
|
66
|
+
unless predicate.respond_to?(:call)
|
|
67
|
+
raise 'Predicate must be callable (lambda, proc, or object with #call method)'
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
private_class_method :validate_callable
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -6,9 +6,7 @@ module SimpleCovMcp
|
|
|
6
6
|
module Presenters
|
|
7
7
|
# Provides shared detailed coverage payloads for CLI and MCP callers.
|
|
8
8
|
class CoverageDetailedPresenter < BaseCoveragePresenter
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
def build_payload
|
|
9
|
+
private def build_payload
|
|
12
10
|
model.detailed_for(path)
|
|
13
11
|
end
|
|
14
12
|
end
|
|
@@ -6,9 +6,7 @@ module SimpleCovMcp
|
|
|
6
6
|
module Presenters
|
|
7
7
|
# Builds a consistent summary payload that both the CLI and MCP surfaces can use.
|
|
8
8
|
class CoverageSummaryPresenter < BaseCoveragePresenter
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
def build_payload
|
|
9
|
+
private def build_payload
|
|
12
10
|
model.summary_for(path)
|
|
13
11
|
end
|
|
14
12
|
end
|
|
@@ -6,9 +6,7 @@ module SimpleCovMcp
|
|
|
6
6
|
module Presenters
|
|
7
7
|
# Provides shared uncovered coverage payloads for CLI and MCP callers.
|
|
8
8
|
class CoverageUncoveredPresenter < BaseCoveragePresenter
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
def build_payload
|
|
9
|
+
private def build_payload
|
|
12
10
|
model.uncovered_for(path)
|
|
13
11
|
end
|
|
14
12
|
end
|
|
@@ -40,9 +40,7 @@ module SimpleCovMcp
|
|
|
40
40
|
relativized_payload['counts']
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def build_counts(files)
|
|
43
|
+
private def build_counts(files)
|
|
46
44
|
total = files.length
|
|
47
45
|
stale = files.count { |f| f['stale'] }
|
|
48
46
|
{ 'total' => total, 'ok' => total - stale, 'stale' => stale }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCovMcp
|
|
4
|
+
module Presenters
|
|
5
|
+
# Provides aggregated line totals and average coverage across the project.
|
|
6
|
+
class ProjectTotalsPresenter
|
|
7
|
+
attr_reader :model, :check_stale, :tracked_globs
|
|
8
|
+
|
|
9
|
+
def initialize(model:, check_stale:, tracked_globs:)
|
|
10
|
+
@model = model
|
|
11
|
+
@check_stale = check_stale
|
|
12
|
+
@tracked_globs = tracked_globs
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def absolute_payload
|
|
16
|
+
@absolute_payload ||= model.project_totals(
|
|
17
|
+
tracked_globs: tracked_globs,
|
|
18
|
+
check_stale: check_stale
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def relativized_payload
|
|
23
|
+
@relativized_payload ||= model.relativize(absolute_payload)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|