Finalize method matching

* Add tests for all edge cases I could create
* Add infrastructure for loading mutations into the vm.
* The fun part is next!
This commit is contained in:
Markus Schirp 2012-07-26 19:25:23 +02:00
parent dc893bfd7d
commit 10c3dfc390
34 changed files with 1070 additions and 108 deletions

11
.travis.yml Normal file
View file

@ -0,0 +1,11 @@
before_install:
- gem install bundler
language: ruby
script: 'bundle exec rake ci'
rvm:
- 1.8.7
- rbx-18mode
- rbx-19mode
notifications:
email:
- mbj@seonic.net

2
TODO
View file

@ -10,3 +10,5 @@
a second time with a differend module name ast node.
* Get a rid of rspec-1 (can be done once we do not use heckle anymore)
* Add an infrastructure to whitelist for components to heckle on ruby-1.8.
* Allow matches on attr_reader with literal name argument(s)?
* Allow matches on define_method with literal name argument?

View file

@ -1,3 +1,3 @@
---
threshold: 8
total_score: 50
threshold: 11
total_score: 96

View file

@ -11,7 +11,7 @@ ClassVariableCheck: {}
CyclomaticComplexityBlockCheck:
complexity: 2
CyclomaticComplexityMethodCheck:
complexity: 3
complexity: 4
EmptyRescueBodyCheck: {}
ForLoopCheck: {}
MethodLineCountCheck:
@ -23,4 +23,4 @@ ModuleLineCountCheck:
ModuleNameCheck:
pattern: !ruby/regexp /\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/
ParameterNumberCheck:
parameter_count: 2
parameter_count: 3

View file

@ -8,10 +8,11 @@ UncommunicativeParameterName:
- !ruby/regexp /[0-9]$/
- !ruby/regexp /[A-Z]/
LargeClass:
max_methods: 11
exclude: []
max_methods: 10
exclude:
- "Mutant::Matcher::Method" # 13 methods
enabled: true
max_instance_variables: 2
max_instance_variables: 3
UncommunicativeMethodName:
accept: []
exclude: []
@ -22,7 +23,8 @@ UncommunicativeMethodName:
- !ruby/regexp /[A-Z]/
LongParameterList:
max_params: 2
exclude: []
exclude:
- "Mutant::Context::Constant#initialize" # 3 params
enabled: true
overrides: {}
FeatureEnvy:

View file

@ -1,4 +1,4 @@
# For Veritas::Immutable will be extracted soon
# For Veritas::Immutable. will be extracted soon
require 'veritas'
# Library namespace
@ -13,12 +13,12 @@ module Mutant
#
# @example
# class Foo
# def x
# def bar
# Mutant.not_implemented(self)
# end
# end
#
# Foo.new.x # raises NotImplementedError "Foo#x is not implemented"
# Foo.new.x # raises NotImplementedError "Foo#bar is not implemented"
#
# @return [undefined]
#
@ -49,6 +49,11 @@ module Mutant
private_class_method :not_implemented_info
end
require 'mutant/mutator'
require 'mutant/loader'
require 'mutant/context'
require 'mutant/context/constant'
require 'mutant/mutatee'
require 'mutant/matcher'
require 'mutant/matcher/method'
require 'mutant/matcher/method/singleton'

18
lib/mutant/context.rb Normal file
View file

@ -0,0 +1,18 @@
module Mutant
# An abstract context where mutations can be appied to.
class Context
include Veritas::Immutable
# Return root ast for mutated node
#
# @param [Rubinius::AST::Node] node
#
# @return [Rubinis::AST::Script]
#
# @api private
#
def root(node)
Mutant.not_implemented(self)
end
end
end

View file

