Add method matcher infrastructure

Needs more specs for sure. Especially edge cases.
This commit is contained in:
Markus Schirp 2012-07-23 22:54:35 +02:00
parent ef472cef20
commit df6ccafeab
20 changed files with 437 additions and 2 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/.rbx
/Gemfile.lock

View file

@ -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'

View file

@ -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
View file

@ -0,0 +1,6 @@
require 'rake'
FileList['tasks/**/*.rake'].each { |task| import task }
desc 'Default: run all specs'
task :default => :spec

3
TODO
View file

@ -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
View 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
View 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

View 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

View 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

View 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

View 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
View file

@ -0,0 +1,7 @@
--exclude-only "spec/,^/"
--sort coverage
--callsites
--xrefs
--profile
--text-summary
--failure-threshold 100

View file

@ -0,0 +1,7 @@
# encoding: utf-8
shared_examples_for 'a command method' do
it 'returns self' do
should equal(object)
end
end

View 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

View 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

View 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

View 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

View file

@ -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

View 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

View 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