evilution 1.2.0 → 1.4.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.
Files changed (213) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +100 -0
  3. data/.rubocop_todo.yml +14 -0
  4. data/CHANGELOG.md +124 -0
  5. data/README.md +142 -28
  6. data/docs/architecture.md +123 -9
  7. data/docs/ast_pattern_syntax.md +50 -5
  8. data/docs/isolation.md +39 -2
  9. data/docs/migration-from-mutant.md +1 -1
  10. data/lib/evilution/ast/aasm_declaration.rb +133 -0
  11. data/lib/evilution/ast/callback_declaration.rb +91 -0
  12. data/lib/evilution/ast/included_block.rb +27 -0
  13. data/lib/evilution/ast/literal_callable.rb +21 -0
  14. data/lib/evilution/ast/parser.rb +120 -16
  15. data/lib/evilution/ast/pattern/filter.rb +1 -0
  16. data/lib/evilution/ast/pattern/method_name.rb +63 -0
  17. data/lib/evilution/ast/pattern/parser.rb +15 -15
  18. data/lib/evilution/ast/pattern.rb +2 -0
  19. data/lib/evilution/ast/regexp_pattern.rb +104 -0
  20. data/lib/evilution/ast/scope_declaration.rb +45 -0
  21. data/lib/evilution/ast/sorbet_sig_detector.rb +1 -0
  22. data/lib/evilution/ast/uncovered_code.rb +88 -0
  23. data/lib/evilution/ast/value_object_definition.rb +25 -0
  24. data/lib/evilution/baseline/failure_formatter.rb +27 -0
  25. data/lib/evilution/baseline/report.rb +66 -0
  26. data/lib/evilution/baseline/spec_failure.rb +38 -0
  27. data/lib/evilution/baseline.rb +101 -42
  28. data/lib/evilution/child_output.rb +1 -1
  29. data/lib/evilution/cli/command.rb +1 -0
  30. data/lib/evilution/cli/commands/tests_list.rb +4 -3
  31. data/lib/evilution/cli/commands.rb +2 -0
  32. data/lib/evilution/cli/dispatcher.rb +2 -0
  33. data/lib/evilution/cli/exit_guard.rb +2 -0
  34. data/lib/evilution/cli/parsed_args.rb +2 -0
  35. data/lib/evilution/cli/parser/command_extractor.rb +2 -0
  36. data/lib/evilution/cli/parser/file_args.rb +4 -1
  37. data/lib/evilution/cli/parser/options_builder.rb +4 -1
  38. data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
  39. data/lib/evilution/cli/parser.rb +1 -0
  40. data/lib/evilution/cli/printers/tests_list.rb +5 -4
  41. data/lib/evilution/cli/printers.rb +2 -0
  42. data/lib/evilution/cli/result.rb +2 -0
  43. data/lib/evilution/cli.rb +8 -2
  44. data/lib/evilution/config/builders.rb +2 -0
  45. data/lib/evilution/config/env_loader.rb +2 -0
  46. data/lib/evilution/config/file_loader.rb +1 -0
  47. data/lib/evilution/config/sources.rb +1 -0
  48. data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
  49. data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
  50. data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
  51. data/lib/evilution/config/validators/fail_fast.rb +1 -0
  52. data/lib/evilution/config/validators/hooks.rb +1 -0
  53. data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
  54. data/lib/evilution/config/validators/integration.rb +1 -0
  55. data/lib/evilution/config/validators/isolation.rb +1 -0
  56. data/lib/evilution/config/validators/jobs.rb +1 -0
  57. data/lib/evilution/config/validators/preload.rb +1 -0
  58. data/lib/evilution/config/validators/profile.rb +1 -0
  59. data/lib/evilution/config/validators/spec_mappings.rb +3 -1
  60. data/lib/evilution/config/validators/spec_pattern.rb +1 -0
  61. data/lib/evilution/config/validators/warmup.rb +17 -0
  62. data/lib/evilution/config/validators.rb +2 -0
  63. data/lib/evilution/config.rb +7 -5
  64. data/lib/evilution/coverage.rb +1 -1
  65. data/lib/evilution/coverage_example_filter.rb +1 -1
  66. data/lib/evilution/diagnostic.rb +1 -1
  67. data/lib/evilution/equivalent/detector.rb +3 -1
  68. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/condition.rb +66 -0
  69. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/disturbance.rb +54 -0
  70. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/early_exit.rb +73 -0
  71. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/guard.rb +36 -0
  72. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/index_read.rb +82 -0
  73. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/node_path.rb +43 -0
  74. data/lib/evilution/equivalent/heuristic/guarded_index_fetch.rb +65 -0
  75. data/lib/evilution/example_filter.rb +46 -1
  76. data/lib/evilution/hooks/registry.rb +2 -1
  77. data/lib/evilution/integration/base.rb +2 -0
  78. data/lib/evilution/integration/known_failures.rb +26 -0
  79. data/lib/evilution/integration/loading/body_call_neutralizer.rb +139 -12
  80. data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
  81. data/lib/evilution/integration/loading/concern_redeclaration.rb +61 -0
  82. data/lib/evilution/integration/loading/concern_state_cleaner.rb +19 -12
  83. data/lib/evilution/integration/loading/mutation_applier.rb +16 -0
  84. data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
  85. data/lib/evilution/integration/loading/test_load_path.rb +21 -13
  86. data/lib/evilution/integration/minitest/test_ids.rb +21 -0
  87. data/lib/evilution/integration/minitest.rb +45 -6
  88. data/lib/evilution/integration/rspec/baseline_runner.rb +47 -3
  89. data/lib/evilution/integration/rspec/example_ids.rb +35 -0
  90. data/lib/evilution/integration/rspec/result_builder.rb +6 -1
  91. data/lib/evilution/integration/rspec/state_guard/anonymous_example_group_examples.rb +36 -0
  92. data/lib/evilution/integration/rspec/state_guard/configuration_state.rb +1 -0
  93. data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +1 -0
  94. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
  95. data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
  96. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
  97. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
  98. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
  99. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
  100. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
  101. data/lib/evilution/integration/rspec/state_guard.rb +10 -1
  102. data/lib/evilution/integration/rspec.rb +22 -4
  103. data/lib/evilution/integration/test_unit/result_builder.rb +6 -0
  104. data/lib/evilution/integration/test_unit/test_ids.rb +18 -0
  105. data/lib/evilution/integration/test_unit.rb +37 -9
  106. data/lib/evilution/isolation/fork.rb +2 -1
  107. data/lib/evilution/isolation/in_process.rb +21 -4
  108. data/lib/evilution/mcp/info_tool/actions/environment.rb +1 -0
  109. data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
  110. data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
  111. data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
  112. data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
  113. data/lib/evilution/mcp/info_tool/status_glossary.rb +2 -2
  114. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +2 -1
  115. data/lib/evilution/memory/leak_check.rb +27 -3
  116. data/lib/evilution/mutation.rb +8 -3
  117. data/lib/evilution/mutator/base.rb +49 -5
  118. data/lib/evilution/mutator/operator/alias_removal.rb +126 -0
  119. data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
  120. data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
  121. data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
  122. data/lib/evilution/mutator/operator/argument_removal.rb +9 -1
  123. data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
  124. data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
  125. data/lib/evilution/mutator/operator/bang_method.rb +11 -3
  126. data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
  127. data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
  128. data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
  129. data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
  130. data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
  131. data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
  132. data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
  133. data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
  134. data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
  135. data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
  136. data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
  137. data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
  138. data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
  139. data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
  140. data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
  141. data/lib/evilution/mutator/operator/format_specifier_swap.rb +100 -0
  142. data/lib/evilution/mutator/operator/forwarded_argument_drop.rb +142 -0
  143. data/lib/evilution/mutator/operator/inequality_to_negated_identity.rb +43 -0
  144. data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
  145. data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
  146. data/lib/evilution/mutator/operator/keyword_argument_removal.rb +47 -0
  147. data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -0
  148. data/lib/evilution/mutator/operator/no_matching_pattern_else.rb +47 -0
  149. data/lib/evilution/mutator/operator/numbered_parameter_swap.rb +78 -0
  150. data/lib/evilution/mutator/operator/off_by_one_boundary.rb +68 -0
  151. data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +14 -3
  152. data/lib/evilution/mutator/operator/pattern_matching_array.rb +12 -1
  153. data/lib/evilution/mutator/operator/pattern_wildcard_widening.rb +117 -0
  154. data/lib/evilution/mutator/operator/pin_operator_removal.rb +48 -0
  155. data/lib/evilution/mutator/operator/proc_to_lambda.rb +43 -0
  156. data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
  157. data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
  158. data/lib/evilution/mutator/operator/regex_simplification.rb +53 -132
  159. data/lib/evilution/mutator/operator/regexp_alternation_branch_deletion.rb +54 -0
  160. data/lib/evilution/mutator/operator/regexp_anchor_promotion.rb +43 -0
  161. data/lib/evilution/mutator/operator/regexp_anchor_to_predicate.rb +136 -0
  162. data/lib/evilution/mutator/operator/regexp_capture_to_passive.rb +61 -0
  163. data/lib/evilution/mutator/operator/regexp_character_type_complement.rb +54 -0
  164. data/lib/evilution/mutator/operator/regexp_named_group_rename.rb +106 -0
  165. data/lib/evilution/mutator/operator/regexp_option_removal.rb +51 -0
  166. data/lib/evilution/mutator/operator/regexp_quantifier_minimum_swap.rb +45 -0
  167. data/lib/evilution/mutator/operator/rescue_else_concatenation.rb +62 -0
  168. data/lib/evilution/mutator/operator/rescue_handler_concatenation.rb +69 -0
  169. data/lib/evilution/mutator/operator/rescue_handler_promotion.rb +65 -0
  170. data/lib/evilution/mutator/operator/return_keyword_removal.rb +79 -0
  171. data/lib/evilution/mutator/operator/rightward_assignment.rb +46 -0
  172. data/lib/evilution/mutator/operator/safe_navigation_removal.rb +67 -0
  173. data/lib/evilution/mutator/operator/send_mutation.rb +44 -6
  174. data/lib/evilution/mutator/operator/splat_operator.rb +38 -13
  175. data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
  176. data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
  177. data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
  178. data/lib/evilution/mutator/primitives.rb +25 -0
  179. data/lib/evilution/mutator/registry.rb +54 -2
  180. data/lib/evilution/mutator/rescue_handlers.rb +81 -0
  181. data/lib/evilution/parallel/pool.rb +1 -0
  182. data/lib/evilution/parallel_db_warning.rb +1 -1
  183. data/lib/evilution/process_supervisor.rb +3 -2
  184. data/lib/evilution/rails_warmup.rb +61 -0
  185. data/lib/evilution/reporter/cli/line_formatters/baseline_neutralized_notice.rb +55 -0
  186. data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
  187. data/lib/evilution/reporter/html/report.rb +1 -0
  188. data/lib/evilution/reporter/json/baseline.rb +15 -0
  189. data/lib/evilution/reporter/json.rb +7 -2
  190. data/lib/evilution/result/baseline_neutralization.rb +10 -0
  191. data/lib/evilution/result/coverage_gap_grouper.rb +1 -0
  192. data/lib/evilution/result/mutation_result.rb +9 -1
  193. data/lib/evilution/result/subject_scorer.rb +1 -0
  194. data/lib/evilution/result/summary.rb +42 -2
  195. data/lib/evilution/runner/baseline_runner.rb +26 -11
  196. data/lib/evilution/runner/canary.rb +2 -57
  197. data/lib/evilution/runner/canary_failure_message.rb +101 -0
  198. data/lib/evilution/runner/isolation_resolver.rb +23 -2
  199. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +35 -14
  200. data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -0
  201. data/lib/evilution/runner/mutation_executor/result_packer.rb +4 -2
  202. data/lib/evilution/runner/mutation_executor.rb +1 -1
  203. data/lib/evilution/runner/mutation_planner.rb +11 -2
  204. data/lib/evilution/runner/report_publisher.rb +3 -2
  205. data/lib/evilution/runner/subject_pipeline.rb +46 -1
  206. data/lib/evilution/runner.rb +4 -1
  207. data/lib/evilution/source_ast_cache.rb +1 -1
  208. data/lib/evilution/spec_ast_cache.rb +1 -1
  209. data/lib/evilution/subject.rb +8 -2
  210. data/lib/evilution/version.rb +1 -1
  211. data/lib/evilution.rb +51 -0
  212. data/script/memory_check +62 -16
  213. metadata +108 -4
