cov-loupe 3.0.0 → 4.0.0.pre
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/AGENTS.md +230 -0
- data/CLAUDE.md +5 -0
- data/CODE_OF_CONDUCT.md +62 -0
- data/CONTRIBUTING.md +102 -0
- data/GEMINI.md +5 -0
- data/README.md +154 -51
- data/RELEASE_NOTES.md +452 -0
- data/dev/images/cov-loupe-icon-lores.png +0 -0
- data/dev/images/cov-loupe-icon-square.png +0 -0
- data/dev/images/cov-loupe-icon.png +0 -0
- data/dev/images/cov-loupe-logo.png +0 -0
- data/dev/prompts/README.md +74 -0
- data/dev/prompts/archive/architectural-review-and-actions-prompt.md +53 -0
- data/dev/prompts/archive/investigate-and-report-issues-prompt.md +33 -0
- data/dev/prompts/archive/produce-action-items-prompt.md +25 -0
- data/dev/prompts/guidelines/ai-code-evaluator-guidelines.md +337 -0
- data/dev/prompts/improve/refactor-test-suite.md +18 -0
- data/dev/prompts/improve/simplify-code-logic.md +133 -0
- data/dev/prompts/improve/update-documentation.md +21 -0
- data/dev/prompts/review/comprehensive-codebase-review.md +176 -0
- data/dev/prompts/review/identify-action-items.md +143 -0
- data/dev/prompts/review/verify-code-changes.md +54 -0
- data/dev/prompts/validate/create-screencast-outline.md +234 -0
- data/dev/prompts/validate/test-documentation-examples.md +180 -0
- data/docs/QUICKSTART.md +63 -0
- data/docs/assets/images/cov-loupe-logo-lores.png +0 -0
- data/docs/assets/images/cov-loupe-logo.png +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/assets/stylesheets/branding.css +16 -0
- data/docs/assets/stylesheets/extra.css +15 -0
- data/docs/code_of_conduct.md +1 -0
- data/docs/contributing.md +1 -0
- data/docs/dev/ARCHITECTURE.md +56 -11
- data/docs/dev/DEVELOPMENT.md +116 -12
- data/docs/dev/FUTURE_ENHANCEMENTS.md +14 -0
- data/docs/dev/README.md +3 -2
- data/docs/dev/RELEASING.md +2 -0
- data/docs/dev/arch-decisions/README.md +10 -7
- data/docs/dev/arch-decisions/application-architecture.md +259 -0
- data/docs/dev/arch-decisions/coverage-data-quality.md +193 -0
- data/docs/dev/arch-decisions/output-character-mode.md +217 -0
- data/docs/dev/arch-decisions/path-resolution.md +90 -0
- data/docs/dev/arch-decisions/{004-x-arch-decision.md → policy-validation.md} +32 -28
- data/docs/dev/arch-decisions/{005-x-arch-decision.md → simplecov-integration.md} +47 -44
- data/docs/dev/presentations/cov-loupe-presentation.md +15 -13
- data/docs/examples/mcp-inputs.md +3 -0
- data/docs/examples/prompts.md +3 -0
- data/docs/examples/success_predicates.md +3 -0
- data/docs/fixtures/demo_project/.resultset.json +170 -0
- data/docs/fixtures/demo_project/README.md +6 -0
- data/docs/fixtures/demo_project/app/controllers/admin/audit_logs_controller.rb +19 -0
- data/docs/fixtures/demo_project/app/controllers/orders_controller.rb +26 -0
- data/docs/fixtures/demo_project/app/models/order.rb +20 -0
- data/docs/fixtures/demo_project/app/models/user.rb +19 -0
- data/docs/fixtures/demo_project/lib/api/client.rb +22 -0
- data/docs/fixtures/demo_project/lib/ops/jobs/cleanup_job.rb +16 -0
- data/docs/fixtures/demo_project/lib/ops/jobs/report_job.rb +17 -0
- data/docs/fixtures/demo_project/lib/payments/processor.rb +15 -0
- data/docs/fixtures/demo_project/lib/payments/refund_service.rb +15 -0
- data/docs/fixtures/demo_project/lib/payments/reporting/exporter.rb +16 -0
- data/docs/index.md +1 -0
- data/docs/license.md +3 -0
- data/docs/release_notes.md +3 -0
- data/docs/user/ADVANCED_USAGE.md +208 -115
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +2 -0
- data/docs/user/CLI_USAGE.md +276 -101
- data/docs/user/ERROR_HANDLING.md +4 -4
- data/docs/user/EXAMPLES.md +121 -128
- data/docs/user/INSTALLATION.md +9 -28
- data/docs/user/LIBRARY_API.md +227 -122
- data/docs/user/MCP_INTEGRATION.md +114 -203
- data/docs/user/README.md +5 -1
- data/docs/user/TROUBLESHOOTING.md +49 -27
- data/docs/user/installing-a-prelease-version-of-covloupe.md +43 -0
- data/docs/user/{V2-BREAKING-CHANGES.md → migrations/MIGRATING_TO_V2.md} +62 -72
- data/docs/user/migrations/MIGRATING_TO_V3.md +72 -0
- data/docs/user/migrations/MIGRATING_TO_V4.md +591 -0
- data/docs/user/migrations/README.md +22 -0
- data/docs/user/prompts/README.md +9 -0
- data/docs/user/prompts/non-web-coverage-analysis-prompt.md +103 -0
- data/docs/user/prompts/rails-coverage-analysis-prompt.md +94 -0
- data/docs/user/prompts/use-cli-not-mcp-prompt.md +53 -0
- data/examples/cli_demo.sh +77 -0
- data/examples/filter_and_table_demo-output.md +114 -0
- data/examples/filter_and_table_demo.rb +174 -0
- data/examples/fixtures/demo_project/coverage/.resultset.json +10 -0
- data/examples/mcp-inputs/README.md +66 -0
- data/examples/mcp-inputs/coverage_detailed.json +1 -0
- data/examples/mcp-inputs/coverage_raw.json +1 -0
- data/examples/mcp-inputs/coverage_summary.json +1 -0
- data/examples/mcp-inputs/list.json +1 -0
- data/examples/mcp-inputs/uncovered_lines.json +1 -0
- data/examples/prompts/README.md +27 -0
- data/examples/prompts/custom_resultset.txt +2 -0
- data/examples/prompts/detailed_with_source.txt +2 -0
- data/examples/prompts/list_lowest.txt +2 -0
- data/examples/prompts/summary.txt +2 -0
- data/examples/prompts/uncovered.txt +2 -0
- data/examples/success_predicates/README.md +198 -0
- data/examples/success_predicates/all_files_above_threshold_predicate.rb +21 -0
- data/examples/success_predicates/directory_specific_thresholds_predicate.rb +30 -0
- data/examples/success_predicates/project_coverage_minimum_predicate.rb +6 -0
- data/lib/cov_loupe/base_tool.rb +229 -20
- data/lib/cov_loupe/cli.rb +132 -23
- data/lib/cov_loupe/commands/base_command.rb +25 -6
- data/lib/cov_loupe/commands/command_factory.rb +0 -1
- data/lib/cov_loupe/commands/detailed_command.rb +10 -5
- data/lib/cov_loupe/commands/list_command.rb +2 -1
- data/lib/cov_loupe/commands/raw_command.rb +7 -5
- data/lib/cov_loupe/commands/summary_command.rb +12 -7
- data/lib/cov_loupe/commands/totals_command.rb +74 -10
- data/lib/cov_loupe/commands/uncovered_command.rb +7 -5
- data/lib/cov_loupe/commands/validate_command.rb +11 -3
- data/lib/cov_loupe/commands/version_command.rb +6 -4
- data/lib/cov_loupe/{app_config.rb → config/app_config.rb} +13 -5
- data/lib/cov_loupe/config/app_context.rb +43 -0
- data/lib/cov_loupe/config/boolean_type.rb +91 -0
- data/lib/cov_loupe/config/logger.rb +92 -0
- data/lib/cov_loupe/{option_normalizers.rb → config/option_normalizers.rb} +55 -24
- data/lib/cov_loupe/{option_parser_builder.rb → config/option_parser_builder.rb} +46 -24
- data/lib/cov_loupe/coverage/coverage_calculator.rb +53 -0
- data/lib/cov_loupe/coverage/coverage_reporter.rb +63 -0
- data/lib/cov_loupe/coverage/coverage_table_formatter.rb +133 -0
- data/lib/cov_loupe/{error_handler.rb → errors/error_handler.rb} +21 -33
- data/lib/cov_loupe/{errors.rb → errors/errors.rb} +48 -71
- data/lib/cov_loupe/formatters/formatters.rb +75 -0
- data/lib/cov_loupe/formatters/source_formatter.rb +18 -7
- data/lib/cov_loupe/formatters/table_formatter.rb +80 -0
- data/lib/cov_loupe/loaders/all.rb +15 -0
- data/lib/cov_loupe/loaders/all_cli.rb +10 -0
- data/lib/cov_loupe/loaders/all_mcp.rb +23 -0
- data/lib/cov_loupe/loaders/resultset_loader.rb +147 -0
- data/lib/cov_loupe/mcp_server.rb +3 -2
- data/lib/cov_loupe/model/model.rb +520 -0
- data/lib/cov_loupe/model/model_data.rb +13 -0
- data/lib/cov_loupe/model/model_data_cache.rb +116 -0
- data/lib/cov_loupe/option_parsers/env_options_parser.rb +17 -6
- data/lib/cov_loupe/option_parsers/error_helper.rb +16 -10
- data/lib/cov_loupe/output_chars.rb +192 -0
- data/lib/cov_loupe/paths/glob_utils.rb +100 -0
- data/lib/cov_loupe/{path_relativizer.rb → paths/path_relativizer.rb} +5 -13
- data/lib/cov_loupe/paths/path_utils.rb +265 -0
- data/lib/cov_loupe/paths/volume_case_sensitivity.rb +173 -0
- data/lib/cov_loupe/presenters/base_coverage_presenter.rb +9 -13
- data/lib/cov_loupe/presenters/coverage_payload_presenter.rb +21 -0
- data/lib/cov_loupe/presenters/payload_caching.rb +23 -0
- data/lib/cov_loupe/presenters/project_coverage_presenter.rb +73 -21
- data/lib/cov_loupe/presenters/project_totals_presenter.rb +16 -10
- data/lib/cov_loupe/repositories/coverage_repository.rb +149 -0
- data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +90 -76
- data/lib/cov_loupe/resolvers/{resolver_factory.rb → resolver_helpers.rb} +6 -5
- data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +40 -12
- data/lib/cov_loupe/scripts/command_execution.rb +113 -0
- data/lib/cov_loupe/scripts/latest_ci_status.rb +97 -0
- data/lib/cov_loupe/scripts/pre_release_check.rb +164 -0
- data/lib/cov_loupe/scripts/setup_doc_server.rb +23 -0
- data/lib/cov_loupe/scripts/start_doc_server.rb +24 -0
- data/lib/cov_loupe/staleness/stale_status.rb +23 -0
- data/lib/cov_loupe/staleness/staleness_checker.rb +328 -0
- data/lib/cov_loupe/staleness/staleness_message_formatter.rb +91 -0
- data/lib/cov_loupe/tools/coverage_detailed_tool.rb +14 -15
- data/lib/cov_loupe/tools/coverage_raw_tool.rb +14 -14
- data/lib/cov_loupe/tools/coverage_summary_tool.rb +16 -16
- data/lib/cov_loupe/tools/coverage_table_tool.rb +139 -21
- data/lib/cov_loupe/tools/coverage_totals_tool.rb +31 -13
- data/lib/cov_loupe/tools/help_tool.rb +16 -20
- data/lib/cov_loupe/tools/list_tool.rb +65 -0
- data/lib/cov_loupe/tools/uncovered_lines_tool.rb +14 -14
- data/lib/cov_loupe/tools/validate_tool.rb +18 -24
- data/lib/cov_loupe/tools/version_tool.rb +8 -3
- data/lib/cov_loupe/version.rb +1 -1
- data/lib/cov_loupe.rb +83 -55
- metadata +184 -154
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +0 -158
- data/docs/dev/arch-decisions/001-x-arch-decision.md +0 -95
- data/docs/dev/arch-decisions/002-x-arch-decision.md +0 -159
- data/docs/dev/arch-decisions/003-x-arch-decision.md +0 -165
- data/lib/cov_loupe/app_context.rb +0 -26
- data/lib/cov_loupe/constants.rb +0 -22
- data/lib/cov_loupe/coverage_reporter.rb +0 -31
- data/lib/cov_loupe/formatters.rb +0 -51
- data/lib/cov_loupe/mode_detector.rb +0 -56
- data/lib/cov_loupe/model.rb +0 -339
- data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +0 -14
- data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +0 -14
- data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +0 -14
- data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +0 -14
- data/lib/cov_loupe/resultset_loader.rb +0 -131
- data/lib/cov_loupe/staleness_checker.rb +0 -247
- data/lib/cov_loupe/table_formatter.rb +0 -64
- data/lib/cov_loupe/tools/all_files_coverage_tool.rb +0 -51
- data/lib/cov_loupe/util.rb +0 -88
- data/spec/MCP_INTEGRATION_TESTS_README.md +0 -111
- data/spec/TIMESTAMPS.md +0 -48
- data/spec/all_files_coverage_tool_spec.rb +0 -53
- data/spec/app_config_spec.rb +0 -142
- data/spec/base_tool_spec.rb +0 -62
- data/spec/cli/show_default_report_spec.rb +0 -33
- data/spec/cli_enumerated_options_spec.rb +0 -90
- data/spec/cli_error_spec.rb +0 -184
- data/spec/cli_format_spec.rb +0 -123
- data/spec/cli_json_options_spec.rb +0 -50
- data/spec/cli_source_spec.rb +0 -44
- data/spec/cli_spec.rb +0 -192
- data/spec/cli_table_spec.rb +0 -28
- data/spec/cli_usage_spec.rb +0 -42
- data/spec/commands/base_command_spec.rb +0 -107
- data/spec/commands/command_factory_spec.rb +0 -76
- data/spec/commands/detailed_command_spec.rb +0 -34
- data/spec/commands/list_command_spec.rb +0 -28
- data/spec/commands/raw_command_spec.rb +0 -69
- data/spec/commands/summary_command_spec.rb +0 -34
- data/spec/commands/totals_command_spec.rb +0 -34
- data/spec/commands/uncovered_command_spec.rb +0 -55
- data/spec/commands/validate_command_spec.rb +0 -213
- data/spec/commands/version_command_spec.rb +0 -38
- data/spec/constants_spec.rb +0 -61
- data/spec/cov_loupe/formatters/source_formatter_spec.rb +0 -267
- data/spec/cov_loupe/formatters_spec.rb +0 -76
- data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +0 -79
- data/spec/cov_loupe_model_spec.rb +0 -454
- data/spec/cov_loupe_module_spec.rb +0 -37
- data/spec/cov_loupe_opts_spec.rb +0 -185
- data/spec/coverage_reporter_spec.rb +0 -102
- data/spec/coverage_table_tool_spec.rb +0 -59
- data/spec/coverage_totals_tool_spec.rb +0 -37
- data/spec/error_handler_spec.rb +0 -197
- data/spec/error_mode_spec.rb +0 -139
- data/spec/errors_edge_cases_spec.rb +0 -312
- data/spec/errors_stale_spec.rb +0 -83
- data/spec/file_based_mcp_tools_spec.rb +0 -99
- data/spec/help_tool_spec.rb +0 -26
- data/spec/integration_spec.rb +0 -789
- data/spec/logging_fallback_spec.rb +0 -128
- data/spec/mcp_logging_spec.rb +0 -44
- data/spec/mcp_server_integration_spec.rb +0 -23
- data/spec/mcp_server_spec.rb +0 -106
- data/spec/mode_detector_spec.rb +0 -153
- data/spec/model_error_handling_spec.rb +0 -269
- data/spec/model_staleness_spec.rb +0 -79
- data/spec/option_normalizers_spec.rb +0 -203
- data/spec/option_parsers/env_options_parser_spec.rb +0 -221
- data/spec/option_parsers/error_helper_spec.rb +0 -222
- data/spec/path_relativizer_spec.rb +0 -98
- data/spec/presenters/coverage_detailed_presenter_spec.rb +0 -19
- data/spec/presenters/coverage_raw_presenter_spec.rb +0 -15
- data/spec/presenters/coverage_summary_presenter_spec.rb +0 -15
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +0 -16
- data/spec/presenters/project_coverage_presenter_spec.rb +0 -87
- data/spec/presenters/project_totals_presenter_spec.rb +0 -144
- data/spec/resolvers/coverage_line_resolver_spec.rb +0 -282
- data/spec/resolvers/resolver_factory_spec.rb +0 -61
- data/spec/resolvers/resultset_path_resolver_spec.rb +0 -60
- data/spec/resultset_loader_spec.rb +0 -167
- data/spec/shared_examples/README.md +0 -115
- data/spec/shared_examples/coverage_presenter_examples.rb +0 -66
- data/spec/shared_examples/file_based_mcp_tools.rb +0 -179
- data/spec/shared_examples/formatted_command_examples.rb +0 -64
- data/spec/shared_examples/mcp_tool_text_json_response.rb +0 -16
- data/spec/spec_helper.rb +0 -127
- data/spec/staleness_checker_spec.rb +0 -374
- data/spec/staleness_more_spec.rb +0 -42
- data/spec/support/cli_helpers.rb +0 -22
- data/spec/support/control_flow_helpers.rb +0 -20
- data/spec/support/fake_mcp.rb +0 -40
- data/spec/support/io_helpers.rb +0 -29
- data/spec/support/mcp_helpers.rb +0 -35
- data/spec/support/mcp_runner.rb +0 -66
- data/spec/support/mocking_helpers.rb +0 -30
- data/spec/table_format_spec.rb +0 -70
- data/spec/tools/validate_tool_spec.rb +0 -132
- data/spec/tools_error_handling_spec.rb +0 -130
- data/spec/util_spec.rb +0 -154
- data/spec/version_spec.rb +0 -123
- data/spec/version_tool_spec.rb +0 -141
- /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/bar.rb +0 -0
- /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/foo.rb +0 -0
- /data/lib/cov_loupe/{config_parser.rb → config/config_parser.rb} +0 -0
- /data/lib/cov_loupe/{predicate_evaluator.rb → config/predicate_evaluator.rb} +0 -0
- /data/lib/cov_loupe/{error_handler_factory.rb → errors/error_handler_factory.rb} +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../staleness/staleness_message_formatter'
|
|
4
|
+
|
|
3
5
|
module CovLoupe
|
|
4
6
|
# Base error class for all SimpleCov MCP errors
|
|
5
7
|
class Error < StandardError
|
|
@@ -13,34 +15,6 @@ module CovLoupe
|
|
|
13
15
|
def user_friendly_message
|
|
14
16
|
message
|
|
15
17
|
end
|
|
16
|
-
|
|
17
|
-
protected def format_epoch_both(epoch_seconds)
|
|
18
|
-
return [nil, nil] unless epoch_seconds
|
|
19
|
-
|
|
20
|
-
t = Time.at(epoch_seconds.to_i)
|
|
21
|
-
[t.utc.iso8601, t.getlocal.iso8601]
|
|
22
|
-
rescue
|
|
23
|
-
[epoch_seconds.to_s, epoch_seconds.to_s]
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
protected def format_time_both(time)
|
|
27
|
-
return [nil, nil] unless time
|
|
28
|
-
|
|
29
|
-
t = time.is_a?(Time) ? time : Time.parse(time.to_s)
|
|
30
|
-
[t.utc.iso8601, t.getlocal.iso8601]
|
|
31
|
-
rescue
|
|
32
|
-
[time.to_s, time.to_s]
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
protected def format_delta_seconds(file_mtime, cov_timestamp)
|
|
36
|
-
return nil unless file_mtime && cov_timestamp
|
|
37
|
-
|
|
38
|
-
seconds = file_mtime.to_i - cov_timestamp.to_i
|
|
39
|
-
sign = seconds >= 0 ? '+' : '-'
|
|
40
|
-
"#{sign}#{seconds.abs}s"
|
|
41
|
-
rescue
|
|
42
|
-
nil
|
|
43
|
-
end
|
|
44
18
|
end
|
|
45
19
|
|
|
46
20
|
# Configuration or setup related errors
|
|
@@ -50,6 +24,13 @@ module CovLoupe
|
|
|
50
24
|
end
|
|
51
25
|
end
|
|
52
26
|
|
|
27
|
+
# Error wrapper when the root cause is unknown or unclassified.
|
|
28
|
+
class UnknownError < Error
|
|
29
|
+
def user_friendly_message
|
|
30
|
+
"An unexpected error occurred: #{message}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
53
34
|
# File or path related errors
|
|
54
35
|
class FileError < Error
|
|
55
36
|
def user_friendly_message
|
|
@@ -89,8 +70,26 @@ module CovLoupe
|
|
|
89
70
|
end
|
|
90
71
|
end
|
|
91
72
|
|
|
73
|
+
class CorruptCoverageDataError < CoverageDataError
|
|
74
|
+
def user_friendly_message
|
|
75
|
+
"Corrupt coverage data: #{message}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Shared module for stale error formatting
|
|
80
|
+
module StalenessFormatterMixin
|
|
81
|
+
private def formatter
|
|
82
|
+
@formatter ||= StalenessMessageFormatter.new(
|
|
83
|
+
cov_timestamp: @cov_timestamp,
|
|
84
|
+
resultset_path: @resultset_path
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
92
89
|
# Coverage data is present but appears stale compared to source files
|
|
93
90
|
class CoverageDataStaleError < CoverageDataError
|
|
91
|
+
include StalenessFormatterMixin
|
|
92
|
+
|
|
94
93
|
attr_reader :file_path, :file_mtime, :cov_timestamp, :src_len, :cov_len, :resultset_path
|
|
95
94
|
|
|
96
95
|
def initialize(message = nil, original_error = nil, file_path: nil, file_mtime: nil,
|
|
@@ -105,77 +104,55 @@ module CovLoupe
|
|
|
105
104
|
end
|
|
106
105
|
|
|
107
106
|
def user_friendly_message
|
|
108
|
-
"Coverage data stale: #{message}" +
|
|
107
|
+
"Coverage data stale: #{message}" + formatter.format_single_file_details(
|
|
108
|
+
file_path: @file_path,
|
|
109
|
+
file_mtime: @file_mtime,
|
|
110
|
+
src_len: @src_len,
|
|
111
|
+
cov_len: @cov_len
|
|
112
|
+
)
|
|
109
113
|
end
|
|
110
114
|
|
|
111
115
|
private def default_message
|
|
112
116
|
fp = file_path || 'file'
|
|
113
117
|
"Coverage data appears stale for #{fp}"
|
|
114
118
|
end
|
|
115
|
-
|
|
116
|
-
private def build_details
|
|
117
|
-
file_utc, file_local = format_time_both(@file_mtime)
|
|
118
|
-
cov_utc, cov_local = format_epoch_both(@cov_timestamp)
|
|
119
|
-
delta_str = format_delta_seconds(@file_mtime, @cov_timestamp)
|
|
120
|
-
|
|
121
|
-
details = <<~DETAILS
|
|
122
|
-
|
|
123
|
-
File - time: #{file_utc || 'not found'} (local #{file_local || 'n/a'}), lines: #{@src_len}
|
|
124
|
-
Coverage - time: #{cov_utc || 'not found'} (local #{cov_local || 'n/a'}), lines: #{@cov_len}
|
|
125
|
-
DETAILS
|
|
126
|
-
|
|
127
|
-
details += "\nDelta - file is #{delta_str} newer than coverage" if delta_str
|
|
128
|
-
details += "\nResultset - #{@resultset_path}" if @resultset_path
|
|
129
|
-
details.chomp
|
|
130
|
-
end
|
|
131
119
|
end
|
|
132
120
|
|
|
133
121
|
# Project-level stale coverage (global) — coverage timestamp older than
|
|
134
122
|
# one or more source files, or new tracked files missing from coverage.
|
|
135
123
|
class CoverageDataProjectStaleError < CoverageDataError
|
|
136
|
-
|
|
124
|
+
include StalenessFormatterMixin
|
|
125
|
+
|
|
126
|
+
attr_reader :cov_timestamp, :newer_files, :missing_files, :deleted_files,
|
|
127
|
+
:length_mismatch_files, :unreadable_files, :resultset_path
|
|
137
128
|
|
|
138
129
|
def initialize(message = nil, original_error = nil, cov_timestamp: nil, newer_files: [],
|
|
139
|
-
missing_files: [], deleted_files: [],
|
|
130
|
+
missing_files: [], deleted_files: [], length_mismatch_files: [], unreadable_files: [],
|
|
131
|
+
resultset_path: nil)
|
|
140
132
|
super(message, original_error)
|
|
141
133
|
@cov_timestamp = cov_timestamp
|
|
142
134
|
@newer_files = Array(newer_files)
|
|
143
135
|
@missing_files = Array(missing_files)
|
|
144
136
|
@deleted_files = Array(deleted_files)
|
|
137
|
+
@length_mismatch_files = Array(length_mismatch_files)
|
|
138
|
+
@unreadable_files = Array(unreadable_files)
|
|
145
139
|
@resultset_path = resultset_path
|
|
146
140
|
end
|
|
147
141
|
|
|
148
142
|
def user_friendly_message
|
|
149
143
|
base = "Coverage data stale (project): #{message || default_message}"
|
|
150
|
-
base +
|
|
144
|
+
base + formatter.format_project_details(
|
|
145
|
+
newer_files: @newer_files,
|
|
146
|
+
missing_files: @missing_files,
|
|
147
|
+
deleted_files: @deleted_files,
|
|
148
|
+
length_mismatch_files: @length_mismatch_files,
|
|
149
|
+
unreadable_files: @unreadable_files
|
|
150
|
+
)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
153
|
private def default_message
|
|
154
154
|
'Coverage data appears stale for project'
|
|
155
155
|
end
|
|
156
|
-
|
|
157
|
-
private def build_details
|
|
158
|
-
cov_utc, cov_local = format_epoch_both(@cov_timestamp)
|
|
159
|
-
parts = []
|
|
160
|
-
parts << "\nCoverage - time: #{cov_utc || 'not found'} (local #{cov_local || 'n/a'})"
|
|
161
|
-
unless @newer_files.empty?
|
|
162
|
-
parts << "\nNewer files (#{@newer_files.size}):"
|
|
163
|
-
parts.concat(@newer_files.first(10).map { |f| " - #{f}" })
|
|
164
|
-
parts << ' ...' if @newer_files.size > 10
|
|
165
|
-
end
|
|
166
|
-
unless @missing_files.empty?
|
|
167
|
-
parts << "\nMissing files (new in project, not in coverage, #{@missing_files.size}):"
|
|
168
|
-
parts.concat(@missing_files.first(10).map { |f| " - #{f}" })
|
|
169
|
-
parts << ' ...' if @missing_files.size > 10
|
|
170
|
-
end
|
|
171
|
-
unless @deleted_files.empty?
|
|
172
|
-
parts << "\nCoverage-only files (deleted or moved in project, #{@deleted_files.size}):"
|
|
173
|
-
parts.concat(@deleted_files.first(10).map { |f| " - #{f}" })
|
|
174
|
-
parts << ' ...' if @deleted_files.size > 10
|
|
175
|
-
end
|
|
176
|
-
parts << "\nResultset - #{@resultset_path}" if @resultset_path
|
|
177
|
-
parts.join
|
|
178
|
-
end
|
|
179
156
|
end
|
|
180
157
|
|
|
181
158
|
# Command line usage errors
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative '../output_chars'
|
|
5
|
+
|
|
6
|
+
module CovLoupe
|
|
7
|
+
module Formatters
|
|
8
|
+
# Maps format symbols to their required libraries
|
|
9
|
+
# Only loaded when the format is actually used
|
|
10
|
+
FORMAT_REQUIRES = {
|
|
11
|
+
yaml: 'yaml',
|
|
12
|
+
amazing_print: 'amazing_print'
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# Ensures required libraries are loaded for the given format
|
|
16
|
+
def self.ensure_requirements_for(format)
|
|
17
|
+
requirement = FORMAT_REQUIRES[format]
|
|
18
|
+
require requirement if requirement
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Formats an object using the specified format.
|
|
22
|
+
#
|
|
23
|
+
# @param obj [Object] The object to format
|
|
24
|
+
# @param format [Symbol] Format type (:table, :json, :pretty_json, :yaml, :amazing_print)
|
|
25
|
+
# @param output_chars [Symbol] Output character mode (:default, :fancy, :ascii)
|
|
26
|
+
# @return [String] Formatted output
|
|
27
|
+
def self.format(obj, format, output_chars: :default)
|
|
28
|
+
ensure_requirements_for(format)
|
|
29
|
+
ascii_mode = OutputChars.ascii_mode?(output_chars)
|
|
30
|
+
|
|
31
|
+
case format
|
|
32
|
+
when :table
|
|
33
|
+
# Pass through - table formatting handled elsewhere with its own output_chars
|
|
34
|
+
obj
|
|
35
|
+
when :json
|
|
36
|
+
ascii_mode ? JSON.generate(obj, ascii_only: true) : obj.to_json
|
|
37
|
+
when :pretty_json
|
|
38
|
+
ascii_mode ? JSON.pretty_generate(obj, ascii_only: true) : JSON.pretty_generate(obj)
|
|
39
|
+
when :yaml
|
|
40
|
+
format_yaml(obj, ascii_mode: ascii_mode)
|
|
41
|
+
when :amazing_print
|
|
42
|
+
require 'amazing_print'
|
|
43
|
+
result = obj.ai
|
|
44
|
+
# AmazingPrint doesn't have native ASCII mode; convert if needed
|
|
45
|
+
ascii_mode ? OutputChars.convert(result, :ascii) : result
|
|
46
|
+
else
|
|
47
|
+
raise ArgumentError, "Unknown format: #{format}"
|
|
48
|
+
end
|
|
49
|
+
rescue LoadError => e
|
|
50
|
+
gem_name = e.message[/-- (\S+)/, 1] || 'required gem'
|
|
51
|
+
raise LoadError, "The #{format} format requires the '#{gem_name}' gem. " \
|
|
52
|
+
"Install it with: gem install #{gem_name}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Formats an object as YAML, with optional ASCII-only output.
|
|
56
|
+
#
|
|
57
|
+
# YAML doesn't have a native ASCII-only mode, so for ASCII mode we:
|
|
58
|
+
# 1. Generate standard YAML
|
|
59
|
+
# 2. Convert any non-ASCII characters using OutputChars.convert
|
|
60
|
+
#
|
|
61
|
+
# This approach preserves YAML structure while ensuring ASCII-only output.
|
|
62
|
+
# Note: This may affect string values containing Unicode, but YAML structure
|
|
63
|
+
# (which is ASCII) remains valid.
|
|
64
|
+
#
|
|
65
|
+
# @param obj [Object] The object to format
|
|
66
|
+
# @param ascii_mode [Boolean] If true, ensure ASCII-only output
|
|
67
|
+
# @return [String] YAML-formatted output
|
|
68
|
+
def self.format_yaml(obj, ascii_mode: false)
|
|
69
|
+
require 'yaml'
|
|
70
|
+
yaml = obj.to_yaml
|
|
71
|
+
ascii_mode ? OutputChars.convert(yaml, :ascii) : yaml
|
|
72
|
+
end
|
|
73
|
+
private_class_method :format_yaml
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../output_chars'
|
|
4
|
+
|
|
3
5
|
module CovLoupe
|
|
4
6
|
module Formatters
|
|
5
7
|
class SourceFormatter
|
|
6
|
-
def initialize(color_enabled: true)
|
|
8
|
+
def initialize(color_enabled: true, output_chars: :default)
|
|
7
9
|
@color_enabled = color_enabled
|
|
10
|
+
@output_chars = output_chars
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def format_source_for(model, path, mode: nil, context: 2)
|
|
@@ -21,8 +24,9 @@ module CovLoupe
|
|
|
21
24
|
format_source_rows(rows)
|
|
22
25
|
rescue ArgumentError
|
|
23
26
|
raise
|
|
24
|
-
rescue
|
|
27
|
+
rescue => e
|
|
25
28
|
# If any unexpected formatting/indexing error occurs, avoid crashing the CLI
|
|
29
|
+
CovLoupe.logger.safe_log("SourceFormatter#format_source_for error for path '#{abs}': #{e.class} - #{e.message}")
|
|
26
30
|
'[source not available]'
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -59,10 +63,14 @@ module CovLoupe
|
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
def format_source_rows(rows)
|
|
66
|
+
# Use ASCII-safe markers when output_chars is :ascii
|
|
67
|
+
check_mark = OutputChars.ascii_mode?(@output_chars) ? '+' : "\u2713" # ✓
|
|
68
|
+
miss_mark = OutputChars.ascii_mode?(@output_chars) ? '-' : "\u00B7" # ·
|
|
69
|
+
|
|
62
70
|
marker = ->(covered, _hits) do
|
|
63
71
|
case covered
|
|
64
|
-
when true then colorize(
|
|
65
|
-
when false then colorize(
|
|
72
|
+
when true then colorize(check_mark, :green)
|
|
73
|
+
when false then colorize(miss_mark, :red)
|
|
66
74
|
else colorize(' ', :dim)
|
|
67
75
|
end
|
|
68
76
|
end
|
|
@@ -73,7 +81,9 @@ module CovLoupe
|
|
|
73
81
|
|
|
74
82
|
rows.each do |r|
|
|
75
83
|
m = marker.call(r['covered'], r['hits'])
|
|
76
|
-
|
|
84
|
+
# Convert source code to ASCII when in ASCII mode
|
|
85
|
+
code = OutputChars.convert(r['code'], @output_chars)
|
|
86
|
+
lines << format('%6d %2s | %s', r['line'], m, code)
|
|
77
87
|
end
|
|
78
88
|
lines.join("\n")
|
|
79
89
|
end
|
|
@@ -89,7 +99,7 @@ module CovLoupe
|
|
|
89
99
|
out.join("\n")
|
|
90
100
|
end
|
|
91
101
|
|
|
92
|
-
attr_reader :color_enabled
|
|
102
|
+
attr_reader :color_enabled, :output_chars
|
|
93
103
|
|
|
94
104
|
private def fetch_raw(model, path)
|
|
95
105
|
@raw_cache ||= {}
|
|
@@ -97,7 +107,8 @@ module CovLoupe
|
|
|
97
107
|
|
|
98
108
|
raw = model.raw_for(path)
|
|
99
109
|
@raw_cache[path] = raw
|
|
100
|
-
rescue
|
|
110
|
+
rescue => e
|
|
111
|
+
CovLoupe.logger.safe_log("SourceFormatter#fetch_raw error for path '#{path}': #{e.class} - #{e.message}")
|
|
101
112
|
nil
|
|
102
113
|
end
|
|
103
114
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../output_chars'
|
|
4
|
+
|
|
5
|
+
module CovLoupe
|
|
6
|
+
# General-purpose table formatter with box-drawing or ASCII characters
|
|
7
|
+
# Used by commands to create consistent formatted output
|
|
8
|
+
class TableFormatter
|
|
9
|
+
# Format data as a table with box-drawing or ASCII characters.
|
|
10
|
+
# @param headers [Array<String>] Column headers
|
|
11
|
+
# @param rows [Array<Array>] Data rows (each row is an array of cell values)
|
|
12
|
+
# @param alignments [Array<Symbol>] Column alignments (:left, :right, :center)
|
|
13
|
+
# @param output_chars [Symbol] Output character mode (:default, :fancy, or :ascii)
|
|
14
|
+
# @return [String] Formatted table
|
|
15
|
+
def self.format(headers:, rows:, alignments: nil, output_chars: :default)
|
|
16
|
+
return 'No data to display' if rows.empty?
|
|
17
|
+
|
|
18
|
+
# Resolve mode and get appropriate charset
|
|
19
|
+
resolved_mode = OutputChars.resolve_mode(output_chars)
|
|
20
|
+
charset = OutputChars.charset_for(resolved_mode)
|
|
21
|
+
|
|
22
|
+
# Convert cell contents to ASCII if needed
|
|
23
|
+
convert = ->(text) { OutputChars.convert(text.to_s, resolved_mode) }
|
|
24
|
+
|
|
25
|
+
alignments ||= [:left] * headers.size
|
|
26
|
+
converted_headers = headers.map(&convert)
|
|
27
|
+
converted_rows = rows.map { |row| row.map(&convert) }
|
|
28
|
+
all_rows = [converted_headers] + converted_rows
|
|
29
|
+
|
|
30
|
+
# Calculate column widths
|
|
31
|
+
widths = headers.size.times.map do |col|
|
|
32
|
+
all_rows.map { |row| row[col].to_s.length }.max
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
lines = []
|
|
36
|
+
lines << border_line(widths, charset[:top_left], charset[:top_tee], charset[:top_right], charset)
|
|
37
|
+
lines << data_row(converted_headers, widths, alignments, charset)
|
|
38
|
+
lines << border_line(widths, charset[:left_tee], charset[:cross], charset[:right_tee], charset)
|
|
39
|
+
converted_rows.each { |row| lines << data_row(row, widths, alignments, charset) }
|
|
40
|
+
lines << border_line(widths, charset[:bottom_left], charset[:bottom_tee], charset[:bottom_right],
|
|
41
|
+
charset)
|
|
42
|
+
|
|
43
|
+
lines.join("\n")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Format a single key-value table (vertical layout)
|
|
47
|
+
# @param data [Hash] Key-value pairs
|
|
48
|
+
# @param output_chars [Symbol] Output character mode (:default, :fancy, or :ascii)
|
|
49
|
+
# @return [String] Formatted table
|
|
50
|
+
def self.format_vertical(data, output_chars: :default)
|
|
51
|
+
rows = data.map { |k, v| [k.to_s, v.to_s] }
|
|
52
|
+
format(headers: ['Key', 'Value'], rows: rows, alignments: [:left, :left], output_chars: output_chars)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private_class_method def self.border_line(widths, left, mid, right, charset)
|
|
56
|
+
h = charset[:horizontal]
|
|
57
|
+
segments = widths.map { |w| h * (w + 2) }
|
|
58
|
+
left + segments.join(mid) + right
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private_class_method def self.data_row(cells, widths, alignments, charset)
|
|
62
|
+
v = charset[:vertical]
|
|
63
|
+
formatted = cells.each_with_index.map do |cell, i|
|
|
64
|
+
align_cell(cell.to_s, widths[i], alignments[i])
|
|
65
|
+
end
|
|
66
|
+
"#{v} #{formatted.join(" #{v} ")} #{v}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private_class_method def self.align_cell(content, width, alignment)
|
|
70
|
+
case alignment
|
|
71
|
+
when :right
|
|
72
|
+
content.rjust(width)
|
|
73
|
+
when :center
|
|
74
|
+
content.center(width)
|
|
75
|
+
else # :left
|
|
76
|
+
content.ljust(width)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Load all CovLoupe components including CLI, MCP server, and all tools.
|
|
4
|
+
# This file is used by the test suite (spec/spec_helper.rb) to ensure all
|
|
5
|
+
# components are loaded for testing.
|
|
6
|
+
#
|
|
7
|
+
# For selective loading at runtime:
|
|
8
|
+
# - Use all_cli.rb for CLI mode (loads optparse + CLI)
|
|
9
|
+
# - Use all_mcp.rb for MCP mode (loads MCP gem + tools)
|
|
10
|
+
#
|
|
11
|
+
# Library users should use `require 'cov_loupe'` instead, which loads only the core
|
|
12
|
+
# components (CoverageModel, errors, utilities) without the CLI/MCP overhead.
|
|
13
|
+
|
|
14
|
+
require_relative 'all_cli'
|
|
15
|
+
require_relative 'all_mcp'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Load CLI-specific components.
|
|
4
|
+
# Used when CovLoupe.run detects CLI mode.
|
|
5
|
+
|
|
6
|
+
require_relative '../../cov_loupe' # Core library components (lib/cov_loupe.rb)
|
|
7
|
+
|
|
8
|
+
# CLI dependencies
|
|
9
|
+
require 'optparse'
|
|
10
|
+
require_relative '../cli'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Load MCP server-specific components including all tools.
|
|
4
|
+
# Used when CovLoupe.run detects MCP mode.
|
|
5
|
+
|
|
6
|
+
require_relative '../../cov_loupe' # Core library components (lib/cov_loupe.rb)
|
|
7
|
+
|
|
8
|
+
# MCP server dependencies
|
|
9
|
+
require 'mcp'
|
|
10
|
+
require 'mcp/server/transports/stdio_transport'
|
|
11
|
+
require_relative '../config/config_parser'
|
|
12
|
+
require_relative '../base_tool'
|
|
13
|
+
require_relative '../tools/coverage_raw_tool'
|
|
14
|
+
require_relative '../tools/coverage_summary_tool'
|
|
15
|
+
require_relative '../tools/uncovered_lines_tool'
|
|
16
|
+
require_relative '../tools/coverage_detailed_tool'
|
|
17
|
+
require_relative '../tools/list_tool'
|
|
18
|
+
require_relative '../tools/coverage_totals_tool'
|
|
19
|
+
require_relative '../tools/coverage_table_tool'
|
|
20
|
+
require_relative '../tools/validate_tool'
|
|
21
|
+
require_relative '../tools/version_tool'
|
|
22
|
+
require_relative '../tools/help_tool'
|
|
23
|
+
require_relative '../mcp_server'
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
require_relative '../errors/errors'
|
|
7
|
+
|
|
8
|
+
module CovLoupe
|
|
9
|
+
class ResultsetLoader
|
|
10
|
+
Result = Struct.new(:coverage_map, :timestamp, :suite_names, keyword_init: true)
|
|
11
|
+
SuiteEntry = Struct.new(:name, :coverage, :timestamp, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
def self.load(resultset_path:, logger: nil)
|
|
14
|
+
logger ||= CovLoupe.logger
|
|
15
|
+
new(resultset_path: resultset_path, logger: logger).load
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(resultset_path:, logger:)
|
|
19
|
+
@resultset_path = resultset_path
|
|
20
|
+
@logger = logger
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def load
|
|
24
|
+
raw = JSON.parse(File.read(@resultset_path))
|
|
25
|
+
|
|
26
|
+
suites = extract_suite_entries(raw)
|
|
27
|
+
if suites.empty?
|
|
28
|
+
raise CoverageDataError, "No test suite with coverage data found in resultset file: #{@resultset_path}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
coverage_map = build_coverage_map(suites)
|
|
32
|
+
Result.new(
|
|
33
|
+
coverage_map: coverage_map,
|
|
34
|
+
timestamp: compute_combined_timestamp(suites),
|
|
35
|
+
suite_names: suites.map(&:name)
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private def extract_suite_entries(raw)
|
|
40
|
+
raw
|
|
41
|
+
.select { |_, data| data.is_a?(Hash) && data.key?('coverage') && !data['coverage'].nil? }
|
|
42
|
+
.map do |name, data|
|
|
43
|
+
SuiteEntry.new(
|
|
44
|
+
name: name.to_s,
|
|
45
|
+
coverage: normalize_suite_coverage(data['coverage'], suite_name: name),
|
|
46
|
+
timestamp: normalize_coverage_timestamp(data['timestamp'], data['created_at'])
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private def build_coverage_map(suites)
|
|
52
|
+
return suites.first&.coverage if suites.length == 1
|
|
53
|
+
|
|
54
|
+
merge_suite_coverages(suites)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private def normalize_suite_coverage(coverage, suite_name:)
|
|
58
|
+
unless coverage.is_a?(Hash)
|
|
59
|
+
raise CoverageDataError, "Invalid coverage data structure for suite #{suite_name.inspect} in resultset file: #{@resultset_path}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
needs_adaptation = coverage.values.any? { |value| value.is_a?(Array) }
|
|
63
|
+
return coverage unless needs_adaptation
|
|
64
|
+
|
|
65
|
+
coverage.transform_values do |value|
|
|
66
|
+
value.is_a?(Array) ? { 'lines' => value } : value
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private def merge_suite_coverages(suites)
|
|
71
|
+
require_simplecov_for_merge!
|
|
72
|
+
log_duplicate_suite_names(suites)
|
|
73
|
+
|
|
74
|
+
suites.reduce(nil) do |memo, suite|
|
|
75
|
+
coverage = suite.coverage
|
|
76
|
+
memo ?
|
|
77
|
+
SimpleCov::Combine.combine(SimpleCov::Combine::ResultsCombiner, memo, coverage) :
|
|
78
|
+
coverage
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private def require_simplecov_for_merge!
|
|
83
|
+
require 'simplecov'
|
|
84
|
+
rescue LoadError
|
|
85
|
+
raise CoverageDataError, "Multiple coverage suites detected in #{@resultset_path}, but the simplecov gem could not be loaded. Install simplecov to enable suite merging."
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private def log_duplicate_suite_names(suites)
|
|
89
|
+
grouped = suites.group_by(&:name)
|
|
90
|
+
duplicates = grouped.select { |_, entries| entries.length > 1 }.keys
|
|
91
|
+
return if duplicates.empty?
|
|
92
|
+
|
|
93
|
+
message = "Merging duplicate coverage suites for #{duplicates.join(', ')}"
|
|
94
|
+
@logger.safe_log(message)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private def compute_combined_timestamp(suites)
|
|
98
|
+
suites.map(&:timestamp).compact.max.to_i
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private def normalize_coverage_timestamp(timestamp_value, created_at_value)
|
|
102
|
+
raw = timestamp_value.nil? ? created_at_value : timestamp_value
|
|
103
|
+
return log_missing_timestamp if raw.nil?
|
|
104
|
+
|
|
105
|
+
timestamp = case raw
|
|
106
|
+
when Integer
|
|
107
|
+
raw
|
|
108
|
+
when Float, Time
|
|
109
|
+
raw.to_i
|
|
110
|
+
when String
|
|
111
|
+
str = raw.strip
|
|
112
|
+
if str.match?(/\A-?\d+(\.\d+)?\z/)
|
|
113
|
+
# Matches optional leading "-", digits, and an optional fractional part.
|
|
114
|
+
str.to_f.to_i
|
|
115
|
+
elsif str.empty?
|
|
116
|
+
0
|
|
117
|
+
else
|
|
118
|
+
Time.parse(str).to_i
|
|
119
|
+
end
|
|
120
|
+
else
|
|
121
|
+
log_timestamp_warning(raw)
|
|
122
|
+
return 0
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
timestamp = [timestamp.to_i, 0].max # change negative numbers to zero
|
|
126
|
+
log_missing_timestamp(raw) if timestamp.zero? # but log the original value
|
|
127
|
+
timestamp
|
|
128
|
+
rescue => e
|
|
129
|
+
log_timestamp_warning(raw, e)
|
|
130
|
+
0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private def log_missing_timestamp(raw_value = nil)
|
|
134
|
+
message = 'Coverage timestamp missing, defaulting to 0. ' \
|
|
135
|
+
'Time-based staleness checks will be disabled.'
|
|
136
|
+
message = "#{message} (value: #{raw_value.inspect})" if raw_value
|
|
137
|
+
@logger.safe_log(message)
|
|
138
|
+
0
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private def log_timestamp_warning(raw_value, error = nil)
|
|
142
|
+
message = "Coverage resultset timestamp could not be parsed: #{raw_value.inspect}"
|
|
143
|
+
message = "#{message} (#{error.message})" if error
|
|
144
|
+
@logger.safe_log(message)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
data/lib/cov_loupe/mcp_server.rb
CHANGED
|
@@ -11,14 +11,15 @@ module CovLoupe
|
|
|
11
11
|
server = ::MCP::Server.new(
|
|
12
12
|
name: 'cov-loupe',
|
|
13
13
|
version: CovLoupe::VERSION,
|
|
14
|
-
tools: toolset
|
|
14
|
+
tools: toolset,
|
|
15
|
+
server_context: context
|
|
15
16
|
)
|
|
16
17
|
::MCP::Server::Transports::StdioTransport.new(server).open
|
|
17
18
|
end
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
TOOLSET = [
|
|
21
|
-
Tools::
|
|
22
|
+
Tools::ListTool,
|
|
22
23
|
Tools::CoverageDetailedTool,
|
|
23
24
|
Tools::CoverageRawTool,
|
|
24
25
|
Tools::CoverageSummaryTool,
|