evilution 0.28.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +52 -0
  3. data/CHANGELOG.md +7 -0
  4. data/lib/evilution/ast/constant_names.rb +28 -11
  5. data/lib/evilution/ast/pattern/parser.rb +29 -17
  6. data/lib/evilution/cli/commands/session_diff.rb +6 -4
  7. data/lib/evilution/cli/commands/subjects.rb +6 -3
  8. data/lib/evilution/cli/commands/util_mutation.rb +24 -19
  9. data/lib/evilution/cli/parser/command_extractor.rb +9 -11
  10. data/lib/evilution/cli/parser/file_args.rb +3 -1
  11. data/lib/evilution/cli/parser/options_builder.rb +29 -1
  12. data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
  13. data/lib/evilution/cli/parser.rb +18 -20
  14. data/lib/evilution/cli/printers/environment.rb +19 -19
  15. data/lib/evilution/cli/printers/session_diff.rb +8 -8
  16. data/lib/evilution/compare/normalizer.rb +10 -5
  17. data/lib/evilution/config.rb +10 -10
  18. data/lib/evilution/disable_comment.rb +21 -12
  19. data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
  20. data/lib/evilution/integration/minitest.rb +25 -16
  21. data/lib/evilution/integration/rspec.rb +4 -0
  22. data/lib/evilution/isolation/fork.rb +27 -17
  23. data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
  24. data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
  25. data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
  26. data/lib/evilution/mcp/info_tool.rb +7 -1
  27. data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
  28. data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
  29. data/lib/evilution/mcp/mutate_tool.rb +27 -14
  30. data/lib/evilution/mcp/session_tool.rb +27 -18
  31. data/lib/evilution/mutation.rb +13 -15
  32. data/lib/evilution/mutator/base.rb +17 -15
  33. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
  34. data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
  35. data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
  36. data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
  37. data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
  38. data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
  39. data/lib/evilution/mutator/operator/case_when.rb +7 -5
  40. data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
  41. data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
  42. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
  43. data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
  44. data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
  45. data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
  46. data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
  47. data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
  48. data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
  49. data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
  50. data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
  51. data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
  52. data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
  53. data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
  54. data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
  55. data/lib/evilution/parallel/work_queue/worker.rb +10 -7
  56. data/lib/evilution/parallel/work_queue.rb +35 -18
  57. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
  58. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
  59. data/lib/evilution/reporter/json.rb +52 -18
  60. data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
  61. data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
  62. data/lib/evilution/reporter/suggestion/templates/minitest.rb +20 -14
  63. data/lib/evilution/reporter/suggestion/templates/rspec.rb +19 -13
  64. data/lib/evilution/runner/baseline_runner.rb +15 -8
  65. data/lib/evilution/runner/diagnostics.rb +13 -9
  66. data/lib/evilution/runner/isolation_resolver.rb +11 -9
  67. data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -1
  68. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +32 -10
  69. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +1 -1
  70. data/lib/evilution/runner/mutation_executor.rb +2 -0
  71. data/lib/evilution/runner/mutation_planner.rb +37 -17
  72. data/lib/evilution/runner/subject_pipeline.rb +21 -11
  73. data/lib/evilution/runner.rb +3 -3
  74. data/lib/evilution/session/diff.rb +15 -6
  75. data/lib/evilution/spec_ast_cache.rb +26 -12
  76. data/lib/evilution/version.rb +1 -1
  77. data/script/memory_check +11 -5
  78. data/scripts/benchmark_density +10 -9
  79. data/scripts/compare_mutations +38 -21
  80. data/scripts/mutant_json_adapter +7 -4
  81. metadata +3 -2
@@ -22,18 +22,33 @@ class Evilution::Reporter::JSON
22
22
  version: Evilution::VERSION,
23
23
  timestamp: Time.now.iso8601,
24
24
  summary: build_summary(summary),
25
- survived: map_details(summary.survived_results),
26
25
  coverage_gaps: build_coverage_gaps(summary),