@@ -2,6 +2,8 @@
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
@@ -12,6 +14,7 @@ class Evilution::Runner::SubjectPipeline
12
14
 
13
15
  def call
14
16
  subjects = parse_subjects
17
+ @subjects_by_file = subjects.group_by(&:file_path)
15
18
  subjects = filter_by_descendants(subjects) if descendants_target?
16
19
  subjects = filter_by_target(subjects) if method_target?
17
20
  subjects = filter_by_line_ranges(subjects) if config.line_ranges?
@@ -22,6 +25,28 @@ class Evilution::Runner::SubjectPipeline
22
25
  @target_files ||= resolve_target_files
23
26
  end
24
27
 
28
+ # Warns about the targeted lines that hold code outside every subject and
29
+ # that no mutation reached either. mutated_lines: the lines mutations were
30
+ # generated for, per file -- known only once #call's subjects are planned.
31
+ #
32
+ # A line range is checked as given; a whole file only when it has no
33
+ # subjects at all -- every class with an `include` or a constant would
34
+ # otherwise warn.
35
+ def report_uncovered_code(mutated_lines = {})
36
+ by_file = @subjects_by_file || {}
37
+ @subjects_by_file = nil
38
+ @uncovered_code = target_files.filter_map do |file|
39
+ uncovered_entry(file, by_file.fetch(file, []), mutated_lines.fetch(file, []))
40
+ end
41
+ @uncovered_code.each { |entry| warn_uncovered(entry) }
42
+ end
43
+
44
+ # Those lines, as `{ file:, lines: ["2-4", "9"] }` per file, once
45
+ # #report_uncovered_code has run.
46
+ def uncovered_code
47
+ @uncovered_code || []
48
+ end
49
+
25
50
  private