@ -0,0 +1,106 @@
module Mutant
class Context
# Constant context for mutation (Class or Module)
class Constant < Context
include Veritas::Immutable
private_class_method :new
TABLE = {
::Class => ['class', Rubinius::AST::ClassScope],
::Module => ['module', Rubinius::AST::ModuleScope]
}.freeze
# Build constant from class or module
#
# @param [::Class|::Module] value
#
# @return [Constant]
#
# @api private
#
def self.build(value)
arguments = TABLE.fetch(value.class) do
raise ArgumentError, 'Can only build constant mutation scope from class or module'
end
new(value,*arguments)
end
# Return ast wrapping mutated node
#
# @return [Rubinius::AST::Script]
#
# @api private
#
def root(node)
root = root_ast
root.body = [@scope_class.new(1,root.name,[node])]
Rubinius::AST::Script.new(root)
end
# Return unqualified name of constant
#
# @return [String]
#
# @api private
#
def unqualified_name
name_nesting.last
end
private
# Return constant wrapped by context
#
# @return [::Module|::Class]
#
# @api private
#
attr_reader :constant
private :constant
# Initialize object
#
# @param [Object] constant
#
# @api private
#
def initialize(constant,keyword,scope_class)
@constant,@keyword,@scope_class = constant,keyword,scope_class
end
# Return new root ast
#
# @return [Rubinius::AST::Node]
#
# @api private
#
def root_ast
"#{@keyword} #{qualified_name}; end".to_ast
end
# Return qualified name of constant
#
# @return [String]
#
# @api private
#
def qualified_name
@constant.name
end
# Return nesting of names of constant
#
# @return [Array<String>]
#
# @api private
#
def name_nesting
@constant.name.split('::')
end
memoize :unqualified_name
end
end
end

82
lib/mutant/loader.rb Normal file
View file

@ -0,0 +1,82 @@
module Mutant
# A method object for inserting an AST into the Rubinius VM
#
# The idea is to split the steps for a mutation into documented
# methods. Also subclasses can override the steps. Also passing
# around the root node is not needed with a method object.
#
# As the initializer does the work there is no need for the
# instances of this class to be used outside of this class, hence
# the Loader.new method is private and the Loader.run method
# returns self.
#
class Loader
private_class_method :new
# Load an AST into the rubinius VM
#
# @param [Rubinius::AST::Script] root
# A root AST node to be loaded into the VM
#
# @return [self]
#
# @api private
#
def self.load(root)
new(root)
self
end
private
# Initialize and insert mutation into vm
#
# @param [Rubinius::AST::Script] root
#
# @return [undefined]
#
# @api private
#
def initialize(root)
@root = root
Rubinius.run_script(script)
end
# Return compiled method script for node
#
# @return [Rubinius::CompiledMethod::Script]A
#
# @api private
#
def script
compiled_code.create_script
end
# Return compiled code for node
#
# This method actually returns a Rubnius::CompiledMethod
# instance. But it is named on the future name of CompiledMethod
# that will be renamed to Rubinius::CompiledCode.
#
# @return [Rubinius::CompiledMethod]
#
# @api private
#
def compiled_code
compiler.run
end
# Return compiler loaded with mutated ast
#
# @return [Rubinius::Compiler]
#
# @api private
#
def compiler
Rubinius::Compiler.new(:bytecode, :compiled_method).tap do |compiler|
compiler.generator.input(@root)
end
end
end
end

View file

@ -1,9 +1,9 @@
module Mutant
# Abstract filter for rubinius asts.
# Abstract matcher to find ASTs to mutate
class Matcher
include Enumerable
# Return each matched node
# Enumerate mutatees
#
# @api private
#
@ -12,5 +12,8 @@ module Mutant
def each
Mutant.not_implemented(self)
end
private
end
end

View file

