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
data/lib/cov_loupe/base_tool.rb
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require 'mcp'
|
|
4
4
|
require 'json'
|
|
5
|
-
require_relative 'errors'
|
|
6
|
-
require_relative 'error_handler'
|
|
5
|
+
require_relative 'errors/errors'
|
|
6
|
+
require_relative 'errors/error_handler'
|
|
7
|
+
require_relative 'model/model'
|
|
8
|
+
require_relative 'presenters/coverage_payload_presenter'
|
|
9
|
+
require_relative 'output_chars'
|
|
10
|
+
require_relative 'config/option_normalizers'
|
|
7
11
|
|
|
8
12
|
module CovLoupe
|
|
9
13
|
class BaseTool < ::MCP::Tool
|
|
@@ -18,19 +22,26 @@ module CovLoupe
|
|
|
18
22
|
type: 'string',
|
|
19
23
|
description: 'Path to the SimpleCov .resultset.json file (absolute or relative to root).'
|
|
20
24
|
},
|
|
21
|
-
|
|
22
|
-
type: '
|
|
23
|
-
description: '
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
default: :off
|
|
25
|
+
raise_on_stale: {
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
description: 'If true, raise error if coverage data is stale (missing files, ' \
|
|
28
|
+
'timestamp mismatch). Defaults to false.',
|
|
29
|
+
default: false
|
|
27
30
|
},
|
|
28
31
|
error_mode: {
|
|
29
32
|
type: 'string',
|
|
30
33
|
description: "Error handling mode: 'off' (silent), 'log' (log errors), " \
|
|
31
|
-
|
|
34
|
+
"'debug' (verbose with backtraces).",
|
|
32
35
|
enum: %w[off log debug],
|
|
33
36
|
default: 'log'
|
|
37
|
+
},
|
|
38
|
+
output_chars: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: "Output character mode: 'default' (UTF-8 encoding uses fancy, else ascii), " \
|
|
41
|
+
"'fancy' (Unicode box-drawing and symbols), 'ascii' (ASCII-only 0x00-0x7F). " \
|
|
42
|
+
'Accepts: d[efault], f[ancy], a[scii].',
|
|
43
|
+
enum: %w[default fancy ascii d f a],
|
|
44
|
+
default: 'default'
|
|
34
45
|
}
|
|
35
46
|
}.freeze
|
|
36
47
|
|
|
@@ -43,6 +54,17 @@ module CovLoupe
|
|
|
43
54
|
items: { type: 'string' }
|
|
44
55
|
}.freeze
|
|
45
56
|
|
|
57
|
+
DEFAULT_SORT_ORDER = CoverageModel::DEFAULT_SORT_ORDER.to_s
|
|
58
|
+
|
|
59
|
+
SORT_ORDER_PROPERTY = {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Sort order for coverage percentages. ' \
|
|
62
|
+
"'#{DEFAULT_SORT_ORDER}' (default) lists highest coverage first. " \
|
|
63
|
+
'Accepts: a[scending], d[escending].',
|
|
64
|
+
default: DEFAULT_SORT_ORDER,
|
|
65
|
+
enum: %w[ascending descending a d]
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
46
68
|
PATH_PROPERTY = {
|
|
47
69
|
type: 'string',
|
|
48
70
|
description: 'Repo-relative or absolute path to the file whose coverage data you need.',
|
|
@@ -50,12 +72,13 @@ module CovLoupe
|
|
|
50
72
|
}.freeze
|
|
51
73
|
|
|
52
74
|
def self.coverage_schema(additional_properties: {}, required: [])
|
|
53
|
-
{
|
|
75
|
+
schema = {
|
|
54
76
|
type: 'object',
|
|
55
77
|
additionalProperties: false,
|
|
56
|
-
properties: COMMON_PROPERTIES.merge(additional_properties)
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
properties: COMMON_PROPERTIES.merge(additional_properties)
|
|
79
|
+
}
|
|
80
|
+
schema[:required] = required unless required.empty?
|
|
81
|
+
schema.freeze
|
|
59
82
|
end
|
|
60
83
|
|
|
61
84
|
FILE_INPUT_SCHEMA = coverage_schema(
|
|
@@ -67,36 +90,222 @@ module CovLoupe
|
|
|
67
90
|
# Wrap tool execution with consistent error handling.
|
|
68
91
|
# Yields to the block and rescues any error, delegating to handle_mcp_error.
|
|
69
92
|
# This eliminates duplicate rescue blocks across all tools.
|
|
70
|
-
|
|
93
|
+
#
|
|
94
|
+
# @param tool_name [String] Name of the tool for error reporting
|
|
95
|
+
# @param error_mode [Symbol, String] Error handling mode (:off, :log, :debug)
|
|
96
|
+
# @param output_chars [Symbol, String, nil] Output character mode for error messages
|
|
97
|
+
def self.with_error_handling(tool_name, error_mode:, output_chars: :default)
|
|
71
98
|
yield
|
|
72
99
|
rescue => e
|
|
73
|
-
handle_mcp_error(e, tool_name, error_mode: error_mode)
|
|
100
|
+
handle_mcp_error(e, tool_name, error_mode: error_mode, output_chars: output_chars)
|
|
74
101
|
end
|
|
75
102
|
|
|
76
103
|
# Handle errors consistently across all MCP tools
|
|
77
104
|
# Returns an MCP::Tool::Response with appropriate error message
|
|
78
|
-
|
|
105
|
+
#
|
|
106
|
+
# @param error [Exception] The error to handle
|
|
107
|
+
# @param tool_name [String] Name of the tool for error reporting
|
|
108
|
+
# @param error_mode [Symbol, String] Error handling mode
|
|
109
|
+
# @param output_chars [Symbol, String, nil] Output character mode for error messages
|
|
110
|
+
# @return [MCP::Tool::Response] Error response
|
|
111
|
+
def self.handle_mcp_error(error, tool_name, error_mode: :log, output_chars: :default)
|
|
112
|
+
# Safely normalize error_mode to a symbol, defaulting to :log for invalid inputs
|
|
113
|
+
# This prevents crashes when MCP clients send invalid types (null, numbers, objects, etc.)
|
|
114
|
+
safe_mode = case error_mode
|
|
115
|
+
when Symbol then error_mode
|
|
116
|
+
when String then error_mode.to_sym
|
|
117
|
+
else :log
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Validate against VALID_ERROR_MODES and fallback to :log if invalid
|
|
121
|
+
# This prevents ArgumentError when handling errors with invalid error_mode values
|
|
122
|
+
safe_mode = :log unless ErrorHandler::VALID_ERROR_MODES.include?(safe_mode)
|
|
123
|
+
|
|
79
124
|
# Create error handler with the specified mode
|
|
80
|
-
error_handler = ErrorHandlerFactory.for_mcp_server(error_mode:
|
|
125
|
+
error_handler = ErrorHandlerFactory.for_mcp_server(error_mode: safe_mode)
|
|
81
126
|
|
|
82
127
|
# Normalize to a CovLoupe::Error so we can handle/log uniformly
|
|
83
128
|
normalized = error.is_a?(CovLoupe::Error) \
|
|
84
129
|
? error : error_handler.convert_standard_error(error)
|
|
85
130
|
log_mcp_error(normalized, tool_name, error_handler)
|
|
86
|
-
|
|
131
|
+
|
|
132
|
+
# Convert error message to ASCII if needed
|
|
133
|
+
error_message = normalized.user_friendly_message
|
|
134
|
+
error_message = OutputChars.convert(error_message, output_chars || :default)
|
|
135
|
+
::MCP::Tool::Response.new([{ 'type' => 'text', 'text' => "Error: #{error_message}" }])
|
|
87
136
|
end
|
|
88
137
|
|
|
89
138
|
# Respond with JSON as a resource to avoid clients mutating content types.
|
|
90
139
|
# The resource embeds the JSON string with a clear MIME type.
|
|
91
|
-
|
|
92
|
-
|
|
140
|
+
#
|
|
141
|
+
# @param payload [Object] The data to serialize as JSON
|
|
142
|
+
# @param name [String] Logical name for the JSON resource (informational)
|
|
143
|
+
# @param pretty [Boolean] Use pretty formatting with indentation
|
|
144
|
+
# @param output_chars [Symbol, String, nil] Output character mode (:default, :fancy, :ascii)
|
|
145
|
+
# @return [MCP::Tool::Response] Response containing the JSON string
|
|
146
|
+
def self.respond_json(payload, name: 'data.json', pretty: false, output_chars: :default)
|
|
147
|
+
ascii_only = ascii_only?(output_chars)
|
|
148
|
+
json = if pretty
|
|
149
|
+
ascii_only ? JSON.pretty_generate(payload, ascii_only: true) : JSON.pretty_generate(payload)
|
|
150
|
+
else
|
|
151
|
+
ascii_only ? JSON.generate(payload, ascii_only: true) : JSON.generate(payload)
|
|
152
|
+
end
|
|
93
153
|
::MCP::Tool::Response.new([{ 'type' => 'text', 'text' => json }])
|
|
94
154
|
end
|
|
95
155
|
|
|
156
|
+
# Determines if ASCII-only output is required based on the character mode setting.
|
|
157
|
+
# Normalizes string inputs to symbols (MCP JSON provides strings, internal code uses symbols).
|
|
158
|
+
# Uses strict validation to raise errors for invalid values.
|
|
159
|
+
#
|
|
160
|
+
# @param char_mode [Symbol, String, nil] The character mode (:default, :fancy, :ascii)
|
|
161
|
+
# @return [Boolean] true if ASCII-only output is required
|
|
162
|
+
# @raise [CovLoupe::UsageError] if char_mode is invalid
|
|
163
|
+
def self.ascii_only?(char_mode)
|
|
164
|
+
return false if char_mode.nil?
|
|
165
|
+
|
|
166
|
+
normalized_mode_name = normalize_output_chars_strict(char_mode)
|
|
167
|
+
OutputChars.ascii_mode?(normalized_mode_name)
|
|
168
|
+
end
|
|
169
|
+
private_class_method :ascii_only?
|
|
170
|
+
|
|
96
171
|
def self.log_mcp_error(error, tool_name, error_handler)
|
|
97
172
|
# Use the provided error handler for logging
|
|
98
173
|
error_handler.send(:log_error, error, tool_name)
|
|
99
174
|
end
|
|
100
175
|
private_class_method :log_mcp_error
|
|
176
|
+
|
|
177
|
+
# Merges configuration from server context (CLI flags) with explicit tool parameters (JSON).
|
|
178
|
+
# Explicit parameters take precedence over context config, which takes precedence over defaults.
|
|
179
|
+
# @param server_context [AppContext] The server context containing app_config from CLI
|
|
180
|
+
# @param model_option_overrides [Hash] Tool call parameters that override model defaults
|
|
181
|
+
# @return [Hash] Merged configuration for CoverageModel initialization
|
|
182
|
+
def self.model_config_for(server_context:, **model_option_overrides)
|
|
183
|
+
# Start with config from context (CLI flags) or hardcoded defaults
|
|
184
|
+
base = server_context.app_config&.model_options || default_model_options
|
|
185
|
+
|
|
186
|
+
# Merge explicit params from JSON, removing nils
|
|
187
|
+
# (nil means "not provided", so use base config)
|
|
188
|
+
base.merge(model_option_overrides.compact)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Creates and configures a CoverageModel instance.
|
|
192
|
+
# Encapsulates the common pattern of merging config and initializing the model.
|
|
193
|
+
# @param server_context [AppContext] The server context
|
|
194
|
+
# @param model_option_overrides [Hash] Tool call parameters that override model defaults
|
|
195
|
+
# @return [CoverageModel] The configured model
|
|
196
|
+
def self.create_model(server_context:, **model_option_overrides)
|
|
197
|
+
model, _config = create_configured_model(server_context: server_context,
|
|
198
|
+
**model_option_overrides)
|
|
199
|
+
model
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Creates and configures a CoverageModel instance, returning both the model and the configuration.
|
|
203
|
+
# Useful when the tool needs access to the resolved configuration (e.g., root, raise_on_stale).
|
|
204
|
+
#
|
|
205
|
+
# Models are now lightweight (data is loaded lazily via ModelDataCache), so we create
|
|
206
|
+
# a fresh instance on each call rather than caching at the model level.
|
|
207
|
+
#
|
|
208
|
+
# @param server_context [AppContext] The server context
|
|
209
|
+
# @param model_option_overrides [Hash] Tool call parameters that override model defaults
|
|
210
|
+
# @return [Array<CoverageModel, Hash>] The configured model and the configuration hash
|
|
211
|
+
def self.create_configured_model(server_context:, **model_option_overrides)
|
|
212
|
+
config = model_config_for(server_context: server_context, **model_option_overrides)
|
|
213
|
+
model = CoverageModel.new(**config)
|
|
214
|
+
[model, config]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Default configuration when no context or explicit params are provided
|
|
218
|
+
def self.default_model_options
|
|
219
|
+
{ root: '.', resultset: nil, raise_on_stale: false, tracked_globs: [] }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Resolves output_chars from tool parameter or server context.
|
|
223
|
+
# Tool parameter takes precedence over server context config.
|
|
224
|
+
# Uses strict validation for tool parameters to catch invalid values.
|
|
225
|
+
#
|
|
226
|
+
# @param output_chars [String, Symbol, nil] Tool parameter value
|
|
227
|
+
# @param server_context [AppContext] Server context with app_config
|
|
228
|
+
# @return [Symbol] Normalized output_chars mode (:default, :fancy, or :ascii)
|
|
229
|
+
# @raise [CovLoupe::UsageError] if output_chars parameter is invalid
|
|
230
|
+
def self.resolve_output_chars(output_chars, server_context)
|
|
231
|
+
# Use explicit parameter if provided
|
|
232
|
+
return normalize_output_chars_strict(output_chars) if output_chars
|
|
233
|
+
|
|
234
|
+
# Fall back to server context config
|
|
235
|
+
server_context.app_config&.output_chars || :default
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Normalizes output_chars value with strict validation.
|
|
239
|
+
# Converts string inputs to symbols and validates against allowed values.
|
|
240
|
+
#
|
|
241
|
+
# @param value [String, Symbol, nil] The output_chars value to normalize
|
|
242
|
+
# @return [Symbol] Normalized output_chars mode (:default, :fancy, or :ascii)
|
|
243
|
+
# @raise [CovLoupe::UsageError] if value is invalid
|
|
244
|
+
def self.normalize_output_chars_strict(value)
|
|
245
|
+
case value
|
|
246
|
+
when Symbol then value
|
|
247
|
+
when String
|
|
248
|
+
begin
|
|
249
|
+
OptionNormalizers.normalize_output_chars(value, strict: true)
|
|
250
|
+
rescue OptionParser::InvalidArgument
|
|
251
|
+
raise CovLoupe::UsageError, "Invalid output_chars value: #{value.inspect}. " \
|
|
252
|
+
'Must be one of: default, fancy, ascii (or abbreviations: d, f, a)'
|
|
253
|
+
end
|
|
254
|
+
else
|
|
255
|
+
raise CovLoupe::UsageError, "Invalid output_chars type: #{value.class.name}. " \
|
|
256
|
+
'Must be a string (one of: default, fancy, ascii, or abbreviations: d, f, a)'
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
private_class_method :normalize_output_chars_strict
|
|
260
|
+
|
|
261
|
+
# Runs a file-based tool request by deriving payload method and JSON name from the tool class.
|
|
262
|
+
# @param path [String] File path to analyze
|
|
263
|
+
# @param error_mode [String] Error handling mode
|
|
264
|
+
# @param output_chars [String, Symbol, nil] Output character mode
|
|
265
|
+
# @param server_context [AppContext] Server context
|
|
266
|
+
# @param model_option_overrides [Hash] Tool call parameters that override model defaults
|
|
267
|
+
# @return [MCP::Tool::Response] JSON response
|
|
268
|
+
def self.call_with_file_payload(path:, error_mode:, server_context:, output_chars: nil,
|
|
269
|
+
**model_option_overrides)
|
|
270
|
+
tool_name = name.split('::').last
|
|
271
|
+
output_chars_sym = resolve_output_chars(output_chars, server_context)
|
|
272
|
+
|
|
273
|
+
with_error_handling(tool_name, error_mode: error_mode, output_chars: output_chars_sym) do
|
|
274
|
+
model, config = create_configured_model(server_context: server_context,
|
|
275
|
+
**model_option_overrides)
|
|
276
|
+
presenter = Presenters::CoveragePayloadPresenter.new(
|
|
277
|
+
model: model,
|
|
278
|
+
path: path,
|
|
279
|
+
payload_method: payload_method_for(tool_name),
|
|
280
|
+
raise_on_stale: config[:raise_on_stale]
|
|
281
|
+
)
|
|
282
|
+
respond_json(presenter.relativized_payload, name: json_name_for(tool_name), pretty: true,
|
|
283
|
+
output_chars: output_chars_sym)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Infer CoverageModel method name from a tool class name.
|
|
288
|
+
# CoverageSummaryTool -> :summary_for, CoverageRawTool -> :raw_for,
|
|
289
|
+
# CoverageDetailedTool -> :detailed_for, UncoveredLinesTool -> :uncovered_for.
|
|
290
|
+
def self.payload_method_for(tool_name)
|
|
291
|
+
base = tool_name.sub(/Tool\z/, '')
|
|
292
|
+
underscored = underscore(base).sub(/\Acoverage_/, '').sub(/_lines\z/, '')
|
|
293
|
+
:"#{underscored}_for"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Infer the MCP JSON resource name from a tool class name.
|
|
297
|
+
# CoverageSummaryTool -> coverage_summary.json, UncoveredLinesTool -> uncovered_lines.json.
|
|
298
|
+
def self.json_name_for(tool_name)
|
|
299
|
+
"#{underscore(tool_name.sub(/Tool\z/, ''))}.json"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Minimal underscore helper to avoid pulling in ActiveSupport.
|
|
303
|
+
def self.underscore(value)
|
|
304
|
+
value
|
|
305
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\\1_\\2')
|
|
306
|
+
.gsub(/([a-z\\d])([A-Z])/, '\\1_\\2')
|
|
307
|
+
.downcase
|
|
308
|
+
end
|
|
309
|
+
private_class_method :payload_method_for, :json_name_for, :underscore
|
|
101
310
|
end
|
|
102
311
|
end
|
data/lib/cov_loupe/cli.rb
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require_relative 'app_config'
|
|
5
|
-
require_relative 'option_parser_builder'
|
|
4
|
+
require_relative 'config/app_config'
|
|
5
|
+
require_relative 'config/option_parser_builder'
|
|
6
6
|
require_relative 'commands/command_factory'
|
|
7
7
|
require_relative 'option_parsers/error_helper'
|
|
8
8
|
require_relative 'option_parsers/env_options_parser'
|
|
9
|
-
require_relative 'constants'
|
|
10
9
|
require_relative 'presenters/project_coverage_presenter'
|
|
10
|
+
require_relative 'output_chars'
|
|
11
11
|
|
|
12
12
|
module CovLoupe
|
|
13
13
|
class CoverageCLI
|
|
14
|
-
SUBCOMMANDS = %w[list summary raw uncovered detailed totals validate version].freeze
|
|
15
14
|
HORIZONTAL_RULE = '-' * 79
|
|
16
15
|
|
|
17
|
-
#
|
|
18
|
-
|
|
16
|
+
# Valid CLI subcommands.
|
|
17
|
+
SUBCOMMANDS = %w[list summary raw uncovered detailed totals validate version].freeze
|
|
19
18
|
|
|
20
19
|
attr_reader :config
|
|
21
20
|
|
|
@@ -44,6 +43,7 @@ module CovLoupe
|
|
|
44
43
|
)
|
|
45
44
|
|
|
46
45
|
CovLoupe.with_context(context) do
|
|
46
|
+
log_cli_params
|
|
47
47
|
if @cmd
|
|
48
48
|
run_subcommand(@cmd, @cmd_args)
|
|
49
49
|
else
|
|
@@ -58,28 +58,49 @@ module CovLoupe
|
|
|
58
58
|
with_context_if_available(context) { handle_user_facing_error(e) }
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
private def log_cli_params
|
|
62
|
+
# Log CLI parameters for transparency
|
|
63
|
+
if CovLoupe.logger
|
|
64
|
+
params = { mode: :cli, subcommand: @cmd || 'default' }
|
|
65
|
+
params[:root] = config.root if config.root
|
|
66
|
+
params[:resultset] = config.resultset if config.resultset
|
|
67
|
+
params[:format] = config.format if config.format
|
|
68
|
+
params[:sort_order] = config.sort_order if config.sort_order
|
|
69
|
+
params[:raise_on_stale] = config.raise_on_stale if config.raise_on_stale
|
|
70
|
+
params[:tracked_globs] = config.tracked_globs if config.tracked_globs&.any?
|
|
71
|
+
params[:error_mode] = config.error_mode if config.error_mode
|
|
72
|
+
CovLoupe.logger.info("CLI parameters: #{params.inspect}")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
61
76
|
def show_default_report(sort_order: :descending, output: $stdout)
|
|
62
77
|
model = CoverageModel.new(**config.model_options)
|
|
63
78
|
presenter = Presenters::ProjectCoveragePresenter.new(
|
|
64
79
|
model: model,
|
|
65
80
|
sort_order: sort_order,
|
|
66
|
-
|
|
81
|
+
raise_on_stale: config.raise_on_stale,
|
|
67
82
|
tracked_globs: config.tracked_globs
|
|
68
83
|
)
|
|
69
84
|
|
|
70
|
-
if config.format
|
|
71
|
-
|
|
72
|
-
output.puts
|
|
73
|
-
|
|
85
|
+
if config.format == :table
|
|
86
|
+
file_summaries = presenter.relative_files
|
|
87
|
+
output.puts model.format_table(
|
|
88
|
+
file_summaries,
|
|
89
|
+
sort_order: sort_order,
|
|
90
|
+
raise_on_stale: config.raise_on_stale,
|
|
91
|
+
tracked_globs: nil,
|
|
92
|
+
output_chars: config.output_chars
|
|
93
|
+
)
|
|
94
|
+
show_exclusions_summary(presenter, $stderr)
|
|
95
|
+
warn_missing_timestamps(presenter, $stderr)
|
|
96
|
+
else
|
|
97
|
+
require_relative 'formatters/formatters'
|
|
98
|
+
output.puts Formatters.format(presenter.relativized_payload, config.format,
|
|
99
|
+
output_chars: config.output_chars)
|
|
74
100
|
end
|
|
75
101
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
file_summaries,
|
|
79
|
-
sort_order: sort_order,
|
|
80
|
-
check_stale: (config.staleness == :error),
|
|
81
|
-
tracked_globs: nil
|
|
82
|
-
)
|
|
102
|
+
warn_skipped_rows(presenter)
|
|
103
|
+
warn_missing_timestamps(presenter)
|
|
83
104
|
end
|
|
84
105
|
|
|
85
106
|
private def parse_options!(argv)
|
|
@@ -149,16 +170,21 @@ module CovLoupe
|
|
|
149
170
|
|
|
150
171
|
private def handle_option_parser_error(error, argv: [])
|
|
151
172
|
@error_helper ||= OptionParsers::ErrorHelper.new(SUBCOMMANDS)
|
|
152
|
-
@error_helper.handle_option_parser_error(error, argv: argv)
|
|
173
|
+
@error_helper.handle_option_parser_error(error, argv: argv, output_chars: config.output_chars)
|
|
153
174
|
end
|
|
154
175
|
|
|
155
176
|
private def check_for_misplaced_global_options(cmd, args)
|
|
156
177
|
# Global options that users commonly place after subcommands by mistake
|
|
157
178
|
global_options = %w[-r --resultset -R --root -f --format -o --sort-order -s --source
|
|
158
|
-
-c --context-lines -S --
|
|
159
|
-
-l --log-file --error-mode --color --
|
|
179
|
+
-c --context-lines -S --raise-on-stale -g --tracked-globs
|
|
180
|
+
-l --log-file --error-mode --color -m --mode -v --version
|
|
181
|
+
-O --output-chars]
|
|
160
182
|
|
|
161
|
-
misplaced = args.select
|
|
183
|
+
misplaced = args.select do |arg|
|
|
184
|
+
# Extract base option (e.g., --format from --format=json)
|
|
185
|
+
base = arg.split('=', 2).first
|
|
186
|
+
global_options.include?(base)
|
|
187
|
+
end
|
|
162
188
|
return if misplaced.empty?
|
|
163
189
|
|
|
164
190
|
option_list = misplaced.join(', ')
|
|
@@ -170,9 +196,92 @@ module CovLoupe
|
|
|
170
196
|
|
|
171
197
|
private def handle_user_facing_error(error)
|
|
172
198
|
error_handler.handle_error(error, context: 'CLI', reraise: false)
|
|
173
|
-
|
|
199
|
+
# Convert error message to ASCII if in ascii mode
|
|
200
|
+
message = OutputChars.convert(error.user_friendly_message, config.output_chars)
|
|
201
|
+
warn message
|
|
174
202
|
warn error.backtrace.first(5).join("\n") if config.error_mode == :debug && error.backtrace
|
|
175
203
|
exit 1
|
|
176
204
|
end
|
|
205
|
+
|
|
206
|
+
private def warn_skipped_rows(presenter)
|
|
207
|
+
skipped = presenter.relative_skipped_files
|
|
208
|
+
return if skipped.nil? || skipped.empty?
|
|
209
|
+
|
|
210
|
+
count = skipped.length
|
|
211
|
+
warn ''
|
|
212
|
+
warn "WARNING: #{count} coverage row#{count == 1 ? '' : 's'} skipped due to errors:"
|
|
213
|
+
skipped.each do |row|
|
|
214
|
+
# Paths are already relativized by presenter
|
|
215
|
+
file_path = OutputChars.convert(row['file'], config.output_chars)
|
|
216
|
+
error_msg = OutputChars.convert(row['error'], config.output_chars)
|
|
217
|
+
warn " - #{file_path}: #{error_msg}"
|
|
218
|
+
end
|
|
219
|
+
warn 'Run again with --raise-on-stale to exit when rows are skipped.'
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private def warn_missing_timestamps(presenter, output = $stderr)
|
|
223
|
+
return unless presenter.timestamp_status == 'missing'
|
|
224
|
+
|
|
225
|
+
output.puts <<~WARNING
|
|
226
|
+
|
|
227
|
+
WARNING: Coverage timestamps are missing. Time-based staleness checks were skipped.
|
|
228
|
+
Files may appear "ok" even if source code is newer than the coverage data.
|
|
229
|
+
Check your coverage tool configuration to ensure timestamps are recorded.
|
|
230
|
+
WARNING
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
private def show_exclusions_summary(presenter, output)
|
|
234
|
+
missing = presenter.relative_missing_tracked_files
|
|
235
|
+
newer = presenter.relative_newer_files
|
|
236
|
+
deleted = presenter.relative_deleted_files
|
|
237
|
+
length_mismatch = presenter.relative_length_mismatch_files
|
|
238
|
+
unreadable = presenter.relative_unreadable_files
|
|
239
|
+
skipped = presenter.relative_skipped_files
|
|
240
|
+
|
|
241
|
+
# Only show if there are any exclusions
|
|
242
|
+
return if missing.empty? && newer.empty? && deleted.empty? &&
|
|
243
|
+
length_mismatch.empty? && unreadable.empty? && skipped.empty?
|
|
244
|
+
|
|
245
|
+
output.puts "\nFiles excluded from coverage:"
|
|
246
|
+
|
|
247
|
+
# Helper to convert paths to ASCII if needed
|
|
248
|
+
convert_path = ->(path) { OutputChars.convert(path, config.output_chars) }
|
|
249
|
+
|
|
250
|
+
unless missing.empty?
|
|
251
|
+
output.puts "\nMissing tracked files (#{missing.length}):"
|
|
252
|
+
missing.each { |file| output.puts " - #{convert_path.call(file)}" }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
unless newer.empty?
|
|
256
|
+
output.puts "\nFiles newer than coverage (#{newer.length}):"
|
|
257
|
+
newer.each { |file| output.puts " - #{convert_path.call(file)}" }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
unless deleted.empty?
|
|
261
|
+
output.puts "\nDeleted files with coverage (#{deleted.length}):"
|
|
262
|
+
deleted.each { |file| output.puts " - #{convert_path.call(file)}" }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
unless length_mismatch.empty?
|
|
266
|
+
output.puts "\nLine count mismatches (#{length_mismatch.length}):"
|
|
267
|
+
length_mismatch.each { |file| output.puts " - #{convert_path.call(file)}" }
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
unless unreadable.empty?
|
|
271
|
+
output.puts "\nUnreadable files (#{unreadable.length}):"
|
|
272
|
+
unreadable.each { |file| output.puts " - #{convert_path.call(file)}" }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
unless skipped.empty?
|
|
276
|
+
output.puts "\nFiles skipped due to errors (#{skipped.length}):"
|
|
277
|
+
skipped.each do |row|
|
|
278
|
+
file_path = OutputChars.convert(row['file'], config.output_chars)
|
|
279
|
+
error_msg = OutputChars.convert(row['error'], config.output_chars)
|
|
280
|
+
output.puts " - #{file_path}: #{error_msg}"
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
output.puts "\nRun with --raise-on-stale to exit when files are excluded."
|
|
285
|
+
end
|
|
177
286
|
end
|
|
178
287
|
end
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require_relative '../formatters'
|
|
4
|
+
require_relative '../formatters/formatters'
|
|
5
5
|
require_relative '../formatters/source_formatter'
|
|
6
|
-
require_relative '../model'
|
|
7
|
-
require_relative '../errors'
|
|
6
|
+
require_relative '../model/model'
|
|
7
|
+
require_relative '../errors/errors'
|
|
8
|
+
require_relative '../output_chars'
|
|
8
9
|
|
|
9
10
|
module CovLoupe
|
|
10
11
|
module Commands
|
|
@@ -23,8 +24,14 @@ module CovLoupe
|
|
|
23
24
|
@model ||= CoverageModel.new(**config.model_options)
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
# Convert text to ASCII if output_chars is :ascii
|
|
28
|
+
protected def convert_text(text)
|
|
29
|
+
OutputChars.convert(text, config.output_chars)
|
|
30
|
+
end
|
|
31
|
+
|
|
26
32
|
protected def handle_with_path(args, name)
|
|
27
33
|
path = args.shift or raise UsageError.for_subcommand("#{name} <path>")
|
|
34
|
+
reject_extra_args(args, name)
|
|
28
35
|
yield(path)
|
|
29
36
|
rescue Errno::ENOENT
|
|
30
37
|
raise FileNotFoundError, "File not found: #{path}"
|
|
@@ -32,10 +39,20 @@ module CovLoupe
|
|
|
32
39
|
raise FilePermissionError, "Permission denied: #{path}"
|
|
33
40
|
end
|
|
34
41
|
|
|
42
|
+
# Validates that no unexpected arguments remain after parsing.
|
|
43
|
+
# Raises UsageError if extra args are present.
|
|
44
|
+
protected def reject_extra_args(args, command_name)
|
|
45
|
+
return if args.empty?
|
|
46
|
+
|
|
47
|
+
extra = args.join(' ')
|
|
48
|
+
raise UsageError, "Unexpected argument(s) for '#{command_name}': #{extra}"
|
|
49
|
+
end
|
|
50
|
+
|
|
35
51
|
protected def maybe_output_structured_format?(obj, model)
|
|
36
52
|
return false if config.format == :table
|
|
37
53
|
|
|
38
|
-
puts CovLoupe::Formatters.format(model.relativize(obj), config.format
|
|
54
|
+
puts CovLoupe::Formatters.format(model.relativize(obj), config.format,
|
|
55
|
+
output_chars: config.output_chars)
|
|
39
56
|
true
|
|
40
57
|
end
|
|
41
58
|
|
|
@@ -45,9 +62,11 @@ module CovLoupe
|
|
|
45
62
|
relativized = model.relativize(data)
|
|
46
63
|
if config.source_mode
|
|
47
64
|
payload = relativized.merge('source' => build_source_payload(model, path))
|
|
48
|
-
puts CovLoupe::Formatters.format(payload, config.format
|
|
65
|
+
puts CovLoupe::Formatters.format(payload, config.format,
|
|
66
|
+
output_chars: config.output_chars)
|
|
49
67
|
else
|
|
50
|
-
puts CovLoupe::Formatters.format(relativized, config.format
|
|
68
|
+
puts CovLoupe::Formatters.format(relativized, config.format,
|
|
69
|
+
output_chars: config.output_chars)
|
|
51
70
|
end
|
|
52
71
|
true
|
|
53
72
|
end
|
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base_command'
|
|
4
4
|
require_relative '../formatters/source_formatter'
|
|
5
|
-
require_relative '../presenters/
|
|
6
|
-
require_relative '../
|
|
5
|
+
require_relative '../presenters/coverage_payload_presenter'
|
|
6
|
+
require_relative '../staleness/stale_status'
|
|
7
7
|
|
|
8
8
|
module CovLoupe
|
|
9
9
|
module Commands
|
|
10
10
|
class DetailedCommand < BaseCommand
|
|
11
11
|
def execute(args)
|
|
12
12
|
handle_with_path(args, 'detailed') do |path|
|
|
13
|
-
presenter = Presenters::
|
|
13
|
+
presenter = Presenters::CoveragePayloadPresenter.new(model: model, path: path,
|
|
14
|
+
payload_method: :detailed_for)
|
|
14
15
|
data = presenter.absolute_payload
|
|
15
16
|
break if emit_structured_format_with_optional_source?(data, model, path)
|
|
16
17
|
|
|
17
|
-
relative_path = presenter.relative_path
|
|
18
|
+
relative_path = convert_text(presenter.relative_path)
|
|
19
|
+
summary = data['summary']
|
|
18
20
|
puts "File: #{relative_path}"
|
|
21
|
+
puts "Coverage: #{summary['covered']}/#{summary['total']} lines (#{format('%.2f%%', summary['percentage'])})"
|
|
22
|
+
puts "Stale: #{data['stale']}" if StaleStatus.stale?(data['stale'])
|
|
19
23
|
puts
|
|
20
24
|
|
|
21
25
|
# Table format with box-drawing
|
|
@@ -27,7 +31,8 @@ module CovLoupe
|
|
|
27
31
|
puts TableFormatter.format(
|
|
28
32
|
headers: headers,
|
|
29
33
|
rows: rows,
|
|
30
|
-
alignments: [:right, :right, :center]
|
|
34
|
+
alignments: [:right, :right, :center],
|
|
35
|
+
output_chars: config.output_chars
|
|
31
36
|
)
|
|
32
37
|
|
|
33
38
|
print_source_for(model, path) if config.source_mode
|