evilution 0.26.0 → 0.28.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 (159) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +23 -0
  3. data/.rubocop_todo.yml +6 -0
  4. data/CHANGELOG.md +54 -0
  5. data/README.md +76 -3
  6. data/lib/evilution/baseline.rb +5 -4
  7. data/lib/evilution/cache.rb +2 -0
  8. data/lib/evilution/child_output.rb +24 -0
  9. data/lib/evilution/cli/commands/run.rb +9 -0
  10. data/lib/evilution/cli/commands/version.rb +2 -0
  11. data/lib/evilution/cli/parser/options_builder.rb +23 -2
  12. data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
  13. data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
  14. data/lib/evilution/compare/diff_extractor.rb +6 -0
  15. data/lib/evilution/compare/fingerprint.rb +15 -72
  16. data/lib/evilution/compare/line_normalizer.rb +72 -0
  17. data/lib/evilution/compare/normalizer.rb +17 -4
  18. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  19. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  20. data/lib/evilution/config/builders.rb +4 -0
  21. data/lib/evilution/config/env_loader.rb +12 -0
  22. data/lib/evilution/config/file_loader.rb +22 -0
  23. data/lib/evilution/config/sources.rb +14 -0
  24. data/lib/evilution/config/validators/base.rb +37 -0
  25. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  26. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  27. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  28. data/lib/evilution/config/validators/hooks.rb +12 -0
  29. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  30. data/lib/evilution/config/validators/integration.rb +11 -0
  31. data/lib/evilution/config/validators/isolation.rb +19 -0
  32. data/lib/evilution/config/validators/jobs.rb +9 -0
  33. data/lib/evilution/config/validators/preload.rb +13 -0
  34. data/lib/evilution/config/validators/profile.rb +11 -0
  35. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  36. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  37. data/lib/evilution/config/validators.rb +4 -0
  38. data/lib/evilution/config.rb +93 -266
  39. data/lib/evilution/feedback/detector.rb +15 -0
  40. data/lib/evilution/feedback/messages.rb +42 -0
  41. data/lib/evilution/feedback.rb +5 -0
  42. data/lib/evilution/integration/crash_detector.rb +2 -2
  43. data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
  44. data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
  45. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  46. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  47. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  48. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  49. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  50. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  51. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  52. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
  53. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  54. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  55. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  56. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  57. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  58. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  59. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  60. data/lib/evilution/integration/rspec.rb +61 -232
  61. data/lib/evilution/isolation/fork.rb +23 -13
  62. data/lib/evilution/isolation/in_process.rb +10 -6
  63. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  64. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  65. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  66. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  67. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  68. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  69. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  70. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  71. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  72. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  73. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  74. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  75. data/lib/evilution/mcp/info_tool.rb +43 -263
  76. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  77. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
  78. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  79. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  80. data/lib/evilution/mcp/session_tool.rb +0 -2
  81. data/lib/evilution/mutation.rb +47 -27
  82. data/lib/evilution/mutator/base.rb +8 -8
  83. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  84. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  85. data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
  86. data/lib/evilution/mutator/registry.rb +20 -0
  87. data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -0
  88. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  89. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  90. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  91. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  92. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  93. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  94. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  95. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  96. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  97. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  98. data/lib/evilution/parallel/work_queue.rb +42 -327
  99. data/lib/evilution/process_cleanup.rb +19 -0
  100. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  101. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  102. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  103. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  104. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  105. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  106. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  107. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  108. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  109. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  110. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  111. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  112. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  113. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  114. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  115. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  116. data/lib/evilution/reporter/cli/pct.rb +9 -0
  117. data/lib/evilution/reporter/cli/section.rb +13 -0
  118. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  119. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  120. data/lib/evilution/reporter/cli.rb +79 -162
  121. data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
  122. data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
  123. data/lib/evilution/reporter/html/escape.rb +1 -1
  124. data/lib/evilution/reporter/html/section.rb +1 -1
  125. data/lib/evilution/reporter/html/sections.rb +4 -2
  126. data/lib/evilution/reporter/html/stylesheet.rb +1 -1
  127. data/lib/evilution/reporter/html.rb +8 -3
  128. data/lib/evilution/reporter/suggestion/registry.rb +1 -5
  129. data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
  130. data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
  131. data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
  132. data/lib/evilution/reporter/suggestion/templates.rb +6 -0
  133. data/lib/evilution/result/error_info.rb +20 -0
  134. data/lib/evilution/result/memory_stats.rb +20 -0
  135. data/lib/evilution/result/mutation_result.rb +30 -14
  136. data/lib/evilution/runner/baseline_runner.rb +1 -2
  137. data/lib/evilution/runner/diagnostics.rb +1 -2
  138. data/lib/evilution/runner/isolation_resolver.rb +10 -4
  139. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
  140. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
  141. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
  142. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
  143. data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
  144. data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
  145. data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
  146. data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
  147. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
  148. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
  149. data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
  150. data/lib/evilution/runner/mutation_executor.rb +53 -292
  151. data/lib/evilution/runner/mutation_planner.rb +1 -2
  152. data/lib/evilution/runner/report_publisher.rb +1 -2
  153. data/lib/evilution/runner/subject_pipeline.rb +1 -2
  154. data/lib/evilution/runner.rb +53 -30
  155. data/lib/evilution/version.rb +1 -1
  156. data/lib/evilution.rb +1 -0
  157. data/script/memory_check +3 -1
  158. metadata +125 -3
  159. data/lib/evilution/reporter/html/namespace.rb +0 -11
