mutant 0.14.2 → 0.15.0

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/mutant/ast/named_children.rb +0 -4
  4. data/lib/mutant/ast/nodes.rb +5 -0
  5. data/lib/mutant/ast/pattern/lexer.rb +0 -1
  6. data/lib/mutant/ast/structure.rb +10 -0
  7. data/lib/mutant/context.rb +2 -6
  8. data/lib/mutant/env.rb +8 -27
  9. data/lib/mutant/hooks.rb +2 -6
  10. data/lib/mutant/integration/null.rb +1 -3
  11. data/lib/mutant/integration.rb +2 -6
  12. data/lib/mutant/isolation/fork.rb +0 -2
  13. data/lib/mutant/matcher/method/instance.rb +2 -2
  14. data/lib/mutant/matcher/method.rb +0 -1
  15. data/lib/mutant/meta/example/verification.rb +0 -1
  16. data/lib/mutant/mutation/operators.rb +141 -44
  17. data/lib/mutant/mutation.rb +6 -18
  18. data/lib/mutant/mutator/node/and_asgn.rb +1 -0
  19. data/lib/mutant/mutator/node/binary.rb +5 -0
  20. data/lib/mutant/mutator/node/block.rb +10 -0
  21. data/lib/mutant/mutator/node/case_match.rb +46 -0
  22. data/lib/mutant/mutator/node/conditional_loop.rb +30 -1
  23. data/lib/mutant/mutator/node/define.rb +26 -0
  24. data/lib/mutant/mutator/node/defined.rb +1 -0
  25. data/lib/mutant/mutator/node/ensure.rb +24 -0
  26. data/lib/mutant/mutator/node/guard.rb +23 -0
  27. data/lib/mutant/mutator/node/in_pattern.rb +25 -0
  28. data/lib/mutant/mutator/node/literal/complex.rb +31 -0
  29. data/lib/mutant/mutator/node/literal/rational.rb +31 -0
  30. data/lib/mutant/mutator/node/literal/string.rb +1 -0
  31. data/lib/mutant/mutator/node/match_alt.rb +26 -0
  32. data/lib/mutant/mutator/node/match_pattern_p.rb +25 -0
  33. data/lib/mutant/mutator/node/op_asgn.rb +21 -1
  34. data/lib/mutant/mutator/node/or_asgn.rb +1 -4
  35. data/lib/mutant/mutator/node/regopt.rb +4 -6
  36. data/lib/mutant/mutator/node/rescue.rb +27 -0
  37. data/lib/mutant/mutator/node/send/binary.rb +45 -0
  38. data/lib/mutant/mutator/node/send.rb +23 -7
  39. data/lib/mutant/mutator/node/super.rb +2 -0
  40. data/lib/mutant/mutator/node/zsuper.rb +17 -0
  41. data/lib/mutant/mutator/node.rb +3 -9
  42. data/lib/mutant/mutator.rb +1 -3
  43. data/lib/mutant/parallel/connection.rb +9 -23
  44. data/lib/mutant/parallel/driver.rb +1 -5
  45. data/lib/mutant/parallel/source.rb +2 -1
  46. data/lib/mutant/parallel/worker.rb +8 -14
  47. data/lib/mutant/registry.rb +2 -1
  48. data/lib/mutant/reporter/cli/format.rb +4 -12
  49. data/lib/mutant/reporter/cli/printer.rb +2 -6
  50. data/lib/mutant/reporter/cli/progress_bar.rb +4 -12
  51. data/lib/mutant/reporter/cli.rb +6 -23
  52. data/lib/mutant/reporter/sequence.rb +1 -3
  53. data/lib/mutant/repository/diff.rb +1 -3
  54. data/lib/mutant/result.rb +16 -48
  55. data/lib/mutant/scope.rb +2 -6
  56. data/lib/mutant/segment.rb +1 -3
  57. data/lib/mutant/subject/method/instance.rb +3 -9
  58. data/lib/mutant/subject/method/metaclass.rb +1 -4
  59. data/lib/mutant/subject/method/singleton.rb +2 -8
  60. data/lib/mutant/subject/method.rb +3 -9
  61. data/lib/mutant/subject.rb +6 -18
  62. data/lib/mutant/timer.rb +2 -6
  63. data/lib/mutant/transform.rb +11 -33
  64. data/lib/mutant/usage.rb +6 -18
  65. data/lib/mutant/variable.rb +2 -6
  66. data/lib/mutant/world.rb +1 -3
  67. data/lib/mutant/zombifier.rb +3 -1
  68. data/lib/mutant.rb +8 -0
  69. metadata +39 -13
