evilution 0.27.0 → 0.29.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +65 -0
  3. data/.rubocop_todo.yml +0 -1
  4. data/CHANGELOG.md +39 -0
  5. data/README.md +19 -0
  6. data/lib/evilution/ast/constant_names.rb +28 -11
  7. data/lib/evilution/ast/pattern/parser.rb +29 -17
  8. data/lib/evilution/baseline.rb +5 -4
  9. data/lib/evilution/cli/commands/session_diff.rb +6 -4
  10. data/lib/evilution/cli/commands/subjects.rb +6 -3
  11. data/lib/evilution/cli/commands/util_mutation.rb +24 -19
  12. data/lib/evilution/cli/parser/command_extractor.rb +9 -11
  13. data/lib/evilution/cli/parser/file_args.rb +3 -1
  14. data/lib/evilution/cli/parser/options_builder.rb +36 -1
  15. data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
  16. data/lib/evilution/cli/parser.rb +18 -20
  17. data/lib/evilution/cli/printers/environment.rb +19 -19
  18. data/lib/evilution/cli/printers/session_diff.rb +8 -8
  19. data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
  20. data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
  21. data/lib/evilution/compare/diff_extractor.rb +6 -0
  22. data/lib/evilution/compare/fingerprint.rb +15 -72
  23. data/lib/evilution/compare/line_normalizer.rb +72 -0
  24. data/lib/evilution/compare/normalizer.rb +27 -9
  25. data/lib/evilution/config/validators/profile.rb +11 -0
  26. data/lib/evilution/config.rb +49 -32
  27. data/lib/evilution/disable_comment.rb +21 -12
  28. data/lib/evilution/integration/crash_detector.rb +2 -2
  29. data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
  30. data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
  31. data/lib/evilution/integration/minitest.rb +25 -16
  32. data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
  33. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +11 -3
  34. data/lib/evilution/integration/rspec.rb +4 -0
  35. data/lib/evilution/isolation/fork.rb +43 -28
  36. data/lib/evilution/isolation/in_process.rb +10 -6
  37. data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
  38. data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
  39. data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
  40. data/lib/evilution/mcp/info_tool.rb +7 -3
  41. data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
  42. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
  43. data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
  44. data/lib/evilution/mcp/mutate_tool.rb +27 -14
  45. data/lib/evilution/mcp/session_tool.rb +27 -20
  46. data/lib/evilution/mutation.rb +60 -42
  47. data/lib/evilution/mutator/base.rb +23 -21
  48. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
  49. data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
  50. data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
  51. data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
  52. data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
  53. data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
  54. data/lib/evilution/mutator/operator/case_when.rb +7 -5
  55. data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
  56. data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
  57. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
  58. data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
  59. data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
  60. data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
  61. data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
  62. data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
  63. data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
  64. data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
  65. data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
  66. data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
  67. data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
  68. data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
  69. data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
  70. data/lib/evilution/mutator/registry.rb +20 -0
  71. data/lib/evilution/parallel/work_queue/channel/frame.rb +5 -1
  72. data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
  73. data/lib/evilution/parallel/work_queue/worker/loop.rb +1 -1
  74. data/lib/evilution/parallel/work_queue/worker.rb +10 -7
  75. data/lib/evilution/parallel/work_queue.rb +35 -18
  76. data/lib/evilution/process_cleanup.rb +19 -0
  77. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
  78. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
  79. data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
  80. data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
  81. data/lib/evilution/reporter/html/escape.rb +1 -1
  82. data/lib/evilution/reporter/html/section.rb +1 -1
  83. data/lib/evilution/reporter/html/sections.rb +4 -2
  84. data/lib/evilution/reporter/html/stylesheet.rb +1 -1
  85. data/lib/evilution/reporter/html.rb +8 -3
  86. data/lib/evilution/reporter/json.rb +52 -18
  87. data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
  88. data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
  89. data/lib/evilution/reporter/suggestion/registry.rb +1 -5
  90. data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
  91. data/lib/evilution/reporter/suggestion/templates/minitest.rb +361 -649
  92. data/lib/evilution/reporter/suggestion/templates/rspec.rb +362 -603
  93. data/lib/evilution/reporter/suggestion/templates.rb +6 -0
  94. data/lib/evilution/result/error_info.rb +20 -0
  95. data/lib/evilution/result/memory_stats.rb +20 -0
  96. data/lib/evilution/result/mutation_result.rb +30 -14
  97. data/lib/evilution/runner/baseline_runner.rb +16 -10
  98. data/lib/evilution/runner/diagnostics.rb +14 -11
  99. data/lib/evilution/runner/isolation_resolver.rb +12 -11
  100. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +1 -3
  101. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +1 -2
  102. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +3 -10
  103. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +3 -10
  104. data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
  105. data/lib/evilution/runner/mutation_executor/result_cache.rb +4 -4
  106. data/lib/evilution/runner/mutation_executor/result_notifier.rb +1 -3
  107. data/lib/evilution/runner/mutation_executor/result_packer.rb +11 -9
  108. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +33 -13
  109. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +2 -4
  110. data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
  111. data/lib/evilution/runner/mutation_executor.rb +14 -20
  112. data/lib/evilution/runner/mutation_planner.rb +38 -19
  113. data/lib/evilution/runner/report_publisher.rb +1 -2
  114. data/lib/evilution/runner/subject_pipeline.rb +22 -13
  115. data/lib/evilution/runner.rb +36 -34
  116. data/lib/evilution/session/diff.rb +15 -6
  117. data/lib/evilution/spec_ast_cache.rb +26 -12
  118. data/lib/evilution/version.rb +1 -1
  119. data/lib/evilution.rb +1 -0
  120. data/script/memory_check +14 -6
  121. data/scripts/benchmark_density +10 -9
  122. data/scripts/compare_mutations +38 -21
  123. data/scripts/mutant_json_adapter +7 -4
  124. metadata +15 -3
  125. 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,
