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
|
gemspec
|
||||||
|
|
||||||
|
# For Veritas::Immutable, will be extracted soon
|
||||||
|
gem 'veritas', :git => 'https://github.com/dkubb/veritas'
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'rake', '~> 0.9.2'
|
gem 'rake', '~> 0.9.2'
|
||||||
gem 'rspec', '~> 1.3.2'
|
gem 'rspec', '~> 1.3.2'
|
||||||
|
|
|
@ -7,6 +7,13 @@ GIT
|
||||||
ruby_parser (~> 2.0)
|
ruby_parser (~> 2.0)
|
||||||
sexp_processor (~> 3.0)
|
sexp_processor (~> 3.0)
|
||||||
|
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/dkubb/veritas
|
||||||
|
revision: 4654c1bc61b18938c38a5e3c2f599e14adda4991
|
||||||
|
specs:
|
||||||
|
veritas (0.0.7)
|
||||||
|
backports (~> 2.6.1)
|
||||||
|
|
||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
|
@ -145,6 +152,7 @@ DEPENDENCIES
|
||||||
roodi (~> 2.1.0)
|
roodi (~> 2.1.0)
|
||||||
rspec (~> 1.3.2)
|
rspec (~> 1.3.2)
|
||||||
ruby2ruby (= 1.2.2)
|
ruby2ruby (= 1.2.2)
|
||||||
|
veritas!
|
||||||
yard (~> 0.8.1)
|
yard (~> 0.8.1)
|
||||||
yard-spellcheck (~> 0.1.5)
|
yard-spellcheck (~> 0.1.5)
|
||||||
yardstick (~> 0.5.0)
|
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.
|
* Get a rid of heckle and test mutant with mutant.
|
||||||
This is interesting IMHO mutant should have another entry point
|
This is interesting IMHO mutant should have another entry point
|
||||||
that does not create the ::Mutant namespace, ideas:
|
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
|
* Maybe the full clone could be generated by evaluating the full mutant ast
|
||||||
a second time with a differend module name ast node.
|
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)
|
* 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 }
|
Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each { |f| require f }
|
||||||
|
|
||||||
Spec::Runner.configure do |config|
|
Spec::Runner.configure do |config|
|
||||||
config.extend Spec::ExampleGroupMethods
|
|
||||||
end
|
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