Add method matcher infrastructure
Needs more specs for sure. Especially edge cases.
This commit is contained in:
parent
ef472cef20
commit
df6ccafeab
20 changed files with 437 additions and 2 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/.rbx
|
||||
/Gemfile.lock
|
3
Gemfile
3
Gemfile
|
@ -4,6 +4,9 @@ source 'https://rubygems.org'
|
|||
|
||||
gemspec
|
||||
|
||||
# For Veritas::Immutable, will be extracted soon
|
||||
gem 'veritas', :git => 'https://github.com/dkubb/veritas'
|
||||
|
||||
group :development do
|
||||
gem 'rake', '~> 0.9.2'
|
||||
gem 'rspec', '~> 1.3.2'
|
||||
|
|
|
@ -7,6 +7,13 @@ GIT
|
|||
ruby_parser (~> 2.0)
|
||||
sexp_processor (~> 3.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/dkubb/veritas
|
||||
revision: 4654c1bc61b18938c38a5e3c2f599e14adda4991
|
||||
specs:
|
||||
veritas (0.0.7)
|
||||
backports (~> 2.6.1)
|
||||
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
|
@ -145,6 +152,7 @@ DEPENDENCIES
|
|||
roodi (~> 2.1.0)
|
||||
rspec (~> 1.3.2)
|
||||
ruby2ruby (= 1.2.2)
|
||||
veritas!
|
||||
yard (~> 0.8.1)
|
||||
yard-spellcheck (~> 0.1.5)
|
||||
yardstick (~> 0.5.0)
|
||||
|
|
6
Rakefile
Normal file
6
Rakefile
Normal file
|
@ -0,0 +1,6 @@
|
|||
require 'rake'
|
||||
|
||||
FileList['tasks/**/*.rake'].each { |task| import task }
|
||||
|
||||
desc 'Default: run all specs'
|
||||
task :default => :spec
|
3
TODO
3
TODO
|
@ -1,3 +1,4 @@
|
|||
* Add a nice way to access the root ast to place the mutated ast nodes into.
|
||||
* Get a rid of heckle and test mutant with mutant.
|
||||
This is interesting IMHO mutant should have another entry point
|
||||
that does not create the ::Mutant namespace, ideas:
|
||||
|
@ -8,4 +9,4 @@
|
|||
* Maybe the full clone could be generated by evaluating the full mutant ast
|
||||
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 components to heckle.
|
||||
* Add an infrastructure to whitelist for components to heckle on ruby-1.8.
|
||||
|
|
37
lib/mutant.rb
Normal file
37
lib/mutant.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
# For Veritas::Immutable will be extracted soon
|
||||
require 'veritas'
|
||||
|
||||
# Library namespace
|
||||
module Mutant
|
||||
# Helper method for raising not implemented exceptions
|
||||
#
|
||||
# @param [Object] object
|
||||
# the object where method is not implemented
|
||||
#
|
||||
# @raise [NotImplementedError]
|
||||
# raises a not implemented error with correct description
|
||||
#
|
||||
# @example
|
||||
# class Foo
|
||||
# def x
|
||||
# Mutant.not_implemented(self)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Foo.new.x # raises NotImplementedError "Foo#x is not implemented"
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
def self.not_implemented(object)
|
||||
method = caller(1).first[/`(.*)'/,1].to_sym
|
||||
delimiter = object.kind_of?(Module) ? '.' : '#'
|
||||
raise NotImplementedError,"#{object.class}#{delimiter}#{method} is not implemented"
|
||||
end
|
||||
end
|
||||
|
||||
require 'mutant/matcher'
|
||||
require 'mutant/matcher/method'
|
||||
require 'mutant/matcher/method/singleton'
|
||||
require 'mutant/matcher/method/instance'
|
||||
require 'mutant/matcher/method/classifier'
|
14
lib/mutant/matcher.rb
Normal file
14
lib/mutant/matcher.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
module Mutant
|
||||
# Abstract filter for rubinius asts.
|
||||
class Matcher
|
||||
include Enumerable
|
||||
|
||||
# Return each matched node
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def each
|
||||
Mutant.not_implemented(self)
|
||||
end
|
||||
end
|
||||
end
|
168
lib/mutant/matcher/method.rb
Normal file
168
lib/mutant/matcher/method.rb
Normal file
|
@ -0,0 +1,168 @@
|
|||
module Mutant
|
||||
class Matcher
|
||||
# A filter for methods
|
||||
class Method < Matcher
|
||||
# Return constant name
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
attr_reader :constant_name
|
||||
|
||||
# Return method name
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
attr_reader :method_name
|
||||
|
||||
# Initialize method filter
|
||||
#
|
||||
# @param [String] constant_name
|
||||
# @param [String] method_name
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def initialize(constant_name,method_name)
|
||||
@constant_name,@method_name = constant_name,method_name
|
||||
end
|
||||
|
||||
# Parse a method string into filter
|
||||
#
|
||||
# @param [String] input
|
||||
#
|
||||
# @return [Matcher::Method]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def self.parse(input)
|
||||
Classifier.run(input).filter
|
||||
end
|
||||
|
||||
# Enumerate matches
|
||||
#
|
||||
# @return [Enumerable]
|
||||
# returns enumerable when no block given
|
||||
#
|
||||
# @return [self]
|
||||
# returns self when block given
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def each
|
||||
return to_enum(__method__) unless block_given?
|
||||
yield root_node
|
||||
self
|
||||
end
|
||||
|
||||
# Check if node is matched
|
||||
#
|
||||
# @param [Rubinius::AST::Node]
|
||||
#
|
||||
# @return [true]
|
||||
# returns true if node matches method
|
||||
#
|
||||
# @return [false]
|
||||
# returns false if node NOT matches method
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def match?(node)
|
||||
node.line == source_file_line && node_class == node.class && node.name.to_s == method_name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Return method
|
||||
#
|
||||
# @return [UnboundMethod]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def method
|
||||
Mutant.not_implemente(self)
|
||||
end
|
||||
|
||||
# Return node classes this matcher matches
|
||||
#
|
||||
# @return [Enumerable]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def node_classes
|
||||
Mutant.not_implemented(self)
|
||||
end
|
||||
|
||||
# Return root node
|
||||
#
|
||||
# @return [Rubinus::AST::Node]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def root_node
|
||||
root_node = nil
|
||||
ast.walk do |_,node|
|
||||
root_node = node if match?(node)
|
||||
true
|
||||
end
|
||||
root_node
|
||||
end
|
||||
|
||||
# Return full ast
|
||||
#
|
||||
# @return [Rubinius::AST::Node]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def ast
|
||||
File.read(source_filename).to_ast
|
||||
end
|
||||
|
||||
# Return source filename
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def source_filename
|
||||
source_location.first
|
||||
end
|
||||
|
||||
# Return source file line
|
||||
#
|
||||
# @return [Integer]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def source_file_line
|
||||
source_location.last
|
||||
end
|
||||
|
||||
# Return source location
|
||||
#
|
||||
# @return [Array]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def source_location
|
||||
method.source_location
|
||||
end
|
||||
|
||||
# Return constant
|
||||
#
|
||||
# @return [Class|Module]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def constant
|
||||
constant_name.split(/::/).inject(Object) do |context, name|
|
||||
context.const_get(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
53
lib/mutant/matcher/method/classifier.rb
Normal file
53
lib/mutant/matcher/method/classifier.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
module Mutant
|
||||
class Matcher
|
||||
class Method < Matcher
|
||||
# A classifier for input strings
|
||||
class Classifier
|
||||
TABLE = {
|
||||
'.' => Matcher::Method::Singleton,
|
||||
'#' => Matcher::Method::Instance
|
||||
}
|
||||
|
||||
SCOPE_FORMAT = Regexp.new('\A([^#.]+)(\.|#)(.+)\z')
|
||||
|
||||
private_class_method :new
|
||||
|
||||
def self.run(input)
|
||||
match = SCOPE_FORMAT.match(input)
|
||||
raise ArgumentError,"Cannot determine subject from #{input.inspect}" unless match
|
||||
new(match)
|
||||
end
|
||||
|
||||
def filter
|
||||
scope.new(constant_name,method_name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize(match)
|
||||
@match = match
|
||||
end
|
||||
|
||||
def constant_name
|
||||
@match[1]
|
||||
end
|
||||
|
||||
def method_name
|
||||
@match[3]
|
||||
end
|
||||
|
||||
def scope_name
|
||||
@match[2]
|
||||
end
|
||||
|
||||
def scope
|
||||
TABLE.fetch(scope_name)
|
||||
end
|
||||
|
||||
def method
|
||||
scope.method(method_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
17
lib/mutant/matcher/method/instance.rb
Normal file
17
lib/mutant/matcher/method/instance.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
module Mutant
|
||||
class Matcher
|
||||
class Method < Matcher
|
||||
# A instance method filter
|
||||
class Instance < Method
|
||||
private
|
||||
def method
|
||||
constant.instance_method(method_name)
|
||||
end
|
||||
|
||||
def node_class
|
||||
Rubinius::AST::Define
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
17
lib/mutant/matcher/method/singleton.rb
Normal file
17
lib/mutant/matcher/method/singleton.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
module Mutant
|
||||
class Matcher
|
||||
class Method
|
||||
# A singleton method filter
|
||||
class Singleton < Method
|
||||
private
|
||||
def method
|
||||
constant.method(method_name)
|
||||
end
|
||||
|
||||
def node_class
|
||||
Rubinius::AST::DefineSingleton
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
7
spec/rcov.opts
Normal file
7
spec/rcov.opts
Normal file
|
@ -0,0 +1,7 @@
|
|||
--exclude-only "spec/,^/"
|
||||
--sort coverage
|
||||
--callsites
|
||||
--xrefs
|
||||
--profile
|
||||
--text-summary
|
||||
--failure-threshold 100
|
7
spec/shared/command_method_behavior.rb
Normal file
7
spec/shared/command_method_behavior.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# encoding: utf-8
|
||||
|
||||
shared_examples_for 'a command method' do
|
||||
it 'returns self' do
|
||||
should equal(object)
|
||||
end
|
||||
end
|
15
spec/shared/each_method_behaviour.rb
Normal file
15
spec/shared/each_method_behaviour.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# encoding: utf-8
|
||||
|
||||
shared_examples_for 'an #each method' do
|
||||
it_should_behave_like 'a command method'
|
||||
|
||||
context 'with no block' do
|
||||
subject { object.each }
|
||||
|
||||
it { should be_instance_of(to_enum.class) }
|
||||
|
||||
it 'yields the expected values' do
|
||||
subject.to_a.should eql(object.to_a)
|
||||
end
|
||||
end
|
||||
end
|
17
spec/shared/hash_method_behavior.rb
Normal file
17
spec/shared/hash_method_behavior.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# encoding: utf-8
|
||||
|
||||
shared_examples_for 'a hash method' do
|
||||
it_should_behave_like 'an idempotent method'
|
||||
|
||||
specification = proc do
|
||||
should be_instance_of(Fixnum)
|
||||
end
|
||||
|
||||
it 'is a fixnum' do
|
||||
instance_eval(&specification)
|
||||
end
|
||||
|
||||
it 'memoizes the hash code' do
|
||||
subject.should eql(object.memoized(:hash))
|
||||
end
|
||||
end
|
7
spec/shared/idempotent_method_behavior.rb
Normal file
7
spec/shared/idempotent_method_behavior.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# encoding: utf-8
|
||||
|
||||
shared_examples_for 'an idempotent method' do
|
||||
it 'is idempotent' do
|
||||
should equal(instance_eval(&self.class.subject))
|
||||
end
|
||||
end
|
9
spec/shared/invertible_method_behaviour.rb
Normal file
9
spec/shared/invertible_method_behaviour.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# encoding: utf-8
|
||||
|
||||
shared_examples_for 'an invertible method' do
|
||||
it_should_behave_like 'an idempotent method'
|
||||
|
||||
it 'is invertible' do
|
||||
subject.inverse.should equal(object)
|
||||
end
|
||||
end
|
|
@ -8,5 +8,4 @@ require 'spec/autorun'
|
|||
Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each { |f| require f }
|
||||
|
||||
Spec::Runner.configure do |config|
|
||||
config.extend Spec::ExampleGroupMethods
|
||||
end
|
||||
|
|
14
spec/unit/mutant/matcher/each_spec.rb
Normal file
14
spec/unit/mutant/matcher/each_spec.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
require 'spec_helper'
|
||||
|
||||
# This spec is only present to ensure 100% test coverage.
|
||||
# The code should not be triggered on runtime.
|
||||
|
||||
describe Mutant::Matcher,'#each' do
|
||||
subject { object.send(:each) }
|
||||
|
||||
let(:object) { described_class.allocate }
|
||||
|
||||
it 'should raise error' do
|
||||
expect { subject }.to raise_error(NotImplementedError,'Mutant::Matcher#each is not implemented')
|
||||
end
|
||||
end
|
34
spec/unit/mutant/matcher/method/class_methods/parse_spec.rb
Normal file
34
spec/unit/mutant/matcher/method/class_methods/parse_spec.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
require 'spec_helper'
|
||||
|
||||
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)
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
describe Mutant::Matcher::Method,'.parse' do
|
||||
subject { described_class.parse(input) }
|
||||
|
||||
before do
|
||||
expected_class.stub(:new => response)
|
||||
end
|
||||
|
||||
let(:response) { mock('Response') }
|
||||
|
||||
context 'when input is in instance method format' do
|
||||
let(:input) { 'Foo#bar' }
|
||||
let(:expected_class) { described_class::Instance }
|
||||
|
||||
it_should_behave_like 'a method filter parse result'
|
||||
end
|
||||
|
||||
context 'when input is in singleton method format' do
|
||||
let(:input) { 'Foo.bar' }
|
||||
let(:expected_class) { described_class::Singleton }
|
||||
|
||||
it_should_behave_like 'a method filter parse result'
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue