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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../suggestion"
4
+
5
+ module Evilution::Reporter::Suggestion::Templates
6
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../result"
4
+
5
+ class Evilution::Result::ErrorInfo
6
+ attr_reader :message, :klass, :backtrace
7
+
8
+ def self.from_fields(message: nil, klass: nil, backtrace: nil)
9
+ return nil if message.nil? && klass.nil? && backtrace.nil?
10
+
11
+ new(message: message, klass: klass, backtrace: backtrace)
12
+ end
13
+
14
+ def initialize(message: nil, klass: nil, backtrace: nil)
15
+ @message = message
16
+ @klass = klass
17
+ @backtrace = backtrace.nil? ? nil : backtrace.dup.freeze
18
+ freeze
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../result"
4
+
5
+ class Evilution::Result::MemoryStats
6
+ attr_reader :child_rss_kb, :memory_delta_kb, :parent_rss_kb
7
+
8
+ def self.from_fields(child_rss_kb: nil, memory_delta_kb: nil, parent_rss_kb: nil)
9
+ return nil if child_rss_kb.nil? && memory_delta_kb.nil? && parent_rss_kb.nil?
10
+
11
+ new(child_rss_kb: child_rss_kb, memory_delta_kb: memory_delta_kb, parent_rss_kb: parent_rss_kb)
12
+ end
13
+
14
+ def initialize(child_rss_kb: nil, memory_delta_kb: nil, parent_rss_kb: nil)
15
+ @child_rss_kb = child_rss_kb
16
+ @memory_delta_kb = memory_delta_kb
17
+ @parent_rss_kb = parent_rss_kb
18
+ freeze
19
+ end
20
+ end
@@ -1,20 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../result"
4
+ require_relative "error_info"
5
+ require_relative "memory_stats"
4
6
 
5
7
  class Evilution::Result::MutationResult
6
8
  STATUSES = %i[killed survived timeout error neutral equivalent unresolved unparseable].freeze
7
9
 
8
- attr_reader :mutation, :status, :duration, :killing_test, :test_command,
9
- :child_rss_kb, :memory_delta_kb, :parent_rss_kb,
10
- :error_message, :error_class, :error_backtrace
10
+ attr_reader :mutation, :status, :duration, :killing_test, :test_command, :memory, :error
11
11
 
12
- # rubocop:disable Metrics/ParameterLists
13
12
  def initialize(mutation:, status:, duration: 0.0, killing_test: nil,
14
- test_command: nil, child_rss_kb: nil, memory_delta_kb: nil,
15
- parent_rss_kb: nil, error_message: nil, error_class: nil,
16
- error_backtrace: nil)
17
- # rubocop:enable Metrics/ParameterLists
13
+ test_command: nil, memory: nil, error: nil)
18
14
  raise ArgumentError, "invalid status: #{status}" unless STATUSES.include?(status)
19
15
 
20
16
  @mutation = mutation
@@ -22,15 +18,35 @@ class Evilution::Result::MutationResult
22
18
  @duration = duration
23
19
  @killing_test = killing_test
24
20
  @test_command = test_command
25
- @child_rss_kb = child_rss_kb
26
- @memory_delta_kb = memory_delta_kb
27
- @parent_rss_kb = parent_rss_kb
28
- @error_message = error_message
29
- @error_class = error_class
30
- @error_backtrace = error_backtrace.nil? ? nil : error_backtrace.dup.freeze
21
+ @memory = memory
22
+ @error = error
31
23
  freeze
32
24
  end
33
25
 
26
+ def child_rss_kb
27
+ @memory.nil? ? nil : @memory.child_rss_kb
28
+ end
29
+
30
+ def memory_delta_kb
31
+ @memory.nil? ? nil : @memory.memory_delta_kb
32
+ end
33
+
34
+ def parent_rss_kb
35
+ @memory.nil? ? nil : @memory.parent_rss_kb
36
+ end
37
+
38
+ def error_message
39
+ @error.nil? ? nil : @error.message
40
+ end
41
+
42
+ def error_class
43
+ @error.nil? ? nil : @error.klass
44
+ end
45
+
46
+ def error_backtrace
47
+ @error.nil? ? nil : @error.backtrace
48
+ end
49
+
34
50
  def killed?
35
51
  status == :killed
36
52
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../runner"
3
4
  require_relative "../baseline"
4
5
  require_relative "../spec_resolver"
5
6
  require_relative "../integration/rspec"
@@ -8,8 +9,6 @@ require_relative "../example_filter"
8
9
  require_relative "../spec_ast_cache"
9
10
  require_relative "../source_ast_cache"
10
11
 
