diff --git a/lib/mutant/cli.rb b/lib/mutant/cli.rb new file mode 100644 index 00000000..558cc0b0 --- /dev/null +++ b/lib/mutant/cli.rb @@ -0,0 +1,112 @@ +module Mutant + # Comandline adapter or mutant runner + class CLI + include Immutable + + # Error raised when CLI argv is inalid + Error = Class.new(RuntimeError) + + def self.run(*arguments) + Runner.run(new(*arguments).runner_options) + end + + def runner_options + { + :mutation_filter => mutation_filter, + :matcher => matcher, + :reporter => Reporter::CLI.new($stderr), + :killer => Killer::Rspec + } + end + memoize :runner_options + + private + + OPTIONS = { + '--code' => [:add_filter, Mutation::Filter::Code].freeze + }.deep_freeze + + OPTION_PATTERN = %r(\A-(?:-)?[a-z0-9]+\z).freeze + + def option(index) + @arguments.fetch(index+1) + end + + def initialize(arguments) + @filters, @matchers = [], [] + + @arguments = arguments + + @index = 0 + + while @index < @arguments.length + dispatch + end + end + + def current_argument + @arguments.fetch(@index) + end + + def current_option_value + @arguments.fetch(@index+1) + rescue IndexError + raise Error,"#{current_argument.inspect} is missing an argument" + end + + def dispatch + if OPTION_PATTERN.match(current_argument) + dispatch_option + else + dispatch_matcher + end + end + + def consume(amount) + @index += amount + end + + def dispatch_matcher + argument = current_argument + matcher = Mutant::Matcher.from_string(argument) + + unless matcher + raise Error, "Invalid matcher syntax: #{argument.inspect}" + end + + @matchers << matcher + + consume(1) + end + + def dispatch_option + argument = current_argument + arguments = *OPTIONS.fetch(argument) do + raise Error, "Unknown option: #{argument.inspect}" + end + send(*arguments) + end + + def add_filter(klass) + @filters << klass.new(current_option_value) + consume(2) + end + + def matcher + if @matchers.empty? + raise Error, 'No matchers given' + end + + Mutant::Matcher::Chain.new(@matchers) + end + + def mutation_filter + if @filters.empty? + Mutation::Filter::ALL + else + Mutation::Filter::Whitelist.new(@filters) + end + end + + end +end diff --git a/spec/unit/mutant/cli/class_methods/run_spec.rb b/spec/unit/mutant/cli/class_methods/run_spec.rb new file mode 100644 index 00000000..e784da5d --- /dev/null +++ b/spec/unit/mutant/cli/class_methods/run_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Mutant::CLI, '.run' do + subject { object.run(argv) } + + let(:object) { described_class } + let(:argv) { mock('ARGV') } + let(:options) { mock('Options') } + let(:runner) { mock('Runner') } + let(:instance) { mock(described_class.name, :runner_options => options) } + + before do + described_class.stub(:new => instance) + Mutant::Runner.stub(:run => runner) + end + + it { should be(runner) } + + it 'should run with options' do + Mutant::Runner.should_receive(:run).with(options).and_return(runner) + should be(runner) + end +end diff --git a/spec/unit/mutant/cli/runner_options_spec.rb b/spec/unit/mutant/cli/runner_options_spec.rb new file mode 100644 index 00000000..a92cc6d7 --- /dev/null +++ b/spec/unit/mutant/cli/runner_options_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +shared_examples_for 'an invalid cli run' do + it 'should raise error' do + expect { subject }.to raise_error(described_class::Error, expected_message) + end +end + +describe Mutant::CLI, '#runner_options' do + subject { object.runner_options } + + let(:object) { described_class.new(arguments) } + + context 'with unknown option' do + let(:arguments) { %w(--invalid Foo) } + + let(:expected_message) { 'Unknown option: "--invalid"' } + + it_should_behave_like 'an invalid cli run' + end + + context 'without arguments' do + let(:arguments) { [] } + + let(:expected_message) { 'No matchers given' } + + it_should_behave_like 'an invalid cli run' + end + + context 'with code filter and missing argument' do + let(:arguments) { %w(--code) } + + let(:expected_message) { '"--code" is missing an argument' } + + it_should_behave_like 'an invalid cli run' + end + + context 'with explicit method matcher' do + let(:arguments) { %w(TestApp::Literal#float) } + + let(:expected_options) do + { + :matcher => Mutant::Matcher::Chain.new([Mutant::Matcher::Method.parse('TestApp::Literal#float')]), + :mutation_filter => Mutant::Mutation::Filter::ALL, + :killer => Mutant::Killer::Rspec + } + end + + it { should eql(expected_options) } + end + + context 'with library name' do + let(:arguments) { %w(::TestApp) } + + let(:expected_options) do + { + :matcher => Mutant::Matcher::Chain.new([Mutant::Matcher::ObjectSpace.new(%r(\ATestApp(::)?\z))]), + :mutation_filter => Mutant::Mutation::Filter::ALL, + :killer => Mutant::Killer::Rspec + } + end + + it { should eql(expected_options) } + end + + context 'with code filter' do + let(:arguments) { %w(--code faa --code bbb TestApp::Literal#float) } + + let(:filters) do + [ + Mutant::Mutation::Filter::Code.new('faa'), + Mutant::Mutation::Filter::Code.new('bbb'), + ] + end + + let(:expected_options) do + { + :mutation_filter => Mutant::Mutation::Filter::Whitelist.new(filters), + :matcher => Mutant::Matcher::Chain.new([Mutant::Matcher::Method.parse('TestApp::Literal#float')]), + :killer => Mutant::Killer::Rspec + } + end + + it { should eql(expected_options) } + end +end