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,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base_command'
|
|
4
|
-
require_relative '../presenters/
|
|
5
|
-
require_relative '../table_formatter'
|
|
4
|
+
require_relative '../presenters/coverage_payload_presenter'
|
|
5
|
+
require_relative '../formatters/table_formatter'
|
|
6
6
|
|
|
7
7
|
module CovLoupe
|
|
8
8
|
module Commands
|
|
9
9
|
class RawCommand < BaseCommand
|
|
10
10
|
def execute(args)
|
|
11
11
|
handle_with_path(args, 'raw') do |path|
|
|
12
|
-
presenter = Presenters::
|
|
12
|
+
presenter = Presenters::CoveragePayloadPresenter.new(model: model, path: path,
|
|
13
|
+
payload_method: :raw_for)
|
|
13
14
|
data = presenter.absolute_payload
|
|
14
15
|
break if maybe_output_structured_format?(data, model)
|
|
15
16
|
|
|
16
|
-
relative_path = presenter.relative_path
|
|
17
|
+
relative_path = convert_text(presenter.relative_path)
|
|
17
18
|
puts "File: #{relative_path}"
|
|
18
19
|
puts
|
|
19
20
|
|
|
@@ -29,7 +30,8 @@ module CovLoupe
|
|
|
29
30
|
puts TableFormatter.format(
|
|
30
31
|
headers: headers,
|
|
31
32
|
rows: rows,
|
|
32
|
-
alignments: [:right, :right]
|
|
33
|
+
alignments: [:right, :right],
|
|
34
|
+
output_chars: config.output_chars
|
|
33
35
|
)
|
|
34
36
|
end
|
|
35
37
|
end
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base_command'
|
|
4
|
-
require_relative '../presenters/
|
|
5
|
-
require_relative '../table_formatter'
|
|
4
|
+
require_relative '../presenters/coverage_payload_presenter'
|
|
5
|
+
require_relative '../formatters/table_formatter'
|
|
6
|
+
require_relative '../staleness/stale_status'
|
|
6
7
|
|
|
7
8
|
module CovLoupe
|
|
8
9
|
module Commands
|
|
9
10
|
class SummaryCommand < BaseCommand
|
|
10
11
|
def execute(args)
|
|
11
12
|
handle_with_path(args, 'summary') do |path|
|
|
12
|
-
presenter = Presenters::
|
|
13
|
+
presenter = Presenters::CoveragePayloadPresenter.new(model: model, path: path,
|
|
14
|
+
payload_method: :summary_for)
|
|
13
15
|
data = presenter.absolute_payload
|
|
14
16
|
break if emit_structured_format_with_optional_source?(data, model, path)
|
|
15
17
|
|
|
16
|
-
relative_path = presenter.relative_path
|
|
18
|
+
relative_path = convert_text(presenter.relative_path)
|
|
17
19
|
summary = data['summary']
|
|
18
20
|
|
|
19
21
|
# Table format with box-drawing
|
|
20
22
|
headers = ['File', '%', 'Covered', 'Total', 'Stale']
|
|
21
|
-
stale_marker = data['stale'] ? 'Yes' : ''
|
|
23
|
+
stale_marker = StaleStatus.stale?(data['stale']) ? 'Yes' : ''
|
|
24
|
+
percent_display = summary['percentage'] ? format('%.2f%%', summary['percentage']) : 'n/a'.rjust(6)
|
|
25
|
+
|
|
22
26
|
rows = [[
|
|
23
27
|
relative_path,
|
|
24
|
-
|
|
28
|
+
percent_display,
|
|
25
29
|
summary['covered'].to_s,
|
|
26
30
|
summary['total'].to_s,
|
|
27
31
|
stale_marker
|
|
@@ -30,7 +34,8 @@ module CovLoupe
|
|
|
30
34
|
puts TableFormatter.format(
|
|
31
35
|
headers: headers,
|
|
32
36
|
rows: rows,
|
|
33
|
-
alignments: [:left, :right, :right, :right, :center]
|
|
37
|
+
alignments: [:left, :right, :right, :right, :center],
|
|
38
|
+
output_chars: config.output_chars
|
|
34
39
|
)
|
|
35
40
|
puts
|
|
36
41
|
print_source_for(model, path) if config.source_mode
|
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base_command'
|
|
4
4
|
require_relative '../presenters/project_totals_presenter'
|
|
5
|
-
require_relative '../table_formatter'
|
|
5
|
+
require_relative '../formatters/table_formatter'
|
|
6
6
|
|
|
7
7
|
module CovLoupe
|
|
8
8
|
module Commands
|
|
9
9
|
class TotalsCommand < BaseCommand
|
|
10
10
|
def execute(args)
|
|
11
|
-
|
|
12
|
-
raise UsageError.for_subcommand('totals')
|
|
13
|
-
end
|
|
11
|
+
reject_extra_args(args, 'totals')
|
|
14
12
|
|
|
15
13
|
presenter = Presenters::ProjectTotalsPresenter.new(
|
|
16
14
|
model: model,
|
|
17
|
-
|
|
15
|
+
raise_on_stale: config.raise_on_stale,
|
|
18
16
|
tracked_globs: config.tracked_globs
|
|
19
17
|
)
|
|
20
18
|
payload = presenter.absolute_payload
|
|
@@ -22,22 +20,36 @@ module CovLoupe
|
|
|
22
20
|
|
|
23
21
|
lines = payload['lines']
|
|
24
22
|
files = payload['files']
|
|
23
|
+
tracking = payload['tracking']
|
|
24
|
+
with_coverage = files['with_coverage']
|
|
25
|
+
without_coverage = files['without_coverage']
|
|
26
|
+
|
|
27
|
+
if tracking && tracking['enabled']
|
|
28
|
+
puts 'Tracked globs:'
|
|
29
|
+
tracking['globs'].each { |glob| puts " - #{convert_text(glob)}" }
|
|
30
|
+
else
|
|
31
|
+
puts 'Tracked globs: (tracking disabled)'
|
|
32
|
+
end
|
|
33
|
+
puts
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
puts 'Totals'
|
|
27
36
|
headers = ['Metric', 'Total', 'Covered', 'Uncovered', '%']
|
|
37
|
+
file_ok = with_coverage['ok']
|
|
38
|
+
file_uncovered = files['total'] - file_ok
|
|
39
|
+
percent_display = lines['percent_covered'].nil? ? 'n/a' : format('%.2f%%', lines['percent_covered'])
|
|
28
40
|
rows = [
|
|
29
41
|
[
|
|
30
42
|
'Lines',
|
|
31
43
|
lines['total'].to_s,
|
|
32
44
|
lines['covered'].to_s,
|
|
33
45
|
lines['uncovered'].to_s,
|
|
34
|
-
|
|
46
|
+
percent_display
|
|
35
47
|
],
|
|
36
48
|
[
|
|
37
49
|
'Files',
|
|
38
50
|
files['total'].to_s,
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
file_ok.to_s,
|
|
52
|
+
file_uncovered.to_s,
|
|
41
53
|
''
|
|
42
54
|
]
|
|
43
55
|
]
|
|
@@ -45,8 +57,60 @@ module CovLoupe
|
|
|
45
57
|
puts TableFormatter.format(
|
|
46
58
|
headers: headers,
|
|
47
59
|
rows: rows,
|
|
48
|
-
alignments: [:left, :right, :right, :right, :right]
|
|
60
|
+
alignments: [:left, :right, :right, :right, :right],
|
|
61
|
+
output_chars: config.output_chars
|
|
49
62
|
)
|
|
63
|
+
with_coverage_line = format_with_coverage_line(with_coverage)
|
|
64
|
+
stale_line = format_stale_breakdown(with_coverage['stale']['by_type'])
|
|
65
|
+
without_coverage_line, without_breakdown_line =
|
|
66
|
+
format_without_coverage_lines(without_coverage)
|
|
67
|
+
|
|
68
|
+
puts <<~BREAKDOWN
|
|
69
|
+
|
|
70
|
+
File breakdown:
|
|
71
|
+
#{with_coverage_line}
|
|
72
|
+
#{stale_line}
|
|
73
|
+
#{without_coverage_line}
|
|
74
|
+
#{without_breakdown_line}
|
|
75
|
+
BREAKDOWN
|
|
76
|
+
|
|
77
|
+
warn_missing_timestamps(presenter)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private def format_with_coverage_line(with_coverage)
|
|
81
|
+
stale = with_coverage['stale']
|
|
82
|
+
" With coverage: #{with_coverage['total']} total, #{with_coverage['ok']} ok, #{stale['total']} stale"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private def format_stale_breakdown(stale_by_type)
|
|
86
|
+
' Stale: missing on disk = ' \
|
|
87
|
+
"#{stale_by_type['missing_from_disk']}, " \
|
|
88
|
+
"newer than coverage = #{stale_by_type['newer']}, " \
|
|
89
|
+
"line mismatch = #{stale_by_type['length_mismatch']}, " \
|
|
90
|
+
"unreadable = #{stale_by_type['unreadable']}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private def format_without_coverage_lines(without_coverage)
|
|
94
|
+
return [nil, nil] unless without_coverage
|
|
95
|
+
|
|
96
|
+
without_by_type = without_coverage['by_type']
|
|
97
|
+
without_coverage_line = " Without coverage: #{without_coverage['total']} total"
|
|
98
|
+
without_breakdown_line = ' Missing from coverage = ' \
|
|
99
|
+
"#{without_by_type['missing_from_coverage']}, " \
|
|
100
|
+
"unreadable = #{without_by_type['unreadable']}, " \
|
|
101
|
+
"skipped (errors) = #{without_by_type['skipped']}"
|
|
102
|
+
[without_coverage_line, without_breakdown_line]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private def warn_missing_timestamps(presenter)
|
|
106
|
+
return unless presenter.timestamp_status == 'missing'
|
|
107
|
+
|
|
108
|
+
warn <<~WARNING
|
|
109
|
+
|
|
110
|
+
WARNING: Coverage timestamps are missing. Time-based staleness checks were skipped.
|
|
111
|
+
Files may appear "ok" even if source code is newer than the coverage data.
|
|
112
|
+
Check your coverage tool configuration to ensure timestamps are recorded.
|
|
113
|
+
WARNING
|
|
50
114
|
end
|
|
51
115
|
end
|
|
52
116
|
end
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base_command'
|
|
4
|
-
require_relative '../presenters/
|
|
5
|
-
require_relative '../table_formatter'
|
|
4
|
+
require_relative '../presenters/coverage_payload_presenter'
|
|
5
|
+
require_relative '../formatters/table_formatter'
|
|
6
6
|
|
|
7
7
|
module CovLoupe
|
|
8
8
|
module Commands
|
|
9
9
|
class UncoveredCommand < BaseCommand
|
|
10
10
|
def execute(args)
|
|
11
11
|
handle_with_path(args, 'uncovered') do |path|
|
|
12
|
-
presenter = Presenters::
|
|
12
|
+
presenter = Presenters::CoveragePayloadPresenter.new(model: model, path: path,
|
|
13
|
+
payload_method: :uncovered_for)
|
|
13
14
|
data = presenter.absolute_payload
|
|
14
15
|
break if emit_structured_format_with_optional_source?(data, model, path)
|
|
15
16
|
|
|
16
|
-
relative_path = presenter.relative_path
|
|
17
|
+
relative_path = convert_text(presenter.relative_path)
|
|
17
18
|
summary = data['summary']
|
|
18
19
|
|
|
19
20
|
puts "File: #{relative_path}"
|
|
@@ -32,7 +33,8 @@ module CovLoupe
|
|
|
32
33
|
puts TableFormatter.format(
|
|
33
34
|
headers: headers,
|
|
34
35
|
rows: rows,
|
|
35
|
-
alignments: [:right]
|
|
36
|
+
alignments: [:right],
|
|
37
|
+
output_chars: config.output_chars
|
|
36
38
|
)
|
|
37
39
|
end
|
|
38
40
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base_command'
|
|
4
|
-
require_relative '../predicate_evaluator'
|
|
4
|
+
require_relative '../config/predicate_evaluator'
|
|
5
5
|
|
|
6
6
|
module CovLoupe
|
|
7
7
|
module Commands
|
|
@@ -35,6 +35,9 @@ module CovLoupe
|
|
|
35
35
|
code = file_path
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Ensure no extra arguments remain
|
|
39
|
+
reject_extra_args(args, 'validate')
|
|
40
|
+
|
|
38
41
|
# Evaluate the predicate
|
|
39
42
|
result = if inline_mode
|
|
40
43
|
PredicateEvaluator.evaluate_code(code, model)
|
|
@@ -51,8 +54,13 @@ module CovLoupe
|
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
private def handle_predicate_error(error)
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
# Convert error message to ASCII if in ascii mode
|
|
58
|
+
message = convert_text(error.message)
|
|
59
|
+
warn "Predicate error: #{message}"
|
|
60
|
+
if config.error_mode == :debug
|
|
61
|
+
backtrace = error.backtrace.first(5).map { |line| convert_text(line) }
|
|
62
|
+
warn backtrace.join("\n")
|
|
63
|
+
end
|
|
56
64
|
exit 2
|
|
57
65
|
end
|
|
58
66
|
end
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require_relative 'base_command'
|
|
5
|
-
require_relative '../table_formatter'
|
|
5
|
+
require_relative '../formatters/table_formatter'
|
|
6
6
|
|
|
7
7
|
module CovLoupe
|
|
8
8
|
module Commands
|
|
9
9
|
class VersionCommand < BaseCommand
|
|
10
|
-
def execute(
|
|
10
|
+
def execute(args)
|
|
11
|
+
reject_extra_args(args, 'version')
|
|
11
12
|
@gem_root = File.expand_path('../../..', __dir__)
|
|
12
13
|
|
|
13
14
|
if config.format == :table
|
|
@@ -16,9 +17,10 @@ module CovLoupe
|
|
|
16
17
|
'Gem Root' => @gem_root,
|
|
17
18
|
'Documentation' => 'README.md and docs/user/**/*.md in gem root'
|
|
18
19
|
}
|
|
19
|
-
puts TableFormatter.format_vertical(data)
|
|
20
|
+
puts TableFormatter.format_vertical(data, output_chars: config.output_chars)
|
|
20
21
|
else
|
|
21
|
-
puts CovLoupe::Formatters.format(version_info, config.format
|
|
22
|
+
puts CovLoupe::Formatters.format(version_info, config.format,
|
|
23
|
+
output_chars: config.output_chars)
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
|
|
@@ -12,10 +12,12 @@ module CovLoupe
|
|
|
12
12
|
:source_context,
|
|
13
13
|
:color,
|
|
14
14
|
:error_mode,
|
|
15
|
-
:
|
|
15
|
+
:raise_on_stale,
|
|
16
16
|
:tracked_globs,
|
|
17
17
|
:log_file,
|
|
18
18
|
:show_version,
|
|
19
|
+
:mode,
|
|
20
|
+
:output_chars,
|
|
19
21
|
keyword_init: true
|
|
20
22
|
) do
|
|
21
23
|
# Set sensible defaults - ALL SYMBOLS FOR ENUMS
|
|
@@ -28,11 +30,16 @@ module CovLoupe
|
|
|
28
30
|
source_context: 2,
|
|
29
31
|
color: $stdout.tty?,
|
|
30
32
|
error_mode: :log,
|
|
31
|
-
|
|
33
|
+
raise_on_stale: false,
|
|
32
34
|
tracked_globs: nil,
|
|
33
35
|
log_file: nil,
|
|
34
|
-
show_version: false
|
|
36
|
+
show_version: false,
|
|
37
|
+
mode: :cli,
|
|
38
|
+
output_chars: :default
|
|
35
39
|
)
|
|
40
|
+
# Default to empty array (show all files in resultset and don't look for files lacking coverage data)
|
|
41
|
+
# Users should set COV_LOUPE_OPTS to match SimpleCov track_files patterns
|
|
42
|
+
tracked_globs = [] if tracked_globs.nil?
|
|
36
43
|
super
|
|
37
44
|
end
|
|
38
45
|
|
|
@@ -41,7 +48,7 @@ module CovLoupe
|
|
|
41
48
|
{
|
|
42
49
|
root: root,
|
|
43
50
|
resultset: resultset,
|
|
44
|
-
|
|
51
|
+
raise_on_stale: raise_on_stale,
|
|
45
52
|
tracked_globs: tracked_globs
|
|
46
53
|
}
|
|
47
54
|
end
|
|
@@ -49,7 +56,8 @@ module CovLoupe
|
|
|
49
56
|
# Convenience method for SourceFormatter initialization
|
|
50
57
|
def formatter_options
|
|
51
58
|
{
|
|
52
|
-
color_enabled: color
|
|
59
|
+
color_enabled: color,
|
|
60
|
+
output_chars: output_chars
|
|
53
61
|
}
|
|
54
62
|
end
|
|
55
63
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'logger'
|
|
4
|
+
|
|
5
|
+
module CovLoupe
|
|
6
|
+
# Encapsulates per-request configuration such as error handling and logging.
|
|
7
|
+
AppContext = Data.define(:error_handler, :log_target, :mode, :app_config, :logger) do
|
|
8
|
+
def initialize(error_handler:, log_target: nil, mode: :library, app_config: nil, logger: nil)
|
|
9
|
+
logger ||= Logger.new(target: log_target, mode: mode)
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Overrides Data#with to handle derived state.
|
|
14
|
+
#
|
|
15
|
+
# Since the `logger` depends on `log_target` and `mode`, we must ensure
|
|
16
|
+
# it is regenerated if either of those fields are changed. Otherwise,
|
|
17
|
+
# the new instance would point to the old logger (e.g. logging to the
|
|
18
|
+
# wrong file).
|
|
19
|
+
def with(**kwargs)
|
|
20
|
+
target_changed = kwargs.key?(:log_target) && kwargs[:log_target] != log_target
|
|
21
|
+
mode_changed = kwargs.key?(:mode) && kwargs[:mode] != mode
|
|
22
|
+
|
|
23
|
+
if target_changed || mode_changed
|
|
24
|
+
target = kwargs.fetch(:log_target, log_target)
|
|
25
|
+
new_mode = kwargs.fetch(:mode, mode)
|
|
26
|
+
kwargs[:logger] = Logger.new(target: target, mode: new_mode)
|
|
27
|
+
end
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def mcp_mode?
|
|
32
|
+
mode == :mcp
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cli_mode?
|
|
36
|
+
mode == :cli
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def library_mode?
|
|
40
|
+
mode == :library
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CovLoupe
|
|
4
|
+
# A pluggable boolean type converter for OptionParser.
|
|
5
|
+
# Accepts various string representations of true/false and converts them to boolean values.
|
|
6
|
+
#
|
|
7
|
+
# Usage with OptionParser:
|
|
8
|
+
# parser.accept(BooleanType) { |v| BooleanType.parse(v) }
|
|
9
|
+
# parser.on('--flag [BOOLEAN]', BooleanType) { |v| config.flag = v }
|
|
10
|
+
#
|
|
11
|
+
# Supported values (case-insensitive):
|
|
12
|
+
# true: yes, y, true, t, on, +, 1
|
|
13
|
+
# false: no, n, false, f, off, -, 0
|
|
14
|
+
# nil: treated as true (for bare flags like --flag)
|
|
15
|
+
#
|
|
16
|
+
# Examples:
|
|
17
|
+
# --flag → true
|
|
18
|
+
# --flag=yes → true
|
|
19
|
+
# --flag=no → false
|
|
20
|
+
# --flag yes → true
|
|
21
|
+
# --flag false → false
|
|
22
|
+
class BooleanType
|
|
23
|
+
# Values that map to true
|
|
24
|
+
TRUE_VALUES = %w[yes y true t on + 1].freeze
|
|
25
|
+
|
|
26
|
+
# Values that map to false
|
|
27
|
+
FALSE_VALUES = %w[no n false f off - 0].freeze
|
|
28
|
+
|
|
29
|
+
# All valid boolean string values
|
|
30
|
+
VALID_VALUES = TRUE_VALUES.zip(FALSE_VALUES).flatten.freeze # %w{yes no y n true false t f on off + - 1 0 }
|
|
31
|
+
|
|
32
|
+
# String representation for help messages ('yes/no/true/false/t/f/on/off/y/n/+/-/1/0')
|
|
33
|
+
BOOLEAN_VALUES_DISPLAY_STRING = VALID_VALUES.join('/').freeze
|
|
34
|
+
|
|
35
|
+
# Pattern object for OptionParser.
|
|
36
|
+
# Proc objects get treated as blocks, and Module instances are rejected outright,
|
|
37
|
+
# so we expose a singleton that only responds to #match like a regex.
|
|
38
|
+
IS_BOOLEAN_STRING_VALUE = Object.new
|
|
39
|
+
IS_BOOLEAN_STRING_VALUE.define_singleton_method(:match) do |value|
|
|
40
|
+
BooleanType.valid?(value) ? value : nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# Parse a string value into a boolean.
|
|
45
|
+
#
|
|
46
|
+
# @param value [String, nil] The value to parse
|
|
47
|
+
# @return [Boolean] true or false
|
|
48
|
+
# @raise [ArgumentError] if the value is not a valid boolean string
|
|
49
|
+
def parse(value)
|
|
50
|
+
# nil means bare flag (e.g., --flag without a value) → true
|
|
51
|
+
return true if value.nil?
|
|
52
|
+
|
|
53
|
+
normalized = value.to_s.strip.downcase
|
|
54
|
+
|
|
55
|
+
return true if TRUE_VALUES.include?(normalized)
|
|
56
|
+
return false if FALSE_VALUES.include?(normalized)
|
|
57
|
+
|
|
58
|
+
raise ArgumentError, "invalid boolean value: #{value.inspect}. " \
|
|
59
|
+
"Valid values: #{VALID_VALUES.join(', ')}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if a value is a valid boolean string.
|
|
63
|
+
#
|
|
64
|
+
# @param value [String, nil] The value to check
|
|
65
|
+
# @return [Boolean] true if valid, false otherwise
|
|
66
|
+
def valid?(value)
|
|
67
|
+
return true if value.nil?
|
|
68
|
+
|
|
69
|
+
VALID_VALUES.include?(value.to_s.strip.downcase)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Pattern matching for OptionParser (called via ===)
|
|
73
|
+
# This is called to determine if a token should be consumed as the option's argument.
|
|
74
|
+
# Returning nil signals OptionParser to NOT consume the token.
|
|
75
|
+
#
|
|
76
|
+
# @param value [String, nil] The value to match
|
|
77
|
+
# @return [Boolean, nil] The parsed boolean if valid, or nil to reject the token
|
|
78
|
+
def ===(value)
|
|
79
|
+
# nil means optional argument not provided - accept as match
|
|
80
|
+
return true if value.nil?
|
|
81
|
+
|
|
82
|
+
# Only consume the token if it's a valid boolean value
|
|
83
|
+
# This prevents consuming subcommand names or other arguments
|
|
84
|
+
return true if valid?(value)
|
|
85
|
+
|
|
86
|
+
# Invalid value - don't consume it, let OptionParser treat it as the next argument
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module CovLoupe
|
|
7
|
+
class Logger
|
|
8
|
+
DEFAULT_LOG_FILESPEC = './cov_loupe.log'
|
|
9
|
+
FALLBACK_LOG_FILE = 'COV-LOUPE-LOG-ERROR.log'
|
|
10
|
+
|
|
11
|
+
attr_reader :target
|
|
12
|
+
|
|
13
|
+
def initialize(target:, mode: :library)
|
|
14
|
+
@mode = mode
|
|
15
|
+
@target = target
|
|
16
|
+
@init_error = nil
|
|
17
|
+
@stderr_warning_emitted = false
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
@logger = build_logger(target)
|
|
21
|
+
rescue => e
|
|
22
|
+
@init_error = e
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def info(msg)
|
|
27
|
+
log_with_level(:info, msg)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def warn(msg)
|
|
31
|
+
log_with_level(:warn, msg)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def error(msg)
|
|
35
|
+
log_with_level(:error, msg)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Safe logging that never raises - use when logging should not interrupt execution.
|
|
39
|
+
def safe_log(msg)
|
|
40
|
+
info(msg)
|
|
41
|
+
rescue
|
|
42
|
+
# Silently ignore all logging failures
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private def log_with_level(level, msg)
|
|
46
|
+
if @init_error
|
|
47
|
+
handle_logging_error(@init_error, msg)
|
|
48
|
+
else
|
|
49
|
+
@logger.send(level, msg)
|
|
50
|
+
end
|
|
51
|
+
rescue => e
|
|
52
|
+
handle_logging_error(e, msg)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private def build_logger(target)
|
|
56
|
+
io_or_path = case target
|
|
57
|
+
when 'stdout' then $stdout
|
|
58
|
+
when 'stderr' then $stderr
|
|
59
|
+
else
|
|
60
|
+
path = target || DEFAULT_LOG_FILESPEC
|
|
61
|
+
File.expand_path(path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
::Logger.new(io_or_path).tap do |l|
|
|
65
|
+
l.formatter = ->(severity, datetime, _progname, msg) { "[#{datetime.iso8601}] #{severity}: #{msg}\n" }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private def handle_logging_error(error, original_msg)
|
|
70
|
+
write_fallback_file(error, original_msg)
|
|
71
|
+
warn_stderr_once if @mode == :cli
|
|
72
|
+
rescue
|
|
73
|
+
# Silently ignore all fallback failures
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private def write_fallback_file(error, original_msg)
|
|
77
|
+
File.open(FALLBACK_LOG_FILE, 'a') do |f|
|
|
78
|
+
timestamp = Time.now.iso8601
|
|
79
|
+
f.puts "[#{timestamp}] MODE:#{@mode} ERROR:#{error.message} MSG:#{original_msg}"
|
|
80
|
+
end
|
|
81
|
+
rescue
|
|
82
|
+
# Best effort - ignore write failures
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private def warn_stderr_once
|
|
86
|
+
return if @stderr_warning_emitted
|
|
87
|
+
|
|
88
|
+
@stderr_warning_emitted = true
|
|
89
|
+
$stderr.puts "Warning: Logging failed. See #{FALLBACK_LOG_FILE} for details."
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|