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,19 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../paths/path_utils'
|
|
4
|
+
|
|
3
5
|
module CovLoupe
|
|
4
6
|
module Resolvers
|
|
7
|
+
# Finds a SimpleCov line coverage array for a given file path.
|
|
8
|
+
#
|
|
9
|
+
# This is a string-based resolver: it does not touch the filesystem. It
|
|
10
|
+
# looks up keys in the coverage map using two strategies:
|
|
11
|
+
# 1) exact match on the provided path
|
|
12
|
+
# 2) match after stripping the configured root prefix
|
|
5
13
|
class CoverageLineResolver
|
|
6
|
-
|
|
14
|
+
# @param cov_data [Hash] coverage data map keyed by file path
|
|
15
|
+
# @param root [String, nil] project root used for path stripping
|
|
16
|
+
# @param volume_case_sensitive [Boolean] whether the volume is case-sensitive
|
|
17
|
+
def initialize(cov_data, root:, volume_case_sensitive:)
|
|
7
18
|
@cov_data = cov_data
|
|
19
|
+
@root = root
|
|
20
|
+
@normalize_case = !volume_case_sensitive
|
|
8
21
|
end
|
|
9
22
|
|
|
23
|
+
# Resolve coverage lines for a file path, trying fallbacks before raising.
|
|
24
|
+
# @param file_abs [String] absolute file path to resolve
|
|
25
|
+
# @return [Array<Integer, nil>] SimpleCov-style line coverage array
|
|
10
26
|
def lookup_lines(file_abs)
|
|
27
|
+
# Normalize the input path first to handle platform-specific differences
|
|
28
|
+
normalized_path = normalize_path(file_abs)
|
|
29
|
+
|
|
11
30
|
# First try exact match
|
|
12
|
-
direct_match = find_direct_match(
|
|
31
|
+
direct_match = find_direct_match(normalized_path)
|
|
13
32
|
return direct_match if direct_match
|
|
14
33
|
|
|
15
34
|
# Then try without current working directory prefix
|
|
16
|
-
stripped_match = find_stripped_match(
|
|
35
|
+
stripped_match = find_stripped_match(normalized_path)
|
|
17
36
|
return stripped_match if stripped_match
|
|
18
37
|
|
|
19
38
|
raise_not_found_error(file_abs)
|
|
@@ -22,100 +41,95 @@ module CovLoupe
|
|
|
22
41
|
attr_reader :cov_data
|
|
23
42
|
|
|
24
43
|
private def find_direct_match(file_abs)
|
|
25
|
-
|
|
26
|
-
lines_from_entry(entry)
|
|
44
|
+
fetch_lines_for_path(file_abs)
|
|
27
45
|
end
|
|
28
46
|
|
|
47
|
+
# Try matching a path after removing the root prefix.
|
|
29
48
|
private def find_stripped_match(file_abs)
|
|
30
|
-
return unless
|
|
49
|
+
return unless @root
|
|
50
|
+
|
|
51
|
+
normalized_file = normalize_path(file_abs)
|
|
52
|
+
return unless normalized_file.start_with?(normalized_root_with_slash)
|
|
53
|
+
|
|
54
|
+
relative_path = normalized_file[(normalized_root.length + 1)..]
|
|
55
|
+
fetch_lines_for_path(relative_path)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Fetch lines for a path, resolving normalized separators when needed.
|
|
59
|
+
private def fetch_lines_for_path(path)
|
|
60
|
+
key = resolve_key(path)
|
|
61
|
+
return unless key
|
|
62
|
+
|
|
63
|
+
entry = cov_data[key]
|
|
64
|
+
lines = lines_from_entry(entry)
|
|
65
|
+
return lines if lines
|
|
66
|
+
|
|
67
|
+
raise CorruptCoverageDataError, "Entry for #{path} has no valid lines"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private def resolution_root
|
|
71
|
+
@resolution_root ||= @root
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private def normalized_root
|
|
75
|
+
@normalized_root ||= normalize_path(resolution_root)
|
|
76
|
+
end
|
|
31
77
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
78
|
+
private def normalized_root_with_slash
|
|
79
|
+
return unless normalized_root
|
|
80
|
+
|
|
81
|
+
@normalized_root_with_slash ||= "#{normalized_root}/"
|
|
35
82
|
end
|
|
36
83
|
|
|
37
|
-
|
|
38
|
-
|
|
84
|
+
# Resolve the coverage key that matches a path (including normalized variants).
|
|
85
|
+
private def resolve_key(path)
|
|
86
|
+
normalized = normalize_path(path)
|
|
87
|
+
match_keys = cov_data.keys.select { |key| normalize_path(key) == normalized }
|
|
88
|
+
|
|
89
|
+
return if match_keys.empty?
|
|
90
|
+
|
|
91
|
+
# If exact path match exists and it's the only one, return it
|
|
92
|
+
return path if cov_data.key?(path) && match_keys.length == 1
|
|
93
|
+
|
|
94
|
+
# If multiple matches, raise ambiguity error
|
|
95
|
+
if match_keys.length > 1
|
|
96
|
+
raise FileError, "Multiple coverage entries match path #{path}: #{match_keys.join(', ')}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Single match found, return it
|
|
100
|
+
match_keys.first
|
|
39
101
|
end
|
|
40
102
|
|
|
41
|
-
|
|
42
|
-
|
|
103
|
+
# Normalize a path using centralized PathUtils
|
|
104
|
+
private def normalize_path(path)
|
|
105
|
+
PathUtils.normalize(path, normalize_case: @normalize_case)
|
|
43
106
|
end
|
|
44
107
|
|
|
45
108
|
private def raise_not_found_error(file_abs)
|
|
46
|
-
raise FileError, "No coverage entry found for #{file_abs}"
|
|
109
|
+
raise FileError, "No coverage entry found for #{normalize_path(file_abs)}"
|
|
47
110
|
end
|
|
48
111
|
|
|
49
|
-
# Entry may store exact line coverage
|
|
50
|
-
#
|
|
51
|
-
#
|
|
112
|
+
# Entry may store exact line coverage.
|
|
113
|
+
#
|
|
114
|
+
# Validates that the entry contains a properly-formed lines array
|
|
115
|
+
# with only Integer or nil elements.
|
|
52
116
|
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
117
|
+
# @raise [CoverageDataError] if lines array contains invalid elements
|
|
118
|
+
# @return [Array<Integer, nil>, nil] validated lines array or nil if entry lacks lines
|
|
55
119
|
private def lines_from_entry(entry)
|
|
56
120
|
return unless entry.is_a?(Hash)
|
|
57
121
|
|
|
58
122
|
lines = entry['lines']
|
|
59
|
-
return
|
|
123
|
+
return nil unless lines.is_a?(Array)
|
|
60
124
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# so the rest of the pipeline (summaries, uncovered lines, staleness) can
|
|
67
|
-
# continue to operate.
|
|
68
|
-
#
|
|
69
|
-
# Branch data looks like:
|
|
70
|
-
# "[:if, 0, 12, 4, 20, 29]" => { "[:then, ...]" => hits, ... }
|
|
71
|
-
# We care about the third tuple element (line number). We sum branch-leg
|
|
72
|
-
# hits per line so the synthetic array still behaves like legacy line
|
|
73
|
-
# coverage (any positive value counts as executed).
|
|
74
|
-
private def synthesize_lines_from_branches(branch_data)
|
|
75
|
-
# Detailed shape and rationale documented in docs/BRANCH_ONLY_COVERAGE.md
|
|
76
|
-
return unless branch_data.is_a?(Hash) && branch_data.any?
|
|
77
|
-
|
|
78
|
-
line_hits = {}
|
|
79
|
-
|
|
80
|
-
branch_data
|
|
81
|
-
.values
|
|
82
|
-
.select { |targets| targets.is_a?(Hash) } # ignore malformed branch entries
|
|
83
|
-
.flat_map(&:to_a) # flatten each branch target into [meta, hits]
|
|
84
|
-
.filter_map do |meta, hits|
|
|
85
|
-
# Extract the covered line; filter_map discards nil results.
|
|
86
|
-
line_number = extract_line_number(meta)
|
|
87
|
-
line_number && [line_number, hits.to_i]
|
|
88
|
-
end
|
|
89
|
-
.each do |line_number, hits|
|
|
90
|
-
line_hits[line_number] = line_hits.fetch(line_number, 0) + hits
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
return if line_hits.empty?
|
|
94
|
-
|
|
95
|
-
max_line = line_hits.keys.max
|
|
96
|
-
# Build a dense array up to the highest line recorded so downstream
|
|
97
|
-
# consumers see the familiar SimpleCov shape (nil for untouched lines).
|
|
98
|
-
Array.new(max_line) { |idx| line_hits[idx + 1] }
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Branch metadata arrives as either the raw SimpleCov array
|
|
102
|
-
# (e.g. [:if, 0, 12, 4, 20, 29]) or the stringified JSON version
|
|
103
|
-
# ("[:if, 0, 12, 4, 20, 29]"). We normalize both forms and pull the line.
|
|
104
|
-
private def extract_line_number(meta)
|
|
105
|
-
if meta.is_a?(Array)
|
|
106
|
-
line_token = meta[2]
|
|
107
|
-
# Integer(..., exception: false) returns nil on failure, so malformed
|
|
108
|
-
# tuples quietly drop out of the synthesized array.
|
|
109
|
-
return Integer(line_token, exception: false)
|
|
125
|
+
# Validate all elements are Integer or nil
|
|
126
|
+
invalid_elements = lines.reject { |v| v.nil? || v.is_a?(Integer) }
|
|
127
|
+
unless invalid_elements.empty?
|
|
128
|
+
raise CoverageDataError,
|
|
129
|
+
"Invalid coverage line array: contains non-integer elements: #{invalid_elements.inspect}"
|
|
110
130
|
end
|
|
111
131
|
|
|
112
|
-
|
|
113
|
-
return if tokens.length < 3
|
|
114
|
-
|
|
115
|
-
Integer(tokens[2], exception: false)
|
|
116
|
-
# Any parsing errors result in nil; callers treat that as "no line".
|
|
117
|
-
rescue ArgumentError, TypeError
|
|
118
|
-
nil
|
|
132
|
+
lines
|
|
119
133
|
end
|
|
120
134
|
end
|
|
121
135
|
end
|
|
@@ -5,23 +5,24 @@ require_relative 'coverage_line_resolver'
|
|
|
5
5
|
|
|
6
6
|
module CovLoupe
|
|
7
7
|
module Resolvers
|
|
8
|
-
class
|
|
8
|
+
class ResolverHelpers
|
|
9
9
|
def self.create_resultset_resolver(root: Dir.pwd, resultset: nil, candidates: nil)
|
|
10
10
|
candidates ?
|
|
11
11
|
ResultsetPathResolver.new(root: root, candidates: candidates) :
|
|
12
12
|
ResultsetPathResolver.new(root: root)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def self.create_coverage_resolver(cov_data)
|
|
16
|
-
CoverageLineResolver.new(cov_data)
|
|
15
|
+
def self.create_coverage_resolver(cov_data, root:, volume_case_sensitive:)
|
|
16
|
+
CoverageLineResolver.new(cov_data, root: root, volume_case_sensitive: volume_case_sensitive)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def self.find_resultset(root, resultset: nil)
|
|
20
20
|
ResultsetPathResolver.new(root: root).find_resultset(resultset: resultset)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def self.lookup_lines(cov, file_abs)
|
|
24
|
-
CoverageLineResolver.new(cov
|
|
23
|
+
def self.lookup_lines(cov, file_abs, root:, volume_case_sensitive:)
|
|
24
|
+
CoverageLineResolver.new(cov, root: root,
|
|
25
|
+
volume_case_sensitive: volume_case_sensitive).lookup_lines(file_abs)
|
|
25
26
|
end
|
|
26
27
|
end
|
|
27
28
|
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require 'pathname'
|
|
4
4
|
|
|
5
|
+
require_relative '../errors/errors'
|
|
6
|
+
require_relative '../paths/path_utils'
|
|
7
|
+
|
|
5
8
|
module CovLoupe
|
|
6
9
|
module Resolvers
|
|
7
10
|
class ResultsetPathResolver
|
|
@@ -39,37 +42,62 @@ module CovLoupe
|
|
|
39
42
|
candidate = File.join(path, '.resultset.json')
|
|
40
43
|
return candidate if File.file?(candidate)
|
|
41
44
|
|
|
42
|
-
raise "No .resultset.json found in directory: #{path}"
|
|
45
|
+
raise ResultsetNotFoundError, "No .resultset.json found in directory: #{path}"
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
private def raise_not_found_error_for_file(path)
|
|
46
|
-
raise "Specified resultset not found: #{path}"
|
|
49
|
+
raise ResultsetNotFoundError, "Specified resultset not found: #{path}"
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
private def resolve_fallback
|
|
50
53
|
@candidates
|
|
51
|
-
.map { |p|
|
|
54
|
+
.map { |p| PathUtils.expand(p, @root) }
|
|
52
55
|
.find { |p| File.file?(p) }
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
private def normalize_resultset_path(resultset)
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
Pathname.new(resultset)
|
|
60
|
+
expanded_resultset = PathUtils.expand(resultset, Dir.pwd)
|
|
61
|
+
expanded_root = PathUtils.expand(resultset, @root)
|
|
62
|
+
|
|
63
|
+
if ambiguous_resultset_path?(expanded_resultset, expanded_root)
|
|
64
|
+
raise_ambiguous_resultset_error(expanded_resultset, expanded_root)
|
|
65
|
+
end
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
return
|
|
67
|
+
return expanded_resultset if valid_resultset_location?(expanded_resultset)
|
|
68
|
+
return expanded_root if valid_resultset_location?(expanded_root)
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
return expanded_resultset if within_root?(expanded_resultset)
|
|
71
|
+
|
|
72
|
+
expanded_root
|
|
63
73
|
end
|
|
64
74
|
|
|
65
75
|
private def within_root?(path)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
PathUtils.within_root?(path, @root)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private def ambiguous_resultset_path?(expanded_pwd, expanded_root)
|
|
80
|
+
return false if expanded_pwd == expanded_root
|
|
81
|
+
|
|
82
|
+
valid_resultset_location?(expanded_pwd) && valid_resultset_location?(expanded_root)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private def valid_resultset_location?(path)
|
|
86
|
+
return true if File.file?(path)
|
|
87
|
+
return false unless File.directory?(path)
|
|
88
|
+
|
|
89
|
+
File.file?(File.join(path, '.resultset.json'))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private def raise_ambiguous_resultset_error(expanded_pwd, expanded_root)
|
|
93
|
+
raise ConfigurationError, "Ambiguous resultset location specified. Both #{expanded_pwd} and #{expanded_root} exist. " \
|
|
94
|
+
'Use `./` or an absolute filespec to disambiguate.'
|
|
69
95
|
end
|
|
70
96
|
|
|
71
97
|
private def raise_not_found_error
|
|
72
|
-
|
|
98
|
+
message = "Could not find .resultset.json under #{@root.inspect}; run tests or set --resultset option"
|
|
99
|
+
CovLoupe.logger.error(message) if CovLoupe.logger
|
|
100
|
+
raise ResultsetNotFoundError, message
|
|
73
101
|
end
|
|
74
102
|
end
|
|
75
103
|
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module CovLoupe
|
|
7
|
+
module Scripts
|
|
8
|
+
module CommandExecution
|
|
9
|
+
# Execute a command and return its stdout.
|
|
10
|
+
#
|
|
11
|
+
# @param cmd [String, Array<String>] The shell command to run.
|
|
12
|
+
# @param print_output [Boolean] If true, prints output to stdout/stderr in real-time.
|
|
13
|
+
# @param fail_on_error [Boolean] If true, aborts execution if the command fails.
|
|
14
|
+
# @return [String] The stdout output of the command (stripped).
|
|
15
|
+
def run_command(cmd, print_output: false, fail_on_error: true)
|
|
16
|
+
if print_output
|
|
17
|
+
run_streamed(cmd, fail_on_error: fail_on_error)
|
|
18
|
+
else
|
|
19
|
+
run_captured(cmd, fail_on_error: fail_on_error)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Execute a command and return stdout and success status.
|
|
24
|
+
#
|
|
25
|
+
# @param cmd [String, Array<String>] The shell command to run.
|
|
26
|
+
# @return [Array<String, Boolean>] The stdout and success boolean.
|
|
27
|
+
def run_command_with_status(cmd)
|
|
28
|
+
stdout, _stderr, status = capture_command(cmd)
|
|
29
|
+
[stdout.strip, status.success?]
|
|
30
|
+
rescue Errno::ENOENT
|
|
31
|
+
["Command not found: #{command_display(cmd)}", false]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Print an error message and exit with status 1.
|
|
35
|
+
def abort_with(message)
|
|
36
|
+
warn "ERROR: #{message}"
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check if a command exists in the system PATH.
|
|
41
|
+
def command_exists?(cmd)
|
|
42
|
+
return true if File.exist?(cmd) && File.executable?(cmd)
|
|
43
|
+
|
|
44
|
+
checker = Gem.win_platform? ? 'where' : 'which'
|
|
45
|
+
system(checker, cmd, out: File::NULL, err: File::NULL)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private def run_streamed(cmd, fail_on_error:)
|
|
49
|
+
puts "→ #{command_display(cmd)}"
|
|
50
|
+
output = +''
|
|
51
|
+
status = nil
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
popen_command(cmd) do |_stdin, stdout_err, wait_thr|
|
|
55
|
+
stdout_err.each do |line|
|
|
56
|
+
print line
|
|
57
|
+
output << line
|
|
58
|
+
end
|
|
59
|
+
status = wait_thr.value
|
|
60
|
+
end
|
|
61
|
+
rescue Errno::ENOENT
|
|
62
|
+
abort_with("Command not found: #{command_display(cmd)}") if fail_on_error
|
|
63
|
+
return ''
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if fail_on_error && !status&.success?
|
|
67
|
+
abort_with("Command failed: #{cmd}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
output.strip
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private def run_captured(cmd, fail_on_error:)
|
|
74
|
+
begin
|
|
75
|
+
stdout, stderr, status = capture_command(cmd)
|
|
76
|
+
rescue Errno::ENOENT
|
|
77
|
+
abort_with("Command not found: #{command_display(cmd)}") if fail_on_error
|
|
78
|
+
return ''
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if fail_on_error && !status.success?
|
|
82
|
+
warn "Error running: #{command_display(cmd)}"
|
|
83
|
+
warn stderr unless stderr.strip.empty?
|
|
84
|
+
exit 1
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
stdout.strip
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private def popen_command(cmd, &)
|
|
91
|
+
if cmd.is_a?(Array)
|
|
92
|
+
Open3.popen2e(*cmd, &)
|
|
93
|
+
else
|
|
94
|
+
Open3.popen2e(cmd, &)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private def capture_command(cmd)
|
|
99
|
+
if cmd.is_a?(Array)
|
|
100
|
+
Open3.capture3(*cmd)
|
|
101
|
+
else
|
|
102
|
+
Open3.capture3(cmd)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private def command_display(cmd)
|
|
107
|
+
return Shellwords.join(cmd) if cmd.is_a?(Array)
|
|
108
|
+
|
|
109
|
+
cmd.to_s
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'command_execution'
|
|
5
|
+
|
|
6
|
+
module CovLoupe
|
|
7
|
+
module Scripts
|
|
8
|
+
class LatestCiStatus
|
|
9
|
+
include CommandExecution
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
branch = fetch_current_branch
|
|
13
|
+
puts "Fetching latest CI run for branch: #{branch}..."
|
|
14
|
+
|
|
15
|
+
run_data = fetch_latest_run(branch)
|
|
16
|
+
|
|
17
|
+
if run_data.nil?
|
|
18
|
+
puts "No workflow runs found for branch '#{branch}'."
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
display_run_details(run_data)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private def fetch_current_branch
|
|
26
|
+
run_command(%w[git rev-parse --abbrev-ref HEAD])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private def fetch_latest_run(branch)
|
|
30
|
+
json_output, success = run_command_with_status(
|
|
31
|
+
['gh', 'run', 'list', '--branch', branch, '--limit', '1', '--json',
|
|
32
|
+
'databaseId,status,conclusion,url,displayTitle,createdAt']
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
unless success
|
|
36
|
+
warn "Failed to fetch runs. Ensure 'gh' is installed and you are authenticated."
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
runs = JSON.parse(json_output)
|
|
41
|
+
runs.first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private def display_run_details(run)
|
|
45
|
+
id = run['databaseId']
|
|
46
|
+
status = run['status']
|
|
47
|
+
conclusion = run['conclusion']
|
|
48
|
+
url = run['url']
|
|
49
|
+
title = run['displayTitle']
|
|
50
|
+
created_at = run['createdAt']
|
|
51
|
+
|
|
52
|
+
color = status_color(status, conclusion)
|
|
53
|
+
display_status = status == 'completed' ? (conclusion || 'unknown').upcase : status.upcase
|
|
54
|
+
|
|
55
|
+
puts "\nLatest Run Details:"
|
|
56
|
+
puts '-------------------'
|
|
57
|
+
puts "Title: #{title}"
|
|
58
|
+
puts "ID: #{id}"
|
|
59
|
+
puts "Time: #{created_at}"
|
|
60
|
+
puts "Status: #{colorize(display_status, color)}"
|
|
61
|
+
puts "URL: #{url}"
|
|
62
|
+
|
|
63
|
+
handle_status_action(status, conclusion, id)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private def handle_status_action(status, conclusion, id)
|
|
67
|
+
if status == 'completed' && ['failure', 'startup_failure', 'timed_out'].include?(conclusion)
|
|
68
|
+
puts "\n#{colorize('Fetching failure logs...', 31)}"
|
|
69
|
+
puts '------------------------'
|
|
70
|
+
system('gh', 'run', 'view', id.to_s, '--log-failed')
|
|
71
|
+
elsif status == 'in_progress'
|
|
72
|
+
puts "\n#{colorize('Build is currently running... ⏳', 34)}"
|
|
73
|
+
puts "You can watch it with: gh run watch #{id}"
|
|
74
|
+
elsif status == 'queued'
|
|
75
|
+
puts "\n#{colorize('Build is queued... 🕒', 34)}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private def colorize(text, color_code)
|
|
80
|
+
"\e[#{color_code}m#{text}\e[0m"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private def status_color(status, conclusion)
|
|
84
|
+
if status == 'completed'
|
|
85
|
+
case conclusion
|
|
86
|
+
when 'success' then 32 # Green
|
|
87
|
+
when 'failure', 'startup_failure', 'timed_out' then 31 # Red
|
|
88
|
+
when 'cancelled' then 33 # Yellow
|
|
89
|
+
else 37 # White
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
34 # Blue for in_progress/queued
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|