26
+ **result_categories(summary)
27
+ }
28
+ append_disabled_to_report(report, summary)
29
+ report
30
+ end
31
+
32
+ def result_categories(summary)
33
+ direct_categories(summary).merge(derived_categories(summary))
34
+ end
35
+
36
+ def direct_categories(summary)
37
+ {
38
+ survived: map_details(summary.survived_results),
27
39
  killed: map_details(summary.killed_results),
28
40
  neutral: map_details(summary.neutral_results),
29
- timed_out: map_details(summary.results.select(&:timeout?)),
30
- errors: map_details(summary.results.select(&:error?)),
31
41
  equivalent: map_details(summary.equivalent_results),
32
42
  unresolved: map_details(summary.unresolved_results),
33
43
  unparseable: map_details(summary.unparseable_results)
34
44
  }
35
- append_disabled_to_report(report, summary)
36
- report
45
+ end
46
+
47
+ def derived_categories(summary)
48
+ {
49
+ timed_out: map_details(summary.results.select(&:timeout?)),
50
+ errors: map_details(summary.results.select(&:error?))
51
+ }
37
52
  end
38
53
 
39
54
  def map_details(results)
@@ -47,7 +62,13 @@ class Evilution::Reporter::JSON
47
62
  end
48
63
 
49
64
  def build_summary(summary)
