diff --git a/lib/mutant.rb b/lib/mutant.rb index 38876986..1247bf3a 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -3,6 +3,7 @@ require 'abstract' require 'securerandom' require 'to_source' require 'ice_nine' +require 'backports' # Library namespace module Mutant @@ -35,8 +36,6 @@ module Mutant end require 'mutant/random' -require 'mutant/killer' -require 'mutant/killer/rspec' require 'mutant/mutator' require 'mutant/mutator/registry' require 'mutant/mutator/literal' @@ -66,3 +65,7 @@ require 'mutant/matcher/method' require 'mutant/matcher/method/singleton' require 'mutant/matcher/method/instance' require 'mutant/matcher/method/classifier' + +require 'mutant/killer' +require 'mutant/killer/rspec' +require 'mutant/runner' diff --git a/lib/mutant/killer.rb b/lib/mutant/killer.rb index 66bb34a9..6762579e 100644 --- a/lib/mutant/killer.rb +++ b/lib/mutant/killer.rb @@ -39,8 +39,8 @@ module Mutant # # @api private # - def killed? - @killed + def fail? + !@killed end private diff --git a/lib/mutant/matcher/method.rb b/lib/mutant/matcher/method.rb index d1d38d5f..60052218 100644 --- a/lib/mutant/matcher/method.rb +++ b/lib/mutant/matcher/method.rb @@ -46,6 +46,20 @@ module Mutant Context::Constant.build(source_path, constant) end + # Initialize method filter + # + # @param [Class|Module] constant + # @param [Symbol] method_name + # + # @return [undefined] + # + # @api private + # + def initialize(constant, method_name) + raise if constant.kind_of?(String) + @constant, @method_name = constant, method_name.to_sym + end + private # Return method name @@ -57,27 +71,23 @@ module Mutant attr_reader :method_name private :method_name + # Return constant + # + # @return [Class|Module] + # + # @api private + # + attr_reader :constant + private :constant + # Return constant name # # @return [String] # # @api private # - attr_reader :constant_name - private :constant_name - - - # Initialize method filter - # - # @param [String] constant_name - # @param [Symbol] method_name - # - # @return [undefined] - # - # @api private - # - def initialize(constant_name, method_name) - @constant_name, @method_name = constant_name, method_name + def constant_name + @constant.name end # Return method @@ -94,7 +104,9 @@ module Mutant # # @api private # - abstract_method :node_class + def node_class + self.class::NODE_CLASS + end # Check if node is matched # @@ -109,7 +121,7 @@ module Mutant # @api private # def match?(node) - node.line == source_file_line && + node.line == source_line && node.class == node_class && node.name == method_name end @@ -121,9 +133,6 @@ module Mutant # @api private # def ast - if source_path == '(mutant)' - raise 'Trying to mutate mutated method!' - end File.read(source_path).to_ast end @@ -143,7 +152,7 @@ module Mutant # # @api private # - def source_file_line + def source_line source_location.last end @@ -181,19 +190,6 @@ module Mutant Subject.new(context, node) end end - - # Return constant - # - # @return [Class|Module] - # - # @api private - # - def constant - constant_name.split('::').inject(::Object) do |parent, name| - parent.const_get(name) - end - end - memoize :subject end end diff --git a/lib/mutant/matcher/method/classifier.rb b/lib/mutant/matcher/method/classifier.rb index 9e0e27ac..9d081114 100644 --- a/lib/mutant/matcher/method/classifier.rb +++ b/lib/mutant/matcher/method/classifier.rb @@ -3,7 +3,7 @@ module Mutant class Method < self # A classifier for input strings class Classifier - extend Immutable + include Immutable TABLE = { '.' => Matcher::Method::Singleton, @@ -34,8 +34,6 @@ module Mutant new(match).matcher end - public - # Return method matcher # # @return [Matcher::Method] @@ -43,7 +41,7 @@ module Mutant # @api private # def matcher - matcher_class.new(constant_name, method_name) + matcher_class.new(constant, method_name) end private @@ -58,6 +56,18 @@ module Mutant @match = match end + # Return constant + # + # @return [Class|Module] + # + # @api private + # + def constant + constant_name.split('::').inject(::Object) do |parent, name| + parent.const_get(name) + end + end + # Return constant name # # @return [String] @@ -90,7 +100,7 @@ module Mutant # Return matcher class # - # @return [Class] + # @return [Class:Mutant::Matcher] # # @api private # diff --git a/lib/mutant/matcher/method/instance.rb b/lib/mutant/matcher/method/instance.rb index 260f1399..2703e441 100644 --- a/lib/mutant/matcher/method/instance.rb +++ b/lib/mutant/matcher/method/instance.rb @@ -4,6 +4,14 @@ module Mutant # Matcher for instance methods class Instance < self + NODE_CLASS = Rubinius::AST::Define + + def self.extract(constant) + constant.public_instance_methods(false).map do |name| + new(constant, name) + end + end + private # Return method instance @@ -16,16 +24,6 @@ module Mutant constant.instance_method(method_name) end - # Return matched node class - # - # @return [Rubinius::AST::Define] - # - # @api private - # - def node_class - Rubinius::AST::Define - end - # Return matched node # # @return [Rubinus::AST::Define] diff --git a/lib/mutant/matcher/method/singleton.rb b/lib/mutant/matcher/method/singleton.rb index d6ea2049..f09a26b8 100644 --- a/lib/mutant/matcher/method/singleton.rb +++ b/lib/mutant/matcher/method/singleton.rb @@ -4,6 +4,17 @@ module Mutant # Matcher for singleton methods class Singleton < self + NODE_CLASS = Rubinius::AST::DefineSingletonScope + + def self.extract(constant) + return [] + constant.singleton_class.public_instance_methods(false).reject do |method| + method.to_sym == :__class_init__ + end.map do |name| + new(constant, name) + end + end + private # Return method instance @@ -16,16 +27,6 @@ module Mutant constant.method(method_name) end - # Return matched node class - # - # @return [Rubinius::AST::DefineSingletonScope] - # - # @api private - # - def node_class - Rubinius::AST::DefineSingletonScope - end - # Check for stopping AST walk on branch # # This method exist to protect against the diff --git a/lib/mutant/random.rb b/lib/mutant/random.rb index e4b1d887..8798579b 100644 --- a/lib/mutant/random.rb +++ b/lib/mutant/random.rb @@ -18,7 +18,7 @@ module Mutant # @api private # def self.fixnum - Random.rand(1000) + ::Random.rand(1000) end # Return random float @@ -28,7 +28,7 @@ module Mutant # @api private # def self.float - Random.rand + ::Random.rand end end end diff --git a/lib/mutant/runner.rb b/lib/mutant/runner.rb new file mode 100644 index 00000000..cb9d45da --- /dev/null +++ b/lib/mutant/runner.rb @@ -0,0 +1,128 @@ +module Mutant + class Runner + class Reporter + def self.run(*args) + new(*args) + end + + private + + def initialize(output, runner) + @output, @runner = output, runner + run + end + + def run + @runner.errors.each do |error| + print_error(error) + end + end + + def print_error(error) + Kill.run(output, error) + end + end + + class Reporter + class Kill + def self.run(*args) + new(*args) + end + + private + + def initialize(output, error) + @output, @error = output, error + run + end + + def mutant + @error.mutant + end + + def root_ast + end + end + end + end +end + +module Mutant + class Runner + include Immutable + + def self.run(options) + killer = options.fetch(:killer) do + raise ArgumentError, 'Missing :killer in options' + end + + pattern = options.fetch(:pattern) do + raise ArgumentError, 'Missing :pattern in options' + end + + new(killer, pattern) + end + + attr_reader :errors + + def errors? + errors.empty? + end + + + private + + def initialize(killer, pattern) + @killer, @pattern, @errors = killer, pattern, [] + run + end + + def matcher_classes + [Matcher::Method::Singleton, Matcher::Method::Instance] + end + + def constants + ObjectSpace.each_object(Module).select do |constant| + @pattern =~ constant.name + end + end + + def matchers + matcher_classes.each_with_object([]) do |klass, matchers| + matchers.concat(matches_for(klass)) + end + end + + def matches_for(klass) + constants.each_with_object([]) do |constant, matches| + matches.concat(klass.extract(constant)) + end + end + + def subjects + matchers.each_with_object([]) do |matcher, subjects| + subjects.concat(matcher.each.to_a) + end + end + + def killers + subjects.each_with_object([]) do |subject, killers| + killers.concat(killers_for(subject)) + end + end + + def killers_for(subject) + subject.map do |mutation| + @killer.run(subject, mutation) + end + end + + def run + killers.select do |killer| + killer.fail? + end.each do |killer| + errors << killer + end + end + end +end diff --git a/lib/mutant/subject.rb b/lib/mutant/subject.rb index dc606697..235d6458 100644 --- a/lib/mutant/subject.rb +++ b/lib/mutant/subject.rb @@ -68,11 +68,23 @@ module Mutant # @api private # def insert(node) - Loader.load(context.root(node)) + Loader.load(root(node)) self end + # Return root AST for node + # + # @param [Rubinius::AST::Node] node + # + # @return [Rubinius::AST::Node] + # + # @api private + # + def root(node) + context.root(node) + end + private # Initialize subject diff --git a/spec/integration/mutant/rspec_killer_spec.rb b/spec/integration/mutant/rspec_killer_spec.rb index ba7ddcf7..b625ed1e 100644 --- a/spec/integration/mutant/rspec_killer_spec.rb +++ b/spec/integration/mutant/rspec_killer_spec.rb @@ -13,7 +13,7 @@ describe Mutant,'rspec integration' do subject.each do |mutation| Mutant::Killer::Rspec.nest do runner = Mutant::Killer::Rspec.run(subject,mutation) - runner.killed?.should be(true) + runner.fail?.should be(false) end end end @@ -22,7 +22,7 @@ describe Mutant,'rspec integration' do subject.each do |mutation| Mutant::Killer::Rspec.nest do runner = Mutant::Killer::Rspec.run(subject,mutation) - runner.killed?.should be(false) + runner.fail?.should be(true) end end end diff --git a/spec/integration/mutant/runner_spec.rb b/spec/integration/mutant/runner_spec.rb new file mode 100644 index 00000000..aaf82695 --- /dev/null +++ b/spec/integration/mutant/runner_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Mutant, 'runner' do + around do |example| + Dir.chdir(TestApp.root) do + example.run + end + end + + it 'allows to run mutant over a project' do + Mutant::Killer::Rspec.nest do + report = Mutant::Runner.run( + :pattern => /\ATestApp::/, + :killer => Mutant::Killer::Rspec + ) + report.errors.size.should be(18) + end + end +end diff --git a/spec/shared/method_filter_parse_behavior.rb b/spec/shared/method_filter_parse_behavior.rb index f88a8848..7a80294e 100644 --- a/spec/shared/method_filter_parse_behavior.rb +++ b/spec/shared/method_filter_parse_behavior.rb @@ -8,7 +8,7 @@ shared_examples_for 'a method filter parse result' do it { should be(response) } it 'should initialize method filter with correct arguments' do - expected_class.should_receive(:new).with('Foo', :bar).and_return(response) + expected_class.should_receive(:new).with(TestApp::Literal, :string).and_return(response) subject end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0d387406..08986893 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,8 +6,8 @@ require 'rspec' Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each { |f| require f } $: << File.join(TestApp.root,'lib') -require 'test_app' +require 'test_app' require 'mutant' RSpec.configure do |config| diff --git a/spec/unit/mutant/killer/killed_ques_spec.rb b/spec/unit/mutant/killer/fail_ques_spec.rb similarity index 91% rename from spec/unit/mutant/killer/killed_ques_spec.rb rename to spec/unit/mutant/killer/fail_ques_spec.rb index dc7bc95f..33645063 100644 --- a/spec/unit/mutant/killer/killed_ques_spec.rb +++ b/spec/unit/mutant/killer/fail_ques_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Mutant::Killer,'#killed?' do - subject { object.killed? } +describe Mutant::Killer,'#fail?' do + subject { object.fail? } let(:object) { class_under_test.run(mutation_subject,mutant) } let(:mutation_subject) { mock('Subject', :insert => nil, :reset => nil) } @@ -21,7 +21,7 @@ describe Mutant::Killer,'#killed?' do it_should_behave_like 'an idempotent method' - it { should be(true) } + it { should be(false) } end context 'when mutant was NOT killed' do @@ -29,6 +29,6 @@ describe Mutant::Killer,'#killed?' do it_should_behave_like 'an idempotent method' - it { should be(false) } + it { should be(true) } end end diff --git a/spec/unit/mutant/killer/rspec/class_methods/run_spec.rb b/spec/unit/mutant/killer/rspec/class_methods/run_spec.rb index 37f999b7..15e7a4dc 100644 --- a/spec/unit/mutant/killer/rspec/class_methods/run_spec.rb +++ b/spec/unit/mutant/killer/rspec/class_methods/run_spec.rb @@ -17,14 +17,14 @@ describe Mutant::Killer::Rspec, '.run' do context 'when run exits zero' do let(:exit_status) { 0 } - its(:killed?) { should be(false) } + its(:fail?) { should be(true) } it { should be_a(described_class) } end context 'when run exits nonzero' do let(:exit_status) { 1 } - its(:killed?) { should be(true) } + its(:fail?) { should be(false) } it { should be_a(described_class) } end end diff --git a/spec/unit/mutant/matcher/method/classifier/class_methods/run_spec.rb b/spec/unit/mutant/matcher/method/classifier/class_methods/run_spec.rb index 9eccdbd1..4d156588 100644 --- a/spec/unit/mutant/matcher/method/classifier/class_methods/run_spec.rb +++ b/spec/unit/mutant/matcher/method/classifier/class_methods/run_spec.rb @@ -5,14 +5,14 @@ describe Mutant::Matcher::Method::Classifier, '.run' do context 'with instance method notation' do - let(:input) { 'Foo#bar' } + let(:input) { 'TestApp::Literal#string' } let(:expected_class) { Mutant::Matcher::Method::Instance } it_should_behave_like 'a method filter parse result' end context 'with singleton method notation' do - let(:input) { 'Foo.bar' } + let(:input) { 'TestApp::Literal.string' } let(:expected_class) { Mutant::Matcher::Method::Singleton } it_should_behave_like 'a method filter parse result' diff --git a/spec/unit/mutant/matcher/method/classifier/matcher_spec.rb b/spec/unit/mutant/matcher/method/classifier/matcher_spec.rb deleted file mode 100644 index ed0b0990..00000000 --- a/spec/unit/mutant/matcher/method/classifier/matcher_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' - -# This method cannot be called directly, spec only exists for heckle demands -describe Mutant::Matcher::Method::Classifier, '#matcher' do - subject { object.matcher } - - let(:object) { described_class.send(:new, match) } - - let(:match) { [mock, constant_name, scope_symbol, method_name] } - - let(:constant_name) { mock('Constant Name') } - let(:method_name) { 'foo' } - - context 'with "#" as scope symbol' do - let(:scope_symbol) { '#' } - - it { should be_a(Mutant::Matcher::Method::Instance) } - its(:method_name) { should be(method_name.to_sym) } - its(:constant_name) { should be(constant_name) } - end - - context 'with "." as scope symbol' do - let(:scope_symbol) { '.' } - - it { should be_a(Mutant::Matcher::Method::Singleton) } - its(:method_name) { should be(method_name.to_sym) } - its(:constant_name) { should be(constant_name) } - end -end diff --git a/spec/unit/mutant/matcher/method/context_spec.rb b/spec/unit/mutant/matcher/method/context_spec.rb index ca46ea82..8c7a3bb4 100644 --- a/spec/unit/mutant/matcher/method/context_spec.rb +++ b/spec/unit/mutant/matcher/method/context_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Mutant::Matcher::Method, '#context' do subject { object.context } - let(:object) { described_class::Singleton.new('TestApp::Literal', 'string') } + let(:object) { described_class::Singleton.new(TestApp::Literal, 'string') } let(:context) { mock('Context') } before do diff --git a/spec/unit/mutant/matcher/method/node_class_spec.rb b/spec/unit/mutant/matcher/method/node_class_spec.rb deleted file mode 100644 index 4ab947fd..00000000 --- a/spec/unit/mutant/matcher/method/node_class_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe Mutant::Matcher::Method, '#node_class' do - subject { object.send(:node_class) } - - let(:object) { described_class.allocate } - - it 'should raise error' do - expect { subject }.to raise_error(NotImplementedError, 'Mutant::Matcher::Method#node_class is not implemented') - end -end -