@ -1,7 +1,9 @@
module Mutant
class Matcher
# A filter for methods
# Matcher to find AST for method
class Method < Matcher
include Veritas::Immutable
# Parse a method string into filter
#
# @param [String] input
@ -26,21 +28,25 @@ module Mutant
#
def each(&block)
return to_enum(__method__) unless block_given?
node = root_node
yield node if node
mutatee.tap do |mutatee|
yield mutatee if mutatee
end
self
end
private
# Return constant name
# Return context of matcher
#
# @return [String]
# @return [Context]
# returns the context this matcher matches AST nodes
#
# @api private
#
attr_reader :constant_name
private :constant_name
def context
Context::Constant.build(constant)
end
private
# Return method name
#
@ -51,6 +57,16 @@ module Mutant
attr_reader :method_name
private :method_name
# Return constant name
#
# @return [String]
#
# @api private
#
attr_reader :constant_name
private :constant_name
# Initialize method filter
#
# @param [String] constant_name
@ -61,7 +77,7 @@ module Mutant
# @api private
#
def initialize(constant_name, method_name)
@constant_name, @method_name = constant_name, method_name
@constant_name,@method_name = constant_name, method_name
end
# Return method
@ -142,19 +158,27 @@ module Mutant
method.source_location
end
# Return root node
# Return matched node
#
# @return [Rubinus::AST]
# @return [Rubinis::AST::Node]
#
# @api private
#
def root_node
root_node = nil
ast.walk do |predicate, node|
root_node = node if match?(node)
true
def matched_node
Mutant.not_implemented(self)
end
# Return mutatee
#
# @return [Mutatee]
#
# @api private
#
def mutatee
node = matched_node
if node
Mutatee.new(context,node)
end
root_node
end
# Return constant
@ -164,10 +188,12 @@ module Mutant
# @api private
#
def constant
constant_name.split(/::/).inject(Object) do |context, name|
context.const_get(name)
constant_name.split('::').inject(::Object) do |parent,name|
parent.const_get(name)
end
end
memoize :mutatee
end
end
end

View file

@ -1,9 +1,11 @@
module Mutant
class Matcher
class Method < Matcher
# A instance method filter
# Matcher for instance methods
class Instance < Method
private
# Return method instance
#
# @return [UnboundMethod]
@ -23,6 +25,21 @@ module Mutant
def node_class
Rubinius::AST::Define
end
# Return matched node
#
# @return [Rubinus::AST::Define]
#
# @api private
#
def matched_node
last_match = nil
ast.walk do |predicate, node|
last_match = node if match?(node)
predicate
end
last_match
end
end
end
end

View file

@ -1,9 +1,11 @@
module Mutant
class Matcher
class Method
# A singleton method filter
# Matcher for singleton methods
class Singleton < Method
private
# Return method instance
#
# @return [UnboundMethod]
@ -16,13 +18,90 @@ module Mutant
# Return matched node class
#
# @return [Rubinius::AST::Define]
# @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
# artifical edge case where DefineSingleton nodes
# with differend receivers exist on the same line.
#
# @param [Rubnius::AST::Node] node
#
# @return [true]
# returns true when node should NOT be followed
#
# @return [false]
# returns false when node can be followed
#
# @api private
#
def stop?(node)
node.is_a?(Rubinius::AST::DefineSingleton) && !match_receiver?(node)
end
# Check if receiver matches
#
# @param [Rubinius::AST::DefineSingleton] node
#
# @return [true]
# returns true when receiver is self or constant from pattern
#
# @return [false]
# returns false otherwise
#
# @api private
#
def match_receiver?(node)
receiver = node.receiver
case receiver
when Rubinius::AST::Self
true
when Rubinius::AST::ConstantAccess
match_receiver_name?(receiver)
else
raise 'Can only match receiver on Rubinius::AST::Self or Rubinius::AST::ConstantAccess'
end
end
# Check if reciver name matches context
#
# @param [Rubinius::AST::Node] node
#
# @return [true]
# returns true when node name matches unqualified constant name
#
# @return [false]
# returns false otherwise
#
# @api private
#
def match_receiver_name?(node)
node.name.to_s == context.unqualified_name
end
# Return matched node
#
# @return [Rubinus::AST::DefineSingletonScope]
#
# @api private
#
def matched_node
last_match = nil
ast.walk do |predicate, node|
if match?(node)
last_match = node
end
!stop?(node)
end
last_match
end
end
end
end

85
lib/mutant/mutatee.rb Normal file
View file