@@ -7,7 +7,14 @@ module Mutant
7
7
  # Mutator for while expressions
8
8
  class ConditionalLoop < self
9
9
 
10
- handle(:until, :while)
10
+ INVERSE = {
11
+ until: :while,
12
+ until_post: :while_post,
13
+ while: :until,
14
+ while_post: :until_post
15
+ }.freeze
16
+
17
+ handle(*INVERSE.keys)
11
18
 
12
19
  children :condition, :body
13
20
 
@@ -16,11 +23,33 @@ module Mutant
16
23
  def dispatch
17
24
  emit_singletons
18
25
  emit_condition_mutations
26
+ emit_type_swap
27
+ if post?
28
+ dispatch_post
29
+ else
30
+ dispatch_regular
31
+ end
32
+ end
33
+
34
+ def dispatch_post
35
+ emit_body_mutations { |node| node.type.equal?(:kwbegin) }
36
+ emit_body(s(:kwbegin, N_RAISE))
37
+ end
38
+
39
+ def dispatch_regular
19
40
  emit_body_mutations if body
20
41
  emit_body(nil)
21
42
  emit_body(N_RAISE)
22
43
  end
23
44
 
45
+ def post?
46
+ node.type.end_with?('_post')
47
+ end
48
+
49
+ def emit_type_swap
50
+ emit(s(INVERSE.fetch(node.type), *children))
51
+ end
52
+
24
53
  end # ConditionalLoop
25
54
  end # Node
26
55
  end # Mutator
@@ -6,6 +6,15 @@ module Mutant
6
6
  # Namespace for define mutations
7
7
  class Define < self
8
8
 
9
+ # Mapping from AST node types to their type-aware empty/default values
10
+ TYPE_TO_DEFAULT = {
11
+ array: N_EMPTY_ARRAY,
12
+ hash: N_EMPTY_HASH,
13
+ str: N_EMPTY_STRING,
14
+ int: N_ZERO_INTEGER,
15
+ float: N_ZERO_FLOAT
16
+ }.freeze
17
+
9
18
  private
10
19
 
11
20
  def dispatch
@@ -13,6 +22,7 @@ module Mutant
13
22
  emit_optarg_body_assignments
14
23
  emit_body(N_RAISE)
15
24
  emit_body(N_ZSUPER)
25
+ emit_type_aware_defaults
16
26
 
17
27
  return if !body || ignore?(body)
18
28
 
@@ -21,6 +31,22 @@ module Mutant
21
31
  emit_body_mutations
22
32
  end
23
33
 
34
+ def emit_type_aware_defaults
35
+ return unless body
36
+
37
+ default_node = TYPE_TO_DEFAULT[return_expression.type]
38
+
39
+ emit_body(default_node) if default_node
40
+ end
41
+
42
+ def return_expression
43
+ if n_begin?(body)
44
+ body.children.last
45
+ else
46
+ body
47
+ end
48
+ end
49
+
24
50
  def emit_optarg_body_assignments
25
51
  arguments.children.each do |argument|
26
52
  next unless n_optarg?(argument) && AST::Meta::Optarg.new(node: argument).used?
@@ -14,6 +14,7 @@ module Mutant
14
14
 
15
15
  def dispatch
16
16
  emit(N_NIL)
17
+ emit(N_TRUE)
17
18
  emit_instance_variable_mutation
18
19
  end