11
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
12
-
13
12
  unless defined?(Evilution::Runner::INTEGRATIONS)
14
13
  Evilution::Runner::INTEGRATIONS = {
15
14
  rspec: Evilution::Integration::RSpec,
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../runner"
3
4
  require_relative "../memory"
4
5
  require_relative "../parallel/pool"
5
6
 
6
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
7
-
8
7
  class Evilution::Runner::Diagnostics
9
8
  def initialize(config, stderr: $stderr)
10
9
  @config = config
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../runner"
3
4
  require_relative "../isolation/fork"
4
5
  require_relative "../isolation/in_process"
5
6
  require_relative "../rails_detector"
6
7
 
7
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
8
-
9
8
  class Evilution::Runner::IsolationResolver
10
9
  PRELOAD_CANDIDATES = [
11
10
  File.join("spec", "rails_helper.rb"),
11
+ File.join("spec", "spec_helper.rb"),
12
12
  File.join("test", "test_helper.rb")
13
13
  ].freeze
14
14
 
@@ -30,13 +30,15 @@ class Evilution::Runner::IsolationResolver
30
30
 
31
31
  def perform_preload
32
32
  return if config.preload == false
33
+ return unless should_preload?
33
34
 
34
35
  path = resolve_preload_path
35
36
  return unless path
36
- return unless should_preload?
37
37
 
38
38
  prepare_load_path_for_preload
39
39
  require File.expand_path(path)
40
+ rescue Evilution::ConfigError
41
+ raise
40
42
  rescue ScriptError, StandardError => e
41
43
  raise Evilution::ConfigError.new(
42
44
  "failed to preload #{path.inspect}: #{e.class}: #{e.message}",
@@ -123,7 +125,11 @@ class Evilution::Runner::IsolationResolver
123
125
  abs = File.join(root, rel)
124
126
  return abs if File.file?(abs)
125
127
  end
126
- nil
128
+
129
+ raise Evilution::ConfigError,
130
+ "Preload file not found. Tried: [#{PRELOAD_CANDIDATES.join(", ")}]. " \
131
+ "Pass --preload <file> or set preload: in .evilution.yml. " \
132
+ "Use --no-preload (or preload: false) to disable preloading entirely."
127
133
  end
128
134
 
129
135
  # When the user explicitly requests InProcess on a Rails project, warn once
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../mutation_executor"
4
+ require_relative "../../result/mutation_result"
5
+
6
+ class Evilution::Runner::MutationExecutor::MutationRunner
7
+ def initialize(config:, cache:, isolator:)
8
+ @config = config
9
+ @cache = cache
10
+ @isolator = isolator
11
+ end
12
+
13
+ def call(mutation, integration:)
14
+ return unparseable_result(mutation) if mutation.unparseable?
15
+
16
+ cached = @cache.fetch(mutation)
17
+ return cached if cached
18
+
19
+ test_command = ->(m) { integration.call(m) }
20
+ result = @isolator.call(mutation: mutation, test_command: test_command, timeout: @config.timeout)
21
+ @cache.store(mutation, result)
22
+ result
23
+ end
24
+
25
+ private
26
+
27
+ def unparseable_result(mutation)
28
+ Evilution::Result::MutationResult.new(mutation: mutation, status: :unparseable)
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../mutation_executor"
4
+
5
+ class Evilution::Runner::MutationExecutor::NeutralizationPipeline
6
+ def initialize(neutralizers)
7
+ @neutralizers = neutralizers
8
+ end
9
+
10
+ def call(result, **ctx)
11
+ @neutralizers.reduce(result) do |acc, nz|
12
+ ctx.empty? ? nz.call(acc) : nz.call(acc, **ctx)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../neutralizer"
4
+ require_relative "../../../result/mutation_result"
5
+
6
+ class Evilution::Runner::MutationExecutor::Neutralizer::BaselineFailed
7
+ def initialize(config:, spec_resolver:, fallback_dir:)
8
+ @config = config
9
+ @spec_resolver = spec_resolver
10
+ @fallback_dir = fallback_dir
11
+ end
12
+
13
+ def call(result, baseline_result:)
14
+ return result unless result.survived? && baseline_result && baseline_result.failed?
15
+
16
+ if @config.spec_files.any?
17
+ should_neutralize = true
18
+ else
19
+ spec_file = @spec_resolver.call(result.mutation.file_path) || @fallback_dir
20
+ should_neutralize = baseline_result.failed_spec_files.include?(spec_file)
21
+ end
22
+ return result unless should_neutralize
23
+
24
+ neutralize(result)
25
+ end
26
+
27
+ private
28
+
29
+ def neutralize(result)
30
+ Evilution::Result::MutationResult.new(
31
+ mutation: result.mutation,
32
+ status: :neutral,
33
+ duration: result.duration,
34
+ test_command: result.test_command,
35
+ memory: result.memory,
36
+ error: result.error
37
+ )
38
+ end
39
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../neutralizer"
4
+ require_relative "../../../result/mutation_result"
5
+
6
+ # Reclassify results as :neutral when the failure was caused by test
7
+ # infrastructure rather than by the mutation. Two independent paths:
8
+ #
9
+ # 1) :error from a missing require / spec_helper / rails_helper / spec/support
10
+ # initialization — detected by error_class ∈ INFRA_ERROR_CLASSES and
11
+ # first backtrace frame matching INFRA_BACKTRACE_PATHS. Origin-only match
12
+ # (not `any?`): Ruby backtraces typically carry spec_helper frames below
13
+ # mutation-caused errors, so matching any frame would misclassify real
14
+ # mutation NameError/LoadError as :neutral.
15
+ #
16
+ # 2) :killed from a CrashDetector test_crashed whose sole crash class is in
17
+ # INFRA_CRASH_CLASSES (ActiveRecord::StatementTimeout, Timeout::Error,
18
+ # etc.). These surface under parallel workers sharing a DB file or on a
19
+ # slow CI; fork.rb initially reports them as :killed, and without this
20
+ # demotion the kill count inflates with infra noise. No backtrace check:
21
+ # the single-class signal from CrashDetector already rules out mixed
22
+ # mutation-caused failures. See EV-toid / GH #814.
23
+ class Evilution::Runner::MutationExecutor::Neutralizer::InfraError
24
+ INFRA_ERROR_CLASSES = %w[LoadError NameError].freeze
25
+ INFRA_BACKTRACE_PATHS = %r{(?:^|/)(?:spec_helper\.rb|rails_helper\.rb|spec/support/)}
26
+ INFRA_CRASH_CLASSES = %w[
27
+ Timeout::Error
28
+ ActiveRecord::StatementTimeout
29
+ ActiveRecord::Deadlocked
30
+ ActiveRecord::ConnectionTimeoutError
31
+ ActiveRecord::LockWaitTimeout
32
+ SQLite3::BusyException
33
+ ].freeze
34
+ private_constant :INFRA_ERROR_CLASSES, :INFRA_BACKTRACE_PATHS, :INFRA_CRASH_CLASSES
35
+
36
+ def call(result, **_ctx)
37
+ return neutralize(result) if infra_crash?(result)
38
+ return result unless result.error?
39
+ return result unless INFRA_ERROR_CLASSES.include?(result.error_class)
40
+ return result unless infra_origin?(result.error_backtrace)
41
+
42
+ neutralize(result)
43
+ end
44
+
45
+ private
46
+
47
+ def infra_crash?(result)
48
+ result.killed? && INFRA_CRASH_CLASSES.include?(result.error_class)
49
+ end
50
+
51
+ def infra_origin?(backtrace)
52
+ frames = Array(backtrace)
53
+ return false if frames.empty?
54
+
55
+ frames.first =~ INFRA_BACKTRACE_PATHS ? true : false
56
+ end
57
+
58
+ def neutralize(result)
59
+ Evilution::Result::MutationResult.new(
60
+ mutation: result.mutation,
61
+ status: :neutral,
62
+ duration: result.duration,
63
+ test_command: result.test_command,
64
+ memory: result.memory,
65
+ error: result.error
66
+ )
67
+ end
68
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../mutation_executor"
4
+
5
+ # Namespace for MutationExecutor's neutralization rules (InfraError,
6
+ # BaselineFailed). Concrete neutralizer classes live in neutralizer/*.rb and
7
+ # are autoloaded on first reference.
8
+ module Evilution::Runner::MutationExecutor::Neutralizer
9
+ autoload :InfraError, File.expand_path("neutralizer/infra_error", __dir__)
10
+ autoload :BaselineFailed, File.expand_path("neutralizer/baseline_failed", __dir__)
11
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../mutation_executor"
4
+ require_relative "../../result/mutation_result"
5
+
6
+ class Evilution::Runner::MutationExecutor::ResultCache
7
+ CACHEABLE_STATUSES = %i[killed timeout].freeze
8
+ private_constant :CACHEABLE_STATUSES
9
+
10
+ def initialize(backend)
11
+ @backend = backend
12
+ end
13
+
14
+ def fetch(mutation)
15
+ return nil unless @backend
16
+
17
+ data = @backend.fetch(mutation)
18
+ return nil unless data
19
+ return nil unless CACHEABLE_STATUSES.include?(data[:status])
20
+
21
+ Evilution::Result::MutationResult.new(
22
+ mutation: mutation,
23
+ status: data[:status],
24
+ duration: data[:duration],
25
+ killing_test: data[:killing_test],
26
+ test_command: data[:test_command]
27
+ )
28
+ end
29
+
30
+ def store(mutation, result)
31
+ return unless @backend
32
+ return unless result.killed? || result.timeout?
33
+
34
+ @backend.store(mutation,
35
+ status: result.status,
36
+ duration: result.duration,
37
+ killing_test: result.killing_test,
38
+ test_command: result.test_command)
39
+ end
40
+
41
+ def partition(batch, packer:)
42
+ uncached_indices = []
43
+ cached_results = {}
44
+
45
+ batch.each_with_index do |mutation, i|
46
+ if mutation.unparseable?
47
+ cached_results[i] = packer.compact(unparseable_result(mutation))
48
+ next
49
+ end
50
+
51
+ cached = fetch(mutation)
52
+ if cached
53
+ cached_results[i] = packer.compact(cached)
54
+ else
55
+ uncached_indices << i
56
+ end
57
+ end
58
+
59
+ [uncached_indices, cached_results]
60
+ end
61
+
62
+ private
63
+
64
+ def unparseable_result(mutation)
65
+ Evilution::Result::MutationResult.new(mutation: mutation, status: :unparseable)
66
+ end
67
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../mutation_executor"
4
+ require_relative "../../reporter/progress_bar"
5
+
6
+ class Evilution::Runner::MutationExecutor::ResultNotifier
7
+ def initialize(config, diagnostics:, on_result:)
8
+ @config = config
9
+ @diagnostics = diagnostics
10
+ @on_result = on_result
11
+ @survived_count = 0
12
+ @progress_bar = nil
13
+ end
14
+
15
+ attr_reader :survived_count
16
+
17
+ def start(total)
18
+ @survived_count = 0
19
+ @progress_bar = build_progress_bar(total)
20
+ end
21
+
22
+ def notify(result, index)
23
+ @on_result.call(result) if @on_result
24
+ @progress_bar.tick(status: result.status) if @progress_bar
25
+ @diagnostics.log_progress(index, result.status)
26
+ @diagnostics.log_mutation_diagnostics(result)
27
+ @survived_count += 1 if result.survived?
28
+ truncate? ? :truncate : :continue
29
+ end
30
+
31
+ def finish
32
+ @progress_bar.finish if @progress_bar
33
+ end
34
+
35
+ private
36
+
37
+ def truncate?
38
+ @config.fail_fast? && @survived_count >= @config.fail_fast
39
+ end
40
+
41
+ def build_progress_bar(total)
42
+ return nil if !@config.progress? || @config.quiet || @config.verbose || !@config.text? || !$stderr.tty?
43
+
44
+ Evilution::Reporter::ProgressBar.new(total: total, output: $stderr)
45
+ end
46
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../mutation_executor"
4
+ require_relative "../../result/mutation_result"
5
+
6
+ class Evilution::Runner::MutationExecutor::ResultPacker
7
+ def compact(result)
8
+ {
9
+ status: result.status,
10
+ duration: result.duration,
11
+ killing_test: result.killing_test,
12
+ test_command: result.test_command,
13
+ child_rss_kb: result.child_rss_kb,
14
+ memory_delta_kb: result.memory_delta_kb,
15
+ parent_rss_kb: result.parent_rss_kb,
16
+ error_message: result.error_message,
17
+ error_class: result.error_class,
18
+ error_backtrace: result.error_backtrace
19
+ }
20
+ end
21
+
22
+ def rebuild(mutation, data)
23
+ Evilution::Result::MutationResult.new(
24
+ mutation: mutation,
25
+ status: data[:status],
26
+ duration: data[:duration],
27
+ killing_test: data[:killing_test],
28
+ test_command: data[:test_command],
29
+ memory: Evilution::Result::MemoryStats.from_fields(
30
+ child_rss_kb: data[:child_rss_kb],
31
+ memory_delta_kb: data[:memory_delta_kb],
32
+ parent_rss_kb: data[:parent_rss_kb]
33
+ ),
34
+ error: Evilution::Result::ErrorInfo.from_fields(
35
+ message: data[:error_message],
36
+ klass: data[:error_class],
37
+ backtrace: data[:error_backtrace]
38
+ )
39
+ )
40
+ end
41
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../strategy"
4
+
5
+ class Evilution::Runner::MutationExecutor::Strategy::Parallel
6
+ def initialize(cache:, isolator:, packer:, pipeline:, notifier:, pool_factory:, config:, diagnostics: nil)
7
+ @cache = cache
8
+ @isolator = isolator
9
+ @packer = packer
10
+ @pipeline = pipeline
11
+ @notifier = notifier
12
+ @pool_factory = pool_factory
13
+ @diagnostics = diagnostics
14
+ @config = config
15
+ end
16
+
17
+ def call(mutations, baseline_result:, integration:)
18
+ @notifier.start(mutations.length)
19
+ pool = @pool_factory.call
20
+ state = { results: [], truncated: false, completed: 0 }
21
+ all_worker_stats = []
22
+
23
+ mutations.each_slice(@config.jobs) do |batch|
24
+ break if state[:truncated]
25
+
26
+ batch_results = run_batch(batch, pool, integration)
27
+ all_worker_stats.concat(pool.worker_stats)
28
+ process_batch(batch_results, baseline_result, state)
29
+ end
30
+
31
+ @diagnostics.log_worker_stats(@diagnostics.aggregate_worker_stats(all_worker_stats)) if @diagnostics
32
+ @notifier.finish
33
+ [state[:results], state[:truncated]]
34
+ end
35
+
36
+ private
37
+
38
+ def run_batch(batch, pool, integration)
39
+ uncached_indices, cached_results = @cache.partition(batch, packer: @packer)
40
+ worker_results = run_uncached(batch, uncached_indices, pool, integration)
41
+ compact_results = merge(batch, uncached_indices, cached_results, worker_results)
42
+ batch_results = batch.zip(compact_results).map { |m, h| @packer.rebuild(m, h) }
43
+ uncached_indices.each { |i| @cache.store(batch_results[i].mutation, batch_results[i]) }
44
+ batch.each(&:strip_sources!)
45
+ batch_results
46
+ end
47
+
48
+ def run_uncached(batch, uncached_indices, pool, integration)
49
+ return [] if uncached_indices.empty?
50
+
51
+ uncached = uncached_indices.map { |i| batch[i] }
52
+ pool.map(uncached) do |mutation|
53
+ test_command = ->(m) { integration.call(m) }
54
+ result = @isolator.call(mutation: mutation, test_command: test_command, timeout: @config.timeout)
55
+ @packer.compact(result)
56
+ end
57
+ end
58
+
59
+ def merge(batch, uncached_indices, cached_results, worker_results)
60
+ result_map = cached_results.dup
61
+ uncached_indices.each_with_index { |batch_idx, worker_idx| result_map[batch_idx] = worker_results[worker_idx] }
62
+ batch.each_index.map { |i| result_map[i] }
63
+ end
64
+
65
+ def process_batch(batch_results, baseline_result, state)
66
+ batch_results.each do |result|
67
+ result = @pipeline.call(result, baseline_result: baseline_result)
68
+ state[:results] << result
69
+ state[:completed] += 1
70
+ if @notifier.notify(result, state[:completed]) == :truncate
71
+ state[:truncated] = true
72
+ break
73
+ end
74
+ end
75
+
76
+ @diagnostics.log_memory("after batch", "#{state[:completed]} complete") if @diagnostics
77
+ end
78
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../strategy"
4
+
5
+ class Evilution::Runner::MutationExecutor::Strategy::Sequential
6
+ def initialize(runner:, pipeline:, notifier:)
7
+ @runner = runner
8
+ @pipeline = pipeline
9
+ @notifier = notifier
10
+ end
11
+
12
+ def call(mutations, baseline_result:, integration:)
13
+ @notifier.start(mutations.length)
14
+ results = []
15
+ truncated = false
16
+
17
+ mutations.each_with_index do |mutation, index|
18
+ result = @runner.call(mutation, integration: integration)
19
+ mutation.strip_sources!
20
+ result = @pipeline.call(result, baseline_result: baseline_result)
21
+ results << result
22
+
23
+ if @notifier.notify(result, index + 1) == :truncate
24
+ truncated = true
25
+ break
26
+ end
27
+ end
28
+
29
+ @notifier.finish
30
+ [results, truncated]
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../mutation_executor"
4
+
5
+ # Namespace for MutationExecutor's execution strategies (Sequential, Parallel).
6
+ # Concrete strategy classes live in strategy/{sequential,parallel}.rb and are
7
+ # autoloaded on first reference.
8
+ module Evilution::Runner::MutationExecutor::Strategy
9
+ autoload :Sequential, File.expand_path("strategy/sequential", __dir__)
10
+ autoload :Parallel, File.expand_path("strategy/parallel", __dir__)
11
+ end