mutant 0.8.7 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +5 -0
  3. data/README.md +64 -3
  4. data/config/flay.yml +1 -1
  5. data/lib/mutant.rb +2 -0
  6. data/lib/mutant/cli.rb +1 -1
  7. data/lib/mutant/env/bootstrap.rb +36 -8
  8. data/lib/mutant/expression/method.rb +3 -5
  9. data/lib/mutant/expression/methods.rb +2 -4
  10. data/lib/mutant/expression/namespace.rb +4 -8
  11. data/lib/mutant/matcher.rb +3 -18
  12. data/lib/mutant/matcher/chain.rb +7 -13
  13. data/lib/mutant/matcher/compiler.rb +2 -13
  14. data/lib/mutant/matcher/filter.rb +6 -19
  15. data/lib/mutant/matcher/method.rb +124 -104
  16. data/lib/mutant/matcher/method/instance.rb +40 -34
  17. data/lib/mutant/matcher/method/singleton.rb +80 -61
  18. data/lib/mutant/matcher/methods.rb +19 -29
  19. data/lib/mutant/matcher/namespace.rb +22 -16
  20. data/lib/mutant/matcher/null.rb +4 -7
  21. data/lib/mutant/matcher/scope.rb +23 -13
  22. data/lib/mutant/matcher/static.rb +17 -0
  23. data/lib/mutant/mutation.rb +0 -5
  24. data/lib/mutant/reporter/cli/format.rb +2 -3
  25. data/lib/mutant/reporter/cli/printer/env_progress.rb +37 -11
  26. data/lib/mutant/reporter/cli/printer/status_progressive.rb +1 -1
  27. data/lib/mutant/scope.rb +6 -0
  28. data/lib/mutant/subject/method.rb +0 -7
  29. data/lib/mutant/subject/method/instance.rb +0 -10
  30. data/lib/mutant/subject/method/singleton.rb +0 -10
  31. data/lib/mutant/version.rb +1 -1
  32. data/lib/mutant/zombifier.rb +2 -1
  33. data/mutant-rspec.gemspec +1 -1
  34. data/spec/integration/mutant/rspec_spec.rb +1 -1
  35. data/spec/shared/method_matcher_behavior.rb +21 -14
  36. data/spec/spec_helper.rb +6 -0
  37. data/spec/unit/mutant/env/boostrap_spec.rb +88 -26
  38. data/spec/unit/mutant/env_spec.rb +0 -1
  39. data/spec/unit/mutant/expression/method_spec.rb +3 -3
  40. data/spec/unit/mutant/expression/methods_spec.rb +3 -4
  41. data/spec/unit/mutant/expression/namespace/flat_spec.rb +2 -3
  42. data/spec/unit/mutant/expression/namespace/recursive_spec.rb +2 -4
  43. data/spec/unit/mutant/matcher/chain_spec.rb +21 -29
  44. data/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb +16 -13
  45. data/spec/unit/mutant/matcher/compiler_spec.rb +49 -60
  46. data/spec/unit/mutant/matcher/filter_spec.rb +15 -31
  47. data/spec/unit/mutant/matcher/method/instance_spec.rb +84 -128
  48. data/spec/unit/mutant/matcher/method/singleton_spec.rb +48 -52
  49. data/spec/unit/mutant/matcher/methods/instance_spec.rb +21 -24
  50. data/spec/unit/mutant/matcher/methods/singleton_spec.rb +18 -21
  51. data/spec/unit/mutant/matcher/namespace_spec.rb +30 -38
  52. data/spec/unit/mutant/matcher/null_spec.rb +5 -20
  53. data/spec/unit/mutant/matcher/scope_spec.rb +33 -0
  54. data/spec/unit/mutant/matcher/static_spec.rb +11 -0
  55. data/spec/unit/mutant/mutation_spec.rb +30 -10
  56. data/spec/unit/mutant/reporter/cli/printer/env_progress_spec.rb +6 -0
  57. data/spec/unit/mutant/reporter/cli/printer/env_result_spec.rb +2 -0
  58. data/spec/unit/mutant/reporter/cli/printer/status_spec.rb +10 -0
  59. data/spec/unit/mutant/reporter/cli_spec.rb +4 -0
  60. data/spec/unit/mutant/subject/method/instance_spec.rb +0 -28
  61. data/spec/unit/mutant/subject/method/singleton_spec.rb +0 -28
  62. data/test_app/Gemfile.rspec3.4 +7 -0
  63. data/test_app/lib/test_app.rb +16 -12
  64. data/test_app/lib/test_app/literal.rb +3 -0
  65. metadata +9 -2
@@ -3,62 +3,68 @@ module Mutant
3
3
  class Method
4
4
  # Matcher for instance methods
5
5
  class Instance < self
