Refactor expression parsing and representation

* Avoids boot time mutation of REGISTER constant
* Allows to define project specific expression parsing
* Avoids custom (slow) serialization of Expression objects speeding up
  reporter / killer IPC
* Improve specification
* Improve integration API as it now finally references an object the config
* Allow reproduction of syntax from Expression#syntax
* Allow instantiation of Expresssion objects without generating the
  syntax, much nicer for most specs & internal code, avoids generating
  a string to parse it into an expression
* Fix LSP violation in Mutant::Matcher namespace
This commit is contained in:
Markus Schirp 2015-06-21 14:44:33 +00:00
parent c7113f7e82
commit d647563055
41 changed files with 467 additions and 464 deletions

View file

@ -1,3 +1,3 @@
---
threshold: 18
total_score: 1194
total_score: 1185

View file

@ -143,6 +143,7 @@ require 'mutant/matcher/scope'
require 'mutant/matcher/filter'
require 'mutant/matcher/null'
require 'mutant/expression'
require 'mutant/expression/parser'
require 'mutant/expression/method'
require 'mutant/expression/methods'
require 'mutant/expression/namespace'
@ -192,7 +193,13 @@ module Mutant
reporter: Reporter::CLI.build($stdout),
zombie: false,
jobs: Mutant.ci? ? CI_DEFAULT_PROCESSOR_COUNT : ::Parallel.processor_count,
expected_coverage: Rational(1)
expected_coverage: Rational(1),
expression_parser: Expression::Parser.new([
Expression::Method,
Expression::Methods,
Expression::Namespace::Exact,
Expression::Namespace::Recursive
])
)
end # Config
end # Mutant

View file

@ -86,8 +86,8 @@ module Mutant
def parse_match_expressions(expressions)
fail Error, 'No expressions given' if expressions.empty?
expressions.map(&Expression.method(:parse)).each do |expression|
add_matcher(:match_expressions, expression)
expressions.each do |expression|
add_matcher(:match_expressions, config.expression_parser.(expression))
end
end
@ -162,7 +162,7 @@ module Mutant
#
def add_filter_options(opts)
opts.on('--ignore-subject PATTERN', 'Ignore subjects that match PATTERN') do |pattern|
add_matcher(:subject_ignores, Expression.parse(pattern))
add_matcher(:subject_ignores, config.expression_parser.(pattern))
end
end

View file

@ -12,7 +12,8 @@ module Mutant
:fail_fast,
:jobs,
:zombie,
:expected_coverage
:expected_coverage,
:expression_parser
)
%i[fail_fast zombie debug].each do |name|

View file

@ -87,7 +87,9 @@ module Mutant
#
def match_expressions
name_nesting.each_index.reverse_each.map do |index|
Expression.parse("#{name_nesting.take(index.succ).join(NAMESPACE_DELIMITER)}*")
Expression::Namespace::Recursive.new(
scope_name: name_nesting.take(index.succ).join(NAMESPACE_DELIMITER)
)
end
end
memoize :match_expressions

View file

@ -106,6 +106,31 @@ module Mutant
@integration = config.integration.new(config).setup
end
# Return matched subjects
#
# @return [Enumerable<Subject>]
#
# @api private
#
def matched_subjects
Matcher::Compiler.call(self, config.matcher).to_a
end
# Initialize matchable scopes
#
# @return [undefined]
#
# @api private
#
def initialize_matchable_scopes
scopes = ObjectSpace.each_object(Module).each_with_object([]) do |scope, aggregate|
expression = expression(scope)
aggregate << Matcher::Scope.new(self, scope, expression) if expression
end
@matchable_scopes = scopes.sort_by { |scope| scope.expression.syntax }
end
# Try to turn scope into expression
#
# @param [Class, Module] scope
@ -126,30 +151,7 @@ module Mutant
return
end
Expression.try_parse(name)
end
# Return matched subjects
#
# @return [Enumerable<Subject>]
#
# @api private
#
def matched_subjects
Matcher::Compiler.call(self, config.matcher).to_a
end
# Initialize matchable scopes
#
# @return [undefined]
#
# @api private
#
def initialize_matchable_scopes
@matchable_scopes = ObjectSpace.each_object(Module).each_with_object([]) do |scope, aggregate|
expression = expression(scope)
aggregate << Matcher::Scope.new(self, scope, expression) if expression
end.sort_by(&:identification)
config.expression_parser.try_parse(name)
end
end # Boostrap
end # Env

View file

