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,76 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'spec_helper'
|
|
4
|
-
|
|
5
|
-
RSpec.describe CovLoupe::Formatters do
|
|
6
|
-
describe '.formatter_for' do
|
|
7
|
-
it 'returns a lambda for known format' do
|
|
8
|
-
expect(described_class.formatter_for(:json)).to respond_to(:call)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
it 'raises ArgumentError for unknown format' do
|
|
12
|
-
expect { described_class.formatter_for(:unknown) }
|
|
13
|
-
.to raise_error(ArgumentError, /Unknown format: unknown/)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
describe '.ensure_requirements_for' do
|
|
18
|
-
it 'requires the library if needed' do
|
|
19
|
-
# We rely on the fact that 'yaml' is in FORMAT_REQUIRES
|
|
20
|
-
expect(described_class).to receive(:require).with('yaml')
|
|
21
|
-
described_class.ensure_requirements_for(:yaml)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
it 'does nothing if no requirement' do
|
|
25
|
-
expect(described_class).not_to receive(:require)
|
|
26
|
-
described_class.ensure_requirements_for(:json) # JSON already required by app
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
describe '.format' do
|
|
31
|
-
let(:obj) { { 'foo' => 'bar' } }
|
|
32
|
-
|
|
33
|
-
[
|
|
34
|
-
[:json, '{"foo":"bar"}', :eq],
|
|
35
|
-
[:pretty_json, "{\n \"foo\": \"bar\"\n}", :include],
|
|
36
|
-
[:table, { 'foo' => 'bar' }, :eq],
|
|
37
|
-
[:yaml, "---\nfoo: bar\n", :include]
|
|
38
|
-
].each do |format, expected, matcher|
|
|
39
|
-
it "formats as #{format}" do
|
|
40
|
-
result = described_class.format(obj, format)
|
|
41
|
-
expect(result).to send(matcher, expected)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
context 'when a required gem is missing' do
|
|
46
|
-
before do
|
|
47
|
-
error = LoadError.new('cannot load such file -- awesome_print')
|
|
48
|
-
allow(described_class).to receive(:require).with('awesome_print').and_raise(error)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
it 'raises a helpful LoadError' do
|
|
52
|
-
expect { described_class.format(obj, :awesome_print) }
|
|
53
|
-
.to raise_error(LoadError, /requires the 'awesome_print' gem/)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
context 'when awesome_print is available' do
|
|
58
|
-
before do
|
|
59
|
-
# Stub require on the module for ensure_requirements_for
|
|
60
|
-
allow(described_class).to receive(:require).with('awesome_print')
|
|
61
|
-
|
|
62
|
-
# Stub global require for the lambda's internal require
|
|
63
|
-
allow(Kernel).to receive(:require).and_call_original
|
|
64
|
-
allow(Kernel).to receive(:require).with('awesome_print').and_return(true)
|
|
65
|
-
|
|
66
|
-
# Mock .ai on the object
|
|
67
|
-
allow(obj).to receive(:ai).and_return('awesome output')
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
it 'formats using awesome_print' do
|
|
71
|
-
result = described_class.format(obj, :awesome_print)
|
|
72
|
-
expect(result).to eq('awesome output')
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'spec_helper'
|
|
4
|
-
|
|
5
|
-
RSpec.describe CovLoupe::Presenters::BaseCoveragePresenter do
|
|
6
|
-
let(:model) { instance_double(CovLoupe::CoverageModel) }
|
|
7
|
-
let(:path) { 'lib/foo.rb' }
|
|
8
|
-
let(:presenter) { described_class.new(model: model, path: path) }
|
|
9
|
-
|
|
10
|
-
describe '#initialize' do
|
|
11
|
-
it 'sets model and path' do
|
|
12
|
-
expect(presenter.model).to eq(model)
|
|
13
|
-
expect(presenter.path).to eq(path)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
describe '#absolute_payload' do
|
|
18
|
-
it 'raises NotImplementedError because build_payload is abstract' do
|
|
19
|
-
expect { presenter.absolute_payload }.to raise_error(NotImplementedError)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
context 'with a concrete implementation' do
|
|
24
|
-
let(:concrete_class) do
|
|
25
|
-
Class.new(described_class) do
|
|
26
|
-
# Provide a concrete implementation of the abstract build_payload method
|
|
27
|
-
# for testing the BaseCoveragePresenter functionality
|
|
28
|
-
def build_payload
|
|
29
|
-
{ 'file' => path, 'data' => 'test' }
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
let(:presenter) { concrete_class.new(model: model, path: path) }
|
|
34
|
-
let(:payload_with_stale) { { 'file' => path, 'data' => 'test', 'stale' => false } }
|
|
35
|
-
|
|
36
|
-
before do
|
|
37
|
-
allow(model).to receive(:staleness_for).with(path).and_return(false)
|
|
38
|
-
allow(model).to receive(:relativize).with(payload_with_stale).and_return(payload_with_stale)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
describe '#absolute_payload' do
|
|
42
|
-
it 'merges stale status into payload' do
|
|
43
|
-
expect(presenter.absolute_payload).to include('stale' => false)
|
|
44
|
-
expect(presenter.absolute_payload).to include('data' => 'test')
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
it 'caches the result' do
|
|
48
|
-
r1 = presenter.absolute_payload
|
|
49
|
-
r2 = presenter.absolute_payload
|
|
50
|
-
expect(r1).to equal(r2)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
describe '#relativized_payload' do
|
|
55
|
-
it 'delegates to model.relativize' do
|
|
56
|
-
expect(model).to receive(:relativize).with(presenter.absolute_payload)
|
|
57
|
-
presenter.relativized_payload
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
it 'caches the result' do
|
|
61
|
-
presenter.relativized_payload
|
|
62
|
-
expect(model).to have_received(:relativize).once
|
|
63
|
-
presenter.relativized_payload
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
describe '#stale' do
|
|
68
|
-
it 'delegates to absolute_payload' do
|
|
69
|
-
expect(presenter.stale).to be(false)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
describe '#relative_path' do
|
|
74
|
-
it 'delegates to relativized_payload' do
|
|
75
|
-
expect(presenter.relative_path).to eq('lib/foo.rb')
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'spec_helper'
|
|
4
|
-
|
|
5
|
-
RSpec.describe CovLoupe::CoverageModel do
|
|
6
|
-
subject(:model) { described_class.new(root: root) }
|
|
7
|
-
|
|
8
|
-
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
describe 'initialization error handling' do
|
|
12
|
-
it 'raises FileError when File.read raises Errno::ENOENT directly' do
|
|
13
|
-
# Stub find_resultset to return a path, but File.read to raise ENOENT
|
|
14
|
-
allow(CovLoupe::CovUtil).to receive(:find_resultset)
|
|
15
|
-
.and_return('/some/path/.resultset.json')
|
|
16
|
-
allow(JSON).to receive(:load_file).with('/some/path/.resultset.json')
|
|
17
|
-
.and_raise(Errno::ENOENT, 'No such file')
|
|
18
|
-
|
|
19
|
-
expect do
|
|
20
|
-
described_class.new(root: root, resultset: '/some/path/.resultset.json')
|
|
21
|
-
end.to raise_error(CovLoupe::FileError, /Coverage data not found/)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
it 'raises ResultsetNotFoundError when resultset file does not exist' do
|
|
25
|
-
expect do
|
|
26
|
-
described_class.new(root: root, resultset: '/nonexistent/path/.resultset.json')
|
|
27
|
-
end.to raise_error(CovLoupe::ResultsetNotFoundError, /Specified resultset not found/)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
describe 'raw_for' do
|
|
32
|
-
it 'returns absolute file and lines array' do
|
|
33
|
-
data = model.raw_for('lib/foo.rb')
|
|
34
|
-
expect(data['file']).to eq(File.expand_path('lib/foo.rb', root))
|
|
35
|
-
expect(data['lines']).to eq([1, 0, nil, 2])
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
describe 'summary_for' do
|
|
40
|
-
it 'computes covered/total/percentage' do
|
|
41
|
-
data = model.summary_for('lib/foo.rb')
|
|
42
|
-
expect(data['summary']['total']).to eq(3)
|
|
43
|
-
expect(data['summary']['covered']).to eq(2)
|
|
44
|
-
expect(data['summary']['percentage']).to be_within(0.01).of(66.67)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
describe '#relativize' do
|
|
49
|
-
it 'returns a copy with file paths relative to the root' do
|
|
50
|
-
data = model.summary_for('lib/foo.rb')
|
|
51
|
-
relative = model.relativize(data)
|
|
52
|
-
|
|
53
|
-
expect(relative['file']).to eq('lib/foo.rb')
|
|
54
|
-
expect(data['file']).not_to eq(relative['file'])
|
|
55
|
-
expect(relative).not_to equal(data)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
describe 'uncovered_for' do
|
|
60
|
-
it 'lists uncovered executable line numbers' do
|
|
61
|
-
data = model.uncovered_for('lib/foo.rb')
|
|
62
|
-
expect(data['uncovered']).to eq([2])
|
|
63
|
-
expect(data['summary']['total']).to eq(3)
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
describe 'detailed_for' do
|
|
68
|
-
it 'returns per-line details for non-nil lines' do
|
|
69
|
-
data = model.detailed_for('lib/foo.rb')
|
|
70
|
-
expect(data['lines']).to eq([
|
|
71
|
-
{ 'line' => 1, 'hits' => 1, 'covered' => true },
|
|
72
|
-
{ 'line' => 2, 'hits' => 0, 'covered' => false },
|
|
73
|
-
{ 'line' => 4, 'hits' => 2, 'covered' => true }
|
|
74
|
-
])
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
describe 'staleness_for' do
|
|
79
|
-
it 'returns the staleness character for a file' do
|
|
80
|
-
checker = instance_double(CovLoupe::StalenessChecker, off?: false)
|
|
81
|
-
allow(CovLoupe::StalenessChecker).to receive(:new).and_return(checker)
|
|
82
|
-
allow(checker).to receive(:stale_for_file?) do |file_abs, _|
|
|
83
|
-
if file_abs == File.expand_path('lib/foo.rb', root)
|
|
84
|
-
'T'
|
|
85
|
-
else
|
|
86
|
-
false
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
expect(model.staleness_for('lib/foo.rb')).to eq('T')
|
|
91
|
-
expect(model.staleness_for('lib/bar.rb')).to be(false)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
it 'returns false when an exception occurs during staleness check' do
|
|
95
|
-
# Stub the checker to raise an error
|
|
96
|
-
checker = instance_double(CovLoupe::StalenessChecker, off?: false)
|
|
97
|
-
allow(CovLoupe::StalenessChecker).to receive(:new).and_return(checker)
|
|
98
|
-
allow(checker).to receive(:stale_for_file?)
|
|
99
|
-
.and_raise(StandardError, 'Something went wrong')
|
|
100
|
-
|
|
101
|
-
# The rescue clause should catch the error and return false
|
|
102
|
-
expect(model.staleness_for('lib/foo.rb')).to be(false)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
it 'returns false when coverage data is not found for the file' do
|
|
106
|
-
# Try to get staleness for a file that doesn't exist in coverage
|
|
107
|
-
expect(model.staleness_for('lib/nonexistent.rb')).to be(false)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
describe 'all_files' do
|
|
112
|
-
it 'sorts descending (default) by percentage then by file path' do
|
|
113
|
-
files = model.all_files
|
|
114
|
-
# lib/foo.rb has 66.67%, lib/bar.rb has 33.33%
|
|
115
|
-
expect(files.first['file']).to eq(File.expand_path('lib/foo.rb', root))
|
|
116
|
-
expect(files.first['percentage']).to be_within(0.01).of(66.67)
|
|
117
|
-
expect(files.last['file']).to eq(File.expand_path('lib/bar.rb', root))
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
it 'sorts ascending by percentage then by file path' do
|
|
121
|
-
files = model.all_files(sort_order: :ascending)
|
|
122
|
-
expect(files.first['file']).to eq(File.expand_path('lib/bar.rb', root))
|
|
123
|
-
expect(files.first['percentage']).to be_within(0.01).of(33.33)
|
|
124
|
-
expect(files.last['file']).to eq(File.expand_path('lib/foo.rb', root))
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
it 'filters rows when tracked_globs are provided' do
|
|
128
|
-
files = model.all_files(tracked_globs: ['lib/foo.rb'])
|
|
129
|
-
|
|
130
|
-
expect(files.length).to eq(1)
|
|
131
|
-
expect(files.first['file']).to eq(File.expand_path('lib/foo.rb', root))
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
it 'combines results from multiple tracked_globs patterns' do
|
|
135
|
-
abs_bar = File.expand_path('lib/bar.rb', root)
|
|
136
|
-
|
|
137
|
-
files = model.all_files(tracked_globs: ['lib/foo.rb', abs_bar])
|
|
138
|
-
|
|
139
|
-
expect(files.map { |f| f['file'] }).to contain_exactly(
|
|
140
|
-
File.expand_path('lib/foo.rb', root),
|
|
141
|
-
abs_bar
|
|
142
|
-
)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
it 'handles files with paths that cannot be relativized' do
|
|
146
|
-
# Create a custom row with a path from a Windows-style drive (C:/) that will cause ArgumentError
|
|
147
|
-
# when trying to make it relative to a Unix-style root
|
|
148
|
-
custom_rows = [
|
|
149
|
-
{
|
|
150
|
-
'file' => 'C:/Windows/system32/file.rb',
|
|
151
|
-
'percentage' => 100.0,
|
|
152
|
-
'covered' => 10,
|
|
153
|
-
'total' => 10,
|
|
154
|
-
'stale' => false
|
|
155
|
-
}
|
|
156
|
-
]
|
|
157
|
-
|
|
158
|
-
# This should trigger the ArgumentError rescue in filter_rows_by_globs
|
|
159
|
-
# When the path cannot be made relative (different path types), it falls back to using the absolute path
|
|
160
|
-
output = model.format_table(custom_rows, tracked_globs: ['C:/Windows/**/*.rb'])
|
|
161
|
-
|
|
162
|
-
# The file should be included because the absolute path fallback matches the glob
|
|
163
|
-
expect(output).to include('C:/Windows/system32/file.rb')
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
describe '#project_totals' do
|
|
168
|
-
it 'aggregates coverage totals across all files' do
|
|
169
|
-
totals = model.project_totals
|
|
170
|
-
|
|
171
|
-
expect(totals['lines']).to include('total' => 6, 'covered' => 3, 'uncovered' => 3)
|
|
172
|
-
expect(totals['percentage']).to be_within(0.01).of(50.0)
|
|
173
|
-
expect(totals['files']).to include('total' => 2)
|
|
174
|
-
expect(totals['files']['ok'] + totals['files']['stale']).to eq(totals['files']['total'])
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
it 'respects tracked_globs filtering' do
|
|
178
|
-
totals = model.project_totals(tracked_globs: ['lib/foo.rb'])
|
|
179
|
-
|
|
180
|
-
expect(totals['lines']).to include('total' => 3, 'covered' => 2, 'uncovered' => 1)
|
|
181
|
-
expect(totals['files']).to include('total' => 1)
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
describe 'resolve method error handling' do
|
|
186
|
-
it 'raises FileError when coverage_lines is nil after lookup' do
|
|
187
|
-
# Stub lookup_lines to return nil without raising
|
|
188
|
-
allow(CovLoupe::CovUtil).to receive(:lookup_lines).and_return(nil)
|
|
189
|
-
|
|
190
|
-
expect do
|
|
191
|
-
model.summary_for('lib/nonexistent.rb')
|
|
192
|
-
end.to raise_error(CovLoupe::FileError, /No coverage data found for file/)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
it 'converts Errno::ENOENT to FileNotFoundError during resolve' do
|
|
196
|
-
# We need to trigger Errno::ENOENT inside the resolve method
|
|
197
|
-
# Stub the checker's check_file! method to raise Errno::ENOENT
|
|
198
|
-
checker = instance_double(CovLoupe::StalenessChecker, off?: false)
|
|
199
|
-
allow(CovLoupe::StalenessChecker).to receive(:new).and_return(checker)
|
|
200
|
-
allow(checker).to receive(:check_file!)
|
|
201
|
-
.and_raise(Errno::ENOENT, 'No such file or directory')
|
|
202
|
-
|
|
203
|
-
# Create a model with staleness checking enabled to trigger the check_file! call
|
|
204
|
-
stale_model = described_class.new(root: root, staleness: :error)
|
|
205
|
-
|
|
206
|
-
expect do
|
|
207
|
-
stale_model.summary_for('lib/foo.rb')
|
|
208
|
-
end.to raise_error(CovLoupe::FileNotFoundError, /File not found/)
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
it 'raises FileError when lookup_lines raises RuntimeError' do
|
|
212
|
-
allow(CovLoupe::CovUtil).to receive(:lookup_lines)
|
|
213
|
-
.and_raise(RuntimeError, 'Could not find coverage data')
|
|
214
|
-
|
|
215
|
-
expect do
|
|
216
|
-
model.summary_for('lib/some_file.rb')
|
|
217
|
-
end.to raise_error(CovLoupe::FileError, /No coverage data found for file/)
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
describe 'resultset directory handling' do
|
|
222
|
-
it 'accepts a directory containing .resultset.json' do
|
|
223
|
-
model = described_class.new(root: root, resultset: 'coverage')
|
|
224
|
-
data = model.summary_for('lib/foo.rb')
|
|
225
|
-
expect(data['summary']['total']).to eq(3)
|
|
226
|
-
expect(data['summary']['covered']).to eq(2)
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
describe 'branch-only coverage resultsets' do
|
|
231
|
-
let(:branch_root) { (FIXTURES_DIR / 'branch_only_project').to_s }
|
|
232
|
-
let(:branch_model) { described_class.new(root: branch_root) }
|
|
233
|
-
|
|
234
|
-
it 'computes summaries by synthesizing branch data' do
|
|
235
|
-
data = branch_model.summary_for('lib/branch_only.rb')
|
|
236
|
-
|
|
237
|
-
expect(data['summary']['total']).to eq(5)
|
|
238
|
-
expect(data['summary']['covered']).to eq(3)
|
|
239
|
-
expect(data['summary']['percentage']).to be_within(0.01).of(60.0)
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
it 'returns detailed data using branch-derived hits' do
|
|
243
|
-
data = branch_model.detailed_for('lib/branch_only.rb')
|
|
244
|
-
|
|
245
|
-
expect(data['lines']).to eq([
|
|
246
|
-
{ 'line' => 6, 'hits' => 3, 'covered' => true },
|
|
247
|
-
{ 'line' => 7, 'hits' => 0, 'covered' => false },
|
|
248
|
-
{ 'line' => 13, 'hits' => 0, 'covered' => false },
|
|
249
|
-
{ 'line' => 14, 'hits' => 2, 'covered' => true },
|
|
250
|
-
{ 'line' => 16, 'hits' => 2, 'covered' => true }
|
|
251
|
-
])
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
it 'identifies uncovered lines based on branch hits' do
|
|
255
|
-
data = branch_model.uncovered_for('lib/branch_only.rb')
|
|
256
|
-
|
|
257
|
-
expect(data['uncovered']).to eq([7, 13])
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
it 'includes branch-only files in all_files results' do
|
|
261
|
-
files = branch_model.all_files(sort_order: :ascending)
|
|
262
|
-
branch_path = File.expand_path('lib/branch_only.rb', branch_root)
|
|
263
|
-
another_path = File.expand_path('lib/another.rb', branch_root)
|
|
264
|
-
|
|
265
|
-
expect(files.map { |f| f['file'] }).to contain_exactly(branch_path, another_path)
|
|
266
|
-
|
|
267
|
-
branch_entry = files.find { |f| f['file'] == branch_path }
|
|
268
|
-
another_entry = files.find { |f| f['file'] == another_path }
|
|
269
|
-
|
|
270
|
-
expect(branch_entry['total']).to eq(5)
|
|
271
|
-
expect(branch_entry['covered']).to eq(3)
|
|
272
|
-
expect(another_entry['total']).to eq(1)
|
|
273
|
-
expect(another_entry['covered']).to eq(0)
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
describe 'multiple suites in resultset' do
|
|
278
|
-
let(:resultset_path) { '/tmp/multi_suite_resultset.json' }
|
|
279
|
-
let(:suite_a_cov) do
|
|
280
|
-
{
|
|
281
|
-
File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, nil, 2] }
|
|
282
|
-
}
|
|
283
|
-
end
|
|
284
|
-
let(:suite_b_cov) do
|
|
285
|
-
{
|
|
286
|
-
File.join(root, 'lib', 'bar.rb') => { 'lines' => [0, 1, 1] }
|
|
287
|
-
}
|
|
288
|
-
end
|
|
289
|
-
let(:resultset) do
|
|
290
|
-
{
|
|
291
|
-
'RSpec' => { 'timestamp' => 100, 'coverage' => suite_a_cov },
|
|
292
|
-
'Cucumber' => { 'timestamp' => 200, 'coverage' => suite_b_cov }
|
|
293
|
-
}
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
let(:shared_file) { File.join(root, 'lib', 'foo.rb') }
|
|
297
|
-
let(:suite_a_cov_combined) do
|
|
298
|
-
{
|
|
299
|
-
shared_file => { 'lines' => [1, 0, nil, 0] }
|
|
300
|
-
}
|
|
301
|
-
end
|
|
302
|
-
let(:suite_b_cov_combined) do
|
|
303
|
-
{
|
|
304
|
-
shared_file => { 'lines' => [0, 3, nil, 1] }
|
|
305
|
-
}
|
|
306
|
-
end
|
|
307
|
-
let(:resultset_combined) do
|
|
308
|
-
{
|
|
309
|
-
'RSpec' => { 'timestamp' => 100, 'coverage' => suite_a_cov_combined },
|
|
310
|
-
'Cucumber' => { 'timestamp' => 150, 'coverage' => suite_b_cov_combined }
|
|
311
|
-
}
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
before do
|
|
315
|
-
allow(CovLoupe::CovUtil).to receive(:find_resultset).and_wrap_original do
|
|
316
|
-
|original, search_root, resultset: nil|
|
|
317
|
-
root_match = File.absolute_path(search_root) == File.absolute_path(root)
|
|
318
|
-
resultset_empty = resultset.nil? || resultset.to_s.empty?
|
|
319
|
-
if root_match && resultset_empty
|
|
320
|
-
resultset_path
|
|
321
|
-
else
|
|
322
|
-
original.call(search_root, resultset: resultset)
|
|
323
|
-
end
|
|
324
|
-
end
|
|
325
|
-
# This line might need to be removed as we now mock JSON.load_file directly
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
it 'merges coverage data from multiple suites while keeping latest timestamp' do
|
|
329
|
-
allow(JSON).to receive(:load_file).with(resultset_path).and_return(resultset)
|
|
330
|
-
|
|
331
|
-
model = described_class.new(root: root)
|
|
332
|
-
files = model.all_files(sort_order: :ascending)
|
|
333
|
-
|
|
334
|
-
expect(files.map { |f| File.basename(f['file']) }).to include('foo.rb', 'bar.rb')
|
|
335
|
-
|
|
336
|
-
timestamp = model.instance_variable_get(:@cov_timestamp)
|
|
337
|
-
expect(timestamp).to eq(200)
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
it 'combines coverage arrays when the same file appears in multiple suites' do
|
|
341
|
-
allow(JSON).to receive(:load_file).with(resultset_path).and_return(resultset_combined)
|
|
342
|
-
|
|
343
|
-
model = described_class.new(root: root)
|
|
344
|
-
detailed = model.detailed_for('lib/foo.rb')
|
|
345
|
-
hits_by_line = detailed['lines'].each_with_object({}) do |row, acc|
|
|
346
|
-
acc[row['line']] = row['hits']
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
expect(hits_by_line[1]).to eq(1)
|
|
350
|
-
expect(hits_by_line[2]).to eq(3)
|
|
351
|
-
expect(hits_by_line[4]).to eq(1)
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
describe 'format_table' do
|
|
356
|
-
it 'returns a formatted table string with all files coverage data' do
|
|
357
|
-
output = model.format_table
|
|
358
|
-
|
|
359
|
-
# Should contain table structure
|
|
360
|
-
expect(output).to include('┌', '┬', '┐', '│', '├', '┼', '┤', '└', '┴', '┘')
|
|
361
|
-
|
|
362
|
-
# Should contain headers
|
|
363
|
-
expect(output).to include('File', '%', 'Covered', 'Total', 'Stale')
|
|
364
|
-
|
|
365
|
-
# Should contain file data
|
|
366
|
-
expect(output).to include('lib/foo.rb', 'lib/bar.rb')
|
|
367
|
-
|
|
368
|
-
# Should contain summary
|
|
369
|
-
expect(output).to include('Files: total', ', ok ', ', stale ')
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
it 'returns "No coverage data found" when rows is empty' do
|
|
373
|
-
rows = []
|
|
374
|
-
output = model.format_table(rows)
|
|
375
|
-
expect(output).to eq('No coverage data found')
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
it 'accepts custom rows parameter' do
|
|
379
|
-
custom_rows = [
|
|
380
|
-
{
|
|
381
|
-
'file' => '/path/to/file1.rb',
|
|
382
|
-
'percentage' => 100.0,
|
|
383
|
-
'covered' => 10,
|
|
384
|
-
'total' => 10,
|
|
385
|
-
'stale' => false
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
'file' => '/path/to/file2.rb',
|
|
389
|
-
'percentage' => 50.0,
|
|
390
|
-
'covered' => 5,
|
|
391
|
-
'total' => 10,
|
|
392
|
-
'stale' => 'M'
|
|
393
|
-
},
|
|
394
|
-
{
|
|
395
|
-
'file' => '/path/to/file3.rb',
|
|
396
|
-
'percentage' => 75.0,
|
|
397
|
-
'covered' => 15,
|
|
398
|
-
'total' => 20,
|
|
399
|
-
'stale' => 'T'
|
|
400
|
-
}
|
|
401
|
-
]
|
|
402
|
-
|
|
403
|
-
output = model.format_table(custom_rows)
|
|
404
|
-
|
|
405
|
-
expect(output).to include('file1.rb')
|
|
406
|
-
expect(output).to include('file2.rb')
|
|
407
|
-
expect(output).to include('file3.rb')
|
|
408
|
-
expect(output).to include('100.00')
|
|
409
|
-
expect(output).to include('50.00')
|
|
410
|
-
expect(output).to include('75.00')
|
|
411
|
-
expect(output).to include('M')
|
|
412
|
-
expect(output).to include('T')
|
|
413
|
-
expect(output).not_to include('!')
|
|
414
|
-
staleness_msg = 'Staleness: M = Missing file, T = Timestamp (source newer), ' \
|
|
415
|
-
'L = Line count mismatch'
|
|
416
|
-
expect(output).to include(staleness_msg)
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
it 'accepts sort_order parameter' do
|
|
420
|
-
# Test that sort_order parameter is passed through correctly
|
|
421
|
-
output_asc = model.format_table(sort_order: :ascending)
|
|
422
|
-
output_desc = model.format_table(sort_order: :descending)
|
|
423
|
-
|
|
424
|
-
# Both should be valid table outputs
|
|
425
|
-
expect(output_asc).to include('┌')
|
|
426
|
-
expect(output_desc).to include('┌')
|
|
427
|
-
expect(output_asc).to include('Files: total')
|
|
428
|
-
expect(output_desc).to include('Files: total')
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
it 'sorts table output correctly when provided with custom rows' do
|
|
432
|
-
# Get all files data to use as custom rows
|
|
433
|
-
all_files_data = model.all_files
|
|
434
|
-
|
|
435
|
-
# Test ascending sort with custom rows
|
|
436
|
-
output_asc = model.format_table(all_files_data, sort_order: :ascending)
|
|
437
|
-
lines_asc = output_asc.split("\n")
|
|
438
|
-
bar_line_asc = lines_asc.find { |line| line.include?('bar.rb') }
|
|
439
|
-
foo_line_asc = lines_asc.find { |line| line.include?('foo.rb') }
|
|
440
|
-
|
|
441
|
-
# In ascending order, bar.rb (33.33%) should come before foo.rb (66.67%)
|
|
442
|
-
expect(lines_asc.index(bar_line_asc)).to be < lines_asc.index(foo_line_asc)
|
|
443
|
-
|
|
444
|
-
# Test descending sort with custom rows
|
|
445
|
-
output_desc = model.format_table(all_files_data, sort_order: :descending)
|
|
446
|
-
lines_desc = output_desc.split("\n")
|
|
447
|
-
bar_line_desc = lines_desc.find { |line| line.include?('bar.rb') }
|
|
448
|
-
foo_line_desc = lines_desc.find { |line| line.include?('foo.rb') }
|
|
449
|
-
|
|
450
|
-
# In descending order, foo.rb (66.67%) should come before bar.rb (33.33%)
|
|
451
|
-
expect(lines_desc.index(foo_line_desc)).to be < lines_desc.index(bar_line_desc)
|
|
452
|
-
end
|
|
453
|
-
end
|
|
454
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'spec_helper'
|
|
4
|
-
|
|
5
|
-
RSpec.describe CovLoupe do
|
|
6
|
-
# Mode detection tests moved to mode_detector_spec.rb
|
|
7
|
-
# These tests verify the integration with ModeDetector
|
|
8
|
-
describe 'mode detection integration' do
|
|
9
|
-
it 'uses ModeDetector for CLI mode detection' do
|
|
10
|
-
allow(described_class::ModeDetector).to receive(:cli_mode?).with(['--force-cli'])
|
|
11
|
-
.and_return(true)
|
|
12
|
-
cli = instance_double(described_class::CoverageCLI, run: nil)
|
|
13
|
-
allow(described_class::CoverageCLI).to receive(:new).and_return(cli)
|
|
14
|
-
|
|
15
|
-
described_class.run(['--force-cli'])
|
|
16
|
-
|
|
17
|
-
expect(described_class::ModeDetector).to have_received(:cli_mode?).with(['--force-cli'])
|
|
18
|
-
expect(described_class::CoverageCLI).to have_received(:new)
|
|
19
|
-
expect(cli).to have_received(:run)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# When no thread-local context exists, active_log_file= creates one
|
|
24
|
-
# from the default context rather than modifying an existing one.
|
|
25
|
-
describe '.active_log_file=' do
|
|
26
|
-
it 'creates context from default when no current context exists' do
|
|
27
|
-
Thread.current[:cov_loupe_context] = nil
|
|
28
|
-
|
|
29
|
-
described_class.active_log_file = '/tmp/test.log'
|
|
30
|
-
|
|
31
|
-
expect(described_class.context).not_to be_nil
|
|
32
|
-
expect(described_class.active_log_file).to eq('/tmp/test.log')
|
|
33
|
-
ensure
|
|
34
|
-
described_class.active_log_file = File::NULL
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|