26
51
 
27
52
  attr_reader :config, :parser
@@ -30,6 +55,26 @@ class Evilution::Runner::SubjectPipeline
30
55
  target_files.flat_map { |file| parser.call(file) }
31
56
  end
32
57
 
58
+ def uncovered_entry(file, file_subjects, mutated_lines)
59
+ range = config.line_ranges[file]
60
+ return unless range || file_subjects.empty?
61
+
62
+ lines = Evilution::AST::UncoveredCode.call(file, file_subjects, lines: range, mutated_lines: mutated_lines)
63
+ { file: file, lines: lines.map { |run| line_label(run) } } unless lines.empty?
64
+ end
65
+
66
+ # Spelled the way a range is passed on the command line: `2-4`, or `9`.
67
+ def line_label(run)
68
+ run.size == 1 ? run.first.to_s : "#{run.first}-#{run.last}"
69
+ end
70
+
71
+ def warn_uncovered(entry)
72
+ Evilution::Diagnostic.warn(
73
+ "[evilution] #{entry[:file]}:#{entry[:lines].join(", ")} holds code outside every subject " \
74
+ "(class-body code such as DSL calls and constants is not mutated); no mutations target those lines."
75
+ )
76
+ end
77
+
33
78
  def source_glob_target?
34
79
  config.target&.start_with?("source:")
