diff --git a/config/flay.yml b/config/flay.yml index 439c6a22..c3a14bb7 100644 --- a/config/flay.yml +++ b/config/flay.yml @@ -1,3 +1,3 @@ --- threshold: 18 -total_score: 1194 +total_score: 1185 diff --git a/lib/mutant.rb b/lib/mutant.rb index 7d120490..0c8a2969 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -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 diff --git a/lib/mutant/cli.rb b/lib/mutant/cli.rb index a17ab652..824e1d4a 100644 --- a/lib/mutant/cli.rb +++ b/lib/mutant/cli.rb @@ -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 diff --git a/lib/mutant/config.rb b/lib/mutant/config.rb index 6338a216..1628100f 100644 --- a/lib/mutant/config.rb +++ b/lib/mutant/config.rb @@ -12,7 +12,8 @@ module Mutant :fail_fast, :jobs, :zombie, - :expected_coverage + :expected_coverage, + :expression_parser ) %i[fail_fast zombie debug].each do |name| diff --git a/lib/mutant/context/scope.rb b/lib/mutant/context/scope.rb index dadbb124..2d70f559 100644 --- a/lib/mutant/context/scope.rb +++ b/lib/mutant/context/scope.rb @@ -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 diff --git a/lib/mutant/env/bootstrap.rb b/lib/mutant/env/bootstrap.rb index 8101395c..fdd23bc2 100644 --- a/lib/mutant/env/bootstrap.rb +++ b/lib/mutant/env/bootstrap.rb @@ -106,6 +106,31 @@ module Mutant @integration = config.integration.new(config).setup end + # Return matched subjects + # + # @return [Enumerable] + # + # @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] - # - # @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 diff --git a/lib/mutant/expression.rb b/lib/mutant/expression.rb index e9197afb..92b8fd35 100644 --- a/lib/mutant/expression.rb +++ b/lib/mutant/expression.rb @@ -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 = /(?#{fragment}(?:#{SCOPE_OPERATOR}#{fragment})*)/.freeze + SCOPE_SYMBOL_PATTERN = '(?[.#])'.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 = ''.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 diff --git a/lib/mutant/expression/method.rb b/lib/mutant/expression/method.rb index 77fcbf25..c4cef2d6 100644 --- a/lib/mutant/expression/method.rb +++ b/lib/mutant/expression/method.rb @@ -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_PATTERN})(?[.#])(?#{METHOD_NAME_PATTERN})\z/ - ) + METHOD_NAME_PATTERN = Regexp.union( + /(?[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 diff --git a/lib/mutant/expression/methods.rb b/lib/mutant/expression/methods.rb index 76fcfd62..a2642ddf 100644 --- a/lib/mutant/expression/methods.rb +++ b/lib/mutant/expression/methods.rb @@ -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_PATTERN})(?[.#])\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 diff --git a/lib/mutant/expression/namespace.rb b/lib/mutant/expression/namespace.rb index 8e926a65..ab750b6b 100644 --- a/lib/mutant/expression/namespace.rb +++ b/lib/mutant/expression/namespace.rb @@ -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(?#{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(?#{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 diff --git a/lib/mutant/expression/parser.rb b/lib/mutant/expression/parser.rb new file mode 100644 index 00000000..75e8c29a --- /dev/null +++ b/lib/mutant/expression/parser.rb @@ -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] + # 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 diff --git a/lib/mutant/integration.rb b/lib/mutant/integration.rb index d145d372..ff2e7e03 100644 --- a/lib/mutant/integration.rb +++ b/lib/mutant/integration.rb @@ -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 diff --git a/lib/mutant/integration/rspec.rb b/lib/mutant/integration/rspec.rb index 2683e55a..ffad42e4 100644 --- a/lib/mutant/integration/rspec.rb +++ b/lib/mutant/integration/rspec.rb @@ -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 diff --git a/lib/mutant/matcher.rb b/lib/mutant/matcher.rb index d44dd649..a5111bff 100644 --- a/lib/mutant/matcher.rb +++ b/lib/mutant/matcher.rb @@ -28,13 +28,5 @@ module Mutant # abstract_method :each - # Return identification - # - # @return [String - # - # @api private - # - abstract_method :identification - end # Matcher end # Mutant diff --git a/lib/mutant/matcher/filter.rb b/lib/mutant/matcher/filter.rb index f6f9f77a..885c89fb 100644 --- a/lib/mutant/matcher/filter.rb +++ b/lib/mutant/matcher/filter.rb @@ -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] diff --git a/lib/mutant/matcher/method.rb b/lib/mutant/matcher/method.rb index f0bc1ad1..e02f1e40 100644 --- a/lib/mutant/matcher/method.rb +++ b/lib/mutant/matcher/method.rb @@ -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. diff --git a/lib/mutant/matcher/method/instance.rb b/lib/mutant/matcher/method/instance.rb index 96b17aeb..04f48788 100644 --- a/lib/mutant/matcher/method/instance.rb +++ b/lib/mutant/matcher/method/instance.rb @@ -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 diff --git a/lib/mutant/matcher/method/singleton.rb b/lib/mutant/matcher/method/singleton.rb index db4e7005..d593c9d7 100644 --- a/lib/mutant/matcher/method/singleton.rb +++ b/lib/mutant/matcher/method/singleton.rb @@ -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 diff --git a/lib/mutant/matcher/methods.rb b/lib/mutant/matcher/methods.rb index 18e90bb1..ea84135c 100644 --- a/lib/mutant/matcher/methods.rb +++ b/lib/mutant/matcher/methods.rb @@ -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] diff --git a/lib/mutant/matcher/scope.rb b/lib/mutant/matcher/scope.rb index 4e80b2c1..49a8752c 100644 --- a/lib/mutant/matcher/scope.rb +++ b/lib/mutant/matcher/scope.rb @@ -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] diff --git a/lib/mutant/subject/method.rb b/lib/mutant/subject/method.rb index 99e3c4b5..7882f50e 100644 --- a/lib/mutant/subject/method.rb +++ b/lib/mutant/subject/method.rb @@ -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 diff --git a/lib/mutant/subject/method/singleton.rb b/lib/mutant/subject/method/singleton.rb index 186ca5df..0850084f 100644 --- a/lib/mutant/subject/method/singleton.rb +++ b/lib/mutant/subject/method/singleton.rb @@ -5,7 +5,7 @@ module Mutant class Singleton < self NAME_INDEX = 1 - SYMBOL = '.'.freeze + SYMBOL = '.'.freeze # Test if method is public # diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 773653f7..059ba416 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 diff --git a/spec/unit/mutant/cli_spec.rb b/spec/unit/mutant/cli_spec.rb index 552c4f73..928910e9 100644 --- a/spec/unit/mutant/cli_spec.rb +++ b/spec/unit/mutant/cli_spec.rb @@ -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' diff --git a/spec/unit/mutant/context/scope_spec.rb b/spec/unit/mutant/context/scope_spec.rb new file mode 100644 index 00000000..a01f9976 --- /dev/null +++ b/spec/unit/mutant/context/scope_spec.rb @@ -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 diff --git a/spec/unit/mutant/env_spec.rb b/spec/unit/mutant/env_spec.rb index f79072a8..3adf9208 100644 --- a/spec/unit/mutant/env_spec.rb +++ b/spec/unit/mutant/env_spec.rb @@ -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( diff --git a/spec/unit/mutant/expression/method_spec.rb b/spec/unit/mutant/expression/method_spec.rb index 79b5fc8d..f5996e45 100644 --- a/spec/unit/mutant/expression/method_spec.rb +++ b/spec/unit/mutant/expression/method_spec.rb @@ -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 diff --git a/spec/unit/mutant/expression/methods_spec.rb b/spec/unit/mutant/expression/methods_spec.rb index be43fcc1..7d3868e9 100644 --- a/spec/unit/mutant/expression/methods_spec.rb +++ b/spec/unit/mutant/expression/methods_spec.rb @@ -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 diff --git a/spec/unit/mutant/expression/namespace/flat_spec.rb b/spec/unit/mutant/expression/namespace/flat_spec.rb index 8e9d0723..817a279c 100644 --- a/spec/unit/mutant/expression/namespace/flat_spec.rb +++ b/spec/unit/mutant/expression/namespace/flat_spec.rb @@ -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 diff --git a/spec/unit/mutant/expression/namespace/recursive_spec.rb b/spec/unit/mutant/expression/namespace/recursive_spec.rb index a975b4bd..ebed3f53 100644 --- a/spec/unit/mutant/expression/namespace/recursive_spec.rb +++ b/spec/unit/mutant/expression/namespace/recursive_spec.rb @@ -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 diff --git a/spec/unit/mutant/expression/parser_spec.rb b/spec/unit/mutant/expression/parser_spec.rb new file mode 100644 index 00000000..ec2df51d --- /dev/null +++ b/spec/unit/mutant/expression/parser_spec.rb @@ -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 diff --git a/spec/unit/mutant/expression_spec.rb b/spec/unit/mutant/expression_spec.rb index 4d1d91f7..51d35a08 100644 --- a/spec/unit/mutant/expression_spec.rb +++ b/spec/unit/mutant/expression_spec.rb @@ -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('') } - 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)/) 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 diff --git a/spec/unit/mutant/integration/rspec_spec.rb b/spec/unit/mutant/integration/rspec_spec.rb index 6e6ad952..fb1d06c9 100644 --- a/spec/unit/mutant/integration/rspec_spec.rb +++ b/spec/unit/mutant/integration/rspec_spec.rb @@ -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 diff --git a/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb b/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb index cfa891e6..264f8243 100644 --- a/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb +++ b/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb @@ -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) } diff --git a/spec/unit/mutant/matcher/compiler_spec.rb b/spec/unit/mutant/matcher/compiler_spec.rb index 97cf910b..5a4ec2f7 100644 --- a/spec/unit/mutant/matcher/compiler_spec.rb +++ b/spec/unit/mutant/matcher/compiler_spec.rb @@ -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) } diff --git a/spec/unit/mutant/matcher/filter_spec.rb b/spec/unit/mutant/matcher/filter_spec.rb index 6b4393e2..aa8a46e5 100644 --- a/spec/unit/mutant/matcher/filter_spec.rb +++ b/spec/unit/mutant/matcher/filter_spec.rb @@ -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 diff --git a/spec/unit/mutant/matcher/namespace_spec.rb b/spec/unit/mutant/matcher/namespace_spec.rb index fee55dc0..bdb51661 100644 --- a/spec/unit/mutant/matcher/namespace_spec.rb +++ b/spec/unit/mutant/matcher/namespace_spec.rb @@ -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 diff --git a/spec/unit/mutant/selector/expression_spec.rb b/spec/unit/mutant/selector/expression_spec.rb index 1f7c39a0..6e415d4f 100644 --- a/spec/unit/mutant/selector/expression_spec.rb +++ b/spec/unit/mutant/selector/expression_spec.rb @@ -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) } diff --git a/spec/unit/mutant/subject/method/instance_spec.rb b/spec/unit/mutant/subject/method/instance_spec.rb index 21b8318a..c7000584 100644 --- a/spec/unit/mutant/subject/method/instance_spec.rb +++ b/spec/unit/mutant/subject/method/instance_spec.rb @@ -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 diff --git a/spec/unit/mutant/subject/method/singleton_spec.rb b/spec/unit/mutant/subject/method/singleton_spec.rb index 87ca2fd2..053c5eae 100644 --- a/spec/unit/mutant/subject/method/singleton_spec.rb +++ b/spec/unit/mutant/subject/method/singleton_spec.rb @@ -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 diff --git a/spec/unit/mutant/subject_spec.rb b/spec/unit/mutant/subject_spec.rb index d634943a..4780e00c 100644 --- a/spec/unit/mutant/subject_spec.rb +++ b/spec/unit/mutant/subject_spec.rb @@ -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