Add AST caching for subject matcher

At least this dramatically speeds up unit tests. And this is a good
thing.
This commit is contained in:
Markus Schirp 2013-06-27 22:18:07 +02:00
parent 8696a796de
commit 49133680ee
23 changed files with 180 additions and 148 deletions

View file

@ -22,6 +22,7 @@ require 'concord'
module Mutant
end
require 'mutant/cache'
require 'mutant/node_helpers'
require 'mutant/singleton_methods'
require 'mutant/constants'
@ -77,6 +78,7 @@ require 'mutant/subject/method'
require 'mutant/matcher'
require 'mutant/matcher/chain'
require 'mutant/matcher/method'
require 'mutant/matcher/method/finder'
require 'mutant/matcher/method/singleton'
require 'mutant/matcher/method/instance'
require 'mutant/matcher/methods'

30
lib/mutant/cache.rb Normal file
View file

@ -0,0 +1,30 @@
module Mutant
# An AST cache
class Cache
# This is explicitly empty! Ask me if you are interested in reasons :D
include Equalizer.new
# Initialize object
#
# @return [undefined]
#
# @api private
#
def initialize
@cache = {}
end
# Return node for file
#
# @return [AST::Node]
#
# @api private
#
def parse(path)
@cache.fetch(path) do
@cache[path] = Parser::CurrentRuby.parse(File.read(path))
end
end
end # Cache
end # Mutant

View file