6
- SUBJECT_CLASS = Subject::Method::Instance
7
6
 
8
7
  # Dispatching builder, detects memoizable case
9
8
  #
10
- # @param [Env::Boostrap] env
11
9
  # @param [Class, Module] scope
12
10
  # @param [UnboundMethod] method
13
11
  #
14
12
  # @return [Matcher::Method::Instance]
15
13
  #
16
14
  # @api private
17
- def self.build(env, scope, target_method)
15
+ def self.new(scope, target_method)
18
16
  name = target_method.name
19
- if scope.ancestors.include?(::Memoizable) && scope.memoized?(name)
20
- return Memoized.new(env, scope, target_method)
21
- end
22
- super
23
- end
24
-
25
- NAME_INDEX = 0
26
-
27
- private
17
+ evaluator =
18
+ if scope.include?(Memoizable) && scope.memoized?(name)
19
+ Evaluator::Memoized
20
+ else
21
+ Evaluator
22
+ end
28
23
 
29
- # Check if node is matched
30
- #
31
- # @param [Parser::AST::Node] node
32
- #
33
- # @return [Boolean]
34
- #
35
- # @api private
36
- def match?(node)
37
- location = node.location || return
38
- expression = location.expression || return
39
-
40
- expression.line.equal?(source_line) &&
41
- node.type.equal?(:def) &&
42
- node.children[NAME_INDEX].equal?(method_name)
24
+ super(scope, target_method, evaluator)
43
25
  end
44
26
 
45
- # Matcher for memoized instance methods
46
- class Memoized < self
47
- SUBJECT_CLASS = Subject::Method::Instance::Memoized
27
+ # Instance method specific evaluator
28
+ class Evaluator < Evaluator
29
+ SUBJECT_CLASS = Subject::Method::Instance
30
+ NAME_INDEX = 0
48
31
 
49
32
  private
50
33
 
51
- # Source location
34
+ # Check if node is matched
35
+ #
36
+ # @param [Parser::AST::Node] node
52
37
  #
53
- # @return [Array{String,Fixnum}]
38
+ # @return [Boolean]
54
39
  #
55
40
  # @api private
56
- def source_location
57
- scope.unmemoized_instance_method(method_name).source_location
41
+ def match?(node)
42
+ n_def?(node) &&
43
+ node.location.line.equal?(source_line) &&
44
+ node.children.fetch(NAME_INDEX).equal?(method_name)
58
45
  end
59
46
 
60
- end # Memoized
47
+ # Evaluator specialized for memoized instance mthods
48
+ class Memoized < self
49
+ SUBJECT_CLASS = Subject::Method::Instance::Memoized
50
+
51
+ private
52
+
53
+ # Source location
54
+ #
55
+ # @return [Array{String,Fixnum}]
56
+ #
57
+ # @api private
58
+ def source_location
59
+ scope
60
+ .unmemoized_instance_method(method_name)
61
+ .source_location
62
+ end
63
+
64
+ end # Memoized
65
+ end # Evaluator
61
66
 
67
+ private_constant(*constants(false))
62
68
  end # Instance
63
69
  end # Method
64
70
  end # Matcher
@@ -3,79 +3,98 @@ module Mutant
3
3
  class Method
4
4
  # Matcher for singleton methods
5
5
  class Singleton < self
6
- SUBJECT_CLASS = Subject::Method::Singleton
7
- RECEIVER_INDEX = 0
8
- NAME_INDEX = 1
9
6
 
10
- private
11
-
12
- # Test for node match
7
+ # New singleton method matcher
13
8
  #
14
- # @param [Parser::AST::Node] node
9
+ # @param [Class, Module] scope
10
+ # @param [Symbol] method_name
15
11
  #
16
- # @return [Boolean]
12
+ # @return [Matcher::Method::Singleton]
17
13
  #
18
14
  # @api private
19
- def match?(node)
20
- line?(node) && name?(node) && receiver?(node)
15
+ def self.new(scope, method_name)
16
+ super(scope, method_name, Evaluator)
21
17
  end
22
18
 
23
- # Test for line match
24
- #
25
- # @param [Parser::AST::Node] node
26
- #
27
- # @return [Boolean]
28
- #
29
- # @api private
30
- def line?(node)
31
- expression = node.location.expression
32
- return false unless expression
33
- expression.line.equal?(source_line)
34
- end
19
+ # Singleton method evaluator
20
+ class Evaluator < Evaluator
21
+ SUBJECT_CLASS = Subject::Method::Singleton
22
+ RECEIVER_INDEX = 0
23
+ NAME_INDEX = 1
35
24
 
36
- # Test for name match
37
- #
38
- # @param [Parser::AST::Node] node
39
- #
40
- # @return [Boolean]
41
- #
42
- # @api private
43
- def name?(node)
44
- node.children[NAME_INDEX].equal?(method_name)
45
- end
25
+ private
46
26
 
