cov-loupe 3.0.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +329 -0
- data/docs/dev/ARCHITECTURE.md +80 -0
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
- data/docs/dev/DEVELOPMENT.md +83 -0
- data/docs/dev/README.md +10 -0
- data/docs/dev/RELEASING.md +146 -0
- data/docs/dev/arch-decisions/001-x-arch-decision.md +95 -0
- data/docs/dev/arch-decisions/002-x-arch-decision.md +159 -0
- data/docs/dev/arch-decisions/003-x-arch-decision.md +165 -0
- data/docs/dev/arch-decisions/004-x-arch-decision.md +203 -0
- data/docs/dev/arch-decisions/005-x-arch-decision.md +189 -0
- data/docs/dev/arch-decisions/README.md +60 -0
- data/docs/dev/presentations/cov-loupe-presentation.md +255 -0
- data/docs/fixtures/demo_project/README.md +9 -0
- data/docs/user/ADVANCED_USAGE.md +777 -0
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
- data/docs/user/CLI_USAGE.md +750 -0
- data/docs/user/ERROR_HANDLING.md +93 -0
- data/docs/user/EXAMPLES.md +588 -0
- data/docs/user/INSTALLATION.md +130 -0
- data/docs/user/LIBRARY_API.md +693 -0
- data/docs/user/MCP_INTEGRATION.md +490 -0
- data/docs/user/README.md +14 -0
- data/docs/user/TROUBLESHOOTING.md +197 -0
- data/docs/user/V2-BREAKING-CHANGES.md +472 -0
- data/exe/cov-loupe +23 -0
- data/lib/cov_loupe/app_config.rb +56 -0
- data/lib/cov_loupe/app_context.rb +26 -0
- data/lib/cov_loupe/base_tool.rb +102 -0
- data/lib/cov_loupe/cli.rb +178 -0
- data/lib/cov_loupe/commands/base_command.rb +67 -0
- data/lib/cov_loupe/commands/command_factory.rb +45 -0
- data/lib/cov_loupe/commands/detailed_command.rb +38 -0
- data/lib/cov_loupe/commands/list_command.rb +13 -0
- data/lib/cov_loupe/commands/raw_command.rb +38 -0
- data/lib/cov_loupe/commands/summary_command.rb +41 -0
- data/lib/cov_loupe/commands/totals_command.rb +53 -0
- data/lib/cov_loupe/commands/uncovered_command.rb +45 -0
- data/lib/cov_loupe/commands/validate_command.rb +60 -0
- data/lib/cov_loupe/commands/version_command.rb +33 -0
- data/lib/cov_loupe/config_parser.rb +32 -0
- data/lib/cov_loupe/constants.rb +22 -0
- data/lib/cov_loupe/coverage_reporter.rb +31 -0
- data/lib/cov_loupe/error_handler.rb +165 -0
- data/lib/cov_loupe/error_handler_factory.rb +31 -0
- data/lib/cov_loupe/errors.rb +191 -0
- data/lib/cov_loupe/formatters/source_formatter.rb +152 -0
- data/lib/cov_loupe/formatters.rb +51 -0
- data/lib/cov_loupe/mcp_server.rb +42 -0
- data/lib/cov_loupe/mode_detector.rb +56 -0
- data/lib/cov_loupe/model.rb +339 -0
- data/lib/cov_loupe/option_normalizers.rb +113 -0
- data/lib/cov_loupe/option_parser_builder.rb +147 -0
- data/lib/cov_loupe/option_parsers/env_options_parser.rb +48 -0
- data/lib/cov_loupe/option_parsers/error_helper.rb +110 -0
- data/lib/cov_loupe/path_relativizer.rb +64 -0
- data/lib/cov_loupe/predicate_evaluator.rb +72 -0
- data/lib/cov_loupe/presenters/base_coverage_presenter.rb +42 -0
- data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/project_coverage_presenter.rb +50 -0
- data/lib/cov_loupe/presenters/project_totals_presenter.rb +27 -0
- data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +122 -0
- data/lib/cov_loupe/resolvers/resolver_factory.rb +28 -0
- data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +76 -0
- data/lib/cov_loupe/resultset_loader.rb +131 -0
- data/lib/cov_loupe/staleness_checker.rb +247 -0
- data/lib/cov_loupe/table_formatter.rb +64 -0
- data/lib/cov_loupe/tools/all_files_coverage_tool.rb +51 -0
- data/lib/cov_loupe/tools/coverage_detailed_tool.rb +35 -0
- data/lib/cov_loupe/tools/coverage_raw_tool.rb +34 -0
- data/lib/cov_loupe/tools/coverage_summary_tool.rb +34 -0
- data/lib/cov_loupe/tools/coverage_table_tool.rb +50 -0
- data/lib/cov_loupe/tools/coverage_totals_tool.rb +44 -0
- data/lib/cov_loupe/tools/help_tool.rb +115 -0
- data/lib/cov_loupe/tools/uncovered_lines_tool.rb +34 -0
- data/lib/cov_loupe/tools/validate_tool.rb +72 -0
- data/lib/cov_loupe/tools/version_tool.rb +32 -0
- data/lib/cov_loupe/util.rb +88 -0
- data/lib/cov_loupe/version.rb +5 -0
- data/lib/cov_loupe.rb +140 -0
- data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
- data/spec/TIMESTAMPS.md +48 -0
- data/spec/all_files_coverage_tool_spec.rb +53 -0
- data/spec/app_config_spec.rb +142 -0
- data/spec/base_tool_spec.rb +62 -0
- data/spec/cli/show_default_report_spec.rb +33 -0
- data/spec/cli_enumerated_options_spec.rb +90 -0
- data/spec/cli_error_spec.rb +184 -0
- data/spec/cli_format_spec.rb +123 -0
- data/spec/cli_json_options_spec.rb +50 -0
- data/spec/cli_source_spec.rb +44 -0
- data/spec/cli_spec.rb +192 -0
- data/spec/cli_table_spec.rb +28 -0
- data/spec/cli_usage_spec.rb +42 -0
- data/spec/commands/base_command_spec.rb +107 -0
- data/spec/commands/command_factory_spec.rb +76 -0
- data/spec/commands/detailed_command_spec.rb +34 -0
- data/spec/commands/list_command_spec.rb +28 -0
- data/spec/commands/raw_command_spec.rb +69 -0
- data/spec/commands/summary_command_spec.rb +34 -0
- data/spec/commands/totals_command_spec.rb +34 -0
- data/spec/commands/uncovered_command_spec.rb +55 -0
- data/spec/commands/validate_command_spec.rb +213 -0
- data/spec/commands/version_command_spec.rb +38 -0
- data/spec/constants_spec.rb +61 -0
- data/spec/cov_loupe/formatters/source_formatter_spec.rb +267 -0
- data/spec/cov_loupe/formatters_spec.rb +76 -0
- data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +79 -0
- data/spec/cov_loupe_model_spec.rb +454 -0
- data/spec/cov_loupe_module_spec.rb +37 -0
- data/spec/cov_loupe_opts_spec.rb +185 -0
- data/spec/coverage_reporter_spec.rb +102 -0
- data/spec/coverage_table_tool_spec.rb +59 -0
- data/spec/coverage_totals_tool_spec.rb +37 -0
- data/spec/error_handler_spec.rb +197 -0
- data/spec/error_mode_spec.rb +139 -0
- data/spec/errors_edge_cases_spec.rb +312 -0
- data/spec/errors_stale_spec.rb +83 -0
- data/spec/file_based_mcp_tools_spec.rb +99 -0
- data/spec/fixtures/project1/lib/bar.rb +5 -0
- data/spec/fixtures/project1/lib/foo.rb +6 -0
- data/spec/help_tool_spec.rb +26 -0
- data/spec/integration_spec.rb +789 -0
- data/spec/logging_fallback_spec.rb +128 -0
- data/spec/mcp_logging_spec.rb +44 -0
- data/spec/mcp_server_integration_spec.rb +23 -0
- data/spec/mcp_server_spec.rb +106 -0
- data/spec/mode_detector_spec.rb +153 -0
- data/spec/model_error_handling_spec.rb +269 -0
- data/spec/model_staleness_spec.rb +79 -0
- data/spec/option_normalizers_spec.rb +203 -0
- data/spec/option_parsers/env_options_parser_spec.rb +221 -0
- data/spec/option_parsers/error_helper_spec.rb +222 -0
- data/spec/path_relativizer_spec.rb +98 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
- data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
- data/spec/presenters/project_coverage_presenter_spec.rb +87 -0
- data/spec/presenters/project_totals_presenter_spec.rb +144 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +282 -0
- data/spec/resolvers/resolver_factory_spec.rb +61 -0
- data/spec/resolvers/resultset_path_resolver_spec.rb +60 -0
- data/spec/resultset_loader_spec.rb +167 -0
- data/spec/shared_examples/README.md +115 -0
- data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
- data/spec/shared_examples/file_based_mcp_tools.rb +179 -0
- data/spec/shared_examples/formatted_command_examples.rb +64 -0
- data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
- data/spec/spec_helper.rb +127 -0
- data/spec/staleness_checker_spec.rb +374 -0
- data/spec/staleness_more_spec.rb +42 -0
- data/spec/support/cli_helpers.rb +22 -0
- data/spec/support/control_flow_helpers.rb +20 -0
- data/spec/support/fake_mcp.rb +40 -0
- data/spec/support/io_helpers.rb +29 -0
- data/spec/support/mcp_helpers.rb +35 -0
- data/spec/support/mcp_runner.rb +66 -0
- data/spec/support/mocking_helpers.rb +30 -0
- data/spec/table_format_spec.rb +70 -0
- data/spec/tools/validate_tool_spec.rb +132 -0
- data/spec/tools_error_handling_spec.rb +130 -0
- data/spec/util_spec.rb +154 -0
- data/spec/version_spec.rb +123 -0
- data/spec/version_tool_spec.rb +141 -0
- metadata +290 -0
|
@@ -0,0 +1,111 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
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
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe::AppConfig do
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'creates a config with default values' do
|
|
8
|
+
config = described_class.new
|
|
9
|
+
expect(config.root).to eq('.')
|
|
10
|
+
expect(config.format).to eq(:table)
|
|
11
|
+
expect(config.sort_order).to eq(:descending)
|
|
12
|
+
expect(config.source_context).to eq(2)
|
|
13
|
+
expect(config.error_mode).to eq(:log)
|
|
14
|
+
expect(config.staleness).to eq(:off)
|
|
15
|
+
expect(config.resultset).to be_nil
|
|
16
|
+
expect(config.source_mode).to be_nil
|
|
17
|
+
expect(config.tracked_globs).to be_nil
|
|
18
|
+
expect(config.log_file).to be_nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'allows overriding defaults via keyword arguments' do
|
|
22
|
+
config = described_class.new(
|
|
23
|
+
root: '/custom',
|
|
24
|
+
format: :json,
|
|
25
|
+
sort_order: :descending,
|
|
26
|
+
staleness: :error
|
|
27
|
+
)
|
|
28
|
+
expect(config.root).to eq('/custom')
|
|
29
|
+
expect(config.format).to eq(:json)
|
|
30
|
+
expect(config.sort_order).to eq(:descending)
|
|
31
|
+
expect(config.staleness).to eq(:error)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'is mutable (struct fields can be changed)' do
|
|
35
|
+
config = described_class.new
|
|
36
|
+
config.root = '/new/root'
|
|
37
|
+
config.format = :json
|
|
38
|
+
expect(config.root).to eq('/new/root')
|
|
39
|
+
expect(config.format).to eq(:json)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#model_options' do
|
|
44
|
+
it 'returns hash suitable for CoverageModel.new' do
|
|
45
|
+
config = described_class.new(
|
|
46
|
+
root: '/custom/root',
|
|
47
|
+
resultset: '/custom/.resultset.json',
|
|
48
|
+
staleness: :error,
|
|
49
|
+
tracked_globs: ['lib/**/*.rb']
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
options = config.model_options
|
|
53
|
+
expect(options).to eq({
|
|
54
|
+
root: '/custom/root',
|
|
55
|
+
resultset: '/custom/.resultset.json',
|
|
56
|
+
staleness: :error,
|
|
57
|
+
tracked_globs: ['lib/**/*.rb']
|
|
58
|
+
})
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'handles nil values correctly' do
|
|
62
|
+
config = described_class.new
|
|
63
|
+
options = config.model_options
|
|
64
|
+
expect(options[:root]).to eq('.')
|
|
65
|
+
expect(options[:resultset]).to be_nil
|
|
66
|
+
expect(options[:staleness]).to eq(:off)
|
|
67
|
+
expect(options[:tracked_globs]).to be_nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '#formatter_options' do
|
|
72
|
+
it 'returns hash suitable for SourceFormatter.new' do
|
|
73
|
+
config = described_class.new(color: true)
|
|
74
|
+
options = config.formatter_options
|
|
75
|
+
expect(options).to eq({ color_enabled: true })
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'handles false color setting' do
|
|
79
|
+
config = described_class.new(color: false)
|
|
80
|
+
options = config.formatter_options
|
|
81
|
+
expect(options).to eq({ color_enabled: false })
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe 'struct behavior' do
|
|
86
|
+
it 'supports equality comparison' do
|
|
87
|
+
config1 = described_class.new(root: '/foo', format: :json)
|
|
88
|
+
config2 = described_class.new(root: '/foo', format: :json)
|
|
89
|
+
config3 = described_class.new(root: '/bar', format: :json)
|
|
90
|
+
|
|
91
|
+
expect(config1).to eq(config2)
|
|
92
|
+
expect(config1).not_to eq(config3)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'provides readable inspect output' do
|
|
96
|
+
config = described_class.new(root: '/test', format: :json)
|
|
97
|
+
output = config.inspect
|
|
98
|
+
expect(output).to include('root="/test"')
|
|
99
|
+
expect(output).to include('format=:json')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'converts to hash' do
|
|
103
|
+
config = described_class.new(root: '/test', format: :json)
|
|
104
|
+
hash = config.to_h
|
|
105
|
+
expect(hash).to be_a(Hash)
|
|
106
|
+
expect(hash[:root]).to eq('/test')
|
|
107
|
+
expect(hash[:format]).to eq(:json)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
describe 'symbol enumerated values' do
|
|
112
|
+
it 'uses symbols for format' do
|
|
113
|
+
config = described_class.new(format: :json)
|
|
114
|
+
expect(config.format).to eq(:json)
|
|
115
|
+
expect(config.format).to be_a(Symbol)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'uses symbols for sort_order' do
|
|
119
|
+
config = described_class.new(sort_order: :descending)
|
|
120
|
+
expect(config.sort_order).to eq(:descending)
|
|
121
|
+
expect(config.sort_order).to be_a(Symbol)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'uses symbols for staleness' do
|
|
125
|
+
config = described_class.new(staleness: :error)
|
|
126
|
+
expect(config.staleness).to eq(:error)
|
|
127
|
+
expect(config.staleness).to be_a(Symbol)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'uses symbols for error_mode' do
|
|
131
|
+
config = described_class.new(error_mode: :debug)
|
|
132
|
+
expect(config.error_mode).to eq(:debug)
|
|
133
|
+
expect(config.error_mode).to be_a(Symbol)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'uses symbols for source_mode' do
|
|
137
|
+
config = described_class.new(source_mode: :uncovered)
|
|
138
|
+
expect(config.source_mode).to eq(:uncovered)
|
|
139
|
+
expect(config.source_mode).to be_a(Symbol)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe::BaseTool do
|
|
6
|
+
let(:handler) { CovLoupe::ErrorHandler.new(error_mode: :log, logger: test_logger) }
|
|
7
|
+
let(:test_logger) do
|
|
8
|
+
Class.new do
|
|
9
|
+
attr_reader :messages
|
|
10
|
+
|
|
11
|
+
def initialize = @messages = []
|
|
12
|
+
def error(msg) = @messages << msg
|
|
13
|
+
end.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:orig_handler) do
|
|
17
|
+
CovLoupe.error_handler
|
|
18
|
+
rescue
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
before do
|
|
23
|
+
CovLoupe.error_handler = handler
|
|
24
|
+
setup_mcp_response_stub
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
after do
|
|
28
|
+
CovLoupe.error_handler = orig_handler if orig_handler
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
shared_examples 'friendly response and logged' do
|
|
32
|
+
it 'returns friendly text' do
|
|
33
|
+
resp = described_class.handle_mcp_error(error, tool, error_mode: :log)
|
|
34
|
+
expect(resp).to be_a(MCP::Tool::Response)
|
|
35
|
+
expect(resp.payload.first['text']).to match(expected_pattern)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'respects error_mode :off' do
|
|
39
|
+
resp = described_class.handle_mcp_error(error, tool, error_mode: :off)
|
|
40
|
+
expect(resp).to be_a(MCP::Tool::Response)
|
|
41
|
+
expect(resp.payload.first['text']).to match(expected_pattern)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
context 'with CovLoupe::Error' do
|
|
46
|
+
let(:error) { CovLoupe::UsageError.new('invalid args') }
|
|
47
|
+
let(:tool) { 'coverage_summary' }
|
|
48
|
+
let(:expected_pattern) { /Error: invalid args/ }
|
|
49
|
+
let(:log_fragment) { 'invalid args' }
|
|
50
|
+
|
|
51
|
+
it_behaves_like 'friendly response and logged'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
context 'with standard error' do
|
|
55
|
+
let(:error) { Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.rb') }
|
|
56
|
+
let(:tool) { 'coverage_raw' }
|
|
57
|
+
let(:expected_pattern) { /Error: .*File not found: missing.rb/ }
|
|
58
|
+
let(:log_fragment) { 'File not found' }
|
|
59
|
+
|
|
60
|
+
it_behaves_like 'friendly response and logged'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe::CoverageCLI do
|
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
7
|
+
let(:cli) { described_class.new }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
cli.config.root = root
|
|
11
|
+
cli.config.resultset = 'coverage'
|
|
12
|
+
cli.config.staleness = :off
|
|
13
|
+
cli.config.tracked_globs = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#show_default_report' do
|
|
17
|
+
it 'prints JSON summary using relativized payload when json mode is enabled' do
|
|
18
|
+
cli.config.format = :json
|
|
19
|
+
|
|
20
|
+
output = nil
|
|
21
|
+
silence_output do |stdout, _stderr|
|
|
22
|
+
cli.show_default_report(sort_order: :ascending, output: stdout)
|
|
23
|
+
output = stdout.string
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
payload = JSON.parse(output)
|
|
27
|
+
|
|
28
|
+
expect(payload['files']).to be_an(Array)
|
|
29
|
+
expect(payload['files'].first['file']).to eq('lib/bar.rb').or eq('lib/foo.rb')
|
|
30
|
+
expect(payload['counts']).to include('total', 'ok', 'stale')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'CLI enumerated option parsing' do
|
|
6
|
+
def parse!(argv)
|
|
7
|
+
cli = CovLoupe::CoverageCLI.new
|
|
8
|
+
cli.send(:parse_options!, argv.dup)
|
|
9
|
+
cli
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe 'accepts short and long forms' do
|
|
13
|
+
cases = [
|
|
14
|
+
{ argv: ['--sort-order', 'a', 'list'], accessor: :sort_order, expected: :ascending },
|
|
15
|
+
{ argv: ['--sort-order', 'd', 'list'], accessor: :sort_order, expected: :descending },
|
|
16
|
+
{ argv: ['--sort-order', 'ascending', 'list'], accessor: :sort_order, expected: :ascending },
|
|
17
|
+
{ argv: ['--sort-order', 'descending', 'list'], accessor: :sort_order,
|
|
18
|
+
expected: :descending },
|
|
19
|
+
|
|
20
|
+
{ argv: ['--source', 'f', 'summary', 'lib/foo.rb'], accessor: :source_mode, expected: :full },
|
|
21
|
+
{ argv: ['--source', 'u', 'summary', 'lib/foo.rb'], accessor: :source_mode,
|
|
22
|
+
expected: :uncovered },
|
|
23
|
+
{ argv: ['--source', 'full', 'summary', 'lib/foo.rb'], accessor: :source_mode,
|
|
24
|
+
expected: :full },
|
|
25
|
+
{ argv: ['--source', 'uncovered', 'summary', 'lib/foo.rb'], accessor: :source_mode,
|
|
26
|
+
expected: :uncovered },
|
|
27
|
+
|
|
28
|
+
{ argv: ['-S', 'e', 'list'], accessor: :staleness, expected: :error },
|
|
29
|
+
{ argv: ['-S', 'o', 'list'], accessor: :staleness, expected: :off },
|
|
30
|
+
{ argv: ['--staleness', 'e', 'list'], accessor: :staleness, expected: :error },
|
|
31
|
+
{ argv: ['--staleness', 'o', 'list'], accessor: :staleness, expected: :off },
|
|
32
|
+
|
|
33
|
+
{ argv: ['--error-mode', 'off', 'list'], accessor: :error_mode, expected: :off },
|
|
34
|
+
{ argv: ['--error-mode', 'o', 'list'], accessor: :error_mode, expected: :off },
|
|
35
|
+
{ argv: ['--error-mode', 'log', 'list'], accessor: :error_mode, expected: :log },
|
|
36
|
+
{ argv: ['--error-mode', 'debug', 'list'], accessor: :error_mode, expected: :debug }
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
cases.each do |c|
|
|
40
|
+
it "parses #{c[:argv].join(' ')}" do
|
|
41
|
+
cli = parse!(c[:argv])
|
|
42
|
+
expect(cli.config.public_send(c[:accessor])).to eq(c[:expected])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe 'rejects invalid values' do
|
|
48
|
+
invalid_cases = [
|
|
49
|
+
{ argv: ['--sort-order', 'asc', 'list'] },
|
|
50
|
+
{ argv: ['--source', 'x', 'summary', 'lib/foo.rb'] },
|
|
51
|
+
{ argv: ['-S', 'x', 'list'] },
|
|
52
|
+
{ argv: ['--staleness', 'x', 'list'] },
|
|
53
|
+
{ argv: ['--error-mode', 'bad', 'list'] },
|
|
54
|
+
{ argv: ['--error-mode', 'on', 'list'] },
|
|
55
|
+
{ argv: ['--error-mode', 'trace', 'list'] }
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
invalid_cases.each do |c|
|
|
59
|
+
it "exits 1 for #{c[:argv].join(' ')}" do
|
|
60
|
+
_out, err, status = run_cli_with_status(*c[:argv])
|
|
61
|
+
expect(status).to eq(1)
|
|
62
|
+
expect(err).to include('Error:')
|
|
63
|
+
expect(err).to include('invalid argument')
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe 'missing value hints' do
|
|
69
|
+
it 'exits 1 when -S is provided without a value' do
|
|
70
|
+
_out, err, status = run_cli_with_status('-S', 'list')
|
|
71
|
+
expect(status).to eq(1)
|
|
72
|
+
expect(err).to include('invalid argument')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'exits 1 when --staleness is provided without a value' do
|
|
76
|
+
_out, err, status = run_cli_with_status('--staleness', 'list')
|
|
77
|
+
expect(status).to eq(1)
|
|
78
|
+
expect(err).to include('invalid argument')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'exits 1 when --source is provided without a value' do
|
|
82
|
+
_out, err, status = run_cli_with_status('--source', 'summary', 'lib/foo.rb')
|
|
83
|
+
expect(status).to eq(1)
|
|
84
|
+
# Depending on OptParse implementation for required argument, it might say "missing argument"
|
|
85
|
+
# But usually it consumes next arg. If 'summary' is consumed as argument for source:
|
|
86
|
+
# normalize_source_mode('summary') -> raises InvalidArgument.
|
|
87
|
+
expect(err).to include('invalid argument')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|