@ -2,83 +2,21 @@ module Mutant
# Abstract base class for match expression
class Expression
include AbstractType, Adamantium::Flat, Concord::Public.new(:match)
include AbstractType, Adamantium::Flat
include Equalizer.new(:syntax)
fragment = /[A-Za-z][A-Za-z\d_]*/.freeze
SCOPE_NAME_PATTERN = /(?<scope_name>#{fragment}(?:#{SCOPE_OPERATOR}#{fragment})*)/.freeze
SCOPE_SYMBOL_PATTERN = '(?<scope_symbol>[.#])'.freeze
SCOPE_NAME_PATTERN = /[A-Za-z][A-Za-z\d_]*/.freeze
private_constant(*constants(false))
METHOD_NAME_PATTERN = Regexp.union(
/[A-Za-z_][A-Za-z\d_]*[!?=]?/,
*AST::Types::OPERATOR_METHODS.map(&:to_s)
).freeze
INSPECT_FORMAT = '<Mutant::Expression: %s>'.freeze
SCOPE_PATTERN = /#{SCOPE_NAME_PATTERN}(?:#{SCOPE_OPERATOR}#{SCOPE_NAME_PATTERN})*/.freeze
REGISTRY = {}
# Error raised on invalid expressions
class InvalidExpressionError < RuntimeError; end
# Error raised on ambiguous expressions
class AmbiguousExpressionError < RuntimeError; end
# Initialize expression
#
# @param [MatchData] match
#
# @api private
#
def initialize(*)
super
@syntax = match.to_s
@inspect = format(INSPECT_FORMAT, syntax)
end
# Return marshallable representation
#
# FIXME: Remove the need for this.
#
# Refactoring Expression objects not to reference a MatchData instance.
# This will make this hack unneeded.
# Return syntax representing this expression
#
# @return [String]
#
# @api private
#
def _dump(_level)
syntax
end
# Load serializable representation
#
# @return [String]
#
# @return [Expression]
#
# @api private
#
def self._load(syntax)
parse(syntax)
end
# Return inspection
#
# @return [String]
#
# @api private
#
attr_reader :inspect
# Return syntax
#
# @return [String]
#
# @api private
#
attr_reader :syntax
abstract_method :syntax
# Return match length for expression
#
@ -108,76 +46,23 @@ module Mutant
!match_length(other).zero?
end
# Register expression
#
# @return [undefined]
#
# @api private
#
def self.register(regexp)
REGISTRY[regexp] = self
end
private_class_method :register
# Parse input into expression or raise
#
# @param [String] syntax
#
# @return [Expression]
# if expression is valid
#
# @raise [RuntimeError]
# otherwise
#
# @api private
#
def self.parse(input)
try_parse(input) or fail InvalidExpressionError, "Expression: #{input.inspect} is not valid"
end
# Parse input into expression
# Try to parse input into expression of receiver class
#
# @param [String] input
#
# @return [Expression]
# if expression is valid
# when successful
#
# @return [nil]
# otherwise
#
# @api private
#
def self.try_parse(input)
expressions = expressions(input)
case expressions.length
when 0
when 1
expressions.first
else
fail AmbiguousExpressionError, "Ambiguous expression: #{input.inspect}"
end
match = self::REGEXP.match(input)
return unless match
names = anima.attribute_names
new(Hash[names.zip(names.map(&match.method(:[])))])
end
# Return expressions for input
#
# @param [String] input
#
# @return [Classifier]
# if classifier can be found
#
# @return [nil]
# otherwise
#
# @api private
#
def self.expressions(input)
REGISTRY.each_with_object([]) do |(regexp, klass), expressions|
match = regexp.match(input)
next unless match
expressions << klass.new(match)
end
end
private_class_method :expressions
end # Expression
end # Mutant

View file

@ -3,43 +3,49 @@ module Mutant
# Explicit method expression
class Method < self
include Anima.new(:scope_name, :scope_symbol, :method_name)
private(*anima.attribute_names)
MATCHERS = IceNine.deep_freeze(
'.' => Matcher::Methods::Singleton,
'#' => Matcher::Methods::Instance
)
register(
/\A(?<scope_name>#{SCOPE_PATTERN})(?<scope_symbol>[.#])(?<method_name>#{METHOD_NAME_PATTERN})\z/
)
METHOD_NAME_PATTERN = Regexp.union(
/(?<method_name>[A-Za-z_][A-Za-z\d_]*[!?=]?)/,
*AST::Types::OPERATOR_METHODS.map(&:to_s)
).freeze
# Return method matcher
#
# @param [Env] env
#
# @return [Matcher::Method]
#
# @api private
#
def matcher(env)
methods_matcher = MATCHERS.fetch(scope_symbol).new(env, scope)
method = methods_matcher.methods.detect do |meth|
meth.name.equal?(method_name)
end or fail NameError, "Cannot find method #{method_name}"
methods_matcher.matcher.build(env, scope, method)
end
private_constant(*constants(false))
private
REGEXP = /\A#{SCOPE_NAME_PATTERN}#{SCOPE_SYMBOL_PATTERN}#{METHOD_NAME_PATTERN}\z/.freeze
# Return scope name
# Return syntax
#
# @return [String]
#
# @api private
#
def scope_name
match[__method__]
def syntax
[scope_name, scope_symbol, method_name].join
end
memoize :syntax
# Return method matcher
#
# @param [Env] env
#
# @return [Matcher]
#
# @api private
#
def matcher(env)
methods_matcher = MATCHERS.fetch(scope_symbol).new(env, scope)
Matcher::Filter.build(methods_matcher) { |subject| subject.expression.eql?(self) }
end
private
# Return scope
#
@ -51,26 +57,6 @@ module Mutant
Object.const_get(scope_name)
end
# Return method name
#
# @return [String]
#
# @api private
#
def method_name
match[__method__].to_sym
end
# Return scope symbol
#
# @return [Symbol]
#
# @api private
#
def scope_symbol
match[__method__]
end
end # Method
end # Expression
end # Mutant

View file

@ -3,15 +3,27 @@ module Mutant
# Abstract base class for methods expression
class Methods < self
include Anima.new(:scope_name, :scope_symbol)
private(*anima.attribute_names)
MATCHERS = IceNine.deep_freeze(
'.' => Matcher::Methods::Singleton,
'#' => Matcher::Methods::Instance
)
private_constant(*constants(false))
register(
/\A(?<scope_name>#{SCOPE_PATTERN})(?<scope_symbol>[.#])\z/
)
REGEXP = /\A#{SCOPE_NAME_PATTERN}#{SCOPE_SYMBOL_PATTERN}\z/.freeze
# Return syntax
#
# @return [String]
#
# @api private
#
def syntax
[scope_name, scope_symbol].join
end
memoize :syntax
# Return method matcher
#
@ -43,16 +55,6 @@ module Mutant
private
# Return scope name
#
# @return [String]
#
# @api private
#
def scope_name
match[__method__]
end
# Return scope
#
# @return [Class, Method]
@ -63,16 +65,6 @@ module Mutant
Object.const_get(scope_name)
end
# Return scope symbol
#
# @return [Symbol]
#
# @api private
#
def scope_symbol
match[__method__]
end
end # Method
end # Expression
end # Mutant

View file

@ -2,24 +2,12 @@ module Mutant
class Expression
# Abstract base class for expressions matching namespaces
class Namespace < self
include AbstractType
private
# Return matched namespace
#
# @return [String]
#
# @api private
#
def namespace
match[__method__]
end
include AbstractType, Anima.new(:scope_name)
private(*anima.attribute_names)
# Recursive namespace expression
class Recursive < self
register(/\A(?<namespace>#{SCOPE_PATTERN})?\*\z/)
REGEXP = /\A#{SCOPE_NAME_PATTERN}?\*\z/.freeze
# Initialize object
#
@ -29,12 +17,23 @@ module Mutant
def initialize(*)
super
@recursion_pattern = Regexp.union(
/\A#{namespace}\z/,
/\A#{namespace}::/,
/\A#{namespace}[.#]/
/\A#{scope_name}\z/,
/\A#{scope_name}::/,
/\A#{scope_name}[.#]/
)
end
# Return the syntax for this expression
#
# @return [String]
#
# @api private
#
def syntax
"#{scope_name}*"
end
memoize :syntax
# Return matcher
#
# @param [Env::Bootstrap] env
@ -57,7 +56,7 @@ module Mutant
#
def match_length(expression)
if @recursion_pattern =~ expression.syntax
namespace.length
scope_name.length
else
0
end
@ -68,9 +67,10 @@ module Mutant
# Exact namespace expression
class Exact < self
register(/\A(?<namespace>#{SCOPE_PATTERN})\z/)
MATCHER = Matcher::Scope
private_constant(*constants(false))
REGEXP = /\A#{SCOPE_NAME_PATTERN}\z/.freeze
# Return matcher
#
@ -81,9 +81,18 @@ module Mutant
# @api private
#
def matcher(env)
Matcher::Scope.new(env, Object.const_get(namespace), self)
Matcher::Scope.new(env, Object.const_get(scope_name), self)
end
# Return the syntax for this expression
#
# @return [String]
#
# @api private
#
alias_method :syntax, :scope_name
public :syntax
end # Exact
end # Namespace
end # Namespace

View file

@ -0,0 +1,74 @@
module Mutant
class Expression
class Parser
include Concord.new(:types)
class ParserError < RuntimeError
include AbstractType
end
# Error raised on invalid expressions
class InvalidExpressionError < ParserError; end
# Error raised on ambiguous expressions
class AmbiguousExpressionError < ParserError; end
# Parse input into expression or raise
#
# @param [String] syntax
#
# @return [Expression]
# if expression is valid
#
# @raise [ParserError]
# otherwise
#
# @api private
#
def call(input)
try_parse(input) or fail InvalidExpressionError, "Expression: #{input.inspect} is not valid"
end
# Try to parse input into expression
#
# @param [String] input
#
# @return [Expression]
# if expression is valid
#
# @return [nil]
# otherwise
#
# @api private
#
def try_parse(input)
expressions = expressions(input)
case expressions.length
when 0, 1
expressions.first
else
fail AmbiguousExpressionError, "Ambiguous expression: #{input.inspect}"
end
end
private
# Return expressions for input
#
# @param [String] input
#
# @return [Array<Expression>]
# if expressions can be parsed from input
#
# @api private
#
def expressions(input)
types.each_with_object([]) do |type, aggregate|
expression = type.try_parse(input)
aggregate << expression if expression
end
end
end # Parser
end # Expression
end # Mutant

View file

@ -73,6 +73,18 @@ module Mutant
#
abstract_method :all_tests
private
# Return expression parser
#
# @return [Expression::Parser]
#
# @api private
#
def expression_parser
config.expression_parser
end
# Null integration that never kills a mutation
class Null < self

View file

@ -19,12 +19,14 @@ module Mutant
# for unique reference.
class Rspec < self
ALL_EXPRESSION = Expression.parse('*').freeze
ALL_EXPRESSION = Expression::Namespace::Recursive.new(scope_name: nil)
EXPRESSION_CANDIDATE = /\A([^ ]+)(?: )?/.freeze
LOCATION_DELIMITER = ':'.freeze
EXIT_SUCCESS = 0
CLI_OPTIONS = IceNine.deep_freeze(%w[spec --fail-fast])
private_constant(*constants(false))
register 'rspec'
# Initialize rspec integration
@ -132,10 +134,10 @@ module Mutant
#
def parse_expression(metadata)
if metadata.key?(:mutant_expression)
Expression.parse(metadata.fetch(:mutant_expression))
expression_parser.(metadata.fetch(:mutant_expression))
else
match = EXPRESSION_CANDIDATE.match(metadata.fetch(:full_description))
Expression.try_parse(match.captures.first) || ALL_EXPRESSION
expression_parser.try_parse(match.captures.first) || ALL_EXPRESSION
end
end

View file

@ -28,13 +28,5 @@ module Mutant
#
abstract_method :each
# Return identification
#
# @return [String
#
# @api private
#
abstract_method :identification
end # Matcher
end # Mutant

View file

@ -4,6 +4,18 @@ module Mutant
class Filter < self
include Concord.new(:matcher, :predicate)
# Return new matcher
#
# @return [Matcher] matcher
#
# @return [Matcher]
#
# @api private
#
def self.build(matcher, &predicate)
new(matcher, predicate)
end
# Enumerate matches
#
# @return [self]

View file

@ -3,7 +3,7 @@ module Mutant
# Matcher for subjects that are a specific method
class Method < self
include Adamantium::Flat, Concord::Public.new(:env, :scope, :target_method)
include AST::NodePredicates, Equalizer.new(:identification)
include AST::NodePredicates
# Methods within rbx kernel directory are precompiled and their source
# cannot be accessed via reading source location. Same for methods created by eval.

View file

@ -23,17 +23,6 @@ module Mutant
super
end
# Return identification
#
# @return [String]
#
# @api private
#
def identification
"#{scope.name}##{method_name}"
end
memoize :identification
NAME_INDEX = 0
private

View file

@ -3,21 +3,9 @@ module Mutant
class Method
# Matcher for singleton methods
class Singleton < self
SUBJECT_CLASS = Subject::Method::Singleton
# Return identification
#
# @return [String]
#
# @api private
#
def identification
"#{scope.name}.#{method_name}"
end
memoize :identification
RECEIVER_INDEX = 0
NAME_INDEX = 1
SUBJECT_CLASS = Subject::Method::Singleton
RECEIVER_INDEX = 0
NAME_INDEX = 1
private

View file

@ -22,6 +22,8 @@ module Mutant
self
end
private
# Return method matcher class
#
# @return [Class:Matcher::Method]
@ -46,8 +48,6 @@ module Mutant
end
memoize :methods
private
# Return subjects
#
# @return [Array<Subject>]

View file

@ -9,17 +9,6 @@ module Mutant
Matcher::Methods::Instance
].freeze
# Return identification
#
# @return [String]
#
# @api private
#
def identification
scope.name
end
memoize :identification
# Enumerate subjects
#
# @return [self]

View file

@ -13,12 +13,12 @@ module Mutant
# Return method name
#
# @return [Symbol]
# @return [Expression]
#
# @api private
#
def name
node.children[self.class::NAME_INDEX]
node.children.fetch(self.class::NAME_INDEX)
end
# Return match expression
@ -28,7 +28,11 @@ module Mutant
# @api private
#
def expression
Expression.parse("#{context.identification}#{self.class::SYMBOL}#{name}")
Expression::Method.new(
scope_symbol: self.class::SYMBOL,
scope_name: scope.name,
method_name: name.to_s
)
end
memoize :expression

View file

@ -5,7 +5,7 @@ module Mutant
class Singleton < self
NAME_INDEX = 1
SYMBOL = '.'.freeze
SYMBOL = '.'.freeze
# Test if method is public
#

View file

@ -35,7 +35,7 @@ require 'test_app'
module Fixtures
TEST_CONFIG = Mutant::Config::DEFAULT.update(reporter: Mutant::Reporter::Trace.new)
TEST_CACHE = Mutant::Cache.new
TEST_ENV = Mutant::Env::Bootstrap.call(TEST_CONFIG, TEST_CACHE)
TEST_ENV = Mutant::Env::Bootstrap.(TEST_CONFIG, TEST_CACHE)
end # Fixtures
module ParserHelper
@ -46,6 +46,10 @@ module ParserHelper
def parse(string)
Unparser::Preprocessor.run(Parser::CurrentRuby.parse(string))
end
def parse_expression(string)
Mutant::Config::DEFAULT.expression_parser.(string)
end
end
module MessageHelper

View file

@ -73,7 +73,7 @@ RSpec.describe Mutant::CLI do
let(:default_matcher_config) do
Mutant::Matcher::Config::DEFAULT
.update(match_expressions: expressions.map(&Mutant::Expression.method(:parse)))
.update(match_expressions: expressions.map(&method(:parse_expression)))
end
let(:flags) { [] }
@ -234,7 +234,7 @@ Options:
let(:flags) { %w[--ignore-subject Foo::Bar] }
let(:expected_matcher_config) do
default_matcher_config.update(subject_ignores: [Mutant::Expression.parse('Foo::Bar')])
default_matcher_config.update(subject_ignores: [parse_expression('Foo::Bar')])
end
it_should_behave_like 'a cli parser'

View file

@ -0,0 +1,11 @@
RSpec.describe Mutant::Context::Scope do
let(:object) { described_class.new(scope, source_path) }
let(:scope) { double('scope', name: double('name')) }
let(:source_path) { double('source path') }
describe '#identification' do
subject { object.identification }
it { should be(scope.name) }
end
end

View file

@ -9,22 +9,25 @@ RSpec.describe Mutant::Env do
subjects: [],
mutations: [],
matchable_scopes: [],
integration: Mutant::Integration::Null.new(config)
integration: integration
)
end
let(:integration) { integration_class.new(config) }
let(:config) do
Mutant::Config::DEFAULT.update(isolation: isolation)
Mutant::Config::DEFAULT.update(isolation: isolation, integration: integration_class)
end
let(:isolation) { double('Isolation') }
let(:mutation) { Mutant::Mutation::Evil.new(mutation_subject, Mutant::AST::Nodes::N_NIL) }
let(:wrapped_node) { double('Wrapped Node') }
let(:context) { double('Context') }
let(:test_a) { double('Test A') }
let(:test_b) { double('Test B') }
let(:tests) { [test_a, test_b] }
let(:selector) { double('Selector') }
let(:isolation) { double('Isolation') }
let(:mutation) { Mutant::Mutation::Evil.new(mutation_subject, Mutant::AST::Nodes::N_NIL) }
let(:wrapped_node) { double('Wrapped Node') }
let(:context) { double('Context') }
let(:test_a) { double('Test A') }
let(:test_b) { double('Test B') }
let(:tests) { [test_a, test_b] }
let(:selector) { double('Selector') }
let(:integration_class) { Mutant::Integration::Null }
let(:mutation_subject) do
double(

View file

@ -1,9 +1,8 @@
RSpec.describe Mutant::Expression::Method do
let(:object) { described_class.parse(input) }
let(:env) { Fixtures::TEST_ENV }
let(:instance_method) { 'TestApp::Literal#string' }
let(:singleton_method) { 'TestApp::Literal.string' }
let(:object) { parse_expression(input) }
let(:env) { Fixtures::TEST_ENV }
let(:instance_method) { 'TestApp::Literal#string' }
let(:singleton_method) { 'TestApp::Literal.string' }
describe '#match_length' do
let(:input) { instance_method }
@ -11,13 +10,13 @@ RSpec.describe Mutant::Expression::Method do
subject { object.match_length(other) }
context 'when other is an equivalent expression' do
let(:other) { described_class.parse(object.syntax) }
let(:other) { parse_expression(object.syntax) }
it { should be(object.syntax.length) }
end
context 'when other is an unequivalent expression' do
let(:other) { described_class.parse('Foo*') }
let(:other) { parse_expression('Foo*') }
it { should be(0) }
end
@ -30,19 +29,16 @@ RSpec.describe Mutant::Expression::Method do
let(:input) { instance_method }
it 'returns correct matcher' do
should eql(
Mutant::Matcher::Method::Instance.new(
env,
TestApp::Literal, TestApp::Literal.instance_method(:string)
)
)
expect(subject.map(&:expression)).to eql([object])
end
end
context 'with a singleton method' do
let(:input) { singleton_method }
it { should eql(Mutant::Matcher::Method::Singleton.new(env, TestApp::Literal, TestApp::Literal.method(:string))) }
it 'returns correct matcher' do
expect(subject.map(&:expression)).to eql([object])
end
end
end
end

View file

@ -1,45 +1,58 @@
RSpec.describe Mutant::Expression::Methods do
let(:object) { described_class.parse(input) }
let(:env) { Fixtures::TEST_ENV }
let(:instance_methods) { 'TestApp::Literal#' }
let(:singleton_methods) { 'TestApp::Literal.' }
let(:env) { Fixtures::TEST_ENV }
let(:object) { described_class.new(attributes) }
describe '#match_length' do
let(:input) { instance_methods }
let(:attributes) { { scope_name: 'TestApp::Literal', scope_symbol: '#' } }
subject { object.match_length(other) }
context 'when other is an equivalent expression' do
let(:other) { described_class.parse(object.syntax) }
let(:other) { parse_expression(object.syntax) }
it { should be(object.syntax.length) }
end
context 'when other is matched' do
let(:other) { described_class.parse('TestApp::Literal#foo') }
let(:other) { parse_expression('TestApp::Literal#foo') }
it { should be(object.syntax.length) }
end
context 'when other is an not matched expression' do
let(:other) { described_class.parse('Foo*') }
let(:other) { parse_expression('Foo*') }
it { should be(0) }
end
end
describe '#syntax' do
subject { object.syntax }
context 'with an instance method' do
let(:attributes) { { scope_name: 'TestApp::Literal', scope_symbol: '#' } }
it { should eql('TestApp::Literal#') }
end
context 'with a singleton method' do
let(:attributes) { { scope_name: 'TestApp::Literal', scope_symbol: '.' } }
it { should eql('TestApp::Literal.') }
end
end
describe '#matcher' do
subject { object.matcher(env) }
context 'with an instance method' do
let(:input) { instance_methods }
let(:attributes) { { scope_name: 'TestApp::Literal', scope_symbol: '#' } }
it { should eql(Mutant::Matcher::Methods::Instance.new(env, TestApp::Literal)) }
end
context 'with a singleton method' do
let(:input) { singleton_methods }
let(:attributes) { { scope_name: 'TestApp::Literal', scope_symbol: '.' } }
it { should eql(Mutant::Matcher::Methods::Singleton.new(env, TestApp::Literal)) }
end

View file

@ -1,8 +1,7 @@
RSpec.describe Mutant::Expression::Namespace::Exact do
let(:object) { described_class.parse(input) }
let(:env) { Fixtures::TEST_ENV }
let(:input) { 'TestApp::Literal' }
let(:object) { parse_expression(input) }
let(:env) { Fixtures::TEST_ENV }
let(:input) { 'TestApp::Literal' }
describe '#matcher' do
subject { object.matcher(env) }
@ -14,13 +13,13 @@ RSpec.describe Mutant::Expression::Namespace::Exact do
subject { object.match_length(other) }
context 'when other is an equivalent expression' do
let(:other) { described_class.parse(object.syntax) }
let(:other) { parse_expression(object.syntax) }
it { should be(object.syntax.length) }
end
context 'when other is an unequivalent expression' do
let(:other) { described_class.parse('Foo*') }
let(:other) { parse_expression('Foo*') }
it { should be(0) }
end

View file

@ -1,8 +1,8 @@
RSpec.describe Mutant::Expression::Namespace::Recursive do
let(:object) { described_class.parse(input) }
let(:input) { 'TestApp::Literal*' }
let(:env) { Fixtures::TEST_ENV }
let(:object) { parse_expression(input) }
let(:input) { 'TestApp::Literal*' }
let(:env) { Fixtures::TEST_ENV }
describe '#matcher' do
subject { object.matcher(env) }
@ -10,48 +10,54 @@ RSpec.describe Mutant::Expression::Namespace::Recursive do
it { should eql(Mutant::Matcher::Namespace.new(env, object)) }
end
describe '#syntax' do
subject { object.syntax }
it { should eql(input) }
end
describe '#match_length' do
subject { object.match_length(other) }
context 'when other is an equivalent expression' do
let(:other) { described_class.parse(object.syntax) }
let(:other) { parse_expression(object.syntax) }
it { should be(0) }
end
context 'when other expression describes a shorter prefix' do
let(:other) { described_class.parse('TestApp') }
let(:other) { parse_expression('TestApp') }
it { should be(0) }
end
context 'when other expression describes adjacent namespace' do
let(:other) { described_class.parse('TestApp::LiteralFoo') }
let(:other) { parse_expression('TestApp::LiteralFoo') }
it { should be(0) }
end
context 'when other expression describes root namespace' do
let(:other) { described_class.parse('TestApp::Literal') }
let(:other) { parse_expression('TestApp::Literal') }
it { should be(16) }
end
context 'when other expression describes a longer prefix' do
context 'on constants' do
let(:other) { described_class.parse('TestApp::Literal::Deep') }
let(:other) { parse_expression('TestApp::Literal::Deep') }
it { should be(input[0..-2].length) }
end
context 'on singleton method' do
let(:other) { described_class.parse('TestApp::Literal.foo') }
let(:other) { parse_expression('TestApp::Literal.foo') }
it { should be(input[0..-2].length) }
end
context 'on instance method' do
let(:other) { described_class.parse('TestApp::Literal#foo') }
let(:other) { parse_expression('TestApp::Literal#foo') }
it { should be(input[0..-2].length) }
end

View file

@ -0,0 +1,67 @@
RSpec.describe Mutant::Expression::Parser do
let(:object) { Mutant::Config::DEFAULT.expression_parser }
describe '#call' do
subject { object.call(input) }
context 'on nonsense' do
let(:input) { 'foo bar' }
it 'raises an exception' do
expect { subject }.to raise_error(
Mutant::Expression::Parser::InvalidExpressionError,
'Expression: "foo bar" is not valid'
)
end
end
context 'on a valid expression' do
let(:input) { 'Foo' }
it { should eql(Mutant::Expression::Namespace::Exact.new(scope_name: 'Foo')) }
end
end
describe '.try_parse' do
subject { object.try_parse(input) }
context 'on nonsense' do
let(:input) { 'foo bar' }
it { should be(nil) }
end
context 'on a valid expression' do
let(:input) { 'Foo' }
it { should eql(Mutant::Expression::Namespace::Exact.new(scope_name: 'Foo')) }
end
context 'on ambiguous expression' do
let(:object) { described_class.new([test_a, test_b]) }
let(:test_a) do
Class.new(Mutant::Expression) do
include Anima.new
const_set(:REGEXP, /\Atest-syntax\z/.freeze)
end
end
let(:test_b) do
Class.new(Mutant::Expression) do
include Anima.new
const_set(:REGEXP, /^test-syntax$/.freeze)
end
end
let(:input) { 'test-syntax' }
it 'raises expected exception' do
expect { subject }.to raise_error(
Mutant::Expression::Parser::AmbiguousExpressionError,
'Ambiguous expression: "test-syntax"'
)
end
end
end
end

View file

@ -1,99 +1,47 @@
RSpec.describe Mutant::Expression do
let(:object) { described_class }
describe '.try_parse' do
subject { object.try_parse(input) }
context 'on nonsense' do
let(:input) { 'foo bar' }
it { should be(nil) }
end
context 'on a valid expression' do
let(:input) { 'Foo' }
it { should eql(Mutant::Expression::Namespace::Exact.new('Foo')) }
end
context 'on ambiguous expression' do
class ExpressionA < Mutant::Expression
register(/\Atest-syntax\z/)
end
class ExpressionB < Mutant::Expression
register(/^test-syntax$/)
end
let(:input) { 'test-syntax' }
it 'raises an exception' do
expect { subject }.to raise_error(
Mutant::Expression::AmbiguousExpressionError,
'Ambiguous expression: "test-syntax"'
)
end
end
end
let(:parser) { Mutant::Config::DEFAULT.expression_parser }
describe '#prefix?' do
let(:object) { described_class.parse('Foo*') }
let(:object) { parser.call('Foo*') }
subject { object.prefix?(other) }
context 'when object is a prefix of other' do
let(:other) { described_class.parse('Foo::Bar') }
let(:other) { parser.call('Foo::Bar') }
it { should be(true) }
end
context 'when other is not a prefix of other' do
let(:other) { described_class.parse('Bar') }
let(:other) { parser.call('Bar') }
it { should be(false) }
end
end
describe '#inspect' do
let(:object) { described_class.parse('Foo') }
describe '.try_parse' do
let(:object) do
Class.new(described_class) do
include Anima.new(:foo)
subject { object.inspect }
it { should eql('<Mutant::Expression: Foo>') }
it_should_behave_like 'an idempotent method'
end
describe '#_dump' do
let(:object) { described_class.parse('Foo') }
subject { object._dump(double('Level')) }
it { should eql('Foo') }
end
describe '.parse' do
subject { object.parse(input) }
context 'on nonsense' do
let(:input) { 'foo bar' }
it 'raises an exception' do
expect { subject }.to raise_error(
Mutant::Expression::InvalidExpressionError,
'Expression: "foo bar" is not valid'
)
const_set(:REGEXP, /(?<foo>foo)/)
end
end
context 'on a valid expression' do
let(:input) { 'Foo' }
subject { object.try_parse(input) }
it { should eql(Mutant::Expression::Namespace::Exact.new('Foo')) }
context 'on succesful parse' do
let(:input) { 'foo' }
it { should eql(object.new(foo: 'foo')) }
end
context 'on unsuccesful parse' do
let(:input) { 'bar' }
it { should be(nil) }
end
end
describe '._load' do
subject { described_class._load('Foo') }
it { should eql(described_class.parse('Foo')) }
end
end

View file

@ -97,19 +97,19 @@ RSpec.describe Mutant::Integration::Rspec do
[
Mutant::Test.new(
id: 'rspec:0:example-a-location/example-a-full-description',
expression: Mutant::Expression.parse('*')
expression: parse_expression('*')
),
Mutant::Test.new(
id: 'rspec:1:example-c-location/Example::C blah',
expression: Mutant::Expression.parse('Example::C')
expression: parse_expression('Example::C')
),
Mutant::Test.new(
id: "rspec:2:example-d-location/Example::D\nblah",
expression: Mutant::Expression.parse('*')
expression: parse_expression('*')
),
Mutant::Test.new(
id: 'rspec:3:example-e-location/Example::E',
expression: Mutant::Expression.parse('Foo')
expression: parse_expression('Foo')
)
]
end

View file

@ -1,7 +1,7 @@
RSpec.describe Mutant::Matcher::Compiler::SubjectPrefix do
let(:object) { described_class.new(Mutant::Expression.parse('Foo*')) }
let(:object) { described_class.new(parse_expression('Foo*')) }
let(:_subject) { double('Subject', expression: Mutant::Expression.parse(subject_expression)) }
let(:_subject) { double('Subject', expression: parse_expression(subject_expression)) }
describe '#call' do
subject { object.call(_subject) }

View file

@ -3,8 +3,8 @@ RSpec.describe Mutant::Matcher::Compiler do
let(:env) { Fixtures::TEST_ENV }
let(:expression_a) { Mutant::Expression.parse('Foo*') }
let(:expression_b) { Mutant::Expression.parse('Bar*') }
let(:expression_a) { parse_expression('Foo*') }
let(:expression_b) { parse_expression('Bar*') }
let(:matcher_a) { expression_a.matcher(env) }
let(:matcher_b) { expression_b.matcher(env) }

View file

@ -1,16 +1,15 @@
RSpec.describe Mutant::Matcher::Filter do
let(:object) { described_class.new(matcher, predicate) }
let(:object) { described_class.new(matcher, predicate) }
let(:matcher) { [subject_a, subject_b] }
let(:subject_a) { double('Subject A') }
let(:subject_b) { double('Subject B') }
describe '#each' do
let(:yields) { [] }
subject { object.each { |entry| yields << entry } }
let(:matcher) { [subject_a, subject_b] }
let(:predicate) { ->(node) { node.eql?(subject_a) } }
let(:subject_a) { double('Subject A') }
let(:subject_b) { double('Subject B') }
# it_should_behave_like 'an #each method'
context 'with no block' do
subject { object.each }
@ -26,4 +25,12 @@ RSpec.describe Mutant::Matcher::Filter do
expect { subject }.to change { yields }.from([]).to([subject_a])
end
end
describe '.build' do
subject { described_class.build(matcher, &predicate) }
let(:predicate) { ->(_subject) { false } }
its(:to_a) { should eql([]) }
end
end

View file

@ -1,7 +1,7 @@
RSpec.describe Mutant::Matcher::Namespace do
let(:object) { described_class.new(env, Mutant::Expression.parse('TestApp*')) }
let(:yields) { [] }
let(:env) { double('Env') }
let(:object) { described_class.new(env, parse_expression('TestApp*')) }
let(:yields) { [] }
let(:env) { double('Env') }
subject { object.each { |item| yields << item } }
@ -22,7 +22,7 @@ RSpec.describe Mutant::Matcher::Namespace do
allow(env).to receive(:matchable_scopes).and_return(
[singleton_a, singleton_b, singleton_c].map do |scope|
Mutant::Matcher::Scope.new(env, scope, Mutant::Expression.parse(scope.name))
Mutant::Matcher::Scope.new(env, scope, parse_expression(scope.name))
end
)
end

View file

@ -3,27 +3,27 @@ RSpec.describe Mutant::Selector::Expression do
let(:object) { described_class.new(integration) }
let(:subject_class) do
parse = method(:parse_expression)
Class.new(Mutant::Subject) do
def expression
Mutant::Expression.parse('SubjectA')
define_method(:expression) do
parse.('SubjectA')
end
def match_expressions
[expression] << Mutant::Expression.parse('SubjectB')
define_method(:match_expressions) do
[expression] << parse.('SubjectB')
end
end
end
let(:mutation_subject) { subject_class.new(context, node) }
let(:context) { double('Context') }
let(:node) { double('Node') }
let(:config) { Mutant::Config::DEFAULT.update(integration: integration) }
let(:integration) { double('Integration', all_tests: all_tests) }
let(:test_a) { double('test a', expression: Mutant::Expression.parse('SubjectA')) }
let(:test_b) { double('test b', expression: Mutant::Expression.parse('SubjectB')) }
let(:test_c) { double('test c', expression: Mutant::Expression.parse('SubjectC')) }
let(:mutation_subject) { subject_class.new(context, node) }
let(:context) { double('Context') }
let(:node) { double('Node') }
let(:config) { Mutant::Config::DEFAULT.update(integration: integration) }
let(:integration) { double('Integration', all_tests: all_tests) }
let(:test_a) { double('test a', expression: parse_expression('SubjectA')) }
let(:test_b) { double('test b', expression: parse_expression('SubjectB')) }
let(:test_c) { double('test c', expression: parse_expression('SubjectC')) }
subject { object.call(mutation_subject) }

View file

@ -26,7 +26,7 @@ RSpec.describe Mutant::Subject::Method::Instance do
describe '#expression' do
subject { object.expression }
it { should eql(Mutant::Expression.parse('Test#foo')) }
it { should eql(parse_expression('Test#foo')) }
it_should_behave_like 'an idempotent method'
end
@ -34,7 +34,7 @@ RSpec.describe Mutant::Subject::Method::Instance do
describe '#match_expression' do
subject { object.match_expressions }
it { should eql(%w[Test#foo Test*].map(&Mutant::Expression.method(:parse))) }
it { should eql(%w[Test#foo Test*].map(&method(:parse_expression))) }
it_should_behave_like 'an idempotent method'
end

View file

@ -21,7 +21,7 @@ RSpec.describe Mutant::Subject::Method::Singleton do
describe '#expression' do
subject { object.expression }
it { should eql(Mutant::Expression.parse('Test.foo')) }
it { should eql(parse_expression('Test.foo')) }
it_should_behave_like 'an idempotent method'
end
@ -29,7 +29,7 @@ RSpec.describe Mutant::Subject::Method::Singleton do
describe '#match_expression' do
subject { object.match_expressions }
it { should eql(%w[Test.foo Test*].map(&Mutant::Expression.method(:parse))) }
it { should eql(%w[Test.foo Test*].map(&method(:parse_expression))) }
it_should_behave_like 'an idempotent method'
end

View file

@ -2,11 +2,14 @@ RSpec.describe Mutant::Subject do
let(:class_under_test) do
Class.new(described_class) do
def expression
Mutant::Expression.parse('SubjectA')
Mutant::Expression::Namespace::Exact.new(scope_name: 'SubjectA')
end
def match_expressions
[expression] << Mutant::Expression.parse('SubjectB')
[
expression,
Mutant::Expression::Namespace::Exact.new(scope_name: 'SubjectB')
]
end
end
end