35
80
  end
@@ -124,7 +169,7 @@ class Evilution::Runner::SubjectPipeline
124
169
  end
125
170
 
126
171
  def class_matcher(target)
127
- ->(s) { s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
172
+ ->(s) { s.name == target || s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
128
173
  end
129
174
 
130
175
  def filter_by_line_ranges(subjects)
@@ -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
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "digest"
4
4
  require "prism"
5
- require_relative "../evilution"
5
+ require_relative "version"
6
6
 
7
7
  # Content-hash-keyed LRU of Prism::ParseResult. Different source bytes always
8
8
  # yield a different key, so the cache is valid for the lifetime of the process.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
- require_relative "../evilution"
4
+ require_relative "version"
5
5
 
6
6
  class Evilution::SpecAstCache
7
7
  Block = Struct.new(:kind, :line, :end_line, :body_text)
@@ -1,14 +1,20 @@
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.
3
8
  class Evilution::Subject
4
- attr_reader :name, :file_path, :line_number, :source, :node
9
+ attr_reader :name, :file_path, :line_number, :source, :node, :kind
5
10
 
6
- def initialize(name:, file_path:, line_number:, source:, node:)
11
+ def initialize(name:, file_path:, line_number:, source:, node:, kind: :method)
7
12
  @name = name
8
13
  @file_path = file_path
9
14
  @line_number = line_number
10
15
  @source = source
11
16
  @node = node
17
+ @kind = kind
12
18
  end
13
19
 
14
20
  def release_node!
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "1.2.0"
4
+ VERSION = "1.4.0"
5
5
  end
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"
@@ -30,6 +31,56 @@ require_relative "evilution/mutator/operator/nil_replacement"
30
31
  require_relative "evilution/mutator/operator/boolean_operator_replacement"
31
32
  require_relative "evilution/mutator/operator/boolean_operand_promotion"
32
33
  require_relative "evilution/mutator/operator/boolean_expression_to_nil"
34
+ require_relative "evilution/mutator/operator/call_to_nil"
35
+ require_relative "evilution/mutator/operator/safe_navigation_removal"
36
+ require_relative "evilution/mutator/operator/attribute_write_to_read"
37
+ require_relative "evilution/mutator/operator/argument_propagation"
38
+ require_relative "evilution/mutator/operator/argument_list_removal"
39
+ require_relative "evilution/mutator/operator/symbol_to_proc_replacement"
40
+ require_relative "evilution/mutator/operator/dynamic_dispatch_resolution"
41
+ require_relative "evilution/mutator/operator/keyword_argument_removal"
42
+ require_relative "evilution/mutator/operator/double_negation_removal"
43
+ require_relative "evilution/mutator/operator/regexp_anchor_to_predicate"
44
+ require_relative "evilution/mutator/operator/reduce_to_sum"
45
+ require_relative "evilution/mutator/operator/array_coercion_to_literal"
46
+ require_relative "evilution/mutator/operator/to_i_to_integer"
47
+ require_relative "evilution/mutator/operator/coercion_emptying"
48
+ require_relative "evilution/mutator/operator/const_get_to_constant_path"
49
+ require_relative "evilution/mutator/operator/proc_to_lambda"
50
+ require_relative "evilution/mutator/operator/dig_to_fetch_chain"
51
+ require_relative "evilution/mutator/operator/inequality_to_negated_identity"
52
+ require_relative "evilution/mutator/operator/binary_operand_promotion"
53
+ require_relative "evilution/mutator/operator/receiver_constructor_swap"
54
+ require_relative "evilution/mutator/operator/block_body_to_nil"
55
+ require_relative "evilution/mutator/operator/block_body_to_raise"
56
+ require_relative "evilution/mutator/operator/block_body_promotion"
57
+ require_relative "evilution/mutator/operator/data_struct_member"
58
+ require_relative "evilution/mutator/operator/pin_operator_removal"
59
+ require_relative "evilution/mutator/operator/rightward_assignment"
60
+ require_relative "evilution/mutator/operator/numbered_parameter_swap"
61
+ require_relative "evilution/mutator/operator/forwarded_argument_drop"
62
+ require_relative "evilution/mutator/operator/pattern_wildcard_widening"
63
+ require_relative "evilution/mutator/operator/no_matching_pattern_else"
64
+ require_relative "evilution/mutator/operator/argument_order_permutation"
65
+ require_relative "evilution/mutator/operator/keyword_value_swap"
66
+ require_relative "evilution/mutator/operator/comparison_operand_swap"
67
+ require_relative "evilution/mutator/operator/integer_division_to_fdiv"
68
+ require_relative "evilution/mutator/operator/off_by_one_boundary"
69
+ require_relative "evilution/mutator/operator/format_specifier_swap"
70
+ require_relative "evilution/mutator/operator/exception_swallow"
71
+ require_relative "evilution/mutator/operator/statement_reorder"
72
+ require_relative "evilution/mutator/operator/alias_removal"
73
+ require_relative "evilution/mutator/operator/regexp_character_type_complement"
74
+ require_relative "evilution/mutator/operator/regexp_anchor_promotion"
75
+ require_relative "evilution/mutator/operator/regexp_alternation_branch_deletion"
76
+ require_relative "evilution/mutator/operator/regexp_quantifier_minimum_swap"
77
+ require_relative "evilution/mutator/operator/regexp_capture_to_passive"
78
+ require_relative "evilution/mutator/operator/regexp_named_group_rename"
79
+ require_relative "evilution/mutator/operator/regexp_option_removal"
80
+ require_relative "evilution/mutator/operator/rescue_handler_promotion"
81
+ require_relative "evilution/mutator/operator/rescue_handler_concatenation"
82
+ require_relative "evilution/mutator/operator/rescue_else_concatenation"
83
+ require_relative "evilution/mutator/operator/return_keyword_removal"
33
84
  require_relative "evilution/mutator/operator/arithmetic_replacement"
34
85
  require_relative "evilution/mutator/operator/string_literal"
35
86
  require_relative "evilution/mutator/operator/array_literal"
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
- check = Evilution::Memory::LeakCheck.new(iterations: iterations, max_growth_kb: max_growth_kb)
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 " Growth: #{metrics[:growth]} (max: #{metrics[:max]})"
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
- all_passed &= run_check("InProcess isolation") do
57
- mutation = mutations.sample
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
- all_passed &= run_check("Fork isolation") do
64
- mutation = mutations.sample
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
- # Budget is generous: per-mutation require() adds the file to $LOADED_FEATURES and
118
- # accumulates constant-redefinition warnings. Local runs land around 20 MB; CI
119
- # varies up to ~30 MB depending on Ruby build and GC pressure, so we leave headroom.
120
- all_passed &= run_check("RSpec integration per-mutation (Config)", iterations: 20, max_growth_kb: 40_960) do
121
- mutation = complex_mutations.sample
122
- integration.call(mutation)
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!"