@ -0,0 +1,85 @@
module Mutant
# Represent a mutatable AST and its context
class Mutatee
include Veritas::Immutable, Enumerable
# Return context
#
# @return [Context]
#
# @api private
#
attr_reader :context
# Return AST node
#
# @return [Rubinius::AST::Node]
#
# @api private
#
attr_reader :node
# Enumerate possible mutations
#
# @return [self]
# returns self if block given
#
# @return [Enumerator]
# returns eumerator if no block given
#
# @api private
#
def each(&block)
return to_enum(__method__) unless block_given?
Mutator.new(node).each(&block)
self
end
# Reset implementation to original
#
# This method inserts the original node again.
#
# @return [self]
#
# @api private
#
def reset
insert(@node)
self
end
private
# Initialize a mutatee
#
# @param [Context] context
# the context of mutations
#
# @param [Rubinius::AST::Node] node
# the node to be mutated
#
# @return [unkown]
#
# @api private
#
def initialize(context,node)
@context,@node = context,node
end
# Insert AST node under context
#
# @param [Rubinius::AST::Node] node
#
# @return [self]
#
# @api private
#
def insert(node)
Loader.load(context.root(node))
self
end
end
end

23
lib/mutant/mutator.rb Normal file
View file

@ -0,0 +1,23 @@
module Mutant
# Mutate rubinius AST nodes
class Mutator
# Initialize mutator
#
# @param [Rubinius::AST::Node] node
#
# @api private
#
def initialize(node)
@node = node
end
# # Enumerate mutated asts
# #
# # @api private
# #
# def each
# return to_enum(__method__) unless block_given?
# self
# end
end
end

View file

@ -1,82 +1,254 @@
require 'spec_helper'
if defined?(Rubinius)
class Toplevel
def simple
end
def self.simple
end
def multiple; end;
def multiple(foo); end
def self.complex; end; def complex(foo); end
class Nested
def foo
if "".respond_to?(:to_ast)
describe Mutant,'method matching' do
after do
if defined?(::Foo)
Object.send(:remove_const,'Foo')
end
end
attr_reader :foo
def multiline(
foo
)
before do
eval(body)
File.stub(:read => body)
end
end
let(:defaults) { {} }
describe Mutant,'method matching' do
def match(input)
Mutant::Matcher::Method.parse(input).to_a.first
context 'on instance methods' do
let(:pattern) { 'Foo#bar' }
let(:defaults) do
{
:constant => Foo,
:node_class => Rubinius::AST::Define,
:method_name => :bar,
:method_arity => 0
}
end
context 'when method is defined once' do
let(:body) do
<<-RUBY
class Foo
def bar; end
end
RUBY
end
let(:expectation) do
{ :method_line => 2 }
end
it_should_behave_like 'a method match'
end
context 'when method is defined multiple times' do
context 'on differend lines' do
let(:body) do
<<-RUBY
class Foo
def bar; end
def bar(arg); end
end
RUBY
end
let(:expectation) do
{
:method_line => 3,
:method_arity => 1
}
end
it_should_behave_like 'a method match'
end
context 'on the same line' do
let(:body) do
<<-RUBY
class Foo
def bar; end; def bar(arg); end
end
RUBY
end
let(:expectation) do
{
:method_line => 2,
:method_arity => 1
}
end
it_should_behave_like 'a method match'
end
context 'on the same line with differend scope' do
let(:body) do
<<-RUBY
class Foo
def self.bar; end; def bar(arg); end
end
RUBY
end
let(:expectation) do
{
:method_line => 2,
:method_arity => 1
}
end
it_should_behave_like 'a method match'
end
context 'when nested' do
let(:pattern) { 'Foo::Bar#baz' }
context 'in class' do
let(:body) do
<<-RUBY
class Foo
class Bar
def baz; end
end
end
RUBY
end
let(:expectation) do
{
:method_line => 3,
:method_name => :baz,
:constant => Foo::Bar
}
end
it_should_behave_like 'a method match'
end
context 'in module' do
let(:body) do
<<-RUBY
module Foo
class Bar
def baz; end
end
end
RUBY
end
let(:expectation) do
{
:method_line => 3,
:method_name => :baz,
:constant => Foo::Bar
}
end
it_should_behave_like 'a method match'
end
end
end
end
it 'allows to match simple instance methods' do
match = match('Toplevel#simple')
match.name.should be(:simple)
match.line.should be(4)
match.arguments.required.should be_empty
end
context 'on singleton methods' do
let(:pattern) { 'Foo.bar' }
let(:defaults) do
{
:constant => Foo,
:node_class => Rubinius::AST::DefineSingletonScope,
:method_arity => 0
}
end
it 'allows to match simple singleton methods' do
match = match('Toplevel.simple')
match.name.should be(:simple)
match.line.should be(7)
match.arguments.required.should be_empty
end
context 'when defined on self' do
let(:body) do
<<-RUBY
class Foo
def self.bar; end
end
RUBY
end
it 'returns last method definition' do
match = match('Toplevel#multiple')
match.name.should be(:multiple)
match.line.should be(11)
match.arguments.required.length.should be(1)
end
it 'does not fail on multiple definitions of differend scope per row' do
match = match('Toplevel.complex')
match.name.should be(:complex)
match.line.should be(13)
match.arguments.required.length.should be(0)
end
let(:expectation) do
{
:method_name => :bar,
:method_line => 2,
}
end
it 'allows matching on nested methods' do
match = match('Toplevel::Nested#foo')
match.name.should be(:foo)
match.line.should be(16)
match.arguments.required.length.should be(0)
end
it_should_behave_like 'a method match'
end
# pending 'allows matching on attr_readers' do
# match = match('Toplevel#foo')
# match.name.should be(:foo)
# match.line.should be(19)
# match.arguments.required.length.should be(0)
# end
context 'when defined on constant' do
it 'does not fail on multi line defs' do
match = match('Toplevel#multiline')
match.name.should be(:multiline)
match.line.should be(23)
match.arguments.required.length.should be(1)
context 'inside namespace' do
let(:body) do
<<-RUBY
class Foo
def Foo.bar; end
end
RUBY
end
let(:expectation) do
{
:method_name => :bar,
:method_line => 2,
}
end
it_should_behave_like 'a method match'
end
context 'outside namespace' do
let(:body) do
<<-RUBY
class Foo; end;
def Foo.bar; end
RUBY
end
let(:expectation) do
{
:method_name => :bar,
:method_line => 2,
}
end
it_should_behave_like 'a method match'
end
end
context 'when defined multiple times in the same line' do
context 'with method on differend scope' do
let(:body) do
<<-RUBY
module Foo; end
module Bar
def self.baz; end; def Foo.baz(arg); end
end
RUBY
end
let(:pattern) { 'Bar.baz' }
let(:expectation) do
{
:constant => Bar,
:method_name => :baz,
:method_line => 4,
:method_arity => 0
}
end
it_should_behave_like 'a method match'
end
end
end
end
end