@@ -1,325 +1,86 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../parallel/pool"
4
- require_relative "../reporter/progress_bar"
5
- require_relative "../result/mutation_result"
6
-
7
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
3
+ require_relative "../runner"
8
4
 
9
5
  class Evilution::Runner::MutationExecutor
6
+ autoload :ResultCache, File.expand_path("mutation_executor/result_cache", __dir__)
7
+ autoload :ResultPacker, File.expand_path("mutation_executor/result_packer", __dir__)
8
+ autoload :ResultNotifier, File.expand_path("mutation_executor/result_notifier", __dir__)
9
+ autoload :MutationRunner, File.expand_path("mutation_executor/mutation_runner", __dir__)
10
+ autoload :NeutralizationPipeline, File.expand_path("mutation_executor/neutralization_pipeline", __dir__)
11
+ autoload :Strategy, File.expand_path("mutation_executor/strategy", __dir__)
12
+ autoload :Neutralizer, File.expand_path("mutation_executor/neutralizer", __dir__)
13
+
10
14
  def initialize(config, isolator:, baseline_runner:, cache:, hooks:, diagnostics:, on_result: nil)
11
15
  @config = config
12
16
  @isolator = isolator
13
17
  @baseline_runner = baseline_runner
14
- @cache = cache
18
+ @cache = ResultCache.new(cache)
19
+ @packer = ResultPacker.new
15
20
  @hooks = hooks
16
21
  @diagnostics = diagnostics
17
22
  @on_result = on_result
18
23
  end
19
24
 
20
25
  def call(mutations, baseline_result = nil)
21
- @progress_bar = build_progress_bar(mutations.length)
22
- result = if config.jobs > 1
23
- run_parallel(mutations, baseline_result)
24
- else
25
- run_sequential(mutations, baseline_result)
26
- end
27
- @progress_bar&.finish
28
- result
26
+ integration = @baseline_runner.build_integration
27
+ spec_resolver = baseline_failed?(baseline_result) ? @baseline_runner.neutralization_resolver : nil
28
+ notifier = build_notifier
29
+ pipeline = build_pipeline(spec_resolver)
30
+ strategy = @config.jobs > 1 ? build_parallel(notifier, pipeline) : build_sequential(notifier, pipeline)
31
+
32
+ strategy.call(mutations, baseline_result: baseline_result, integration: integration)
29
33
  end
30
34
 
31
35
  private
32
36
 