19
20
 
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+ # Mutator for ensure nodes
7
+ class Ensure < self
8
+
9
+ handle(:ensure)
10
+
11
+ children :body, :ensure_body
12
+
13
+ private
14
+
15
+ def dispatch
16
+ emit(body) if body
17
+ emit_body_mutations if body
18
+ emit_ensure_body_mutations if ensure_body
19
+ end
20
+
21
+ end # Ensure
22
+ end # Node
23
+ end # Mutator
24
+ end # Mutant
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+ # Mutator for pattern match guard nodes (if_guard, unless_guard)
7
+ class Guard < self
8
+ handle(:if_guard, :unless_guard)
9
+
10
+ children :condition
11
+
12
+ private
13
+
14
+ def dispatch
15
+ emit_condition_mutations
16
+ emit_type(N_TRUE)
17
+ emit_type(N_FALSE)
18
+ end
19
+
20
+ end # Guard
21
+ end # Node
22
+ end # Mutator
23
+ end # Mutant
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+
7
+ # Mutator for in pattern clauses
8
+ class InPattern < self
9
+
10
+ handle(:in_pattern)
11
+
12
+ children :pattern, :guard, :body
13
+
14
+ private
15
+
16
+ def dispatch
17
+ emit_pattern_mutations
18
+ emit_guard_mutations if guard
19
+ emit_body_mutations if body
20
+ end
21
+
22
+ end # InPattern
23
+ end # Node
24
+ end # Mutator
25
+ end # Mutant
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+ class Literal < self
7
+ # Mutator for complex literals
8
+ class Complex < self
9
+
10
+ ONE = 1i
11
+
12
+ handle(:complex)
13
+
14
+ children :value
15
+
16
+ private
17
+
18
+ def dispatch
19
+ emit_singletons
20
+ emit_values
21
+ end
22
+
23
+ def values
24
+ [0i, ONE, value + ONE, value - ONE]
25
+ end
26
+
27
+ end # Complex
28
+ end # Literal
29
+ end # Node
30
+ end # Mutator
31
+ end # Mutant
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+ class Literal < self
7
+ # Mutator for rational literals
8
+ class Rational < self
9
+
10
+ ONE = 1r
11
+
12
+ handle(:rational)
13
+
14
+ children :value
15
+
16
+ private
17
+
18
+ def dispatch
19
+ emit_singletons
20
+ emit_values
21
+ end
22
+
23
+ def values
24
+ [0r, ONE, value + ONE, value - ONE]
25
+ end
26
+
27
+ end # Rational
28
+ end # Literal
29
+ end # Node
30
+ end # Mutator
31
+ end # Mutant
@@ -13,6 +13,7 @@ module Mutant
13
13
 
14
14
  def dispatch
15
15
  emit_singletons
16
+ emit(N_EMPTY_STRING)
16
17
  end
17
18
 
18
19
  end # String
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+
7
+ # Mutator for pattern alternation nodes
8
+ class MatchAlt < self
9
+
10
+ handle(:match_alt)
11
+
12
+ children :left, :right
13
+
14
+ private
15
+
16
+ def dispatch
17
+ emit(left)
18
+ emit(right)
19
+ emit_left_mutations
20
+ emit_right_mutations
21
+ end
22
+
23
+ end # MatchAlt
24
+ end # Node
25
+ end # Mutator
26
+ end # Mutant
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+
7
+ # Mutator for `expr in pattern` predicate nodes
8
+ class MatchPatternP < self
9
+
10
+ handle(:match_pattern_p)
11
+
12
+ children :expression, :pattern
13
+
14
+ private
15
+
16
+ def dispatch
17
+ emit(N_FALSE)
18
+ emit_expression_mutations
19
+ emit_pattern_mutations
20
+ end
21
+
22
+ end # MatchPatternP
23
+ end # Node
24
+ end # Mutator
25
+ end # Mutant
@@ -11,14 +11,34 @@ module Mutant
11
11
 
12
12
  children :left, :operation, :right
13
13
 
14
+ OPERATOR_SWAPS = {
15
+ :+ => %i[-],
16
+ :- => %i[+],
17
+ :* => %i[/],
18
+ :/ => %i[*],
19
+ :% => %i[/],
20
+ :** => %i[*],
21
+ :& => %i[|],
22
+ :| => %i[& ^],
23
+ :^ => %i[& |],
24
+ :<< => %i[>>],
25
+ :>> => %i[<<]
26
+ }.each_value(&:freeze).freeze
27
+
14
28
  private
15
29
 
16
30
  def dispatch
17
31
  left_mutations
18
-
32
+ emit_operator_replacements
19
33
  emit_right_mutations
20
34
  end
21
35
 
36
+ def emit_operator_replacements
37
+ OPERATOR_SWAPS.fetch(operation).each do |replacement|
38
+ emit_operation(replacement)
39
+ end
40
+ end
41
+
22
42
  def left_mutations
23
43
  emit_left_mutations do |node|
24
44
  !n_self?(node)
@@ -16,10 +16,7 @@ module Mutant
16
16
  def dispatch
17
17
  emit_singletons
18
18
  emit_right_mutations
19
- return if n_ivasgn?(left)
20
- emit_left_mutations do |node|
21
- AST::Types::ASSIGNABLE_VARIABLES.include?(node.type)
22
- end
19
+ emit(s(:and_asgn, *children))
23
20
  end
24
21
 
25
22
  end # OrAsgn
@@ -7,18 +7,16 @@ module Mutant
7
7
  # Regular expression options mutation
8
8
  class Regopt < self
9
9
 
10
- MUTATED_FLAGS = %i[i].freeze
10
+ MUTATED_FLAGS = %i[i m].freeze
11
11
 
12
12
  handle(:regopt)
13
13
 
14
14
  private
15
15
 
16
16
  def dispatch