@@ -31,18 +30,25 @@ class Evilution::Runner::BaselineRunner
31
30
 
32
31
  def build_integration
33
32
  klass = integration_class
34
- test_files = config.spec_files.empty? ? nil : config.spec_files
35
- kwargs = {
36
- test_files: test_files,
33
+ kwargs = base_integration_kwargs
34
+ kwargs.merge!(rspec_integration_kwargs) if klass == Evilution::Integration::RSpec
35
+ klass.new(**kwargs)
36
+ end
37
+
38
+ def base_integration_kwargs
39
+ {
40
+ test_files: config.spec_files.empty? ? nil : config.spec_files,
37
41
  hooks: hooks,
38
42
  fallback_to_full_suite: config.fallback_to_full_suite?,
39
43
  spec_selector: config.spec_selector
40
44
  }
41
- if klass == Evilution::Integration::RSpec
42
- kwargs[:related_specs_heuristic] = config.related_specs_heuristic?
43
- kwargs[:example_filter] = build_example_filter
44
- end
45
- klass.new(**kwargs)
45
+ end
46
+
47
+ def rspec_integration_kwargs
48
+ {
49
+ related_specs_heuristic: config.related_specs_heuristic?,
50
+ example_filter: build_example_filter
51
+ }
46
52
  end
47
53
 
48
54
  def call(subjects)
@@ -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
@@ -33,19 +32,23 @@ class Evilution::Runner::Diagnostics
33
32
  def log_mutation_diagnostics(result)
34
33
  return unless verbose?
35
34
 
36
- parts = []
37
- parts << format("child_rss: %<mb>.1f MB", mb: result.child_rss_kb / 1024.0) if result.child_rss_kb
35
+ parts = mutation_metric_parts(result)
36
+ stderr.write("[verbose] #{result.mutation}: #{parts.join(", ")}\n") unless parts.empty?
38
37
 
39
- if result.memory_delta_kb
40
- sign = result.memory_delta_kb.negative? ? "" : "+"
41
- parts << format("delta: %<sign>s%<mb>.1f MB", sign: sign, mb: result.memory_delta_kb / 1024.0)
42
- end
38
+ log_mutation_error(result) if result.error?
39
+ end
43
40
 
41
+ def mutation_metric_parts(result)
42
+ parts = []
43
+ parts << format("child_rss: %<mb>.1f MB", mb: result.child_rss_kb / 1024.0) if result.child_rss_kb
44
+ parts << format_memory_delta(result.memory_delta_kb) if result.memory_delta_kb
44
45
  parts << gc_stats_string
46
+ parts
47
+ end
45
48
 
46
- stderr.write("[verbose] #{result.mutation}: #{parts.join(", ")}\n") unless parts.empty?
47
-
48
- log_mutation_error(result) if result.error?
49
+ def format_memory_delta(delta_kb)
50
+ sign = delta_kb.negative? ? "" : "+"
51
+ format("delta: %<sign>s%<mb>.1f MB", sign: sign, mb: delta_kb / 1024.0)
49
52
  end
50
53
 
51
54
  def log_worker_stats(stats)
@@ -1,11 +1,10 @@
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"),
@@ -109,16 +108,18 @@ class Evilution::Runner::IsolationResolver
109
108
  end
110
109
 
111
110
  def resolve_preload_path
112
- if config.preload.is_a?(String)
113
- unless File.file?(config.preload)
114
- raise Evilution::ConfigError.new(
115
- "preload file not found: #{config.preload.inspect}",
116
- file: config.preload
117
- )
118
- end
119
- return config.preload
120
- end
111
+ return resolve_explicit_preload(config.preload) if config.preload.is_a?(String)
112
+
113
+ resolve_autodetected_preload
114
+ end
115
+
116
+ def resolve_explicit_preload(path)
117
+ return path if File.file?(path)
118
+
119
+ raise Evilution::ConfigError.new("preload file not found: #{path.inspect}", file: path)
120
+ end
121
121
 
122
+ def resolve_autodetected_preload
122
123
  root = detected_rails_root
123
124
  return nil unless root
124
125
 
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../mutation_executor"
3
4
  require_relative "../../result/mutation_result"
4
5
 
5
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
6
- class Evilution::Runner::MutationExecutor; end unless defined?(Evilution::Runner::MutationExecutor) # rubocop:disable Lint/EmptyClass
7
-
8
6
  class Evilution::Runner::MutationExecutor::MutationRunner
9
7
  def initialize(config:, cache:, isolator:)
10
8
  @config = config
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
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
3
+ require_relative "../mutation_executor"
5
4
 
6
5
  class Evilution::Runner::MutationExecutor::NeutralizationPipeline
7
6
  def initialize(neutralizers)
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../neutralizer"
3
4
  require_relative "../../../result/mutation_result"
4
5
 
5
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
6
- class Evilution::Runner::MutationExecutor; end unless defined?(Evilution::Runner::MutationExecutor) # rubocop:disable Lint/EmptyClass
7
- module Evilution::Runner::MutationExecutor::Neutralizer; end unless defined?(Evilution::Runner::MutationExecutor::Neutralizer)
8
-
9
6
  class Evilution::Runner::MutationExecutor::Neutralizer::BaselineFailed
10
7
  def initialize(config:, spec_resolver:, fallback_dir:)
11
8
  @config = config
@@ -35,12 +32,8 @@ class Evilution::Runner::MutationExecutor::Neutralizer::BaselineFailed
35
32
  status: :neutral,
36
33
  duration: result.duration,
37
34
  test_command: result.test_command,
38
- child_rss_kb: result.child_rss_kb,
39
- memory_delta_kb: result.memory_delta_kb,
40
- parent_rss_kb: result.parent_rss_kb,
41
- error_message: result.error_message,
42
- error_class: result.error_class,
43
- error_backtrace: result.error_backtrace
35
+ memory: result.memory,
36
+ error: result.error
44
37
  )