33
- attr_reader :config, :isolator, :baseline_runner, :cache, :hooks, :diagnostics, :on_result
34
-
35
- def run_sequential(mutations, baseline_result)
36
- integration = baseline_runner.build_integration
37
- spec_resolver = baseline_result&.failed? ? baseline_runner.neutralization_resolver : nil
38
- results = []
39
- survived_count = 0
40
- truncated = false
41
-
42
- mutations.each_with_index do |mutation, index|
43
- result = execute_one(mutation, integration)
44
- mutation.strip_sources!
45
- result = neutralize_if_infra_error(result)
46
- result = neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
47
- results << result
48
- survived_count += 1 if result.survived?
49
- notify_result(result, index + 1)
50
-
51
- if config.fail_fast? && survived_count >= config.fail_fast
52
- truncated = true
53
- break
54
- end
55
- end
56
-
57
- [results, truncated]
37
+ def baseline_failed?(baseline_result)
38
+ baseline_result && baseline_result.failed?
58
39
  end
59
40
 
60
- def run_parallel(mutations, baseline_result)
61
- integration = baseline_runner.build_integration
62
- pool = build_pool
63
- spec_resolver = baseline_result&.failed? ? baseline_runner.neutralization_resolver : nil
64
- state = { results: [], survived_count: 0, truncated: false, completed: 0 }
65
- all_worker_stats = []
66
-
67
- mutations.each_slice(config.jobs) do |batch|
68
- break if state[:truncated]
69
-
70
- batch_results = run_parallel_batch(batch, pool, isolator, integration)
71
- all_worker_stats.concat(pool.worker_stats)
72
- process_batch(batch_results, baseline_result, spec_resolver, state)
73
- end
74
-
75
- diagnostics.log_worker_stats(diagnostics.aggregate_worker_stats(all_worker_stats))
76
- [state[:results], state[:truncated]]
41
+ def build_notifier
42
+ ResultNotifier.new(@config, diagnostics: @diagnostics, on_result: @on_result)
77
43
  end
78
44
 
