evilution 0.28.0 → 0.29.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 +4 -4
- data/.beads/interactions.jsonl +52 -0
- data/CHANGELOG.md +7 -0
- data/lib/evilution/ast/constant_names.rb +28 -11
- data/lib/evilution/ast/pattern/parser.rb +29 -17
- data/lib/evilution/cli/commands/session_diff.rb +6 -4
- data/lib/evilution/cli/commands/subjects.rb +6 -3
- data/lib/evilution/cli/commands/util_mutation.rb +24 -19
- data/lib/evilution/cli/parser/command_extractor.rb +9 -11
- data/lib/evilution/cli/parser/file_args.rb +3 -1
- data/lib/evilution/cli/parser/options_builder.rb +29 -1
- data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
- data/lib/evilution/cli/parser.rb +18 -20
- data/lib/evilution/cli/printers/environment.rb +19 -19
- data/lib/evilution/cli/printers/session_diff.rb +8 -8
- data/lib/evilution/compare/normalizer.rb +10 -5
- data/lib/evilution/config.rb +10 -10
- data/lib/evilution/disable_comment.rb +21 -12
- data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
- data/lib/evilution/integration/minitest.rb +25 -16
- data/lib/evilution/integration/rspec.rb +4 -0
- data/lib/evilution/isolation/fork.rb +27 -17
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
- data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
- data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
- data/lib/evilution/mcp/info_tool.rb +7 -1
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
- data/lib/evilution/mcp/mutate_tool.rb +27 -14
- data/lib/evilution/mcp/session_tool.rb +27 -18
- data/lib/evilution/mutation.rb +13 -15
- data/lib/evilution/mutator/base.rb +17 -15
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
- data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
- data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
- data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
- data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
- data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
- data/lib/evilution/mutator/operator/case_when.rb +7 -5
- data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
- data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
- data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
- data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
- data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
- data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
- data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
- data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
- data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
- data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
- data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
- data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
- data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
- data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
- data/lib/evilution/parallel/work_queue/worker.rb +10 -7
- data/lib/evilution/parallel/work_queue.rb +35 -18
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
- data/lib/evilution/reporter/json.rb +52 -18
- data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
- data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +20 -14
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +19 -13
- data/lib/evilution/runner/baseline_runner.rb +15 -8
- data/lib/evilution/runner/diagnostics.rb +13 -9
- data/lib/evilution/runner/isolation_resolver.rb +11 -9
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -1
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +32 -10
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +1 -1
- data/lib/evilution/runner/mutation_executor.rb +2 -0
- data/lib/evilution/runner/mutation_planner.rb +37 -17
- data/lib/evilution/runner/subject_pipeline.rb +21 -11
- data/lib/evilution/runner.rb +3 -3
- data/lib/evilution/session/diff.rb +15 -6
- data/lib/evilution/spec_ast_cache.rb +26 -12
- data/lib/evilution/version.rb +1 -1
- data/script/memory_check +11 -5
- data/scripts/benchmark_density +10 -9
- data/scripts/compare_mutations +38 -21
- data/scripts/mutant_json_adapter +7 -4
- metadata +3 -2
|
@@ -13,27 +13,33 @@ class Evilution::Mutator::Operator::MixinRemoval < Evilution::Mutator::Base
|
|
|
13
13
|
@mutations = []
|
|
14
14
|
@filter = filter
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
enclosing = find_enclosing_scope(tree, subject.line_number)
|
|
16
|
+
enclosing = find_target_scope(subject)
|
|
18
17
|
return @mutations unless enclosing
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
return @mutations unless first_method_line == subject.line_number
|
|
22
|
-
|
|
23
|
-
find_mixin_calls(enclosing).each do |call_node|
|
|
24
|
-
add_mutation(
|
|
25
|
-
offset: call_node.location.start_offset,
|
|
26
|
-
length: call_node.location.length,
|
|
27
|
-
replacement: "",
|
|
28
|
-
node: call_node
|
|
29
|
-
)
|
|
30
|
-
end
|
|
31
|
-
|
|
19
|
+
find_mixin_calls(enclosing).each { |call_node| emit_mixin_removal(call_node) }
|
|
32
20
|
@mutations
|
|
33
21
|
end
|
|
34
22
|
|
|
35
23
|
private
|
|
36
24
|
|
|
25
|
+
def find_target_scope(subject)
|
|
26
|
+
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
27
|
+
enclosing = find_enclosing_scope(tree, subject.line_number)
|
|
28
|
+
return nil unless enclosing
|
|
29
|
+
return nil unless find_first_method_line(enclosing) == subject.line_number
|
|
30
|
+
|
|
31
|
+
enclosing
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def emit_mixin_removal(call_node)
|
|
35
|
+
add_mutation(
|
|
36
|
+
offset: call_node.location.start_offset,
|
|
37
|
+
length: call_node.location.length,
|
|
38
|
+
replacement: "",
|
|
39
|
+
node: call_node
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
37
43
|
def find_enclosing_scope(tree, target_line)
|
|
38
44
|
finder = ScopeFinder.new(target_line)
|
|
39
45
|
finder.visit(tree)
|
|
@@ -18,19 +18,18 @@ class Evilution::Mutator::Operator::MultipleAssignment < Evilution::Mutator::Bas
|
|
|
18
18
|
private
|
|
19
19
|
|
|
20
20
|
def mutate_target_removal(node, lefts, values)
|
|
21
|
-
lefts.each_index
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
21
|
+
lefts.each_index { |i| emit_target_removal_at(node, lefts, values, i) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def emit_target_removal_at(node, lefts, values, i)
|
|
25
|
+
remaining_lefts = lefts.each_with_index.filter_map { |l, j| l.slice if j != i }
|
|
26
|
+
remaining_values = values.each_with_index.filter_map { |v, j| v.slice if j != i }
|
|
27
|
+
add_mutation(
|
|
28
|
+
offset: node.location.start_offset,
|
|
29
|
+
length: node.location.length,
|
|
30
|
+
replacement: "#{remaining_lefts.join(", ")} = #{remaining_values.join(", ")}",
|
|
31
|
+
node: node
|
|
32
|
+
)
|
|
34
33
|
end
|
|
35
34
|
|
|
36
35
|
def mutate_swap(node, lefts, values)
|
|
@@ -5,19 +5,22 @@ require_relative "../operator"
|
|
|
5
5
|
class Evilution::Mutator::Operator::ReceiverReplacement < Evilution::Mutator::Base
|
|
6
6
|
def visit_call_node(node)
|
|
7
7
|
if node.receiver.is_a?(Prism::SelfNode)
|
|
8
|
-
call_without_self = @file_source.byteslice(
|
|
9
|
-
node.message_loc.start_offset,
|
|
10
|
-
node.location.start_offset + node.location.length - node.message_loc.start_offset
|
|
11
|
-
)
|
|
12
|
-
|
|
13
8
|
add_mutation(
|
|
14
9
|
offset: node.location.start_offset,
|
|
15
10
|
length: node.location.length,
|
|
16
|
-
replacement:
|
|
11
|
+
replacement: call_without_self_text(node),
|
|
17
12
|
node: node
|
|
18
13
|
)
|
|
19
14
|
end
|
|
20
15
|
|
|
21
16
|
super
|
|
22
17
|
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def call_without_self_text(node)
|
|
22
|
+
message_start = node.message_loc.start_offset
|
|
23
|
+
call_end = node.location.start_offset + node.location.length
|
|
24
|
+
@file_source.byteslice(message_start, call_end - message_start)
|
|
25
|
+
end
|
|
23
26
|
end
|
|
@@ -19,31 +19,21 @@ class Evilution::Mutator::Operator::RegexSimplification < Evilution::Mutator::Ba
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def remove_quantifiers(node, content, content_offset)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
scan_regex_positions(content) do |kind, i|
|
|
23
|
+
case kind
|
|
24
|
+
when :backslash then 2
|
|
25
|
+
when :class_open then class_skip(content, i)
|
|
26
|
+
when :char then emit_quantifier_at(node, content, content_offset, i)
|
|
27
27
|
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
31
|
+
def emit_quantifier_at(node, content, content_offset, i)
|
|
32
|
+
match = match_quantifier(content, i)
|
|
33
|
+
return 1 if match.nil?
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
add_mutation(
|
|
37
|
-
offset: content_offset + i,
|
|
38
|
-
length: match.length,
|
|
39
|
-
replacement: "",
|
|
40
|
-
node: node
|
|
41
|
-
)
|
|
42
|
-
i += match.length
|
|
43
|
-
else
|
|
44
|
-
i += 1
|
|
45
|
-
end
|
|
46
|
-
end
|
|
35
|
+
add_mutation(offset: content_offset + i, length: match.length, replacement: "", node: node)
|
|
36
|
+
match.length
|
|
47
37
|
end
|
|
48
38
|
|
|
49
39
|
def match_quantifier(content, pos)
|
|
@@ -58,40 +48,29 @@ class Evilution::Mutator::Operator::RegexSimplification < Evilution::Mutator::Ba
|
|
|
58
48
|
end
|
|
59
49
|
|
|
60
50
|
def remove_anchors(node, content, content_offset)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
length: anchor.length,
|
|
69
|
-
replacement: "",
|
|
70
|
-
node: node
|
|
71
|
-
)
|
|
72
|
-
i += anchor.length
|
|
73
|
-
else
|
|
74
|
-
i += 2
|
|
75
|
-
end
|
|
76
|
-
next
|
|
51
|
+
scan_regex_positions(content) do |kind, i|
|
|
52
|
+
case kind
|
|
53
|
+
when :backslash then try_emit_backslash_anchor(node, content, content_offset, i)
|
|
54
|
+
when :class_open then class_skip(content, i)
|
|
55
|
+
when :char
|
|
56
|
+
try_emit_caret_dollar(node, content, content_offset, i)
|
|
57
|
+
1
|
|
77
58
|
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
78
61
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
end
|
|
62
|
+
def try_emit_backslash_anchor(node, content, content_offset, i)
|
|
63
|
+
anchor = match_backslash_anchor(content, i)
|
|
64
|
+
return 2 if anchor.nil?
|
|
83
65
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
length: 1,
|
|
88
|
-
replacement: "",
|
|
89
|
-
node: node
|
|
90
|
-
)
|
|
91
|
-
end
|
|
66
|
+
add_mutation(offset: content_offset + i, length: anchor.length, replacement: "", node: node)
|
|
67
|
+
anchor.length
|
|
68
|
+
end
|
|
92
69
|
|
|
93
|
-
|
|
94
|
-
|
|
70
|
+
def try_emit_caret_dollar(node, content, content_offset, i)
|
|
71
|
+
return unless %w[^ $].include?(content[i])
|
|
72
|
+
|
|
73
|
+
add_mutation(offset: content_offset + i, length: 1, replacement: "", node: node)
|
|
95
74
|
end
|
|
96
75
|
|
|
97
76
|
def match_backslash_anchor(content, pos)
|
|
@@ -104,18 +83,13 @@ class Evilution::Mutator::Operator::RegexSimplification < Evilution::Mutator::Ba
|
|
|
104
83
|
end
|
|
105
84
|
|
|
106
85
|
def remove_character_class_ranges(node, content, content_offset)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
next
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
if content[i] == "["
|
|
86
|
+
scan_regex_positions(content) do |kind, i|
|
|
87
|
+
case kind
|
|
88
|
+
when :backslash then 2
|
|
89
|
+
when :class_open
|
|
115
90
|
scan_ranges_in_class(node, content, content_offset, i)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
i += 1
|
|
91
|
+
class_skip(content, i)
|
|
92
|
+
when :char then 1
|
|
119
93
|
end
|
|
120
94
|
end
|
|
121
95
|
end
|
|
@@ -153,17 +127,38 @@ class Evilution::Mutator::Operator::RegexSimplification < Evilution::Mutator::Ba
|
|
|
153
127
|
)
|
|
154
128
|
end
|
|
155
129
|
|
|
130
|
+
# Walks `content` yielding (kind, position) for each significant token:
|
|
131
|
+
# :backslash for an escape sequence, :class_open for `[`, :char for any
|
|
132
|
+
# other byte. The block returns the number of characters to advance from
|
|
133
|
+
# `position` — callers decide how to handle each case (skip, emit a
|
|
134
|
+
# mutation, descend into a character class, etc.).
|
|
135
|
+
def scan_regex_positions(content)
|
|
136
|
+
i = 0
|
|
137
|
+
while i < content.length
|
|
138
|
+
advance = case content[i]
|
|
139
|
+
when "\\" then yield(:backslash, i)
|
|
140
|
+
when "[" then yield(:class_open, i)
|
|
141
|
+
else yield(:char, i)
|
|
142
|
+
end
|
|
143
|
+
i += advance
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def class_skip(content, pos)
|
|
148
|
+
skip_character_class(content, pos) - pos
|
|
149
|
+
end
|
|
150
|
+
|
|
156
151
|
def skip_character_class(content, pos)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
i += 1 if i < content.length && content[i] == "]"
|
|
152
|
+
scan_to_class_close(content, skip_class_prefix(content, pos))
|
|
153
|
+
end
|
|
160
154
|
|
|
155
|
+
def scan_to_class_close(content, start)
|
|
156
|
+
i = start
|
|
161
157
|
while i < content.length
|
|
162
158
|
return i + 1 if content[i] == "]"
|
|
163
159
|
|
|
164
160
|
i += content[i] == "\\" ? 2 : 1
|
|
165
161
|
end
|
|
166
|
-
|
|
167
162
|
i
|
|
168
163
|
end
|
|
169
164
|
end
|
|
@@ -72,14 +72,15 @@ class Evilution::Mutator::Operator::RescueBodyReplacement < Evilution::Mutator::
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def rescue_line_end(node)
|
|
75
|
-
if node.reference
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
loc = if node.reference
|
|
76
|
+
node.reference.location
|
|
77
|
+
elsif node.exceptions.any?
|
|
78
|
+
node.exceptions.last.location
|
|
79
|
+
else
|
|
80
|
+
node.keyword_loc
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
loc.start_offset + loc.length
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
def indentation_of(offset)
|
|
@@ -20,13 +20,10 @@ class Evilution::Mutator::Operator::RescueRemoval < Evilution::Mutator::Base
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
22
|
def rescue_end_offset(node)
|
|
23
|
-
if node.subsequent
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
else
|
|
28
|
-
node.keyword_loc.start_offset + node.keyword_loc.length
|
|
29
|
-
end
|
|
23
|
+
return line_start_before(node.subsequent.keyword_loc.start_offset) if node.subsequent
|
|
24
|
+
|
|
25
|
+
loc = node.statements ? node.statements.location : node.keyword_loc
|
|
26
|
+
loc.start_offset + loc.length
|
|
30
27
|
end
|
|
31
28
|
|
|
32
29
|
def line_start_before(offset)
|
|
@@ -11,29 +11,35 @@ class Evilution::Mutator::Operator::SuperclassRemoval < Evilution::Mutator::Base
|
|
|
11
11
|
@mutations = []
|
|
12
12
|
@filter = filter
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
enclosing = find_enclosing_class(tree, subject.line_number)
|
|
14
|
+
enclosing = find_target_class(subject)
|
|
16
15
|
return @mutations unless enclosing
|
|
17
|
-
return @mutations unless enclosing.superclass
|
|
18
|
-
|
|
19
|
-
first_method_line = find_first_method_line(enclosing)
|
|
20
|
-
return @mutations unless first_method_line == subject.line_number
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
add_mutation(
|
|
26
|
-
offset: name_end,
|
|
27
|
-
length: superclass_end - name_end,
|
|
28
|
-
replacement: "",
|
|
29
|
-
node: enclosing
|
|
30
|
-
)
|
|
17
|
+
offset, length = superclass_range(enclosing)
|
|
18
|
+
add_mutation(offset: offset, length: length, replacement: "", node: enclosing)
|
|
31
19
|
|
|
32
20
|
@mutations
|
|
33
21
|
end
|
|
34
22
|
|
|
35
23
|
private
|
|
36
24
|
|
|
25
|
+
def find_target_class(subject)
|
|
26
|
+
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
27
|
+
enclosing = find_enclosing_class(tree, subject.line_number)
|
|
28
|
+
return nil unless enclosing && enclosing.superclass
|
|
29
|
+
return nil unless find_first_method_line(enclosing) == subject.line_number
|
|
30
|
+
|
|
31
|
+
enclosing
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def superclass_range(class_node)
|
|
35
|
+
name_loc = class_node.constant_path.location
|
|
36
|
+
superclass_loc = class_node.superclass.location
|
|
37
|
+
name_end = name_loc.start_offset + name_loc.length
|
|
38
|
+
superclass_end = superclass_loc.start_offset + superclass_loc.length
|
|
39
|
+
|
|
40
|
+
[name_end, superclass_end - name_end]
|
|
41
|
+
end
|
|
42
|
+
|
|
37
43
|
def find_enclosing_class(tree, target_line)
|
|
38
44
|
finder = ClassFinder.new(target_line)
|
|
39
45
|
finder.visit(tree)
|
|
@@ -4,6 +4,8 @@ require_relative "../work_queue"
|
|
|
4
4
|
require_relative "collection_state"
|
|
5
5
|
|
|
6
6
|
class Evilution::Parallel::WorkQueue::Dispatcher
|
|
7
|
+
RunResult = Data.define(:results, :retired)
|
|
8
|
+
|
|
7
9
|
attr_reader :first_error
|
|
8
10
|
|
|
9
11
|
def initialize(workers:, items:, prefetch:, item_timeout:, worker_max_items:, recycle_factory:)
|
|
@@ -21,7 +23,7 @@ class Evilution::Parallel::WorkQueue::Dispatcher
|
|
|
21
23
|
seed
|
|
22
24
|
collect
|
|
23
25
|
@first_error = @state.first_error
|
|
24
|
-
|
|
26
|
+
RunResult.new(results: @state.results, retired: @retired)
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
private
|
|
@@ -42,20 +44,25 @@ class Evilution::Parallel::WorkQueue::Dispatcher
|
|
|
42
44
|
|
|
43
45
|
while @state.in_flight.positive?
|
|
44
46
|
readable, = IO.select(result_ios, nil, nil, @item_timeout)
|
|
45
|
-
|
|
46
47
|
if readable.nil?
|
|
47
|
-
|
|
48
|
-
@state.first_error ||= Evilution::Error.new("worker timed out after #{@item_timeout}s")
|
|
48
|
+
record_timeout
|
|
49
49
|
break
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
readable.each
|
|
53
|
-
alive = handle(io_to_worker[io], io_to_worker, result_ios)
|
|
54
|
-
result_ios.delete(io) unless alive
|
|
55
|
-
end
|
|
52
|
+
readable.each { |io| process_readable(io, io_to_worker, result_ios) }
|
|
56
53
|
end
|
|
57
54
|
end
|
|
58
55
|
|
|
56
|
+
def record_timeout
|
|
57
|
+
terminate_stuck
|
|
58
|
+
@state.first_error ||= Evilution::Error.new("worker timed out after #{@item_timeout}s")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def process_readable(io, io_to_worker, result_ios)
|
|
62
|
+
alive = handle(io_to_worker[io], io_to_worker, result_ios)
|
|
63
|
+
result_ios.delete(io) unless alive
|
|
64
|
+
end
|
|
65
|
+
|
|
59
66
|
def handle(worker, io_to_worker, result_ios)
|
|
60
67
|
message = worker.read_result
|
|
61
68
|
return handle_dead(worker) if message.nil?
|
|
@@ -6,6 +6,8 @@ require_relative "channel"
|
|
|
6
6
|
require_relative "channel/frame"
|
|
7
7
|
|
|
8
8
|
class Evilution::Parallel::WorkQueue::Worker
|
|
9
|
+
Timing = Data.define(:busy, :wall)
|
|
10
|
+
|
|
9
11
|
attr_reader :pid, :worker_index
|
|
10
12
|
attr_accessor :items_completed, :pending, :busy_time, :wall_time
|
|
11
13
|
|
|
@@ -84,11 +86,11 @@ class Evilution::Parallel::WorkQueue::Worker
|
|
|
84
86
|
|
|
85
87
|
def retire
|
|
86
88
|
shutdown
|
|
87
|
-
|
|
89
|
+
timing = drain_stats
|
|
88
90
|
close_pipes
|
|
89
91
|
reap
|
|
90
|
-
@busy_time = busy
|
|
91
|
-
@wall_time = wall
|
|
92
|
+
@busy_time = timing.busy
|
|
93
|
+
@wall_time = timing.wall
|
|
92
94
|
to_stat
|
|
93
95
|
end
|
|
94
96
|
|
|
@@ -101,14 +103,15 @@ class Evilution::Parallel::WorkQueue::Worker
|
|
|
101
103
|
private
|
|
102
104
|
|
|
103
105
|
def drain_stats
|
|
104
|
-
|
|
106
|
+
zero = Timing.new(busy: 0.0, wall: 0.0)
|
|
107
|
+
return zero unless @res_read.wait_readable(Evilution::Parallel::WorkQueue::TIMING_GRACE_PERIOD)
|
|
105
108
|
|
|
106
109
|
message = read_result
|
|
107
|
-
return
|
|
110
|
+
return zero if message.nil?
|
|
108
111
|
|
|
109
112
|
tag, busy, wall = message
|
|
110
|
-
return
|
|
113
|
+
return zero unless tag == Evilution::Parallel::WorkQueue::STATS
|
|
111
114
|
|
|
112
|
-
|
|
115
|
+
Timing.new(busy: busy, wall: wall)
|
|
113
116
|
end
|
|
114
117
|
end
|
|
@@ -27,24 +27,17 @@ class Evilution::Parallel::WorkQueue
|
|
|
27
27
|
return [] if items.empty?
|
|
28
28
|
|
|
29
29
|
workers = (0...[@size, items.length].min).map { |i| spawn_one(i, &block) }
|
|
30
|
-
dispatcher =
|
|
31
|
-
workers: workers, items: items, prefetch: @prefetch,
|
|
32
|
-
item_timeout: @item_timeout, worker_max_items: @worker_max_items,
|
|
33
|
-
recycle_factory: ->(old) { spawn_one(old.worker_index, &block) }
|
|
34
|
-
)
|
|
30
|
+
dispatcher = build_dispatcher(workers, items, &block)
|
|
35
31
|
|
|
36
32
|
retired = []
|
|
37
33
|
begin
|
|
38
|
-
|
|
34
|
+
run_result = dispatcher.run
|
|
35
|
+
retired = run_result.retired
|
|
39
36
|
raise dispatcher.first_error if dispatcher.first_error
|
|
40
37
|
|
|
41
|
-
results
|
|
38
|
+
run_result.results
|
|
42
39
|
ensure
|
|
43
|
-
workers
|
|
44
|
-
collect_final_timings(workers)
|
|
45
|
-
workers.each(&:close_pipes)
|
|
46
|
-
workers.each(&:reap)
|
|
47
|
-
@worker_stats = retired + workers.map(&:to_stat)
|
|
40
|
+
cleanup_workers(workers, retired)
|
|
48
41
|
end
|
|
49
42
|
end
|
|
50
43
|
|
|
@@ -58,19 +51,43 @@ class Evilution::Parallel::WorkQueue
|
|
|
58
51
|
Worker.spawn(worker_index: worker_index, hooks: @hooks, &)
|
|
59
52
|
end
|
|
60
53
|
|
|
54
|
+
def build_dispatcher(workers, items, &block)
|
|
55
|
+
Dispatcher.new(
|
|
56
|
+
workers: workers, items: items, prefetch: @prefetch,
|
|
57
|
+
item_timeout: @item_timeout, worker_max_items: @worker_max_items,
|
|
58
|
+
recycle_factory: ->(old) { spawn_one(old.worker_index, &block) }
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def cleanup_workers(workers, retired)
|
|
63
|
+
workers.each(&:shutdown)
|
|
64
|
+
collect_final_timings(workers)
|
|
65
|
+
workers.each(&:close_pipes)
|
|
66
|
+
workers.each(&:reap)
|
|
67
|
+
@worker_stats = retired + workers.map(&:to_stat)
|
|
68
|
+
end
|
|
69
|
+
|
|
61
70
|
def collect_final_timings(workers)
|
|
62
71
|
io_to_worker = workers.reject { |w| w.res_io.closed? }.to_h { |w| [w.res_io, w] }
|
|
63
|
-
deadline =
|
|
72
|
+
deadline = monotonic_now + TIMING_GRACE_PERIOD
|
|
64
73
|
|
|
65
74
|
until io_to_worker.empty?
|
|
66
|
-
remaining = deadline -
|
|
75
|
+
remaining = deadline - monotonic_now
|
|
67
76
|
break if remaining <= 0
|
|
77
|
+
break unless poll_and_apply(io_to_worker, remaining)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
def poll_and_apply(io_to_worker, remaining)
|
|
82
|
+
readable, = IO.select(io_to_worker.keys, nil, nil, remaining)
|
|
83
|
+
return false unless readable
|
|
71
84
|
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
readable.each { |io| apply_final_timing(io_to_worker.delete(io), io) }
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def monotonic_now
|
|
90
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
74
91
|
end
|
|
75
92
|
|
|
76
93
|
def apply_final_timing(worker, io)
|
|
@@ -5,14 +5,19 @@ require_relative "../item_formatters"
|
|
|
5
5
|
class Evilution::Reporter::CLI::ItemFormatters::CoverageGap
|
|
6
6
|
def format(gap)
|
|
7
7
|
location = "#{gap.file_path}:#{gap.line}"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
"#{format_header(gap, location)}\n#{format_body(gap)}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def format_header(gap, location)
|
|
14
|
+
return " #{gap.primary_operator}: #{location} (#{gap.subject_name})" if gap.single?
|
|
15
|
+
|
|
16
|
+
" #{location} (#{gap.subject_name}) [#{gap.count} mutations: #{gap.operator_names.join(", ")}]"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def format_body(gap)
|
|
14
20
|
body = gap.mutation_results.first.mutation.unified_diff || gap.primary_diff
|
|
15
|
-
|
|
16
|
-
"#{header}\n#{indented}"
|
|
21
|
+
body.split("\n").map { |l| " #{l}" }.join("\n")
|
|
17
22
|
end
|
|
18
23
|
end
|
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
require_relative "../line_formatters"
|
|
4
4
|
|
|
5
5
|
class Evilution::Reporter::CLI::LineFormatters::Mutations
|
|
6
|
+
OPTIONAL_FIELDS = %i[neutral equivalent unresolved unparseable skipped].freeze
|
|
7
|
+
|
|
6
8
|
def format(summary)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
base_line(summary) + optional_sections(summary)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def base_line(summary)
|
|
15
|
+
"Mutations: #{summary.total} total, #{summary.killed} killed, " \
|
|
16
|
+
"#{summary.survived} survived, #{summary.timed_out} timed out"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def optional_sections(summary)
|
|
20
|
+
OPTIONAL_FIELDS.filter_map do |field|
|
|
21
|
+
count = summary.public_send(field)
|
|
22
|
+
", #{count} #{field}" if count.positive?
|
|
23
|
+
end.join
|
|
15
24
|
end
|
|
16
25
|
end
|