evilution 0.25.0 → 0.27.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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +15 -0
  3. data/.claude/prompts/architect.md +14 -1
  4. data/.claude/skills/create-issue/SKILL.md +55 -0
  5. data/.rubocop_todo.yml +7 -0
  6. data/CHANGELOG.md +38 -0
  7. data/README.md +57 -3
  8. data/lib/evilution/ast/constant_names.rb +34 -0
  9. data/lib/evilution/cache.rb +2 -0
  10. data/lib/evilution/child_output.rb +24 -0
  11. data/lib/evilution/cli/commands/run.rb +9 -0
  12. data/lib/evilution/cli/commands/version.rb +2 -0
  13. data/lib/evilution/cli/parser/options_builder.rb +16 -2
  14. data/lib/evilution/compare/invalid_input.rb +12 -0
  15. data/lib/evilution/compare.rb +1 -10
  16. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  17. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  18. data/lib/evilution/config/builders.rb +4 -0
  19. data/lib/evilution/config/env_loader.rb +12 -0
  20. data/lib/evilution/config/file_loader.rb +22 -0
  21. data/lib/evilution/config/sources.rb +14 -0
  22. data/lib/evilution/config/validators/base.rb +37 -0
  23. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  24. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  25. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  26. data/lib/evilution/config/validators/hooks.rb +12 -0
  27. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  28. data/lib/evilution/config/validators/integration.rb +11 -0
  29. data/lib/evilution/config/validators/isolation.rb +19 -0
  30. data/lib/evilution/config/validators/jobs.rb +9 -0
  31. data/lib/evilution/config/validators/preload.rb +13 -0
  32. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  33. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  34. data/lib/evilution/config/validators.rb +4 -0
  35. data/lib/evilution/config.rb +78 -268
  36. data/lib/evilution/feedback/detector.rb +15 -0
  37. data/lib/evilution/feedback/messages.rb +42 -0
  38. data/lib/evilution/feedback.rb +5 -0
  39. data/lib/evilution/integration/base.rb +4 -155
  40. data/lib/evilution/integration/loading/concern_state_cleaner.rb +49 -0
  41. data/lib/evilution/integration/loading/constant_pinner.rb +24 -0
  42. data/lib/evilution/integration/loading/mutation_applier.rb +52 -0
  43. data/lib/evilution/integration/loading/redefinition_recovery.rb +54 -0
  44. data/lib/evilution/integration/loading/source_evaluator.rb +15 -0
  45. data/lib/evilution/integration/loading/syntax_validator.rb +19 -0
  46. data/lib/evilution/integration/loading.rb +6 -0
  47. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  48. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  49. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  50. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  51. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  52. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  53. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  54. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +35 -0
  55. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  56. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  57. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  58. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  59. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  60. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  61. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  62. data/lib/evilution/integration/rspec.rb +61 -232
  63. data/lib/evilution/isolation/fork.rb +7 -2
  64. data/lib/evilution/load_path/subpath_resolver.rb +25 -0
  65. data/lib/evilution/load_path.rb +4 -0
  66. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  67. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  68. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  69. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  70. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  71. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  72. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  73. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  74. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  75. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  76. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  77. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  78. data/lib/evilution/mcp/info_tool.rb +43 -261
  79. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  80. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  81. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  82. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  83. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  84. data/lib/evilution/parallel/work_queue/channel/frame.rb +21 -0
  85. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  86. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  87. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  88. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  89. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  90. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  91. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  92. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  93. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  94. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  95. data/lib/evilution/parallel/work_queue.rb +42 -327
  96. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  97. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  98. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  99. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  100. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  101. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  102. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  103. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  104. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  105. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  106. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  107. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  108. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  109. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  110. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  111. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  112. data/lib/evilution/reporter/cli/pct.rb +9 -0
  113. data/lib/evilution/reporter/cli/section.rb +13 -0
  114. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  115. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  116. data/lib/evilution/reporter/cli.rb +79 -162
  117. data/lib/evilution/runner/isolation_resolver.rb +20 -2
  118. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +32 -0
  119. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +16 -0
  120. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +46 -0
  121. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +75 -0
  122. data/lib/evilution/runner/mutation_executor/result_cache.rb +69 -0
  123. data/lib/evilution/runner/mutation_executor/result_notifier.rb +48 -0
  124. data/lib/evilution/runner/mutation_executor/result_packer.rb +39 -0
  125. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +80 -0
  126. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +34 -0
  127. data/lib/evilution/runner/mutation_executor.rb +58 -289
  128. data/lib/evilution/runner/subject_pipeline.rb +18 -8
  129. data/lib/evilution/runner.rb +21 -0
  130. data/lib/evilution/version.rb +1 -1
  131. metadata +125 -5
  132. data/lib/evilution/mcp/session_diff_tool.rb +0 -63
  133. data/lib/evilution/mcp/session_list_tool.rb +0 -50
  134. data/lib/evilution/mcp/session_show_tool.rb +0 -57
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
4
+ class Evilution::Runner::MutationExecutor; end unless defined?(Evilution::Runner::MutationExecutor) # rubocop:disable Lint/EmptyClass
5
+ module Evilution::Runner::MutationExecutor::Strategy; end unless defined?(Evilution::Runner::MutationExecutor::Strategy)
6
+
7
+ class Evilution::Runner::MutationExecutor::Strategy::Parallel
8
+ def initialize(cache:, isolator:, packer:, pipeline:, notifier:, pool_factory:, config:, diagnostics: nil)
9
+ @cache = cache
10
+ @isolator = isolator
11
+ @packer = packer
12
+ @pipeline = pipeline
13
+ @notifier = notifier
14
+ @pool_factory = pool_factory
15
+ @diagnostics = diagnostics
16
+ @config = config
17
+ end
18
+
19
+ def call(mutations, baseline_result:, integration:)
20
+ @notifier.start(mutations.length)
21
+ pool = @pool_factory.call
22
+ state = { results: [], truncated: false, completed: 0 }
23
+ all_worker_stats = []
24
+
25
+ mutations.each_slice(@config.jobs) do |batch|
26
+ break if state[:truncated]
27
+
28
+ batch_results = run_batch(batch, pool, integration)
29
+ all_worker_stats.concat(pool.worker_stats)
30
+ process_batch(batch_results, baseline_result, state)
31
+ end
32
+
33
+ @diagnostics.log_worker_stats(@diagnostics.aggregate_worker_stats(all_worker_stats)) if @diagnostics
34
+ @notifier.finish
35
+ [state[:results], state[:truncated]]
36
+ end
37
+
38
+ private
39
+
40
+ def run_batch(batch, pool, integration)
41
+ uncached_indices, cached_results = @cache.partition(batch, packer: @packer)
42
+ worker_results = run_uncached(batch, uncached_indices, pool, integration)
43
+ compact_results = merge(batch, uncached_indices, cached_results, worker_results)
44
+ batch_results = batch.zip(compact_results).map { |m, h| @packer.rebuild(m, h) }
45
+ uncached_indices.each { |i| @cache.store(batch_results[i].mutation, batch_results[i]) }
46
+ batch.each(&:strip_sources!)
47
+ batch_results
48
+ end
49
+
50
+ def run_uncached(batch, uncached_indices, pool, integration)
51
+ return [] if uncached_indices.empty?
52
+
53
+ uncached = uncached_indices.map { |i| batch[i] }
54
+ pool.map(uncached) do |mutation|
55
+ test_command = ->(m) { integration.call(m) }
56
+ result = @isolator.call(mutation: mutation, test_command: test_command, timeout: @config.timeout)
57
+ @packer.compact(result)
58
+ end
59
+ end
60
+
61
+ def merge(batch, uncached_indices, cached_results, worker_results)
62
+ result_map = cached_results.dup
63
+ uncached_indices.each_with_index { |batch_idx, worker_idx| result_map[batch_idx] = worker_results[worker_idx] }
64
+ batch.each_index.map { |i| result_map[i] }
65
+ end
66
+
67
+ def process_batch(batch_results, baseline_result, state)
68
+ batch_results.each do |result|
69
+ result = @pipeline.call(result, baseline_result: baseline_result)
70
+ state[:results] << result
71
+ state[:completed] += 1
72
+ if @notifier.notify(result, state[:completed]) == :truncate
73
+ state[:truncated] = true
74
+ break
75
+ end
76
+ end
77
+
78
+ @diagnostics.log_memory("after batch", "#{state[:completed]} complete") if @diagnostics
79
+ end
80
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
4
+ class Evilution::Runner::MutationExecutor; end unless defined?(Evilution::Runner::MutationExecutor) # rubocop:disable Lint/EmptyClass
5
+ module Evilution::Runner::MutationExecutor::Strategy; end unless defined?(Evilution::Runner::MutationExecutor::Strategy)
6
+
7
+ class Evilution::Runner::MutationExecutor::Strategy::Sequential
8
+ def initialize(runner:, pipeline:, notifier:)
9
+ @runner = runner
10
+ @pipeline = pipeline
11
+ @notifier = notifier
12
+ end
13
+
14
+ def call(mutations, baseline_result:, integration:)
15
+ @notifier.start(mutations.length)
16
+ results = []
17
+ truncated = false
18
+
19
+ mutations.each_with_index do |mutation, index|
20
+ result = @runner.call(mutation, integration: integration)
21
+ mutation.strip_sources!
22
+ result = @pipeline.call(result, baseline_result: baseline_result)
23
+ results << result
24
+
25
+ if @notifier.notify(result, index + 1) == :truncate
26
+ truncated = true
27
+ break
28
+ end
29
+ end
30
+
31
+ @notifier.finish
32
+ [results, truncated]
33
+ end
34
+ end
@@ -1,325 +1,94 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../parallel/pool"
4
- require_relative "../reporter/progress_bar"
5
- require_relative "../result/mutation_result"
4
+ require_relative "mutation_executor/result_cache"
5
+ require_relative "mutation_executor/result_packer"
6
+ require_relative "mutation_executor/result_notifier"
7
+ require_relative "mutation_executor/mutation_runner"
8
+ require_relative "mutation_executor/neutralization_pipeline"
9
+ require_relative "mutation_executor/neutralizer/infra_error"
10
+ require_relative "mutation_executor/neutralizer/baseline_failed"
11
+ require_relative "mutation_executor/strategy/sequential"
12
+ require_relative "mutation_executor/strategy/parallel"
6
13
 
