mutineer-mongoid 2.0.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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1194 -0
  3. data/LICENSE +21 -0
  4. data/README.md +659 -0
  5. data/bin/mutineer +6 -0
  6. data/lib/mutineer/baseline.rb +185 -0
  7. data/lib/mutineer/changed_lines.rb +136 -0
  8. data/lib/mutineer/child_stdout.rb +35 -0
  9. data/lib/mutineer/cli.rb +681 -0
  10. data/lib/mutineer/config.rb +372 -0
  11. data/lib/mutineer/coverage_map.rb +1366 -0
  12. data/lib/mutineer/daemon_backend.rb +351 -0
  13. data/lib/mutineer/daemon_client.rb +407 -0
  14. data/lib/mutineer/daemon_server.rb +351 -0
  15. data/lib/mutineer/external_backend.rb +288 -0
  16. data/lib/mutineer/file_swap.rb +194 -0
  17. data/lib/mutineer/isolation.rb +321 -0
  18. data/lib/mutineer/job_plan.rb +352 -0
  19. data/lib/mutineer/kill_channel.rb +284 -0
  20. data/lib/mutineer/kill_matrix.rb +88 -0
  21. data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
  22. data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
  23. data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +145 -0
  24. data/lib/mutineer/minitest_integration.rb +137 -0
  25. data/lib/mutineer/mongoid_failures.rb +82 -0
  26. data/lib/mutineer/mongoid_worker_db.rb +97 -0
  27. data/lib/mutineer/mutant_id.rb +126 -0
  28. data/lib/mutineer/mutation.rb +27 -0
  29. data/lib/mutineer/mutator_registry.rb +109 -0
  30. data/lib/mutineer/mutators/arithmetic.rb +35 -0
  31. data/lib/mutineer/mutators/array_literal.rb +44 -0
  32. data/lib/mutineer/mutators/base.rb +105 -0
  33. data/lib/mutineer/mutators/boolean_connector.rb +52 -0
  34. data/lib/mutineer/mutators/boolean_literal.rb +56 -0
  35. data/lib/mutineer/mutators/chain_link.rb +132 -0
  36. data/lib/mutineer/mutators/collection_method.rb +44 -0
  37. data/lib/mutineer/mutators/comparison.rb +35 -0
  38. data/lib/mutineer/mutators/condition_forcing.rb +134 -0
  39. data/lib/mutineer/mutators/condition_negation.rb +49 -0
  40. data/lib/mutineer/mutators/literal_mutation.rb +51 -0
  41. data/lib/mutineer/mutators/negation_removal.rb +44 -0
  42. data/lib/mutineer/mutators/operand_removal.rb +58 -0
  43. data/lib/mutineer/mutators/operator_assignment.rb +113 -0
  44. data/lib/mutineer/mutators/range_literal.rb +54 -0
  45. data/lib/mutineer/mutators/regex_literal.rb +72 -0
  46. data/lib/mutineer/mutators/return_nil.rb +72 -0
  47. data/lib/mutineer/mutators/safe_navigation.rb +76 -0
  48. data/lib/mutineer/mutators/statement_removal.rb +33 -0
  49. data/lib/mutineer/mutators/string_literal.rb +34 -0
  50. data/lib/mutineer/orphan_guard.rb +28 -0
  51. data/lib/mutineer/pairing.rb +298 -0
  52. data/lib/mutineer/parser.rb +45 -0
  53. data/lib/mutineer/progress.rb +46 -0
  54. data/lib/mutineer/project.rb +506 -0
  55. data/lib/mutineer/project_path.rb +57 -0
  56. data/lib/mutineer/rails_worker_db.rb +212 -0
  57. data/lib/mutineer/reporter.rb +926 -0
  58. data/lib/mutineer/result.rb +240 -0
  59. data/lib/mutineer/runner.rb +418 -0
  60. data/lib/mutineer/statement_lines.rb +183 -0
  61. data/lib/mutineer/subject.rb +38 -0
  62. data/lib/mutineer/test_runners/minitest.rb +22 -0
  63. data/lib/mutineer/test_runners/rspec.rb +330 -0
  64. data/lib/mutineer/test_runners.rb +30 -0
  65. data/lib/mutineer/version.rb +6 -0
  66. data/lib/mutineer/worker_pool.rb +147 -0
  67. data/lib/mutineer.rb +48 -0
  68. metadata +143 -0
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../mutation"
5
+
6
+ module Mutineer
7
+ # Namespace for all built-in mutator implementations.
8
+ module Mutators
9
+ # Base Prism visitor for operators.
10
+ #
11
+ # Subclasses override `visit_*` methods to push `Mutation` objects onto
12
+ # `@mutations`. Visiting only `def_node.body` is the body-only enforcement:
13
+ # the def signature line is never touched.
14
+ class Base < Prism::Visitor
15
+ # Walks the subject body and collects mutations.
16
+ #
17
+ # @param subject [Mutineer::Subject] subject whose body is visited.
18
+ # @param source [String] full source text for byte-based slicing.
19
+ # @return [Array<Mutineer::Mutation>] collected mutations.
20
+ def mutations_for(subject, source)
21
+ @source = source
22
+ @mutations = []
23
+ @body = subject.def_node.body
24
+ @body&.accept(self)
25
+ drop_dangling_heredocs
26
+ @mutations
27
+ end
28
+
29
+ # Skips a nested method definition. The project finds it as a subject of
30
+ # its own, so a visit here counts its mutations twice. Inside
31
+ # `class << obj`, the project finds no subject, so the visit continues.
32
+ #
33
+ # @param node [Prism::DefNode] nested definition node.
34
+ # @return [void]
35
+ def visit_def_node(node)
36
+ super if @in_undiscovered
37
+ end
38
+
39
+ # Tracks a `class << obj` whose `obj` is not `self`. The project does not
40
+ # recurse into it, so its defs are no subjects (see
41
+ # `Project::SubjectVisitor#visit_singleton_class_node`).
42
+ #
43
+ # @param node [Prism::SingletonClassNode] singleton-class node.
44
+ # @return [void]
45
+ def visit_singleton_class_node(node)
46
+ return super if node.expression.is_a?(Prism::SelfNode)
47
+
48
+ outer, @in_undiscovered = @in_undiscovered, true
49
+ super
50
+ @in_undiscovered = outer
51
+ end
52
+
53
+ private
54
+
55
+ # Drops a mutation that deletes a heredoc opener and leaves its body.
56
+ #
57
+ # The body sits past the opener's node. Replacing the opener with text
58
+ # that does not contain it turns the body into Ruby, which parses and
59
+ # then raises. A replacement that still contains the opener keeps the
60
+ # body attached, so that mutation stays.
61
+ #
62
+ # @return [void]
63
+ def drop_dangling_heredocs
64
+ @mutations.reject! { |mutation| dangling_heredoc?(mutation) }
65
+ end
66
+
67
+ # Returns whether the mutation removes a heredoc opener whose body hangs
68
+ # past the replaced range.
69
+ #
70
+ # @param mutation [Mutineer::Mutation] candidate edit.
71
+ # @param node [Prism::Node, nil] subtree to search. Defaults to the body.
72
+ # @return [Boolean] true when the edit would leave a heredoc body behind.
73
+ def dangling_heredoc?(mutation, node = @body)
74
+ return false if node.nil? || !node.is_a?(Prism::Node)
75
+
76
+ if node.respond_to?(:heredoc?) && node.heredoc? && node.respond_to?(:opening_loc)
77
+ opening = node.opening_loc
78
+ if opening && opening.start_offset >= mutation.start_offset && opening.start_offset < mutation.end_offset
79
+ closing = node.respond_to?(:closing_loc) ? node.closing_loc : nil
80
+ hangs = closing.nil? || closing.end_offset > mutation.end_offset
81
+ if hangs
82
+ opener = @source.byteslice(opening.start_offset, opening.end_offset - opening.start_offset)
83
+ return true if opener.empty? || !mutation.replacement.include?(opener)
84
+ end
85
+ end
86
+ end
87
+
88
+ node.compact_child_nodes.any? { |child| dangling_heredoc?(mutation, child) }
89
+ end
90
+
91
+ # Returns whether a node is, or contains, a heredoc.
92
+ #
93
+ # A heredoc's body lies outside its node's byte range. A mutation that
94
+ # deletes the node leaves the body behind as code, so the mutant
95
+ # always raises.
96
+ #
97
+ # @param node [Prism::Node] node to inspect.
98
+ # @return [Boolean] true when a heredoc is inside the node.
99
+ def heredoc?(node)
100
+ (node.respond_to?(:heredoc?) && node.heredoc?) ||
101
+ node.compact_child_nodes.any? { |child| heredoc?(child) }
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Boolean connector mutator.
8
+ #
9
+ # Replaces symbolic and keyword connectors with their opposite form.
10
+ class BooleanConnector < Base
11
+ # Token replacements for boolean connectors.
12
+ REPLACEMENTS = { "&&" => "||", "||" => "&&", "and" => "or", "or" => "and" }.freeze
13
+
14
+ # Visits `and` nodes.
15
+ #
16
+ # @param node [Prism::AndNode] node to inspect.
17
+ # @return [void]
18
+ def visit_and_node(node)
19
+ emit(node)
20
+ super
21
+ end
22
+
23
+ # Visits `or` nodes.
24
+ #
25
+ # @param node [Prism::OrNode] node to inspect.
26
+ # @return [void]
27
+ def visit_or_node(node)
28
+ emit(node)
29
+ super
30
+ end
31
+
32
+ private
33
+
34
+ # Emits a connector mutation when the token is replaceable.
35
+ #
36
+ # @param node [Prism::Node] connector node.
37
+ # @return [void]
38
+ def emit(node)
39
+ loc = node.operator_loc
40
+ replacement = REPLACEMENTS[loc.slice]
41
+ return unless replacement
42
+
43
+ @mutations << Mutation.new(
44
+ start_offset: loc.start_offset,
45
+ end_offset: loc.end_offset,
46
+ replacement: replacement,
47
+ operator: :boolean_connector
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Boolean literal mutator.
8
+ #
9
+ # Mutates true/false and nil literals in the boolean_literal family.
10
+ class BooleanLiteral < Base
11
+ # Visits true literals.
12
+ #
13
+ # @param node [Prism::TrueNode] node to inspect.
14
+ # @return [void]
15
+ def visit_true_node(node)
16
+ emit(node, "false")
17
+ super
18
+ end
19
+
20
+ # Visits false literals.
21
+ #
22
+ # @param node [Prism::FalseNode] node to inspect.
23
+ # @return [void]
24
+ def visit_false_node(node)
25
+ emit(node, "true")
26
+ super
27
+ end
28
+
29
+ # Visits nil literals.
30
+ #
31
+ # @param node [Prism::NilNode] node to inspect.
32
+ # @return [void]
33
+ def visit_nil_node(node)
34
+ emit(node, "true")
35
+ super
36
+ end
37
+
38
+ private
39
+
40
+ # Emits a boolean-literal mutation.
41
+ #
42
+ # @param node [Prism::Node] literal node.
43
+ # @param replacement [String] replacement token.
44
+ # @return [void]
45
+ def emit(node, replacement)
46
+ loc = node.location
47
+ @mutations << Mutation.new(
48
+ start_offset: loc.start_offset,
49
+ end_offset: loc.end_offset,
50
+ replacement: replacement,
51
+ operator: :boolean_literal
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Chain-link mutator (Tier-2).
8
+ #
9
+ # Drops one dotted call from a chain of calls, with its arguments and block:
10
+ # `user.account.owner.name` becomes `user.owner.name` and `user.account.name`.
11
+ # The chain's receiver and its final call stay, so a chain of n dotted calls
12
+ # gives at most n - 1 mutations. A chain begins at a receiver that is not a
13
+ # dotted call: a local, a constant, `self`, `list[i]`, `(a + b)`. A call in
14
+ # an argument or a block starts a chain of its own.
15
+ #
16
+ # The mutant survives when no test tells the chain apart from the same chain
17
+ # without that step: a scope, a filter or a lookup the tests never see.
18
+ #
19
+ # Links in {SKIPPED} are never dropped. On a value that already has the
20
+ # target type or needs no copy, a conversion (`name.to_s.strip`) or a copy
21
+ # (`list.dup.sort`) is a no-op, so dropping it most often makes an
22
+ # equivalent mutant. Dropping `new` sends the next call to the class, which
23
+ # raises (killed by any test that runs the line) or reaches a class method
24
+ # that builds the instance itself (equivalent); neither says anything about
25
+ # the tests.
26
+ class ChainLink < Base
27
+ # Method names whose link is never dropped: core conversions and copies,
28
+ # plus `new`.
29
+ SKIPPED = %i[
30
+ to_s to_str to_sym to_i to_int to_f to_r to_c to_a to_ary to_h to_hash to_proc to_set
31
+ dup clone freeze itself
32
+ new
33
+ ].freeze
34
+
35
+ # Resets the per-subject record of calls already placed in a chain.
36
+ #
37
+ # @param subject [Mutineer::Subject] subject whose body is visited.
38
+ # @param source [String] full source text for byte-based slicing.
39
+ # @return [Array<Mutineer::Mutation>] collected mutations.
40
+ def mutations_for(subject, source)
41
+ @links = {}.compare_by_identity
42
+ super
43
+ end
44
+
45
+ # Visits a call. The outermost dotted call of a chain is its final call;
46
+ # the dotted calls below it in the receiver are its links.
47
+ #
48
+ # @param node [Prism::CallNode] call node to inspect.
49
+ # @return [void]
50
+ def visit_call_node(node)
51
+ chain(node) if dotted?(node) && !@links.key?(node)
52
+ super
53
+ end
54
+
55
+ # Visits `a.b.c += 1`, whose final call Prism parses as its own node.
56
+ #
57
+ # @param node [Prism::CallOperatorWriteNode] node to inspect.
58
+ # @return [void]
59
+ def visit_call_operator_write_node(node)
60
+ chain(node)
61
+ super
62
+ end
63
+
64
+ # Visits `a.b.c ||= 1`.
65
+ #
66
+ # @param node [Prism::CallOrWriteNode] node to inspect.
67
+ # @return [void]
68
+ def visit_call_or_write_node(node)
69
+ chain(node)
70
+ super
71
+ end
72
+
73
+ # Visits `a.b.c &&= 1`.
74
+ #
75
+ # @param node [Prism::CallAndWriteNode] node to inspect.
76
+ # @return [void]
77
+ def visit_call_and_write_node(node)
78
+ chain(node)
79
+ super
80
+ end
81
+
82
+ # Visits a call used as an assignment target, as in `a.b.c, d = 1, 2`.
83
+ #
84
+ # @param node [Prism::CallTargetNode] node to inspect.
85
+ # @return [void]
86
+ def visit_call_target_node(node)
87
+ chain(node)
88
+ super
89
+ end
90
+
91
+ private
92
+
93
+ # Walks a final call's receiver chain, recording each link so it is not
94
+ # taken for the final call of a chain of its own, and dropping each link
95
+ # not in {SKIPPED}.
96
+ #
97
+ # @param top [Prism::Node] the chain's final call; responds to `receiver`.
98
+ # @return [void]
99
+ def chain(top)
100
+ link = top.receiver
101
+ while dotted?(link)
102
+ @links[link] = true
103
+ drop(link) unless SKIPPED.include?(link.name)
104
+ link = link.receiver
105
+ end
106
+ end
107
+
108
+ # Emits the removal of one link: from the end of its receiver to the end
109
+ # of its arguments and block, so a chain split across lines keeps the
110
+ # layout of the lines that remain.
111
+ #
112
+ # @param link [Prism::CallNode] the dotted call to drop.
113
+ # @return [void]
114
+ def drop(link)
115
+ @mutations << Mutation.new(
116
+ start_offset: link.receiver.location.end_offset,
117
+ end_offset: link.location.end_offset,
118
+ replacement: "",
119
+ operator: :chain_link
120
+ )
121
+ end
122
+
123
+ # Returns whether a node is a call through `.`, `&.` or `::`.
124
+ #
125
+ # @param node [Prism::Node, nil] node to inspect.
126
+ # @return [Boolean] true for a call node with a call operator.
127
+ def dotted?(node)
128
+ node.is_a?(Prism::CallNode) && !node.call_operator_loc.nil?
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Collection / enumerable method-name mutator (Tier-2).
8
+ #
9
+ # Swaps the method-name token of a call to its semantic opposite, one
10
+ # mutation per occurrence, exactly like the arithmetic/comparison operators.
11
+ class CollectionMethod < Base
12
+ # Method-name swaps. All targets are core-Ruby Enumerable/Array methods so
13
+ # the mutant exercises real behaviour rather than always raising.
14
+ #
15
+ # include? -> exclude? is left out: exclude? is not
16
+ # core Ruby, so that mutant would always NoMethodError (a weak mutant).
17
+ REPLACEMENTS = {
18
+ map: "each", each: "map",
19
+ all?: "any?", any?: "all?",
20
+ first: "last", last: "first",
21
+ min: "max", max: "min",
22
+ select: "reject", reject: "select"
23
+ }.freeze
24
+
25
+ # Visits call nodes and emits collection-method mutations.
26
+ #
27
+ # @param node [Prism::CallNode] call node to inspect.
28
+ # @return [void]
29
+ def visit_call_node(node)
30
+ replacement = REPLACEMENTS[node.name]
31
+ loc = node.message_loc
32
+ if replacement && loc
33
+ @mutations << Mutation.new(
34
+ start_offset: loc.start_offset,
35
+ end_offset: loc.end_offset,
36
+ replacement: replacement,
37
+ operator: :collection_method
38
+ )
39
+ end
40
+ super
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Comparison and boundary mutator.
8
+ #
9
+ # Rewrites comparison operators one occurrence at a time.
10
+ class Comparison < Base
11
+ # Token replacements for comparison operators.
12
+ REPLACEMENTS = {
13
+ :< => "<=", :<= => "<", :> => ">=", :>= => ">", :== => "!=", :!= => "=="
14
+ }.freeze
15
+
16
+ # Visits call nodes and emits comparison mutations.
17
+ #
18
+ # @param node [Prism::CallNode] call node to inspect.
19
+ # @return [void]
20
+ def visit_call_node(node)
21
+ replacement = REPLACEMENTS[node.name]
22
+ loc = node.message_loc
23
+ if replacement && loc && node.receiver
24
+ @mutations << Mutation.new(
25
+ start_offset: loc.start_offset,
26
+ end_offset: loc.end_offset,
27
+ replacement: replacement,
28
+ operator: :comparison
29
+ )
30
+ end
31
+ super # nested comparisons (a >= b && c <= d) each get their own mutation
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Shared base of the condition-forcing mutators (Tier-2).
8
+ #
9
+ # Replaces the condition of an `if`, `elsif`, `unless`, ternary, modifier
10
+ # guard (`x if y`) or `case`/`in` guard with a literal, so its branch always
11
+ # runs or never runs. {ConditionTrue} forces `true` and {ConditionFalse}
12
+ # forces `false`; one mutation per condition each.
13
+ #
14
+ # A condition that is already `true`, `false` or `nil`, even in
15
+ # parentheses, is left alone: forcing it changes nothing or repeats the
16
+ # `boolean_literal` flip. This holds even when `boolean_literal` does not run.
17
+ # A condition that holds a heredoc is left alone too: the heredoc body lies
18
+ # outside the condition, so it would stay behind as code. A condition that
19
+ # assigns a variable keeps its code and only its value is forced.
20
+ #
21
+ # The never-runs side of an else-less conditional can be the same program
22
+ # as the `nil` that `statement_removal` or `return_nil` puts in place of the
23
+ # whole conditional. It is still made: dropping it would make the mutants
24
+ # of one operator depend on which other operators run, and on what the user
25
+ # suppressed for them. The two then get the same verdict.
26
+ #
27
+ # `condition_negation` wraps the condition in `!( ... )`, which neither
28
+ # forced value is.
29
+ class ConditionForcing < Base
30
+ # Condition nodes that `boolean_literal` already mutates.
31
+ LITERALS = [Prism::TrueNode, Prism::FalseNode, Prism::NilNode].freeze
32
+
33
+ # Visits if nodes: `if`, `elsif`, a ternary, a modifier `if` and an
34
+ # `in ... if` guard.
35
+ #
36
+ # @param node [Prism::IfNode] node to inspect.
37
+ # @return [void]
38
+ def visit_if_node(node)
39
+ force(node)
40
+ super
41
+ end
42
+
43
+ # Visits unless nodes: `unless`, a modifier `unless` and an
44
+ # `in ... unless` guard.
45
+ #
46
+ # @param node [Prism::UnlessNode] node to inspect.
47
+ # @return [void]
48
+ def visit_unless_node(node)
49
+ force(node)
50
+ super
51
+ end
52
+
53
+ private
54
+
55
+ # Emits the forced condition unless the condition is a literal or holds a
56
+ # heredoc, whose body would stay behind as code.
57
+ #
58
+ # @param node [Prism::IfNode, Prism::UnlessNode] the conditional.
59
+ # @return [void]
60
+ def force(node)
61
+ predicate = node.predicate
62
+ return if LITERALS.include?(unwrap(predicate).class) || heredoc?(predicate)
63
+
64
+ loc = predicate.location
65
+ @mutations << Mutation.new(
66
+ start_offset: loc.start_offset,
67
+ end_offset: loc.end_offset,
68
+ replacement: replacement(predicate),
69
+ operator: self.class::OPERATOR
70
+ )
71
+ end
72
+
73
+ # Returns the forced value for the condition, always in parentheses, so it
74
+ # cannot fuse with a keyword or a `?` next to it: `x if@a` becomes
75
+ # `x if(true)` and `@a?1:2` becomes `(true)?1:2`. A condition that assigns
76
+ # a variable keeps its code, so later reads still see the variable:
77
+ # `(m = x; true)`.
78
+ #
79
+ # @param predicate [Prism::Node] the condition.
80
+ # @return [String] the replacement source.
81
+ def replacement(predicate)
82
+ value = self.class::VALUE.to_s
83
+ writes?(predicate) ? "(#{predicate.slice}; #{value})" : "(#{value})"
84
+ end
85
+
86
+ # Returns whether the node writes a variable or constant anywhere inside
87
+ # it: a local, instance, class or global variable, or a constant, in a
88
+ # plain, compound, multiple or pattern write, or a named regex capture.
89
+ #
90
+ # @param node [Prism::Node] the node to inspect.
91
+ # @return [Boolean] true when a write or target node is inside.
92
+ def writes?(node)
93
+ return true if node.type.name.end_with?("_write_node", "_target_node")
94
+
95
+ node.compact_child_nodes.any? { |child| writes?(child) }
96
+ end
97
+
98
+ # Returns the node inside parentheses that hold exactly one expression,
99
+ # so `(true)` and `((true))` read as `true`.
100
+ #
101
+ # @param node [Prism::Node] the condition.
102
+ # @return [Prism::Node] the innermost wrapped node, or `node` itself.
103
+ def unwrap(node)
104
+ body = node.body if node.is_a?(Prism::ParenthesesNode)
105
+ body = body.body.first if body.is_a?(Prism::StatementsNode) && body.body.size == 1
106
+ body && !body.is_a?(Prism::StatementsNode) ? unwrap(body) : node
107
+ end
108
+ end
109
+
110
+ # Condition-forcing mutator: the condition becomes `true` (Tier-2).
111
+ #
112
+ # An `if` branch always runs; an `unless` branch never runs. The mutant
113
+ # survives when no test runs the code with the condition false (for an
114
+ # `if`) or true (for an `unless`) and checks what changes.
115
+ class ConditionTrue < ConditionForcing
116
+ # The literal the condition is replaced with.
117
+ VALUE = true
118
+ # The operator name mutations carry.
119
+ OPERATOR = :condition_true
120
+ end
121
+
122
+ # Condition-forcing mutator: the condition becomes `false` (Tier-2).
123
+ #
124
+ # An `if` branch never runs; an `unless` branch always runs. The mutant
125
+ # survives when no test needs the branch the condition guards (for an
126
+ # `if`) or skips it (for an `unless`).
127
+ class ConditionFalse < ConditionForcing
128
+ # The literal the condition is replaced with.
129
+ VALUE = false
130
+ # The operator name mutations carry.
131
+ OPERATOR = :condition_false
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Condition-negation mutator.
8
+ #
9
+ # Wraps if/unless predicates in `!( ... )` textually.
10
+ class ConditionNegation < Base
11
+ # Visits if nodes.
12
+ #
13
+ # @param node [Prism::IfNode] node to inspect.
14
+ # @return [void]
15
+ def visit_if_node(node)
16
+ wrap(node.predicate)
17
+ super
18
+ end
19
+
20
+ # Visits unless nodes.
21
+ #
22
+ # @param node [Prism::UnlessNode] node to inspect.
23
+ # @return [void]
24
+ def visit_unless_node(node)
25
+ wrap(node.predicate)
26
+ super
27
+ end
28
+
29
+ private
30
+
31
+ # Wraps a predicate in negation.
32
+ #
33
+ # @param predicate [Prism::Node, nil] predicate node.
34
+ # @return [void]
35
+ def wrap(predicate)
36
+ return unless predicate
37
+
38
+ loc = predicate.location
39
+ original = @source.byteslice(loc.start_offset...loc.end_offset)
40
+ @mutations << Mutation.new(
41
+ start_offset: loc.start_offset,
42
+ end_offset: loc.end_offset,
43
+ replacement: "!( #{original} )",
44
+ operator: :condition_negation
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Literal mutation mutator.
8
+ #
9
+ # Mutates integers and strings with the Tier-2 literal rules.
10
+ class LiteralMutation < Base
11
+ # Visits integer literals.
12
+ #
13
+ # @param node [Prism::IntegerNode] node to inspect.
14
+ # @return [void]
15
+ def visit_integer_node(node)
16
+ n = node.value
17
+ emit(node.location, "0") unless n.zero?
18
+ emit(node.location, "1") unless n == 1
19
+ emit(node.location, (n + 1).to_s)
20
+ super
21
+ end
22
+
23
+ # Visits string literals.
24
+ #
25
+ # @param node [Prism::StringNode] node to inspect.
26
+ # @return [void]
27
+ def visit_string_node(node)
28
+ loc = node.location
29
+ token = @source.byteslice(loc.start_offset...loc.end_offset)
30
+ emit(loc, '""') unless token == '""' || token == "''" || node.unescaped.empty?
31
+ super
32
+ end
33
+
34
+ private
35
+
36
+ # Emits a literal mutation.
37
+ #
38
+ # @param loc [Prism::Location] source location.
39
+ # @param replacement [String] replacement literal.
40
+ # @return [void]
41
+ def emit(loc, replacement)
42
+ @mutations << Mutation.new(
43
+ start_offset: loc.start_offset,
44
+ end_offset: loc.end_offset,
45
+ replacement: replacement,
46
+ operator: :literal_mutation
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end