50
- data = {
65
+ data = build_core_summary(summary).merge(build_metrics_summary(summary))
66
+ append_optional_summary_fields(data, summary)
67
+ data
68
+ end
69
+
70
+ def build_core_summary(summary)
71
+ {
51
72
  total: summary.total,
52
73
  killed: summary.killed,
53
74
  survived: summary.survived,
@@ -56,23 +77,39 @@ class Evilution::Reporter::JSON
56
77
  neutral: summary.neutral,
57
78
  equivalent: summary.equivalent,
58
79
  unresolved: summary.unresolved,
59
- unparseable: summary.unparseable,
80
+ unparseable: summary.unparseable
81
+ }
82
+ end
83
+
84
+ def build_metrics_summary(summary)
85
+ {
60
86
  score: summary.score.round(4),
61
87
  duration: summary.duration.round(4),
62
88
  killtime: summary.killtime.round(4),
63
89
  efficiency: summary.efficiency.round(4),
64
90
  mutations_per_second: summary.mutations_per_second.round(2)
65
91
  }
92
+ end
93
+
94
+ def append_optional_summary_fields(data, summary)
66
95
  data[:truncated] = true if summary.truncated?
67
96
  data[:skipped] = summary.skipped if summary.skipped.positive?
68
97
  peak = summary.peak_memory_mb
69
98
  data[:peak_memory_mb] = peak.round(1) if peak
70
- data
71
99
  end
72
100
 
73
101
  def build_mutation_detail(result)
74
102
  mutation = result.mutation
75
- detail = {
103
+ detail = base_mutation_fields(mutation, result)
104
+ append_survived_fields(detail, mutation) if result.status == :survived
105
+ detail[:test_command] = result.test_command if result.test_command
106
+ append_memory_fields(detail, result)
107
+ append_error_fields(detail, result)
108
+ detail
109
+ end
110
+
111
+ def base_mutation_fields(mutation, result)
112
+ {
76
113
  operator: mutation.operator_name,
77
114
  file: mutation.file_path,
78
115
  line: mutation.line,
@@ -80,15 +117,12 @@ class Evilution::Reporter::JSON
80
117
  duration: result.duration.round(4),
81
118
  diff: mutation.diff
82
119
  }
83
- if result.status == :survived
84
- detail[:suggestion] = @suggestion.suggestion_for(mutation)
85
- unified = mutation.unified_diff
86
- detail[:unified_diff] = unified if unified
87
- end
88
- detail[:test_command] = result.test_command if result.test_command
89
- append_memory_fields(detail, result)
90
- append_error_fields(detail, result)
91
- detail
120
+ end
121
+
122
+ def append_survived_fields(detail, mutation)
123
+ detail[:suggestion] = @suggestion.suggestion_for(mutation)
124
+ unified = mutation.unified_diff
125
+ detail[:unified_diff] = unified if unified
92
126
  end
93
127
 
94
128
  def append_memory_fields(detail, result)
@@ -12,17 +12,4 @@ module Evilution::Reporter::Suggestion::DiffHelpers
12
12
  def sanitize_method_name(name)
13
13
  name.gsub(/[^a-zA-Z0-9_]/, "_").gsub(/_+/, "_").gsub(/\A_|_\z/, "")
14
14
  end
15
-
16
- def extract_diff_lines(diff)
17
- lines = diff.split("\n")
18
- original = lines.find { |l| l.start_with?("- ") }
19
- mutated = lines.find { |l| l.start_with?("+ ") }
20
- [clean_diff_line(original, "- "), clean_diff_line(mutated, "+ ")]
21
- end
22
-
23
- def clean_diff_line(line, prefix)
24
- return nil if line.nil?
25
-
26
- line.sub(/^#{Regexp.escape(prefix)}/, "").strip
27
- end
28
15
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../suggestion"
4
+
5
+ class Evilution::Reporter::Suggestion::DiffLines
6
+ def self.from_diff(raw_diff)
7
+ lines = raw_diff.split("\n")
8
+ new(
9
+ original: clean(lines.find { |l| l.start_with?("- ") }, "- "),
10
+ mutated: clean(lines.find { |l| l.start_with?("+ ") }, "+ ")
11
+ )
12
+ end
13
+
14
+ def self.clean(line, prefix)
15
+ return nil if line.nil?
16
+
17
+ line.sub(/^#{Regexp.escape(prefix)}/, "").strip
18
+ end
19
+ private_class_method :clean
20
+
21
+ attr_reader :original, :mutated
22
+
23
+ def initialize(original:, mutated:)
24
+ @original = original
25
+ @mutated = mutated
26
+ freeze
27
+ end
28
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../templates"
4
4
  require_relative "../diff_helpers"
5
+ require_relative "../diff_lines"
5
6
 
6
7
  module Evilution::Reporter::Suggestion::Templates::Minitest
7
8
  H = Evilution::Reporter::Suggestion::DiffHelpers
@@ -17,21 +18,26 @@ module Evilution::Reporter::Suggestion::Templates::Minitest
17
18
  end
18
19
 
19
20
  def self.build(test_name:, action: :changed, &body_block)
20
- lambda do |mutation|
21
- method_name = H.parse_method_name(mutation.subject.name)
22
- safe_name = H.sanitize_method_name(method_name)
23
- original_line, mutated_line = H.extract_diff_lines(mutation.diff)
24
- body = body_block.call(method_name)
25
- indented = body.lines.map { |l| " #{l}" }.join.chomp
21
+ ->(mutation) { render(test_name, action, body_block, mutation) }
22
+ end
26
23
 
27
- <<~MINITEST.strip
28
- # Mutation: #{format_header(action, original_line, mutated_line, mutation.subject.name)}
29
- # #{mutation.file_path}:#{mutation.line}
30
- def test_#{test_name}_#{safe_name}
31
- #{indented}
32
- end
33
- MINITEST
34
- end
24
+ def self.render(test_name, action, body_block, mutation)
25
+ method_name = H.parse_method_name(mutation.subject.name)
26
+ safe_name = H.sanitize_method_name(method_name)
27
+ diff_lines = Evilution::Reporter::Suggestion::DiffLines.from_diff(mutation.diff)
28
+ indented = indent_body(body_block.call(method_name))
29
+
30
+ <<~MINITEST.strip
31
+ # Mutation: #{format_header(action, diff_lines.original, diff_lines.mutated, mutation.subject.name)}
32
+ # #{mutation.file_path}:#{mutation.line}
33
+ def test_#{test_name}_#{safe_name}
34
+ #{indented}
35
+ end
36
+ MINITEST
37
+ end
38
+
39
+ def self.indent_body(body)
40
+ body.lines.map { |l| " #{l}" }.join.chomp
35
41
  end
36
42
 
37
43
  MINITEST_ENTRIES = {
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../templates"
4
4
  require_relative "../diff_helpers"
5
+ require_relative "../diff_lines"
5
6
 
6
7
  module Evilution::Reporter::Suggestion::Templates::Rspec
7
8
  H = Evilution::Reporter::Suggestion::DiffHelpers
@@ -17,20 +18,25 @@ module Evilution::Reporter::Suggestion::Templates::Rspec
17
18
  end
18
19
 
19
20
  def self.build(it_desc:, action: :changed, &body_block)
20
- lambda do |mutation|
21
- method_name = H.parse_method_name(mutation.subject.name)
22
- original_line, mutated_line = H.extract_diff_lines(mutation.diff)
23
- body = body_block.call(method_name)
24
- indented = body.lines.map { |l| " #{l}" }.join.chomp
21
+ ->(mutation) { render(it_desc, action, body_block, mutation) }
22
+ end
25
23
 
26
- <<~RSPEC.strip
27
- # Mutation: #{format_header(action, original_line, mutated_line, mutation.subject.name)}
28
- # #{mutation.file_path}:#{mutation.line}
29
- it '#{it_desc} ##{method_name}' do
30
- #{indented}
31
- end
32
- RSPEC
33
- end
24
+ def self.render(it_desc, action, body_block, mutation)
25
+ method_name = H.parse_method_name(mutation.subject.name)
26
+ diff_lines = Evilution::Reporter::Suggestion::DiffLines.from_diff(mutation.diff)
27
+ indented = indent_body(body_block.call(method_name))
28
+
29
+ <<~RSPEC.strip
30
+ # Mutation: #{format_header(action, diff_lines.original, diff_lines.mutated, mutation.subject.name)}
31
+ # #{mutation.file_path}:#{mutation.line}
32
+ it '#{it_desc} ##{method_name}' do
33
+ #{indented}
34
+ end
35
+ RSPEC
36
+ end
37
+
38
+ def self.indent_body(body)
39
+ body.lines.map { |l| " #{l}" }.join.chomp
34
40
  end
35
41
 
36
42
  RSPEC_ENTRIES = {
@@ -30,18 +30,25 @@ class Evilution::Runner::BaselineRunner
30
30
 
31
31
  def build_integration
32
32
  klass = integration_class
33
- test_files = config.spec_files.empty? ? nil : config.spec_files
34
- kwargs = {
35
- 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,
36
41
  hooks: hooks,
37
42
  fallback_to_full_suite: config.fallback_to_full_suite?,
38
43
  spec_selector: config.spec_selector
39
44
  }
40
- if klass == Evilution::Integration::RSpec
41
- kwargs[:related_specs_heuristic] = config.related_specs_heuristic?
42
- kwargs[:example_filter] = build_example_filter
43
- end
44
- 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
+ }
45
52
  end
46
53
 
47
54
  def call(subjects)
@@ -32,19 +32,23 @@ class Evilution::Runner::Diagnostics
32
32
  def log_mutation_diagnostics(result)
33
33
  return unless verbose?
34
34
 
35
- parts = []
36
- 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?
37
37
 
38
- if result.memory_delta_kb
39
- sign = result.memory_delta_kb.negative? ? "" : "+"
40
- parts << format("delta: %<sign>s%<mb>.1f MB", sign: sign, mb: result.memory_delta_kb / 1024.0)
41
- end
38
+ log_mutation_error(result) if result.error?
39
+ end
42
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
43
45
  parts << gc_stats_string
46
+ parts
47
+ end
44
48
 
45
- stderr.write("[verbose] #{result.mutation}: #{parts.join(", ")}\n") unless parts.empty?
46
-
47
- 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)
48
52
  end
49
53
 
50
54
  def log_worker_stats(stats)
@@ -108,16 +108,18 @@ class Evilution::Runner::IsolationResolver
108
108
  end
109
109
 
110
110
  def resolve_preload_path
111
- if config.preload.is_a?(String)
112
- unless File.file?(config.preload)
113
- raise Evilution::ConfigError.new(
114
- "preload file not found: #{config.preload.inspect}",
115
- file: config.preload
116
- )
117
- end
118
- return config.preload
119
- 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
120
121
 
122
+ def resolve_autodetected_preload
121
123
  root = detected_rails_root
122
124
  return nil unless root
123
125
 
@@ -7,6 +7,8 @@ class Evilution::Runner::MutationExecutor::ResultCache
7
7
  CACHEABLE_STATUSES = %i[killed timeout].freeze
8
8
  private_constant :CACHEABLE_STATUSES
9
9
 
10
+ Partition = Data.define(:uncached_indices, :cached_results)
11
+
10
12
  def initialize(backend)
11
13
  @backend = backend
12
14
  end
@@ -56,7 +58,7 @@ class Evilution::Runner::MutationExecutor::ResultCache
56
58
  end
57
59
  end
58
60
 
59
- [uncached_indices, cached_results]
61
+ Partition.new(uncached_indices: uncached_indices, cached_results: cached_results)
60
62
  end
61
63
 
62
64
  private
@@ -18,8 +18,17 @@ class Evilution::Runner::MutationExecutor::Strategy::Parallel
18
18
  @notifier.start(mutations.length)
19
19
  pool = @pool_factory.call
20
20
  state = { results: [], truncated: false, completed: 0 }
21
- all_worker_stats = []
21
+ all_worker_stats = run_batches(mutations, pool, baseline_result, integration, state)
22
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 = []
23
32
  mutations.each_slice(@config.jobs) do |batch|
24
33
  break if state[:truncated]
25
34
 
@@ -27,24 +36,37 @@ class Evilution::Runner::MutationExecutor::Strategy::Parallel
27
36
  all_worker_stats.concat(pool.worker_stats)
28
37
  process_batch(batch_results, baseline_result, state)
29
38
  end
39
+ all_worker_stats
40
+ end
30
41
 
31
- @diagnostics.log_worker_stats(@diagnostics.aggregate_worker_stats(all_worker_stats)) if @diagnostics
32
- @notifier.finish
33
- [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))
34
46
  end
35
47
 
36
- private
48
+ def build_result(state)
49
+ Evilution::Runner::MutationExecutor::ExecutionResult.new(results: state[:results], truncated: state[:truncated])
50
+ end
37
51
 
38
52
  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]) }
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)
44
58
  batch.each(&:strip_sources!)