45
38
  end
46
39
  end
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../neutralizer"
3
4
  require_relative "../../../result/mutation_result"
4
5
 
5
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
6
- class Evilution::Runner::MutationExecutor; end unless defined?(Evilution::Runner::MutationExecutor) # rubocop:disable Lint/EmptyClass
7
- module Evilution::Runner::MutationExecutor::Neutralizer; end unless defined?(Evilution::Runner::MutationExecutor::Neutralizer)
8
-
9
6
  # Reclassify results as :neutral when the failure was caused by test
10
7
  # infrastructure rather than by the mutation. Two independent paths:
11
8
  #
@@ -64,12 +61,8 @@ class Evilution::Runner::MutationExecutor::Neutralizer::InfraError
64
61
  status: :neutral,
65
62
  duration: result.duration,
66
63
  test_command: result.test_command,
67
- child_rss_kb: result.child_rss_kb,
68
- memory_delta_kb: result.memory_delta_kb,
69
- parent_rss_kb: result.parent_rss_kb,
70
- error_message: result.error_message,
71
- error_class: result.error_class,
72
- error_backtrace: result.error_backtrace
64
+ memory: result.memory,
65
+ error: result.error
73
66
  )
74
67
  end
75
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
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../mutation_executor"
3
4
  require_relative "../../result/mutation_result"