@ -41,6 +41,8 @@ module Mutant
def initialize(arguments=[])
@filters, @matchers = [], []
@cache = Mutant::Cache.new
parse(arguments)
strategy
matcher
@ -54,6 +56,7 @@ module Mutant
#
def config
Config.new(
:cache => @cache,
:debug => debug?,
:matcher => matcher,
:filter => filter,
@ -212,7 +215,7 @@ module Mutant
#
def parse_matchers(patterns)
patterns.each do |pattern|
matcher = Classifier.build(pattern)
matcher = Classifier.build(@cache, pattern)
@matchers << matcher if matcher
end
end

View file

@ -2,7 +2,9 @@ module Mutant
class CLI
# A classifier for input strings
class Classifier < Matcher
include AbstractType, Adamantium::Flat, Equalizer.new(:identification)
include AbstractType, Adamantium::Flat, Concord.new(:cache, :match)
include Equalizer.new(:identifier)
SCOPE_NAME_PATTERN = /[A-Za-z][A-Za-z_0-9]*/.freeze
METHOD_NAME_PATTERN = /[_A-Za-z][A-Za-z0-9_]*[!?=]?/.freeze
@ -39,8 +41,6 @@ module Mutant
# Return matchers for input
#
# @param [String] input
#
# @return [Classifier]
# if a classifier handles the input
#
@ -49,9 +49,9 @@ module Mutant
#
# @api private
#
def self.build(input)
def self.build(*arguments)
classifiers = REGISTRY.map do |descendant|
descendant.run(input)
descendant.run(*arguments)
end.compact
raise if classifiers.length > 1
@ -61,6 +61,10 @@ module Mutant
# Run classifier
#
# @param [Cache] cache
#
# @param [String] input
#
# @return [Classifier]
# if input is handled by classifier
#
@ -69,11 +73,11 @@ module Mutant
#
# @api private
#
def self.run(input)
def self.run(cache, input)
match = self::REGEXP.match(input)
return unless match
new(match)
new(cache, match)
end
# No protected_class_method in ruby :(
@ -95,38 +99,16 @@ module Mutant
self
end
# Return identification
# Return identifier
#
# @return [String]
#
# @api private
#
def identification
def identifier
match.to_s
end
memoize :identification
private
# Initialize object
#
# @param [MatchData] match
#
# @return [undefined]
#
# @api private
#
def initialize(match)
@match = match
end
# Return match
#
# @return [MatchData]
#
# @api private
#
attr_reader :match
memoize :identifier
# Return matcher
#

View file

@ -24,7 +24,7 @@ module Mutant
# @api private
#
def matcher
methods_matcher.matcher.new(scope, method)
methods_matcher.matcher.new(cache, scope, method)
end
memoize :matcher
@ -90,7 +90,7 @@ module Mutant
# @api private
#
def methods_matcher
TABLE.fetch(scope_symbol).new(scope)
TABLE.fetch(scope_symbol).new(cache, scope)
end
memoize :methods_matcher

View file

@ -14,7 +14,7 @@ module Mutant
# @api private
#
def matcher
self.class::MATCHER.new(namespace)
self.class::MATCHER.new(cache, namespace)
end
# Return namespace

View file

@ -2,7 +2,7 @@ module Mutant
# The configuration of a mutator run
class Config
include Adamantium::Flat, Anima.new(
:debug, :strategy, :matcher, :filter, :reporter
:cache, :debug, :strategy, :matcher, :filter, :reporter
)
# Enumerate subjects

View file

@ -15,10 +15,10 @@ module Mutant
#
# @api private
#
def self.each(input, &block)
return to_enum(__method__, input) unless block_given?
def self.each(cache, input, &block)
return to_enum(__method__, cache, input) unless block_given?
new(input).each(&block)
new(cache, input).each(&block)
self
end

View file

@ -2,7 +2,7 @@ module Mutant
class Matcher
# Matcher for subjects that are a specific method
class Method < self
include Adamantium::Flat, Concord::Public.new(:scope, :method)
include Adamantium::Flat, Concord::Public.new(:cache, :scope, :method)
# Methods within rbx kernel directory are precompiled and their source
# cannot be accessed via reading source location
@ -18,13 +18,12 @@ module Mutant
#
# @api private
#
def each(&block)
def each
return to_enum unless block_given?
return self if skip?
util = subject
yield util if util
unless skip?
yield subject if subject
end
self
end
@ -78,7 +77,7 @@ module Mutant
# @api private
#
def ast
Parser::CurrentRuby.parse(File.read(source_path))
cache.parse(source_path)
end
# Return path to source
@ -128,72 +127,6 @@ module Mutant
end
memoize :subject
# Visitor to find last match inside AST
class Finder
# Run finder
#
# @param [Parser::AST::Node]
#
# @return [Parser::AST::Node]
# if found
#
# @return [nil]
# otherwise
#
# @api private
#
#
def self.run(root, &predicate)
new(root, predicate).match
end
private_class_method :new
# Return match
#
# @return [Parser::AST::Node]
#
# @api private
#
attr_reader :match
private
# Initialize object
#
# @param [Parer::AST::Node]
#
# @return [undefined]
#
# @api private
#
#
def initialize(root, predicate)
@root, @predicate = root, predicate
visit(root)
end
# Visit node
#
# @param [Parser::AST::Node] node
#
# @return [undefined]
#
# @api private
#
def visit(node)
if @predicate.call(node)
@match = node
end
node.children.each do |child|
visit(child) if child.kind_of?(Parser::AST::Node)
end
end
end # Finder
# Return matched node
#
# @return [Parser::AST::Node]

View file

@ -0,0 +1,72 @@
module Mutant
class Matcher
class Method
# Visitor to find last match inside AST
class Finder
# Run finder
#
# @param [Parser::AST::Node]
#
# @return [Parser::AST::Node]
# if found
#
# @return [nil]
# otherwise
#
# @api private
#
#
def self.run(root, &predicate)
new(root, predicate).match
end
private_class_method :new
# Return match
#
# @return [Parser::AST::Node]
#
# @api private
#
attr_reader :match
private
# Initialize object
#
# @param [Parer::AST::Node]
#
# @return [undefined]
#
# @api private
#
#
def initialize(root, predicate)
@root, @predicate = root, predicate
visit(root)
end
# Visit node
#
# @param [Parser::AST::Node] node
#
# @return [undefined]
#
# @api private
#
def visit(node)
if @predicate.call(node)
@match = node
end
node.children.each do |child|
visit(child) if child.kind_of?(Parser::AST::Node)
end
end
end # Finder
end # Method
end # Matcher
end # Mutant

View file

@ -16,8 +16,8 @@ module Mutant
end
memoize :identification
RECEIVER_INDEX = 0
NAME_INDEX = 1
RECEIVER_INDEX = 0
NAME_INDEX = 1
CONST_NAME_INDEX = 1
private

View file

@ -2,7 +2,7 @@ module Mutant
class Matcher
# Abstract base class for matcher that returns method subjects extracted from scope
class Methods < self
include AbstractType, Concord::Public.new(:scope)
include AbstractType, Concord::Public.new(:cache, :scope)
# Enumerate subjects
#
@ -59,7 +59,7 @@ module Mutant
# @api private
#
def emit_matches(method)
matcher.new(scope, method).each do |subject|
matcher.new(cache, scope, method).each do |subject|
yield subject
end
end

View file

@ -3,7 +3,7 @@ module Mutant
# Matcher for specific namespace
class Namespace < self
include Concord::Public.new(:namespace)
include Concord::Public.new(:cache, :namespace)
# Enumerate subjects
#
@ -19,7 +19,7 @@ module Mutant
return to_enum unless block_given?
scopes.each do |scope|
Scope.each(scope, &block)
Scope.each(cache, scope, &block)
end
self

View file

@ -2,7 +2,7 @@ module Mutant
class Matcher
# Matcher for specific namespace
class Scope < self
include Concord::Public.new(:scope)
include Concord::Public.new(:cache, :scope)
MATCHERS = [
Matcher::Methods::Singleton,
@ -23,7 +23,7 @@ module Mutant
return to_enum unless block_given?
MATCHERS.each do |matcher|
matcher.each(scope, &block)
matcher.each(cache, scope, &block)
end
self

View file

@ -1,7 +1,6 @@
shared_examples_for 'a method matcher' do
before do
subject
end
before { subject }
let(:node) { mutation_subject.node }
let(:context) { mutation_subject.context }

View file

@ -6,6 +6,10 @@ $: << File.join(TestApp.root,'lib')
require 'test_app'
module Fixtures
AST_CACHE = Mutant::Cache.new
end
module ParserHelper
def generate(node)
Unparser.unparse(node)

View file

@ -70,15 +70,15 @@ describe Mutant::CLI, '.new' do
end
context 'with explicit method matcher' do
let(:arguments) { %w(--rspec-unit TestApp::Literal#float) }
let(:expected_matcher) { Mutant::CLI::Classifier::Method.new('TestApp::Literal#float') }
let(:arguments) { %w(--rspec-unit TestApp::Literal#float) }
let(:expected_matcher) { Mutant::CLI::Classifier::Method.new(Mutant::Cache.new, 'TestApp::Literal#float') }
it_should_behave_like 'a cli parser'
end
context 'with namespace matcher' do
let(:arguments) { %w(--rspec-unit ::TestApp*) }
let(:expected_matcher) { Mutant::CLI::Classifier::Namespace::Recursive.new('::TestApp*') }
let(:expected_matcher) { Mutant::CLI::Classifier::Namespace::Recursive.new(Mutant::Cache.new, '::TestApp*') }
it_should_behave_like 'a cli parser'
end
@ -93,7 +93,7 @@ describe Mutant::CLI, '.new' do
]
end
let(:expected_matcher) { Mutant::CLI::Classifier::Method.new('TestApp::Literal#float') }
let(:expected_matcher) { Mutant::CLI::Classifier::Method.new(Mutant::Cache.new, 'TestApp::Literal#float') }
let(:expected_filter) { Mutant::Mutation::Filter::Whitelist.new(filters) }
it_should_behave_like 'a cli parser'

View file

@ -1,20 +1,23 @@
require 'spec_helper'
describe Mutant::CLI::Classifier, '.build' do
subject { described_class.build(input) }
subject { described_class.build(cache, input) }
let(:cache) { mock('Cache') }
this_spec = 'Mutant::CLI::Classifier.build'
shared_examples_for this_spec do
it 'shoud return expected instance' do
should eql(expected_class.new(expected_class::REGEXP.match(input)))
should eql(expected_class.new(cache, expected_class::REGEXP.match(input)))
end
let(:expected_class) { Mutant::CLI::Classifier::Method }
end
context 'with explicit toplevel scope' do
let(:input) { '::TestApp::Literal#string' }
let(:expected_class) { Mutant::CLI::Classifier::Method }
it_should_behave_like this_spec
end
@ -22,14 +25,12 @@ describe Mutant::CLI::Classifier, '.build' do
context 'with instance method notation' do
let(:input) { 'TestApp::Literal#string' }
let(:expected_class) { Mutant::CLI::Classifier::Method }
it_should_behave_like this_spec
end
context 'with singleton method notation' do
let(:input) { 'TestApp::Literal.string' }
let(:expected_class) { Mutant::CLI::Classifier::Method }
it_should_behave_like this_spec
end

View file

@ -1,8 +1,9 @@
require 'spec_helper'
describe Mutant::Matcher::Method::Instance, '#each' do
let(:object) { described_class.new(scope, method) }
let(:method) { scope.instance_method(method_name) }
let(:cache) { Fixtures::AST_CACHE }
let(:object) { described_class.new(cache, scope, method) }
let(:method) { scope.instance_method(method_name) }
let(:yields) { [] }

View file

@ -1,8 +1,9 @@
require 'spec_helper'
describe Mutant::Matcher::Method::Singleton, '#each' do
let(:object) { described_class.new(scope, method) }
let(:method) { scope.method(method_name) }
let(:object) { described_class.new(cache, scope, method) }
let(:method) { scope.method(method_name) }
let(:cache) { Fixtures::AST_CACHE }
let(:yields) { [] }

View file

@ -1,7 +1,8 @@
require 'spec_helper'
describe Mutant::Matcher::Methods::Instance, '#each' do
let(:object) { described_class.new(Foo) }
let(:object) { described_class.new(cache, Foo) }
let(:cache) { Mutant::Cache.new }
subject { object.each { |matcher| yields << matcher } }
@ -45,9 +46,9 @@ describe Mutant::Matcher::Methods::Instance, '#each' do
before do
matcher = Mutant::Matcher::Method::Instance
matcher.stub(:new).with(Foo, Foo.instance_method(:method_a)).and_return([subject_a])
matcher.stub(:new).with(Foo, Foo.instance_method(:method_b)).and_return([subject_b])
matcher.stub(:new).with(Foo, Foo.instance_method(:method_c)).and_return([subject_c])
matcher.stub(:new).with(cache, Foo, Foo.instance_method(:method_a)).and_return([subject_a])
matcher.stub(:new).with(cache, Foo, Foo.instance_method(:method_b)).and_return([subject_b])
matcher.stub(:new).with(cache, Foo, Foo.instance_method(:method_c)).and_return([subject_c])
end
it 'should yield expected subjects' do

View file

@ -1,7 +1,8 @@
require 'spec_helper'
describe Mutant::Matcher::Methods::Singleton, '#each' do
let(:object) { described_class.new(Foo) }
let(:object) { described_class.new(cache, Foo) }
let(:cache) { Mutant::Cache.new }
subject { object.each { |matcher| yields << matcher } }
@ -39,9 +40,9 @@ describe Mutant::Matcher::Methods::Singleton, '#each' do
before do
matcher = Mutant::Matcher::Method::Singleton
matcher.stub(:new).with(Foo, Foo.method(:method_a)).and_return([subject_a])
matcher.stub(:new).with(Foo, Foo.method(:method_b)).and_return([subject_b])
matcher.stub(:new).with(Foo, Foo.method(:method_c)).and_return([subject_c])
matcher.stub(:new).with(cache, Foo, Foo.method(:method_a)).and_return([subject_a])
matcher.stub(:new).with(cache, Foo, Foo.method(:method_b)).and_return([subject_b])
matcher.stub(:new).with(cache, Foo, Foo.method(:method_c)).and_return([subject_c])
end
it 'should yield expected subjects' do

View file

@ -4,7 +4,9 @@ describe Mutant::Matcher::Namespace, '#each' do
subject { object.each { |item| yields << item } }
let(:yields) { [] }
let(:object) { described_class.new(TestApp::Literal) }
let(:object) { described_class.new(cache, TestApp::Literal) }
let(:cache) { Mutant::Cache.new }
let(:singleton_a) { mock('SingletonA', :name => 'TestApp::Literal') }
let(:singleton_b) { mock('SingletonB', :name => 'TestApp::Foo') }
@ -12,8 +14,8 @@ describe Mutant::Matcher::Namespace, '#each' do
let(:subject_b) { mock('SubjectB') }
before do
Mutant::Matcher::Methods::Singleton.stub(:each).with(singleton_a).and_yield(subject_a)
Mutant::Matcher::Methods::Instance.stub(:each).with(singleton_a).and_yield(subject_b)
Mutant::Matcher::Methods::Singleton.stub(:each).with(cache, singleton_a).and_yield(subject_a)
Mutant::Matcher::Methods::Instance.stub(:each).with(cache, singleton_a).and_yield(subject_b)
ObjectSpace.stub(:each_object => [singleton_a, singleton_b])
end