1
0
Fork 0

Reorganize classifiers

* Classifiers are matcher subclass and delegate to lazy builded matcher
  interfaces
* Solves chicken egg problem between target library load and classifier
  matcher instantiation
This commit is contained in:
Markus Schirp 2013-01-21 20:08:30 +01:00
parent b024508ae9
commit a3fc233d95
10 changed files with 242 additions and 155 deletions

View file

@ -89,12 +89,11 @@ require 'mutant/subject'
require 'mutant/subject/method'
require 'mutant/matcher'
require 'mutant/matcher/chain'
require 'mutant/matcher/object_space'
require 'mutant/matcher/method'
require 'mutant/matcher/method/singleton'
require 'mutant/matcher/method/instance'
require 'mutant/matcher/scope_methods'
require 'mutant/matcher/method/classifier'
require 'mutant/matcher/methods'
require 'mutant/matcher/namespace'
require 'mutant/killer'
require 'mutant/killer/static'
require 'mutant/killer/rspec'
@ -109,6 +108,9 @@ require 'mutant/strategy/rspec/dm2/lookup'
require 'mutant/strategy/rspec/dm2/lookup/method'
require 'mutant/runner'
require 'mutant/cli'
require 'mutant/cli/classifier'
require 'mutant/cli/classifier/namespace'
require 'mutant/cli/classifier/method'
require 'mutant/color'
require 'mutant/differ'
require 'mutant/reporter'

View file

@ -9,6 +9,19 @@ module Mutant
EXIT_FAILURE = 1
EXIT_SUCCESS = 0
OPTIONS = {
'--code' => [:add_filter, Mutation::Filter::Code ],
'--debug' => [:set_debug ],
'-d' => [:set_debug ],
'--rspec-unit' => [:set_strategy, Strategy::Rspec::Unit ],
'--rspec-full' => [:set_strategy, Strategy::Rspec::Full ],
'--rspec-dm2' => [:set_strategy, Strategy::Rspec::DM2 ],
'--static-fail' => [:set_strategy, Strategy::Static::Fail ],
'--static-success' => [:set_strategy, Strategy::Static::Success ]
}.freeze
OPTION_PATTERN = %r(\A-(?:-)?[a-zA-Z0-9\-]+\z).freeze
# Run cli with arguments
#
# @param [Array<String>] arguments
@ -98,19 +111,6 @@ module Mutant
private
OPTIONS = {
'--code' => [:add_filter, Mutation::Filter::Code ],
'--debug' => [:set_debug ],
'-d' => [:set_debug ],
'--rspec-unit' => [:set_strategy, Strategy::Rspec::Unit ],
'--rspec-full' => [:set_strategy, Strategy::Rspec::Full ],
'--rspec-dm2' => [:set_strategy, Strategy::Rspec::DM2 ],
'--static-fail' => [:set_strategy, Strategy::Static::Fail ],
'--static-success' => [:set_strategy, Strategy::Static::Success ]
}.freeze
OPTION_PATTERN = %r(\A-(?:-)?[a-zA-Z0-9\-]+\z).freeze
# Initialize CLI
#
# @param [Array<String>] arguments
@ -204,15 +204,8 @@ module Mutant
#
def dispatch_matcher
argument = current_argument
matcher = Mutant::Matcher.from_string(argument)
unless matcher
raise Error, "Invalid matcher syntax: #{argument.inspect}"
end
@matchers << matcher
consume(1)
@matchers << Classifier.build(argument)
end
# Process option argument

View file