4
5
 
5
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
6
- class Evilution::Runner::MutationExecutor; end unless defined?(Evilution::Runner::MutationExecutor) # rubocop:disable Lint/EmptyClass
7
-
8
6
  class Evilution::Runner::MutationExecutor::ResultCache
9
7
  CACHEABLE_STATUSES = %i[killed timeout].freeze
10
8
  private_constant :CACHEABLE_STATUSES
11
9
 
10
+ Partition = Data.define(:uncached_indices, :cached_results)
11
+
12
12
  def initialize(backend)
13
13
  @backend = backend
14
14
  end
@@ -58,7 +58,7 @@ class Evilution::Runner::MutationExecutor::ResultCache
58
58
  end
59
59
  end
60
60
 
61
- [uncached_indices, cached_results]
61
+ Partition.new(uncached_indices: uncached_indices, cached_results: cached_results)
62
62
  end
63
63
 
64
64
  private
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../mutation_executor"
3
4
  require_relative "../../reporter/progress_bar"
4
5
 
5
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
6
- class Evilution::Runner::MutationExecutor; end unless defined?(Evilution::Runner::MutationExecutor) # rubocop:disable Lint/EmptyClass
7
-
8
6
  class Evilution::Runner::MutationExecutor::ResultNotifier
9
7
  def initialize(config, diagnostics:, on_result:)
10
8
  @config = config
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../mutation_executor"
3
4
  require_relative "../../result/mutation_result"
4
5
 
5
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
6
- class Evilution::Runner::MutationExecutor; end unless defined?(Evilution::Runner::MutationExecutor) # rubocop:disable Lint/EmptyClass
7
-
8
6
  class Evilution::Runner::MutationExecutor::ResultPacker
9
7
  def compact(result)
10
8
  {
@@ -28,12 +26,16 @@ class Evilution::Runner::MutationExecutor::ResultPacker
28
26
  duration: data[:duration],
29
27
  killing_test: data[:killing_test],
30
28
  test_command: data[:test_command],
31
- child_rss_kb: data[:child_rss_kb],
32
- memory_delta_kb: data[:memory_delta_kb],
33
- parent_rss_kb: data[:parent_rss_kb],
34
- error_message: data[:error_message],
35
- error_class: data[:error_class],
36
- error_backtrace: data[:error_backtrace]
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
+ )
37
39
  )
38
40
  end
39
41
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
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)
3
+ require_relative "../strategy"
6
4
 
7
5
  class Evilution::Runner::MutationExecutor::Strategy::Parallel
8
6
  def initialize(cache:, isolator:, packer:, pipeline:, notifier:, pool_factory:, config:, diagnostics: nil)
@@ -20,8 +18,17 @@ class Evilution::Runner::MutationExecutor::Strategy::Parallel
20
18
  @notifier.start(mutations.length)
21
19
  pool = @pool_factory.call
22
20
  state = { results: [], truncated: false, completed: 0 }
23
- all_worker_stats = []
21
+ all_worker_stats = run_batches(mutations, pool, baseline_result, integration, state)
24
22
 
23
+ log_worker_diagnostics(all_worker_stats)
24
+ @notifier.finish
25
+ build_result(state)
26
+ end
27
+
28
+ private
29
+
30
+ def run_batches(mutations, pool, baseline_result, integration, state)
31
+ all_worker_stats = []
25
32
  mutations.each_slice(@config.jobs) do |batch|
26
33
  break if state[:truncated]
27
34
 
@@ -29,24 +36,37 @@ class Evilution::Runner::MutationExecutor::Strategy::Parallel
29
36
  all_worker_stats.concat(pool.worker_stats)
30
37
  process_batch(batch_results, baseline_result, state)
31
38
  end
39
+ all_worker_stats
40
+ end
32
41
 
33
- @diagnostics.log_worker_stats(@diagnostics.aggregate_worker_stats(all_worker_stats)) if @diagnostics
34
- @notifier.finish
35
- [state[:results], state[:truncated]]
42
+ def log_worker_diagnostics(all_worker_stats)
43
+ return unless @diagnostics
44
+
45
+ @diagnostics.log_worker_stats(@diagnostics.aggregate_worker_stats(all_worker_stats))
36
46
  end
37
47
 
