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.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +98 -50
  3. data/docs/{ARCHITECTURE.md → dev/ARCHITECTURE.md} +11 -10
  4. data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
  5. data/docs/{DEVELOPMENT.md → dev/DEVELOPMENT.md} +2 -1
  6. data/docs/dev/README.md +10 -0
  7. data/docs/dev/RELEASING.md +146 -0
  8. data/docs/{arch-decisions → dev/arch-decisions}/001-x-arch-decision.md +3 -1
  9. data/docs/{arch-decisions → dev/arch-decisions}/002-x-arch-decision.md +7 -5
  10. data/docs/{arch-decisions → dev/arch-decisions}/003-x-arch-decision.md +2 -0
  11. data/docs/{arch-decisions → dev/arch-decisions}/004-x-arch-decision.md +6 -2
  12. data/docs/{arch-decisions → dev/arch-decisions}/005-x-arch-decision.md +4 -2
  13. data/docs/{arch-decisions → dev/arch-decisions}/README.md +3 -3
  14. data/docs/{presentations → dev/presentations}/simplecov-mcp-presentation.md +28 -22
  15. data/docs/fixtures/demo_project/README.md +9 -0
  16. data/docs/{ADVANCED_USAGE.md → user/ADVANCED_USAGE.md} +129 -319
  17. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
  18. data/docs/user/CLI_USAGE.md +750 -0
  19. data/docs/{ERROR_HANDLING.md → user/ERROR_HANDLING.md} +12 -12
  20. data/docs/user/EXAMPLES.md +588 -0
  21. data/docs/user/INSTALLATION.md +130 -0
  22. data/docs/{LIBRARY_API.md → user/LIBRARY_API.md} +90 -32
  23. data/docs/{MCP_INTEGRATION.md → user/MCP_INTEGRATION.md} +36 -34
  24. data/docs/user/README.md +14 -0
  25. data/docs/{TROUBLESHOOTING.md → user/TROUBLESHOOTING.md} +21 -100
  26. data/docs/user/V2-BREAKING-CHANGES.md +472 -0
  27. data/exe/simplecov-mcp +1 -1
  28. data/lib/simplecov_mcp/{cli_config.rb → app_config.rb} +12 -12
  29. data/lib/simplecov_mcp/app_context.rb +1 -1
  30. data/lib/simplecov_mcp/base_tool.rb +66 -38
  31. data/lib/simplecov_mcp/cli.rb +67 -123
  32. data/lib/simplecov_mcp/commands/base_command.rb +16 -27
  33. data/lib/simplecov_mcp/commands/command_factory.rb +8 -2
  34. data/lib/simplecov_mcp/commands/detailed_command.rb +16 -2
  35. data/lib/simplecov_mcp/commands/list_command.rb +1 -1
  36. data/lib/simplecov_mcp/commands/raw_command.rb +18 -2
  37. data/lib/simplecov_mcp/commands/summary_command.rb +20 -3
  38. data/lib/simplecov_mcp/commands/totals_command.rb +53 -0
  39. data/lib/simplecov_mcp/commands/uncovered_command.rb +24 -5
  40. data/lib/simplecov_mcp/commands/validate_command.rb +60 -0
  41. data/lib/simplecov_mcp/commands/version_command.rb +19 -4
  42. data/lib/simplecov_mcp/config_parser.rb +32 -0
  43. data/lib/simplecov_mcp/constants.rb +3 -3
  44. data/lib/simplecov_mcp/coverage_reporter.rb +31 -0
  45. data/lib/simplecov_mcp/error_handler.rb +81 -40
  46. data/lib/simplecov_mcp/error_handler_factory.rb +2 -2
  47. data/lib/simplecov_mcp/errors.rb +12 -19
  48. data/lib/simplecov_mcp/formatters/source_formatter.rb +23 -19
  49. data/lib/simplecov_mcp/formatters.rb +51 -0
  50. data/lib/simplecov_mcp/mcp_server.rb +9 -7
  51. data/lib/simplecov_mcp/mode_detector.rb +6 -5
  52. data/lib/simplecov_mcp/model.rb +122 -88
  53. data/lib/simplecov_mcp/option_normalizers.rb +39 -18
  54. data/lib/simplecov_mcp/option_parser_builder.rb +82 -65
  55. data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +3 -5
  56. data/lib/simplecov_mcp/option_parsers/error_helper.rb +18 -17
  57. data/lib/simplecov_mcp/path_relativizer.rb +17 -14
  58. data/lib/simplecov_mcp/predicate_evaluator.rb +72 -0
  59. data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +1 -3
  60. data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +1 -3
  61. data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +1 -3
  62. data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +1 -3
  63. data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +1 -3
  64. data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +1 -3
  65. data/lib/simplecov_mcp/presenters/project_totals_presenter.rb +27 -0
  66. data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +14 -18
  67. data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +7 -9
  68. data/lib/simplecov_mcp/resultset_loader.rb +20 -25
  69. data/lib/simplecov_mcp/staleness_checker.rb +50 -46
  70. data/lib/simplecov_mcp/table_formatter.rb +64 -0
  71. data/lib/simplecov_mcp/tools/all_files_coverage_tool.rb +20 -50
  72. data/lib/simplecov_mcp/tools/coverage_detailed_tool.rb +13 -7
  73. data/lib/simplecov_mcp/tools/coverage_raw_tool.rb +12 -7
  74. data/lib/simplecov_mcp/tools/coverage_summary_tool.rb +13 -8
  75. data/lib/simplecov_mcp/tools/coverage_table_tool.rb +20 -60
  76. data/lib/simplecov_mcp/tools/coverage_totals_tool.rb +44 -0
  77. data/lib/simplecov_mcp/tools/help_tool.rb +38 -66
  78. data/lib/simplecov_mcp/tools/uncovered_lines_tool.rb +13 -8
  79. data/lib/simplecov_mcp/tools/validate_tool.rb +72 -0
  80. data/lib/simplecov_mcp/tools/version_tool.rb +7 -14
  81. data/lib/simplecov_mcp/util.rb +18 -12
  82. data/lib/simplecov_mcp/version.rb +1 -1
  83. data/lib/simplecov_mcp.rb +23 -29
  84. data/spec/all_files_coverage_tool_spec.rb +4 -3
  85. data/spec/{cli_config_spec.rb → app_config_spec.rb} +31 -26
  86. data/spec/base_tool_spec.rb +17 -14
  87. data/spec/cli/show_default_report_spec.rb +2 -2
  88. data/spec/cli_enumerated_options_spec.rb +31 -9
  89. data/spec/cli_error_spec.rb +46 -23
  90. data/spec/cli_format_spec.rb +123 -0
  91. data/spec/cli_json_options_spec.rb +50 -0
  92. data/spec/cli_source_spec.rb +11 -63
  93. data/spec/cli_spec.rb +82 -97
  94. data/spec/cli_usage_spec.rb +15 -15
  95. data/spec/commands/base_command_spec.rb +12 -92
  96. data/spec/commands/command_factory_spec.rb +7 -3
  97. data/spec/commands/detailed_command_spec.rb +10 -24
  98. data/spec/commands/list_command_spec.rb +28 -0
  99. data/spec/commands/raw_command_spec.rb +43 -20
  100. data/spec/commands/summary_command_spec.rb +10 -23
  101. data/spec/commands/totals_command_spec.rb +34 -0
  102. data/spec/commands/uncovered_command_spec.rb +29 -23
  103. data/spec/commands/validate_command_spec.rb +213 -0
  104. data/spec/commands/version_command_spec.rb +38 -0
  105. data/spec/constants_spec.rb +3 -3
  106. data/spec/coverage_reporter_spec.rb +102 -0
  107. data/spec/coverage_table_tool_spec.rb +21 -10
  108. data/spec/coverage_totals_tool_spec.rb +37 -0
  109. data/spec/error_handler_spec.rb +120 -4
  110. data/spec/error_mode_spec.rb +18 -22
  111. data/spec/errors_edge_cases_spec.rb +101 -28
  112. data/spec/errors_stale_spec.rb +34 -0
  113. data/spec/file_based_mcp_tools_spec.rb +6 -6
  114. data/spec/fixtures/project1/lib/bar.rb +2 -0
  115. data/spec/fixtures/project1/lib/foo.rb +2 -0
  116. data/spec/help_tool_spec.rb +2 -18
  117. data/spec/integration_spec.rb +103 -161
  118. data/spec/logging_fallback_spec.rb +3 -3
  119. data/spec/mcp_server_integration_spec.rb +1 -1
  120. data/spec/mcp_server_spec.rb +70 -53
  121. data/spec/mode_detector_spec.rb +46 -41
  122. data/spec/model_error_handling_spec.rb +139 -78
  123. data/spec/model_staleness_spec.rb +13 -13
  124. data/spec/option_normalizers_spec.rb +111 -112
  125. data/spec/option_parsers/env_options_parser_spec.rb +25 -37
  126. data/spec/option_parsers/error_helper_spec.rb +56 -56
  127. data/spec/path_relativizer_spec.rb +15 -0
  128. data/spec/presenters/coverage_detailed_presenter_spec.rb +1 -1
  129. data/spec/presenters/coverage_summary_presenter_spec.rb +1 -1
  130. data/spec/presenters/coverage_uncovered_presenter_spec.rb +1 -1
  131. data/spec/presenters/project_coverage_presenter_spec.rb +9 -8
  132. data/spec/presenters/project_totals_presenter_spec.rb +144 -0
  133. data/spec/resolvers/coverage_line_resolver_spec.rb +261 -36
  134. data/spec/resolvers/resultset_path_resolver_spec.rb +13 -8
  135. data/spec/shared_examples/file_based_mcp_tools.rb +23 -18
  136. data/spec/shared_examples/formatted_command_examples.rb +64 -0
  137. data/spec/simple_cov_mcp_module_spec.rb +24 -3
  138. data/spec/simplecov_mcp/formatters/source_formatter_spec.rb +267 -0
  139. data/spec/simplecov_mcp/formatters_spec.rb +76 -0
  140. data/spec/simplecov_mcp/presenters/base_coverage_presenter_spec.rb +79 -0
  141. data/spec/simplecov_mcp_model_spec.rb +97 -47
  142. data/spec/simplecov_mcp_opts_spec.rb +42 -39
  143. data/spec/spec_helper.rb +27 -92
  144. data/spec/staleness_checker_spec.rb +10 -9
  145. data/spec/staleness_more_spec.rb +4 -4
  146. data/spec/support/cli_helpers.rb +22 -0
  147. data/spec/support/control_flow_helpers.rb +20 -0
  148. data/spec/support/fake_mcp.rb +40 -0
  149. data/spec/support/io_helpers.rb +29 -0
  150. data/spec/support/mcp_helpers.rb +35 -0
  151. data/spec/support/mcp_runner.rb +10 -8
  152. data/spec/support/mocking_helpers.rb +30 -0
  153. data/spec/table_format_spec.rb +70 -0
  154. data/spec/tools/validate_tool_spec.rb +132 -0
  155. data/spec/tools_error_handling_spec.rb +34 -48
  156. data/spec/util_spec.rb +5 -4
  157. data/spec/version_spec.rb +7 -7
  158. data/spec/version_tool_spec.rb +20 -22
  159. metadata +90 -23
  160. data/docs/BRANCH_ONLY_COVERAGE.md +0 -81
  161. data/docs/CLI_USAGE.md +0 -637
  162. data/docs/EXAMPLES.md +0 -430
  163. data/docs/INSTALLATION.md +0 -352
  164. 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 maybe_output_json(data, model)
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 data['lines'].inspect
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 emit_json_with_optional_source(data, model, path)
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
- printf "%8.2f%% %6d/%-6d %s\n\n", summary['pct'], summary['covered'], summary['total'],
18
- relative_path
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 emit_json_with_optional_source(data, model, path)
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
- printf "Summary: %8.2f%% %6d/%-6d\n\n", summary['pct'], summary['covered'],
20
- summary['total']
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(args)
10
- if config.json
11
- puts JSON.pretty_generate({ version: SimpleCovMcp::VERSION })
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 "SimpleCovMcp version #{SimpleCovMcp::VERSION}"
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 --source-context
15
- -S --stale
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, :on, :trace].freeze
12
+ VALID_ERROR_MODES = [:off, :log, :debug].freeze
13
13
 