45
59
  batch_results
46
60
  end
47
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
+
48
70
  def run_uncached(batch, uncached_indices, pool, integration)
49
71
  return [] if uncached_indices.empty?
50
72
 
@@ -27,6 +27,6 @@ class Evilution::Runner::MutationExecutor::Strategy::Sequential
27
27
  end
28
28
 
29
29
  @notifier.finish
30
- [results, truncated]
30
+ Evilution::Runner::MutationExecutor::ExecutionResult.new(results: results, truncated: truncated)
31
31
  end
32
32
  end
@@ -3,6 +3,8 @@
3
3
  require_relative "../runner"
4
4
 
5
5
  class Evilution::Runner::MutationExecutor
6
+ ExecutionResult = Data.define(:results, :truncated)
7
+
6
8
  autoload :ResultCache, File.expand_path("mutation_executor/result_cache", __dir__)
7
9
  autoload :ResultPacker, File.expand_path("mutation_executor/result_packer", __dir__)
8
10
  autoload :ResultNotifier, File.expand_path("mutation_executor/result_notifier", __dir__)
@@ -9,6 +9,12 @@ require_relative "../equivalent/detector"
9
9
  class Evilution::Runner::MutationPlanner
10
10
  Plan = Struct.new(:enabled, :equivalent, :skipped_count, :disabled_mutations, keyword_init: true)
