From 6b17438c05da2476c1849817bca8bb8e754dd81b Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Wed, 28 Oct 2015 20:13:00 +0000 Subject: [PATCH] Reduce matcher interface * New primary interface #call makes specs and implementations much easier * We had #each mostly for historical reasons that are not relevant anymore * Mutation covers the Mutant::Matcher namespace --- config/flay.yml | 2 +- lib/mutant.rb | 2 + lib/mutant/env/bootstrap.rb | 44 +++- lib/mutant/expression/method.rb | 8 +- lib/mutant/expression/methods.rb | 6 +- lib/mutant/expression/namespace.rb | 12 +- lib/mutant/matcher.rb | 21 +- lib/mutant/matcher/chain.rb | 20 +- lib/mutant/matcher/compiler.rb | 15 +- lib/mutant/matcher/filter.rb | 25 +- lib/mutant/matcher/method.rb | 228 ++++++++++-------- lib/mutant/matcher/method/instance.rb | 74 +++--- lib/mutant/matcher/method/singleton.rb | 141 ++++++----- lib/mutant/matcher/methods.rb | 48 ++-- lib/mutant/matcher/namespace.rb | 38 +-- lib/mutant/matcher/null.rb | 11 +- lib/mutant/matcher/scope.rb | 36 ++- lib/mutant/matcher/static.rb | 17 ++ lib/mutant/scope.rb | 6 + spec/shared/method_matcher_behavior.rb | 36 +-- spec/unit/mutant/env/boostrap_spec.rb | 114 +++++++-- spec/unit/mutant/expression/method_spec.rb | 6 +- spec/unit/mutant/expression/methods_spec.rb | 7 +- .../mutant/expression/namespace/flat_spec.rb | 5 +- .../expression/namespace/recursive_spec.rb | 6 +- spec/unit/mutant/matcher/chain_spec.rb | 50 ++-- .../matcher/compiler/subject_prefix_spec.rb | 29 ++- spec/unit/mutant/matcher/compiler_spec.rb | 109 ++++----- spec/unit/mutant/matcher/filter_spec.rb | 46 ++-- .../mutant/matcher/method/instance_spec.rb | 206 ++++++---------- .../mutant/matcher/method/singleton_spec.rb | 70 +++--- .../mutant/matcher/methods/instance_spec.rb | 45 ++-- .../mutant/matcher/methods/singleton_spec.rb | 39 ++- spec/unit/mutant/matcher/namespace_spec.rb | 68 +++--- spec/unit/mutant/matcher/null_spec.rb | 25 +- spec/unit/mutant/matcher/scope_spec.rb | 33 +++ spec/unit/mutant/matcher/static_spec.rb | 11 + test_app/lib/test_app.rb | 28 ++- test_app/lib/test_app/literal.rb | 3 + 39 files changed, 869 insertions(+), 821 deletions(-) create mode 100644 lib/mutant/matcher/static.rb create mode 100644 lib/mutant/scope.rb create mode 100644 spec/unit/mutant/matcher/scope_spec.rb create mode 100644 spec/unit/mutant/matcher/static_spec.rb diff --git a/config/flay.yml b/config/flay.yml index d8f24430f..bc67177da 100644 --- a/config/flay.yml +++ b/config/flay.yml @@ -1,3 +1,3 @@ --- threshold: 18 -total_score: 1253 +total_score: 1237 diff --git a/lib/mutant.rb b/lib/mutant.rb index 7b59d43d9..9e194a725 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -132,6 +132,7 @@ def self.ci? require 'mutant/loader' require 'mutant/context' require 'mutant/context/scope' +require 'mutant/scope' require 'mutant/subject' require 'mutant/subject/method' require 'mutant/subject/method/instance' @@ -148,6 +149,7 @@ def self.ci? require 'mutant/matcher/scope' require 'mutant/matcher/filter' require 'mutant/matcher/null' +require 'mutant/matcher/static' require 'mutant/expression' require 'mutant/expression/parser' require 'mutant/expression/method' diff --git a/lib/mutant/env/bootstrap.rb b/lib/mutant/env/bootstrap.rb index d7b6ce279..cd32abea0 100644 --- a/lib/mutant/env/bootstrap.rb +++ b/lib/mutant/env/bootstrap.rb @@ -4,10 +4,18 @@ class Env class Bootstrap include Adamantium::Flat, Concord::Public.new(:config, :cache), Procto.call(:env) - SEMANTICS_MESSAGE = - "Fix your lib to follow normal ruby semantics!\n" \ + SEMANTICS_MESSAGE_FORMAT = + "%s. Fix your lib to follow normal ruby semantics!\n" \ '{Module,Class}#name should return resolvable constant name as String or nil'.freeze + CLASS_NAME_RAISED_EXCEPTION = + '%s#name from: %s raised an error: %s'.freeze + + CLASS_NAME_TYPE_MISMATCH_FORMAT = + '%s#name from: %s returned %s'.freeze + + private_constant(*constants(false)) + # Scopes that are eligible for matching # # @return [Enumerable] @@ -84,7 +92,12 @@ def env def scope_name(scope) scope.name rescue => exception - warn("#{scope.class}#name from: #{scope.inspect} raised an error: #{exception.inspect}. #{SEMANTICS_MESSAGE}") + semantics_warning( + CLASS_NAME_RAISED_EXCEPTION, + scope_class: scope.class, + scope: scope, + exception: exception.inspect + ) nil end @@ -95,7 +108,7 @@ def scope_name(scope) # @api private def infect config.includes.each(&$LOAD_PATH.method(:<<)) - config.requires.each(&method(:require)) + config.requires.each(&Kernel.method(:require)) @integration = config.integration.new(config).setup end @@ -105,7 +118,7 @@ def infect # # @api private def matched_subjects - Matcher::Compiler.call(self, config.matcher).to_a + Matcher::Compiler.call(config.matcher).call(self) end # Initialize matchable scopes @@ -115,8 +128,8 @@ def matched_subjects # @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 + expression = expression(scope) || next + aggregate << Scope.new(scope, expression) end @matchable_scopes = scopes.sort_by { |scope| scope.expression.syntax } @@ -137,12 +150,27 @@ def expression(scope) name = scope_name(scope) or return unless name.instance_of?(String) - warn("#{scope.class}#name from: #{scope.inspect} returned #{name.inspect}. #{SEMANTICS_MESSAGE}") + semantics_warning( + CLASS_NAME_TYPE_MISMATCH_FORMAT, + scope_class: scope.class, + scope: scope, + name: name + ) return end config.expression_parser.try_parse(name) end + + # Write a semantics warning + # + # @return [undefined] + # + # @api private + def semantics_warning(format, options) + message = format % options + warn(SEMANTICS_MESSAGE_FORMAT % { message: message }) + end end # Boostrap end # Env end # Mutant diff --git a/lib/mutant/expression/method.rb b/lib/mutant/expression/method.rb index 30953f7bf..67b736e0b 100644 --- a/lib/mutant/expression/method.rb +++ b/lib/mutant/expression/method.rb @@ -32,15 +32,13 @@ def syntax # Matcher for expression # - # @param [Env] env - # # @return [Matcher] # # @api private - def matcher(env) - methods_matcher = MATCHERS.fetch(scope_symbol).new(env, scope) + def matcher + methods_matcher = MATCHERS.fetch(scope_symbol).new(scope) - Matcher::Filter.build(methods_matcher) { |subject| subject.expression.eql?(self) } + Matcher::Filter.new(methods_matcher, ->(subject) { subject.expression.eql?(self) }) end private diff --git a/lib/mutant/expression/methods.rb b/lib/mutant/expression/methods.rb index 44813e4f7..1549c8aa7 100644 --- a/lib/mutant/expression/methods.rb +++ b/lib/mutant/expression/methods.rb @@ -26,13 +26,11 @@ def syntax # Matcher on expression # - # @param [Env] env - # # @return [Matcher::Method] # # @api private - def matcher(env) - MATCHERS.fetch(scope_symbol).new(env, scope) + def matcher + MATCHERS.fetch(scope_symbol).new(scope) end # Length of match with other expression diff --git a/lib/mutant/expression/namespace.rb b/lib/mutant/expression/namespace.rb index bfae67d57..22a591081 100644 --- a/lib/mutant/expression/namespace.rb +++ b/lib/mutant/expression/namespace.rb @@ -35,13 +35,11 @@ def syntax # Matcher for expression # - # @param [Env::Bootstrap] env - # # @return [Matcher] # # @api private - def matcher(env) - Matcher::Namespace.new(env, self) + def matcher + Matcher::Namespace.new(self) end # Length of match with other expression @@ -71,13 +69,11 @@ class Exact < self # Matcher matcher on expression # - # @param [Env::Bootstrap] env - # # @return [Matcher] # # @api private - def matcher(env) - Matcher::Scope.new(env, Object.const_get(scope_name), self) + def matcher + Matcher::Scope.new(Object.const_get(scope_name)) end # Syntax for expression diff --git a/lib/mutant/matcher.rb b/lib/mutant/matcher.rb index d555c22ca..3e51b7a6d 100644 --- a/lib/mutant/matcher.rb +++ b/lib/mutant/matcher.rb @@ -1,30 +1,15 @@ module Mutant # Abstract matcher to find subjects to mutate class Matcher - include Adamantium::Flat, Enumerable, AbstractType + include Adamantium::Flat, AbstractType - # Default matcher build implementation + # Call matcher # # @param [Env] env - # @param [Object] input - # - # @return [undefined] - # - # @api private - def self.build(*arguments) - new(*arguments) - end - - # Enumerate subjects - # - # @return [self] - # if block given # # @return [Enumerable] - # otherwise # - # @api private - abstract_method :each + abstract_method :call end # Matcher end # Mutant diff --git a/lib/mutant/matcher/chain.rb b/lib/mutant/matcher/chain.rb index 702b07b82..7263c378b 100644 --- a/lib/mutant/matcher/chain.rb +++ b/lib/mutant/matcher/chain.rb @@ -1,26 +1,20 @@ module Mutant class Matcher - # A chain of matchers + # Matcher chaining results of other matchers together class Chain < self include Concord.new(:matchers) - # Enumerate subjects + # Call matcher # - # @return [Enumerator] # # @api private - def each(&block) - return to_enum unless block_given? - - matchers.each do |matcher| - matcher.each(&block) + def call(env) + matchers.flat_map do |matcher| + matcher.call(env) end - - self end end # Chain diff --git a/lib/mutant/matcher/compiler.rb b/lib/mutant/matcher/compiler.rb index 2ae5afcb2..2a1399f8b 100644 --- a/lib/mutant/matcher/compiler.rb +++ b/lib/mutant/matcher/compiler.rb @@ -3,7 +3,7 @@ class Matcher # Compiler for complex matchers class Compiler - include Concord.new(:env, :config), AST::Sexp, Procto.call(:result) + include Concord.new(:config), AST::Sexp, Procto.call(:result) # Generated matcher # @@ -12,7 +12,7 @@ class Compiler # @api private def result Filter.new( - Chain.build(config.match_expressions.map(&method(:matcher))), + Chain.new(config.match_expressions.map(&:matcher)), Morpher::Evaluator::Predicate::Boolean::And.new( [ ignored_subjects, @@ -61,17 +61,6 @@ def filtered_subjects Morpher::Evaluator::Predicate::Boolean::And.new(config.subject_filters) end - # Matcher for expression on env - # - # @param [Mutant::Expression] expression - # - # @return [Matcher] - # - # @api private - def matcher(expression) - expression.matcher(env) - end - end # Compiler end # Matcher end # Mutant diff --git a/lib/mutant/matcher/filter.rb b/lib/mutant/matcher/filter.rb index 61f59027f..b65dda9cd 100644 --- a/lib/mutant/matcher/filter.rb +++ b/lib/mutant/matcher/filter.rb @@ -4,30 +4,17 @@ class Matcher class Filter < self include Concord.new(:matcher, :predicate) - # New matcher - # - # @param [Matcher] matcher - # - # @return [Matcher] - # - # @api private - def self.build(matcher, &predicate) - new(matcher, predicate) - end - # Enumerate matches # - # @return [self] - # if block given + # @param [Env] env # - # @return [Enumerator] - # otherwise + # @return [Enumerable] # # @api private - def each(&block) - return to_enum unless block_given? - matcher.select(&predicate.method(:call)).each(&block) - self + def call(env) + matcher + .call(env) + .select(&predicate.method(:call)) end end # Filter diff --git a/lib/mutant/matcher/method.rb b/lib/mutant/matcher/method.rb index ac5b3bc1f..02306179d 100644 --- a/lib/mutant/matcher/method.rb +++ b/lib/mutant/matcher/method.rb @@ -1,133 +1,153 @@ module Mutant class Matcher - # Matcher for subjects that are a specific method + # Abstract base class for method matchers class Method < self - include Adamantium::Flat, Concord::Public.new(:env, :scope, :target_method) - include AST::NodePredicates + include AbstractType, + Adamantium::Flat, + Concord::Public.new(:scope, :target_method, :evaluator) # Methods within rbx kernel directory are precompiled and their source # cannot be accessed via reading source location. Same for methods created by eval. BLACKLIST = %r{\A(kernel/|\(eval\)\z)}.freeze - # Enumerate matches + SOURCE_LOCATION_WARNING_FORMAT = + '%s does not have a valid source location, unable to emit subject'.freeze + + CLOSURE_WARNING_FORMAT = + '%s is dynamically defined in a closure, unable to emit subject'.freeze + + # Matched subjects # - # @return [Enumerable] - # if no block given + # @param [Env] env # - # @return [self] - # otherwise + # @return [Enumerable] # # @api private - def each - return to_enum unless block_given? + def call(env) + evaluator.call(scope, target_method, env) + end + + # Abstract method match evaluator + # + # Present to avoid passing the env argument around in case the + # logic would be implemnented directly on the Matcher::Method + # instance + class Evaluator + include AbstractType, + Adamantium, + Concord.new(:scope, :target_method, :env), + Procto.call, + AST::NodePredicates + + # Matched subjects + # + # @return [Enumerable] + # + # @api private + def call + return EMPTY_ARRAY if skip? - if !skip? && subject - yield subject + [subject].compact end - self - end + private - private + # Test if method should be skipped + # + # @return [Truthy] + # + # @api private + def skip? + location = source_location + if location.nil? || BLACKLIST.match(location.first) + env.warn(SOURCE_LOCATION_WARNING_FORMAT % target_method) + elsif matched_node_path.any?(&method(:n_block?)) + env.warn(CLOSURE_WARNING_FORMAT % target_method) + end + end - # Test if method should be skipped - # - # @return [Boolean] - # - # @api private - def skip? - location = source_location - if location.nil? || BLACKLIST.match(location.first) - env.warn(format('%s does not have valid source location unable to emit subject', target_method.inspect)) - true - elsif matched_node_path.any?(&method(:n_block?)) - env.warn(format('%s is defined from a 3rd party lib unable to emit subject', target_method.inspect)) - true - else - false + # Target method name + # + # @return [String] + # + # @api private + def method_name + target_method.name end - end - # Target method name - # - # @return [String] - # - # @api private - def method_name - target_method.name - end + # Target context + # + # @return [Context::Scope] + # + # @api private + def context + Context::Scope.new(scope, source_path) + end - # Target context - # - # @return [Context::Scope] - # - # @api private - def context - Context::Scope.new(scope, source_path) - end + # Root source node + # + # @return [Parser::AST::Node] + # + # @api private + def ast + env.cache.parse(source_path) + end - # Root source node - # - # @return [Parser::AST::Node] - # - # @api private - def ast - env.cache.parse(source_path) - end + # Path to source + # + # @return [Pathname] + # + # @api private + def source_path + Pathname.new(source_location.first) + end + memoize :source_path - # Path to source - # - # @return [Pathname] - # - # @api private - def source_path - Pathname.new(source_location.first) - end - memoize :source_path + # Source file line + # + # @return [Fixnum] + # + # @api private + def source_line + source_location.last + end - # Source file line - # - # @return [Fixnum] - # - # @api private - def source_line - source_location.last - end + # Full source location + # + # @return [Array{String,Fixnum}] + # + # @api private + def source_location + target_method.source_location + end - # Full source location - # - # @return [Array{String,Fixnum}] - # - # @api private - def source_location - target_method.source_location - end + # Matched subject + # + # @return [Subject] + # if there is a matched node + # + # @return [nil] + # otherwise + # + # @api private + def subject + node = matched_node_path.last || return + self.class::SUBJECT_CLASS.new(context, node) + end + memoize :subject - # Matched subject - # - # @return [Subject] - # if there is a matched node - # - # @return [nil] - # otherwise - # - # @api private - def subject - node = matched_node_path.last - return unless node - self.class::SUBJECT_CLASS.new(context, node) - end - memoize :subject + # Matched node path + # + # @return [Array] + # + # @api private + def matched_node_path + AST.find_last_path(ast, &method(:match?)) + end + memoize :matched_node_path + end # Evaluator - # Matched node path - # - # @return [Array] - # - # @api private - def matched_node_path - AST.find_last_path(ast, &method(:match?)) - end - memoize :matched_node_path + private_constant(*constants(false)) end # Method end # Matcher diff --git a/lib/mutant/matcher/method/instance.rb b/lib/mutant/matcher/method/instance.rb index 433a8b917..a0e2b9179 100644 --- a/lib/mutant/matcher/method/instance.rb +++ b/lib/mutant/matcher/method/instance.rb @@ -3,62 +3,68 @@ class Matcher class Method # Matcher for instance methods class Instance < self - SUBJECT_CLASS = Subject::Method::Instance # Dispatching builder, detects memoizable case # - # @param [Env::Boostrap] env # @param [Class, Module] scope # @param [UnboundMethod] method # # @return [Matcher::Method::Instance] # # @api private - def self.build(env, scope, target_method) + def self.new(scope, target_method) name = target_method.name - if scope.ancestors.include?(::Memoizable) && scope.memoized?(name) - return Memoized.new(env, scope, target_method) - end - super - end - - NAME_INDEX = 0 - - private + evaluator = + if scope.include?(Memoizable) && scope.memoized?(name) + Evaluator::Memoized + else + Evaluator + end - # Check if node is matched - # - # @param [Parser::AST::Node] node - # - # @return [Boolean] - # - # @api private - def match?(node) - location = node.location || return - expression = location.expression || return - - expression.line.equal?(source_line) && - node.type.equal?(:def) && - node.children[NAME_INDEX].equal?(method_name) + super(scope, target_method, evaluator) end - # Matcher for memoized instance methods - class Memoized < self - SUBJECT_CLASS = Subject::Method::Instance::Memoized + # Instance method specific evaluator + class Evaluator < Evaluator + SUBJECT_CLASS = Subject::Method::Instance + NAME_INDEX = 0 private - # Source location + # Check if node is matched + # + # @param [Parser::AST::Node] node # - # @return [Array{String,Fixnum}] + # @return [Boolean] # # @api private - def source_location - scope.unmemoized_instance_method(method_name).source_location + def match?(node) + n_def?(node) && + node.location.line.equal?(source_line) && + node.children.fetch(NAME_INDEX).equal?(method_name) end - end # Memoized + # Evaluator specialized for memoized instance mthods + class Memoized < self + SUBJECT_CLASS = Subject::Method::Instance::Memoized + + private + + # Source location + # + # @return [Array{String,Fixnum}] + # + # @api private + def source_location + scope + .unmemoized_instance_method(method_name) + .source_location + end + + end # Memoized + end # Evaluator + private_constant(*constants(false)) end # Instance end # Method end # Matcher diff --git a/lib/mutant/matcher/method/singleton.rb b/lib/mutant/matcher/method/singleton.rb index b0c4dfe71..32abbfee4 100644 --- a/lib/mutant/matcher/method/singleton.rb +++ b/lib/mutant/matcher/method/singleton.rb @@ -3,79 +3,98 @@ class Matcher class Method # Matcher for singleton methods class Singleton < self - SUBJECT_CLASS = Subject::Method::Singleton - RECEIVER_INDEX = 0 - NAME_INDEX = 1 - private - - # Test for node match + # New singleton method matcher # - # @param [Parser::AST::Node] node + # @param [Class, Module] scope + # @param [Symbol] method_name # - # @return [Boolean] + # @return [Matcher::Method::Singleton] # # @api private - def match?(node) - line?(node) && name?(node) && receiver?(node) + def self.new(scope, method_name) + super(scope, method_name, Evaluator) end - # Test for line match - # - # @param [Parser::AST::Node] node - # - # @return [Boolean] - # - # @api private - def line?(node) - expression = node.location.expression - return false unless expression - expression.line.equal?(source_line) - end + # Singleton method evaluator + class Evaluator < Evaluator + SUBJECT_CLASS = Subject::Method::Singleton + RECEIVER_INDEX = 0 + NAME_INDEX = 1 - # Test for name match - # - # @param [Parser::AST::Node] node - # - # @return [Boolean] - # - # @api private - def name?(node) - node.children[NAME_INDEX].equal?(method_name) - end + private - # Test for receiver match - # - # @param [Parser::AST::Node] node - # - # @return [Boolean] - # - # @api private - def receiver?(node) - receiver = node.children[RECEIVER_INDEX] - case receiver.type - when :self - true - when :const - receiver_name?(receiver) - else - env.warn(format('Can only match :defs on :self or :const got %s unable to match', receiver.type.inspect)) - false + # Test for node match + # + # @param [Parser::AST::Node] node + # + # @return [Boolean] + # + # @api private + def match?(node) + n_defs?(node) && line?(node) && name?(node) && receiver?(node) end - end - # Test if receiver name matches context - # - # @param [Parser::AST::Node] node - # - # @return [Boolean] - # - # @api private - def receiver_name?(node) - name = node.children[NAME_INDEX] - name.to_s.eql?(context.unqualified_name) - end + # Test for line match + # + # @param [Parser::AST::Node] node + # + # @return [Boolean] + # + # @api private + def line?(node) + node + .location + .line + .equal?(source_line) + end + + # Test for name match + # + # @param [Parser::AST::Node] node + # + # @return [Boolean] + # + # @api private + def name?(node) + node.children.fetch(NAME_INDEX).equal?(method_name) + end + + # Test for receiver match + # + # @param [Parser::AST::Node] node + # + # @return [Boolean] + # + # @api private + def receiver?(node) + receiver = node.children.fetch(RECEIVER_INDEX) + case receiver.type + when :self + true + when :const + receiver_name?(receiver) + else + env.warn(format('Can only match :defs on :self or :const got %s unable to match', receiver.type.inspect)) + nil + end + end + + # Test if receiver name matches context + # + # @param [Parser::AST::Node] node + # + # @return [Boolean] + # + # @api private + def receiver_name?(node) + name = node.children.fetch(NAME_INDEX) + name.to_s.eql?(context.unqualified_name) + end + + end # Evaluator + private_constant(*constants(false)) end # Singleton end # Method end # Matcher diff --git a/lib/mutant/matcher/methods.rb b/lib/mutant/matcher/methods.rb index 6a83fea3f..612a1cddd 100644 --- a/lib/mutant/matcher/methods.rb +++ b/lib/mutant/matcher/methods.rb @@ -2,23 +2,27 @@ module Mutant class Matcher # Abstract base class for matcher that returns method subjects from scope class Methods < self - include AbstractType, Concord::Public.new(:env, :scope) + include AbstractType, Concord.new(:scope) + + CANDIDATE_NAMES = IceNine.deep_freeze(%i[ + public_instance_methods + private_instance_methods + protected_instance_methods + ]) + + private_constant(*constants(false)) # Enumerate subjects # - # @return [self] - # if block given + # @param [Env] env # - # @return [Enumerator] - # otherwise + # @return [Enumerable] # # @api private - def each(&block) - return to_enum unless block_given? - - subjects.each(&block) - - self + def call(env) + Chain.new( + methods.map { |method| matcher.new(scope, method) } + ).call(env) end private @@ -45,29 +49,16 @@ def methods end memoize :methods - # Subjects detected on scope - # - # @return [Array] - # - # @api private - def subjects - methods.map do |method| - matcher.build(env, scope, method) - end.flat_map(&:to_a) - end - memoize :subjects - # Candidate method names on target scope # # @return [Enumerable] # # @api private def candidate_names - ( - candidate_scope.public_instance_methods(false) + - candidate_scope.private_instance_methods(false) + - candidate_scope.protected_instance_methods(false) - ).sort + CANDIDATE_NAMES + .map(&candidate_scope.method(:public_send)) + .reduce(:+) + .sort end # Candidate scope @@ -133,7 +124,6 @@ def candidate_scope end end # Instance - end # Methods end # Matcher end # Mutant diff --git a/lib/mutant/matcher/namespace.rb b/lib/mutant/matcher/namespace.rb index ca10019e4..83d8984cb 100644 --- a/lib/mutant/matcher/namespace.rb +++ b/lib/mutant/matcher/namespace.rb @@ -1,34 +1,40 @@ module Mutant class Matcher - # Matcher for specific namespace class Namespace < self - include Concord::Public.new(:env, :expression) + include Concord::Public.new(:expression) # Enumerate subjects # - # @return [self] - # if block given + # @param [Env] env # - # @return [Enumerator] - # otherwise + # @return [Enumerable] # # @api private - def each(&block) - return to_enum unless block_given? - - env.matchable_scopes.select do |scope| - scope.each(&block) if match?(scope) - end - - self + def call(env) + Chain.new( + matched_scopes(env).map { |scope| Scope.new(scope.raw) } + ).call(env) end private - # Test scope if name matches expression + # The matched scopes + # + # @param [Env] env + # + # @return [Enumerable] + # + # @api private + def matched_scopes(env) + env + .matchable_scopes + .select(&method(:match?)) + end + + # Test scope if matches expression # - # @param [Module, Class] scope + # @param [Scope] scope # # @return [Boolean] # diff --git a/lib/mutant/matcher/null.rb b/lib/mutant/matcher/null.rb index fe7a98649..92c7b3030 100644 --- a/lib/mutant/matcher/null.rb +++ b/lib/mutant/matcher/null.rb @@ -6,16 +6,13 @@ class Null < self # Enumerate subjects # - # @return [Enumerator] # # @api private - def each - return to_enum unless block_given? - self + def call(_env) + EMPTY_ARRAY end end # Null diff --git a/lib/mutant/matcher/scope.rb b/lib/mutant/matcher/scope.rb index 9d9414d97..fcde01a7b 100644 --- a/lib/mutant/matcher/scope.rb +++ b/lib/mutant/matcher/scope.rb @@ -1,31 +1,41 @@ module Mutant class Matcher - # Matcher for specific namespace + # Matcher expanding Mutant::Scope objects into method matches + # at singleton or instance level + # + # If we *ever* get other subjects than methods, its likely the place + # to hook in custom matchers. In that case the scope matchers to expand + # should be passed as arguments to the constructor. class Scope < self - include Concord::Public.new(:env, :scope, :expression) + include Concord.new(:scope) MATCHERS = [ Matcher::Methods::Singleton, Matcher::Methods::Instance ].freeze - # Enumerate subjects + private_constant(*constants(false)) + + # Matched subjects # - # @return [self] - # if block given + # @param [Env] env # - # @return [Enumerator] - # otherwise + # @return [Enumerable] # # @api private - def each(&block) - return to_enum unless block_given? + def call(env) + Chain.new(effective_matchers).call(env) + end - MATCHERS.each do |matcher| - matcher.new(env, scope).each(&block) - end + private - self + # Effective matchers + # + # @return [Enumerable] + # + # @api private + def effective_matchers + MATCHERS.map { |matcher| matcher.new(scope) } end end # Scope diff --git a/lib/mutant/matcher/static.rb b/lib/mutant/matcher/static.rb new file mode 100644 index 000000000..01074c016 --- /dev/null +++ b/lib/mutant/matcher/static.rb @@ -0,0 +1,17 @@ +module Mutant + class Matcher + # Matcher returning subjects already known at its creation time + class Static + include Concord.new(:subjects) + + # Call matcher + # + # @return [Enumerable] + # + # @api private + def call(_env) + subjects + end + end # Static + end # Matcher +end # Mutant diff --git a/lib/mutant/scope.rb b/lib/mutant/scope.rb new file mode 100644 index 000000000..f4334c1f7 --- /dev/null +++ b/lib/mutant/scope.rb @@ -0,0 +1,6 @@ +module Mutant + # Class or Module bound to an exact expression + class Scope + include Concord::Public.new(:raw, :expression) + end # Scope +end # Mutant diff --git a/spec/shared/method_matcher_behavior.rb b/spec/shared/method_matcher_behavior.rb index f0abe40ac..895cfb019 100644 --- a/spec/shared/method_matcher_behavior.rb +++ b/spec/shared/method_matcher_behavior.rb @@ -1,38 +1,46 @@ RSpec.shared_examples_for 'a method matcher' do - - before { subject } - let(:node) { mutation_subject.node } let(:context) { mutation_subject.context } - let(:mutation_subject) { yields.first } + let(:mutation_subject) { subject.first } - it 'should return one subject' do - expect(yields.size).to be(1) + it 'returns one subject' do + expect(subject.size).to be(1) end - it_should_behave_like 'an #each method' - - it 'should have correct method name' do + it 'has expected method name' do expect(name).to eql(method_name) end - it 'should have correct line number' do + it 'has epxected line number' do expect(node.location.expression.line).to eql(method_line) end - it 'should have correct arity' do + it 'has expected arity' do expect(arguments.children.length).to eql(method_arity) end - it 'should have correct scope in context' do + it 'has expected scope in context' do expect(context.scope).to eql(scope) end - it 'should have the correct source path in context' do + it 'has source path in context' do expect(context.source_path).to eql(source_path) end - it 'should have the correct node type' do + it 'has the correct node type' do expect(node.type).to be(type) end end + +RSpec.shared_examples_for 'skipped candidate' do + it 'does not emit matcher' do + expect(subject).to eql([]) + end + + it 'does warn' do + subject + expected_warnings.each do |warning| + expect(env.config.reporter.warn_calls).to include(warning) + end + end +end diff --git a/spec/unit/mutant/env/boostrap_spec.rb b/spec/unit/mutant/env/boostrap_spec.rb index 0176d31f9..b6a1a0eca 100644 --- a/spec/unit/mutant/env/boostrap_spec.rb +++ b/spec/unit/mutant/env/boostrap_spec.rb @@ -1,16 +1,29 @@ +# This spec is a good example for: +# +# If test look that ugly the class under test sucks. +# +# As the bootstrap needs to infect global VM state +# this is to some degree acceptable. +# +# Still the bootstrap needs to be cleaned up. +# And the change that added this warning did the groundwork. RSpec.describe Mutant::Env::Bootstrap do + let(:matcher_config) { Mutant::Matcher::Config::DEFAULT } + let(:integration) { instance_double(Mutant::Integration) } + let(:integration_class) { instance_double(Class) } + let(:object_space_modules) { [] } + let(:config) do Mutant::Config::DEFAULT.with( - jobs: 1, - reporter: Mutant::Reporter::Trace.new, - includes: [], - requires: [], - matcher: Mutant::Matcher::Config::DEFAULT + jobs: 1, + reporter: Mutant::Reporter::Trace.new, + includes: [], + requires: [], + integration: integration_class, + matcher: matcher_config ) end - let(:integration) { Mutant::Integration::Null.new(config) } - let(:expected_env) do Mutant::Env.new( cache: Mutant::Cache.new, @@ -28,16 +41,40 @@ it { should eql(expected_env) } end - let(:object_space_modules) { [] } - before do - allow(ObjectSpace).to receive(:each_object).with(Module).and_return(object_space_modules.each) + expect(integration_class).to receive(:new) + .with(config) + .and_return(integration) + + expect(integration).to receive(:setup).and_return(integration) + + expect(ObjectSpace).to receive(:each_object) + .with(Module) + .and_return(object_space_modules.each) + end + + describe '#warn' do + let(:object) { described_class.new(config) } + let(:message) { instance_double(String) } + + subject { object.warn(message) } + + it 'reports a warning' do + expect { subject } + .to change { object.config.reporter.warn_calls } + .from([]) + .to([message]) + end + + it_behaves_like 'a command method' end describe '.call' do subject { described_class.call(config) } context 'when Module#name calls result in exceptions' do + let(:object_space_modules) { [invalid_class] } + let(:invalid_class) do Class.new do def self.name @@ -46,8 +83,6 @@ def self.name end end - let(:object_space_modules) { [invalid_class] } - after do # Fix Class#name so other specs do not see this one class << invalid_class @@ -59,21 +94,41 @@ def name it 'warns via reporter' do expected_warnings = [ - "Class#name from: #{invalid_class} raised an error: RuntimeError. #{Mutant::Env::SEMANTICS_MESSAGE}" + "Class#name from: #{invalid_class} raised an error: " \ + "RuntimeError. #{Mutant::Env::SEMANTICS_MESSAGE}" ] - expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings) + expect { subject } + .to change { config.reporter.warn_calls } + .from([]) + .to(expected_warnings) end include_examples 'bootstrap call' end - context 'when includes are present' do + context 'when requires are configured' do + let(:config) { super().with(requires: %w[foo bar]) } + + before do + %w[foo bar].each do |component| + expect(Kernel).to receive(:require) + .with(component) + .and_return(true) + end + end + + include_examples 'bootstrap call' + end + + context 'when includes are configured' do let(:config) { super().with(includes: %w[foo bar]) } before do %w[foo bar].each do |component| - expect($LOAD_PATH).to receive(:<<).with(component).and_return($LOAD_PATH) + expect($LOAD_PATH).to receive(:<<) + .with(component) + .and_return($LOAD_PATH) end end @@ -81,6 +136,8 @@ def name end context 'when Module#name does not return a String or nil' do + let(:object_space_modules) { [invalid_class] } + let(:invalid_class) do Class.new do def self.name @@ -89,8 +146,6 @@ def self.name end end - let(:object_space_modules) { [invalid_class] } - after do # Fix Class#name so other specs do not see this one class << invalid_class @@ -101,29 +156,36 @@ def name end it 'warns via reporter' do - expected_warnings = [ "Class#name from: #{invalid_class.inspect} returned Object. #{Mutant::Env::SEMANTICS_MESSAGE}" ] - expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings) + expect { subject } + .to change { config.reporter.warn_calls } + .from([]).to(expected_warnings) end include_examples 'bootstrap call' end context 'when scope matches expression' do - let(:mutations) { [double('Mutation')] } - let(:subjects) { [double('Subject', mutations: mutations)] } + let(:object_space_modules) { [TestApp::Literal, TestApp::Empty] } + let(:match_expressions) { object_space_modules.map(&:name).map(&method(:parse_expression)) } - before do - expect(Mutant::Matcher::Compiler).to receive(:call).and_return(subjects) + let(:matcher_config) do + super().with(match_expressions: match_expressions) end let(:expected_env) do + subjects = Mutant::Matcher::Scope.new(TestApp::Literal).call(Fixtures::TEST_ENV) + super().with( - subjects: subjects, - mutations: mutations + matchable_scopes: [ + Mutant::Scope.new(TestApp::Empty, match_expressions.last), + Mutant::Scope.new(TestApp::Literal, match_expressions.first) + ], + subjects: subjects, + mutations: subjects.flat_map(&:mutations) ) end diff --git a/spec/unit/mutant/expression/method_spec.rb b/spec/unit/mutant/expression/method_spec.rb index f5996e459..fb780c48b 100644 --- a/spec/unit/mutant/expression/method_spec.rb +++ b/spec/unit/mutant/expression/method_spec.rb @@ -23,13 +23,13 @@ end describe '#matcher' do - subject { object.matcher(env) } + subject { object.matcher } context 'with an instance method' do let(:input) { instance_method } it 'returns correct matcher' do - expect(subject.map(&:expression)).to eql([object]) + expect(subject.call(env).map(&:expression)).to eql([object]) end end @@ -37,7 +37,7 @@ let(:input) { singleton_method } it 'returns correct matcher' do - expect(subject.map(&:expression)).to eql([object]) + expect(subject.call(env).map(&:expression)).to eql([object]) end end end diff --git a/spec/unit/mutant/expression/methods_spec.rb b/spec/unit/mutant/expression/methods_spec.rb index 7d3868e9e..835bd2c26 100644 --- a/spec/unit/mutant/expression/methods_spec.rb +++ b/spec/unit/mutant/expression/methods_spec.rb @@ -1,5 +1,4 @@ RSpec.describe Mutant::Expression::Methods do - let(:env) { Fixtures::TEST_ENV } let(:object) { described_class.new(attributes) } describe '#match_length' do @@ -43,18 +42,18 @@ end describe '#matcher' do - subject { object.matcher(env) } + subject { object.matcher } context 'with an instance method' do let(:attributes) { { scope_name: 'TestApp::Literal', scope_symbol: '#' } } - it { should eql(Mutant::Matcher::Methods::Instance.new(env, TestApp::Literal)) } + it { should eql(Mutant::Matcher::Methods::Instance.new(TestApp::Literal)) } end context 'with a singleton method' do let(:attributes) { { scope_name: 'TestApp::Literal', scope_symbol: '.' } } - it { should eql(Mutant::Matcher::Methods::Singleton.new(env, TestApp::Literal)) } + it { should eql(Mutant::Matcher::Methods::Singleton.new(TestApp::Literal)) } end end end diff --git a/spec/unit/mutant/expression/namespace/flat_spec.rb b/spec/unit/mutant/expression/namespace/flat_spec.rb index 817a279c6..24c3d3b75 100644 --- a/spec/unit/mutant/expression/namespace/flat_spec.rb +++ b/spec/unit/mutant/expression/namespace/flat_spec.rb @@ -1,12 +1,11 @@ RSpec.describe Mutant::Expression::Namespace::Exact do let(:object) { parse_expression(input) } - let(:env) { Fixtures::TEST_ENV } let(:input) { 'TestApp::Literal' } describe '#matcher' do - subject { object.matcher(env) } + subject { object.matcher } - it { should eql(Mutant::Matcher::Scope.new(env, TestApp::Literal, object)) } + it { should eql(Mutant::Matcher::Scope.new(TestApp::Literal)) } end describe '#match_length' do diff --git a/spec/unit/mutant/expression/namespace/recursive_spec.rb b/spec/unit/mutant/expression/namespace/recursive_spec.rb index ebed3f53f..d32bfecb7 100644 --- a/spec/unit/mutant/expression/namespace/recursive_spec.rb +++ b/spec/unit/mutant/expression/namespace/recursive_spec.rb @@ -1,13 +1,11 @@ RSpec.describe Mutant::Expression::Namespace::Recursive do - let(:object) { parse_expression(input) } let(:input) { 'TestApp::Literal*' } - let(:env) { Fixtures::TEST_ENV } describe '#matcher' do - subject { object.matcher(env) } + subject { object.matcher } - it { should eql(Mutant::Matcher::Namespace.new(env, object)) } + it { should eql(Mutant::Matcher::Namespace.new(object)) } end describe '#syntax' do diff --git a/spec/unit/mutant/matcher/chain_spec.rb b/spec/unit/mutant/matcher/chain_spec.rb index f5273a189..ff3408d00 100644 --- a/spec/unit/mutant/matcher/chain_spec.rb +++ b/spec/unit/mutant/matcher/chain_spec.rb @@ -1,32 +1,24 @@ -RSpec.describe Mutant::Matcher::Chain do - - let(:object) { described_class.new(matchers) } - - describe '#each' do - let(:yields) { [] } - subject { object.each { |entry| yields << entry } } - - let(:matchers) { [matcher_a, matcher_b] } - - let(:matcher_a) { [subject_a] } - let(:matcher_b) { [subject_b] } - - 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 } - - it { should be_instance_of(to_enum.class) } - - it 'yields the expected values' do - expect(subject.to_a).to eql(object.to_a) - end - end +RSpec.describe Mutant::Matcher::Chain, '#call' do + subject { object.call(env) } + + let(:object) { described_class.new([matcher_a, matcher_b]) } + let(:env) { instance_double(Mutant::Env) } + let(:matcher_a) { instance_double(Mutant::Matcher) } + let(:matcher_b) { instance_double(Mutant::Matcher) } + let(:subject_a) { instance_double(Mutant::Subject) } + let(:subject_b) { instance_double(Mutant::Subject) } + + before do + expect(matcher_a).to receive(:call) + .with(env) + .and_return([subject_a]) + + expect(matcher_b).to receive(:call) + .with(env) + .and_return([subject_b]) + end - it 'should yield subjects' do - expect { subject }.to change { yields }.from([]).to([subject_a, subject_b]) - end + it 'returns concatenated matches' do + should eql([subject_a, subject_b]) end end diff --git a/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb b/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb index 264f8243d..c6c81bef6 100644 --- a/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb +++ b/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb @@ -1,21 +1,24 @@ -RSpec.describe Mutant::Matcher::Compiler::SubjectPrefix do - let(:object) { described_class.new(parse_expression('Foo*')) } +RSpec.describe Mutant::Matcher::Compiler::SubjectPrefix, '#call' do + let(:object) { described_class.new(parse_expression('Foo*')) } - let(:_subject) { double('Subject', expression: parse_expression(subject_expression)) } + let(:_subject) do + instance_double( + Mutant::Subject, + expression: parse_expression(subject_expression) + ) + end - describe '#call' do - subject { object.call(_subject) } + subject { object.call(_subject) } - context 'when subject expression is prefixed by expression' do - let(:subject_expression) { 'Foo::Bar' } + context 'when subject expression is prefixed by expression' do + let(:subject_expression) { 'Foo::Bar' } - it { should be(true) } - end + it { should be(true) } + end - context 'when subject expression is NOT prefixed by expression' do - let(:subject_expression) { 'Bar' } + context 'when subject expression is NOT prefixed by expression' do + let(:subject_expression) { 'Bar' } - it { should be(false) } - end + it { should be(false) } end end diff --git a/spec/unit/mutant/matcher/compiler_spec.rb b/spec/unit/mutant/matcher/compiler_spec.rb index d6898d30c..338242336 100644 --- a/spec/unit/mutant/matcher/compiler_spec.rb +++ b/spec/unit/mutant/matcher/compiler_spec.rb @@ -1,89 +1,78 @@ -RSpec.describe Mutant::Matcher::Compiler do - let(:object) { described_class } - - let(:env) { Fixtures::TEST_ENV } - - 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) } +RSpec.describe Mutant::Matcher::Compiler, '#call' do + let(:object) { described_class } + let(:env) { Fixtures::TEST_ENV } + let(:matcher_config) { Mutant::Matcher::Config::DEFAULT } + let(:expression_a) { parse_expression('Foo*') } + let(:expression_b) { parse_expression('Bar*') } + let(:matcher_a) { expression_a.matcher } + let(:matcher_b) { expression_b.matcher } let(:expected_matcher) do - Mutant::Matcher::Filter.new(expected_positive_matcher, expected_predicate) + Mutant::Matcher::Filter.new( + expected_positive_matcher, + expected_predicate + ) end let(:expected_predicate) do Morpher.compile(s(:and, s(:negate, s(:or)), s(:and))) end - describe '.call' do - subject { object.call(env, matcher_config.with(attributes)) } + subject { object.call(matcher_config.with(attributes)) } - let(:matcher_config) { Mutant::Matcher::Config::DEFAULT } + context 'on empty config' do + let(:attributes) { {} } - context 'on empty config' do - let(:attributes) { {} } + let(:expected_positive_matcher) { Mutant::Matcher::Chain.new([]) } - let(:expected_positive_matcher) { Mutant::Matcher::Chain.new([]) } + it { should eql(expected_matcher) } + end - it { should eql(expected_matcher) } + context 'on config with match expression' do + let(:expected_predicate) do + Morpher::Evaluator::Predicate::Boolean::And.new( + [ + Morpher::Evaluator::Predicate::Negation.new( + Morpher::Evaluator::Predicate::Boolean::Or.new(ignore_expression_predicates) + ), + Morpher::Evaluator::Predicate::Boolean::And.new(subject_filter_predicates) + ] + ) end - context 'on config with match expression' do - let(:expected_predicate) do - Morpher::Evaluator::Predicate::Boolean::And.new( - [ - Morpher::Evaluator::Predicate::Negation.new( - Morpher::Evaluator::Predicate::Boolean::Or.new(ignore_expression_predicates) - ), - Morpher::Evaluator::Predicate::Boolean::And.new(subject_filter_predicates) - ] - ) - end + let(:expected_positive_matcher) { Mutant::Matcher::Chain.new([matcher_a]) } + let(:attributes) { { match_expressions: [expression_a] } } + let(:ignore_expression_predicates) { [] } + let(:subject_filter_predicates) { [] } - let(:expected_positive_matcher) { Mutant::Matcher::Chain.new([matcher_a]) } - let(:attributes) { { match_expressions: [expression_a] } } - let(:ignore_expression_predicates) { [] } - let(:subject_filter_predicates) { [] } + context 'and no other constraints' do + it { should eql(expected_matcher) } + end - context 'and no other constraints' do - it { should eql(expected_matcher) } + context 'and ignore expressions' do + let(:attributes) do + super().merge(ignore_expressions: [expression_b]) end - context 'and ignore expressions' do - let(:attributes) do - super().merge(ignore_expressions: [expression_b]) - end - - let(:ignore_expression_predicates) do - [Mutant::Matcher::Compiler::SubjectPrefix.new(expression_b)] - end - - it { should eql(expected_matcher) } + let(:ignore_expression_predicates) do + [Mutant::Matcher::Compiler::SubjectPrefix.new(expression_b)] end - context 'and subject filters' do - let(:filter) { double('filter') } - - let(:attributes) do - super().merge(subject_filters: [filter]) - end + it { should eql(expected_matcher) } + end - let(:subject_filter_predicates) do - [filter] - end + context 'and subject filters' do + let(:filter) { double('filter') } - it { should eql(expected_matcher) } + let(:attributes) do + super().merge(subject_filters: [filter]) end - end - context 'on config with multiple match expressions' do - let(:attributes) do - { match_expressions: [expression_a, expression_b] } + let(:subject_filter_predicates) do + [filter] end - let(:expected_positive_matcher) { Mutant::Matcher::Chain.new([matcher_a, matcher_b]) } + it { should eql(expected_matcher) } end end end diff --git a/spec/unit/mutant/matcher/filter_spec.rb b/spec/unit/mutant/matcher/filter_spec.rb index aa8a46e5b..066729bd4 100644 --- a/spec/unit/mutant/matcher/filter_spec.rb +++ b/spec/unit/mutant/matcher/filter_spec.rb @@ -1,36 +1,20 @@ -RSpec.describe Mutant::Matcher::Filter do - 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(:predicate) { ->(node) { node.eql?(subject_a) } } - - # it_should_behave_like 'an #each method' - context 'with no block' do - subject { object.each } - - it { should be_instance_of(to_enum.class) } +RSpec.describe Mutant::Matcher::Filter, '#call' do + subject { object.call(env) } - it 'yields the expected values' do - expect(subject.to_a).to eql(object.to_a) - end - end - - it 'should yield subjects' do - expect { subject }.to change { yields }.from([]).to([subject_a]) - end + let(:object) { described_class.new(matcher, predicate) } + let(:matcher) { instance_double(Mutant::Matcher) } + let(:subject_a) { instance_double(Mutant::Subject) } + let(:subject_b) { instance_double(Mutant::Subject) } + let(:env) { instance_double(Mutant::Env) } + let(:predicate) { ->(node) { node.eql?(subject_a) } } + + before do + expect(matcher).to receive(:call) + .with(env) + .and_return([subject_a, subject_b]) end - describe '.build' do - subject { described_class.build(matcher, &predicate) } - - let(:predicate) { ->(_subject) { false } } - - its(:to_a) { should eql([]) } + it 'returns subjects after filtering' do + should eql([subject_a]) end end diff --git a/spec/unit/mutant/matcher/method/instance_spec.rb b/spec/unit/mutant/matcher/method/instance_spec.rb index 991c5f2b3..3b819b45b 100644 --- a/spec/unit/mutant/matcher/method/instance_spec.rb +++ b/spec/unit/mutant/matcher/method/instance_spec.rb @@ -1,157 +1,103 @@ -RSpec.describe Mutant::Matcher::Method::Instance do - - let(:env) { Fixtures::TEST_ENV } - let(:reporter) { Fixtures::TEST_CONFIG.reporter } +RSpec.describe Mutant::Matcher::Method::Instance, '#call' do + subject { object.call(env) } + + let(:env) { Fixtures::TEST_ENV } + let(:method_name) { :foo } + let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } + let(:object) { described_class.new(scope, method) } + let(:method) { scope.instance_method(method_name) } + let(:namespace) { self.class } + let(:type) { :def } + let(:method_arity) { 0 } + let(:base) { TestApp::InstanceMethodTests } + + def name + node.children.fetch(0) + end - describe '#each' do - subject { object.each(&yields.method(:<<)) } + def arguments + node.children.fetch(1) + end - let(:object) { described_class.build(env, scope, method) } - let(:method) { scope.instance_method(method_name) } - let(:yields) { [] } - let(:namespace) { self.class } - let(:type) { :def } - let(:method_name) { :foo } - let(:method_arity) { 0 } - let(:base) { TestApp::InstanceMethodTests } + context 'when method is defined inside eval' do + let(:scope) { base::WithMemoizer } + let(:method) { scope.instance_method(:boz) } + let(:method_name) { :boz } + let(:expected_warnings) { [] } - def name - node.children[0] - end + include_examples 'skipped candidate' + end - def arguments - node.children[1] - end + context 'when method is defined without source location' do + let(:scope) { Module } + let(:method) { scope.instance_method(:object_id) } + let(:expected_warnings) { [] } - context 'when method is defined without source location' do - let(:scope) { Module } - let(:method) { scope.instance_method(:object_id) } - - it 'does not emit matcher' do - subject - expect(yields.length).to be(0) - end - - it 'does warn' do - subject - expect(reporter.warn_calls.last).to( - eql("#{method.inspect} does not have valid source location unable to emit subject") - ) - end - end + include_examples 'skipped candidate' + end - context 'when method is defined once' do - let(:scope) { base::DefinedOnce } - let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } - let(:method_line) { 10 } + context 'in module eval' do + let(:scope) { base::InModuleEval } - it_should_behave_like 'a method matcher' + let(:expected_warnings) do + [ + "#{method} is dynamically defined in a closure, unable to emit subject" + ] end - context 'when method is defined once with a memoizer' do - let(:scope) { base::WithMemoizer } - let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } - let(:method_line) { 15 } + include_examples 'skipped candidate' + end + + context 'in class eval' do + let(:scope) { base::InClassEval } - it_should_behave_like 'a method matcher' + let(:expected_warnings) do + [ + "#{method} is dynamically defined in a closure, unable to emit subject" + ] end - context 'when method is defined multiple times' do - context 'on different lines' do - let(:scope) { base::DefinedMultipleTimes::DifferentLines } - let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } - let(:method_line) { 24 } - let(:method_arity) { 1 } - - it_should_behave_like 'a method matcher' - end - - context 'on the same line' do - let(:scope) { base::DefinedMultipleTimes::SameLineSameScope } - let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } - let(:method_line) { 29 } - let(:method_arity) { 1 } - - it_should_behave_like 'a method matcher' - end - - context 'on the same line with different scope' do - let(:scope) { base::DefinedMultipleTimes::SameLineDifferentScope } - let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } - let(:method_line) { 33 } - let(:method_arity) { 1 } - - it_should_behave_like 'a method matcher' - end - - context 'in module eval' do - let(:scope) { base::InModuleEval } - - it 'does not emit matcher' do - subject - expect(yields.length).to be(0) - end - - it 'does warn' do - subject - expect(reporter.warn_calls.last).to( - eql("#{method.inspect} is defined from a 3rd party lib unable to emit subject") - ) - end - end - - context 'in class eval' do - let(:scope) { base::InClassEval } - - it 'does not emit matcher' do - subject - expect(yields.length).to be(0) - end - - it 'does warn' do - subject - expect(reporter.warn_calls.last).to( - eql("#{method.inspect} is defined from a 3rd party lib unable to emit subject") - ) - end - end - end + include_examples 'skipped candidate' end - describe '.build' do - let(:object) { described_class } + context 'when method is defined once' do + let(:method_name) { :bar } + let(:scope) { base::WithMemoizer } + let(:method_line) { 13 } - subject { object.build(env, scope, method) } + it_should_behave_like 'a method matcher' + end - let(:scope) do - Class.new do - include Adamantium + context 'when method is defined once with a memoizer' do + let(:scope) { base::WithMemoizer } + let(:method_line) { 15 } - def foo - end - memoize :foo + it_should_behave_like 'a method matcher' + end - def bar - end - end - end + context 'when method is defined multiple times' do + context 'on different lines' do + let(:scope) { base::DefinedMultipleTimes::DifferentLines } + let(:method_line) { 24 } + let(:method_arity) { 1 } - let(:method) do - scope.instance_method(method_name) + it_should_behave_like 'a method matcher' end - context 'with adamantium infected scope' do - context 'with unmemoized method' do - let(:method_name) { :bar } + context 'on the same line' do + let(:scope) { base::DefinedMultipleTimes::SameLineSameScope } + let(:method_line) { 29 } + let(:method_arity) { 1 } - it { should eql(described_class.new(env, scope, method)) } - end + it_should_behave_like 'a method matcher' + end - context 'with memoized method' do - let(:method_name) { :foo } + context 'on the same line with different scope' do + let(:scope) { base::DefinedMultipleTimes::SameLineDifferentScope } + let(:method_line) { 33 } + let(:method_arity) { 1 } - it { should eql(described_class::Memoized.new(env, scope, method)) } - end + it_should_behave_like 'a method matcher' end end end diff --git a/spec/unit/mutant/matcher/method/singleton_spec.rb b/spec/unit/mutant/matcher/method/singleton_spec.rb index d6c8275ee..671ba538c 100644 --- a/spec/unit/mutant/matcher/method/singleton_spec.rb +++ b/spec/unit/mutant/matcher/method/singleton_spec.rb @@ -1,62 +1,54 @@ -RSpec.describe Mutant::Matcher::Method::Singleton, '#each' do - subject { object.each(&yields.method(:<<)) } - - let(:object) { described_class.new(env, scope, method) } - let(:method) { scope.method(method_name) } - let(:env) { Fixtures::TEST_ENV } - let(:yields) { [] } - let(:type) { :defs } - let(:method_name) { :foo } - let(:method_arity) { 0 } - let(:base) { TestApp::SingletonMethodTests } +RSpec.describe Mutant::Matcher::Method::Singleton, '#call' do + subject { object.call(env) } + + let(:object) { described_class.new(scope, method) } + let(:method) { scope.method(method_name) } + let(:env) { Fixtures::TEST_ENV } + let(:type) { :defs } + let(:method_name) { :foo } + let(:method_arity) { 0 } + let(:base) { TestApp::SingletonMethodTests } + let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } def name - node.children[1] + node.children.fetch(1) end def arguments - node.children[2] + node.children.fetch(2) end context 'on singleton methods' do - context 'when also defined on lvar' do - let(:scope) { base::AlsoDefinedOnLvar } - let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } - let(:method_line) { 66 } - - it_should_behave_like 'a method matcher' - - it 'warns about definition on non const/self' do - subject - expect(env.config.reporter.warn_calls).to( - include('Can only match :defs on :self or :const got :lvar unable to match') - ) + let(:scope) { base::DefinedOnLvar } + let(:method_line) { 66 } + let(:expected_warnings) do + [ + 'Can only match :defs on :self or :const got :lvar unable to match' + ] end + + include_examples 'skipped candidate' end context 'when defined on self' do - let(:scope) { base::DefinedOnSelf } - let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } - let(:method_line) { 61 } + let(:scope) { base::DefinedOnSelf } + let(:method_line) { 61 } it_should_behave_like 'a method matcher' end context 'when defined on constant' do - context 'inside namespace' do - let(:scope) { base::DefinedOnConstant::InsideNamespace } - let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } - let(:method_line) { 71 } + let(:scope) { base::DefinedOnConstant::InsideNamespace } + let(:method_line) { 71 } it_should_behave_like 'a method matcher' end context 'outside namespace' do - let(:scope) { base::DefinedOnConstant::OutsideNamespace } - let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } - let(:method_line) { 78 } + let(:scope) { base::DefinedOnConstant::OutsideNamespace } + let(:method_line) { 78 } it_should_behave_like 'a method matcher' end @@ -65,12 +57,18 @@ def arguments context 'when defined multiple times in the same line' do context 'with method on different scope' do let(:scope) { base::DefinedMultipleTimes::SameLine::DifferentScope } - let(:source_path) { MutantSpec::ROOT.join('test_app/lib/test_app.rb') } let(:method_line) { 97 } let(:method_arity) { 1 } it_should_behave_like 'a method matcher' end + + context 'with different name' do + let(:scope) { base::DefinedMultipleTimes::SameLine::DifferentName } + let(:method_line) { 101 } + + it_should_behave_like 'a method matcher' + end end end end diff --git a/spec/unit/mutant/matcher/methods/instance_spec.rb b/spec/unit/mutant/matcher/methods/instance_spec.rb index 5ac55bdc5..a0e047e81 100644 --- a/spec/unit/mutant/matcher/methods/instance_spec.rb +++ b/spec/unit/mutant/matcher/methods/instance_spec.rb @@ -1,10 +1,6 @@ -RSpec.describe Mutant::Matcher::Methods::Instance, '#each' do - let(:object) { described_class.new(env, class_under_test) } - let(:env) { Fixtures::TEST_ENV } - - subject { object.each { |matcher| yields << matcher } } - - let(:yields) { [] } +RSpec.describe Mutant::Matcher::Methods::Instance, '#call' do + let(:object) { described_class.new(class_under_test) } + let(:env) { Fixtures::TEST_ENV } let(:class_under_test) do parent = Module.new do @@ -37,26 +33,27 @@ def method_c end end - let(:subject_a) { double('Subject A') } - let(:subject_b) { double('Subject B') } - let(:subject_c) { double('Subject C') } - - let(:subjects) { [subject_a, subject_b, subject_c] } + let(:subject_a) { instance_double(Mutant::Subject) } + let(:subject_b) { instance_double(Mutant::Subject) } + let(:subject_c) { instance_double(Mutant::Subject) } + let(:subjects) { [subject_a, subject_b, subject_c] } before do - matcher = Mutant::Matcher::Method::Instance - allow(matcher).to receive(:new) - .with(env, class_under_test, class_under_test.instance_method(:method_a)).and_return([subject_a]) - allow(matcher).to receive(:new) - .with(env, class_under_test, class_under_test.instance_method(:method_b)).and_return([subject_b]) - allow(matcher).to receive(:new) - .with(env, class_under_test, class_under_test.instance_method(:method_c)).and_return([subject_c]) + { + method_a: subject_a, + method_b: subject_b, + method_c: subject_c + }.each do |method, subject| + matcher = instance_double(Mutant::Matcher) + expect(matcher).to receive(:call).with(env).and_return([subject]) + + expect(Mutant::Matcher::Method::Instance).to receive(:new) + .with(class_under_test, class_under_test.instance_method(method)) + .and_return(matcher) + end end - it 'should yield expected subjects' do - subject - expect(yields).to eql(subjects) + it 'returns expected subjects' do + expect(object.call(env)).to eql(subjects) end - - it_should_behave_like 'an #each method' end diff --git a/spec/unit/mutant/matcher/methods/singleton_spec.rb b/spec/unit/mutant/matcher/methods/singleton_spec.rb index a946bff81..7f8970df7 100644 --- a/spec/unit/mutant/matcher/methods/singleton_spec.rb +++ b/spec/unit/mutant/matcher/methods/singleton_spec.rb @@ -1,10 +1,6 @@ -RSpec.describe Mutant::Matcher::Methods::Singleton, '#each' do - let(:object) { described_class.new(env, class_under_test) } - let(:env) { Fixtures::TEST_ENV } - - subject { object.each { |matcher| yields << matcher } } - - let(:yields) { [] } +RSpec.describe Mutant::Matcher::Methods::Singleton, '#call' do + let(:object) { described_class.new(class_under_test) } + let(:env) { Fixtures::TEST_ENV } let(:class_under_test) do parent = Module.new do @@ -32,26 +28,27 @@ def self.method_c end end - let(:subject_a) { double('Subject A') } - let(:subject_b) { double('Subject B') } - let(:subject_c) { double('Subject C') } + let(:subject_a) { instance_double(Mutant::Subject, 'A') } + let(:subject_b) { instance_double(Mutant::Subject, 'B') } + let(:subject_c) { instance_double(Mutant::Subject, 'C') } let(:subjects) { [subject_a, subject_b, subject_c] } before do matcher = Mutant::Matcher::Method::Singleton - allow(matcher).to receive(:new) - .with(env, class_under_test, class_under_test.method(:method_a)).and_return([subject_a]) - allow(matcher).to receive(:new) - .with(env, class_under_test, class_under_test.method(:method_b)).and_return([subject_b]) - allow(matcher).to receive(:new) - .with(env, class_under_test, class_under_test.method(:method_c)).and_return([subject_c]) - end - it 'should yield expected subjects' do - subject - expect(yields).to eql(subjects) + { + method_a: subject_a, + method_b: subject_b, + method_c: subject_c + }.each do |method, subject| + allow(matcher).to receive(:new) + .with(class_under_test, class_under_test.method(method)) + .and_return(Mutant::Matcher::Static.new([subject])) + end end - it_should_behave_like 'an #each method' + it 'returns expected subjects' do + expect(object.call(env)).to eql(subjects) + end end diff --git a/spec/unit/mutant/matcher/namespace_spec.rb b/spec/unit/mutant/matcher/namespace_spec.rb index bdb51661f..a892e176b 100644 --- a/spec/unit/mutant/matcher/namespace_spec.rb +++ b/spec/unit/mutant/matcher/namespace_spec.rb @@ -1,44 +1,36 @@ -RSpec.describe Mutant::Matcher::Namespace do - let(:object) { described_class.new(env, parse_expression('TestApp*')) } - let(:yields) { [] } - let(:env) { double('Env') } - - subject { object.each { |item| yields << item } } - - describe '#each' do - - let(:singleton_a) { double('SingletonA', name: 'TestApp::Literal') } - let(:singleton_b) { double('SingletonB', name: 'TestApp::Foo') } - let(:singleton_c) { double('SingletonC', name: 'TestAppOther') } - let(:subject_a) { double('SubjectA') } - let(:subject_b) { double('SubjectB') } - - before do - allow(Mutant::Matcher::Methods::Singleton).to receive(:new).with(env, singleton_b).and_return([subject_b]) - allow(Mutant::Matcher::Methods::Instance).to receive(:new).with(env, singleton_b).and_return([]) - - allow(Mutant::Matcher::Methods::Singleton).to receive(:new).with(env, singleton_a).and_return([subject_a]) - allow(Mutant::Matcher::Methods::Instance).to receive(:new).with(env, singleton_a).and_return([]) - - allow(env).to receive(:matchable_scopes).and_return( - [singleton_a, singleton_b, singleton_c].map do |scope| - Mutant::Matcher::Scope.new(env, scope, parse_expression(scope.name)) - end - ) +RSpec.describe Mutant::Matcher::Namespace, '#call' do + let(:object) { described_class.new(parse_expression('TestApp*')) } + let(:env) { instance_double(Mutant::Env) } + let(:raw_scope_a) { double('SingletonA', name: 'TestApp::Literal') } + + let(:raw_scope_b) { double('SingletonB', name: 'TestApp::Foo') } + let(:raw_scope_c) { double('SingletonC', name: 'TestAppOther') } + let(:subject_a) { double('SubjectA') } + let(:subject_b) { double('SubjectB') } + + before do + [ + [Mutant::Matcher::Methods::Singleton, raw_scope_b, [subject_b]], + [Mutant::Matcher::Methods::Instance, raw_scope_b, []], + [Mutant::Matcher::Methods::Singleton, raw_scope_a, [subject_a]], + [Mutant::Matcher::Methods::Instance, raw_scope_a, []] + ].each do |klass, scope, subjects| + matcher = instance_double(Mutant::Matcher) + expect(matcher).to receive(:call).with(env).and_return(subjects) + + expect(klass).to receive(:new) + .with(scope) + .and_return(matcher) end - context 'with no block' do - subject { object.each } - - it { should be_instance_of(to_enum.class) } - - it 'yields the expected values' do - expect(subject.to_a).to eql(object.to_a) + allow(env).to receive(:matchable_scopes).and_return( + [raw_scope_a, raw_scope_b, raw_scope_c].map do |raw_scope| + Mutant::Scope.new(raw_scope, parse_expression(raw_scope.name)) end - end + ) + end - it 'should yield subjects' do - expect { subject }.to change { yields }.from([]).to([subject_a, subject_b]) - end + it 'returns subjects' do + expect(object.call(env)).to eql([subject_a, subject_b]) end end diff --git a/spec/unit/mutant/matcher/null_spec.rb b/spec/unit/mutant/matcher/null_spec.rb index 48a8dfa54..accda2d36 100644 --- a/spec/unit/mutant/matcher/null_spec.rb +++ b/spec/unit/mutant/matcher/null_spec.rb @@ -1,24 +1,9 @@ -RSpec.describe Mutant::Matcher::Null do +RSpec.describe Mutant::Matcher::Null, '#call' do let(:object) { described_class.new } + let(:env) { instance_double(Mutant::Env) } + subject { object.call(env) } - describe '#each' do - let(:yields) { [] } - - subject { object.each { |entry| yields << entry } } - - # it_should_behave_like 'an #each method' - context 'with no block' do - subject { object.each } - - it { should be_instance_of(to_enum.class) } - - it 'yields the expected values' do - expect(subject.to_a).to eql(object.to_a) - end - end - - it 'should yield subjects' do - expect { subject }.not_to change { yields }.from([]) - end + it 'returns no subjects' do + should eql([]) end end diff --git a/spec/unit/mutant/matcher/scope_spec.rb b/spec/unit/mutant/matcher/scope_spec.rb new file mode 100644 index 000000000..909c6cca2 --- /dev/null +++ b/spec/unit/mutant/matcher/scope_spec.rb @@ -0,0 +1,33 @@ +RSpec.describe Mutant::Matcher::Scope, '#call' do + let(:scope) { TestApp } + let(:object) { described_class.new(scope) } + let(:env) { instance_double(Mutant::Env) } + let(:matcher_a) { instance_double(Mutant::Matcher) } + let(:matcher_b) { instance_double(Mutant::Matcher) } + let(:subject_a) { instance_double(Mutant::Subject) } + let(:subject_b) { instance_double(Mutant::Subject) } + + subject { object.call(env) } + + before do + expect(Mutant::Matcher::Methods::Singleton).to receive(:new) + .with(scope) + .and_return(matcher_a) + + expect(Mutant::Matcher::Methods::Instance).to receive(:new) + .with(scope) + .and_return(matcher_b) + + expect(matcher_a).to receive(:call) + .with(env) + .and_return([subject_a]) + + expect(matcher_b).to receive(:call) + .with(env) + .and_return([subject_b]) + end + + it 'concatenates subjects from matched singleton and instance methods' do + should eql([subject_a, subject_b]) + end +end diff --git a/spec/unit/mutant/matcher/static_spec.rb b/spec/unit/mutant/matcher/static_spec.rb new file mode 100644 index 000000000..1299c5ddb --- /dev/null +++ b/spec/unit/mutant/matcher/static_spec.rb @@ -0,0 +1,11 @@ +RSpec.describe Mutant::Matcher::Static, '#call' do + let(:object) { described_class.new(subjects) } + let(:env) { instance_double(Mutant::Env) } + let(:subjects) { instance_double(Array) } + + subject { object.call(env) } + + it 'returns its predefined subjects' do + should be(subjects) + end +end diff --git a/test_app/lib/test_app.rb b/test_app/lib/test_app.rb index 0a34285a3..e10179a16 100644 --- a/test_app/lib/test_app.rb +++ b/test_app/lib/test_app.rb @@ -1,18 +1,18 @@ require 'adamantium' original = $VERBOSE -# Namespace for test application -# Silence intentional violations +# Silence intentional violations made to exercise the method matcher edge cases. +# This is NOT representative for could you should write! $VERBOSE = false +# Namespace for test application module TestApp module InstanceMethodTests - module DefinedOnce - def foo; end - end - class WithMemoizer include Adamantium - def foo; end + + def bar; end; def baz; end + eval('def boz; end') + def foo; end; memoize :foo end @@ -61,9 +61,9 @@ module DefinedOnSelf def self.foo; end end - module AlsoDefinedOnLvar - a = Object.new - def a.foo; end; def self.foo; end + module DefinedOnLvar + a = self + def a.foo; end end module DefinedOnConstant @@ -90,11 +90,15 @@ def self.foo(_arg) module SameLine module SameScope - def self.foo; end; def self.foo(_arg); end; + def self.foo; end; def self.foo(_arg); end end module DifferentScope - def self.foo; end; def DifferentScope.foo(_arg); end + def self.foo; end; def DifferentScope.foo(_arg); end; def SingletonMethodTests.foo; end + end + + module DifferentName + def self.foo; end; def self.bar(_arg); end end end end diff --git a/test_app/lib/test_app/literal.rb b/test_app/lib/test_app/literal.rb index 10a366c82..aa5106e71 100644 --- a/test_app/lib/test_app/literal.rb +++ b/test_app/lib/test_app/literal.rb @@ -29,4 +29,7 @@ def float 2.4 end end + + class Empty + end end