Progress on method matching

* Adjust metrics
* Add initial integration spec on method matching
* Yard and Heckle coverage is at 100% (heckle cov is disputable)
* Rcov does not really make sense as MRI 1.8 cannot reach all code
  paths.
This commit is contained in:
Markus Schirp 2012-07-24 01:41:08 +02:00
parent d74481b8fb
commit dc893bfd7d
21 changed files with 446 additions and 105 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
/.rbx /.rbx
/Gemfile.lock /Gemfile.lock
/tmp
/coverage

View file

@ -1,3 +1,3 @@
--- ---
threshold: 9 threshold: 8
total_score: 41 total_score: 50

View file

@ -1,2 +1,2 @@
--- ---
threshold: 12.4 threshold: 13.7

View file

@ -11,7 +11,7 @@ ClassVariableCheck: {}
CyclomaticComplexityBlockCheck: CyclomaticComplexityBlockCheck:
complexity: 2 complexity: 2
CyclomaticComplexityMethodCheck: CyclomaticComplexityMethodCheck:
complexity: 2 complexity: 3
EmptyRescueBodyCheck: {} EmptyRescueBodyCheck: {}
ForLoopCheck: {} ForLoopCheck: {}
MethodLineCountCheck: MethodLineCountCheck:

View file

@ -8,10 +8,10 @@ UncommunicativeParameterName:
- !ruby/regexp /[0-9]$/ - !ruby/regexp /[0-9]$/
- !ruby/regexp /[A-Z]/ - !ruby/regexp /[A-Z]/
LargeClass: LargeClass:
max_methods: 10 max_methods: 11
exclude: [] exclude: []
enabled: true enabled: true
max_instance_variables: 3 max_instance_variables: 2
UncommunicativeMethodName: UncommunicativeMethodName:
accept: [] accept: []
exclude: [] exclude: []

View file

