From df6ccafeab9792b659e64eeb13043982792c9211 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Mon, 23 Jul 2012 22:54:35 +0200 Subject: [PATCH] Add method matcher infrastructure Needs more specs for sure. Especially edge cases. --- .gitignore | 2 + Gemfile | 3 + Gemfile.lock | 8 + Rakefile | 6 + TODO | 3 +- lib/mutant.rb | 37 ++++ lib/mutant/matcher.rb | 14 ++ lib/mutant/matcher/method.rb | 168 ++++++++++++++++++ lib/mutant/matcher/method/classifier.rb | 53 ++++++ lib/mutant/matcher/method/instance.rb | 17 ++ lib/mutant/matcher/method/singleton.rb | 17 ++ spec/rcov.opts | 7 + spec/shared/command_method_behavior.rb | 7 + spec/shared/each_method_behaviour.rb | 15 ++ spec/shared/hash_method_behavior.rb | 17 ++ spec/shared/idempotent_method_behavior.rb | 7 + spec/shared/invertible_method_behaviour.rb | 9 + spec/spec_helper.rb | 1 - spec/unit/mutant/matcher/each_spec.rb | 14 ++ .../method/class_methods/parse_spec.rb | 34 ++++ 20 files changed, 437 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Rakefile create mode 100644 lib/mutant.rb create mode 100644 lib/mutant/matcher.rb create mode 100644 lib/mutant/matcher/method.rb create mode 100644 lib/mutant/matcher/method/classifier.rb create mode 100644 lib/mutant/matcher/method/instance.rb create mode 100644 lib/mutant/matcher/method/singleton.rb create mode 100644 spec/rcov.opts create mode 100644 spec/shared/command_method_behavior.rb create mode 100644 spec/shared/each_method_behaviour.rb create mode 100644 spec/shared/hash_method_behavior.rb create mode 100644 spec/shared/idempotent_method_behavior.rb create mode 100644 spec/shared/invertible_method_behaviour.rb create mode 100644 spec/unit/mutant/matcher/each_spec.rb create mode 100644 spec/unit/mutant/matcher/method/class_methods/parse_spec.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..51d67d81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.rbx +/Gemfile.lock diff --git a/Gemfile b/Gemfile index 33a76e1a..8cce8735 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index 184dc5fb..413bcf6c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..5582ca2a --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +require 'rake' + +FileList['tasks/**/*.rake'].each { |task| import task } + +desc 'Default: run all specs' +task :default => :spec diff --git a/TODO b/TODO index ce694eb4..a3cb363d 100644 --- a/TODO +++ b/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. diff --git a/lib/mutant.rb b/lib/mutant.rb new file mode 100644 index 00000000..df4e90c2 --- /dev/null +++ b/lib/mutant.rb @@ -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' diff --git a/lib/mutant/matcher.rb b/lib/mutant/matcher.rb new file mode 100644 index 00000000..cda424df --- /dev/null +++ b/lib/mutant/matcher.rb @@ -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 diff --git a/lib/mutant/matcher/method.rb b/lib/mutant/matcher/method.rb new file mode 100644 index 00000000..78b377c4 --- /dev/null +++ b/lib/mutant/matcher/method.rb @@ -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 diff --git a/lib/mutant/matcher/method/classifier.rb b/lib/mutant/matcher/method/classifier.rb new file mode 100644 index 00000000..f3bf5178 --- /dev/null +++ b/lib/mutant/matcher/method/classifier.rb @@ -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 diff --git a/lib/mutant/matcher/method/instance.rb b/lib/mutant/matcher/method/instance.rb new file mode 100644 index 00000000..4754e944 --- /dev/null +++ b/lib/mutant/matcher/method/instance.rb @@ -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 diff --git a/lib/mutant/matcher/method/singleton.rb b/lib/mutant/matcher/method/singleton.rb new file mode 100644 index 00000000..1be34040 --- /dev/null +++ b/lib/mutant/matcher/method/singleton.rb @@ -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 diff --git a/spec/rcov.opts b/spec/rcov.opts new file mode 100644 index 00000000..fb8e0e08 --- /dev/null +++ b/spec/rcov.opts @@ -0,0 +1,7 @@ +--exclude-only "spec/,^/" +--sort coverage +--callsites +--xrefs +--profile +--text-summary +--failure-threshold 100 diff --git a/spec/shared/command_method_behavior.rb b/spec/shared/command_method_behavior.rb new file mode 100644 index 00000000..c62854d4 --- /dev/null +++ b/spec/shared/command_method_behavior.rb @@ -0,0 +1,7 @@ +# encoding: utf-8 + +shared_examples_for 'a command method' do + it 'returns self' do + should equal(object) + end +end diff --git a/spec/shared/each_method_behaviour.rb b/spec/shared/each_method_behaviour.rb new file mode 100644 index 00000000..d9b29d79 --- /dev/null +++ b/spec/shared/each_method_behaviour.rb @@ -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 diff --git a/spec/shared/hash_method_behavior.rb b/spec/shared/hash_method_behavior.rb new file mode 100644 index 00000000..7b20c7d3 --- /dev/null +++ b/spec/shared/hash_method_behavior.rb @@ -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 diff --git a/spec/shared/idempotent_method_behavior.rb b/spec/shared/idempotent_method_behavior.rb new file mode 100644 index 00000000..220d792d --- /dev/null +++ b/spec/shared/idempotent_method_behavior.rb @@ -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 diff --git a/spec/shared/invertible_method_behaviour.rb b/spec/shared/invertible_method_behaviour.rb new file mode 100644 index 00000000..936d65d0 --- /dev/null +++ b/spec/shared/invertible_method_behaviour.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index addf90b3..0119d2bc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 diff --git a/spec/unit/mutant/matcher/each_spec.rb b/spec/unit/mutant/matcher/each_spec.rb new file mode 100644 index 00000000..5a2f8a4f --- /dev/null +++ b/spec/unit/mutant/matcher/each_spec.rb @@ -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 diff --git a/spec/unit/mutant/matcher/method/class_methods/parse_spec.rb b/spec/unit/mutant/matcher/method/class_methods/parse_spec.rb new file mode 100644 index 00000000..7e0d10b6 --- /dev/null +++ b/spec/unit/mutant/matcher/method/class_methods/parse_spec.rb @@ -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