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/model.rb
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'time'
|
|
4
|
-
require 'json'
|
|
5
|
-
|
|
6
|
-
require_relative 'util'
|
|
7
|
-
require_relative 'errors'
|
|
8
|
-
require_relative 'error_handler'
|
|
9
|
-
require_relative 'staleness_checker'
|
|
10
|
-
require_relative 'path_relativizer'
|
|
11
|
-
require_relative 'resultset_loader'
|
|
12
|
-
|
|
13
|
-
module CovLoupe
|
|
14
|
-
class CoverageModel
|
|
15
|
-
RELATIVIZER_SCALAR_KEYS = %w[file file_path].freeze
|
|
16
|
-
RELATIVIZER_ARRAY_KEYS = %w[newer_files missing_files deleted_files].freeze
|
|
17
|
-
|
|
18
|
-
attr_reader :relativizer
|
|
19
|
-
|
|
20
|
-
# Create a CoverageModel
|
|
21
|
-
#
|
|
22
|
-
# Params:
|
|
23
|
-
# - root: project root directory (default '.')
|
|
24
|
-
# - resultset: path or directory to .resultset.json
|
|
25
|
-
# - staleness: :off or :error (default :off). When :error, raises
|
|
26
|
-
# stale errors if sources are newer than coverage or line counts mismatch.
|
|
27
|
-
# - tracked_globs: only used for all_files project-level staleness.
|
|
28
|
-
def initialize(root: '.', resultset: nil, staleness: :off, tracked_globs: nil)
|
|
29
|
-
@root = File.absolute_path(root || '.')
|
|
30
|
-
@resultset = resultset
|
|
31
|
-
@relativizer = PathRelativizer.new(
|
|
32
|
-
root: @root,
|
|
33
|
-
scalar_keys: RELATIVIZER_SCALAR_KEYS,
|
|
34
|
-
array_keys: RELATIVIZER_ARRAY_KEYS
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
load_coverage_data(resultset, staleness, tracked_globs)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Returns { 'file' => <absolute_path>, 'lines' => [hits|nil,...] }
|
|
41
|
-
def raw_for(path)
|
|
42
|
-
file_abs, coverage_lines = coverage_data_for(path)
|
|
43
|
-
{ 'file' => file_abs, 'lines' => coverage_lines }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def relativize(payload)
|
|
47
|
-
relativizer.relativize(payload)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Returns { 'file' => <absolute_path>, 'summary' => {'covered'=>, 'total'=>, 'percentage'=>} }
|
|
51
|
-
def summary_for(path)
|
|
52
|
-
file_abs, coverage_lines = coverage_data_for(path)
|
|
53
|
-
{ 'file' => file_abs, 'summary' => CovUtil.summary(coverage_lines) }
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Returns { 'file' => <absolute_path>, 'uncovered' => [line,...], 'summary' => {...} }
|
|
57
|
-
def uncovered_for(path)
|
|
58
|
-
file_abs, coverage_lines = coverage_data_for(path)
|
|
59
|
-
{
|
|
60
|
-
'file' => file_abs,
|
|
61
|
-
'uncovered' => CovUtil.uncovered(coverage_lines),
|
|
62
|
-
'summary' => CovUtil.summary(coverage_lines)
|
|
63
|
-
}
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Returns { 'file' => <absolute_path>, 'lines' => [{'line'=>,'hits'=>,'covered'=>},...], 'summary' => {...} }
|
|
67
|
-
def detailed_for(path)
|
|
68
|
-
file_abs, coverage_lines = coverage_data_for(path)
|
|
69
|
-
{
|
|
70
|
-
'file' => file_abs,
|
|
71
|
-
'lines' => CovUtil.detailed(coverage_lines),
|
|
72
|
-
'summary' => CovUtil.summary(coverage_lines)
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Returns [ { 'file' =>, 'covered' =>, 'total' =>, 'percentage' =>, 'stale' => }, ... ]
|
|
77
|
-
def all_files(sort_order: :descending, check_stale: !@checker.off?, tracked_globs: nil)
|
|
78
|
-
stale_checker = build_staleness_checker(mode: :off, tracked_globs: tracked_globs)
|
|
79
|
-
|
|
80
|
-
rows = @cov.map do |abs_path, _data|
|
|
81
|
-
begin
|
|
82
|
-
coverage_lines = CovUtil.lookup_lines(@cov, abs_path)
|
|
83
|
-
rescue FileError
|
|
84
|
-
next
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
s = CovUtil.summary(coverage_lines)
|
|
88
|
-
stale = stale_checker.stale_for_file?(abs_path, coverage_lines)
|
|
89
|
-
{
|
|
90
|
-
'file' => abs_path,
|
|
91
|
-
'covered' => s['covered'],
|
|
92
|
-
'total' => s['total'],
|
|
93
|
-
'percentage' => s['percentage'],
|
|
94
|
-
'stale' => stale
|
|
95
|
-
}
|
|
96
|
-
end.compact
|
|
97
|
-
|
|
98
|
-
rows = filter_rows_by_globs(rows, tracked_globs)
|
|
99
|
-
|
|
100
|
-
if check_stale
|
|
101
|
-
build_staleness_checker(mode: :error, tracked_globs: tracked_globs).check_project!(@cov)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
sort_rows(rows, sort_order: sort_order)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def project_totals(tracked_globs: nil, check_stale: !@checker.off?)
|
|
108
|
-
rows = all_files(sort_order: :ascending, check_stale: check_stale,
|
|
109
|
-
tracked_globs: tracked_globs)
|
|
110
|
-
totals_from_rows(rows)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def staleness_for(path)
|
|
114
|
-
file_abs = File.absolute_path(path, @root)
|
|
115
|
-
coverage_lines = CovUtil.lookup_lines(@cov, file_abs)
|
|
116
|
-
@checker.stale_for_file?(file_abs, coverage_lines)
|
|
117
|
-
rescue => e
|
|
118
|
-
CovUtil.safe_log("Failed to check staleness for #{path}: #{e.message}")
|
|
119
|
-
false
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Returns formatted table string for all files coverage data
|
|
123
|
-
def format_table(rows = nil, sort_order: :descending, check_stale: !@checker.off?,
|
|
124
|
-
tracked_globs: nil)
|
|
125
|
-
rows = prepare_rows(rows, sort_order: sort_order, check_stale: check_stale,
|
|
126
|
-
tracked_globs: tracked_globs)
|
|
127
|
-
return 'No coverage data found' if rows.empty?
|
|
128
|
-
|
|
129
|
-
widths = compute_table_widths(rows)
|
|
130
|
-
lines = []
|
|
131
|
-
lines << border_line(widths, '┌', '┬', '┐')
|
|
132
|
-
lines << header_row(widths)
|
|
133
|
-
lines << border_line(widths, '├', '┼', '┤')
|
|
134
|
-
rows.each { |file_data| lines << data_row(file_data, widths) }
|
|
135
|
-
lines << border_line(widths, '└', '┴', '┘')
|
|
136
|
-
lines << summary_counts(rows)
|
|
137
|
-
if rows.any? { |f| f['stale'] }
|
|
138
|
-
lines <<
|
|
139
|
-
'Staleness: M = Missing file, T = Timestamp (source newer), L = Line count mismatch'
|
|
140
|
-
end
|
|
141
|
-
lines.join("\n")
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
private def load_coverage_data(resultset, staleness, tracked_globs)
|
|
145
|
-
rs = CovUtil.find_resultset(@root, resultset: resultset)
|
|
146
|
-
loaded = ResultsetLoader.load(resultset_path: rs)
|
|
147
|
-
coverage_map = loaded.coverage_map or raise(CoverageDataError, "No 'coverage' key found in resultset file: #{rs}")
|
|
148
|
-
|
|
149
|
-
@cov = coverage_map.transform_keys { |k| File.absolute_path(k, @root) }
|
|
150
|
-
@cov_timestamp = loaded.timestamp
|
|
151
|
-
|
|
152
|
-
@checker = StalenessChecker.new(
|
|
153
|
-
root: @root,
|
|
154
|
-
resultset: @resultset,
|
|
155
|
-
mode: staleness,
|
|
156
|
-
tracked_globs: tracked_globs,
|
|
157
|
-
timestamp: @cov_timestamp
|
|
158
|
-
)
|
|
159
|
-
rescue CovLoupe::Error
|
|
160
|
-
raise # Re-raise our own errors as-is
|
|
161
|
-
rescue => e
|
|
162
|
-
raise ErrorHandler.new.convert_standard_error(e, context: :coverage_loading)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
private def build_staleness_checker(mode:, tracked_globs:)
|
|
166
|
-
StalenessChecker.new(
|
|
167
|
-
root: @root,
|
|
168
|
-
resultset: @resultset,
|
|
169
|
-
mode: mode,
|
|
170
|
-
tracked_globs: tracked_globs,
|
|
171
|
-
timestamp: @cov_timestamp
|
|
172
|
-
)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
private def prepare_rows(rows, sort_order:, check_stale:, tracked_globs:)
|
|
176
|
-
if rows.nil?
|
|
177
|
-
all_files(sort_order: sort_order, check_stale: check_stale, tracked_globs: tracked_globs)
|
|
178
|
-
else
|
|
179
|
-
rows = sort_rows(rows.dup, sort_order: sort_order)
|
|
180
|
-
filter_rows_by_globs(rows, tracked_globs)
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
private def sort_rows(rows, sort_order: :descending)
|
|
185
|
-
rows.sort do |a, b|
|
|
186
|
-
pct_cmp = (sort_order == :descending) \
|
|
187
|
-
? (b['percentage'] <=> a['percentage'])
|
|
188
|
-
: (a['percentage'] <=> b['percentage'])
|
|
189
|
-
pct_cmp == 0 ? (a['file'] <=> b['file']) : pct_cmp
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
private def compute_table_widths(rows)
|
|
194
|
-
max_file_length = rows.map { |f| f['file'].length }.max.to_i
|
|
195
|
-
file_width = [max_file_length, 'File'.length].max + 2
|
|
196
|
-
pct_width = 8
|
|
197
|
-
max_covered = rows.map { |f| f['covered'].to_s.length }.max
|
|
198
|
-
max_total = rows.map { |f| f['total'].to_s.length }.max
|
|
199
|
-
covered_width = [max_covered, 'Covered'.length].max + 2
|
|
200
|
-
total_width = [max_total, 'Total'.length].max + 2
|
|
201
|
-
stale_width = 'Stale'.length
|
|
202
|
-
{
|
|
203
|
-
file: file_width,
|
|
204
|
-
pct: pct_width,
|
|
205
|
-
covered: covered_width,
|
|
206
|
-
total: total_width,
|
|
207
|
-
stale: stale_width
|
|
208
|
-
}
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
private def border_line(widths, left, middle, right)
|
|
212
|
-
h_line = ->(col_width) { '─' * (col_width + 2) }
|
|
213
|
-
left +
|
|
214
|
-
h_line.call(widths[:file]) +
|
|
215
|
-
middle + h_line.call(widths[:pct]) +
|
|
216
|
-
middle + h_line.call(widths[:covered]) +
|
|
217
|
-
middle + h_line.call(widths[:total]) +
|
|
218
|
-
middle + h_line.call(widths[:stale]) +
|
|
219
|
-
right
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
private def header_row(widths)
|
|
223
|
-
format(
|
|
224
|
-
"│ %-#{widths[:file]}s │ %#{widths[:pct]}s │ %#{widths[:covered]}s │ %#{widths[:total]}s │ %#{widths[:stale]}s │",
|
|
225
|
-
'File', ' %', 'Covered', 'Total', 'Stale'.center(widths[:stale])
|
|
226
|
-
)
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
private def data_row(file_data, widths)
|
|
230
|
-
stale_text_str = file_data['stale'] ? file_data['stale'].to_s : ''
|
|
231
|
-
format(
|
|
232
|
-
"│ %-#{widths[:file]}s │ %#{widths[:pct] - 1}.2f%% │ %#{widths[:covered]}d │ %#{widths[:total]}d │ %#{widths[:stale]}s │",
|
|
233
|
-
file_data['file'],
|
|
234
|
-
file_data['percentage'],
|
|
235
|
-
file_data['covered'],
|
|
236
|
-
file_data['total'],
|
|
237
|
-
stale_text_str.center(widths[:stale])
|
|
238
|
-
)
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
private def summary_counts(rows)
|
|
242
|
-
total = rows.length
|
|
243
|
-
stale_count = rows.count { |f| f['stale'] }
|
|
244
|
-
ok_count = total - stale_count
|
|
245
|
-
"Files: total #{total}, ok #{ok_count}, stale #{stale_count}"
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Filters coverage rows to only include files matching the given glob patterns.
|
|
249
|
-
#
|
|
250
|
-
# @param rows [Array<Hash>] coverage rows with 'file' keys containing absolute paths
|
|
251
|
-
# @param tracked_globs [Array<String>, String, nil] glob patterns to match against
|
|
252
|
-
# @return [Array<Hash>] rows whose files match at least one pattern (or all rows if no patterns)
|
|
253
|
-
private def filter_rows_by_globs(rows, tracked_globs)
|
|
254
|
-
patterns = Array(tracked_globs).compact.map(&:to_s).reject(&:empty?)
|
|
255
|
-
return rows if patterns.empty?
|
|
256
|
-
|
|
257
|
-
absolute_patterns = patterns.map { |p| absolutize_pattern(p) }
|
|
258
|
-
rows.select { |row| matches_any_pattern?(row['file'], absolute_patterns) }
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
# Converts a relative pattern to absolute by joining with root.
|
|
262
|
-
# Absolute patterns are returned unchanged.
|
|
263
|
-
#
|
|
264
|
-
# @param pattern [String] glob pattern (e.g., "lib/**/*.rb")
|
|
265
|
-
# @return [String] absolute pattern
|
|
266
|
-
private def absolutize_pattern(pattern)
|
|
267
|
-
absolute_pattern?(pattern) ? pattern : File.join(@root, pattern)
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
# Checks if a pattern is absolute, handling both Unix and Windows-style paths.
|
|
271
|
-
# On Unix, Pathname won't recognize "C:/" as absolute, so we check explicitly.
|
|
272
|
-
#
|
|
273
|
-
# @param pattern [String] glob pattern
|
|
274
|
-
# @return [Boolean] true if pattern is absolute
|
|
275
|
-
private def absolute_pattern?(pattern)
|
|
276
|
-
Pathname.new(pattern).absolute? || pattern.match?(/\A[A-Za-z]:/)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
# Tests if a file path matches any of the given absolute glob patterns.
|
|
280
|
-
# Uses File.fnmatch? for pure string matching without filesystem access,
|
|
281
|
-
# which is faster and works for paths that may no longer exist on disk.
|
|
282
|
-
#
|
|
283
|
-
# @param abs_path [String] absolute file path to test
|
|
284
|
-
# @param patterns [Array<String>] absolute glob patterns
|
|
285
|
-
# @return [Boolean] true if the path matches at least one pattern
|
|
286
|
-
private def matches_any_pattern?(abs_path, patterns)
|
|
287
|
-
flags = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
|
288
|
-
patterns.any? { |pattern| File.fnmatch?(pattern, abs_path, flags) }
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
# Retrieves coverage data for a file path.
|
|
292
|
-
# Converts the path to absolute form and performs staleness checking if enabled.
|
|
293
|
-
#
|
|
294
|
-
# @param path [String] relative or absolute file path
|
|
295
|
-
# @return [Array(String, Array)] tuple of [absolute_path, coverage_lines]
|
|
296
|
-
# @raise [FileError] if no coverage data exists for the file
|
|
297
|
-
# @raise [FileNotFoundError] if the file does not exist
|
|
298
|
-
# @raise [CoverageDataStaleError] if staleness checking is enabled and data is stale
|
|
299
|
-
private def coverage_data_for(path)
|
|
300
|
-
file_abs = File.absolute_path(path, @root)
|
|
301
|
-
begin
|
|
302
|
-
coverage_lines = CovUtil.lookup_lines(@cov, file_abs)
|
|
303
|
-
rescue RuntimeError
|
|
304
|
-
raise FileError, "No coverage data found for file: #{path}"
|
|
305
|
-
end
|
|
306
|
-
@checker.check_file!(file_abs, coverage_lines) unless @checker.off?
|
|
307
|
-
if coverage_lines.nil?
|
|
308
|
-
raise FileError, "No coverage data found for file: #{path}"
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
[file_abs, coverage_lines]
|
|
312
|
-
rescue Errno::ENOENT
|
|
313
|
-
raise FileNotFoundError, "File not found: #{path}"
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
private def totals_from_rows(rows)
|
|
317
|
-
covered = rows.sum { |row| row['covered'].to_i }
|
|
318
|
-
total = rows.sum { |row| row['total'].to_i }
|
|
319
|
-
uncovered = total - covered
|
|
320
|
-
percentage = total.zero? ? 100.0 : ((covered.to_f * 100.0 / total) * 100).round / 100.0
|
|
321
|
-
stale_count = rows.count { |row| row['stale'] }
|
|
322
|
-
files_total = rows.length
|
|
323
|
-
|
|
324
|
-
{
|
|
325
|
-
'lines' => {
|
|
326
|
-
'covered' => covered,
|
|
327
|
-
'uncovered' => uncovered,
|
|
328
|
-
'total' => total
|
|
329
|
-
},
|
|
330
|
-
'percentage' => percentage,
|
|
331
|
-
'files' => {
|
|
332
|
-
'total' => files_total,
|
|
333
|
-
'ok' => files_total - stale_count,
|
|
334
|
-
'stale' => stale_count
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base_coverage_presenter'
|
|
4
|
-
|
|
5
|
-
module CovLoupe
|
|
6
|
-
module Presenters
|
|
7
|
-
# Provides shared detailed coverage payloads for CLI and MCP callers.
|
|
8
|
-
class CoverageDetailedPresenter < BaseCoveragePresenter
|
|
9
|
-
private def build_payload
|
|
10
|
-
model.detailed_for(path)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base_coverage_presenter'
|
|
4
|
-
|
|
5
|
-
module CovLoupe
|
|
6
|
-
module Presenters
|
|
7
|
-
# Provides shared raw coverage payloads for CLI and MCP callers.
|
|
8
|
-
class CoverageRawPresenter < BaseCoveragePresenter
|
|
9
|
-
private def build_payload
|
|
10
|
-
model.raw_for(path)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base_coverage_presenter'
|
|
4
|
-
|
|
5
|
-
module CovLoupe
|
|
6
|
-
module Presenters
|
|
7
|
-
# Builds a consistent summary payload that both the CLI and MCP surfaces can use.
|
|
8
|
-
class CoverageSummaryPresenter < BaseCoveragePresenter
|
|
9
|
-
private def build_payload
|
|
10
|
-
model.summary_for(path)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base_coverage_presenter'
|
|
4
|
-
|
|
5
|
-
module CovLoupe
|
|
6
|
-
module Presenters
|
|
7
|
-
# Provides shared uncovered coverage payloads for CLI and MCP callers.
|
|
8
|
-
class CoverageUncoveredPresenter < BaseCoveragePresenter
|
|
9
|
-
private def build_payload
|
|
10
|
-
model.uncovered_for(path)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'time'
|
|
5
|
-
|
|
6
|
-
require_relative 'errors'
|
|
7
|
-
require_relative 'util'
|
|
8
|
-
|
|
9
|
-
module CovLoupe
|
|
10
|
-
class ResultsetLoader
|
|
11
|
-
Result = Struct.new(:coverage_map, :timestamp, :suite_names, keyword_init: true)
|
|
12
|
-
SuiteEntry = Struct.new(:name, :coverage, :timestamp, keyword_init: true)
|
|
13
|
-
|
|
14
|
-
class << self
|
|
15
|
-
def load(resultset_path:)
|
|
16
|
-
raw = JSON.load_file(resultset_path)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
suites = extract_suite_entries(raw, resultset_path)
|
|
20
|
-
raise CoverageDataError, "No test suite with coverage data found in resultset file: #{resultset_path}" if suites.empty?
|
|
21
|
-
|
|
22
|
-
coverage_map = build_coverage_map(suites, resultset_path)
|
|
23
|
-
Result.new(
|
|
24
|
-
coverage_map: coverage_map,
|
|
25
|
-
timestamp: compute_combined_timestamp(suites),
|
|
26
|
-
suite_names: suites.map(&:name)
|
|
27
|
-
)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
private def extract_suite_entries(raw, resultset_path)
|
|
31
|
-
raw
|
|
32
|
-
.select { |_, data| data.is_a?(Hash) && data.key?('coverage') && !data['coverage'].nil? }
|
|
33
|
-
.map do |name, data|
|
|
34
|
-
SuiteEntry.new(
|
|
35
|
-
name: name.to_s,
|
|
36
|
-
coverage: normalize_suite_coverage(data['coverage'], suite_name: name,
|
|
37
|
-
resultset_path: resultset_path),
|
|
38
|
-
timestamp: normalize_coverage_timestamp(data['timestamp'], data['created_at'])
|
|
39
|
-
)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private def build_coverage_map(suites, resultset_path)
|
|
44
|
-
return suites.first&.coverage if suites.length == 1
|
|
45
|
-
|
|
46
|
-
merge_suite_coverages(suites, resultset_path)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private def normalize_suite_coverage(coverage, suite_name:, resultset_path:)
|
|
50
|
-
unless coverage.is_a?(Hash)
|
|
51
|
-
raise CoverageDataError, "Invalid coverage data structure for suite #{suite_name.inspect} in resultset file: #{resultset_path}"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
needs_adaptation = coverage.values.any? { |value| value.is_a?(Array) }
|
|
55
|
-
return coverage unless needs_adaptation
|
|
56
|
-
|
|
57
|
-
coverage.transform_values do |value|
|
|
58
|
-
value.is_a?(Array) ? { 'lines' => value } : value
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private def merge_suite_coverages(suites, resultset_path)
|
|
63
|
-
require_simplecov_for_merge!(resultset_path)
|
|
64
|
-
log_duplicate_suite_names(suites)
|
|
65
|
-
|
|
66
|
-
suites.reduce(nil) do |memo, suite|
|
|
67
|
-
coverage = suite.coverage
|
|
68
|
-
memo ?
|
|
69
|
-
SimpleCov::Combine.combine(SimpleCov::Combine::ResultsCombiner, memo, coverage) :
|
|
70
|
-
coverage
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
private def require_simplecov_for_merge!(resultset_path)
|
|
75
|
-
require 'simplecov'
|
|
76
|
-
rescue LoadError
|
|
77
|
-
raise CoverageDataError, "Multiple coverage suites detected in #{resultset_path}, but the simplecov gem could not be loaded. Install simplecov to enable suite merging."
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
private def log_duplicate_suite_names(suites)
|
|
81
|
-
grouped = suites.group_by(&:name)
|
|
82
|
-
duplicates = grouped.select { |_, entries| entries.length > 1 }.keys
|
|
83
|
-
return if duplicates.empty?
|
|
84
|
-
|
|
85
|
-
message = "Merging duplicate coverage suites for #{duplicates.join(', ')}"
|
|
86
|
-
CovUtil.safe_log(message)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
private def compute_combined_timestamp(suites)
|
|
90
|
-
suites.map(&:timestamp).compact.max.to_i
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
private def normalize_coverage_timestamp(timestamp_value, created_at_value)
|
|
94
|
-
raw = timestamp_value.nil? ? created_at_value : timestamp_value
|
|
95
|
-
return 0 if raw.nil?
|
|
96
|
-
|
|
97
|
-
case raw
|
|
98
|
-
when Integer
|
|
99
|
-
raw
|
|
100
|
-
when Float, Time
|
|
101
|
-
raw.to_i
|
|
102
|
-
when String
|
|
103
|
-
normalize_string_timestamp(raw)
|
|
104
|
-
else
|
|
105
|
-
log_timestamp_warning(raw)
|
|
106
|
-
0
|
|
107
|
-
end
|
|
108
|
-
rescue => e
|
|
109
|
-
log_timestamp_warning(raw, e)
|
|
110
|
-
0
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
private def normalize_string_timestamp(value)
|
|
114
|
-
str = value.strip
|
|
115
|
-
return 0 if str.empty?
|
|
116
|
-
|
|
117
|
-
if str.match?(/\A-?\d+(\.\d+)?\z/)
|
|
118
|
-
str.to_f.to_i
|
|
119
|
-
else
|
|
120
|
-
Time.parse(str).to_i
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
private def log_timestamp_warning(raw_value, error = nil)
|
|
125
|
-
message = "Coverage resultset timestamp could not be parsed: #{raw_value.inspect}"
|
|
126
|
-
message = "#{message} (#{error.message})" if error
|
|
127
|
-
CovUtil.safe_log(message)
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|