@ -23,11 +23,30 @@ module Mutant
# @return [undefined] # @return [undefined]
# #
# @api private # @api private
#
def self.not_implemented(object) def self.not_implemented(object)
method = caller(1).first[/`(.*)'/,1].to_sym method = caller(1).first[/`(.*)'/,1].to_sym
delimiter = object.kind_of?(Module) ? '.' : '#' constant_name,delimiter = not_implemented_info(object)
raise NotImplementedError,"#{object.class}#{delimiter}#{method} is not implemented" raise NotImplementedError,"#{constant_name}#{delimiter}#{method} is not implemented"
end end
# Return name and delimiter
#
# @param [Object] object
#
# @return [Array]
#
# @api private
#
def self.not_implemented_info(object)
if object.kind_of?(Module)
[object.name,'.']
else
[object.class.name,'#']
end
end
private_class_method :not_implemented_info
end end
require 'mutant/matcher' require 'mutant/matcher'

View file

@ -7,6 +7,8 @@ module Mutant
# #
# @api private # @api private
# #
# @return [undefined]
#
def each def each
Mutant.not_implemented(self) Mutant.not_implemented(self)
end end

View file

@ -2,35 +2,6 @@ module Mutant
class Matcher class Matcher
# A filter for methods # A filter for methods
class Method < Matcher 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 # Parse a method string into filter
# #
# @param [String] input # @param [String] input
@ -53,15 +24,69 @@ module Mutant
# #
# @api private # @api private
# #
def each def each(&block)
return to_enum(__method__) unless block_given? return to_enum(__method__) unless block_given?
yield root_node node = root_node
yield node if node
self self
end end
private
# Return constant name
#
# @return [String]
#
# @api private
#
attr_reader :constant_name
private :constant_name
# Return method name
#
# @return [String]
#
# @api private
#
attr_reader :method_name
private :method_name
# Initialize method filter
#
# @param [String] constant_name
# @param [Symbol] method_name
#
# @return [undefined]
#
# @api private
#
def initialize(constant_name, method_name)
@constant_name, @method_name = constant_name, method_name
end
# Return method
#
# @return [UnboundMethod]
#
# @api private
#
def method
Mutant.not_implemented(self)
end
# Return node classes this matcher matches
#
# @return [Rubinius::AST::Node]
#
# @api private
#
def node_class
Mutant.not_implemented(self)
end
# Check if node is matched # Check if node is matched
# #
# @param [Rubinius::AST::Node] # @param [Rubinius::AST::Node] node
# #
# @return [true] # @return [true]
# returns true if node matches method # returns true if node matches method
@ -72,44 +97,9 @@ module Mutant
# @api private # @api private
# #
def match?(node) def match?(node)
node.line == source_file_line && node_class == node.class && node.name.to_s == method_name node.line == source_file_line &&
end node.class == node_class &&
node.name == method_name
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 end
# Return full ast # Return full ast
@ -152,6 +142,21 @@ module Mutant
method.source_location method.source_location
end end
# Return root node
#
# @return [Rubinus::AST]
#
# @api private
#
def root_node
root_node = nil
ast.walk do |predicate, node|
root_node = node if match?(node)
true
end
root_node
end
# Return constant # Return constant
# #
# @return [Class|Module] # @return [Class|Module]

View file

@ -3,6 +3,8 @@ module Mutant
class Method < Matcher class Method < Matcher
# A classifier for input strings # A classifier for input strings
class Classifier class Classifier
extend Veritas::Immutable
TABLE = { TABLE = {
'.' => Matcher::Method::Singleton, '.' => Matcher::Method::Singleton,
'#' => Matcher::Method::Instance '#' => Matcher::Method::Instance
@ -12,35 +14,81 @@ module Mutant
private_class_method :new private_class_method :new
# Run classifier
#
# @param [String] input
#
# @return [Matcher::Method]
#
# @api private
#
def self.run(input) def self.run(input)
match = SCOPE_FORMAT.match(input) match = SCOPE_FORMAT.match(input)
raise ArgumentError, "Cannot determine subject from #{input.inspect}" unless match raise ArgumentError, "Cannot determine subject from #{input.inspect}" unless match
new(match).matcher new(match).matcher
end end
public
# Return method matcher
#
# @return [Matcher::Method]
#
# @api private
#
def matcher def matcher
scope.new(constant_name, method_name) matcher_class.new(constant_name, method_name)
end end
private private
# Initialize matcher
#
# @param [MatchData] match
#
# @api private
#
def initialize(match) def initialize(match)
@match = match @match = match
end end
# Return constant name
#
# @return [String]
#
# @api private
#
def constant_name def constant_name
@match[1] @match[1]
end end
# Return method name
#
# @return [String]
#
# @api private
#
def method_name def method_name
@match[3] @match[3].to_sym
end end
# Return scope symbol
#
# @return [Symbol]
#
# @api private
#
def scope_symbol def scope_symbol
@match[2] @match[2]
end end
def scope # Return matcher class
#
# @return [Class<Matcher>]
#
# @api private
#
def matcher_class
TABLE.fetch(scope_symbol) TABLE.fetch(scope_symbol)
end end
end end

View file

@ -4,10 +4,22 @@ module Mutant
# A instance method filter # A instance method filter
class Instance < Method class Instance < Method
private private
# Return method instance
#
# @return [UnboundMethod]
#
# @api private
#
def method def method
constant.instance_method(method_name) constant.instance_method(method_name)
end end
# Return matched node class
#
# @return [Rubinius::AST::Define]
#
# @api private
#
def node_class def node_class
Rubinius::AST::Define Rubinius::AST::Define
end end

View file

@ -4,12 +4,24 @@ module Mutant
# A singleton method filter # A singleton method filter
class Singleton < Method class Singleton < Method
private private
# Return method instance
#
# @return [UnboundMethod]
#
# @api private
#
def method def method
constant.method(method_name) constant.method(method_name)
end end
# Return matched node class
#
# @return [Rubinius::AST::Define]
#
# @api private
#
def node_class def node_class
Rubinius::AST::DefineSingleton Rubinius::AST::DefineSingletonScope
end end
end end
end end

View file

@ -0,0 +1,82 @@
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
end
end
attr_reader :foo
def multiline(
foo
)
end
end
describe Mutant,'method matching' do
def match(input)
Mutant::Matcher::Method.parse(input).to_a.first
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
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
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
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
# 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
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)
end
end
end

View file

@ -0,0 +1,35 @@
require 'spec_helper'
describe Mutant,'.not_implemented' do
let(:object) { described_class.new }
let(:described_class) do
Class.new do
def foo
Mutant.not_implemented(self)
end
def self.foo
Mutant.not_implemented(self)
end
def self.name
'Test'
end
end
end
context 'on instance method' do
subject { object.foo }
it 'should raise error' do
expect { subject }.to raise_error(NotImplementedError,'Test#foo is not implemented')
end
end
context 'on singleton method' do
subject { described_class.foo }
it 'should raise error' do
expect { subject }.to raise_error(NotImplementedError,'Test.foo is not implemented')
end
end
end

View file

@ -1,34 +1,21 @@
require 'spec_helper' 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 describe Mutant::Matcher::Method, '.parse' do
subject { described_class.parse(input) } subject { described_class.parse(input) }
before do
expected_class.stub(:new => response)
end
let(:response) { mock('Response') } let(:response) { mock('Response') }
let(:input) { mock('Input') }
context 'when input is in instance method format' do let(:classifier) { described_class::Classifier }
let(:input) { 'Foo#bar' }
let(:expected_class) { described_class::Instance }
it_should_behave_like 'a method filter parse result' before do
classifier.stub(:run => response)
end end
context 'when input is in singleton method format' do it { should be(response) }
let(:input) { 'Foo.bar' }
let(:expected_class) { described_class::Singleton }
it_should_behave_like 'a method filter parse result' it 'should call classifier' do
classifier.should_receive(:run).with(input).and_return(response)
subject
end end
end end

View file

@ -0,0 +1,44 @@
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::Classifier, '.run' do
subject { described_class.run(input) }
context 'with format' do
before do
expected_class.stub(:new => response)
end
let(:response) { mock('Response') }
context 'in instance method notation' do
let(:input) { 'Foo#bar' }
let(:expected_class) { Mutant::Matcher::Method::Instance }
it_should_behave_like 'a method filter parse result'
end
context 'when input is in singleton method notation' do
let(:input) { 'Foo.bar' }
let(:expected_class) { Mutant::Matcher::Method::Singleton }
it_should_behave_like 'a method filter parse result'
end
end
context 'when input is not in a valid format' do
let(:input) { 'Foo' }
it 'should raise error' do
expect { subject }.to raise_error(ArgumentError,"Cannot determine subject from #{input.inspect}")
end
end
end

View file

@ -0,0 +1,29 @@
require 'spec_helper'
# This method cannot be called directly, spec only exists for heckle demands
describe Mutant::Matcher::Method::Classifier,'#matcher' do
subject { object.matcher }
let(:object) { described_class.send(:new,match) }
let(:match) { [mock,constant_name,scope_symbol,method_name] }
let(:constant_name) { mock('Constant Name') }
let(:method_name) { 'foo' }
context 'with "#" as scope symbol' do
let(:scope_symbol) { '#' }
it { should be_a(Mutant::Matcher::Method::Instance) }
its(:method_name) { should be(method_name.to_sym) }
its(:constant_name) { should be(constant_name) }
end
context 'with "." as scope symbol' do
let(:scope_symbol) { '.' }
it { should be_a(Mutant::Matcher::Method::Singleton) }
its(:method_name) { should be(method_name.to_sym) }
its(:constant_name) { should be(constant_name) }
end
end

View file

@ -0,0 +1,39 @@
require 'spec_helper'
# This method implementation cannot be called from the outside, but heckle needs to be happy.
describe Mutant::Matcher::Method,'#each' do
let(:class_under_test) do
node = self.root_node
Class.new(described_class) do
define_method(:root_node) do
node
end
end
end
subject { object.each { |item| yields << item } }
let(:object) { class_under_test.allocate }
let(:yields) { [] }
it_should_behave_like 'an #each method'
let(:root_node) { mock('Root Node') }
context 'with match' do
it 'should yield root node' do
expect { subject }.to change { yields.dup }.from([]).to([root_node])
end
end
context 'without match' do
let(:root_node) { nil }
it 'should yield nothing' do
subject
yields.should eql([])
end
end
end

View file

@ -0,0 +1,11 @@
require 'spec_helper'
describe Mutant::Matcher::Method,'#method' do
subject { object.send(:method) }
let(:object) { described_class.allocate }
it 'should raise error' do
expect { subject }.to raise_error(NotImplementedError,'Mutant::Matcher::Method#method is not implemented')
end
end

View file

@ -0,0 +1,12 @@
require 'spec_helper'
describe Mutant::Matcher::Method,'#node_class' do
subject { object.send(:node_class) }
let(:object) { described_class.allocate }
it 'should raise error' do
expect { subject }.to raise_error(NotImplementedError,'Mutant::Matcher::Method#node_class is not implemented')
end
end

View file

@ -29,12 +29,12 @@ begin
end end
desc 'Heckle each module and class' desc 'Heckle each module and class'
task :heckle => :rcov do task :heckle do
unless Ruby2Ruby::VERSION == '1.2.2' unless Ruby2Ruby::VERSION == '1.2.2'
raise "ruby2ruby version #{Ruby2Ruby::VERSION} may not work properly, 1.2.2 *only* is recommended for use with heckle" raise "ruby2ruby version #{Ruby2Ruby::VERSION} may not work properly, 1.2.2 *only* is recommended for use with heckle"
end end
require 'veritas-mongo-adapter' require 'mutant'
root_module_regexp = Regexp.union('Mutant') root_module_regexp = Regexp.union('Mutant')

View file

@ -1,4 +1,6 @@
begin begin
# Require veritas before metric foo pulls AS
require 'veritas'
require 'metric_fu' require 'metric_fu'
require 'json' require 'json'