evilution 1.3.0 → 1.5.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 +132 -0
- data/.rubocop_todo.yml +5 -0
- data/CHANGELOG.md +127 -0
- data/README.md +136 -38
- data/docs/architecture.md +135 -11
- data/docs/ast_pattern_syntax.md +50 -5
- data/docs/isolation.md +121 -2
- data/docs/migration-from-mutant.md +1 -1
- data/lib/evilution/ast/aasm_declaration.rb +133 -0
- data/lib/evilution/ast/callback_declaration.rb +91 -0
- data/lib/evilution/ast/class_body.rb +101 -0
- data/lib/evilution/ast/included_block.rb +41 -0
- data/lib/evilution/ast/literal_callable.rb +21 -0
- data/lib/evilution/ast/never_nil.rb +36 -0
- data/lib/evilution/ast/parser.rb +187 -21
- data/lib/evilution/ast/pattern/method_name.rb +63 -0
- data/lib/evilution/ast/pattern/parser.rb +14 -15
- data/lib/evilution/ast/regexp_pattern.rb +104 -0
- data/lib/evilution/ast/round_half_mode.rb +38 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/source_surgeon.rb +2 -3
- data/lib/evilution/ast/syntax_check.rb +56 -0
- data/lib/evilution/ast/uncovered_code.rb +88 -0
- data/lib/evilution/ast/value_object_definition.rb +25 -0
- data/lib/evilution/baseline/failure_formatter.rb +27 -0
- data/lib/evilution/baseline/report.rb +66 -0
- data/lib/evilution/baseline/spec_failure.rb +38 -0
- data/lib/evilution/baseline.rb +67 -30
- data/lib/evilution/cli/exit_guard.rb +26 -4
- data/lib/evilution/cli/parser/file_args.rb +2 -1
- data/lib/evilution/cli/parser/options_builder.rb +1 -1
- data/lib/evilution/cli.rb +3 -2
- data/lib/evilution/config/validators/spec_mappings.rb +2 -1
- data/lib/evilution/config.rb +3 -2
- data/lib/evilution/equivalent/detector.rb +3 -1
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/condition.rb +66 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/disturbance.rb +54 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/early_exit.rb +73 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/guard.rb +36 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/index_read.rb +82 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/node_path.rb +43 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch.rb +65 -0
- data/lib/evilution/example_filter.rb +45 -0
- data/lib/evilution/hooks/registry.rb +2 -1
- data/lib/evilution/integration/base.rb +11 -1
- data/lib/evilution/integration/known_failures.rb +26 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +177 -13
- data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
- data/lib/evilution/integration/loading/concern_redeclaration.rb +80 -0
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +22 -15
- data/lib/evilution/integration/loading/mutation_applier.rb +44 -1
- data/lib/evilution/integration/loading/own_methods.rb +87 -0
- data/lib/evilution/integration/loading/redefinition_recovery.rb +2 -3
- data/lib/evilution/integration/loading/source_evaluator.rb +9 -2
- data/lib/evilution/integration/loading/superseded_modules.rb +103 -0
- data/lib/evilution/integration/loading/syntax_validator.rb +2 -2
- data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
- data/lib/evilution/integration/minitest/test_ids.rb +21 -0
- data/lib/evilution/integration/minitest.rb +45 -6
- data/lib/evilution/integration/rspec/baseline_runner.rb +47 -3
- data/lib/evilution/integration/rspec/example_ids.rb +35 -0
- data/lib/evilution/integration/rspec/result_builder.rb +6 -1
- data/lib/evilution/integration/rspec/state_guard/anonymous_example_group_examples.rb +36 -0
- data/lib/evilution/integration/rspec/state_guard.rb +4 -1
- data/lib/evilution/integration/rspec.rb +22 -4
- data/lib/evilution/integration/test_unit/result_builder.rb +6 -0
- data/lib/evilution/integration/test_unit/test_ids.rb +18 -0
- data/lib/evilution/integration/test_unit.rb +37 -9
- data/lib/evilution/isolation/fork.rb +84 -11
- data/lib/evilution/isolation/in_process.rb +100 -8
- data/lib/evilution/mcp/info_tool/status_glossary.rb +15 -4
- data/lib/evilution/mcp/info_tool.rb +3 -2
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +2 -1
- data/lib/evilution/memory/leak_check.rb +27 -3
- data/lib/evilution/mutation.rb +21 -9
- data/lib/evilution/mutator/base.rb +69 -5
- data/lib/evilution/mutator/constant_value_subjects.rb +13 -0
- data/lib/evilution/mutator/operator/alias_removal.rb +56 -0
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +2 -2
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +10 -2
- data/lib/evilution/mutator/operator/array_literal.rb +29 -0
- data/lib/evilution/mutator/operator/attr_accessor_swap.rb +68 -0
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +12 -3
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/complex_literal.rb +39 -0
- data/lib/evilution/mutator/operator/compound_assignment.rb +47 -0
- data/lib/evilution/mutator/operator/constant_namespace_strip.rb +57 -0
- data/lib/evilution/mutator/operator/constant_read_to_nil.rb +58 -0
- data/lib/evilution/mutator/operator/constant_write_to_nil.rb +32 -0
- data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
- data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +2 -2
- data/lib/evilution/mutator/operator/float_literal.rb +54 -19
- data/lib/evilution/mutator/operator/format_specifier_swap.rb +100 -0
- data/lib/evilution/mutator/operator/forwarded_argument_drop.rb +142 -0
- data/lib/evilution/mutator/operator/freeze_removal.rb +86 -0
- data/lib/evilution/mutator/operator/hash_literal.rb +51 -0
- data/lib/evilution/mutator/operator/index_range_to_drop.rb +67 -0
- data/lib/evilution/mutator/operator/index_receiver_to_self.rb +20 -0
- data/lib/evilution/mutator/operator/index_to_key_predicate.rb +58 -0
- data/lib/evilution/mutator/operator/index_write_to_value.rb +37 -0
- data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
- data/lib/evilution/mutator/operator/integer_literal.rb +39 -9
- data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
- data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -0
- data/lib/evilution/mutator/operator/memoization_break.rb +18 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +18 -55
- data/lib/evilution/mutator/operator/nil_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/no_matching_pattern_else.rb +47 -0
- data/lib/evilution/mutator/operator/numbered_parameter_swap.rb +78 -0
- data/lib/evilution/mutator/operator/off_by_one_boundary.rb +68 -0
- data/lib/evilution/mutator/operator/operator_write_receiver_promotion.rb +38 -0
- data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +14 -3
- data/lib/evilution/mutator/operator/pattern_matching_array.rb +12 -1
- data/lib/evilution/mutator/operator/pattern_wildcard_widening.rb +117 -0
- data/lib/evilution/mutator/operator/pin_operator_removal.rb +48 -0
- data/lib/evilution/mutator/operator/range_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/rational_literal.rb +40 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +1 -1
- data/lib/evilution/mutator/operator/regex_simplification.rb +53 -132
- data/lib/evilution/mutator/operator/regexp_alternation_branch_deletion.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_anchor_promotion.rb +43 -0
- data/lib/evilution/mutator/operator/regexp_capture_to_passive.rb +61 -0
- data/lib/evilution/mutator/operator/regexp_character_type_complement.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_named_group_rename.rb +106 -0
- data/lib/evilution/mutator/operator/regexp_option_removal.rb +51 -0
- data/lib/evilution/mutator/operator/regexp_quantifier_minimum_swap.rb +45 -0
- data/lib/evilution/mutator/operator/rescue_class_widening.rb +54 -0
- data/lib/evilution/mutator/operator/rescue_else_concatenation.rb +62 -0
- data/lib/evilution/mutator/operator/rescue_handler_concatenation.rb +69 -0
- data/lib/evilution/mutator/operator/rescue_handler_promotion.rb +65 -0
- data/lib/evilution/mutator/operator/return_keyword_removal.rb +79 -0
- data/lib/evilution/mutator/operator/rightward_assignment.rb +46 -0
- data/lib/evilution/mutator/operator/round_half_mode_swap.rb +36 -0
- data/lib/evilution/mutator/operator/safe_navigation_insertion.rb +57 -0
- data/lib/evilution/mutator/operator/safe_navigation_removal.rb +2 -24
- data/lib/evilution/mutator/operator/send_mutation.rb +2 -0
- data/lib/evilution/mutator/operator/short_circuit_to_eager.rb +34 -0
- data/lib/evilution/mutator/operator/sleep_to_zero.rb +43 -0
- data/lib/evilution/mutator/operator/splat_operator.rb +41 -24
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/operator/string_literal.rb +29 -0
- data/lib/evilution/mutator/operator/superclass_removal.rb +14 -40
- data/lib/evilution/mutator/operator/symbol_literal.rb +47 -2
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +1 -1
- data/lib/evilution/mutator/operator/variable_read_to_nil.rb +113 -0
- data/lib/evilution/mutator/operator/visibility_removal.rb +74 -0
- data/lib/evilution/mutator/primitives.rb +24 -2
- data/lib/evilution/mutator/receiver_positions.rb +79 -0
- data/lib/evilution/mutator/registry.rb +58 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +6 -0
- data/lib/evilution/parallel/work_queue/channel.rb +58 -5
- data/lib/evilution/parallel/work_queue/dispatcher/reply.rb +41 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +14 -1
- data/lib/evilution/parallel/work_queue/worker/loop.rb +5 -0
- data/lib/evilution/parallel/work_queue/worker.rb +13 -4
- data/lib/evilution/parallel/work_queue.rb +15 -9
- data/lib/evilution/process_supervisor.rb +3 -2
- data/lib/evilution/reporter/cli/line_formatters/baseline_neutralized_notice.rb +55 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
- data/lib/evilution/reporter/json/baseline.rb +15 -0
- data/lib/evilution/reporter/json.rb +7 -2
- data/lib/evilution/result/baseline_neutralization.rb +10 -0
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/summary.rb +42 -2
- data/lib/evilution/runner/baseline_runner.rb +9 -4
- data/lib/evilution/runner/canary.rb +2 -57
- data/lib/evilution/runner/canary_failure_message.rb +101 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +22 -8
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +4 -2
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +10 -0
- data/lib/evilution/runner/mutation_executor.rb +1 -1
- data/lib/evilution/runner/mutation_planner.rb +11 -2
- data/lib/evilution/runner/report_publisher.rb +3 -2
- data/lib/evilution/runner/subject_pipeline/target.rb +86 -0
- data/lib/evilution/runner/subject_pipeline.rb +57 -43
- data/lib/evilution/runner.rb +5 -2
- data/lib/evilution/subject.rb +25 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +90 -0
- data/script/memory_check +62 -16
- metadata +113 -4
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../runner"
|
|
4
4
|
require_relative "../ast/inheritance_scanner"
|
|
5
|
+
require_relative "../ast/uncovered_code"
|
|
6
|
+
require_relative "../diagnostic"
|
|
5
7
|
require_relative "../git/changed_files"
|
|
6
8
|
|
|
7
9
|
class Evilution::Runner::SubjectPipeline
|
|
8
|
-
|
|
10
|
+
autoload :Target, File.expand_path("subject_pipeline/target", __dir__)
|
|
11
|
+
|
|
12
|
+
def initialize(config, parser:, registry: Evilution::Mutator::Registry.for_profile(config.profile))
|
|
9
13
|
@config = config
|
|
10
14
|
@parser = parser
|
|
15
|
+
@registry = registry
|
|
16
|
+
@target = Target.parse(config.target)
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
def call
|
|
14
20
|
subjects = parse_subjects
|
|
15
|
-
|
|
16
|
-
subjects =
|
|
21
|
+
@subjects_by_file = subjects.group_by(&:file_path)
|
|
22
|
+
subjects = filter_by_descendants(subjects) if target.descendants?
|
|
23
|
+
subjects = filter_by_target(subjects) if target.method?
|
|
17
24
|
subjects = filter_by_line_ranges(subjects) if config.line_ranges?
|
|
18
25
|
subjects
|
|
19
26
|
end
|
|
@@ -22,28 +29,61 @@ class Evilution::Runner::SubjectPipeline
|
|
|
22
29
|
@target_files ||= resolve_target_files
|
|
23
30
|
end
|
|
24
31
|
|
|
32
|
+
# Warns about the targeted lines that hold code outside every subject and
|
|
33
|
+
# that no mutation reached either. mutated_lines: the lines mutations were
|
|
34
|
+
# generated for, per file -- known only once #call's subjects are planned.
|
|
35
|
+
#
|
|
36
|
+
# A line range is checked as given; a whole file only when it has no
|
|
37
|
+
# subjects at all -- every class with an `include` or a DSL call would
|
|
38
|
+
# otherwise warn.
|
|
39
|
+
def report_uncovered_code(mutated_lines = {})
|
|
40
|
+
by_file = @subjects_by_file || {}
|
|
41
|
+
@subjects_by_file = nil
|
|
42
|
+
@uncovered_code = target_files.filter_map do |file|
|
|
43
|
+
uncovered_entry(file, by_file.fetch(file, []), mutated_lines.fetch(file, []))
|
|
44
|
+
end
|
|
45
|
+
@uncovered_code.each { |entry| warn_uncovered(entry) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Those lines, as `{ file:, lines: ["2-4", "9"] }` per file, once
|
|
49
|
+
# #report_uncovered_code has run.
|
|
50
|
+
def uncovered_code
|
|
51
|
+
@uncovered_code || []
|
|
52
|
+
end
|
|
53
|
+
|
|
25
54
|
private
|
|
26
55
|
|
|
27
|
-
attr_reader :config, :parser
|
|
56
|
+
attr_reader :config, :parser, :registry, :target
|
|
28
57
|
|
|
58
|
+
# The parser finds every subject there is; those of a kind no operator of
|
|
59
|
+
# this run mutates are left out, so that they neither count as covering
|
|
60
|
+
# their lines nor show up with nothing generated for them.
|
|
29
61
|
def parse_subjects
|
|
30
|
-
target_files.flat_map { |file| parser.call(file) }
|
|
62
|
+
target_files.flat_map { |file| parser.call(file) }.select { |subject| registry.accepts?(subject) }
|
|
31
63
|
end
|
|
32
64
|
|
|
33
|
-
def
|
|
34
|
-
config.
|
|
65
|
+
def uncovered_entry(file, file_subjects, mutated_lines)
|
|
66
|
+
range = config.line_ranges[file]
|
|
67
|
+
return unless range || file_subjects.empty?
|
|
68
|
+
|
|
69
|
+
lines = Evilution::AST::UncoveredCode.call(file, file_subjects, lines: range, mutated_lines: mutated_lines)
|
|
70
|
+
{ file: file, lines: lines.map { |run| line_label(run) } } unless lines.empty?
|
|
35
71
|
end
|
|
36
72
|
|
|
37
|
-
|
|
38
|
-
|
|
73
|
+
# Spelled the way a range is passed on the command line: `2-4`, or `9`.
|
|
74
|
+
def line_label(run)
|
|
75
|
+
run.size == 1 ? run.first.to_s : "#{run.first}-#{run.last}"
|
|
39
76
|
end
|
|
40
77
|
|
|
41
|
-
def
|
|
42
|
-
|
|
78
|
+
def warn_uncovered(entry)
|
|
79
|
+
Evilution::Diagnostic.warn(
|
|
80
|
+
"[evilution] #{entry[:file]}:#{entry[:lines].join(", ")} holds code outside every subject " \
|
|
81
|
+
"(class-body code such as DSL calls is not mutated); no mutations target those lines."
|
|
82
|
+
)
|
|
43
83
|
end
|
|
44
84
|
|
|
45
85
|
def resolve_source_glob
|
|
46
|
-
pattern =
|
|
86
|
+
pattern = target.value
|
|
47
87
|
files = Dir.glob(pattern)
|
|
48
88
|
raise Evilution::Error, "no files found matching '#{pattern}'" if files.empty?
|
|
49
89
|
|
|
@@ -51,10 +91,9 @@ class Evilution::Runner::SubjectPipeline
|
|
|
51
91
|
end
|
|
52
92
|
|
|
53
93
|
def filter_by_descendants(subjects)
|
|
54
|
-
base_name = config.target.delete_prefix("descendants:")
|
|
55
94
|
inheritance = Evilution::AST::InheritanceScanner.call(target_files)
|
|
56
|
-
class_names = resolve_descendant_set(
|
|
57
|
-
raise Evilution::Error, "no classes found matching '#{
|
|
95
|
+
class_names = resolve_descendant_set(target.value, inheritance)
|
|
96
|
+
raise Evilution::Error, "no classes found matching '#{target}'" if class_names.empty?
|
|
58
97
|
|
|
59
98
|
subjects.select { |s| class_names.include?(s.name.split(/[#.]/).first) }
|
|
60
99
|
end
|
|
@@ -80,14 +119,14 @@ class Evilution::Runner::SubjectPipeline
|
|
|
80
119
|
end
|
|
81
120
|
|
|
82
121
|
def filter_by_target(subjects)
|
|
83
|
-
matched = subjects.select(
|
|
122
|
+
matched = subjects.select { |subject| target.selects?(subject) }
|
|
84
123
|
raise Evilution::Error, build_no_match_error if matched.empty?
|
|
85
124
|
|
|
86
125
|
matched
|
|
87
126
|
end
|
|
88
127
|
|
|
89
128
|
def resolve_target_files
|
|
90
|
-
return resolve_source_glob if
|
|
129
|
+
return resolve_source_glob if target.source_glob?
|
|
91
130
|
return config.target_files unless config.target_files.empty?
|
|
92
131
|
|
|
93
132
|
@used_git_fallback = true
|
|
@@ -95,38 +134,13 @@ class Evilution::Runner::SubjectPipeline
|
|
|
95
134
|
end
|
|
96
135
|
|
|
97
136
|
def build_no_match_error
|
|
98
|
-
base = "no subject matched '#{
|
|
137
|
+
base = "no subject matched '#{target}'"
|
|
99
138
|
return base unless @used_git_fallback
|
|
100
139
|
|
|
101
140
|
"#{base}; scanned git-changed files only. Pass file paths or " \
|
|
102
141
|
"--target source:<glob> to scan the full codebase."
|
|
103
142
|
end
|
|
104
143
|
|
|
105
|
-
def target_matcher
|
|
106
|
-
target = config.target
|
|
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}.") }
|
|
128
|
-
end
|
|
129
|
-
|
|
130
144
|
def filter_by_line_ranges(subjects)
|
|
131
145
|
subjects.select do |subject|
|
|
132
146
|
range = config.line_ranges[subject.file_path]
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -38,6 +38,7 @@ class Evilution::Runner
|
|
|
38
38
|
baseline_result = run_baseline(subjects)
|
|
39
39
|
|
|
40
40
|
plan = mutation_planner.call(subjects)
|
|
41
|
+
subject_pipeline.report_uncovered_code(plan.mutated_lines)
|
|
41
42
|
release_subject_nodes(subjects)
|
|
42
43
|
clear_operator_caches
|
|
43
44
|
execution = run_mutations(plan.enabled, baseline_result)
|
|
@@ -52,7 +53,9 @@ class Evilution::Runner
|
|
|
52
53
|
disabled_mutations: plan.disabled_mutations,
|
|
53
54
|
unresolved_target_files: target_spec_audit.call(audited_files),
|
|
54
55
|
target_file_count: audited_files.length,
|
|
55
|
-
infra_retried: execution.infra_retried
|
|
56
|
+
infra_retried: execution.infra_retried,
|
|
57
|
+
uncovered_code: subject_pipeline.uncovered_code,
|
|
58
|
+
baseline_failures: baseline_result ? baseline_result.failures : [])
|
|
56
59
|
output_report(summary)
|
|
57
60
|
save_session(summary)
|
|
58
61
|
|
|
@@ -68,7 +71,7 @@ class Evilution::Runner
|
|
|
68
71
|
attr_reader :parser, :registry, :cache, :on_result, :hooks
|
|
69
72
|
|
|
70
73
|
def subject_pipeline
|
|
71
|
-
@subject_pipeline ||= Evilution::Runner::SubjectPipeline.new(config, parser: parser)
|
|
74
|
+
@subject_pipeline ||= Evilution::Runner::SubjectPipeline.new(config, parser: parser, registry: registry)
|
|
72
75
|
end
|
|
73
76
|
|
|
74
77
|
def mutation_planner
|
data/lib/evilution/subject.rb
CHANGED
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# What mutations are generated for and reported against. A :method subject is
|
|
4
|
+
# a method definition; a :constant subject is a definition assigned to a
|
|
5
|
+
# constant outside any method, such as `Point = Data.define(:x, :y)`. A :scope
|
|
6
|
+
# or :aasm subject is the body of a callable handed to a class-body
|
|
7
|
+
# declaration.
|
|
8
|
+
#
|
|
9
|
+
# A :constant_write subject is any other assignment to a constant, the whole
|
|
10
|
+
# of it: `LIMIT = 10`. Applying a mutation re-evaluates the file, which
|
|
11
|
+
# assigns the constant again, and evaluating the original after the tests
|
|
12
|
+
# puts it back. What read the constant while the file first loaded is
|
|
13
|
+
# not run again -- a class-body call (`validates length: { maximum: LIMIT }`)
|
|
14
|
+
# or a constant computed from it in another file keeps the old value -- so
|
|
15
|
+
# only code that reads the constant when called sees the mutant.
|
|
16
|
+
#
|
|
17
|
+
# A subject written in a concern's `included` or `prepended` block is marked
|
|
18
|
+
# `concern_hook`: the block runs in each class that takes the concern, so a
|
|
19
|
+
# mutation of it has to be run again on the classes that took it already.
|
|
3
20
|
class Evilution::Subject
|
|
4
|
-
attr_reader :name, :file_path, :line_number, :source, :node
|
|
21
|
+
attr_reader :name, :file_path, :line_number, :source, :node, :kind
|
|
5
22
|
|
|
6
|
-
def initialize(name:, file_path:, line_number:, source:, node:)
|
|
23
|
+
def initialize(name:, file_path:, line_number:, source:, node:, kind: :method, concern_hook: false)
|
|
7
24
|
@name = name
|
|
8
25
|
@file_path = file_path
|
|
9
26
|
@line_number = line_number
|
|
10
27
|
@source = source
|
|
11
28
|
@node = node
|
|
29
|
+
@kind = kind
|
|
30
|
+
@concern_hook = concern_hook
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def concern_hook?
|
|
34
|
+
@concern_hook
|
|
12
35
|
end
|
|
13
36
|
|
|
14
37
|
def release_node!
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "evilution/ast/inheritance_scanner"
|
|
|
14
14
|
require_relative "evilution/ast/pattern"
|
|
15
15
|
require_relative "evilution/ast/sorbet_sig_detector"
|
|
16
16
|
require_relative "evilution/ast/local_reads"
|
|
17
|
+
require_relative "evilution/ast/regexp_pattern"
|
|
17
18
|
require_relative "evilution/ast/pattern/matcher"
|
|
18
19
|
require_relative "evilution/ast/pattern/parser"
|
|
19
20
|
require_relative "evilution/hooks"
|
|
@@ -26,6 +27,8 @@ require_relative "evilution/mutator/operator/comparison_replacement"
|
|
|
26
27
|
require_relative "evilution/mutator/operator/boolean_literal_replacement"
|
|
27
28
|
require_relative "evilution/mutator/operator/integer_literal"
|
|
28
29
|
require_relative "evilution/mutator/operator/float_literal"
|
|
30
|
+
require_relative "evilution/mutator/operator/complex_literal"
|
|
31
|
+
require_relative "evilution/mutator/operator/rational_literal"
|
|
29
32
|
require_relative "evilution/mutator/operator/nil_replacement"
|
|
30
33
|
require_relative "evilution/mutator/operator/boolean_operator_replacement"
|
|
31
34
|
require_relative "evilution/mutator/operator/boolean_operand_promotion"
|
|
@@ -53,6 +56,51 @@ require_relative "evilution/mutator/operator/receiver_constructor_swap"
|
|
|
53
56
|
require_relative "evilution/mutator/operator/block_body_to_nil"
|
|
54
57
|
require_relative "evilution/mutator/operator/block_body_to_raise"
|
|
55
58
|
require_relative "evilution/mutator/operator/block_body_promotion"
|
|
59
|
+
require_relative "evilution/mutator/operator/data_struct_member"
|
|
60
|
+
require_relative "evilution/mutator/operator/pin_operator_removal"
|
|
61
|
+
require_relative "evilution/mutator/operator/rightward_assignment"
|
|
62
|
+
require_relative "evilution/mutator/operator/numbered_parameter_swap"
|
|
63
|
+
require_relative "evilution/mutator/operator/forwarded_argument_drop"
|
|
64
|
+
require_relative "evilution/mutator/operator/pattern_wildcard_widening"
|
|
65
|
+
require_relative "evilution/mutator/operator/no_matching_pattern_else"
|
|
66
|
+
require_relative "evilution/mutator/operator/argument_order_permutation"
|
|
67
|
+
require_relative "evilution/mutator/operator/keyword_value_swap"
|
|
68
|
+
require_relative "evilution/mutator/operator/comparison_operand_swap"
|
|
69
|
+
require_relative "evilution/mutator/operator/integer_division_to_fdiv"
|
|
70
|
+
require_relative "evilution/mutator/operator/round_half_mode_swap"
|
|
71
|
+
require_relative "evilution/mutator/operator/off_by_one_boundary"
|
|
72
|
+
require_relative "evilution/mutator/operator/format_specifier_swap"
|
|
73
|
+
require_relative "evilution/mutator/operator/exception_swallow"
|
|
74
|
+
require_relative "evilution/mutator/operator/statement_reorder"
|
|
75
|
+
require_relative "evilution/mutator/operator/alias_removal"
|
|
76
|
+
require_relative "evilution/mutator/operator/regexp_character_type_complement"
|
|
77
|
+
require_relative "evilution/mutator/operator/regexp_anchor_promotion"
|
|
78
|
+
require_relative "evilution/mutator/operator/regexp_alternation_branch_deletion"
|
|
79
|
+
require_relative "evilution/mutator/operator/regexp_quantifier_minimum_swap"
|
|
80
|
+
require_relative "evilution/mutator/operator/regexp_capture_to_passive"
|
|
81
|
+
require_relative "evilution/mutator/operator/regexp_named_group_rename"
|
|
82
|
+
require_relative "evilution/mutator/operator/regexp_option_removal"
|
|
83
|
+
require_relative "evilution/mutator/operator/rescue_handler_promotion"
|
|
84
|
+
require_relative "evilution/mutator/operator/rescue_handler_concatenation"
|
|
85
|
+
require_relative "evilution/mutator/operator/rescue_else_concatenation"
|
|
86
|
+
require_relative "evilution/mutator/operator/return_keyword_removal"
|
|
87
|
+
require_relative "evilution/mutator/operator/memoization_break"
|
|
88
|
+
require_relative "evilution/mutator/operator/short_circuit_to_eager"
|
|
89
|
+
require_relative "evilution/mutator/operator/freeze_removal"
|
|
90
|
+
require_relative "evilution/mutator/operator/rescue_class_widening"
|
|
91
|
+
require_relative "evilution/mutator/operator/visibility_removal"
|
|
92
|
+
require_relative "evilution/mutator/operator/attr_accessor_swap"
|
|
93
|
+
require_relative "evilution/mutator/operator/sleep_to_zero"
|
|
94
|
+
require_relative "evilution/mutator/operator/index_receiver_to_self"
|
|
95
|
+
require_relative "evilution/mutator/operator/index_to_key_predicate"
|
|
96
|
+
require_relative "evilution/mutator/operator/index_range_to_drop"
|
|
97
|
+
require_relative "evilution/mutator/operator/index_write_to_value"
|
|
98
|
+
require_relative "evilution/mutator/operator/constant_read_to_nil"
|
|
99
|
+
require_relative "evilution/mutator/operator/constant_namespace_strip"
|
|
100
|
+
require_relative "evilution/mutator/operator/constant_write_to_nil"
|
|
101
|
+
require_relative "evilution/mutator/operator/variable_read_to_nil"
|
|
102
|
+
require_relative "evilution/mutator/operator/operator_write_receiver_promotion"
|
|
103
|
+
require_relative "evilution/mutator/operator/safe_navigation_insertion"
|
|
56
104
|
require_relative "evilution/mutator/operator/arithmetic_replacement"
|
|
57
105
|
require_relative "evilution/mutator/operator/string_literal"
|
|
58
106
|
require_relative "evilution/mutator/operator/array_literal"
|
|
@@ -205,6 +253,48 @@ module Evilution
|
|
|
205
253
|
@in_isolated_worker = previous
|
|
206
254
|
end
|
|
207
255
|
|
|
256
|
+
# Marks the stretch in which the tests run against a mutation whose source has
|
|
257
|
+
# already loaded. A fork child that dies in it reports the death as the
|
|
258
|
+
# mutation's doing; one that dies earlier cannot say so.
|
|
259
|
+
#
|
|
260
|
+
# The child asks at exit, and by then the block is over in one of two ways.
|
|
261
|
+
# A stack overflow that exhausts the machine stack before the VM's own makes
|
|
262
|
+
# Ruby run neither `rescue` nor `ensure`, so the mark is still set. Which of
|
|
263
|
+
# the two stacks gives out first depends on the stack limit of the process,
|
|
264
|
+
# not on the Ruby version. Otherwise the exception unwinds through here, the
|
|
265
|
+
# mark is cleared, and the exception itself is kept to be recognised by.
|
|
266
|
+
def self.while_running_tests
|
|
267
|
+
previous = @running_tests
|
|
268
|
+
@running_tests = true
|
|
269
|
+
@tests_left_by = nil
|
|
270
|
+
yield
|
|
271
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- recorded and re-raised, never handled
|
|
272
|
+
@tests_left_by = e
|
|
273
|
+
raise
|
|
274
|
+
ensure
|
|
275
|
+
@running_tests = previous
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def self.running_tests?
|
|
279
|
+
@running_tests == true
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Whether a process dying on `error` is dying of its test run.
|
|
283
|
+
def self.died_running_tests?(error)
|
|
284
|
+
running_tests? || @tests_left_by.equal?(error)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Set by fork isolation in the child it forks for one mutation. Such a
|
|
288
|
+
# process applies its mutation, runs the tests and ends, so work that only
|
|
289
|
+
# serves a later mutation in the same process can be left out of it.
|
|
290
|
+
def self.single_mutation_process!
|
|
291
|
+
@single_mutation_process = true
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def self.single_mutation_process?
|
|
295
|
+
@single_mutation_process == true
|
|
296
|
+
end
|
|
297
|
+
|
|
208
298
|
# Base directory for resolving project-relative paths. An isolated worker
|
|
209
299
|
# has chdir'd into a per-mutation sandbox (EV-wqxu / GH #1278), so callers
|
|
210
300
|
# in that context must anchor against PROJECT_ROOT rather than Dir.pwd —
|
data/script/memory_check
CHANGED
|
@@ -11,6 +11,13 @@ COMPLEX_FIXTURE = File.expand_path("../spec/support/fixtures/memory_check/target
|
|
|
11
11
|
COMPLEX_FIXTURE_SPEC = File.expand_path("../spec/support/fixtures/memory_check/target_examples.rb", __dir__)
|
|
12
12
|
ITERATIONS = Integer(ENV.fetch("MEMORY_CHECK_ITERATIONS", 50))
|
|
13
13
|
MAX_GROWTH_KB = Integer(ENV.fetch("MEMORY_CHECK_MAX_GROWTH_KB", 10_240))
|
|
14
|
+
# Every check that picks mutations at random draws from its own Random seeded
|
|
15
|
+
# with this, so a run exercises the same mutations every time and a failure can
|
|
16
|
+
# be replayed by passing the printed seed back.
|
|
17
|
+
SEED = Integer(ENV.fetch("MEMORY_CHECK_SEED", 42))
|
|
18
|
+
COMPLEX_POOL_SIZE = 10
|
|
19
|
+
COMPLEX_ITERATIONS = Integer(ENV.fetch("MEMORY_CHECK_COMPLEX_ITERATIONS", 100))
|
|
20
|
+
COMPLEX_MAX_GROWTH_KB = Integer(ENV.fetch("MEMORY_CHECK_COMPLEX_MAX_GROWTH_KB", 10_240))
|
|
14
21
|
|
|
15
22
|
def setup_workload
|
|
16
23
|
parser = Evilution::AST::Parser.new
|
|
@@ -21,25 +28,40 @@ def setup_workload
|
|
|
21
28
|
mutations
|
|
22
29
|
end
|
|
23
30
|
|
|
24
|
-
def run_check(name, iterations: ITERATIONS, max_growth_kb: MAX_GROWTH_KB,
|
|
25
|
-
|
|
31
|
+
def run_check(name, iterations: ITERATIONS, max_growth_kb: MAX_GROWTH_KB, picked: nil,
|
|
32
|
+
warmup_iterations: Evilution::Memory::LeakCheck::WARMUP_ITERATIONS, &)
|
|
33
|
+
check = Evilution::Memory::LeakCheck.new(iterations: iterations, max_growth_kb: max_growth_kb,
|
|
34
|
+
warmup_iterations: warmup_iterations)
|
|
26
35
|
result = check.run(&)
|
|
27
|
-
report(name, result)
|
|
36
|
+
report(name, result, picked)
|
|
28
37
|
result[:passed]
|
|
29
38
|
end
|
|
30
39
|
|
|
31
|
-
def report(name, result)
|
|
40
|
+
def report(name, result, picked)
|
|
32
41
|
status = result[:passed] ? "PASS" : "FAIL"
|
|
33
42
|
metrics = format_metrics(result)
|
|
34
43
|
|
|
35
44
|
puts "[#{status}] #{name}"
|
|
36
|
-
puts "
|
|
45
|
+
puts " Sustained growth: #{metrics[:sustained]} (max: #{metrics[:max]})"
|
|
46
|
+
puts " Growth: #{metrics[:growth]}"
|
|
37
47
|
puts " Samples (MB): #{metrics[:samples]}"
|
|
48
|
+
report_picked(picked) unless result[:passed]
|
|
38
49
|
puts
|
|
39
50
|
end
|
|
40
51
|
|
|
52
|
+
# What a failed run exercised, so it can be replayed: the seed, and how often
|
|
53
|
+
# each operator came up among the mutations the check picked.
|
|
54
|
+
def report_picked(picked)
|
|
55
|
+
puts " Seed: #{SEED} (MEMORY_CHECK_SEED)"
|
|
56
|
+
return if picked.nil? || picked.empty?
|
|
57
|
+
|
|
58
|
+
tally = picked.map(&:operator_name).tally.sort_by { |operator, count| [-count, operator.to_s] }
|
|
59
|
+
puts " Operators (#{picked.size} picks): #{tally.map { |operator, count| "#{operator} x#{count}" }.join(", ")}"
|
|
60
|
+
end
|
|
61
|
+
|
|
41
62
|
def format_metrics(result)
|
|
42
63
|
{
|
|
64
|
+
sustained: result[:sustained_growth_mb] ? format("%.1f MB", result[:sustained_growth_mb]) : "N/A",
|
|
43
65
|
growth: result[:growth_mb] ? format("%.1f MB", result[:growth_mb]) : "N/A",
|
|
44
66
|
max: format("%.1f MB", result[:max_growth_kb] / 1024.0),
|
|
45
67
|
samples: result[:samples].map { |s| s ? format("%.1f", s / 1024.0) : "N/A" }.join(" -> ")
|
|
@@ -53,15 +75,21 @@ stub_test_command = ->(_m) { { passed: false } }
|
|
|
53
75
|
all_passed = true
|
|
54
76
|
|
|
55
77
|
# 1. InProcess isolation
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
random = Random.new(SEED)
|
|
79
|
+
picked = []
|
|
80
|
+
all_passed &= run_check("InProcess isolation", picked: picked) do
|
|
81
|
+
mutation = mutations.sample(random: random)
|
|
82
|
+
picked << mutation
|
|
58
83
|
isolator = Evilution::Isolation::InProcess.new
|
|
59
84
|
isolator.call(mutation: mutation, test_command: stub_test_command, timeout: 5)
|
|
60
85
|
end
|
|
61
86
|
|
|
62
87
|
# 2. Fork isolation
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
random = Random.new(SEED)
|
|
89
|
+
picked = []
|
|
90
|
+
all_passed &= run_check("Fork isolation", picked: picked) do
|
|
91
|
+
mutation = mutations.sample(random: random)
|
|
92
|
+
picked << mutation
|
|
65
93
|
isolator = Evilution::Isolation::Fork.new
|
|
66
94
|
isolator.call(mutation: mutation, test_command: stub_test_command, timeout: 5)
|
|
67
95
|
end
|
|
@@ -113,13 +141,31 @@ complex_subjects = complex_parser.call(COMPLEX_FIXTURE)
|
|
|
113
141
|
complex_mutations = complex_subjects.flat_map { |s| complex_registry.mutations_for(s) }
|
|
114
142
|
|
|
115
143
|
integration = Evilution::Integration::RSpec.new(test_files: [COMPLEX_FIXTURE_SPEC])
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
144
|
+
complex_pool = complex_mutations.sample(COMPLEX_POOL_SIZE, random: Random.new(SEED))
|
|
145
|
+
complex_turn = complex_pool.cycle
|
|
146
|
+
|
|
147
|
+
# This check used to grow 20-41 MB over 20 mutations, which was put down to
|
|
148
|
+
# per-mutation require() and redefinition warnings and given a 40 MB budget. It
|
|
149
|
+
# was a leak: every run left its examples reachable from
|
|
150
|
+
# RSpec::Core::AnonymousExampleGroup, about 1 MB per mutation (fixed by
|
|
151
|
+
# StateGuard::AnonymousExampleGroupExamples).
|
|
152
|
+
#
|
|
153
|
+
# The first run of a mutation still grows the process, as the heap reaches the
|
|
154
|
+
# size that mutation needs, and the growth depends on which mutation it is. So
|
|
155
|
+
# the check cycles through a fixed pool: warm-up runs each mutation twice, and
|
|
156
|
+
# every measured run repeats work the warm-up already did. Growth from there
|
|
157
|
+
# on is retained per run.
|
|
158
|
+
#
|
|
159
|
+
# The budget is 10 MB over 100 runs, about 100 KB per mutation: a leak of 200 KB
|
|
160
|
+
# per mutation grows about 18 MB once its largest step is taken out, and fails.
|
|
161
|
+
# Measured on Linux (aarch64) over seeds 1, 7, 42 and 99, sustained growth was
|
|
162
|
+
# -0.9 to 0.6 MB on Ruby 3.4.10 and 0.7 to 2.8 MB on 4.0.7. Ruby 4.0.7 creeps
|
|
163
|
+
# by about 30 KB per mutation in RSS with no Ruby objects retained, which is
|
|
164
|
+
# below what this check can tell apart from allocator noise.
|
|
165
|
+
all_passed &= run_check("RSpec integration per-mutation (Config)",
|
|
166
|
+
iterations: COMPLEX_ITERATIONS, max_growth_kb: COMPLEX_MAX_GROWTH_KB,
|
|
167
|
+
warmup_iterations: COMPLEX_POOL_SIZE * 2, picked: complex_pool) do
|
|
168
|
+
integration.call(complex_turn.next)
|
|
123
169
|
end
|
|
124
170
|
|
|
125
171
|
puts all_passed ? "All memory checks passed." : "Some memory checks failed!"
|