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:
parent
dc893bfd7d
commit
10c3dfc390
34 changed files with 1070 additions and 108 deletions
11
.travis.yml
Normal file
11
.travis.yml
Normal 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
2
TODO
|
@ -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?
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
---
|
||||
threshold: 8
|
||||
total_score: 50
|
||||
threshold: 11
|
||||
total_score: 96
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
18
lib/mutant/context.rb
Normal 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
|
106
lib/mutant/context/constant.rb
Normal file
106
lib/mutant/context/constant.rb
Normal 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
82
lib/mutant/loader.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
85
lib/mutant/mutatee.rb
Normal 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
23
lib/mutant/mutator.rb
Normal 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
|
|
@ -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
|
||||
|
|
39
spec/shared/method_match_behavior.rb
Normal file
39
spec/shared/method_match_behavior.rb
Normal 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
|
|
@ -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
48
spec/support/fake_ast.rb
Normal 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
|
6
spec/support/sample_subjects.rb
Normal file
6
spec/support/sample_subjects.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
module SampleSubjects
|
||||
module ExampleModule
|
||||
def foo; end
|
||||
def self.bar; end
|
||||
end
|
||||
end
|
|
@ -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
|
24
spec/unit/mutant/context/constant/root_spec.rb
Normal file
24
spec/unit/mutant/context/constant/root_spec.rb
Normal 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
|
13
spec/unit/mutant/context/constant/unqualified_name_spec.rb
Normal file
13
spec/unit/mutant/context/constant/unqualified_name_spec.rb
Normal 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
|
11
spec/unit/mutant/context/root_spec.rb
Normal file
11
spec/unit/mutant/context/root_spec.rb
Normal 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
|
7
spec/unit/mutant/loader/class_methods/load_spec.rb
Normal file
7
spec/unit/mutant/loader/class_methods/load_spec.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Mutant::Loader,'.load' do
|
||||
subject { object.load(node) }
|
||||
|
||||
let(:object) { described_class }
|
||||
end
|
18
spec/unit/mutant/matcher/context_spec.rb
Normal file
18
spec/unit/mutant/matcher/context_spec.rb
Normal 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
|
21
spec/unit/mutant/matcher/method/context_spec.rb
Normal file
21
spec/unit/mutant/matcher/method/context_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
12
spec/unit/mutant/mutatee/class_methods/new_spec.rb
Normal file
12
spec/unit/mutant/mutatee/class_methods/new_spec.rb
Normal 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
|
13
spec/unit/mutant/mutatee/context_spec.rb
Normal file
13
spec/unit/mutant/mutatee/context_spec.rb
Normal 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
|
27
spec/unit/mutant/mutatee/each_spec.rb
Normal file
27
spec/unit/mutant/mutatee/each_spec.rb
Normal 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
|
12
spec/unit/mutant/mutatee/node_spec.rb
Normal file
12
spec/unit/mutant/mutatee/node_spec.rb
Normal 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
|
26
spec/unit/mutant/mutatee/reset_spec.rb
Normal file
26
spec/unit/mutant/mutatee/reset_spec.rb
Normal 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
|
|
@ -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 = []
|
||||
|
|
Loading…
Add table
Reference in a new issue