38
- private
48
+ def build_result(state)
49
+ Evilution::Runner::MutationExecutor::ExecutionResult.new(results: state[:results], truncated: state[:truncated])
50
+ end
39
51
 
40
52
  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]) }
53
+ partition = @cache.partition(batch, packer: @packer)
54
+ worker_results = run_uncached(batch, partition.uncached_indices, pool, integration)
55
+ compact_results = merge(batch, partition.uncached_indices, partition.cached_results, worker_results)
56
+ batch_results = rebuild_results(batch, compact_results)
57
+ cache_results(batch_results, partition.uncached_indices)
46
58
  batch.each(&:strip_sources!)
47
59
  batch_results
48
60
  end
49
61
 
62
+ def rebuild_results(batch, compact_results)
63
+ batch.zip(compact_results).map { |m, h| @packer.rebuild(m, h) }
64
+ end
65
+
66
+ def cache_results(batch_results, uncached_indices)
67
+ uncached_indices.each { |i| @cache.store(batch_results[i].mutation, batch_results[i]) }
68
+ end
69
+
50
70
  def run_uncached(batch, uncached_indices, pool, integration)
51
71
  return [] if uncached_indices.empty?
52
72
 
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
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)
3
+ require_relative "../strategy"
6
4
 
7
5
  class Evilution::Runner::MutationExecutor::Strategy::Sequential
8
6
  def initialize(runner:, pipeline:, notifier:)
@@ -29,6 +27,6 @@ class Evilution::Runner::MutationExecutor::Strategy::Sequential
29
27
  end
30
28
 
31
29
  @notifier.finish
32
- [results, truncated]
30
+ Evilution::Runner::MutationExecutor::ExecutionResult.new(results: results, truncated: truncated)
33
31
  end
34
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
@@ -1,23 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../parallel/pool"
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"
13
-
14
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
3
+ require_relative "../runner"
15
4
 
16
5
  class Evilution::Runner::MutationExecutor
17
- InfraError = Neutralizer::InfraError
18
- BaselineFailed = Neutralizer::BaselineFailed
19
- Sequential = Strategy::Sequential
20
- Parallel = Strategy::Parallel
6
+ ExecutionResult = Data.define(:results, :truncated)
7
+
8
+ autoload :ResultCache, File.expand_path("mutation_executor/result_cache", __dir__)
9
+ autoload :ResultPacker, File.expand_path("mutation_executor/result_packer", __dir__)
10
+ autoload :ResultNotifier, File.expand_path("mutation_executor/result_notifier", __dir__)
11
+ autoload :MutationRunner, File.expand_path("mutation_executor/mutation_runner", __dir__)
12
+ autoload :NeutralizationPipeline, File.expand_path("mutation_executor/neutralization_pipeline", __dir__)
13
+ autoload :Strategy, File.expand_path("mutation_executor/strategy", __dir__)
14
+ autoload :Neutralizer, File.expand_path("mutation_executor/neutralizer", __dir__)
21
15
 
22
16
  def initialize(config, isolator:, baseline_runner:, cache:, hooks:, diagnostics:, on_result: nil)
23
17
  @config = config
@@ -53,8 +47,8 @@ class Evilution::Runner::MutationExecutor
53
47
  def build_pipeline(spec_resolver)
54
48
  NeutralizationPipeline.new(
55
49
  [
56
- InfraError.new,
57
- BaselineFailed.new(
50
+ Neutralizer::InfraError.new,
51
+ Neutralizer::BaselineFailed.new(
58
52
  config: @config,
59
53
  spec_resolver: spec_resolver || ->(_f) {},
60
54
  fallback_dir: @baseline_runner.neutralization_fallback_dir
@@ -64,7 +58,7 @@ class Evilution::Runner::MutationExecutor
64
58
  end
65
59
 
66
60
  def build_sequential(notifier, pipeline)
67
- Sequential.new(
61
+ Strategy::Sequential.new(
68
62
  runner: MutationRunner.new(config: @config, cache: @cache, isolator: @isolator),
69
63
  pipeline: pipeline,
70
64
  notifier: notifier
@@ -72,7 +66,7 @@ class Evilution::Runner::MutationExecutor
72
66
  end
73
67
 
74
68
  def build_parallel(notifier, pipeline)
75
- Parallel.new(
69
+ Strategy::Parallel.new(
76
70
  cache: @cache,
77
71
  isolator: @isolator,
78
72
  packer: @packer,