14
- def initialize(error_mode: :on, logger: nil)
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 == :trace
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
- def convert_standard_error(error)
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
- filename = extract_filename(error.message)
44
- FileNotFoundError.new("File not found: #{filename}", error)
45
+ convert_enoent(error, context)
45
46
  when Errno::EACCES
46
- filename = extract_filename(error.message)
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 - JSON parsing failed: #{error.message}", error)
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
- if error.message.include?('wrong number of arguments')
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
- when RuntimeError, StandardError
63
- # Handle string errors from CovUtil and other runtime errors
64
- if error.message.include?('Could not find .resultset.json')
65
- # Extract directory info if available
66
- dir_info = error.message.match(/under (.+?)(?:;|$)/)&.[](1) || 'project directory'
67
- CoverageDataError.new("Coverage data not found in #{dir_info} - please run your tests first", error)
68
- elsif error.message.include?('No .resultset.json found in directory')
69
- # Extract directory from error message
70
- dir_info = error.message.match(/directory: (.+)$/)&.[](1) || 'specified directory'
71
- CoverageDataError.new("Coverage data not found in directory: #{dir_info}", error)
72
- elsif error.message.include?('Specified resultset not found')
73
- # Extract path from error message
74
- path_info = error.message.match(/not found: (.+)$/)&.[](1) || 'specified path'
75
- ResultsetNotFoundError.new("Resultset file not found: #{path_info}", error)
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
- Error.new("An unexpected error occurred: #{error.message}", error)
119
+ CoverageDataError.new("Failed to load coverage data: #{message}", error)
78
120
  end
79
121
  else
80
- Error.new("An unexpected error occurred: #{error.message}", error)
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
- parts = ["Error#{context ? " in #{context}" : ''}: #{error.class}: #{error.message}"]
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
- # Extract method info from "undefined method `foo' for #<Object:0x...>"
115
- if match = message.match(/undefined method `(.+?)' for (.+)$/)
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: :on)
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: :on)
27
+ def self.for_mcp_server(error_mode: :log)
28
28
  ErrorHandler.new(error_mode: error_mode)
29
29
  end
30
30
  end
@@ -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 StandardError
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 StandardError
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 StandardError
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
- base = "Coverage data stale: #{message || default_message}"
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'})"