47
- # Test for receiver match
48
- #
49
- # @param [Parser::AST::Node] node
50
- #
51
- # @return [Boolean]
52
- #
53
- # @api private
54
- def receiver?(node)
55
- receiver = node.children[RECEIVER_INDEX]
56
- case receiver.type
57
- when :self
58
- true
59
- when :const
60
- receiver_name?(receiver)
61
- else
62
- env.warn(format('Can only match :defs on :self or :const got %s unable to match', receiver.type.inspect))
63
- false
27
+ # Test for node match
28
+ #
29
+ # @param [Parser::AST::Node] node
30
+ #
31
+ # @return [Boolean]
32
+ #
33
+ # @api private
34
+ def match?(node)
35
+ n_defs?(node) && line?(node) && name?(node) && receiver?(node)
64
36
  end
65
- end
66
37
 
67
- # Test if receiver name matches context
68
- #
69
- # @param [Parser::AST::Node] node
70
- #
71
- # @return [Boolean]
72
- #
73
- # @api private
74
- def receiver_name?(node)
75
- name = node.children[NAME_INDEX]
76
- name.to_s.eql?(context.unqualified_name)
77
- end
38
+ # Test for line match
39
+ #
40
+ # @param [Parser::AST::Node] node
41
+ #
42
+ # @return [Boolean]
43
+ #
44
+ # @api private
45
+ def line?(node)
46
+ node
47
+ .location
48
+ .line
49
+ .equal?(source_line)
50
+ end
51
+
52
+ # Test for name match
53
+ #
54
+ # @param [Parser::AST::Node] node
55
+ #
56
+ # @return [Boolean]
57
+ #
58
+ # @api private
59
+ def name?(node)
60
+ node.children.fetch(NAME_INDEX).equal?(method_name)
61
+ end
62
+
63
+ # Test for receiver match
64
+ #
65
+ # @param [Parser::AST::Node] node
66
+ #
67
+ # @return [Boolean]
68
+ #
69
+ # @api private
70
+ def receiver?(node)
71
+ receiver = node.children.fetch(RECEIVER_INDEX)
72
+ case receiver.type
73
+ when :self
74
+ true
75
+ when :const
76
+ receiver_name?(receiver)
77
+ else
78
+ env.warn(format('Can only match :defs on :self or :const got %s unable to match', receiver.type.inspect))
79
+ nil
80
+ end
81
+ end
82
+
83
+ # Test if receiver name matches context
84
+ #
85
+ # @param [Parser::AST::Node] node
86
+ #
87
+ # @return [Boolean]
88
+ #
89
+ # @api private
90
+ def receiver_name?(node)
91
+ name = node.children.fetch(NAME_INDEX)
92
+ name.to_s.eql?(context.unqualified_name)
93
+ end
94
+
95
+ end # Evaluator
78
96
 
97
+ private_constant(*constants(false))
79
98
  end # Singleton
80
99
  end # Method
81
100
  end # Matcher
@@ -2,23 +2,27 @@ module Mutant
2
2
  class Matcher
3
3
  # Abstract base class for matcher that returns method subjects from scope
4
4
  class Methods < self
5
- include AbstractType, Concord::Public.new(:env, :scope)
5
+ include AbstractType, Concord.new(:scope)
6
+
7
+ CANDIDATE_NAMES = IceNine.deep_freeze(%i[
8
+ public_instance_methods
9
+ private_instance_methods
10
+ protected_instance_methods
11
+ ])
12
+
13
+ private_constant(*constants(false))
6
14
 
7
15
  # Enumerate subjects
8
16
  #
9
- # @return [self]
10
- # if block given
17
+ # @param [Env] env
11
18
  #
12
- # @return [Enumerator<Subject>]
13
- # otherwise
19
+ # @return [Enumerable<Subject>]
14
20
  #
15
21
  # @api private
16
- def each(&block)
17
- return to_enum unless block_given?
18
-
19
- subjects.each(&block)
20
-
21
- self
22
+ def call(env)
23
+ Chain.new(
24
+ methods.map { |method| matcher.new(scope, method) }
25
+ ).call(env)
22
26
  end
23
27
 
24
28
  private
@@ -45,29 +49,16 @@ module Mutant
45
49
  end
46
50
  memoize :methods
47
51
 
48
- # Subjects detected on scope
49
- #
50
- # @return [Array<Subject>]
51
- #
52
- # @api private
53
- def subjects
54
- methods.map do |method|
55
- matcher.build(env, scope, method)
56
- end.flat_map(&:to_a)
57
- end
58
- memoize :subjects
59
-
60
52
  # Candidate method names on target scope