11
11
 
12
+ GenerationResult = Data.define(:mutations, :skipped)
13
+ DisabledFilterResult = Data.define(:enabled, :disabled)
14
+ SigFilterResult = Data.define(:enabled, :skipped)
15
+ EquivalentFilterResult = Data.define(:equivalent, :enabled)
16
+ private_constant :GenerationResult, :DisabledFilterResult, :SigFilterResult, :EquivalentFilterResult
17
+
12
18
  def initialize(config, registry:, disable_detector: Evilution::DisableComment.new,
13
19
  sig_detector: Evilution::AST::SorbetSigDetector.new)
14
20
  @config = config
@@ -20,26 +26,39 @@ class Evilution::Runner::MutationPlanner
20
26
  end
21
27
 
22
28
  def call(subjects)
23
- mutations, generation_skipped = generate(subjects)
24
- mutations, disabled = filter_disabled(mutations)
25
- disabled.each(&:strip_sources!) if config.show_disabled?
26
- disabled_mutations = config.show_disabled? ? disabled : []
27
-
28
- mutations, sig_skipped = filter_sig_blocks(mutations)
29
- equivalent, enabled = filter_equivalent(mutations)
29
+ generation = generate(subjects)
30
+ disabled_filter = filter_disabled(generation.mutations)
31
+ disabled_mutations = compute_disabled_mutations(disabled_filter)
32
+ sig_filter = filter_sig_blocks(disabled_filter.enabled)
33
+ equivalent_filter = filter_equivalent(sig_filter.enabled)
30
34
 
