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,247 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'time'
|
|
4
|
-
require 'pathname'
|
|
5
|
-
require_relative 'errors'
|
|
6
|
-
require_relative 'util'
|
|
7
|
-
|
|
8
|
-
module CovLoupe
|
|
9
|
-
# Lightweight service object to check staleness of coverage vs. sources
|
|
10
|
-
class StalenessChecker
|
|
11
|
-
MODES = [:off, :error].freeze
|
|
12
|
-
|
|
13
|
-
def initialize(root:, resultset:, mode: :off, tracked_globs: nil, timestamp: nil)
|
|
14
|
-
@root = File.absolute_path(root || '.')
|
|
15
|
-
@resultset = resultset
|
|
16
|
-
@mode = (mode || :off).to_sym
|
|
17
|
-
@tracked_globs = tracked_globs
|
|
18
|
-
@cov_timestamp = timestamp
|
|
19
|
-
@resultset_path = nil
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def off?
|
|
23
|
-
@mode == :off
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Raise CoverageDataStaleError if stale (only in error mode)
|
|
27
|
-
def check_file!(file_abs, coverage_lines)
|
|
28
|
-
return if off?
|
|
29
|
-
|
|
30
|
-
d = compute_file_staleness_details(file_abs, coverage_lines)
|
|
31
|
-
# For single-file checks, missing files with recorded coverage count as stale
|
|
32
|
-
# via length mismatch; project-level checks also handle deleted files explicitly.
|
|
33
|
-
if d[:newer] || d[:len_mismatch]
|
|
34
|
-
raise CoverageDataStaleError.new(
|
|
35
|
-
nil,
|
|
36
|
-
nil,
|
|
37
|
-
file_path: rel(file_abs),
|
|
38
|
-
file_mtime: d[:file_mtime],
|
|
39
|
-
cov_timestamp: d[:coverage_timestamp],
|
|
40
|
-
src_len: d[:src_len],
|
|
41
|
-
cov_len: d[:cov_len],
|
|
42
|
-
resultset_path: resultset_path
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Compute whether a specific file appears stale relative to coverage.
|
|
48
|
-
# Ignores mode and never raises; returns true when:
|
|
49
|
-
# - the file is missing/deleted, or
|
|
50
|
-
# - the file mtime is newer than the coverage timestamp, or
|
|
51
|
-
# - the source line count differs from the coverage lines array length (when present).
|
|
52
|
-
def stale_for_file?(file_abs, coverage_lines)
|
|
53
|
-
d = compute_file_staleness_details(file_abs, coverage_lines)
|
|
54
|
-
return 'M' unless d[:exists]
|
|
55
|
-
return 'T' if d[:newer]
|
|
56
|
-
return 'L' if d[:len_mismatch]
|
|
57
|
-
|
|
58
|
-
false
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Raise CoverageDataProjectStaleError if any covered file is newer or if
|
|
62
|
-
# tracked files are missing from coverage, or coverage includes deleted files.
|
|
63
|
-
def check_project!(coverage_map)
|
|
64
|
-
return if off?
|
|
65
|
-
|
|
66
|
-
ts = coverage_timestamp
|
|
67
|
-
coverage_files = coverage_map.keys
|
|
68
|
-
|
|
69
|
-
newer, deleted = compute_newer_and_deleted_files(coverage_files, ts)
|
|
70
|
-
missing = compute_missing_files(coverage_files)
|
|
71
|
-
|
|
72
|
-
return if newer.empty? && missing.empty? && deleted.empty?
|
|
73
|
-
|
|
74
|
-
raise CoverageDataProjectStaleError.new(
|
|
75
|
-
nil,
|
|
76
|
-
nil,
|
|
77
|
-
cov_timestamp: ts,
|
|
78
|
-
newer_files: newer,
|
|
79
|
-
missing_files: missing,
|
|
80
|
-
deleted_files: deleted,
|
|
81
|
-
resultset_path: resultset_path
|
|
82
|
-
)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
private def compute_newer_and_deleted_files(coverage_files, timestamp)
|
|
86
|
-
existing, deleted_abs = coverage_files.partition { |abs| File.file?(abs) }
|
|
87
|
-
|
|
88
|
-
newer = existing
|
|
89
|
-
.select { |abs| File.mtime(abs).to_i > timestamp.to_i }
|
|
90
|
-
.map { |abs| rel(abs) }
|
|
91
|
-
deleted = deleted_abs.map { |abs| rel(abs) }
|
|
92
|
-
|
|
93
|
-
[newer, deleted]
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Identifies tracked files that are missing from coverage.
|
|
97
|
-
# Returns array of relative paths for files matched by tracked_globs but not in coverage.
|
|
98
|
-
private def compute_missing_files(coverage_files)
|
|
99
|
-
return [] unless @tracked_globs && Array(@tracked_globs).any?
|
|
100
|
-
|
|
101
|
-
patterns = Array(@tracked_globs).map { |g| File.absolute_path(g, @root) }
|
|
102
|
-
tracked = patterns
|
|
103
|
-
.flat_map { |p| Dir.glob(p, File::FNM_EXTGLOB | File::FNM_PATHNAME) }
|
|
104
|
-
.select { |p| File.file?(p) }
|
|
105
|
-
|
|
106
|
-
covered_set = coverage_files.to_set
|
|
107
|
-
tracked.reject { |abs| covered_set.include?(abs) }.map { |abs| rel(abs) }
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
private def coverage_timestamp
|
|
111
|
-
@cov_timestamp || 0
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
private def resultset_path
|
|
115
|
-
@resultset_path ||= CovUtil.find_resultset(@root, resultset: @resultset)
|
|
116
|
-
rescue
|
|
117
|
-
nil
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
private def safe_count_lines(path)
|
|
121
|
-
return 0 unless File.file?(path)
|
|
122
|
-
|
|
123
|
-
File.foreach(path).count
|
|
124
|
-
rescue
|
|
125
|
-
0
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
private def missing_trailing_newline?(path)
|
|
129
|
-
return false unless File.file?(path)
|
|
130
|
-
|
|
131
|
-
File.open(path, 'rb') do |f|
|
|
132
|
-
size = f.size
|
|
133
|
-
return false if size.zero?
|
|
134
|
-
|
|
135
|
-
f.seek(-1, IO::SEEK_END)
|
|
136
|
-
f.getbyte != 0x0A
|
|
137
|
-
end
|
|
138
|
-
rescue
|
|
139
|
-
false
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
private def rel(path)
|
|
143
|
-
# Handle relative vs absolute path mismatches that cause ArgumentError
|
|
144
|
-
Pathname.new(path).relative_path_from(Pathname.new(@root)).to_s
|
|
145
|
-
rescue ArgumentError
|
|
146
|
-
# Path is outside the project root or has a different prefix type, fall back to absolute path
|
|
147
|
-
path.to_s
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Centralized computation of staleness-related details for a single file.
|
|
151
|
-
# Returns a Hash with keys:
|
|
152
|
-
# :exists, :file_mtime, :coverage_timestamp, :cov_len, :src_len, :newer, :len_mismatch
|
|
153
|
-
private def compute_file_staleness_details(file_abs, coverage_lines)
|
|
154
|
-
coverage_ts = coverage_timestamp
|
|
155
|
-
|
|
156
|
-
exists = File.file?(file_abs)
|
|
157
|
-
file_mtime = exists ? File.mtime(file_abs) : nil
|
|
158
|
-
|
|
159
|
-
cov_len = coverage_lines.respond_to?(:length) ? coverage_lines.length : 0
|
|
160
|
-
src_len = exists ? safe_count_lines(file_abs) : 0
|
|
161
|
-
|
|
162
|
-
# Adjust source line count to handle edge cases with missing trailing newlines
|
|
163
|
-
adjusted_src_len = adjust_line_count_for_missing_newline(
|
|
164
|
-
file_abs: file_abs,
|
|
165
|
-
exists: exists,
|
|
166
|
-
cov_len: cov_len,
|
|
167
|
-
src_len: src_len
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
# Check if the source file has been modified since coverage was generated
|
|
171
|
-
len_mismatch = length_mismatch?(cov_len, adjusted_src_len)
|
|
172
|
-
newer = check_file_newer_than_coverage(file_mtime, coverage_ts, len_mismatch)
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
exists: exists,
|
|
176
|
-
file_mtime: file_mtime,
|
|
177
|
-
coverage_timestamp: coverage_ts,
|
|
178
|
-
cov_len: cov_len,
|
|
179
|
-
src_len: src_len,
|
|
180
|
-
newer: newer,
|
|
181
|
-
len_mismatch: len_mismatch
|
|
182
|
-
}
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Adjusts the source line count to account for files missing trailing newlines.
|
|
186
|
-
#
|
|
187
|
-
# Why this edge case exists:
|
|
188
|
-
# - File.foreach counts lines by separator (typically \n)
|
|
189
|
-
# - For a file with no trailing newline, File.foreach still counts all lines correctly
|
|
190
|
-
# - However, some editors or file operations may report one extra line when checking
|
|
191
|
-
# if the file doesn't end with a newline
|
|
192
|
-
# - SimpleCov's coverage array length matches the logical line count (excluding trailing newline)
|
|
193
|
-
# - If src_len is exactly one more than cov_len AND the file is missing a trailing newline,
|
|
194
|
-
# we adjust src_len down by 1 to match SimpleCov's convention
|
|
195
|
-
#
|
|
196
|
-
# Example: A file with "line1\nline2\nline3" (no final \n)
|
|
197
|
-
# - File.foreach counts: 3 lines
|
|
198
|
-
# - SimpleCov coverage array length: 3
|
|
199
|
-
# - No adjustment needed
|
|
200
|
-
#
|
|
201
|
-
# However, in certain edge cases where the file system or parsing reports an extra line:
|
|
202
|
-
# - Reported line count: 4
|
|
203
|
-
# - SimpleCov coverage array length: 3
|
|
204
|
-
# - Missing trailing newline: true
|
|
205
|
-
# - Adjustment: 4 - 1 = 3 (now matches)
|
|
206
|
-
private def adjust_line_count_for_missing_newline(file_abs:, exists:, cov_len:, src_len:)
|
|
207
|
-
# Only adjust if:
|
|
208
|
-
# 1. File exists (can't check newlines for missing files)
|
|
209
|
-
# 2. Coverage data is present (cov_len > 0)
|
|
210
|
-
# 3. Source has exactly one more line than coverage
|
|
211
|
-
# 4. File is missing a trailing newline
|
|
212
|
-
needs_adjusting =
|
|
213
|
-
exists && cov_len.positive? && src_len == cov_len + 1 && missing_trailing_newline?(file_abs)
|
|
214
|
-
needs_adjusting ? src_len - 1 : src_len
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# Checks if the source line count differs from the coverage line count.
|
|
218
|
-
#
|
|
219
|
-
# Why this check exists:
|
|
220
|
-
# - When a file is modified after coverage is generated, the line count often changes
|
|
221
|
-
# - A mismatch indicates the coverage data is stale and no longer represents the current file
|
|
222
|
-
# - We only flag as mismatch when coverage data exists (cov_len > 0)
|
|
223
|
-
#
|
|
224
|
-
# Note: Empty coverage (cov_len == 0) is not considered a mismatch, as it may represent
|
|
225
|
-
# files that were never executed or files that are legitimately empty.
|
|
226
|
-
private def length_mismatch?(cov_len, adjusted_src_len)
|
|
227
|
-
cov_len.positive? && adjusted_src_len != cov_len
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
# Determines if a file has been modified more recently than the coverage timestamp.
|
|
231
|
-
#
|
|
232
|
-
# Why this check exists:
|
|
233
|
-
# - Files modified after coverage generation may have behavioral changes not captured
|
|
234
|
-
# - However, if there's already a length mismatch, we prioritize that as the staleness indicator
|
|
235
|
-
# - This prevents double-flagging: if lines changed, the file is already stale (length mismatch)
|
|
236
|
-
#
|
|
237
|
-
# The logic: newer &&= !len_mismatch means:
|
|
238
|
-
# - If len_mismatch is true, set newer to false (length mismatch takes precedence)
|
|
239
|
-
# - This way, staleness is categorized as either 'T' (time-based) OR 'L' (length-based), not both
|
|
240
|
-
private def check_file_newer_than_coverage(file_mtime, coverage_ts, len_mismatch)
|
|
241
|
-
newer = !!(file_mtime && file_mtime.to_i > coverage_ts.to_i)
|
|
242
|
-
# If there's a length mismatch, don't also flag as "newer" - the mismatch is more specific
|
|
243
|
-
newer &&= !len_mismatch
|
|
244
|
-
newer
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
end
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module CovLoupe
|
|
4
|
-
# General-purpose table formatter with box-drawing characters
|
|
5
|
-
# Used by commands to create consistent formatted output
|
|
6
|
-
class TableFormatter
|
|
7
|
-
# Format data as a table with box-drawing characters
|
|
8
|
-
# @param headers [Array<String>] Column headers
|
|
9
|
-
# @param rows [Array<Array>] Data rows (each row is an array of cell values)
|
|
10
|
-
# @param alignments [Array<Symbol>] Column alignments (:left, :right, :center)
|
|
11
|
-
# @return [String] Formatted table
|
|
12
|
-
def self.format(headers:, rows:, alignments: nil)
|
|
13
|
-
return 'No data to display' if rows.empty?
|
|
14
|
-
|
|
15
|
-
alignments ||= [:left] * headers.size
|
|
16
|
-
all_rows = [headers] + rows.map { |row| row.map(&:to_s) }
|
|
17
|
-
|
|
18
|
-
# Calculate column widths
|
|
19
|
-
widths = headers.size.times.map do |col|
|
|
20
|
-
all_rows.map { |row| row[col].to_s.length }.max
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
lines = []
|
|
24
|
-
lines << border_line(widths, '┌', '┬', '┐')
|
|
25
|
-
lines << data_row(headers, widths, alignments)
|
|
26
|
-
lines << border_line(widths, '├', '┼', '┤')
|
|
27
|
-
rows.each { |row| lines << data_row(row, widths, alignments) }
|
|
28
|
-
lines << border_line(widths, '└', '┴', '┘')
|
|
29
|
-
|
|
30
|
-
lines.join("\n")
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Format a single key-value table (vertical layout)
|
|
34
|
-
# @param data [Hash] Key-value pairs
|
|
35
|
-
# @return [String] Formatted table
|
|
36
|
-
def self.format_vertical(data)
|
|
37
|
-
rows = data.map { |k, v| [k.to_s, v.to_s] }
|
|
38
|
-
format(headers: ['Key', 'Value'], rows: rows, alignments: [:left, :left])
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private_class_method def self.border_line(widths, left, mid, right)
|
|
42
|
-
segments = widths.map { |w| '─' * (w + 2) }
|
|
43
|
-
left + segments.join(mid) + right
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private_class_method def self.data_row(cells, widths, alignments)
|
|
47
|
-
formatted = cells.each_with_index.map do |cell, i|
|
|
48
|
-
align_cell(cell.to_s, widths[i], alignments[i])
|
|
49
|
-
end
|
|
50
|
-
"│ #{formatted.join(' │ ')} │"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private_class_method def self.align_cell(content, width, alignment)
|
|
54
|
-
case alignment
|
|
55
|
-
when :right
|
|
56
|
-
content.rjust(width)
|
|
57
|
-
when :center
|
|
58
|
-
content.center(width)
|
|
59
|
-
else # :left
|
|
60
|
-
content.ljust(width)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../model'
|
|
4
|
-
require_relative '../base_tool'
|
|
5
|
-
require_relative '../presenters/project_coverage_presenter'
|
|
6
|
-
|
|
7
|
-
module CovLoupe
|
|
8
|
-
module Tools
|
|
9
|
-
class AllFilesCoverageTool < BaseTool
|
|
10
|
-
description <<~DESC
|
|
11
|
-
Use this when the user wants coverage percentages for every tracked file in the project.
|
|
12
|
-
Do not use this for single-file stats; prefer coverage.summary or coverage.uncovered_lines for that.
|
|
13
|
-
Inputs: optional project root, alternate .resultset path, sort order, staleness mode, and tracked_globs to alert on new files.
|
|
14
|
-
Output: JSON {"files": [{"file","covered","total","percentage","stale"}, ...], "counts": {"total", "ok", "stale"}} sorted as requested. "stale" is a string ('M', 'T', 'L') or false.
|
|
15
|
-
Examples: "List files with the lowest coverage"; "Show repo coverage sorted descending".
|
|
16
|
-
DESC
|
|
17
|
-
input_schema(**coverage_schema(
|
|
18
|
-
additional_properties: {
|
|
19
|
-
sort_order: {
|
|
20
|
-
type: 'string',
|
|
21
|
-
description: 'Sort order for coverage percentages.' \
|
|
22
|
-
"'ascending' highlights the riskiest files first.",
|
|
23
|
-
default: 'ascending',
|
|
24
|
-
enum: ['ascending', 'descending']
|
|
25
|
-
},
|
|
26
|
-
tracked_globs: TRACKED_GLOBS_PROPERTY
|
|
27
|
-
}
|
|
28
|
-
))
|
|
29
|
-
class << self
|
|
30
|
-
def call(root: '.', resultset: nil, sort_order: 'ascending', staleness: :off,
|
|
31
|
-
tracked_globs: nil, error_mode: 'log', server_context:)
|
|
32
|
-
with_error_handling('AllFilesCoverageTool', error_mode: error_mode) do
|
|
33
|
-
# Convert string inputs from MCP to symbols for internal use
|
|
34
|
-
sort_order_sym = sort_order.to_sym
|
|
35
|
-
staleness_sym = staleness.to_sym
|
|
36
|
-
|
|
37
|
-
model = CoverageModel.new(root: root, resultset: resultset, staleness: staleness_sym,
|
|
38
|
-
tracked_globs: tracked_globs)
|
|
39
|
-
presenter = Presenters::ProjectCoveragePresenter.new(
|
|
40
|
-
model: model,
|
|
41
|
-
sort_order: sort_order_sym,
|
|
42
|
-
check_stale: (staleness_sym == :error),
|
|
43
|
-
tracked_globs: tracked_globs
|
|
44
|
-
)
|
|
45
|
-
respond_json(presenter.relativized_payload, name: 'all_files_coverage.json')
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
data/lib/cov_loupe/util.rb
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'resolvers/resolver_factory'
|
|
4
|
-
|
|
5
|
-
module CovLoupe
|
|
6
|
-
RESULTSET_CANDIDATES = [
|
|
7
|
-
'.resultset.json',
|
|
8
|
-
'coverage/.resultset.json',
|
|
9
|
-
'tmp/.resultset.json'
|
|
10
|
-
].freeze
|
|
11
|
-
|
|
12
|
-
DEFAULT_LOG_FILESPEC = './cov_loupe.log'
|
|
13
|
-
|
|
14
|
-
module CovUtil
|
|
15
|
-
module_function def log(msg)
|
|
16
|
-
log_file = CovLoupe.active_log_file
|
|
17
|
-
|
|
18
|
-
case log_file
|
|
19
|
-
when 'stdout'
|
|
20
|
-
$stdout.puts "[#{Time.now.iso8601}] #{msg}"
|
|
21
|
-
when 'stderr'
|
|
22
|
-
$stderr.puts "[#{Time.now.iso8601}] #{msg}"
|
|
23
|
-
else
|
|
24
|
-
# Handles both nil (default) and custom file paths
|
|
25
|
-
path_to_log = log_file || DEFAULT_LOG_FILESPEC
|
|
26
|
-
File.open(File.expand_path(path_to_log), 'a') { |f| f.puts "[#{Time.now.iso8601}] #{msg}" }
|
|
27
|
-
end
|
|
28
|
-
rescue => e
|
|
29
|
-
# Fallback to stderr if file logging fails, but suppress in MCP mode
|
|
30
|
-
# to avoid interfering with JSON-RPC protocol
|
|
31
|
-
unless CovLoupe.context.mcp_mode?
|
|
32
|
-
begin
|
|
33
|
-
$stderr.puts "[#{Time.now.iso8601}] LOGGING ERROR: #{e.message}"
|
|
34
|
-
$stderr.puts "[#{Time.now.iso8601}] #{msg}"
|
|
35
|
-
rescue
|
|
36
|
-
# Silently ignore only stderr fallback failures
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Safe logging that never raises - use when logging should not interrupt execution.
|
|
42
|
-
# Unlike `log`, this method guarantees it will never propagate exceptions.
|
|
43
|
-
module_function def safe_log(msg)
|
|
44
|
-
log(msg)
|
|
45
|
-
rescue
|
|
46
|
-
# Silently ignore all logging failures
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
module_function def find_resultset(root, resultset: nil)
|
|
50
|
-
Resolvers::ResolverFactory.find_resultset(root, resultset: resultset)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
module_function def lookup_lines(cov, file_abs)
|
|
54
|
-
Resolvers::ResolverFactory.lookup_lines(cov, file_abs)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
module_function def summary(arr)
|
|
58
|
-
total = 0
|
|
59
|
-
covered = 0
|
|
60
|
-
arr.compact.each do |hits|
|
|
61
|
-
total += 1
|
|
62
|
-
covered += 1 if hits.to_i > 0
|
|
63
|
-
end
|
|
64
|
-
percentage = total.zero? ? 100.0 : ((covered.to_f * 100.0 / total) * 100).round / 100.0
|
|
65
|
-
{ 'covered' => covered, 'total' => total, 'percentage' => percentage }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
module_function def uncovered(arr)
|
|
69
|
-
out = []
|
|
70
|
-
|
|
71
|
-
arr.each_with_index do |hits, i|
|
|
72
|
-
next if hits.nil?
|
|
73
|
-
|
|
74
|
-
out << (i + 1) if hits.to_i.zero?
|
|
75
|
-
end
|
|
76
|
-
out
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
module_function def detailed(arr)
|
|
80
|
-
rows = []
|
|
81
|
-
arr.each_with_index do |hits, i|
|
|
82
|
-
h = hits&.to_i
|
|
83
|
-
rows << { 'line' => i + 1, 'hits' => h, 'covered' => h.positive? } if h
|
|
84
|
-
end
|
|
85
|
-
rows
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# MCP Server Protocol Integration Tests
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This document describes the comprehensive integration tests added for the SimpleCov MCP server protocol in `spec/integration_spec.rb`.
|
|
6
|
-
|
|
7
|
-
## Test Coverage
|
|
8
|
-
|
|
9
|
-
The integration tests spawn the actual MCP server as a subprocess and communicate with it via JSON-RPC over stdio, testing the complete end-to-end protocol implementation.
|
|
10
|
-
|
|
11
|
-
### Tests Added (12 total)
|
|
12
|
-
|
|
13
|
-
1. **starts MCP server without errors** - Verifies the server starts and responds to basic requests without NameError or other initialization issues
|
|
14
|
-
2. **handles tools/list request** - Confirms all 8 expected tools are properly registered
|
|
15
|
-
3. **executes coverage_summary_tool via JSON-RPC** - Tests single-file coverage summary queries
|
|
16
|
-
4. **executes all_files_coverage_tool via JSON-RPC** - Tests project-wide coverage listing
|
|
17
|
-
5. **executes uncovered_lines_tool via JSON-RPC** - Tests uncovered line detection
|
|
18
|
-
6. **executes help_tool via JSON-RPC** - Tests help/documentation retrieval
|
|
19
|
-
7. **executes version_tool via JSON-RPC** - Tests version information queries
|
|
20
|
-
8. **handles error responses for invalid tool calls** - Verifies graceful error handling
|
|
21
|
-
9. **handles malformed JSON-RPC requests** - Tests robustness against invalid input
|
|
22
|
-
10. **respects --log-file configuration in MCP mode** - Tests logging configuration
|
|
23
|
-
11. **prohibits stdout logging in MCP mode** - Ensures stdout isn't corrupted
|
|
24
|
-
12. **handles multiple sequential requests** - Tests statelessness and multi-request handling
|
|
25
|
-
|
|
26
|
-
## Why These Tests Are Critical
|
|
27
|
-
|
|
28
|
-
### Issue #1 from Analysis: Missing `require 'optparse'`
|
|
29
|
-
|
|
30
|
-
The critical bug (missing `require 'optparse'` in `lib/cov_loupe.rb:110`) was not caught by existing tests because:
|
|
31
|
-
|
|
32
|
-
- Unit tests loaded the full gem which transitively required optparse through the CLI
|
|
33
|
-
- MCP tools were tested in-process without spawning the server
|
|
34
|
-
- No integration tests verified the MCP server startup sequence
|
|
35
|
-
|
|
36
|
-
### What These Tests Catch
|
|
37
|
-
|
|
38
|
-
* ✅ **Server Initialization Errors**: NameError, LoadError, missing requires
|
|
39
|
-
* ✅ **Protocol Compliance**: Valid JSON-RPC request/response format
|
|
40
|
-
* ✅ **Tool Registration**: All tools properly configured and accessible
|
|
41
|
-
* ✅ **Data Accuracy**: Coverage data correctly passed from fixtures
|
|
42
|
-
* ✅ **Error Handling**: Graceful responses for invalid requests
|
|
43
|
-
* ✅ **Configuration**: Environment variables and options properly handled
|
|
44
|
-
* ✅ **Statelessness**: Multiple requests handled independently
|
|
45
|
-
* ✅ **Stream Integrity**: Stdout not corrupted by logging
|
|
46
|
-
|
|
47
|
-
## Test Architecture
|
|
48
|
-
|
|
49
|
-
### Helper Methods
|
|
50
|
-
|
|
51
|
-
- **`run_mcp_request(request_hash, timeout: 5)`**: Spawns MCP server, sends JSON-RPC request, returns stdout/stderr/status
|
|
52
|
-
- **`parse_jsonrpc_response(output)`**: Extracts JSON-RPC response from output (handles mixed stderr/stdout)
|
|
53
|
-
|
|
54
|
-
### Test Fixtures
|
|
55
|
-
|
|
56
|
-
Uses `spec/fixtures/project1/` with known coverage data:
|
|
57
|
-
- `lib/foo.rb`: 66.67% coverage (2/3 lines, line 2 uncovered)
|
|
58
|
-
- `lib/bar.rb`: 33.33% coverage (1/3 lines)
|
|
59
|
-
|
|
60
|
-
### Test Execution
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
# Run all MCP integration tests
|
|
64
|
-
bundle exec rspec spec/integration_spec.rb --tag slow
|
|
65
|
-
|
|
66
|
-
# Run specific integration test
|
|
67
|
-
bundle exec rspec spec/integration_spec.rb:363
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Performance
|
|
71
|
-
|
|
72
|
-
- Total execution time: ~2.1 seconds for all 12 tests
|
|
73
|
-
- Tagged with `:slow` to allow exclusion from quick test runs
|
|
74
|
-
- Uses `Open3.popen3` for subprocess management
|
|
75
|
-
- 5-second timeout per request (configurable)
|
|
76
|
-
|
|
77
|
-
## Coverage Impact
|
|
78
|
-
|
|
79
|
-
These tests increased the overall test count from 272 to 284 examples and improved confidence in the MCP server mode, which is the primary use case for AI assistant integration.
|
|
80
|
-
|
|
81
|
-
### Before Integration Tests
|
|
82
|
-
- 272 examples
|
|
83
|
-
- Missing `require 'optparse'` bug went undetected
|
|
84
|
-
- MCP server mode untested end-to-end
|
|
85
|
-
|
|
86
|
-
### After Integration Tests
|
|
87
|
-
- 284 examples
|
|
88
|
-
- MCP server startup verified
|
|
89
|
-
- Full JSON-RPC protocol tested
|
|
90
|
-
- Would catch Issue #1 immediately
|
|
91
|
-
|
|
92
|
-
## Future Enhancements
|
|
93
|
-
|
|
94
|
-
Potential additions:
|
|
95
|
-
- Test connection lifecycle (startup, multiple sessions, shutdown)
|
|
96
|
-
- Test concurrent requests (if supported)
|
|
97
|
-
- Test large coverage datasets (performance)
|
|
98
|
-
- Test network transport (if added)
|
|
99
|
-
- Test authentication/authorization (if added)
|
|
100
|
-
|
|
101
|
-
## Related Files
|
|
102
|
-
|
|
103
|
-
- `spec/integration_spec.rb` - Main integration test file (lines 308-683)
|
|
104
|
-
- `lib/cov_loupe.rb` - Entry point with mode detection
|
|
105
|
-
- `lib/cov_loupe/mcp_server.rb` - MCP server implementation
|
|
106
|
-
- `exe/cov-loupe` - Executable entry point
|
|
107
|
-
|
|
108
|
-
## References
|
|
109
|
-
|
|
110
|
-
- [MCP Protocol Specification](https://modelcontextprotocol.io/)
|
|
111
|
-
- [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)
|
data/spec/TIMESTAMPS.md
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# Test Timestamp Documentation
|
|
2
|
-
|
|
3
|
-
This document explains the timestamp constants used throughout the test suite for consistent and documented test data.
|
|
4
|
-
|
|
5
|
-
## Constants (defined in `spec_helper.rb`)
|
|
6
|
-
|
|
7
|
-
### `FIXTURE_COVERAGE_TIMESTAMP = 1_720_000_000`
|
|
8
|
-
- **Human readable**: 2024-07-03 16:26:40 UTC (July 3rd, 2024)
|
|
9
|
-
- **Purpose**: The "generated" timestamp for coverage data in `spec/fixtures/project1/coverage/.resultset.json`
|
|
10
|
-
- **Usage**: Used in tests that verify timestamp parsing and calculations with realistic coverage data
|
|
11
|
-
|
|
12
|
-
### `VERY_OLD_TIMESTAMP = 0`
|
|
13
|
-
- **Human readable**: 1970-01-01 00:00:00 UTC (Unix epoch)
|
|
14
|
-
- **Purpose**: Simulates extremely stale coverage data (much older than any real file)
|
|
15
|
-
- **Usage**: Used in staleness tests to force stale coverage scenarios
|
|
16
|
-
|
|
17
|
-
### `TEST_FILE_TIMESTAMP = 1_000`
|
|
18
|
-
- **Human readable**: 1970-01-01 00:16:40 UTC (16 minutes and 40 seconds after epoch)
|
|
19
|
-
- **Purpose**: Used for stale error formatting tests to create predictable time deltas
|
|
20
|
-
- **Usage**: Creates a 1000-second (16m 40s) difference from `VERY_OLD_TIMESTAMP` for delta calculations
|
|
21
|
-
|
|
22
|
-
## Conversion Reference
|
|
23
|
-
|
|
24
|
-
To convert timestamps for debugging:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
# Unix timestamp to human readable
|
|
28
|
-
date -d @1720000000
|
|
29
|
-
# Wed Jul 3 16:26:40 UTC 2024
|
|
30
|
-
|
|
31
|
-
# Human readable to Unix timestamp
|
|
32
|
-
date -d "2024-07-03 16:26:40 UTC" +%s
|
|
33
|
-
# 1720000000
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Why These Values?
|
|
37
|
-
|
|
38
|
-
- **Realistic but static**: `FIXTURE_COVERAGE_TIMESTAMP` is a realistic recent date that won't change
|
|
39
|
-
- **Predictable deltas**: The differences between timestamps create predictable test scenarios
|
|
40
|
-
- **Clear intent**: Named constants make it obvious what each timestamp represents in tests
|
|
41
|
-
|
|
42
|
-
## Files Using These Constants
|
|
43
|
-
|
|
44
|
-
- `spec/util_spec.rb` - Tests timestamp parsing from fixture
|
|
45
|
-
- `spec/model_staleness_spec.rb` - Tests staleness detection logic
|
|
46
|
-
- `spec/errors_stale_spec.rb` - Tests stale error message formatting
|
|
47
|
-
- `spec/cli_error_spec.rb` - Tests CLI error handling for stale coverage
|
|
48
|
-
- `spec/fixtures/project1/coverage/.resultset.json` - Contains the actual timestamp data
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'spec_helper'
|
|
4
|
-
require 'cov_loupe/tools/all_files_coverage_tool'
|
|
5
|
-
|
|
6
|
-
RSpec.describe CovLoupe::Tools::AllFilesCoverageTool do
|
|
7
|
-
subject(:call_tool) { described_class.call(root: root, server_context: server_context) }
|
|
8
|
-
|
|
9
|
-
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
10
|
-
let(:server_context) { instance_double('ServerContext').as_null_object }
|
|
11
|
-
|
|
12
|
-
before do
|
|
13
|
-
setup_mcp_response_stub
|
|
14
|
-
model = instance_double(CovLoupe::CoverageModel)
|
|
15
|
-
allow(CovLoupe::CoverageModel).to receive(:new).and_return(model)
|
|
16
|
-
|
|
17
|
-
payload = {
|
|
18
|
-
'files' => [
|
|
19
|
-
{ 'file' => 'lib/foo.rb', 'percentage' => 100.0, 'covered' => 10, 'total' => 10,
|
|
20
|
-
'stale' => false },
|
|
21
|
-
{ 'file' => 'lib/bar.rb', 'percentage' => 50.0, 'covered' => 5, 'total' => 10,
|
|
22
|
-
'stale' => true }
|
|
23
|
-
],
|
|
24
|
-
'counts' => { 'total' => 2, 'ok' => 1, 'stale' => 1 }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
presenter = instance_double(CovLoupe::Presenters::ProjectCoveragePresenter)
|
|
28
|
-
allow(CovLoupe::Presenters::ProjectCoveragePresenter).to receive(:new).and_return(presenter)
|
|
29
|
-
allow(presenter).to receive(:relativized_payload).and_return(payload)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
it_behaves_like 'an MCP tool that returns text JSON'
|
|
34
|
-
|
|
35
|
-
it 'returns all files coverage data with counts' do
|
|
36
|
-
response = call_tool
|
|
37
|
-
data, _item = expect_mcp_text_json(response, expected_keys: ['files', 'counts'])
|
|
38
|
-
|
|
39
|
-
files = data['files']
|
|
40
|
-
counts = data['counts']
|
|
41
|
-
|
|
42
|
-
expect(files.length).to eq(2)
|
|
43
|
-
expect(counts).to include('total' => 2).or include(total: 2)
|
|
44
|
-
expect(files.map { |f| f['file'] }).to eq(['lib/foo.rb', 'lib/bar.rb'])
|
|
45
|
-
|
|
46
|
-
# ok + stale equals total
|
|
47
|
-
ok = counts[:ok] || counts['ok']
|
|
48
|
-
stale = counts[:stale] || counts['stale']
|
|
49
|
-
total = counts[:total] || counts['total']
|
|
50
|
-
expect(ok + stale).to eq(total)
|
|
51
|
-
expect(stale).to eq(1)
|
|
52
|
-
end
|
|
53
|
-
end
|