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,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Negation-removal mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Removes the `!` or `not` of a negation, one mutation per occurrence:
|
|
10
|
+
# `!x` becomes `x` and `not x` becomes ` x`. The mutant survives when no
|
|
11
|
+
# test depends on the negated value.
|
|
12
|
+
class NegationRemoval < Base
|
|
13
|
+
# Visits call nodes and emits negation-removal mutations.
|
|
14
|
+
#
|
|
15
|
+
# @param node [Prism::CallNode] call node to inspect.
|
|
16
|
+
# @return [void]
|
|
17
|
+
def visit_call_node(node)
|
|
18
|
+
emit(node)
|
|
19
|
+
super # nested negations (!!x) each get their own mutation
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Emits a mutation when the node is a prefix `!` or `not`.
|
|
25
|
+
#
|
|
26
|
+
# The explicit form `x.!` is skipped: without its message, `x.` does
|
|
27
|
+
# not parse.
|
|
28
|
+
#
|
|
29
|
+
# @param node [Prism::CallNode] call node to inspect.
|
|
30
|
+
# @return [void]
|
|
31
|
+
def emit(node)
|
|
32
|
+
return unless node.name == :! && node.call_operator_loc.nil?
|
|
33
|
+
|
|
34
|
+
loc = node.message_loc
|
|
35
|
+
@mutations << Mutation.new(
|
|
36
|
+
start_offset: loc.start_offset,
|
|
37
|
+
end_offset: loc.end_offset,
|
|
38
|
+
replacement: "",
|
|
39
|
+
operator: :negation_removal
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Operand-removal mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Replaces a boolean expression with one of its operands, two mutations
|
|
10
|
+
# per `&&`, `||`, `and` or `or`: `a && b` becomes `(a)` and `(b)`. The
|
|
11
|
+
# parentheses keep the precedence of `and` and `or`. A surviving mutant
|
|
12
|
+
# shows that no test needs the operand that was removed.
|
|
13
|
+
class OperandRemoval < Base
|
|
14
|
+
# Node types for jumps. A jump in a value context does not parse, so
|
|
15
|
+
# the operator never keeps a jump alone.
|
|
16
|
+
JUMPS = [Prism::ReturnNode, Prism::BreakNode, Prism::NextNode, Prism::RedoNode, Prism::RetryNode].freeze
|
|
17
|
+
|
|
18
|
+
# Visits `and` nodes.
|
|
19
|
+
#
|
|
20
|
+
# @param node [Prism::AndNode] node to inspect.
|
|
21
|
+
# @return [void]
|
|
22
|
+
def visit_and_node(node)
|
|
23
|
+
emit(node)
|
|
24
|
+
super # nested connectors (a && b && c) each get their own mutations
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Visits `or` nodes.
|
|
28
|
+
#
|
|
29
|
+
# @param node [Prism::OrNode] node to inspect.
|
|
30
|
+
# @return [void]
|
|
31
|
+
def visit_or_node(node)
|
|
32
|
+
emit(node)
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Emits one mutation that keeps the left operand, then one that keeps
|
|
39
|
+
# the right operand. The order is fixed, because the two mutants share
|
|
40
|
+
# a token and their occurrence ordinals tell them apart.
|
|
41
|
+
#
|
|
42
|
+
# @param node [Prism::AndNode, Prism::OrNode] connector node.
|
|
43
|
+
# @return [void]
|
|
44
|
+
def emit(node)
|
|
45
|
+
[[node.left, node.right], [node.right, node.left]].each do |kept, removed|
|
|
46
|
+
next if JUMPS.any? { |type| kept.is_a?(type) } || heredoc?(removed)
|
|
47
|
+
|
|
48
|
+
@mutations << Mutation.new(
|
|
49
|
+
start_offset: node.location.start_offset,
|
|
50
|
+
end_offset: node.location.end_offset,
|
|
51
|
+
replacement: "(#{kept.slice})",
|
|
52
|
+
operator: :operand_removal
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Operator-assignment mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Swaps the operator of a compound assignment, one mutation per
|
|
10
|
+
# occurrence: `t += x` -> `t -= x`. Prism parses `t += x` as its own
|
|
11
|
+
# node, not as a CallNode, so the arithmetic mutator never sees it.
|
|
12
|
+
#
|
|
13
|
+
# The mapping is the same as for arithmetic. Other compound operators
|
|
14
|
+
# (`||=`, `&&=`, `|=`, `<<=` and the rest) are not changed.
|
|
15
|
+
class OperatorAssignment < Base
|
|
16
|
+
# Token replacements for compound arithmetic operators.
|
|
17
|
+
REPLACEMENTS = {
|
|
18
|
+
:+ => "-=", :- => "+=", :* => "/=", :/ => "*=", :% => "*=", :** => "*="
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Visits `t += x`.
|
|
22
|
+
#
|
|
23
|
+
# @param node [Prism::LocalVariableOperatorWriteNode] node to inspect.
|
|
24
|
+
# @return [void]
|
|
25
|
+
def visit_local_variable_operator_write_node(node)
|
|
26
|
+
emit(node)
|
|
27
|
+
super # the value can hold its own compound assignment
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Visits `@t += x`.
|
|
31
|
+
#
|
|
32
|
+
# @param node [Prism::InstanceVariableOperatorWriteNode] node to inspect.
|
|
33
|
+
# @return [void]
|
|
34
|
+
def visit_instance_variable_operator_write_node(node)
|
|
35
|
+
emit(node)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Visits `@@t += x`.
|
|
40
|
+
#
|
|
41
|
+
# @param node [Prism::ClassVariableOperatorWriteNode] node to inspect.
|
|
42
|
+
# @return [void]
|
|
43
|
+
def visit_class_variable_operator_write_node(node)
|
|
44
|
+
emit(node)
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Visits `$t += x`.
|
|
49
|
+
#
|
|
50
|
+
# @param node [Prism::GlobalVariableOperatorWriteNode] node to inspect.
|
|
51
|
+
# @return [void]
|
|
52
|
+
def visit_global_variable_operator_write_node(node)
|
|
53
|
+
emit(node)
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Visits `T += x`.
|
|
58
|
+
#
|
|
59
|
+
# @param node [Prism::ConstantOperatorWriteNode] node to inspect.
|
|
60
|
+
# @return [void]
|
|
61
|
+
def visit_constant_operator_write_node(node)
|
|
62
|
+
emit(node)
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Visits `A::T += x` and `::T += x`.
|
|
67
|
+
#
|
|
68
|
+
# @param node [Prism::ConstantPathOperatorWriteNode] node to inspect.
|
|
69
|
+
# @return [void]
|
|
70
|
+
def visit_constant_path_operator_write_node(node)
|
|
71
|
+
emit(node)
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Visits `a.b += x` and `a&.b += x`.
|
|
76
|
+
#
|
|
77
|
+
# @param node [Prism::CallOperatorWriteNode] node to inspect.
|
|
78
|
+
# @return [void]
|
|
79
|
+
def visit_call_operator_write_node(node)
|
|
80
|
+
emit(node)
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Visits `a[i] += x`.
|
|
85
|
+
#
|
|
86
|
+
# @param node [Prism::IndexOperatorWriteNode] node to inspect.
|
|
87
|
+
# @return [void]
|
|
88
|
+
def visit_index_operator_write_node(node)
|
|
89
|
+
emit(node)
|
|
90
|
+
super
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Emits a mutation when the node's operator has a replacement.
|
|
96
|
+
#
|
|
97
|
+
# @param node [Prism::Node] a node with `binary_operator` and `binary_operator_loc`.
|
|
98
|
+
# @return [void]
|
|
99
|
+
def emit(node)
|
|
100
|
+
replacement = REPLACEMENTS[node.binary_operator]
|
|
101
|
+
return unless replacement
|
|
102
|
+
|
|
103
|
+
loc = node.binary_operator_loc
|
|
104
|
+
@mutations << Mutation.new(
|
|
105
|
+
start_offset: loc.start_offset,
|
|
106
|
+
end_offset: loc.end_offset,
|
|
107
|
+
replacement: replacement,
|
|
108
|
+
operator: :operator_assignment
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Range mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Swaps `..` with `...` and `...` with `..`, one mutation per range. The
|
|
10
|
+
# `..` -> `...` mutant survives when no test checks the last element.
|
|
11
|
+
# Flip-flops are FlipFlopNode, not RangeNode, so this mutator skips them.
|
|
12
|
+
#
|
|
13
|
+
# Endless ranges (`1..`, `1..nil`) are skipped. `(1..)` and `(1...)` give
|
|
14
|
+
# the same result for slicing, `include?`, `===`, `size` and patterns, so
|
|
15
|
+
# the mutant is equivalent and no normal test can kill it.
|
|
16
|
+
class RangeLiteral < Base
|
|
17
|
+
# Maps each range operator to its opposite.
|
|
18
|
+
SWAPS = { ".." => "...", "..." => ".." }.freeze
|
|
19
|
+
|
|
20
|
+
# Visits range nodes and emits range mutations.
|
|
21
|
+
#
|
|
22
|
+
# @param node [Prism::RangeNode] range node to inspect.
|
|
23
|
+
# @return [void]
|
|
24
|
+
def visit_range_node(node)
|
|
25
|
+
emit(node) unless endless?(node)
|
|
26
|
+
super # nested ranges ((1..2)...(3..4)) each get their own mutation
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Whether the range has no end: `1..` or `1..nil`.
|
|
32
|
+
#
|
|
33
|
+
# @param node [Prism::RangeNode] range node to inspect.
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def endless?(node)
|
|
36
|
+
node.right.nil? || node.right.is_a?(Prism::NilNode)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Emits the mutation that swaps the range operator.
|
|
40
|
+
#
|
|
41
|
+
# @param node [Prism::RangeNode] range node to mutate.
|
|
42
|
+
# @return [void]
|
|
43
|
+
def emit(node)
|
|
44
|
+
loc = node.operator_loc
|
|
45
|
+
@mutations << Mutation.new(
|
|
46
|
+
start_offset: loc.start_offset,
|
|
47
|
+
end_offset: loc.end_offset,
|
|
48
|
+
replacement: SWAPS.fetch(loc.slice),
|
|
49
|
+
operator: :range
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Regular-expression literal mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Conservative, clearly-safe textual swaps inside a plain regex literal:
|
|
10
|
+
# drop a leading +^+ anchor, drop a trailing +$+ anchor, and swap the +
|
|
11
|
+
# and * quantifiers. Interpolated regexes use a different node type and are
|
|
12
|
+
# never visited. Escaped tokens (preceded by a backslash) are left alone.
|
|
13
|
+
class RegexLiteral < Base
|
|
14
|
+
# Visits regular-expression literals.
|
|
15
|
+
#
|
|
16
|
+
# @param node [Prism::RegularExpressionNode] node to inspect.
|
|
17
|
+
# @return [void]
|
|
18
|
+
def visit_regular_expression_node(node)
|
|
19
|
+
loc = node.content_loc
|
|
20
|
+
content = @source.byteslice(loc.start_offset...loc.end_offset)
|
|
21
|
+
scan(content, loc.start_offset)
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Scans the pattern bytes and emits anchor/quantifier mutations.
|
|
28
|
+
#
|
|
29
|
+
# @param content [String] regex pattern between the slashes.
|
|
30
|
+
# @param base [Integer] byte offset of the pattern start in source.
|
|
31
|
+
# @return [void]
|
|
32
|
+
def scan(content, base)
|
|
33
|
+
emit(base, base + 1, "") if content.start_with?("^")
|
|
34
|
+
emit(base + content.bytesize - 1, base + content.bytesize, "") if trailing_anchor?(content)
|
|
35
|
+
|
|
36
|
+
escaped = false
|
|
37
|
+
offset = base
|
|
38
|
+
content.each_char do |ch|
|
|
39
|
+
unless escaped
|
|
40
|
+
emit(offset, offset + 1, "*") if ch == "+"
|
|
41
|
+
emit(offset, offset + 1, "+") if ch == "*"
|
|
42
|
+
end
|
|
43
|
+
escaped = ch == "\\" && !escaped
|
|
44
|
+
offset += ch.bytesize
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether the pattern ends with an unescaped +$+ anchor.
|
|
49
|
+
#
|
|
50
|
+
# @param content [String] regex pattern.
|
|
51
|
+
# @return [Boolean] true when a trailing anchor is droppable.
|
|
52
|
+
def trailing_anchor?(content)
|
|
53
|
+
content.end_with?("$") && !content.end_with?("\\$")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Emits a regex mutation.
|
|
57
|
+
#
|
|
58
|
+
# @param start_offset [Integer] byte start.
|
|
59
|
+
# @param end_offset [Integer] byte end.
|
|
60
|
+
# @param replacement [String] replacement text.
|
|
61
|
+
# @return [void]
|
|
62
|
+
def emit(start_offset, end_offset, replacement)
|
|
63
|
+
@mutations << Mutation.new(
|
|
64
|
+
start_offset: start_offset,
|
|
65
|
+
end_offset: end_offset,
|
|
66
|
+
replacement: replacement,
|
|
67
|
+
operator: :regex
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Return-nil mutator.
|
|
8
|
+
#
|
|
9
|
+
# Replaces explicit return values and final expressions with nil.
|
|
10
|
+
class ReturnNil < Base
|
|
11
|
+
# Collects return-nil mutations for a subject.
|
|
12
|
+
#
|
|
13
|
+
# @param subject [Mutineer::Subject] subject to inspect.
|
|
14
|
+
# @param source [String] full source text.
|
|
15
|
+
# @return [Array<Mutineer::Mutation>] collected mutations.
|
|
16
|
+
def mutations_for(subject, source)
|
|
17
|
+
@source = source
|
|
18
|
+
@mutations = []
|
|
19
|
+
@body = subject.def_node.body
|
|
20
|
+
if @body
|
|
21
|
+
@body.accept(self) # rule 1 (explicit return nodes in this body)
|
|
22
|
+
final_expression_nil(@body) # rule 2 (method's final expression)
|
|
23
|
+
end
|
|
24
|
+
drop_dangling_heredocs
|
|
25
|
+
@mutations
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Visits return nodes.
|
|
29
|
+
#
|
|
30
|
+
# @param node [Prism::ReturnNode] node to inspect.
|
|
31
|
+
# @return [void]
|
|
32
|
+
def visit_return_node(node)
|
|
33
|
+
args = node.arguments
|
|
34
|
+
if args
|
|
35
|
+
values = args.arguments
|
|
36
|
+
unless values.size == 1 && values.first.is_a?(Prism::NilNode)
|
|
37
|
+
emit(args.location)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Mutates a method's final expression to nil when eligible.
|
|
46
|
+
#
|
|
47
|
+
# @param body [Prism::Node] method body node.
|
|
48
|
+
# @return [void]
|
|
49
|
+
def final_expression_nil(body)
|
|
50
|
+
return unless body.is_a?(Prism::StatementsNode)
|
|
51
|
+
|
|
52
|
+
last = body.body.last
|
|
53
|
+
return if last.nil? || last.is_a?(Prism::ReturnNode) || last.is_a?(Prism::NilNode)
|
|
54
|
+
|
|
55
|
+
emit(last.location)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Emits a return-nil mutation.
|
|
59
|
+
#
|
|
60
|
+
# @param loc [Prism::Location] source location.
|
|
61
|
+
# @return [void]
|
|
62
|
+
def emit(loc)
|
|
63
|
+
@mutations << Mutation.new(
|
|
64
|
+
start_offset: loc.start_offset,
|
|
65
|
+
end_offset: loc.end_offset,
|
|
66
|
+
replacement: "nil",
|
|
67
|
+
operator: :return_nil
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Safe-navigation mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Replaces `&.` with `.`, one mutation per occurrence. The mutant raises
|
|
10
|
+
# NoMethodError on a nil receiver, so it survives when no test passes nil.
|
|
11
|
+
class SafeNavigation < Base
|
|
12
|
+
# Visits call nodes and emits safe-navigation mutations.
|
|
13
|
+
#
|
|
14
|
+
# @param node [Prism::CallNode] call node to inspect.
|
|
15
|
+
# @return [void]
|
|
16
|
+
def visit_call_node(node)
|
|
17
|
+
emit(node)
|
|
18
|
+
super # chained calls (a&.b&.c) each get their own mutation
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Visits `a&.b += 1`, which Prism parses as its own node, not a CallNode.
|
|
22
|
+
#
|
|
23
|
+
# @param node [Prism::CallOperatorWriteNode] node to inspect.
|
|
24
|
+
# @return [void]
|
|
25
|
+
def visit_call_operator_write_node(node)
|
|
26
|
+
emit(node)
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Visits `a&.b ||= 1`.
|
|
31
|
+
#
|
|
32
|
+
# @param node [Prism::CallOrWriteNode] node to inspect.
|
|
33
|
+
# @return [void]
|
|
34
|
+
def visit_call_or_write_node(node)
|
|
35
|
+
emit(node)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Visits `a&.b &&= 1`.
|
|
40
|
+
#
|
|
41
|
+
# @param node [Prism::CallAndWriteNode] node to inspect.
|
|
42
|
+
# @return [void]
|
|
43
|
+
def visit_call_and_write_node(node)
|
|
44
|
+
emit(node)
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Visits a call used as an assignment target, as in `for a&.b in list`.
|
|
49
|
+
#
|
|
50
|
+
# @param node [Prism::CallTargetNode] node to inspect.
|
|
51
|
+
# @return [void]
|
|
52
|
+
def visit_call_target_node(node)
|
|
53
|
+
emit(node)
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Emits a mutation when the node's call operator is `&.`.
|
|
60
|
+
#
|
|
61
|
+
# @param node [Prism::Node] a node with `safe_navigation?` and `call_operator_loc`.
|
|
62
|
+
# @return [void]
|
|
63
|
+
def emit(node)
|
|
64
|
+
return unless node.safe_navigation?
|
|
65
|
+
|
|
66
|
+
loc = node.call_operator_loc
|
|
67
|
+
@mutations << Mutation.new(
|
|
68
|
+
start_offset: loc.start_offset,
|
|
69
|
+
end_offset: loc.end_offset,
|
|
70
|
+
replacement: ".",
|
|
71
|
+
operator: :safe_navigation
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Statement-removal mutator.
|
|
8
|
+
#
|
|
9
|
+
# Replaces each non-final method statement with `nil`.
|
|
10
|
+
class StatementRemoval < Base
|
|
11
|
+
# Visits statement nodes and emits removals.
|
|
12
|
+
#
|
|
13
|
+
# @param node [Prism::StatementsNode] statement list to inspect.
|
|
14
|
+
# @return [void]
|
|
15
|
+
def visit_statements_node(node)
|
|
16
|
+
stmts = node.body
|
|
17
|
+
if stmts.length >= 2
|
|
18
|
+
stmts[0...-1].each do |stmt|
|
|
19
|
+
loc = stmt.location
|
|
20
|
+
@mutations << Mutation.new(
|
|
21
|
+
start_offset: loc.start_offset,
|
|
22
|
+
end_offset: loc.end_offset,
|
|
23
|
+
replacement: "nil",
|
|
24
|
+
operator: :statement_removal
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
# Nested statement lists (if/begin/while bodies) are separate StatementsNodes.
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# String literal mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Empties a non-empty string literal and fills an empty one. Only plain
|
|
10
|
+
# quoted strings are touched; interpolated strings, heredocs and %-literals
|
|
11
|
+
# are skipped for safety (they re-parse unpredictably).
|
|
12
|
+
class StringLiteral < Base
|
|
13
|
+
# Visits string literals.
|
|
14
|
+
#
|
|
15
|
+
# @param node [Prism::StringNode] node to inspect.
|
|
16
|
+
# @return [void]
|
|
17
|
+
def visit_string_node(node)
|
|
18
|
+
# Only plain "..." / '...' quotes. opening is nil for
|
|
19
|
+
# interpolation parts and %w[] elements; heredocs/%-literals use a
|
|
20
|
+
# different opening token. Skipping them keeps mutants re-parseable.
|
|
21
|
+
if %w[" '].include?(node.opening)
|
|
22
|
+
loc = node.content_loc
|
|
23
|
+
@mutations << Mutation.new(
|
|
24
|
+
start_offset: loc.start_offset,
|
|
25
|
+
end_offset: loc.end_offset,
|
|
26
|
+
replacement: node.unescaped.empty? ? "mutineer" : "",
|
|
27
|
+
operator: :string_literal
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutineer
|
|
4
|
+
# Ends a forked child's process group once the process that forked it is
|
|
5
|
+
# gone (#101). A mutant or capture child leads its own process group, so a
|
|
6
|
+
# parent that dies, or is killed by a client that gave up on it, does not
|
|
7
|
+
# take the child's tests with it; without this, a hung test would run on and
|
|
8
|
+
# keep its worker database open. Needs no other part of Mutineer, so the
|
|
9
|
+
# app-side daemon can load it.
|
|
10
|
+
module OrphanGuard
|
|
11
|
+
# Seconds between checks. A forked child gets no signal on parent death.
|
|
12
|
+
POLL = 0.5
|
|
13
|
+
|
|
14
|
+
# Starts the watchdog in the current (child) process.
|
|
15
|
+
#
|
|
16
|
+
# @param parent [Integer] the parent's pid, read in the parent before
|
|
17
|
+
# `fork`, so a parent that dies before this call is still noticed.
|
|
18
|
+
# @return [Thread]
|
|
19
|
+
def self.start(parent)
|
|
20
|
+
Thread.new do
|
|
21
|
+
sleep(POLL) while Process.ppid == parent
|
|
22
|
+
# Group 0 only when this child leads its own group: a failed setpgid
|
|
23
|
+
# leaves it in the parent's group, which must not be killed.
|
|
24
|
+
Process.kill(:KILL, Process.getpgrp == Process.pid ? 0 : Process.pid)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|