7
14
  class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
8
15
 
9
16
  class Evilution::Runner::MutationExecutor
17
+ InfraError = Neutralizer::InfraError
18
+ BaselineFailed = Neutralizer::BaselineFailed
19
+ Sequential = Strategy::Sequential
20
+ Parallel = Strategy::Parallel
21
+
10
22
  def initialize(config, isolator:, baseline_runner:, cache:, hooks:, diagnostics:, on_result: nil)
11
23
  @config = config
12
24
  @isolator = isolator
13
25
  @baseline_runner = baseline_runner
14
- @cache = cache
26
+ @cache = ResultCache.new(cache)
27
+ @packer = ResultPacker.new
15
28
  @hooks = hooks
16
29
  @diagnostics = diagnostics
17
30
  @on_result = on_result
18
31
  end
19
32
 
20
33
  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
34
+ integration = @baseline_runner.build_integration
35
+ spec_resolver = baseline_failed?(baseline_result) ? @baseline_runner.neutralization_resolver : nil
36
+ notifier = build_notifier
37
+ pipeline = build_pipeline(spec_resolver)
38
+ strategy = @config.jobs > 1 ? build_parallel(notifier, pipeline) : build_sequential(notifier, pipeline)
39
+
40
+ strategy.call(mutations, baseline_result: baseline_result, integration: integration)
29
41
  end