79
- def build_pool
80
- Evilution::Parallel::Pool.new(
81
- size: config.jobs,
82
- hooks: hooks,
83
- item_timeout: config.timeout ? config.timeout * 2 : nil
45
+ def build_pipeline(spec_resolver)
46
+ NeutralizationPipeline.new(
47
+ [
48
+ Neutralizer::InfraError.new,
49
+ Neutralizer::BaselineFailed.new(
50
+ config: @config,
51
+ spec_resolver: spec_resolver || ->(_f) {},
52
+ fallback_dir: @baseline_runner.neutralization_fallback_dir
53
+ )
54
+ ]
84
55
  )
85
56
  end
86
57
 
87
- def run_parallel_batch(batch, pool, worker_isolator, integration)
88
- uncached_indices, cached_results = partition_cached(batch)
89
- worker_results = run_uncached_workers(batch, uncached_indices, pool, worker_isolator, integration)
90
- compact_results = merge_parallel_results(batch, uncached_indices, cached_results, worker_results)
91
- batch.each(&:strip_sources!)
92
- batch_results = rebuild_results(batch, compact_results)
93
- batch_results.each { |r| store_cached_result(r.mutation, r) }
94
- batch_results
95
- end
96
-
97
- def run_uncached_workers(batch, uncached_indices, pool, worker_isolator, integration)
98
- return [] if uncached_indices.empty?
99
-
100
- uncached = uncached_indices.map { |i| batch[i] }
101
- pool.map(uncached) do |mutation|
102
- test_command = ->(m) { integration.call(m) }
103
- result = worker_isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
104
- compact_result(result)
105
- end
106
- end
107
-
108
- def process_batch(batch_results, baseline_result, spec_resolver, state)
109
- batch_results.each do |result|
110
- result = neutralize_if_infra_error(result)
111
- result = neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
112
- state[:results] << result
113
- state[:survived_count] += 1 if result.survived?
114
- state[:completed] += 1
115
- notify_result(result, state[:completed])
116
- end
117
-
118
- diagnostics.log_memory("after batch", "#{state[:completed]} complete")
119
- state[:truncated] = true if should_truncate?(state[:survived_count])
120
- end
121
-
122
- # Reclassify results as :neutral when the failure was caused by test
123
- # infrastructure rather than by the mutation. Two independent paths:
124
- #
125
- # 1) :error from a missing require / spec_helper / rails_helper / spec/support
126
- # initialization — detected by error_class ∈ INFRA_ERROR_CLASSES and
127
- # first backtrace frame matching INFRA_BACKTRACE_PATHS. Origin-only match
128
- # (not `any?`): Ruby backtraces typically carry spec_helper frames below
129
- # mutation-caused errors, so matching any frame would misclassify real
130
- # mutation NameError/LoadError as :neutral.
131
- #
132
- # 2) :killed from a CrashDetector test_crashed whose sole crash class is in
133
- # INFRA_CRASH_CLASSES (ActiveRecord::StatementTimeout, Timeout::Error,
134
- # etc.). These surface under parallel workers sharing a DB file or on a
135
- # slow CI; fork.rb initially reports them as :killed, and without this
136
- # demotion the kill count inflates with infra noise. No backtrace check:
137
- # the single-class signal from CrashDetector already rules out mixed
138
- # mutation-caused failures. See EV-toid / GH #814.
139
- INFRA_ERROR_CLASSES = %w[LoadError NameError].freeze
140
- INFRA_BACKTRACE_PATHS = %r{(?:^|/)(?:spec_helper\.rb|rails_helper\.rb|spec/support/)}
141
- INFRA_CRASH_CLASSES = %w[
142
- Timeout::Error
143
- ActiveRecord::StatementTimeout
144
- ActiveRecord::Deadlocked
145
- ActiveRecord::ConnectionTimeoutError
146
- ActiveRecord::LockWaitTimeout
147
- SQLite3::BusyException
148
- ].freeze
149
- private_constant :INFRA_ERROR_CLASSES, :INFRA_BACKTRACE_PATHS, :INFRA_CRASH_CLASSES
150
-
151
- def neutralize_if_infra_error(result)
152
- return neutralize(result) if infra_crash?(result)
153
- return result unless result.error?
154
- return result unless INFRA_ERROR_CLASSES.include?(result.error_class)
155
- return result unless infra_origin?(result.error_backtrace)
156
-
157
- neutralize(result)
158
- end
159
-
160
- def infra_crash?(result)
161
- result.killed? && INFRA_CRASH_CLASSES.include?(result.error_class)
162
- end
163
-
164
- def infra_origin?(backtrace)
165
- frames = Array(backtrace)
166
- return false if frames.empty?
167
-
168
- frames.first =~ INFRA_BACKTRACE_PATHS ? true : false
169
- end
170
-
171
- def neutralize(result)
172
- Evilution::Result::MutationResult.new(
173
- mutation: result.mutation,
174
- status: :neutral,
175
- duration: result.duration,
176
- test_command: result.test_command,
177
- child_rss_kb: result.child_rss_kb,
178
- memory_delta_kb: result.memory_delta_kb,
179
- parent_rss_kb: result.parent_rss_kb,
180
- error_message: result.error_message,
181
- error_class: result.error_class,
182
- error_backtrace: result.error_backtrace
58
+ def build_sequential(notifier, pipeline)
59
+ Strategy::Sequential.new(
60
+ runner: MutationRunner.new(config: @config, cache: @cache, isolator: @isolator),
61
+ pipeline: pipeline,
62
+ notifier: notifier
183
63
  )
184
64
  end
185
65
 
186
- def neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
187
- return result unless result.survived? && baseline_result && baseline_result.failed?
188
-
189
- if config.spec_files.any?
190
- should_neutralize = true
191
- else
192
- spec_file = spec_resolver.call(result.mutation.file_path) || baseline_runner.neutralization_fallback_dir
193
- should_neutralize = baseline_result.failed_spec_files.include?(spec_file)
194
- end
195
- return result unless should_neutralize
196
-
197
- neutralize(result)
198
- end
199
-
200
- def compact_result(result)
201
- {
202
- status: result.status,
203
- duration: result.duration,
204
- killing_test: result.killing_test,
205
- test_command: result.test_command,
206
- child_rss_kb: result.child_rss_kb,
207
- memory_delta_kb: result.memory_delta_kb,
208
- parent_rss_kb: result.parent_rss_kb,
209
- error_message: result.error_message,
210
- error_class: result.error_class,
211
- error_backtrace: result.error_backtrace
212
- }
213
- end
214
-
215
- def rebuild_results(batch, compact_results)
216
- batch.zip(compact_results).map do |mutation, data|
217
- Evilution::Result::MutationResult.new(
218
- mutation: mutation,
219
- status: data[:status],
220
- duration: data[:duration],
221
- killing_test: data[:killing_test],
222
- test_command: data[:test_command],
223
- child_rss_kb: data[:child_rss_kb],
224
- memory_delta_kb: data[:memory_delta_kb],
225
- parent_rss_kb: data[:parent_rss_kb],
226
- error_message: data[:error_message],
227
- error_class: data[:error_class],
228
- error_backtrace: data[:error_backtrace]
229
- )
230
- end
231
- end
232
-
233
- def should_truncate?(survived_count)
234
- config.fail_fast? && survived_count >= config.fail_fast
235
- end
236
-
237
- def partition_cached(batch)
238
- uncached_indices = []
239
- cached_results = {}
240
-
241
- batch.each_with_index do |mutation, i|
242
- if mutation.unparseable?
243
- cached_results[i] = compact_result(build_unparseable_result(mutation))
244
- next
245
- end
246
-
247
- cached = fetch_cached_result(mutation)
248
- if cached
249
- cached_results[i] = compact_result(cached)
250
- else
251
- uncached_indices << i
252
- end
253
- end
254
-
255
- [uncached_indices, cached_results]
256
- end
257
-
258
- def execute_one(mutation, integration)
259
- return build_unparseable_result(mutation) if mutation.unparseable?
260
-
261
- execute_or_fetch(mutation) do
262
- test_command = ->(m) { integration.call(m) }
263
- isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
264
- end
265
- end
266
-
267
- def build_unparseable_result(mutation)
268
- Evilution::Result::MutationResult.new(mutation: mutation, status: :unparseable)
269
- end
270
-
271
- def merge_parallel_results(batch, uncached_indices, cached_results, worker_results)
272
- result_map = cached_results.dup
273
- uncached_indices.each_with_index { |batch_idx, worker_idx| result_map[batch_idx] = worker_results[worker_idx] }
274
- batch.each_index.map { |i| result_map[i] }
275
- end
276
-
277
- def execute_or_fetch(mutation)
278
- cached = fetch_cached_result(mutation)
279
- return cached if cached
280
-
281
- result = yield
282
- store_cached_result(mutation, result)
283
- result
284
- end
285
-
286
- def fetch_cached_result(mutation)
287
- return nil unless cache
288
-
289
- data = cache.fetch(mutation)
290
- return nil unless data
291
- return nil unless %i[killed timeout].include?(data[:status])
292
-
293
- Evilution::Result::MutationResult.new(
294
- mutation: mutation,
295
- status: data[:status],
296
- duration: data[:duration],
297
- killing_test: data[:killing_test],
298
- test_command: data[:test_command]
66
+ def build_parallel(notifier, pipeline)
67
+ Strategy::Parallel.new(
68
+ cache: @cache,
69
+ isolator: @isolator,
70
+ packer: @packer,
71
+ pipeline: pipeline,
72
+ notifier: notifier,
73
+ pool_factory: -> { build_pool },
74
+ diagnostics: @diagnostics,
75
+ config: @config
299
76
  )
300
77
  end
301
78
 
302
- def store_cached_result(mutation, result)
303
- return unless cache
304
- return unless result.killed? || result.timeout?
305
-
306
- cache.store(mutation,
307
- status: result.status,
308
- duration: result.duration,
309
- killing_test: result.killing_test,
310
- test_command: result.test_command)
311
- end
312
-
313
- def notify_result(result, index)
314
- on_result&.call(result)
315
- @progress_bar&.tick(status: result.status)
316
- diagnostics.log_progress(index, result.status)
317
- diagnostics.log_mutation_diagnostics(result)
318
- end
319
-
320
- def build_progress_bar(total)
321
- return nil if !config.progress? || config.quiet || config.verbose || !config.text? || !$stderr.tty?
322
-
323
- Evilution::Reporter::ProgressBar.new(total: total, output: $stderr)
79
+ def build_pool
80
+ Evilution::Parallel::Pool.new(
81
+ size: @config.jobs,
82
+ hooks: @hooks,
83
+ item_timeout: @config.timeout ? @config.timeout * 2 : nil
84
+ )
324
85
  end
325
86
  end
@@ -1,12 +1,11 @@
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
 
@@ -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
@@ -1,34 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "config"
4
- require_relative "ast/parser"
5
- require_relative "memory"
6
- require_relative "mutator/registry"
7
- require_relative "isolation/fork"
8
- require_relative "isolation/in_process"
9
- require_relative "integration/rspec"
10
- require_relative "integration/minitest"
11
- require_relative "reporter/json"
12
- require_relative "reporter/cli"
13
- require_relative "reporter/html"
14
- require_relative "reporter/suggestion"
15
- require_relative "git/changed_files"
16
- require_relative "result/mutation_result"
17
- require_relative "result/summary"
18
- require_relative "baseline"
19
- require_relative "cache"
20
- require_relative "parallel/pool"
21
- require_relative "session/store"
22
- require_relative "temp_dir_tracker"
23
- require_relative "rails_detector"
24
- require_relative "parallel_db_warning"
25
- require_relative "runner/subject_pipeline"
26
- require_relative "runner/mutation_planner"
27
- require_relative "runner/isolation_resolver"
28
- require_relative "runner/baseline_runner"
29
- require_relative "runner/diagnostics"
30
- require_relative "runner/mutation_executor"
31
- require_relative "runner/report_publisher"
3
+ require "fileutils"
4
+ require_relative "../evilution"
32
5
 
33
6
  class Evilution::Runner
34
7
  attr_reader :config
@@ -38,12 +11,13 @@ class Evilution::Runner
38
11
  @on_result = on_result
39
12
  @hooks = hooks
40
13
  @parser = Evilution::AST::Parser.new
41
- @registry = Evilution::Mutator::Registry.default
14
+ @registry = Evilution::Mutator::Registry.for_profile(config.profile)
42
15
  @cache = config.incremental? ? Evilution::Cache.new : nil
43
16
  end
44
17
 
45
18
  def call
46
19
  install_signal_handlers
20
+ configure_child_output
47
21
  emit_parallel_db_warning
48
22
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
23
 
@@ -156,6 +130,24 @@ class Evilution::Runner
156
130
  Evilution::ParallelDbWarning.warn_if_sqlite_parallel(config)
157
131
  end
158
132
 
133
+ def configure_child_output
134
+ unless config.quiet_children
135
+ Evilution::ChildOutput.log_dir = nil
136
+ return
137
+ end
138
+
139
+ dir = config.quiet_children_dir
140
+ begin
141
+ FileUtils.rm_rf(dir)
142
+ FileUtils.mkdir_p(dir)
143
+ rescue SystemCallError => e
144
+ raise Evilution::ConfigError,
145
+ "quiet_children_dir #{dir.inspect} is not writable: #{e.class}: #{e.message}. " \
146
+ "Pass --quiet-children-dir <writable path> or drop --quiet-children."
147
+ end
148
+ Evilution::ChildOutput.log_dir = dir
149
+ end
150
+
159
151
  def install_signal_handlers
160
152
  %w[INT TERM].each { |sig| install_signal_handler(sig) }
161
153
  end
@@ -192,3 +184,34 @@ class Evilution::Runner
192
184
  diagnostics.log_memory(phase, context)
193
185
  end
194
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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.26.0"
4
+ VERSION = "0.28.0"
5
5
  end
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
@@ -88,7 +88,9 @@ if mutations.size >= 2
88
88
  batch.zip(compact_results).map do |mutation, data|
89
89
  Evilution::Result::MutationResult.new(
90
90
  mutation: mutation, status: data[:status], duration: data[:duration],
91
- child_rss_kb: data[:child_rss_kb], memory_delta_kb: data[:memory_delta_kb]
91
+ memory: Evilution::Result::MemoryStats.new(
92
+ child_rss_kb: data[:child_rss_kb], memory_delta_kb: data[:memory_delta_kb]
93
+ )
92
94
  )
93
95
  end
94
96
  end