61
53
  #
62
54
  # @return [Enumerable<Symbol>]
63
55
  #
64
56
  # @api private
65
57
  def candidate_names
66
- (
67
- candidate_scope.public_instance_methods(false) +
68
- candidate_scope.private_instance_methods(false) +
69
- candidate_scope.protected_instance_methods(false)
70
- ).sort
58
+ CANDIDATE_NAMES
59
+ .map(&candidate_scope.method(:public_send))
60
+ .reduce(:+)
61
+ .sort
71
62
  end
72
63
 
73
64
  # Candidate scope
@@ -133,7 +124,6 @@ module Mutant
133
124
  end
134
125
 
135
126
  end # Instance
136
-
137
127
  end # Methods
138
128
  end # Matcher
139
129
  end # Mutant
@@ -1,34 +1,40 @@
1
1
  module Mutant
2
2
  class Matcher
3
-
4
3
  # Matcher for specific namespace
5
4
  class Namespace < self
6
- include Concord::Public.new(:env, :expression)
5
+ include Concord::Public.new(:expression)
7
6
 
8
7
  # Enumerate subjects
9
8
  #
10
- # @return [self]
11
- # if block given
9
+ # @param [Env] env
12
10
  #
13
- # @return [Enumerator<Subject>]
14
- # otherwise
11
+ # @return [Enumerable<Subject>]
15
12
  #
16
13
  # @api private
17
- def each(&block)
18
- return to_enum unless block_given?
19
-
20
- env.matchable_scopes.select do |scope|
21
- scope.each(&block) if match?(scope)
22
- end
23
-
24
- self
14
+ def call(env)
15
+ Chain.new(
16
+ matched_scopes(env).map { |scope| Scope.new(scope.raw) }
17
+ ).call(env)
25
18
  end
26
19
 
27
20
  private
28
21
 
29
- # Test scope if name matches expression
22
+ # The matched scopes
23
+ #
24
+ # @param [Env] env
25
+ #
26
+ # @return [Enumerable<Scope>]
27
+ #
28
+ # @api private
29
+ def matched_scopes(env)
30
+ env
31
+ .matchable_scopes
32
+ .select(&method(:match?))
33
+ end
34
+
35
+ # Test scope if matches expression
30
36
  #
31
- # @param [Module, Class] scope
37
+ # @param [Scope] scope
32
38
  #
33
39
  # @return [Boolean]
34
40
  #
@@ -6,16 +6,13 @@ module Mutant
6
6
 
7
7
  # Enumerate subjects
8
8
  #
9
- # @return [Enumerator<Subject]
10
- # if no block given
9
+ # @param [Env] env
11
10
  #
12
- # @return [self]
13
- # otherwise
11
+ # @return [Enumerable<Subject>]
14
12
  #
15
13
  # @api private
16
- def each
17
- return to_enum unless block_given?
18
- self
14
+ def call(_env)
15
+ EMPTY_ARRAY
19
16
  end
20
17
 
21
18
  end # Null
@@ -1,31 +1,41 @@
1
1
  module Mutant
2
2
  class Matcher
3
- # Matcher for specific namespace
3
+ # Matcher expanding Mutant::Scope objects into method matches
4
+ # at singleton or instance level
5
+ #
6
+ # If we *ever* get other subjects than methods, its likely the place
7
+ # to hook in custom matchers. In that case the scope matchers to expand
8
+ # should be passed as arguments to the constructor.
4
9
  class Scope < self
5
- include Concord::Public.new(:env, :scope, :expression)
10
+ include Concord.new(:scope)
6
11
 
7
12
  MATCHERS = [
8
13
  Matcher::Methods::Singleton,
9
14
  Matcher::Methods::Instance
10
15
  ].freeze
11
16
 
12
- # Enumerate subjects
17
+ private_constant(*constants(false))
18
+
19
+ # Matched subjects
13
20
  #
14
- # @return [self]
15
- # if block given
21
+ # @param [Env] env
16
22
  #
17
- # @return [Enumerator<Subject>]
18
- # otherwise
23
+ # @return [Enumerable<Subject>]
19
24
  #
20
25
  # @api private
21
- def each(&block)
22
- return to_enum unless block_given?
26
+ def call(env)
27
+ Chain.new(effective_matchers).call(env)
28
+ end
23
29
 
24
- MATCHERS.each do |matcher|
25
- matcher.new(env, scope).each(&block)
26
- end
30
+ private
27
31
 
28
- self
32
+ # Effective matchers
33
+ #
34
+ # @return [Enumerable<Matcher>]
35
+ #
36
+ # @api private
37
+ def effective_matchers
38
+ MATCHERS.map { |matcher| matcher.new(scope) }
29
39
  end
30
40
 
31
41
  end # Scope