Add a spike runner
This commit is contained in:
parent
a11803f0c8
commit
d20655f4c2
19 changed files with 246 additions and 120 deletions
|
@ -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'
|
||||
|
|
|
@ -39,8 +39,8 @@ module Mutant
|
|||
#
|
||||
# @api private
|
||||
#
|
||||
def killed?
|
||||
@killed
|
||||
def fail?
|
||||
!@killed
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
128
lib/mutant/runner.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
19
spec/integration/mutant/runner_spec.rb
Normal file
19
spec/integration/mutant/runner_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
Loading…
Reference in a new issue