Skip to content

Commit ee4bd5d

Browse files
authored
Merge pull request #889 from mbj/refactor/cli
Refactor CLI
2 parents 7aab5bc + 6f5016c commit ee4bd5d

File tree

3 files changed

+95
-67
lines changed

3 files changed

+95
-67
lines changed

lib/mutant/cli.rb

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,22 @@
22

33
module Mutant
44
# Commandline parser / runner
5+
#
6+
# rubocop:disable Metrics/ClassLength
57
class CLI
6-
include Adamantium::Flat, Equalizer.new(:config), Procto.call(:config)
8+
include Concord.new(:config)
79

8-
Error = Class.new(RuntimeError)
10+
private_class_method :new
11+
12+
OPTIONS =
13+
%i[
14+
add_environment_options
15+
add_mutation_options
16+
add_filter_options
17+
add_debug_options
18+
].freeze
19+
20+
private_constant(*constants(false))
921

1022
# Run cli with arguments
1123
#
@@ -16,61 +28,57 @@ class CLI
1628
# the user provided arguments
1729
#
1830
# @return [Boolean]
31+
#
32+
# rubocop:disable Style/Semicolon
1933
def self.run(config, arguments)
20-
Runner.call(Env::Bootstrap.call(call(config, arguments))).success?
21-
rescue Error => exception
22-
config.stderr.puts(exception.message)
23-
false
34+
apply(config, arguments)
35+
.fmap(&Env::Bootstrap.method(:call))
36+
.fmap(&Runner.method(:call))
37+
.from_right { |error| config.stderr.puts(error); return false }
38+
.success?
2439
end
40+
# rubocop:enable Style/Semicolon
2541

26-
# Initialize object
42+
# Parse arguments into config
2743
#
2844
# @param [Config] config
2945
# @param [Array<String>] arguments
3046
#
31-
# @return [undefined]
32-
def initialize(config, arguments)
33-
@config = config
34-
35-
parse(arguments)
47+
# @return [Either<OptionParser::ParseError, Config>]
48+
def self.apply(config, arguments)
49+
Either
50+
.wrap_error(OptionParser::ParseError) { new(config).parse(arguments) }
51+
.lmap(&:message)
3652
end
3753

38-
# Config parsed from CLI
39-
#
40-
# @return [Config]
41-
attr_reader :config
42-
4354
# Local opt out of option parser defaults
4455
class OptionParser < ::OptionParser
4556
# Kill defaults added by option parser that
4657
# inference with ours under mutation testing.
4758
define_method(:add_officious) {}
4859
end # OptionParser
4960

50-
private
51-
5261
# Parse the command-line options
5362
#
5463
# @param [Array<String>] arguments
5564
# Command-line options and arguments to be parsed.
5665
#
57-
# @fail [Error]
58-
# An error occurred while parsing the options.
59-
#
60-
# @return [undefined]
66+
# @return [Config]
6167
def parse(arguments)
6268
opts = OptionParser.new do |builder|
6369
builder.banner = 'usage: mutant [options] MATCH_EXPRESSION ...'
64-
%i[add_environment_options add_mutation_options add_filter_options add_debug_options].each do |name|
70+
OPTIONS.each do |name|
6571
__send__(name, builder)
6672
end
6773
end
6874

6975
parse_match_expressions(opts.parse!(arguments))
70-
rescue OptionParser::ParseError => error
71-
raise(Error, error)
76+
77+
config
7278
end
7379

80+
private
81+
7482
# Parse matchers
7583
#
7684
# @param [Array<String>] expressions
@@ -113,7 +121,10 @@ def add_environment_options(opts)
113121
def setup_integration(name)
114122
with(integration: Integration.setup(config.kernel, name))
115123
rescue LoadError
116-
raise Error, "Could not load integration #{name.inspect} (you may want to try installing the gem mutant-#{name})"
124+
raise(
125+
OptionParser::InvalidArgument,
126+
"Could not load integration #{name.inspect} (you may want to try installing the gem mutant-#{name})"
127+
)
117128
end
118129

119130
# Add mutation options

lib/mutant/env/bootstrap.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Mutant
44
class Env
55
# Bootstrap environment
66
class Bootstrap
7-
include Adamantium::Flat, Concord::Public.new(:config), Procto.call(:env)
7+
include Adamantium::Flat, Concord::Public.new(:config)
88

99
SEMANTICS_MESSAGE_FORMAT =
1010
"%<message>s. Fix your lib to follow normal ruby semantics!\n" \
@@ -28,6 +28,15 @@ class Bootstrap
2828
# @return [Parser]
2929
attr_reader :parser
3030

31+
# Run Bootstrap
32+
#
33+
# @param [Config] config
34+
#
35+
# @return [Env]
36+
def self.call(config)
37+
new(config).env
38+
end
39+
3140
# Initialize object
3241
#
3342
# @return [Object]

spec/unit/mutant/cli_spec.rb

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -47,26 +47,38 @@ def apply
4747
described_class.run(config, arguments)
4848
end
4949

50-
let(:arguments) { instance_double(Array) }
51-
let(:env) { instance_double(Mutant::Env) }
52-
let(:report_success) { true }
50+
let(:arguments) { instance_double(Array) }
51+
let(:env) { instance_double(Mutant::Env) }
52+
let(:report_success) { true }
53+
let(:cli_result) { Mutant::Either::Right.new(config) }
5354