@ -0,0 +1,129 @@
module Mutant
class CLI
# A classifier for input strings
class Classifier < Matcher
include AbstractType, Adamantium::Flat, Equalizer.new(:identification)
extend DescendantsTracker
SCOPE_NAME_PATTERN = /[A-Za-z][A-Za-z_0-9]*/.freeze
METHOD_NAME_PATTERN = /[_A-Za-z][A-Za-z0-9_]*[!?=]?/.freeze
SCOPE_PATTERN = /(?:::)?#{SCOPE_NAME_PATTERN}(?:::#{SCOPE_NAME_PATTERN})*/.freeze
SINGLETON_PATTERN = %r(\A(#{SCOPE_PATTERN})\z).freeze
# Return constant
#
# @param [String] location
#
# @return [Class|Module]
#
# @api private
#
def self.constant_lookup(location)
location.gsub(%r(\A::), '').split('::').inject(::Object) do |parent, name|
parent.const_get(name)
end
end
# Return matchers for input
#
# @param [String] input
#
# @return [Classifier]
# if a classifier handles the input
#
# @return [nil]
# otherwise
#
# @api private
#
def self.build(input)
classifiers = descendants.map do |descendant|
descendant.run(input)
end.compact
raise if classifiers.length > 1
classifiers.first
end
# Run classifier
#
# @return [Classifier]
# if input is handled by classifier
#
# @return [nil]
# otherwise
#
# @api private
#
def self.run(input)
match = self::REGEXP.match(input)
return unless match
new(match)
end
# No protected_class_method in ruby :(
class << self; protected :run; end
# Enumerate subjects
#
# @return [self]
# if block given
#
# @return [Enumerator<Subject>]
# otherwise
#
# @api private
#
def each(&block)
return to_enum unless block_given?
matcher.each(&block)
self
end
# Return identification
#
# @return [String]
#
# @api private
#
def identification
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
# Return matcher
#
# @return [Matcher]
#
# @api private
#
abstract_method :matcher
end
end
end

View file

@ -1,41 +1,20 @@
module Mutant
class Matcher
class Method < self
# A classifier for input strings
class Classifier
include Adamantium::Flat
class CLI
class Classifier
# Explicit method classifier
class Method < self
TABLE = {
'.' => Matcher::ScopeMethods::Singleton,
'#' => Matcher::ScopeMethods::Instance
'.' => Matcher::Methods::Singleton,
'#' => Matcher::Methods::Instance
}.freeze
SCOPE_FORMAT = /\A([^#.]+)(\.|#)(.+)\z/.freeze
REGEXP = %r(\A(#{SCOPE_PATTERN})([.#])(#{METHOD_NAME_PATTERN}\z)).freeze
# Positions of captured regexp groups
SCOPE_NAME_POSITION = 1
SCOPE_SYMBOL_POSITION = 2
METHOD_NAME_POSITION = 3
private_class_method :new
# Run classifier
#
# @param [String] input
#
# @return [Matcher::Method]
# returns matcher when input is in
#
# @return [nil]
# returns nil otherwise
#
# @api private
#
def self.run(input)
match = SCOPE_FORMAT.match(input)
return unless match
new(match).matcher
end
SCOPE_NAME_POSITION = 1
SCOPE_SYMBOL_POSITION = 2
METHOD_NAME_POSITION = 3
# Return method matcher
#
@ -48,6 +27,8 @@ module Mutant
end
memoize :matcher
private
# Return method
#
# @return [Method, UnboundMethod]
@ -61,39 +42,6 @@ module Mutant
end
memoize :method, :freezer => :noop
# Return match
#
# @return [Matche]
#
# @api private
#
attr_reader :match
private
# Initialize matcher
#
# @param [MatchData] match
#
# @api private
#
def initialize(match)
@match = match
end
# Return scope
#
# @return [Class|Module]
#
# @api private
#
def scope
scope_name.gsub(%r(\A::), '').split('::').inject(::Object) do |parent, name|
parent.const_get(name)
end
end
memoize :scope
# Return scope name
#
# @return [String]
@ -104,6 +52,16 @@ module Mutant
match[SCOPE_NAME_POSITION]
end
# Return scope
#
# @return [Class, Method]
#
# @api private
#
def scope
Classifier.constant_lookup(scope_name)
end
# Return method name
#
# @return [String]
@ -134,6 +92,7 @@ module Mutant
TABLE.fetch(scope_symbol).new(scope)
end
memoize :scope_matcher
end
end
end

View file

@ -0,0 +1,34 @@
module Mutant
class CLI
class Classifier
# Namespace classifier
class Namespace < self
REGEXP = %r(\A(#{SCOPE_PATTERN})\*\z).freeze
private
# Return matcher
#
# @return [Matcher]
#
# @api private
#
def matcher
Matcher::Namespace.new(namespace)
end
# Return namespace
#
# @return [Class, Module]
#
# @api private
#
def namespace
Classifier.const_lookup(match.to_s)
end
end
end
end
end

View file

@ -1,5 +1,5 @@
module Mutant
# Abstract matcher to find ASTs to mutate
# Abstract matcher to find subjects to mutate
class Matcher
include Adamantium::Flat, Enumerable, AbstractType
extend DescendantsTracker
@ -8,7 +8,11 @@ module Mutant
#
# @api private
#
# @return [undefined]
# @return [self]
# if block given
#
# @return [Enumerabe<Subject>]
# otherwise
#
abstract_method :each
@ -19,39 +23,5 @@ module Mutant
# @api private
#
abstract_method :identification
# Return matcher
#
# @param [String] input
#
# @return [nil]
# returns nil as default implementation
#
# @api private
#
def self.parse(input)
nil
end
# Return match from string
#
# @param [String] input
#
# @return [Matcher]
# returns matcher input if successful
#
# @return [nil]
# returns nil otherwise
#
# @api private
#
def self.from_string(input)
descendants.each do |descendant|
matcher = descendant.parse(input)
return matcher if matcher
end
nil
end
end
end

View file

@ -4,18 +4,6 @@ module Mutant
class Method < self
include Adamantium::Flat, Equalizer.new(:identification)
# Parse a method string into filter
#
# @param [String] input
#
# @return [Matcher::Method]
#
# @api private
#
def self.parse(input)
Classifier.run(input)
end
# Methods within rbx kernel directory are precompiled and their source
# cannot be accessed via reading source location
BLACKLIST = %r(\Akernel/).freeze

View file

@ -1,7 +1,7 @@
module Mutant
class Matcher
# Abstract base class for matcher that returns subjects extracted from scope methods
class ScopeMethods < self
# Abstract base class for matcher that returns method subjects extracted from scope
class Methods < self
include AbstractType
# Return scope

View file

@ -1,16 +1,16 @@
module Mutant
class Matcher
# Matcher against object space
class ObjectSpace < self
include Equalizer.new(:scope_name_pattern)
# Matcher for specific namespace
class Namespace < self
include Equalizer.new(:pattern)
# Enumerate subjects
#
# @return [Enumerator<Subject>]
# returns subject enumerator when no block given
#
# @return [self]
# returns self otherwise
# if block given
#
# @return [Enumerator<Subject>]
# otherwise
#
# @api private
#
@ -24,29 +24,41 @@ module Mutant
self
end
# Return scope name pattern
# Return namespace
#
# @return [Regexp]
# @return [Class::Module]
#
# @api private
#
attr_reader :scope_name_pattern
attr_reader :namespace
MATCHERS = [Matcher::Methods::Singleton, Matcher::Methods::Instance]
private
# Initialize object space matcher
#
# @param [Regexp] scope_name_pattern
# @param [Enumerable<#each(scope)>] matchers
# @param [Class, Module] namespace
#
# @return [undefined]
#
# @api private
#
def initialize(scope_name_pattern, matchers = [Matcher::ScopeMethods::Singleton, Matcher::ScopeMethods::Instance])
@scope_name_pattern, @matchers = scope_name_pattern, @matchers = matchers #[Method::Singleton, Method::Instance]
def initialize(namespace)
@namespace = namespace
end
# Return pattern
#
# @return [Regexp]
#
# @api private
#
def pattern
%r(\A#{Regexp.escape(namespace_name)}(?:::)?\z)
end
memoize :pattern
# Yield matchers for scope
#
# @param [::Class,::Module] scope
@ -56,7 +68,7 @@ module Mutant
# @api private
#
def emit_scope_matches(scope, &block)
@matchers.each do |matcher|
MATCHERS.each do |matcher|
matcher.new(scope).each(&block)
end
end
@ -84,7 +96,7 @@ module Mutant
# @api private
#
def emit_scope(scope)
if [::Module, ::Class].include?(scope.class) and scope_name_pattern =~ scope.name
if [::Module, ::Class].include?(scope.class) and pattern =~ scope.name
yield scope
end
end

View file

@ -10,7 +10,7 @@ describe Mutant, 'method matching' do
this_example = 'Mutant method matching'
shared_examples_for this_example do
subject { Mutant::Matcher::Method.parse(pattern).to_a }
subject { Mutant::CLI::Classifier.build(pattern).to_a }
let(:values) { defaults.merge(expectation) }