30
42
 
31
43
  private
32
44
 
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]
45
+ def baseline_failed?(baseline_result)
46
+ baseline_result && baseline_result.failed?
58
47
  end
59
48
 
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]]
49
+ def build_notifier
50
+ ResultNotifier.new(@config, diagnostics: @diagnostics, on_result: @on_result)
77
51
  end
78
52
 
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
53
+ def build_pipeline(spec_resolver)
54
+ NeutralizationPipeline.new(
55
+ [
56
+ InfraError.new,
57
+ BaselineFailed.new(
58
+ config: @config,
59
+ spec_resolver: spec_resolver || ->(_f) {},
60
+ fallback_dir: @baseline_runner.neutralization_fallback_dir
61
+ )
62
+ ]
84
63
  )
85
64
  end
86
65
 
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
66
+ def build_sequential(notifier, pipeline)
67
+ Sequential.new(
68
+ runner: MutationRunner.new(config: @config, cache: @cache, isolator: @isolator),
69
+ pipeline: pipeline,
70
+ notifier: notifier
183
71
  )
184
72
  end
185
73
 
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]
74
+ def build_parallel(notifier, pipeline)
75
+ Parallel.new(
76
+ cache: @cache,
77
+ isolator: @isolator,
78
+ packer: @packer,
79
+ pipeline: pipeline,
80
+ notifier: notifier,
81
+ pool_factory: -> { build_pool },
82
+ diagnostics: @diagnostics,
83
+ config: @config
299
84
  )