View file

@ -0,0 +1,39 @@
shared_examples_for 'a method match' do
subject { Mutant::Matcher::Method.parse(pattern).to_a }
let(:values) { defaults.merge(expectation) }
let(:method_name) { values.fetch(:method_name) }
let(:method_line) { values.fetch(:method_line) }
let(:method_arity) { values.fetch(:method_arity) }
let(:constant) { values.fetch(:constant) }
let(:node_class) { values.fetch(:node_class) }
let(:node) { mutatee.node }
let(:context) { mutatee.context }
let(:mutatee) { subject.first }
it 'should return one mutatee' do
subject.size.should be(1)
end
it 'should have correct method name' do
node.name.should eql(method_name)
end
it 'should have correct line number' do
node.line.should eql(method_line)
end
it 'should have correct arity' do
node.arguments.required.length.should eql(method_arity)
end
it 'should have correct constant in context' do
context.send(:constant).should eql(constant)
end
it 'should have the correct node class' do
node.should be_a(node_class)
end
end

View file

@ -1,11 +1,12 @@
# encoding: utf-8
require 'mutant'
require 'spec'
require 'spec/autorun'
# require spec support files and shared behavior
Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each { |f| require f }
require 'mutant'
Spec::Runner.configure do |config|
end

48
spec/support/fake_ast.rb Normal file
View file

@ -0,0 +1,48 @@
unless defined?(Rubinius)
module Rubinius
# Dummy AST namespace
module AST
# Dummy node
class Node
attr_reader :line, :name
attr_accessor :body
def initialize(line,name,body=[])
@line,@name,@body = line,name,body
end
end
class ConstantScope < Node
end
class ClassScope < ConstantScope
end
class ModuleScope < ConstantScope
end
class Script < Node
end
# Dummy class node
class Class < Node
def initialize(line,name,superclass,body)
super(line,name)
@superclass,@body = superclass,body
end
end
# Dummy module node
class Module < Node
def initialize(line,name,body)
super(line,name)
@body = body
end
end
class ConstantAccess < Node
end
end
end
end

