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,454 @@
|
|
|
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
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'COV_LOUPE_OPTS Environment Variable' do
|
|
6
|
+
let(:cli) { CovLoupe::CoverageCLI.new }
|
|
7
|
+
|
|
8
|
+
around do |example|
|
|
9
|
+
original_value = ENV['COV_LOUPE_OPTS']
|
|
10
|
+
example.run
|
|
11
|
+
ensure
|
|
12
|
+
ENV['COV_LOUPE_OPTS'] = original_value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe 'CLI option parsing from environment' do
|
|
16
|
+
it 'parses simple options from COV_LOUPE_OPTS' do
|
|
17
|
+
ENV['COV_LOUPE_OPTS'] = '--error-mode off --format json'
|
|
18
|
+
env_opts = CovLoupe.send(:extract_env_opts)
|
|
19
|
+
|
|
20
|
+
swallow_system_exit do
|
|
21
|
+
silence_output do
|
|
22
|
+
cli.send(:run, env_opts + ['summary', 'lib/foo.rb'])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
rescue CovLoupe::Error => e
|
|
26
|
+
# Expected to fail due to missing file, but options should be parsed
|
|
27
|
+
puts "DEBUG: Caught exception: #{e.class}: #{e.message}" if ENV['DEBUG']
|
|
28
|
+
ensure
|
|
29
|
+
expect(cli.config.error_mode).to eq(:off)
|
|
30
|
+
expect(cli.config.format).to eq(:json)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'handles quoted options with spaces' do
|
|
34
|
+
test_path = File.join(Dir.tmpdir, 'test path with spaces', '.resultset.json')
|
|
35
|
+
ENV['COV_LOUPE_OPTS'] = "--resultset \"#{test_path}\""
|
|
36
|
+
env_opts = CovLoupe.send(:extract_env_opts)
|
|
37
|
+
|
|
38
|
+
exit_status = swallow_system_exit do
|
|
39
|
+
silence_output do
|
|
40
|
+
cli.send(:run, env_opts + ['--help'])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
expect(exit_status).to eq(0) # --help exits cleanly
|
|
45
|
+
expect(cli.config.resultset).to eq(test_path)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'supports setting log-file to stdout from environment' do
|
|
49
|
+
ENV['COV_LOUPE_OPTS'] = '--log-file stdout'
|
|
50
|
+
env_opts = CovLoupe.send(:extract_env_opts)
|
|
51
|
+
|
|
52
|
+
swallow_system_exit do
|
|
53
|
+
silence_output do
|
|
54
|
+
cli.send(:run, env_opts + ['--help'])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
expect(cli.config.log_file).to eq('stdout')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'command line arguments override environment options' do
|
|
62
|
+
ENV['COV_LOUPE_OPTS'] = '--error-mode off'
|
|
63
|
+
env_opts = CovLoupe.send(:extract_env_opts)
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
args = env_opts + ['--error-mode', 'debug', 'summary', 'lib/foo.rb']
|
|
67
|
+
silence_output { cli.send(:run, args) }
|
|
68
|
+
rescue SystemExit, CovLoupe::Error
|
|
69
|
+
# Expected to fail, but options should be parsed
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Command line should override environment
|
|
73
|
+
expect(cli.config.error_mode).to eq(:debug)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'handles malformed COV_LOUPE_OPTS gracefully' do
|
|
77
|
+
ENV['COV_LOUPE_OPTS'] = '--option "unclosed quote'
|
|
78
|
+
|
|
79
|
+
# Should catch the ConfigurationError and exit cleanly
|
|
80
|
+
_out, _err, status = run_cli_with_status('summary', 'lib/foo.rb')
|
|
81
|
+
expect(status).not_to eq(0)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns empty array when COV_LOUPE_OPTS is not set' do
|
|
85
|
+
# ENV is already cleared by around block
|
|
86
|
+
opts = CovLoupe.send(:extract_env_opts)
|
|
87
|
+
expect(opts).to eq([])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'returns empty array when COV_LOUPE_OPTS is empty' do
|
|
91
|
+
ENV['COV_LOUPE_OPTS'] = ''
|
|
92
|
+
opts = CovLoupe.send(:extract_env_opts)
|
|
93
|
+
expect(opts).to eq([])
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe 'CLI mode detection with COV_LOUPE_OPTS' do
|
|
98
|
+
it 'respects --force-cli from environment variable' do
|
|
99
|
+
ENV['COV_LOUPE_OPTS'] = '--force-cli'
|
|
100
|
+
|
|
101
|
+
# This would normally be MCP mode (no TTY, no subcommand)
|
|
102
|
+
stdin = double('stdin', tty?: false)
|
|
103
|
+
|
|
104
|
+
env_opts = CovLoupe.send(:extract_env_opts)
|
|
105
|
+
full_argv = env_opts + []
|
|
106
|
+
|
|
107
|
+
expect(CovLoupe::ModeDetector.cli_mode?(full_argv, stdin: stdin)).to be true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'handles parse errors gracefully in mode detection' do
|
|
111
|
+
ENV['COV_LOUPE_OPTS'] = '--option "unclosed quote'
|
|
112
|
+
|
|
113
|
+
# Should return empty array and not crash
|
|
114
|
+
opts = CovLoupe.send(:extract_env_opts)
|
|
115
|
+
expect(opts).to eq([])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'actually runs CLI when --force-cli is in COV_LOUPE_OPTS' do
|
|
119
|
+
ENV['COV_LOUPE_OPTS'] = '--force-cli'
|
|
120
|
+
|
|
121
|
+
# Mock STDIN to not be a TTY (would normally trigger MCP server mode)
|
|
122
|
+
allow($stdin).to receive(:tty?).and_return(false)
|
|
123
|
+
|
|
124
|
+
# Run with --help which should produce help output
|
|
125
|
+
output = nil
|
|
126
|
+
silence_output do |out, err|
|
|
127
|
+
swallow_system_exit do
|
|
128
|
+
CovLoupe.run(['--help'])
|
|
129
|
+
end
|
|
130
|
+
output = out.string + err.string
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Verify CLI actually ran by checking for help text
|
|
134
|
+
expect(output).to include('Usage:')
|
|
135
|
+
expect(output).to include('cov-loupe')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'actually runs MCP server mode when no CLI indicators present' do
|
|
139
|
+
ENV['COV_LOUPE_OPTS'] = ''
|
|
140
|
+
|
|
141
|
+
# Mock STDIN to not be a TTY and to provide valid JSON-RPC
|
|
142
|
+
allow($stdin).to receive(:tty?).and_return(false)
|
|
143
|
+
|
|
144
|
+
# Provide a minimal JSON-RPC request that the server can handle
|
|
145
|
+
json_request = JSON.generate({
|
|
146
|
+
jsonrpc: '2.0',
|
|
147
|
+
id: 1,
|
|
148
|
+
method: 'initialize',
|
|
149
|
+
params: {
|
|
150
|
+
protocolVersion: '2024-11-05',
|
|
151
|
+
capabilities: {},
|
|
152
|
+
clientInfo: { name: 'test', version: '1.0' }
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
allow($stdin).to receive(:gets).and_return(json_request, nil)
|
|
157
|
+
|
|
158
|
+
# Capture output to verify MCP server response
|
|
159
|
+
output = nil
|
|
160
|
+
silence_output do |out, err|
|
|
161
|
+
CovLoupe.run([])
|
|
162
|
+
output = out.string + err.string
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Verify MCP server ran by checking for JSON-RPC response
|
|
166
|
+
expect(output).to include('"jsonrpc"')
|
|
167
|
+
expect(output).to include('"result"')
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
describe 'integration with actual CLI usage' do
|
|
172
|
+
it 'works end-to-end with --resultset option' do
|
|
173
|
+
test_resultset = File.join(Dir.tmpdir, 'test_coverage', '.resultset.json')
|
|
174
|
+
ENV['COV_LOUPE_OPTS'] = "--resultset #{test_resultset} --format json"
|
|
175
|
+
env_opts = CovLoupe.send(:extract_env_opts)
|
|
176
|
+
|
|
177
|
+
swallow_system_exit do
|
|
178
|
+
silence_output { cli.send(:run, env_opts + ['--help']) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
expect(cli.config.resultset).to eq(test_resultset)
|
|
182
|
+
expect(cli.config.format).to eq(:json)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|