5455
let(:report) do
5556
instance_double(Mutant::Result::Env, success?: report_success)
5657
end
5758

5859
before do
59-
allow(Mutant::CLI).to receive_messages(call: config)
60+
allow(Mutant::CLI).to receive_messages(apply: cli_result)
6061
allow(Mutant::Env::Bootstrap).to receive_messages(call: env)
6162
allow(Mutant::Runner).to receive_messages(call: report)
6263
end
6364

6465
it 'performs calls in expected sequence' do
6566
apply
6667

67-
expect(Mutant::CLI).to have_received(:call).with(config, arguments).ordered
68-
expect(Mutant::Env::Bootstrap).to have_received(:call).with(config).ordered
69-
expect(Mutant::Runner).to have_received(:call).with(env).ordered
68+
expect(Mutant::CLI)
69+
.to have_received(:apply)
70+
.with(config, arguments)
71+
.ordered
72+
73+
expect(Mutant::Env::Bootstrap)
74+
.to have_received(:call)
75+
.with(config)
76+
.ordered
77+
78+
expect(Mutant::Runner)
79+
.to have_received(:call)
80+
.with(env)
81+
.ordered
7082
end
7183

7284
context 'when report signals success' do
@@ -85,30 +97,27 @@ def apply
8597
end
8698
end
8799

88-
context 'when execution raises an Mutant::CLI::Error' do
89-
let(:exception) { Mutant::CLI::Error.new('test-error') }
90-
let(:expected_message) { 'test-error' }
91-
let(:report_success) { false }
92-
let(:target_stream) { stderr }
100+
context 'when parts of the chain fail' do
101+
let(:cli_result) { Mutant::Either::Left.new(expected_message) }
102+
let(:expected_message) { 'cli-error' }
103+
let(:target_stream) { stderr }
93104

94-
before do
95-
allow(report).to receive(:success?).and_raise(exception)
96-
end
105+
include_examples 'prints expected message'
97106

98-
it 'exits with failure' do
107+
it 'exits failure' do
99108
expect(apply).to be(false)
100109
end
101-
102-
include_examples 'prints expected message'
103110
end
104111
end
105112

106-
describe '.new' do
113+
describe '.apply' do
114+
def apply
115+
described_class.apply(config, arguments)
116+
end
117+
107118
shared_examples 'invalid arguments' do
108-
it 'raises error' do
109-
expect do
110-
apply
111-
end.to raise_error(Mutant::CLI::Error, expected_message)
119+
it 'returns left error' do
120+
expect(apply).to eql(Mutant::Either::Left.new(expected_message))
112121
end
113122
end
114123

@@ -127,12 +136,8 @@ def apply
127136
end
128137

129138
shared_examples_for 'cli parser' do
130-
it { expect(apply.config.integration).to eql(expected_integration) }
131-
it { expect(apply.config.matcher).to eql(expected_matcher_config) }
132-
end
133-
134-
def apply
135-
described_class.new(config, arguments)
139+
it { expect(apply.from_right.integration).to eql(expected_integration) }
140+
it { expect(apply.from_right.matcher).to eql(expected_matcher_config) }
136141
end
137142

138143
before do
@@ -192,7 +197,7 @@ def apply
192197
include_examples 'no explicit exit'
193198

194199
it 'configures includes' do
195-
expect(apply.config.includes).to eql(%w[foo])
200+
expect(apply.from_right.includes).to eql(%w[foo])
196201
end
197202
end
198203

@@ -222,15 +227,18 @@ def apply
222227
context 'when integration does NOT exist' do
223228
let(:options) { %w[--use other] }
224229

230+
let(:expected_message) do
231+
'invalid argument: ' \
232+
'--use Could not load integration "other" ' \
233+
'(you may want to try installing the gem mutant-other)'
234+
end
235+
225236
before do
226237
allow(Mutant::Integration).to receive(:setup).and_raise(LoadError)
227238
end
228239

229-
it 'raises error' do
230-
expect { apply }.to raise_error(
231-
Mutant::CLI::Error,
232-
'Could not load integration "other" (you may want to try installing the gem mutant-other)'
233-
)
240+
it 'returns error' do
241+
expect(apply).to eql(Mutant::Either::Left.new(expected_message))
234242
end
235243
end
236244
end
@@ -251,7 +259,7 @@ def apply
251259
include_examples 'no explicit exit'
252260

253261
it 'configures expected coverage' do
254-
expect(apply.config.jobs).to eql(0)
262+
expect(apply.from_right.jobs).to eql(0)
255263
end
256264
end
257265

@@ -262,7 +270,7 @@ def apply
262270
include_examples 'no explicit exit'
263271

264272
it 'configures requires' do
265-
expect(apply.config.requires).to eql(%w[foo bar])
273+
expect(apply.from_right.requires).to eql(%w[foo bar])
266274
end
267275
end
268276

@@ -305,7 +313,7 @@ def apply
305313
include_examples 'no explicit exit'
306314

307315
it 'sets the fail fast option' do
308-
expect(apply.config.fail_fast).to be(true)
316+
expect(apply.from_right.fail_fast).to be(true)
309317
end
310318
end
311319

@@ -316,7 +324,7 @@ def apply
316324
include_examples 'no explicit exit'
317325

318326
it 'sets the zombie option' do
319-
expect(apply.config.zombie).to be(true)
327+
expect(apply.from_right.zombie).to be(true)
320328
end
321329
end
322330
end

0 commit comments

Comments
 (0)