View file

@ -0,0 +1,6 @@
module SampleSubjects
module ExampleModule
def foo; end
def self.bar; end
end
end

View file

@ -0,0 +1,42 @@
require 'spec_helper'
describe Mutant::Context::Constant,'.build' do
subject { described_class.build(constant) }
let(:object) { described_class }
let(:context) { mock('Context') }
before do
described_class.stub(:new => context)
end
context 'when constant is a module' do
let(:constant) { Module.new }
it 'should initialize context correctly' do
described_class.should_receive(:new).with(constant,'module',Rubinius::AST::ModuleScope).and_return(context)
should be(context)
end
it { should be(context) }
end
context 'when constant is a class' do
let(:constant) { Class.new }
it 'should initialize context correctly' do
described_class.should_receive(:new).with(constant,'class',Rubinius::AST::ClassScope).and_return(context)
should be(context)
end
it { should be(context) }
end
context 'when constant is not a class nor a module' do
let(:constant) { Object.new }
it 'should raise error' do
expect { subject }.to raise_error(ArgumentError,'Can only build constant mutation scope from class or module')
end
end
end

View file

@ -0,0 +1,24 @@
require 'spec_helper'
if "".respond_to?(:to_ast)
describe Mutant::Context::Constant, '#root' do
subject { object.root(node) }
let(:object) { described_class.build(SampleSubjects::ExampleModule) }
let(:node) { mock('Node') }
let(:constant) { subject.body }
let(:scope) { constant.body.first }
let(:scope_body) { scope.body }
it { should be_a(Rubinius::AST::Script) }
it 'should wrap the ast under constant' do
scope.should be_kind_of(Rubinius::AST::ModuleScope)
end
it 'should place the ast under scope body' do
scope_body.should == [node]
end
end
end

View file

@ -0,0 +1,13 @@
require 'spec_helper'
describe Mutant::Context::Constant, '#unqualified_name' do
subject { object.unqualified_name }
let(:object) { described_class.build(SampleSubjects::ExampleModule) }
it 'should return wrapped constants unqualified name' do
should eql('ExampleModule')
end
it_should_behave_like 'an idempotent method'
end

View file

@ -0,0 +1,11 @@
require 'spec_helper'
describe Mutant::Context,'#root' do
subject { object.root(mock) }
let(:object) { described_class.allocate }
it 'should raise error' do
expect { subject }.to raise_error('Mutant::Context#root is not implemented')
end
end

View file

@ -0,0 +1,7 @@
require 'spec_helper'
describe Mutant::Loader,'.load' do
subject { object.load(node) }
let(:object) { described_class }
end

View file

@ -0,0 +1,18 @@
require 'spec_helper'
describe Mutant::Matcher,'#context' do
subject { object.context }
let(:object) { described_class.new(constant_name) }
let(:constant_name) { 'Foo' }
let(:context) { mock('Context') }
before do
Mutant::Context.stub(:build => context)
end
#it { should be(context) }
#it_should_behave_like 'an idempotent method'
end

View file

@ -0,0 +1,21 @@
require 'spec_helper'
describe Mutant::Matcher::Method,'#context' do
subject { object.context }
let(:object) { described_class::Singleton.new('SampleSubjects::ExampleModule','foo') }
let(:context) { mock('Context') }
before do
Mutant::Context::Constant.stub(:build => context)
end
it { should be(context); }
it 'should build context with subject' do
Mutant::Context::Constant.should_receive(:build).with(::SampleSubjects::ExampleModule).and_return(context)
should be(context)
end
it_should_behave_like 'an idempotent method'
end

View file

