evilution 0.27.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 +65 -0
- data/.rubocop_todo.yml +0 -1
- data/CHANGELOG.md +39 -0
- data/README.md +19 -0
- data/lib/evilution/ast/constant_names.rb +28 -11
- data/lib/evilution/ast/pattern/parser.rb +29 -17
- data/lib/evilution/baseline.rb +5 -4
- 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 +36 -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/diff_extractor/evilution.rb +22 -0
- data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
- data/lib/evilution/compare/diff_extractor.rb +6 -0
- data/lib/evilution/compare/fingerprint.rb +15 -72
- data/lib/evilution/compare/line_normalizer.rb +72 -0
- data/lib/evilution/compare/normalizer.rb +27 -9
- data/lib/evilution/config/validators/profile.rb +11 -0
- data/lib/evilution/config.rb +49 -32
- data/lib/evilution/disable_comment.rb +21 -12
- data/lib/evilution/integration/crash_detector.rb +2 -2
- data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
- data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
- data/lib/evilution/integration/minitest.rb +25 -16
- data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +11 -3
- data/lib/evilution/integration/rspec.rb +4 -0
- data/lib/evilution/isolation/fork.rb +43 -28
- data/lib/evilution/isolation/in_process.rb +10 -6
- 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 -3
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -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 -20
- data/lib/evilution/mutation.rb +60 -42
- data/lib/evilution/mutator/base.rb +23 -21
- 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/predicate_to_nil.rb +20 -0
- 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/mutator/registry.rb +20 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +5 -1
- data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
- data/lib/evilution/parallel/work_queue/worker/loop.rb +1 -1
- data/lib/evilution/parallel/work_queue/worker.rb +10 -7
- data/lib/evilution/parallel/work_queue.rb +35 -18
- data/lib/evilution/process_cleanup.rb +19 -0
- 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/html/baseline_keys.rb +1 -1
- data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
- data/lib/evilution/reporter/html/escape.rb +1 -1
- data/lib/evilution/reporter/html/section.rb +1 -1
- data/lib/evilution/reporter/html/sections.rb +4 -2
- data/lib/evilution/reporter/html/stylesheet.rb +1 -1
- data/lib/evilution/reporter/html.rb +8 -3
- 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/registry.rb +1 -5
- data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +361 -649
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +362 -603
- data/lib/evilution/reporter/suggestion/templates.rb +6 -0
- data/lib/evilution/result/error_info.rb +20 -0
- data/lib/evilution/result/memory_stats.rb +20 -0
- data/lib/evilution/result/mutation_result.rb +30 -14
- data/lib/evilution/runner/baseline_runner.rb +16 -10
- data/lib/evilution/runner/diagnostics.rb +14 -11
- data/lib/evilution/runner/isolation_resolver.rb +12 -11
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +1 -3
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +1 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +3 -10
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +3 -10
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +4 -4
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +1 -3
- data/lib/evilution/runner/mutation_executor/result_packer.rb +11 -9
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +33 -13
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +2 -4
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +14 -20
- data/lib/evilution/runner/mutation_planner.rb +38 -19
- data/lib/evilution/runner/report_publisher.rb +1 -2
- data/lib/evilution/runner/subject_pipeline.rb +22 -13
- data/lib/evilution/runner.rb +36 -34
- 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/lib/evilution.rb +1 -0
- data/script/memory_check +14 -6
- data/scripts/benchmark_density +10 -9
- data/scripts/compare_mutations +38 -21
- data/scripts/mutant_json_adapter +7 -4
- metadata +15 -3
- data/lib/evilution/reporter/html/namespace.rb +0 -11
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../runner"
|
|
3
4
|
require_relative "../disable_comment"
|
|
4
5
|
require_relative "../ast/sorbet_sig_detector"
|
|
5
6
|
require_relative "../ast/pattern/filter"
|
|
6
7
|
require_relative "../equivalent/detector"
|
|
7
8
|
|
|
8
|
-
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
9
|
-
|
|
10
9
|
class Evilution::Runner::MutationPlanner
|
|
11
10
|
Plan = Struct.new(:enabled, :equivalent, :skipped_count, :disabled_mutations, keyword_init: true)
|
|
12
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
|
+
|
|
13
18
|
def initialize(config, registry:, disable_detector: Evilution::DisableComment.new,
|
|
14
19
|
sig_detector: Evilution::AST::SorbetSigDetector.new)
|
|
15
20
|
@config = config
|
|
@@ -21,26 +26,39 @@ class Evilution::Runner::MutationPlanner
|
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
def call(subjects)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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)
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
equivalent, enabled = filter_equivalent(mutations)
|
|
31
|
-
|
|
32
|
-
Plan.new(
|
|
33
|
-
enabled: enabled,
|
|
34
|
-
equivalent: equivalent,
|
|
35
|
-
skipped_count: generation_skipped + disabled.length + sig_skipped,
|
|
36
|
-
disabled_mutations: disabled_mutations
|
|
37
|
-
)
|
|
35
|
+
build_plan(equivalent_filter, disabled_mutations, total_skipped(generation, disabled_filter, sig_filter))
|
|
38
36
|
end
|
|
39
37
|
|
|
40
38
|
private
|
|
41
39
|
|
|
42
40
|
attr_reader :config, :registry
|
|
43
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
|
+
|
|
44
62
|
def generate(subjects)
|
|
45
63
|
filter = build_ignore_filter
|
|
46
64
|
operator_options = build_operator_options
|
|
@@ -48,7 +66,7 @@ class Evilution::Runner::MutationPlanner
|
|
|
48
66
|
registry.mutations_for(subject, filter: filter, operator_options: operator_options)
|
|
49
67
|
end
|
|
50
68
|
skipped = filter ? filter.skipped_count : 0
|
|
51
|
-
|
|
69
|
+
GenerationResult.new(mutations: mutations, skipped: skipped)
|
|
52
70
|
end
|
|
53
71
|
|
|
54
72
|
def build_operator_options
|
|
@@ -74,7 +92,7 @@ class Evilution::Runner::MutationPlanner
|
|
|
74
92
|
end
|
|
75
93
|
end
|
|
76
94
|
|
|
77
|
-
|
|
95
|
+
DisabledFilterResult.new(enabled: enabled, disabled: disabled)
|
|
78
96
|
end
|
|
79
97
|
|
|
80
98
|
def mutation_disabled?(mutation)
|
|
@@ -103,7 +121,7 @@ class Evilution::Runner::MutationPlanner
|
|
|
103
121
|
end
|
|
104
122
|
end
|
|
105
123
|
|
|
106
|
-
|
|
124
|
+
SigFilterResult.new(enabled: enabled, skipped: skipped)
|
|
107
125
|
end
|
|
108
126
|
|
|
109
127
|
def mutation_in_sig_block?(mutation)
|
|
@@ -121,6 +139,7 @@ class Evilution::Runner::MutationPlanner
|
|
|
121
139
|
end
|
|
122
140
|
|
|
123
141
|
def filter_equivalent(mutations)
|
|
124
|
-
Evilution::Equivalent::Detector.new.call(mutations)
|
|
142
|
+
equivalent, enabled = Evilution::Equivalent::Detector.new.call(mutations)
|
|
143
|
+
EquivalentFilterResult.new(equivalent: equivalent, enabled: enabled)
|
|
125
144
|
end
|
|
126
145
|
end
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../runner"
|
|
3
4
|
require_relative "../reporter/json"
|
|
4
5
|
require_relative "../reporter/cli"
|
|
5
6
|
require_relative "../reporter/html"
|
|
6
7
|
require_relative "../session/store"
|
|
7
8
|
|
|
8
|
-
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
9
|
-
|
|
10
9
|
class Evilution::Runner::ReportPublisher
|
|
11
10
|
def initialize(config)
|
|
12
11
|
@config = config
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../runner"
|
|
3
4
|
require_relative "../ast/inheritance_scanner"
|
|
4
5
|
require_relative "../git/changed_files"
|
|
5
6
|
|
|
6
|
-
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
7
|
-
|
|
8
7
|
class Evilution::Runner::SubjectPipeline
|
|
9
8
|
def initialize(config, parser:)
|
|
10
9
|
@config = config
|
|
@@ -105,17 +104,27 @@ class Evilution::Runner::SubjectPipeline
|
|
|
105
104
|
|
|
106
105
|
def target_matcher
|
|
107
106
|
target = config.target
|
|
108
|
-
if target.end_with?("*")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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}.") }
|
|
119
128
|
end
|
|
120
129
|
|
|
121
130
|
def filter_by_line_ranges(subjects)
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -1,36 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "ast/parser"
|
|
6
|
-
require_relative "memory"
|
|
7
|
-
require_relative "mutator/registry"
|
|
8
|
-
require_relative "isolation/fork"
|
|
9
|
-
require_relative "isolation/in_process"
|
|
10
|
-
require_relative "integration/rspec"
|
|
11
|
-
require_relative "integration/minitest"
|
|
12
|
-
require_relative "reporter/json"
|
|
13
|
-
require_relative "reporter/cli"
|
|
14
|
-
require_relative "reporter/html"
|
|
15
|
-
require_relative "reporter/suggestion"
|
|
16
|
-
require_relative "git/changed_files"
|
|
17
|
-
require_relative "result/mutation_result"
|
|
18
|
-
require_relative "result/summary"
|
|
19
|
-
require_relative "baseline"
|
|
20
|
-
require_relative "cache"
|
|
21
|
-
require_relative "parallel/pool"
|
|
22
|
-
require_relative "session/store"
|
|
23
|
-
require_relative "temp_dir_tracker"
|
|
24
|
-
require_relative "rails_detector"
|
|
25
|
-
require_relative "parallel_db_warning"
|
|
26
|
-
require_relative "child_output"
|
|
27
|
-
require_relative "runner/subject_pipeline"
|
|
28
|
-
require_relative "runner/mutation_planner"
|
|
29
|
-
require_relative "runner/isolation_resolver"
|
|
30
|
-
require_relative "runner/baseline_runner"
|
|
31
|
-
require_relative "runner/diagnostics"
|
|
32
|
-
require_relative "runner/mutation_executor"
|
|
33
|
-
require_relative "runner/report_publisher"
|
|
4
|
+
require_relative "../evilution"
|
|
34
5
|
|
|
35
6
|
class Evilution::Runner
|
|
36
7
|
attr_reader :config
|
|
@@ -40,7 +11,7 @@ class Evilution::Runner
|
|
|
40
11
|
@on_result = on_result
|
|
41
12
|
@hooks = hooks
|
|
42
13
|
@parser = Evilution::AST::Parser.new
|
|
43
|
-
@registry = Evilution::Mutator::Registry.
|
|
14
|
+
@registry = Evilution::Mutator::Registry.for_profile(config.profile)
|
|
44
15
|
@cache = config.incremental? ? Evilution::Cache.new : nil
|
|
45
16
|
end
|
|
46
17
|
|
|
@@ -61,13 +32,13 @@ class Evilution::Runner
|
|
|
61
32
|
plan = mutation_planner.call(subjects)
|
|
62
33
|
release_subject_nodes(subjects)
|
|
63
34
|
clear_operator_caches
|
|
64
|
-
|
|
65
|
-
results
|
|
35
|
+
execution = run_mutations(plan.enabled, baseline_result)
|
|
36
|
+
results = execution.results + equivalent_results(plan.equivalent)
|
|
66
37
|
log_memory("after run_mutations", "#{results.length} results")
|
|
67
38
|
|
|
68
39
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
69
40
|
|
|
70
|
-
summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: truncated,
|
|
41
|
+
summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: execution.truncated,
|
|
71
42
|
skipped: plan.skipped_count,
|
|
72
43
|
disabled_mutations: plan.disabled_mutations)
|
|
73
44
|
output_report(summary)
|
|
@@ -213,3 +184,34 @@ class Evilution::Runner
|
|
|
213
184
|
diagnostics.log_memory(phase, context)
|
|
214
185
|
end
|
|
215
186
|
end
|
|
187
|
+
|
|
188
|
+
require_relative "config"
|
|
189
|
+
require_relative "ast/parser"
|
|
190
|
+
require_relative "memory"
|
|
191
|
+
require_relative "mutator/registry"
|
|
192
|
+
require_relative "isolation/fork"
|
|
193
|
+
require_relative "isolation/in_process"
|
|
194
|
+
require_relative "integration/rspec"
|
|
195
|
+
require_relative "integration/minitest"
|
|
196
|
+
require_relative "reporter/json"
|
|
197
|
+
require_relative "reporter/cli"
|
|
198
|
+
require_relative "reporter/html"
|
|
199
|
+
require_relative "reporter/suggestion"
|
|
200
|
+
require_relative "git/changed_files"
|
|
201
|
+
require_relative "result/mutation_result"
|
|
202
|
+
require_relative "result/summary"
|
|
203
|
+
require_relative "baseline"
|
|
204
|
+
require_relative "cache"
|
|
205
|
+
require_relative "parallel/pool"
|
|
206
|
+
require_relative "session/store"
|
|
207
|
+
require_relative "temp_dir_tracker"
|
|
208
|
+
require_relative "rails_detector"
|
|
209
|
+
require_relative "parallel_db_warning"
|
|
210
|
+
require_relative "child_output"
|
|
211
|
+
require_relative "runner/subject_pipeline"
|
|
212
|
+
require_relative "runner/mutation_planner"
|
|
213
|
+
require_relative "runner/isolation_resolver"
|
|
214
|
+
require_relative "runner/baseline_runner"
|
|
215
|
+
require_relative "runner/diagnostics"
|
|
216
|
+
require_relative "runner/mutation_executor"
|
|
217
|
+
require_relative "runner/report_publisher"
|
|
@@ -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)
|
|
@@ -62,18 +62,27 @@ class Evilution::SpecAstCache
|
|
|
62
62
|
raise Evilution::ParseError.new("file not found: #{path}", file: path) unless File.exist?(path)
|
|
63
63
|
|
|
64
64
|
source = read_source(path)
|
|
65
|
+
result = parse_source(path, source)
|
|
66
|
+
collect_blocks(source, result, extract_comment_ranges(result))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_source(path, source)
|
|
65
70
|
result = Prism.parse(source)
|
|
71
|
+
return result unless result.failure?
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end
|
|
73
|
+
raise Evilution::ParseError.new(
|
|
74
|
+
"failed to parse #{path}: #{result.errors.map(&:message).join(", ")}",
|
|
75
|
+
file: path
|
|
76
|
+
)
|
|
77
|
+
end
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
def extract_comment_ranges(result)
|
|
80
|
+
result.comments
|
|
81
|
+
.map { |c| c.location.start_offset...c.location.end_offset }
|
|
82
|
+
.sort_by(&:begin)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def collect_blocks(source, result, comment_ranges)
|
|
77
86
|
collector = BlockCollector.new(source, comment_ranges)
|
|
78
87
|
collector.visit(result.value)
|
|
79
88
|
collector.blocks
|
|
@@ -133,16 +142,21 @@ class Evilution::SpecAstCache
|
|
|
133
142
|
def strip_comments(slice, base_offset)
|
|
134
143
|
return slice if @comment_ranges.empty?
|
|
135
144
|
|
|
136
|
-
|
|
145
|
+
end_offset = base_offset + slice.bytesize
|
|
146
|
+
ranges = comment_ranges_within(base_offset, end_offset)
|
|
137
147
|
return slice if ranges.empty?
|
|
138
148
|
|
|
149
|
+
splice_excluding_ranges(base_offset, end_offset, ranges)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def splice_excluding_ranges(start_off, end_off, ranges)
|
|
139
153
|
result = +""
|
|
140
|
-
cursor =
|
|
154
|
+
cursor = start_off
|
|
141
155
|
ranges.each do |range|
|
|
142
156
|
result << @source.byteslice(cursor, range.begin - cursor)
|
|
143
157
|
cursor = range.end
|
|
144
158
|
end
|
|
145
|
-
result << @source.byteslice(cursor,
|
|
159
|
+
result << @source.byteslice(cursor, end_off - cursor)
|
|
146
160
|
result
|
|
147
161
|
end
|
|
148
162
|
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -89,6 +89,7 @@ require_relative "evilution/mutator/operator/string_interpolation"
|
|
|
89
89
|
require_relative "evilution/mutator/operator/retry_removal"
|
|
90
90
|
require_relative "evilution/mutator/operator/case_when"
|
|
91
91
|
require_relative "evilution/mutator/operator/predicate_replacement"
|
|
92
|
+
require_relative "evilution/mutator/operator/predicate_to_nil"
|
|
92
93
|
require_relative "evilution/mutator/operator/equality_to_identity"
|
|
93
94
|
require_relative "evilution/mutator/operator/lambda_body"
|
|
94
95
|
require_relative "evilution/mutator/operator/begin_unwrap"
|
data/script/memory_check
CHANGED
|
@@ -30,16 +30,22 @@ end
|
|
|
30
30
|
|
|
31
31
|
def report(name, result)
|
|
32
32
|
status = result[:passed] ? "PASS" : "FAIL"
|
|
33
|
-
|
|
34
|
-
max = format("%.1f MB", result[:max_growth_kb] / 1024.0)
|
|
35
|
-
samples = result[:samples].map { |s| s ? format("%.1f", s / 1024.0) : "N/A" }.join(" -> ")
|
|
33
|
+
metrics = format_metrics(result)
|
|
36
34
|
|
|
37
35
|
puts "[#{status}] #{name}"
|
|
38
|
-
puts " Growth: #{growth} (max: #{max})"
|
|
39
|
-
puts " Samples (MB): #{samples}"
|
|
36
|
+
puts " Growth: #{metrics[:growth]} (max: #{metrics[:max]})"
|
|
37
|
+
puts " Samples (MB): #{metrics[:samples]}"
|
|
40
38
|
puts
|
|
41
39
|
end
|
|
42
40
|
|
|
41
|
+
def format_metrics(result)
|
|
42
|
+
{
|
|
43
|
+
growth: result[:growth_mb] ? format("%.1f MB", result[:growth_mb]) : "N/A",
|
|
44
|
+
max: format("%.1f MB", result[:max_growth_kb] / 1024.0),
|
|
45
|
+
samples: result[:samples].map { |s| s ? format("%.1f", s / 1024.0) : "N/A" }.join(" -> ")
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
abort("RSS measurement unavailable (requires /proc filesystem)") unless Evilution::Memory.rss_kb
|
|
44
50
|
|
|
45
51
|
mutations = setup_workload
|
|
@@ -88,7 +94,9 @@ if mutations.size >= 2
|
|
|
88
94
|
batch.zip(compact_results).map do |mutation, data|
|
|
89
95
|
Evilution::Result::MutationResult.new(
|
|
90
96
|
mutation: mutation, status: data[:status], duration: data[:duration],
|
|
91
|
-
|
|
97
|
+
memory: Evilution::Result::MemoryStats.new(
|
|
98
|
+
child_rss_kb: data[:child_rss_kb], memory_delta_kb: data[:memory_delta_kb]
|
|
99
|
+
)
|
|
92
100
|
)
|
|
93
101
|
end
|
|
94
102
|
end
|
data/scripts/benchmark_density
CHANGED
|
@@ -155,18 +155,19 @@ module BenchmarkDensity
|
|
|
155
155
|
def print_table(results)
|
|
156
156
|
puts format(HEADER_FMT, file: "File", evilution: "Evilution", reference: "Reference", ratio: "Ratio")
|
|
157
157
|
puts "-" * 75
|
|
158
|
-
|
|
159
|
-
results.each do |r|
|
|
160
|
-
ratio = compute_ratio(r[:evilution], r[:reference])
|
|
161
|
-
ratio_str = ratio ? format("%.2fx", ratio) : "N/A"
|
|
162
|
-
ev_str = r[:evilution]&.to_s || "ERR"
|
|
163
|
-
ref_str = r[:reference]&.to_s || "ERR"
|
|
164
|
-
puts format(ROW_FMT, file: r[:path], evilution: ev_str, reference: ref_str, ratio: ratio_str)
|
|
165
|
-
end
|
|
166
|
-
|
|
158
|
+
results.each { |r| puts format_result_row(r) }
|
|
167
159
|
puts "-" * 75
|
|
168
160
|
end
|
|
169
161
|
|
|
162
|
+
def format_result_row(result)
|
|
163
|
+
ratio = compute_ratio(result[:evilution], result[:reference])
|
|
164
|
+
format(ROW_FMT,
|
|
165
|
+
file: result[:path],
|
|
166
|
+
evilution: result[:evilution]&.to_s || "ERR",
|
|
167
|
+
reference: result[:reference]&.to_s || "ERR",
|
|
168
|
+
ratio: ratio ? format("%.2fx", ratio) : "N/A")
|
|
169
|
+
end
|
|
170
|
+
|
|
170
171
|
def print_summary(results)
|
|
171
172
|
totals = compute_totals(results)
|
|
172
173
|
print_total_line(totals)
|
data/scripts/compare_mutations
CHANGED
|
@@ -221,17 +221,25 @@ module CompareMutations
|
|
|
221
221
|
lines = ["## Extra in reference (#{extra.size})", ""]
|
|
222
222
|
|
|
223
223
|
catalog.summary.each do |entry|
|
|
224
|
-
lines
|
|
225
|
-
catalog.by_operator[entry[:operator]].each do |m|
|
|
226
|
-
lines << " Line #{m["line"]}:"
|
|
227
|
-
m["diff"].to_s.each_line { |l| lines << " #{l.chomp}" }
|
|
228
|
-
lines << ""
|
|
229
|
-
end
|
|
224
|
+
lines.concat(format_operator_group(entry, catalog.by_operator[entry[:operator]]))
|
|
230
225
|
end
|
|
231
226
|
|
|
232
227
|
lines
|
|
233
228
|
end
|
|
234
229
|
|
|
230
|
+
def format_operator_group(entry, mutations)
|
|
231
|
+
lines = ["### #{entry[:operator]} (#{entry[:count]})"]
|
|
232
|
+
mutations.each { |m| lines.concat(format_extra_mutation(m)) }
|
|
233
|
+
lines
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def format_extra_mutation(mutation)
|
|
237
|
+
lines = [" Line #{mutation["line"]}:"]
|
|
238
|
+
mutation["diff"].to_s.each_line { |l| lines << " #{l.chomp}" }
|
|
239
|
+
lines << ""
|
|
240
|
+
lines
|
|
241
|
+
end
|
|
242
|
+
|
|
235
243
|
def build_extra_evilution_section(comparison)
|
|
236
244
|
ev_extra = comparison.extra_in_evilution
|
|
237
245
|
return [] if ev_extra.empty?
|
|
@@ -343,34 +351,43 @@ module CompareMutations
|
|
|
343
351
|
|
|
344
352
|
def compare_file(path, _reference_target, reporter)
|
|
345
353
|
full_path = File.join(@config.project_root, path)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
ev_set = MutationSet.from_json(ev_data)
|
|
350
|
-
ref_set = MutationSet.from_json(ref_data)
|
|
354
|
+
ev_set = MutationSet.from_json(@evilution.collect(path))
|
|
355
|
+
ref_set = MutationSet.from_json(@reference.collect(full_path))
|
|
351
356
|
comparison = Comparison.new(evilution: ev_set, reference: ref_set)
|
|
352
357
|
catalog = Catalog.new(comparison.extra_in_reference)
|
|
353
358
|
|
|
354
359
|
reporter.write_file_report(path, comparison, catalog)
|
|
360
|
+
build_file_result(path, comparison, catalog)
|
|
361
|
+
end
|
|
355
362
|
|
|
356
|
-
|
|
363
|
+
def build_file_result(path, comparison, catalog)
|
|
364
|
+
{
|
|
365
|
+
file: path,
|
|
366
|
+
evilution_count: comparison.evilution_set.size,
|
|
367
|
+
reference_count: comparison.reference_set.size,
|
|
357
368
|
density_ratio: comparison.density_ratio.round(2),
|
|
358
369
|
extra_count: comparison.extra_in_reference.size,
|
|
359
|
-
operator_summary: catalog.summary
|
|
370
|
+
operator_summary: catalog.summary
|
|
371
|
+
}
|
|
360
372
|
end
|
|
361
373
|
|
|
362
374
|
def print_summary(file_results)
|
|
363
|
-
|
|
364
|
-
ref_total = file_results.sum { |r| r[:reference_count] }
|
|
365
|
-
extra_total = file_results.sum { |r| r[:extra_count] }
|
|
366
|
-
ratio = ev_total.positive? ? (ref_total.to_f / ev_total).round(2) : 0.0
|
|
375
|
+
totals = compute_totals(file_results)
|
|
367
376
|
|
|
368
377
|
puts "Comparison complete. Results in #{@config.output_dir}/"
|
|
369
378
|
puts " Files: #{file_results.size}"
|
|
370
|
-
puts " Evilution: #{
|
|
371
|
-
puts " Reference: #{
|
|
372
|
-
puts " Ratio: #{ratio}x"
|
|
373
|
-
puts " Extra in reference: #{
|
|
379
|
+
puts " Evilution: #{totals[:ev]} mutations"
|
|
380
|
+
puts " Reference: #{totals[:ref]} mutations"
|
|
381
|
+
puts " Ratio: #{totals[:ratio]}x"
|
|
382
|
+
puts " Extra in reference: #{totals[:extra]}"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def compute_totals(file_results)
|
|
386
|
+
ev = file_results.sum { |r| r[:evilution_count] }
|
|
387
|
+
ref = file_results.sum { |r| r[:reference_count] }
|
|
388
|
+
extra = file_results.sum { |r| r[:extra_count] }
|
|
389
|
+
{ ev: ev, ref: ref, extra: extra,
|
|
390
|
+
ratio: ev.positive? ? (ref.to_f / ev).round(2) : 0.0 }
|
|
374
391
|
end
|
|
375
392
|
end
|
|
376
393
|
end
|
data/scripts/mutant_json_adapter
CHANGED
|
@@ -128,14 +128,17 @@ module MutantJsonAdapter
|
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
def infer_operator(diff_lines)
|
|
131
|
-
removed = diff_lines
|
|
132
|
-
|
|
133
|
-
added = diff_lines.select { |l| l.start_with?("+") && !l.start_with?("+++") }
|
|
134
|
-
.map { |l| l[1..].strip }
|
|
131
|
+
removed = extract_diff_side(diff_lines, "-", "---")
|
|
132
|
+
added = extract_diff_side(diff_lines, "+", "+++")
|
|
135
133
|
|
|
136
134
|
categorize_mutation(removed, added)
|
|
137
135
|
end
|
|
138
136
|
|
|
137
|
+
def extract_diff_side(diff_lines, prefix, header)
|
|
138
|
+
diff_lines.select { |l| l.start_with?(prefix) && !l.start_with?(header) }
|
|
139
|
+
.map { |l| l[1..].strip }
|
|
140
|
+
end
|
|
141
|
+
|
|
139
142
|
def categorize_mutation(removed, added)
|
|
140
143
|
return "replacement" if removed.empty?
|
|
141
144
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evilution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.29.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis Kiselev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -158,8 +158,12 @@ files:
|
|
|
158
158
|
- lib/evilution/compare.rb
|
|
159
159
|
- lib/evilution/compare/categorizer.rb
|
|
160
160
|
- lib/evilution/compare/detector.rb
|
|
161
|
+
- lib/evilution/compare/diff_extractor.rb
|
|
162
|
+
- lib/evilution/compare/diff_extractor/evilution.rb
|
|
163
|
+
- lib/evilution/compare/diff_extractor/mutant.rb
|
|
161
164
|
- lib/evilution/compare/fingerprint.rb
|
|
162
165
|
- lib/evilution/compare/invalid_input.rb
|
|
166
|
+
- lib/evilution/compare/line_normalizer.rb
|
|
163
167
|
- lib/evilution/compare/normalizer.rb
|
|
164
168
|
- lib/evilution/compare/record.rb
|
|
165
169
|
- lib/evilution/config.rb
|
|
@@ -180,6 +184,7 @@ files:
|
|
|
180
184
|
- lib/evilution/config/validators/isolation.rb
|
|
181
185
|
- lib/evilution/config/validators/jobs.rb
|
|
182
186
|
- lib/evilution/config/validators/preload.rb
|
|
187
|
+
- lib/evilution/config/validators/profile.rb
|
|
183
188
|
- lib/evilution/config/validators/spec_mappings.rb
|
|
184
189
|
- lib/evilution/config/validators/spec_pattern.rb
|
|
185
190
|
- lib/evilution/disable_comment.rb
|
|
@@ -316,6 +321,7 @@ files:
|
|
|
316
321
|
- lib/evilution/mutator/operator/pattern_matching_array.rb
|
|
317
322
|
- lib/evilution/mutator/operator/pattern_matching_guard.rb
|
|
318
323
|
- lib/evilution/mutator/operator/predicate_replacement.rb
|
|
324
|
+
- lib/evilution/mutator/operator/predicate_to_nil.rb
|
|
319
325
|
- lib/evilution/mutator/operator/range_replacement.rb
|
|
320
326
|
- lib/evilution/mutator/operator/receiver_replacement.rb
|
|
321
327
|
- lib/evilution/mutator/operator/redo_statement.rb
|
|
@@ -352,6 +358,7 @@ files:
|
|
|
352
358
|
- lib/evilution/parallel/work_queue/worker/loop.rb
|
|
353
359
|
- lib/evilution/parallel/work_queue/worker_stat.rb
|
|
354
360
|
- lib/evilution/parallel_db_warning.rb
|
|
361
|
+
- lib/evilution/process_cleanup.rb
|
|
355
362
|
- lib/evilution/rails_detector.rb
|
|
356
363
|
- lib/evilution/related_spec_heuristic.rb
|
|
357
364
|
- lib/evilution/reporter.rb
|
|
@@ -381,7 +388,6 @@ files:
|
|
|
381
388
|
- lib/evilution/reporter/html/baseline_keys.rb
|
|
382
389
|
- lib/evilution/reporter/html/diff_formatter.rb
|
|
383
390
|
- lib/evilution/reporter/html/escape.rb
|
|
384
|
-
- lib/evilution/reporter/html/namespace.rb
|
|
385
391
|
- lib/evilution/reporter/html/report.rb
|
|
386
392
|
- lib/evilution/reporter/html/section.rb
|
|
387
393
|
- lib/evilution/reporter/html/sections.rb
|
|
@@ -417,13 +423,17 @@ files:
|
|
|
417
423
|
- lib/evilution/reporter/progress_bar.rb
|
|
418
424
|
- lib/evilution/reporter/suggestion.rb
|
|
419
425
|
- lib/evilution/reporter/suggestion/diff_helpers.rb
|
|
426
|
+
- lib/evilution/reporter/suggestion/diff_lines.rb
|
|
420
427
|
- lib/evilution/reporter/suggestion/registry.rb
|
|
428
|
+
- lib/evilution/reporter/suggestion/templates.rb
|
|
421
429
|
- lib/evilution/reporter/suggestion/templates/generic.rb
|
|
422
430
|
- lib/evilution/reporter/suggestion/templates/minitest.rb
|
|
423
431
|
- lib/evilution/reporter/suggestion/templates/rspec.rb
|
|
424
432
|
- lib/evilution/result.rb
|
|
425
433
|
- lib/evilution/result/coverage_gap.rb
|
|
426
434
|
- lib/evilution/result/coverage_gap_grouper.rb
|
|
435
|
+
- lib/evilution/result/error_info.rb
|
|
436
|
+
- lib/evilution/result/memory_stats.rb
|
|
427
437
|
- lib/evilution/result/mutation_result.rb
|
|
428
438
|
- lib/evilution/result/summary.rb
|
|
429
439
|
- lib/evilution/runner.rb
|
|
@@ -433,11 +443,13 @@ files:
|
|
|
433
443
|
- lib/evilution/runner/mutation_executor.rb
|
|
434
444
|
- lib/evilution/runner/mutation_executor/mutation_runner.rb
|
|
435
445
|
- lib/evilution/runner/mutation_executor/neutralization_pipeline.rb
|
|
446
|
+
- lib/evilution/runner/mutation_executor/neutralizer.rb
|
|
436
447
|
- lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb
|
|
437
448
|
- lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb
|
|
438
449
|
- lib/evilution/runner/mutation_executor/result_cache.rb
|
|
439
450
|
- lib/evilution/runner/mutation_executor/result_notifier.rb
|
|
440
451
|
- lib/evilution/runner/mutation_executor/result_packer.rb
|
|
452
|
+
- lib/evilution/runner/mutation_executor/strategy.rb
|
|
441
453
|
- lib/evilution/runner/mutation_executor/strategy/parallel.rb
|
|
442
454
|
- lib/evilution/runner/mutation_executor/strategy/sequential.rb
|
|
443
455
|
- lib/evilution/runner/mutation_planner.rb
|