31
- Plan.new(
32
- enabled: enabled,
33
- equivalent: equivalent,
34
- skipped_count: generation_skipped + disabled.length + sig_skipped,
35
- disabled_mutations: disabled_mutations
36
- )
35
+ build_plan(equivalent_filter, disabled_mutations, total_skipped(generation, disabled_filter, sig_filter))
37
36
  end
38
37
 
39
38
  private
40
39
 
41
40
  attr_reader :config, :registry
42
41
 
42
+ def compute_disabled_mutations(disabled_filter)
43
+ return [] unless config.show_disabled?
44
+
45
+ disabled_filter.disabled.each(&:strip_sources!)
46
+ disabled_filter.disabled
47
+ end
48
+
49
+ def total_skipped(generation, disabled_filter, sig_filter)
50
+ generation.skipped + disabled_filter.disabled.length + sig_filter.skipped
51
+ end
52
+
53
+ def build_plan(equivalent_filter, disabled_mutations, skipped_count)
54
+ Plan.new(
55
+ enabled: equivalent_filter.enabled,
56
+ equivalent: equivalent_filter.equivalent,
57
+ skipped_count: skipped_count,
58
+ disabled_mutations: disabled_mutations
59
+ )
60
+ end
61
+
43
62
  def generate(subjects)
44
63
  filter = build_ignore_filter
45
64
  operator_options = build_operator_options
@@ -47,7 +66,7 @@ class Evilution::Runner::MutationPlanner
47
66
  registry.mutations_for(subject, filter: filter, operator_options: operator_options)
48
67
  end
49
68
  skipped = filter ? filter.skipped_count : 0
50
- [mutations, skipped]
69
+ GenerationResult.new(mutations: mutations, skipped: skipped)
51
70
  end
52
71
 
53
72
  def build_operator_options
@@ -73,7 +92,7 @@ class Evilution::Runner::MutationPlanner
73
92
  end
74
93
  end
75
94
 
76
- [enabled, disabled]
95
+ DisabledFilterResult.new(enabled: enabled, disabled: disabled)
77
96
  end
78
97
 
79
98
  def mutation_disabled?(mutation)
@@ -102,7 +121,7 @@ class Evilution::Runner::MutationPlanner
102
121
  end
103
122
  end
104
123
 
105
- [enabled, skipped]
124
+ SigFilterResult.new(enabled: enabled, skipped: skipped)
106
125
  end
107
126
 
108
127
  def mutation_in_sig_block?(mutation)
@@ -120,6 +139,7 @@ class Evilution::Runner::MutationPlanner
120
139
  end
121
140
 
122
141
  def filter_equivalent(mutations)
123
- Evilution::Equivalent::Detector.new.call(mutations)
142
+ equivalent, enabled = Evilution::Equivalent::Detector.new.call(mutations)
143
+ EquivalentFilterResult.new(equivalent: equivalent, enabled: enabled)
124
144
  end
125
145
  end
