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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1194 -0
- data/LICENSE +21 -0
- data/README.md +659 -0
- data/bin/mutineer +6 -0
- data/lib/mutineer/baseline.rb +185 -0
- data/lib/mutineer/changed_lines.rb +136 -0
- data/lib/mutineer/child_stdout.rb +35 -0
- data/lib/mutineer/cli.rb +681 -0
- data/lib/mutineer/config.rb +372 -0
- data/lib/mutineer/coverage_map.rb +1366 -0
- data/lib/mutineer/daemon_backend.rb +351 -0
- data/lib/mutineer/daemon_client.rb +407 -0
- data/lib/mutineer/daemon_server.rb +351 -0
- data/lib/mutineer/external_backend.rb +288 -0
- data/lib/mutineer/file_swap.rb +194 -0
- data/lib/mutineer/isolation.rb +321 -0
- data/lib/mutineer/job_plan.rb +352 -0
- data/lib/mutineer/kill_channel.rb +284 -0
- data/lib/mutineer/kill_matrix.rb +88 -0
- data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
- data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
- data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +145 -0
- data/lib/mutineer/minitest_integration.rb +137 -0
- data/lib/mutineer/mongoid_failures.rb +82 -0
- data/lib/mutineer/mongoid_worker_db.rb +97 -0
- data/lib/mutineer/mutant_id.rb +126 -0
- data/lib/mutineer/mutation.rb +27 -0
- data/lib/mutineer/mutator_registry.rb +109 -0
- data/lib/mutineer/mutators/arithmetic.rb +35 -0
- data/lib/mutineer/mutators/array_literal.rb +44 -0
- data/lib/mutineer/mutators/base.rb +105 -0
- data/lib/mutineer/mutators/boolean_connector.rb +52 -0
- data/lib/mutineer/mutators/boolean_literal.rb +56 -0
- data/lib/mutineer/mutators/chain_link.rb +132 -0
- data/lib/mutineer/mutators/collection_method.rb +44 -0
- data/lib/mutineer/mutators/comparison.rb +35 -0
- data/lib/mutineer/mutators/condition_forcing.rb +134 -0
- data/lib/mutineer/mutators/condition_negation.rb +49 -0
- data/lib/mutineer/mutators/literal_mutation.rb +51 -0
- data/lib/mutineer/mutators/negation_removal.rb +44 -0
- data/lib/mutineer/mutators/operand_removal.rb +58 -0
- data/lib/mutineer/mutators/operator_assignment.rb +113 -0
- data/lib/mutineer/mutators/range_literal.rb +54 -0
- data/lib/mutineer/mutators/regex_literal.rb +72 -0
- data/lib/mutineer/mutators/return_nil.rb +72 -0
- data/lib/mutineer/mutators/safe_navigation.rb +76 -0
- data/lib/mutineer/mutators/statement_removal.rb +33 -0
- data/lib/mutineer/mutators/string_literal.rb +34 -0
- data/lib/mutineer/orphan_guard.rb +28 -0
- data/lib/mutineer/pairing.rb +298 -0
- data/lib/mutineer/parser.rb +45 -0
- data/lib/mutineer/progress.rb +46 -0
- data/lib/mutineer/project.rb +506 -0
- data/lib/mutineer/project_path.rb +57 -0
- data/lib/mutineer/rails_worker_db.rb +212 -0
- data/lib/mutineer/reporter.rb +926 -0
- data/lib/mutineer/result.rb +240 -0
- data/lib/mutineer/runner.rb +418 -0
- data/lib/mutineer/statement_lines.rb +183 -0
- data/lib/mutineer/subject.rb +38 -0
- data/lib/mutineer/test_runners/minitest.rb +22 -0
- data/lib/mutineer/test_runners/rspec.rb +330 -0
- data/lib/mutineer/test_runners.rb +30 -0
- data/lib/mutineer/version.rb +6 -0
- data/lib/mutineer/worker_pool.rb +147 -0
- data/lib/mutineer.rb +48 -0
- 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
|