@ -4,15 +4,18 @@ require 'spec_helper'
describe Mutant::Matcher::Method,'#each' do
let(:class_under_test) do
node = self.root_node
node = self.matched_node
Class.new(described_class) do
define_method(:root_node) do
define_method(:matched_node) do
node
end
define_method(:constant) do
::SampleSubjects::ExampleModule
end
end
end
subject { object.each { |item| yields << item } }
let(:object) { class_under_test.allocate }
@ -20,16 +23,16 @@ describe Mutant::Matcher::Method,'#each' do
it_should_behave_like 'an #each method'
let(:root_node) { mock('Root Node') }
let(:matched_node) { mock('Root Node') }
context 'with match' do
it 'should yield root node' do
expect { subject }.to change { yields.dup }.from([]).to([root_node])
it 'should yield mutatee' do
expect { subject }.to change { yields.dup }.from([]).to([object.send(:mutatee)])
end
end
context 'without match' do
let(:root_node) { nil }
let(:matched_node) { nil }
it 'should yield nothing' do
subject

View file

@ -0,0 +1,12 @@
require 'spec_helper'
describe Mutant::Mutatee,'.new' do
subject { object.new(context,ast) }
let(:object) { described_class }
let(:context) { mock('Context') }
let(:ast) { mock('AST') }
it { should be_frozen }
end

View file

@ -0,0 +1,13 @@
require 'spec_helper'
describe Mutant::Mutatee,'#context' do
subject { object.context }
let(:object) { described_class.new(context,ast) }
let(:ast) { mock('AST') }
let(:context) { mock('Context') }
it { should be(context) }
it_should_behave_like 'an idempotent method'
end

View file

@ -0,0 +1,27 @@
require 'spec_helper'
describe Mutant::Mutatee,'#each' do
subject { object.each { |item| yields << item } }
let(:object) { described_class.new(context,ast) }
let(:root) { mock('Root AST') }
let(:ast) { mock('AST') }
let(:context) { mock('Context', :root => root) }
let(:mutation) { mock('Mutation') }
let(:mutator) { [mutation] }
let(:yields) { [] }
before do
Mutant::Mutator.stub(:new => mutator)
end
it_should_behave_like 'an #each method'
#it 'should initialize mutator with ast' do
# Mutant::Mutator.should_receive(:new).with(ast).and_return(mutator)
#end
it 'should yield mutations' do
expect { subject }.to change { yields.dup }.from([]).to([mutation])
end
end

View file

@ -0,0 +1,12 @@
require 'spec_helper'
describe Mutant::Mutatee,'#node' do
subject { object.node }
let(:object) { described_class.new(context,node) }
let(:node) { mock('Node') }
let(:context) { mock('Context') }
it { should be(node) }
it_should_behave_like 'an idempotent method'
end

View file

@ -0,0 +1,26 @@
require 'spec_helper'
describe Mutant::Mutatee,'#reset' do
subject { object.reset }
let(:object) { described_class.new(context,ast) }
let(:root) { mock('Root AST') }
let(:ast) { mock('AST') }
let(:context) { mock('Context', :root => root) }
it_should_behave_like 'a command method'
before do
Mutant::Loader.stub(:load => Mutant::Loader)
end
it 'should create root ast from context' do
context.should_receive(:root).with(ast).and_return(root)
should be(object)
end
it 'should insert root ast' do
Mutant::Loader.should_receive(:load).with(root).and_return(Mutant::Loader)
should be(object)
end
end

View file

@ -34,6 +34,7 @@ begin
raise "ruby2ruby version #{Ruby2Ruby::VERSION} may not work properly, 1.2.2 *only* is recommended for use with heckle"
end
require File.expand_path('../../../spec/support/fake_ast',__FILE__)
require 'mutant'
root_module_regexp = Regexp.union('Mutant')
@ -54,6 +55,13 @@ begin
ObjectSpace.each_object(Module) do |mod|
next unless mod.name =~ /\A#{root_module_regexp}(?::|\z)/
# Mutation::Loader is to rbx specific
next if mod == Mutant::Loader
# Mutation::Matcher::Method is to rbx specific
next if mod == Mutant::Matcher::Method
# Mutation::Context::Constant is to rbx specific
next if mod == Mutant::Context::Constant
spec_prefix = spec_dir.join(mod.name.underscore)
specs = []