Add a spike runner

This commit is contained in:
Markus Schirp 2012-08-14 22:45:34 +02:00
parent a11803f0c8
commit d20655f4c2
19 changed files with 246 additions and 120 deletions

View file

@ -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'

View file

@ -39,8 +39,8 @@ module Mutant
#
# @api private
#
def killed?
@killed
def fail?
!@killed
end
private

View file

@ -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

View file

@ -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<Matcher>]
# @return [Class:Mutant::Matcher]
#
# @api private
#

View file

@ -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]

View file

@ -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

View file

@ -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

128
lib/mutant/runner.rb Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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|

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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