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
|
@@ -22,18 +22,33 @@ class Evilution::Reporter::JSON
|
|
|
22
22
|
version: Evilution::VERSION,
|
|
23
23
|
timestamp: Time.now.iso8601,
|
|
24
24
|
summary: build_summary(summary),
|
|
25
|
-
survived: map_details(summary.survived_results),
|
|
26
25
|
coverage_gaps: build_coverage_gaps(summary),
|
|
26
|
+
**result_categories(summary)
|
|
27
|
+
}
|
|
28
|
+
append_disabled_to_report(report, summary)
|
|
29
|
+
report
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def result_categories(summary)
|
|
33
|
+
direct_categories(summary).merge(derived_categories(summary))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def direct_categories(summary)
|
|
37
|
+
{
|
|
38
|
+
survived: map_details(summary.survived_results),
|
|
27
39
|
killed: map_details(summary.killed_results),
|
|
28
40
|
neutral: map_details(summary.neutral_results),
|
|
29
|
-
timed_out: map_details(summary.results.select(&:timeout?)),
|
|
30
|
-
errors: map_details(summary.results.select(&:error?)),
|
|
31
41
|
equivalent: map_details(summary.equivalent_results),
|
|
32
42
|
unresolved: map_details(summary.unresolved_results),
|
|
33
43
|
unparseable: map_details(summary.unparseable_results)
|
|
34
44
|
}
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def derived_categories(summary)
|
|
48
|
+
{
|
|
49
|
+
timed_out: map_details(summary.results.select(&:timeout?)),
|
|
50
|
+
errors: map_details(summary.results.select(&:error?))
|
|
51
|
+
}
|
|
37
52
|
end
|
|
38
53
|
|
|
39
54
|
def map_details(results)
|
|
@@ -47,7 +62,13 @@ class Evilution::Reporter::JSON
|
|
|
47
62
|
end
|
|
48
63
|
|
|
49
64
|
def build_summary(summary)
|
|
50
|
-
data =
|
|
65
|
+
data = build_core_summary(summary).merge(build_metrics_summary(summary))
|
|
66
|
+
append_optional_summary_fields(data, summary)
|
|
67
|
+
data
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_core_summary(summary)
|
|
71
|
+
{
|
|
51
72
|
total: summary.total,
|
|
52
73
|
killed: summary.killed,
|
|
53
74
|
survived: summary.survived,
|
|
@@ -56,23 +77,39 @@ class Evilution::Reporter::JSON
|
|
|
56
77
|
neutral: summary.neutral,
|
|
57
78
|
equivalent: summary.equivalent,
|
|
58
79
|
unresolved: summary.unresolved,
|
|
59
|
-
unparseable: summary.unparseable
|
|
80
|
+
unparseable: summary.unparseable
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_metrics_summary(summary)
|
|
85
|
+
{
|
|
60
86
|
score: summary.score.round(4),
|
|
61
87
|
duration: summary.duration.round(4),
|
|
62
88
|
killtime: summary.killtime.round(4),
|
|
63
89
|
efficiency: summary.efficiency.round(4),
|
|
64
90
|
mutations_per_second: summary.mutations_per_second.round(2)
|
|
65
91
|
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def append_optional_summary_fields(data, summary)
|
|
66
95
|
data[:truncated] = true if summary.truncated?
|
|
67
96
|
data[:skipped] = summary.skipped if summary.skipped.positive?
|
|
68
97
|
peak = summary.peak_memory_mb
|
|
69
98
|
data[:peak_memory_mb] = peak.round(1) if peak
|
|
70
|
-
data
|
|
71
99
|
end
|
|
72
100
|
|
|
73
101
|
def build_mutation_detail(result)
|
|
74
102
|
mutation = result.mutation
|
|
75
|
-
detail =
|
|
103
|
+
detail = base_mutation_fields(mutation, result)
|
|
104
|
+
append_survived_fields(detail, mutation) if result.status == :survived
|
|
105
|
+
detail[:test_command] = result.test_command if result.test_command
|
|
106
|
+
append_memory_fields(detail, result)
|
|
107
|
+
append_error_fields(detail, result)
|
|
108
|
+
detail
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def base_mutation_fields(mutation, result)
|
|
112
|
+
{
|
|
76
113
|
operator: mutation.operator_name,
|
|
77
114
|
file: mutation.file_path,
|
|
78
115
|
line: mutation.line,
|
|
@@ -80,15 +117,12 @@ class Evilution::Reporter::JSON
|
|
|
80
117
|
duration: result.duration.round(4),
|
|
81
118
|
diff: mutation.diff
|
|
82
119
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
detail[:
|
|
89
|
-
append_memory_fields(detail, result)
|
|
90
|
-
append_error_fields(detail, result)
|
|
91
|
-
detail
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def append_survived_fields(detail, mutation)
|
|
123
|
+
detail[:suggestion] = @suggestion.suggestion_for(mutation)
|
|
124
|
+
unified = mutation.unified_diff
|
|
125
|
+
detail[:unified_diff] = unified if unified
|
|
92
126
|
end
|
|
93
127
|
|
|
94
128
|
def append_memory_fields(detail, result)
|
|
@@ -12,17 +12,4 @@ module Evilution::Reporter::Suggestion::DiffHelpers
|
|
|
12
12
|
def sanitize_method_name(name)
|
|
13
13
|
name.gsub(/[^a-zA-Z0-9_]/, "_").gsub(/_+/, "_").gsub(/\A_|_\z/, "")
|
|
14
14
|
end
|
|
15
|
-
|
|
16
|
-
def extract_diff_lines(diff)
|
|
17
|
-
lines = diff.split("\n")
|
|
18
|
-
original = lines.find { |l| l.start_with?("- ") }
|
|
19
|
-
mutated = lines.find { |l| l.start_with?("+ ") }
|
|
20
|
-
[clean_diff_line(original, "- "), clean_diff_line(mutated, "+ ")]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def clean_diff_line(line, prefix)
|
|
24
|
-
return nil if line.nil?
|
|
25
|
-
|
|
26
|
-
line.sub(/^#{Regexp.escape(prefix)}/, "").strip
|
|
27
|
-
end
|
|
28
15
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../suggestion"
|
|
4
|
+
|
|
5
|
+
class Evilution::Reporter::Suggestion::DiffLines
|
|
6
|
+
def self.from_diff(raw_diff)
|
|
7
|
+
lines = raw_diff.split("\n")
|
|
8
|
+
new(
|
|
9
|
+
original: clean(lines.find { |l| l.start_with?("- ") }, "- "),
|
|
10
|
+
mutated: clean(lines.find { |l| l.start_with?("+ ") }, "+ ")
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.clean(line, prefix)
|
|
15
|
+
return nil if line.nil?
|
|
16
|
+
|
|
17
|
+
line.sub(/^#{Regexp.escape(prefix)}/, "").strip
|
|
18
|
+
end
|
|
19
|
+
private_class_method :clean
|
|
20
|
+
|
|
21
|
+
attr_reader :original, :mutated
|
|
22
|
+
|
|
23
|
+
def initialize(original:, mutated:)
|
|
24
|
+
@original = original
|
|
25
|
+
@mutated = mutated
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../templates"
|
|
4
4
|
require_relative "../diff_helpers"
|
|
5
|
+
require_relative "../diff_lines"
|
|
5
6
|
|
|
6
7
|
module Evilution::Reporter::Suggestion::Templates::Minitest
|
|
7
8
|
H = Evilution::Reporter::Suggestion::DiffHelpers
|
|
@@ -17,21 +18,26 @@ module Evilution::Reporter::Suggestion::Templates::Minitest
|
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def self.build(test_name:, action: :changed, &body_block)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
safe_name = H.sanitize_method_name(method_name)
|
|
23
|
-
original_line, mutated_line = H.extract_diff_lines(mutation.diff)
|
|
24
|
-
body = body_block.call(method_name)
|
|
25
|
-
indented = body.lines.map { |l| " #{l}" }.join.chomp
|
|
21
|
+
->(mutation) { render(test_name, action, body_block, mutation) }
|
|
22
|
+
end
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
def self.render(test_name, action, body_block, mutation)
|
|
25
|
+
method_name = H.parse_method_name(mutation.subject.name)
|
|
26
|
+
safe_name = H.sanitize_method_name(method_name)
|
|
27
|
+
diff_lines = Evilution::Reporter::Suggestion::DiffLines.from_diff(mutation.diff)
|
|
28
|
+
indented = indent_body(body_block.call(method_name))
|
|
29
|
+
|
|
30
|
+
<<~MINITEST.strip
|
|
31
|
+
# Mutation: #{format_header(action, diff_lines.original, diff_lines.mutated, mutation.subject.name)}
|
|
32
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
33
|
+
def test_#{test_name}_#{safe_name}
|
|
34
|
+
#{indented}
|
|
35
|
+
end
|
|
36
|
+
MINITEST
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.indent_body(body)
|
|
40
|
+
body.lines.map { |l| " #{l}" }.join.chomp
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
MINITEST_ENTRIES = {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../templates"
|
|
4
4
|
require_relative "../diff_helpers"
|
|
5
|
+
require_relative "../diff_lines"
|
|
5
6
|
|
|
6
7
|
module Evilution::Reporter::Suggestion::Templates::Rspec
|
|
7
8
|
H = Evilution::Reporter::Suggestion::DiffHelpers
|
|
@@ -17,20 +18,25 @@ module Evilution::Reporter::Suggestion::Templates::Rspec
|
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def self.build(it_desc:, action: :changed, &body_block)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
original_line, mutated_line = H.extract_diff_lines(mutation.diff)
|
|
23
|
-
body = body_block.call(method_name)
|
|
24
|
-
indented = body.lines.map { |l| " #{l}" }.join.chomp
|
|
21
|
+
->(mutation) { render(it_desc, action, body_block, mutation) }
|
|
22
|
+
end
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
def self.render(it_desc, action, body_block, mutation)
|
|
25
|
+
method_name = H.parse_method_name(mutation.subject.name)
|
|
26
|
+
diff_lines = Evilution::Reporter::Suggestion::DiffLines.from_diff(mutation.diff)
|
|
27
|
+
indented = indent_body(body_block.call(method_name))
|
|
28
|
+
|
|
29
|
+
<<~RSPEC.strip
|
|
30
|
+
# Mutation: #{format_header(action, diff_lines.original, diff_lines.mutated, mutation.subject.name)}
|
|
31
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
32
|
+
it '#{it_desc} ##{method_name}' do
|
|
33
|
+
#{indented}
|
|
34
|
+
end
|
|
35
|
+
RSPEC
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.indent_body(body)
|
|
39
|
+
body.lines.map { |l| " #{l}" }.join.chomp
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
RSPEC_ENTRIES = {
|
|
@@ -30,18 +30,25 @@ class Evilution::Runner::BaselineRunner
|
|
|
30
30
|
|
|
31
31
|
def build_integration
|
|
32
32
|
klass = integration_class
|
|
33
|
-
|
|
34
|
-
kwargs
|
|
35
|
-
|
|
33
|
+
kwargs = base_integration_kwargs
|
|
34
|
+
kwargs.merge!(rspec_integration_kwargs) if klass == Evilution::Integration::RSpec
|
|
35
|
+
klass.new(**kwargs)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def base_integration_kwargs
|
|
39
|
+
{
|
|
40
|
+
test_files: config.spec_files.empty? ? nil : config.spec_files,
|
|
36
41
|
hooks: hooks,
|
|
37
42
|
fallback_to_full_suite: config.fallback_to_full_suite?,
|
|
38
43
|
spec_selector: config.spec_selector
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def rspec_integration_kwargs
|
|
48
|
+
{
|
|
49
|
+
related_specs_heuristic: config.related_specs_heuristic?,
|
|
50
|
+
example_filter: build_example_filter
|
|
51
|
+
}
|
|
45
52
|
end
|
|
46
53
|
|
|
47
54
|
def call(subjects)
|
|
@@ -32,19 +32,23 @@ class Evilution::Runner::Diagnostics
|
|
|
32
32
|
def log_mutation_diagnostics(result)
|
|
33
33
|
return unless verbose?
|
|
34
34
|
|
|
35
|
-
parts =
|
|
36
|
-
|
|
35
|
+
parts = mutation_metric_parts(result)
|
|
36
|
+
stderr.write("[verbose] #{result.mutation}: #{parts.join(", ")}\n") unless parts.empty?
|
|
37
37
|
|
|
38
|
-
if result.
|
|
39
|
-
|
|
40
|
-
parts << format("delta: %<sign>s%<mb>.1f MB", sign: sign, mb: result.memory_delta_kb / 1024.0)
|
|
41
|
-
end
|
|
38
|
+
log_mutation_error(result) if result.error?
|
|
39
|
+
end
|
|
42
40
|
|
|
41
|
+
def mutation_metric_parts(result)
|
|
42
|
+
parts = []
|
|
43
|
+
parts << format("child_rss: %<mb>.1f MB", mb: result.child_rss_kb / 1024.0) if result.child_rss_kb
|
|
44
|
+
parts << format_memory_delta(result.memory_delta_kb) if result.memory_delta_kb
|
|
43
45
|
parts << gc_stats_string
|
|
46
|
+
parts
|
|
47
|
+
end
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
def format_memory_delta(delta_kb)
|
|
50
|
+
sign = delta_kb.negative? ? "" : "+"
|
|
51
|
+
format("delta: %<sign>s%<mb>.1f MB", sign: sign, mb: delta_kb / 1024.0)
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
def log_worker_stats(stats)
|
|
@@ -108,16 +108,18 @@ class Evilution::Runner::IsolationResolver
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def resolve_preload_path
|
|
111
|
-
if config.preload.is_a?(String)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
return resolve_explicit_preload(config.preload) if config.preload.is_a?(String)
|
|
112
|
+
|
|
113
|
+
resolve_autodetected_preload
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def resolve_explicit_preload(path)
|
|
117
|
+
return path if File.file?(path)
|
|
118
|
+
|
|
119
|
+
raise Evilution::ConfigError.new("preload file not found: #{path.inspect}", file: path)
|
|
120
|
+
end
|
|
120
121
|
|
|
122
|
+
def resolve_autodetected_preload
|
|
121
123
|
root = detected_rails_root
|
|
122
124
|
return nil unless root
|
|
123
125
|
|
|
@@ -7,6 +7,8 @@ class Evilution::Runner::MutationExecutor::ResultCache
|
|
|
7
7
|
CACHEABLE_STATUSES = %i[killed timeout].freeze
|
|
8
8
|
private_constant :CACHEABLE_STATUSES
|
|
9
9
|
|
|
10
|
+
Partition = Data.define(:uncached_indices, :cached_results)
|
|
11
|
+
|
|
10
12
|
def initialize(backend)
|
|
11
13
|
@backend = backend
|
|
12
14
|
end
|
|
@@ -56,7 +58,7 @@ class Evilution::Runner::MutationExecutor::ResultCache
|
|
|
56
58
|
end
|
|
57
59
|
end
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
Partition.new(uncached_indices: uncached_indices, cached_results: cached_results)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
private
|
|
@@ -18,8 +18,17 @@ class Evilution::Runner::MutationExecutor::Strategy::Parallel
|
|
|
18
18
|
@notifier.start(mutations.length)
|
|
19
19
|
pool = @pool_factory.call
|
|
20
20
|
state = { results: [], truncated: false, completed: 0 }
|
|
21
|
-
all_worker_stats =
|
|
21
|
+
all_worker_stats = run_batches(mutations, pool, baseline_result, integration, state)
|
|
22
22
|
|
|
23
|
+
log_worker_diagnostics(all_worker_stats)
|
|
24
|
+
@notifier.finish
|
|
25
|
+
build_result(state)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def run_batches(mutations, pool, baseline_result, integration, state)
|
|
31
|
+
all_worker_stats = []
|
|
23
32
|
mutations.each_slice(@config.jobs) do |batch|
|
|
24
33
|
break if state[:truncated]
|
|
25
34
|
|
|
@@ -27,24 +36,37 @@ class Evilution::Runner::MutationExecutor::Strategy::Parallel
|
|
|
27
36
|
all_worker_stats.concat(pool.worker_stats)
|
|
28
37
|
process_batch(batch_results, baseline_result, state)
|
|
29
38
|
end
|
|
39
|
+
all_worker_stats
|
|
40
|
+
end
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
@
|
|
33
|
-
|
|
42
|
+
def log_worker_diagnostics(all_worker_stats)
|
|
43
|
+
return unless @diagnostics
|
|
44
|
+
|
|
45
|
+
@diagnostics.log_worker_stats(@diagnostics.aggregate_worker_stats(all_worker_stats))
|
|
34
46
|
end
|
|
35
47
|
|
|
36
|
-
|
|
48
|
+
def build_result(state)
|
|
49
|
+
Evilution::Runner::MutationExecutor::ExecutionResult.new(results: state[:results], truncated: state[:truncated])
|
|
50
|
+
end
|
|
37
51
|
|
|
38
52
|
def run_batch(batch, pool, integration)
|
|
39
|
-
|
|
40
|
-
worker_results = run_uncached(batch, uncached_indices, pool, integration)
|
|
41
|
-
compact_results = merge(batch, uncached_indices, cached_results, worker_results)
|
|
42
|
-
batch_results = batch
|
|
43
|
-
|
|
53
|
+
partition = @cache.partition(batch, packer: @packer)
|
|
54
|
+
worker_results = run_uncached(batch, partition.uncached_indices, pool, integration)
|
|
55
|
+
compact_results = merge(batch, partition.uncached_indices, partition.cached_results, worker_results)
|
|
56
|
+
batch_results = rebuild_results(batch, compact_results)
|
|
57
|
+
cache_results(batch_results, partition.uncached_indices)
|
|
44
58
|
batch.each(&:strip_sources!)
|
|
45
59
|
batch_results
|
|
46
60
|
end
|
|
47
61
|
|
|
62
|
+
def rebuild_results(batch, compact_results)
|
|
63
|
+
batch.zip(compact_results).map { |m, h| @packer.rebuild(m, h) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cache_results(batch_results, uncached_indices)
|
|
67
|
+
uncached_indices.each { |i| @cache.store(batch_results[i].mutation, batch_results[i]) }
|
|
68
|
+
end
|
|
69
|
+
|
|
48
70
|
def run_uncached(batch, uncached_indices, pool, integration)
|
|
49
71
|
return [] if uncached_indices.empty?
|
|
50
72
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "../runner"
|
|
4
4
|
|
|
5
5
|
class Evilution::Runner::MutationExecutor
|
|
6
|
+
ExecutionResult = Data.define(:results, :truncated)
|
|
7
|
+
|
|
6
8
|
autoload :ResultCache, File.expand_path("mutation_executor/result_cache", __dir__)
|
|
7
9
|
autoload :ResultPacker, File.expand_path("mutation_executor/result_packer", __dir__)
|
|
8
10
|
autoload :ResultNotifier, File.expand_path("mutation_executor/result_notifier", __dir__)
|
|
@@ -9,6 +9,12 @@ require_relative "../equivalent/detector"
|
|
|
9
9
|
class Evilution::Runner::MutationPlanner
|
|
10
10
|
Plan = Struct.new(:enabled, :equivalent, :skipped_count, :disabled_mutations, keyword_init: true)
|
|
11
11
|
|
|
12
|
+
GenerationResult = Data.define(:mutations, :skipped)
|
|
13
|
+
DisabledFilterResult = Data.define(:enabled, :disabled)
|
|
14
|
+
SigFilterResult = Data.define(:enabled, :skipped)
|
|
15
|
+
EquivalentFilterResult = Data.define(:equivalent, :enabled)
|
|
16
|
+
private_constant :GenerationResult, :DisabledFilterResult, :SigFilterResult, :EquivalentFilterResult
|
|
17
|
+
|
|
12
18
|
def initialize(config, registry:, disable_detector: Evilution::DisableComment.new,
|
|
13
19
|
sig_detector: Evilution::AST::SorbetSigDetector.new)
|
|
14
20
|
@config = config
|
|
@@ -20,26 +26,39 @@ class Evilution::Runner::MutationPlanner
|
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
def call(subjects)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
mutations, sig_skipped = filter_sig_blocks(mutations)
|
|
29
|
-
equivalent, enabled = filter_equivalent(mutations)
|
|
29
|
+
generation = generate(subjects)
|
|
30
|
+
disabled_filter = filter_disabled(generation.mutations)
|
|
31
|
+
disabled_mutations = compute_disabled_mutations(disabled_filter)
|
|
32
|
+
sig_filter = filter_sig_blocks(disabled_filter.enabled)
|
|
33
|
+
equivalent_filter = filter_equivalent(sig_filter.enabled)
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
enabled: enabled,
|
|
33
|
-
equivalent: equivalent,
|
|
34
|
-
skipped_count: generation_skipped + disabled.length + sig_skipped,
|
|
35
|
-
disabled_mutations: disabled_mutations
|
|
36
|
-
)
|
|
35
|
+
build_plan(equivalent_filter, disabled_mutations, total_skipped(generation, disabled_filter, sig_filter))
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
private
|
|
40
39
|
|
|
41
40
|
attr_reader :config, :registry
|
|
42
41
|
|
|
42
|
+
def compute_disabled_mutations(disabled_filter)
|
|
43
|
+
return [] unless config.show_disabled?
|
|
44
|
+
|
|
45
|
+
disabled_filter.disabled.each(&:strip_sources!)
|
|
46
|
+
disabled_filter.disabled
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def total_skipped(generation, disabled_filter, sig_filter)
|
|
50
|
+
generation.skipped + disabled_filter.disabled.length + sig_filter.skipped
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_plan(equivalent_filter, disabled_mutations, skipped_count)
|
|
54
|
+
Plan.new(
|
|
55
|
+
enabled: equivalent_filter.enabled,
|
|
56
|
+
equivalent: equivalent_filter.equivalent,
|
|
57
|
+
skipped_count: skipped_count,
|
|
58
|
+
disabled_mutations: disabled_mutations
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
43
62
|
def generate(subjects)
|
|
44
63
|
filter = build_ignore_filter
|
|
45
64
|
operator_options = build_operator_options
|
|
@@ -47,7 +66,7 @@ class Evilution::Runner::MutationPlanner
|
|
|
47
66
|
registry.mutations_for(subject, filter: filter, operator_options: operator_options)
|
|
48
67
|
end
|
|
49
68
|
skipped = filter ? filter.skipped_count : 0
|
|
50
|
-
|
|
69
|
+
GenerationResult.new(mutations: mutations, skipped: skipped)
|
|
51
70
|
end
|
|
52
71
|
|
|
53
72
|
def build_operator_options
|
|
@@ -73,7 +92,7 @@ class Evilution::Runner::MutationPlanner
|
|
|
73
92
|
end
|
|
74
93
|
end
|
|
75
94
|
|
|
76
|
-
|
|
95
|
+
DisabledFilterResult.new(enabled: enabled, disabled: disabled)
|
|
77
96
|
end
|
|
78
97
|
|
|
79
98
|
def mutation_disabled?(mutation)
|
|
@@ -102,7 +121,7 @@ class Evilution::Runner::MutationPlanner
|
|
|
102
121
|
end
|
|
103
122
|
end
|
|
104
123
|
|
|
105
|
-
|
|
124
|
+
SigFilterResult.new(enabled: enabled, skipped: skipped)
|
|
106
125
|
end
|
|
107
126
|
|
|
108
127
|
def mutation_in_sig_block?(mutation)
|
|
@@ -120,6 +139,7 @@ class Evilution::Runner::MutationPlanner
|
|
|
120
139
|
end
|
|
121
140
|
|
|
122
141
|
def filter_equivalent(mutations)
|
|
123
|
-
Evilution::Equivalent::Detector.new.call(mutations)
|
|
142
|
+
equivalent, enabled = Evilution::Equivalent::Detector.new.call(mutations)
|
|
143
|
+
EquivalentFilterResult.new(equivalent: equivalent, enabled: enabled)
|
|
124
144
|
end
|
|
125
145
|
end
|
|
@@ -104,17 +104,27 @@ class Evilution::Runner::SubjectPipeline
|
|
|
104
104
|
|
|
105
105
|
def target_matcher
|
|
106
106
|
target = config.target
|
|
107
|
-
if target.end_with?("*")
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
return wildcard_matcher(target.chomp("*")) if target.end_with?("*")
|
|
108
|
+
return prefix_matcher(target) if target.end_with?("#", ".")
|
|
109
|
+
return exact_matcher(target) if target.include?("#") || target.include?(".")
|
|
110
|
+
|
|
111
|
+
class_matcher(target)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def wildcard_matcher(prefix)
|
|
115
|
+
->(s) { s.name.split(/[#.]/).first.start_with?(prefix) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def prefix_matcher(prefix)
|
|
119
|
+
->(s) { s.name.start_with?(prefix) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def exact_matcher(target)
|
|
123
|
+
->(s) { s.name == target }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def class_matcher(target)
|
|
127
|
+
->(s) { s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
|
|
118
128
|
end
|
|
119
129
|
|
|
120
130
|
def filter_by_line_ranges(subjects)
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -32,13 +32,13 @@ class Evilution::Runner
|
|
|
32
32
|
plan = mutation_planner.call(subjects)
|
|
33
33
|
release_subject_nodes(subjects)
|
|
34
34
|
clear_operator_caches
|
|
35
|
-
|
|
36
|
-
results
|
|
35
|
+
execution = run_mutations(plan.enabled, baseline_result)
|
|
36
|
+
results = execution.results + equivalent_results(plan.equivalent)
|
|
37
37
|
log_memory("after run_mutations", "#{results.length} results")
|
|
38
38
|
|
|
39
39
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
40
40
|
|
|
41
|
-
summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: truncated,
|
|
41
|
+
summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: execution.truncated,
|
|
42
42
|
skipped: plan.skipped_count,
|
|
43
43
|
disabled_mutations: plan.disabled_mutations)
|
|
44
44
|
output_report(summary)
|
|
@@ -38,20 +38,29 @@ class Evilution::Session::Diff
|
|
|
38
38
|
def call(base_data, head_data)
|
|
39
39
|
base_survivors = base_data["survived"] || []
|
|
40
40
|
head_survivors = head_data["survived"] || []
|
|
41
|
-
|
|
42
|
-
base_keys = base_survivors.to_set { |m| mutation_key(m) }
|
|
43
|
-
head_keys = head_survivors.to_set { |m| mutation_key(m) }
|
|
41
|
+
fixed, new_survivors, persistent = partition_survivors(base_survivors, head_survivors)
|
|
44
42
|
|
|
45
43
|
Result.new(
|
|
46
44
|
summary: build_summary_diff(base_data, head_data),
|
|
47
|
-
fixed:
|
|
48
|
-
new_survivors:
|
|
49
|
-
persistent:
|
|
45
|
+
fixed: fixed,
|
|
46
|
+
new_survivors: new_survivors,
|
|
47
|
+
persistent: persistent
|
|
50
48
|
)
|
|
51
49
|
end
|
|
52
50
|
|
|
53
51
|
private
|
|
54
52
|
|
|
53
|
+
def partition_survivors(base_survivors, head_survivors)
|
|
54
|
+
base_keys = base_survivors.to_set { |m| mutation_key(m) }
|
|
55
|
+
head_keys = head_survivors.to_set { |m| mutation_key(m) }
|
|
56
|
+
|
|
57
|
+
[
|
|
58
|
+
base_survivors.reject { |m| head_keys.include?(mutation_key(m)) },
|
|
59
|
+
head_survivors.reject { |m| base_keys.include?(mutation_key(m)) },
|
|
60
|
+
head_survivors.select { |m| base_keys.include?(mutation_key(m)) }
|
|
61
|
+
]
|
|
62
|
+
end
|
|
63
|
+
|
|
55
64
|
def build_summary_diff(base_data, head_data)
|
|
56
65
|
base = extract_summary_values(base_data)
|
|
57
66
|
head = extract_summary_values(head_data)
|