17
- emit_type(*mutated_flags)
18
- end
19
-
20
- def mutated_flags
21
- (children - MUTATED_FLAGS)
17
+ MUTATED_FLAGS.each do |flag|
18
+ emit_type(*(children - [flag]))
19
+ end
22
20
  end
23
21
 
24
22
  end # Regopt
@@ -20,6 +20,11 @@ module Mutant
20
20
  mutate_body
21
21
  mutate_rescue_bodies
22
22
  mutate_else_body
23
+ emit_singletons if standalone?
24
+ end
25
+
26
+ def standalone?
27
+ parent_type.nil?
23
28
  end
24
29
 
25
30
  def mutate_rescue_bodies
@@ -30,6 +35,28 @@ module Mutant
30
35
  emit_concat(resbody.body)
31
36
  end
32
37
  end
38
+ emit_rescue_clause_removals
39
+ emit_handler_promotion
40
+ end
41
+
42
+ def emit_handler_promotion
43
+ return unless standalone?
44
+
45
+ children_indices(RESCUE_INDICES).each do |index|
46
+ resbody = AST::Meta::Resbody.new(node: children.fetch(index))
47
+ emit(resbody.body)
48
+ end
49
+ end
50
+
51
+ def emit_rescue_clause_removals
52
+ rescue_indices = children_indices(RESCUE_INDICES).to_a
53
+ return unless rescue_indices.length > 1
54
+
55
+ rescue_indices.each do |index|
56
+ dup_children = children.dup
57
+ dup_children.delete_at(index)
58
+ emit_type(*dup_children)
59
+ end
33
60
  end
34
61
 
35
62
  def emit_concat(child)
@@ -10,6 +10,13 @@ module Mutant
10
10
 
11
11
  children :left, :operator, :right
12
12
 
13
+ # Pairs of operators where swapping produces equivalent mutants
14
+ # when the right operand is a multiplicative identity (1 or -1).
15
+ MULTIPLICATION_DIVISION_SWAP = {
16
+ %i[* /].freeze => true,
17
+ %i[/ *].freeze => true
18
+ }.freeze
19
+
13
20
  private
14
21
 
15
22
  def dispatch
@@ -21,6 +28,44 @@ module Mutant
21
28
  emit_not_equality_mutations
22
29
  end
23
30
 
31
+ def emit_selector_replacement
32
+ config
33
+ .operators
34
+ .selector_replacements
35
+ .fetch(operator, EMPTY_ARRAY)
36
+ .each do |replacement|
37
+ emit_selector(replacement) unless equivalent_multiplication_division_swap?(replacement)
38
+ end
39
+ end
40
+
41
+ # Multiplication and division by 1 or -1 are equivalent operations:
42
+ # a * 1 == a / 1 (both equal a)
43
+ # a * -1 == a / -1 (both equal -a)
44
+ #
45
+ # Swapping * <-> / when RIGHT operand is 1 or -1 produces an equivalent
46
+ # mutant that can never be killed, wasting test resources.
47
+ #
48
+ # Note: LEFT operand identity (e.g., 1 * a -> 1 / a) produces different
49
+ # results (a vs 1/a) and should NOT be skipped.
50
+ def equivalent_multiplication_division_swap?(replacement)
51
+ MULTIPLICATION_DIVISION_SWAP.key?([operator, replacement]) && multiplicative_identity?(right)
52
+ end
53
+
54
+ def multiplicative_identity?(node)
55
+ return false unless %i[int float].include?(node.type)
56
+
57
+ value = Mutant::Util.one(node.children)
58
+
59
+ case node.type
60
+ when :int
61
+ value.equal?(1) || value.equal?(-1)
62
+ when :float
63
+ # rubocop:disable Lint/FloatComparison
64
+ value.equal?(1.0) || value.equal?(-1.0)
65
+ # rubocop:enable Lint/FloatComparison
66
+ end
67
+ end
68
+
24
69
  def emit_not_equality_mutations
25
70
  return unless operator.equal?(:'!=')
26
71
 
@@ -69,15 +69,20 @@ module Mutant
69
69
  emit_reduce_to_sum_mutation
70
70
  emit_start_end_with_mutations
71
71
  emit_predicate_mutations
72
- emit_array_mutation
72
+ emit_type_coercion_mutations
73
73
  emit_static_send
74
74
  emit_const_get_mutation
75
- emit_integer_mutation
76
75
  emit_dig_mutation
77
76
  emit_double_negation_mutation
78
77
  emit_lambda_mutation
79
78
  end
80
79
 
80
+ def emit_type_coercion_mutations
81
+ emit_array_mutation
82
+ emit_integer_mutation
83
+ emit_empty_collection_mutation
84
+ end
85
+
81
86
  def emit_reduce_to_sum_mutation