300
85
  end
301
86
 
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)
87
+ def build_pool
88
+ Evilution::Parallel::Pool.new(
89
+ size: @config.jobs,
90
+ hooks: @hooks,
91
+ item_timeout: @config.timeout ? @config.timeout * 2 : nil
92
+ )
324
93
  end
325
94
  end
@@ -20,13 +20,7 @@ class Evilution::Runner::SubjectPipeline
20
20
  end
21
21
 
22
22
  def target_files
23
- @target_files ||= if source_glob_target?
24
- resolve_source_glob
25
- elsif !config.target_files.empty?
26
- config.target_files
27
- else
28
- Evilution::Git::ChangedFiles.new.call
29
- end
23
+ @target_files ||= resolve_target_files
30
24
  end
31
25
 
32
26
  private
@@ -88,11 +82,27 @@ class Evilution::Runner::SubjectPipeline
88
82
 
89
83
  def filter_by_target(subjects)
90
84
  matched = subjects.select(&target_matcher)
91
- raise Evilution::Error, "no method found matching '#{config.target}'" if matched.empty?
85
+ raise Evilution::Error, build_no_match_error if matched.empty?
92
86
 
93
87
  matched
94
88
  end
95
89
 
90
+ def resolve_target_files
91
+ return resolve_source_glob if source_glob_target?
92
+ return config.target_files unless config.target_files.empty?
93
+
94
+ @used_git_fallback = true
95
+ Evilution::Git::ChangedFiles.new.call
96
+ end
97
+
98
+ def build_no_match_error
99
+ base = "no subject matched '#{config.target}'"
100
+ return base unless @used_git_fallback
101
+
102
+ "#{base}; scanned git-changed files only. Pass file paths or " \
103
+ "--target source:<glob> to scan the full codebase."
104
+ end
105
+
96
106
  def target_matcher
97
107
  target = config.target
98
108
  if target.end_with?("*")
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
3
4
  require_relative "config"
4
5
  require_relative "ast/parser"
5
6
  require_relative "memory"
@@ -22,6 +23,7 @@ require_relative "session/store"
22
23
  require_relative "temp_dir_tracker"
23
24
  require_relative "rails_detector"
24
25
  require_relative "parallel_db_warning"
26
+ require_relative "child_output"
25
27
  require_relative "runner/subject_pipeline"
26
28
  require_relative "runner/mutation_planner"
27
29
  require_relative "runner/isolation_resolver"
@@ -44,6 +46,7 @@ class Evilution::Runner
44
46
 
45
47
  def call
46
48
  install_signal_handlers
49
+ configure_child_output
47
50
  emit_parallel_db_warning
48
51
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
52
 
@@ -156,6 +159,24 @@ class Evilution::Runner
156
159
  Evilution::ParallelDbWarning.warn_if_sqlite_parallel(config)
157
160
  end
158
161
 
162
+ def configure_child_output
163
+ unless config.quiet_children
164
+ Evilution::ChildOutput.log_dir = nil
165
+ return
166
+ end
167
+
168
+ dir = config.quiet_children_dir
169
+ begin
170
+ FileUtils.rm_rf(dir)
171
+ FileUtils.mkdir_p(dir)
172
+ rescue SystemCallError => e
173
+ raise Evilution::ConfigError,
174
+ "quiet_children_dir #{dir.inspect} is not writable: #{e.class}: #{e.message}. " \
175
+ "Pass --quiet-children-dir <writable path> or drop --quiet-children."
176
+ end
177
+ Evilution::ChildOutput.log_dir = dir
178
+ end
179
+
159
180
  def install_signal_handlers
160
181
  %w[INT TERM].each { |sig| install_signal_handler(sig) }
161
182
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.25.0"
4
+ VERSION = "0.27.0"
5
5
  end