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

View file

@ -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
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.
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
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 }
Spec::Runner.configure do |config|
config.extend Spec::ExampleGroupMethods
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