82
87
  return unless selector.equal?(:reduce)
83
88
 
@@ -93,8 +98,6 @@ module Mutant
93
98
  end
94
99
  end
95
100
 
96
- # rubocop:disable Metrics/CyclomaticComplexity
97
- # rubocop:disable Metrics/MethodLength
98
101
  def emit_start_end_with_mutations
99
102
  return unless REGEXP_MATCH_METHODS.include?(selector) && arguments.one?
100
103
 
@@ -102,6 +105,10 @@ module Mutant
102
105
 
103
106
  return unless argument.type.equal?(:regexp)
104
107
 
108
+ emit_regexp_to_prefix_suffix(argument)
109
+ end
110
+
111
+ def emit_regexp_to_prefix_suffix(argument)
105
112
  string = Regexp.regexp_body(argument) or return
106
113
 
107
114
  expressions = ::Regexp::Parser.parse(string)
@@ -171,6 +178,17 @@ module Mutant
171
178
  emit(s(:send, nil, :lambda)) if meta.proc?
172
179
  end
173
180
 
181
+ def emit_empty_collection_mutation
182
+ case selector
183
+ when :to_a, :to_ary
184
+ emit(s(:array))
185
+ when :to_h, :to_hash
186
+ emit(s(:hash))
187
+ when :to_s, :to_str
188
+ emit(s(:str, ''))
189
+ end
190
+ end
191
+
174
192
  def emit_dig_mutation
175
193
  return if !selector.equal?(:dig) || arguments.none?
176
194
 
@@ -206,11 +224,9 @@ module Mutant
206
224
  emit(receiver) if receiver && !left_op_assignment?
207
225
  end
208
226
 
209
- # rubocop:disable Style/HashEachMethods
210
- # - its not a hash ;)
211
227
  def mutate_arguments
212
228
  emit_type(receiver, selector)
213
- remaining_children_with_index.each do |_node, index|
229
+ remaining_children_indices.each do |index|
214
230
  mutate_argument_index(index)
215
231
  delete_child(index)
216
232
  end
@@ -6,6 +6,7 @@ module Mutant
6
6
 
7
7
  # Mutator for super with parentheses
8
8
  class Super < self
9
+ include AST::Nodes
9
10
 
10
11
  handle(:super)
11
12
 
@@ -13,6 +14,7 @@ module Mutant
13
14
 
14
15
  def dispatch
15
16
  emit_singletons
17
+ emit(N_ZSUPER) unless children.empty?
16
18
  children.each_index do |index|
17
19
  mutate_child(index)
18
20
  delete_child(index)
@@ -9,10 +9,27 @@ module Mutant
9
9
 
10
10
  handle(:zsuper)
11
11
 
12
+ include AST::Nodes
13
+
14
+ ARGUMENTS_DESCENDANT = {
15
+ def: AST::Structure.for(:def).descendant(:arguments),
16
+ defs: AST::Structure.for(:defs).descendant(:arguments)
17
+ }.freeze
18
+
12
19
  private
13
20
 
14
21
  def dispatch
15
22
  emit_singletons
23
+ emit(N_EMPTY_SUPER) if enclosing_method_has_arguments?
24
+ end
25
+
26
+ def enclosing_method_has_arguments?
27
+ current = parent
28
+ while current
29
+ descendant = ARGUMENTS_DESCENDANT[current.node.type]
30
+ return !descendant.value(current.node).children.empty? if descendant
31
+ current = current.parent
32
+ end
16
33
  end
17
34
 
18
35
  end # ZSuper
@@ -98,21 +98,15 @@ module Mutant
98
98
  emit(node) unless AST::Types::NOT_STANDALONE.include?(node.type)
99
99
  end
100
100
 
101
- def emit_singletons
102
- emit_nil
103
- end
101
+ def emit_singletons = emit_nil
104
102
 
105
103
  def emit_nil
106
104
  emit(N_NIL) unless left_op_assignment?
107
105
  end
108
106
 
109
- def parent_node
110
- parent&.node
111
- end
107
+ def parent_node = parent&.node
112
108
 
113
- def parent_type
114
- parent_node&.type
115
- end
109
+ def parent_type = parent_node&.type
116
110
 
117
111
  def left_op_assignment?
118
112
  AST::Types::OP_ASSIGN.include?(parent_type) && parent.node.children.first.equal?(node)
@@ -40,9 +40,7 @@ module Mutant
40
40
  output << object
41
41
  end
42
42
 
43
- def dup_input
44
- input.dup
45
- end
43
+ def dup_input = input.dup
46
44
 
47
45
  end # Mutator
48
46
  end # Mutant