@@ -104,17 +104,27 @@ class Evilution::Runner::SubjectPipeline
104
104
 
105
105
  def target_matcher
106
106
  target = config.target
107
- if target.end_with?("*")
108
- prefix = target.chomp("*")
109
- ->(s) { s.name.split(/[#.]/).first.start_with?(prefix) }
110
- elsif target.end_with?("#", ".")
111
- prefix = target
112
- ->(s) { s.name.start_with?(prefix) }
113
- elsif target.include?("#") || target.include?(".")
114
- ->(s) { s.name == target }
115
- else
116
- ->(s) { s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
117
- end
107
+ return wildcard_matcher(target.chomp("*")) if target.end_with?("*")
108
+ return prefix_matcher(target) if target.end_with?("#", ".")
109
+ return exact_matcher(target) if target.include?("#") || target.include?(".")
110
+
111
+ class_matcher(target)
112
+ end
113
+
114
+ def wildcard_matcher(prefix)
115
+ ->(s) { s.name.split(/[#.]/).first.start_with?(prefix) }
116
+ end
117
+
118
+ def prefix_matcher(prefix)
119
+ ->(s) { s.name.start_with?(prefix) }
120
+ end
121
+
122
+ def exact_matcher(target)
123
+ ->(s) { s.name == target }
124
+ end
125
+
126
+ def class_matcher(target)
127
+ ->(s) { s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
118
128
  end
119
129
 
120
130
  def filter_by_line_ranges(subjects)
@@ -32,13 +32,13 @@ class Evilution::Runner
32
32
  plan = mutation_planner.call(subjects)
33
33
  release_subject_nodes(subjects)
34
34
  clear_operator_caches
35
- results, truncated = run_mutations(plan.enabled, baseline_result)
36
- results += equivalent_results(plan.equivalent)
35
+ execution = run_mutations(plan.enabled, baseline_result)
36
+ results = execution.results + equivalent_results(plan.equivalent)
37
37
  log_memory("after run_mutations", "#{results.length} results")
38
38
 
39
39
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
40
40
 
41
- summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: truncated,
41
+ summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: execution.truncated,
42
42
  skipped: plan.skipped_count,
43
43
  disabled_mutations: plan.disabled_mutations)
44
44
  output_report(summary)
@@ -38,20 +38,29 @@ class Evilution::Session::Diff
38
38
  def call(base_data, head_data)
39
39
  base_survivors = base_data["survived"] || []
40
40
  head_survivors = head_data["survived"] || []
41
-
42
- base_keys = base_survivors.to_set { |m| mutation_key(m) }
43
- head_keys = head_survivors.to_set { |m| mutation_key(m) }
41
+ fixed, new_survivors, persistent = partition_survivors(base_survivors, head_survivors)
44
42
 
45
43
  Result.new(
46
44
  summary: build_summary_diff(base_data, head_data),
47
- fixed: base_survivors.reject { |m| head_keys.include?(mutation_key(m)) },
48
- new_survivors: head_survivors.reject { |m| base_keys.include?(mutation_key(m)) },
49
- persistent: head_survivors.select { |m| base_keys.include?(mutation_key(m)) }
45
+ fixed: fixed,
46
+ new_survivors: new_survivors,
47
+ persistent: persistent
50
48
  )
51
49
  end
52
50
 
53
51
  private
54
52
 
53
+ def partition_survivors(base_survivors, head_survivors)
54
+ base_keys = base_survivors.to_set { |m| mutation_key(m) }
55
+ head_keys = head_survivors.to_set { |m| mutation_key(m) }
56
+
57
+ [
58
+ base_survivors.reject { |m| head_keys.include?(mutation_key(m)) },
59
+ head_survivors.reject { |m| base_keys.include?(mutation_key(m)) },
60
+ head_survivors.select { |m| base_keys.include?(mutation_key(m)) }
61
+ ]
62
+ end
63
+
55
64
  def build_summary_diff(base_data, head_data)
56
65
  base = extract_summary_values(base_data)
57
66
  head = extract_summary_values(head_data)