evilution 0.19.0 → 0.20.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +22 -22
  4. data/CHANGELOG.md +18 -0
  5. data/README.md +22 -3
  6. data/lib/evilution/equivalent/detector.rb +3 -1
  7. data/lib/evilution/equivalent/heuristic/alias_swap.rb +2 -1
  8. data/lib/evilution/equivalent/heuristic/void_context.rb +77 -0
  9. data/lib/evilution/integration/crash_detector.rb +55 -0
  10. data/lib/evilution/integration/rspec.rb +32 -5
  11. data/lib/evilution/mutator/operator/begin_unwrap.rb +21 -0
  12. data/lib/evilution/mutator/operator/block_param_removal.rb +57 -0
  13. data/lib/evilution/mutator/operator/case_when.rb +55 -0
  14. data/lib/evilution/mutator/operator/equality_to_identity.rb +22 -0
  15. data/lib/evilution/mutator/operator/lambda_body.rb +18 -0
  16. data/lib/evilution/mutator/operator/loop_flip.rb +27 -0
  17. data/lib/evilution/mutator/operator/method_body_replacement.rb +10 -6
  18. data/lib/evilution/mutator/operator/predicate_replacement.rb +27 -0
  19. data/lib/evilution/mutator/operator/retry_removal.rb +16 -0
  20. data/lib/evilution/mutator/operator/send_mutation.rb +8 -1
  21. data/lib/evilution/mutator/operator/string_interpolation.rb +32 -0
  22. data/lib/evilution/mutator/registry.rb +10 -1
  23. data/lib/evilution/related_spec_heuristic.rb +63 -0
  24. data/lib/evilution/reporter/cli.rb +14 -8
  25. data/lib/evilution/reporter/html.rb +32 -2
  26. data/lib/evilution/reporter/json.rb +14 -0
  27. data/lib/evilution/result/coverage_gap.rb +35 -0
  28. data/lib/evilution/result/coverage_gap_grouper.rb +22 -0
  29. data/lib/evilution/result/summary.rb +5 -0
  30. data/lib/evilution/session/store.rb +13 -0
  31. data/lib/evilution/version.rb +1 -1
  32. data/lib/evilution.rb +9 -0
  33. metadata +16 -2
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::BlockParamRemoval < Evilution::Mutator::Base
6
+ def visit_def_node(node)
7
+ return super unless node.parameters
8
+ return super unless node.parameters.block
9
+
10
+ if only_block_param?(node.parameters)
11
+ remove_entire_params(node)
12
+ else
13
+ remove_block_param(node)
14
+ end
15
+
16
+ super
17
+ end
18
+
19
+ private
20
+
21
+ def only_block_param?(params)
22
+ params.requireds.empty? &&
23
+ params.optionals.empty? &&
24
+ params.keywords.empty? &&
25
+ params.rest.nil? &&
26
+ params.keyword_rest.nil?
27
+ end
28
+
29
+ def remove_entire_params(node)
30
+ start_offset = node.lparen_loc.start_offset
31
+ end_offset = node.rparen_loc.start_offset + node.rparen_loc.length
32
+ add_mutation(
33
+ offset: start_offset,
34
+ length: end_offset - start_offset,
35
+ replacement: "",
36
+ node: node
37
+ )
38
+ end
39
+
40
+ def remove_block_param(node)
41
+ block_loc = node.parameters.block.location
42
+ params_text = @file_source.byteslice(node.parameters.location.start_offset, node.parameters.location.length)
43
+ block_rel = block_loc.start_offset - node.parameters.location.start_offset
44
+
45
+ # Find the comma before the block param and remove ", &block"
46
+ comma_pos = params_text.rindex(",", block_rel - 1)
47
+ remove_start = node.parameters.location.start_offset + comma_pos
48
+ remove_end = block_loc.start_offset + block_loc.length
49
+
50
+ add_mutation(
51
+ offset: remove_start,
52
+ length: remove_end - remove_start,
53
+ replacement: "",
54
+ node: node
55
+ )
56
+ end
57
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::CaseWhen < Evilution::Mutator::Base
6
+ def visit_case_node(node)
7
+ remove_when_branches(node)
8
+ replace_when_bodies(node)
9
+ remove_else_branch(node)
10
+
11
+ super
12
+ end
13
+
14
+ private
15
+
16
+ def remove_when_branches(node)
17
+ return if node.conditions.length < 2
18
+
19
+ node.conditions.each do |when_node|
20
+ add_mutation(
21
+ offset: when_node.location.start_offset,
22
+ length: when_node.location.length,
23
+ replacement: "",
24
+ node: when_node
25
+ )
26
+ end
27
+ end
28
+
29
+ def replace_when_bodies(node)
30
+ node.conditions.each do |when_node|
31
+ next if when_node.statements.nil? || when_node.statements.body.empty?
32
+
33
+ add_mutation(
34
+ offset: when_node.statements.location.start_offset,
35
+ length: when_node.statements.location.length,
36
+ replacement: "nil",
37
+ node: when_node
38
+ )
39
+ end
40
+ end
41
+
42
+ def remove_else_branch(node)
43
+ return if node.else_clause.nil?
44
+ return if node.else_clause.statements.nil?
45
+
46
+ start_offset = node.else_clause.else_keyword_loc.start_offset
47
+ end_offset = node.else_clause.statements.location.start_offset + node.else_clause.statements.location.length
48
+ add_mutation(
49
+ offset: start_offset,
50
+ length: end_offset - start_offset,
51
+ replacement: "",
52
+ node: node.else_clause
53
+ )
54
+ end
55
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::EqualityToIdentity < Evilution::Mutator::Base
6
+ def visit_call_node(node)
7
+ if node.name == :== && node.receiver && node.arguments
8
+ receiver_text = @file_source.byteslice(node.receiver.location.start_offset, node.receiver.location.length)
9
+ arg = node.arguments.arguments.first
10
+ arg_text = @file_source.byteslice(arg.location.start_offset, arg.location.length)
11
+
12
+ add_mutation(
13
+ offset: node.location.start_offset,
14
+ length: node.location.length,
15
+ replacement: "#{receiver_text}.equal?(#{arg_text})",
16
+ node: node
17
+ )
18
+ end
19
+
20
+ super
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::LambdaBody < Evilution::Mutator::Base
6
+ def visit_lambda_node(node)
7
+ if node.body
8
+ add_mutation(
9
+ offset: node.body.location.start_offset,
10
+ length: node.body.location.length,
11
+ replacement: "nil",
12
+ node: node
13
+ )
14
+ end
15
+
16
+ super
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::LoopFlip < Evilution::Mutator::Base
6
+ def visit_while_node(node)
7
+ add_mutation(
8
+ offset: node.keyword_loc.start_offset,
9
+ length: node.keyword_loc.length,
10
+ replacement: "until",
11
+ node: node
12
+ )
13
+
14
+ super
15
+ end
16
+
17
+ def visit_until_node(node)
18
+ add_mutation(
19
+ offset: node.keyword_loc.start_offset,
20
+ length: node.keyword_loc.length,
21
+ replacement: "while",
22
+ node: node
23
+ )
24
+
25
+ super
26
+ end
27
+ end
@@ -3,14 +3,18 @@
3
3
  require_relative "../operator"
4
4
 
5
5
  class Evilution::Mutator::Operator::MethodBodyReplacement < Evilution::Mutator::Base
6
+ REPLACEMENTS = %w[nil self super].freeze
7
+
6
8
  def visit_def_node(node)
7
9
  if node.body
8
- add_mutation(
9
- offset: node.body.location.start_offset,
10
- length: node.body.location.length,
11
- replacement: "nil",
12
- node: node
13
- )
10
+ REPLACEMENTS.each do |replacement|
11
+ add_mutation(
12
+ offset: node.body.location.start_offset,
13
+ length: node.body.location.length,
14
+ replacement: replacement,
15
+ node: node
16
+ )
17
+ end
14
18
  end
15
19
 
16
20
  super
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::PredicateReplacement < Evilution::Mutator::Base
6
+ def visit_call_node(node)
7
+ if node.name.to_s.end_with?("?")
8
+ loc = node.location
9
+
10
+ add_mutation(
11
+ offset: loc.start_offset,
12
+ length: loc.length,
13
+ replacement: "true",
14
+ node: node
15
+ )
16
+
17
+ add_mutation(
18
+ offset: loc.start_offset,
19
+ length: loc.length,
20
+ replacement: "false",
21
+ node: node
22
+ )
23
+ end
24
+
25
+ super
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::RetryRemoval < Evilution::Mutator::Base
6
+ def visit_retry_node(node)
7
+ add_mutation(
8
+ offset: node.location.start_offset,
9
+ length: node.location.length,
10
+ replacement: "nil",
11
+ node: node
12
+ )
13
+
14
+ super
15
+ end
16
+ end
@@ -29,7 +29,14 @@ class Evilution::Mutator::Operator::SendMutation < Evilution::Mutator::Base
29
29
  to_i: [:to_s],
30
30
  to_f: [:to_i],
31
31
  to_a: [:to_h],
32
- to_h: [:to_a]
32
+ to_h: [:to_a],
33
+ downcase: [:upcase],
34
+ upcase: [:downcase],
35
+ strip: %i[lstrip rstrip],
36
+ lstrip: [:strip],
37
+ rstrip: [:strip],
38
+ chomp: [:chop],
39
+ chop: [:chomp]
33
40
  }.freeze
34
41
 
35
42
  def visit_call_node(node)
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::StringInterpolation < Evilution::Mutator::Base
6
+ def visit_interpolated_string_node(node)
7
+ mutate_embedded_statements(node)
8
+ super
9
+ end
10
+
11
+ def visit_interpolated_symbol_node(node)
12
+ mutate_embedded_statements(node)
13
+ super
14
+ end
15
+
16
+ private
17
+
18
+ def mutate_embedded_statements(node)
19
+ node.parts.each do |part|
20
+ next unless part.is_a?(Prism::EmbeddedStatementsNode)
21
+ next if part.statements.nil? || part.statements.body.empty?
22
+
23
+ stmt = part.statements
24
+ add_mutation(
25
+ offset: stmt.location.start_offset,
26
+ length: stmt.location.length,
27
+ replacement: "nil",
28
+ node: part
29
+ )
30
+ end
31
+ end
32
+ end
@@ -65,7 +65,16 @@ class Evilution::Mutator::Registry
65
65
  Evilution::Mutator::Operator::YieldStatement,
66
66
  Evilution::Mutator::Operator::SplatOperator,
67
67
  Evilution::Mutator::Operator::DefinedCheck,
68
- Evilution::Mutator::Operator::RegexCapture
68
+ Evilution::Mutator::Operator::RegexCapture,
69
+ Evilution::Mutator::Operator::LoopFlip,
70
+ Evilution::Mutator::Operator::StringInterpolation,
71
+ Evilution::Mutator::Operator::RetryRemoval,
72
+ Evilution::Mutator::Operator::CaseWhen,
73
+ Evilution::Mutator::Operator::PredicateReplacement,
74
+ Evilution::Mutator::Operator::EqualityToIdentity,
75
+ Evilution::Mutator::Operator::LambdaBody,
76
+ Evilution::Mutator::Operator::BeginUnwrap,
77
+ Evilution::Mutator::Operator::BlockParamRemoval
69
78
  ].each { |op| registry.register(op) }
70
79
  registry
71
80
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Evilution::RelatedSpecHeuristic
4
+ RELATED_SPEC_DIRS = %w[
5
+ spec/requests
6
+ spec/integration
7
+ spec/features
8
+ spec/system
9
+ ].freeze
10
+
11
+ INCLUDES_PATTERN = /\bincludes\(/
12
+
13
+ def call(mutation)
14
+ return [] unless includes_mutation?(mutation)
15
+
16
+ domain = extract_domain(mutation.file_path)
17
+ return [] unless domain
18
+
19
+ find_related_specs(domain)
20
+ end
21
+
22
+ private
23
+
24
+ def includes_mutation?(mutation)
25
+ diff = mutation.diff
26
+ return false unless diff
27
+
28
+ diff.split("\n").any? { |line| line.start_with?("- ") && line.match?(INCLUDES_PATTERN) }
29
+ end
30
+
31
+ def extract_domain(file_path)
32
+ normalized = normalize_path(file_path)
33
+
34
+ # Strip common prefixes and get the relative path under app/ or lib/
35
+ relative = normalized
36
+ .delete_prefix("app/controllers/")
37
+ .delete_prefix("app/models/")
38
+ .delete_prefix("app/")
39
+ .delete_prefix("lib/")
40
+
41
+ # Remove .rb extension and _controller suffix
42
+ basename = relative.sub(/\.rb\z/, "")
43
+ basename = basename.sub(/_controller\z/, "")
44
+
45
+ basename.empty? ? nil : basename
46
+ end
47
+
48
+ def normalize_path(path)
49
+ path = path.delete_prefix("./")
50
+ path = path.delete_prefix("#{Dir.pwd}/") if path.start_with?("/")
51
+ path
52
+ end
53
+
54
+ def find_related_specs(domain)
55
+ RELATED_SPEC_DIRS.flat_map { |dir| find_specs_in_dir(dir, domain) }.sort
56
+ end
57
+
58
+ def find_specs_in_dir(dir, domain)
59
+ return [] unless Dir.exist?(dir)
60
+
61
+ Dir.glob(File.join(dir, "**", "#{domain}_spec.rb"))
62
+ end
63
+ end
@@ -30,11 +30,12 @@ class Evilution::Reporter::CLI
30
30
  private
31
31
 
32
32
  def append_survived(lines, summary)
33
- return unless summary.survived_results.any?
33
+ gaps = summary.coverage_gaps
34
+ return unless gaps.any?
34
35
 
35
36
  lines << ""
36
- lines << "Survived mutations:"
37
- summary.survived_results.each { |result| lines << format_survived(result) }
37
+ lines << "Survived mutations (#{gaps.length} coverage gap#{"s" unless gaps.length == 1}):"
38
+ gaps.each { |gap| lines << format_coverage_gap(gap) }
38
39
  end
39
40
 
40
41
  def append_neutral(lines, summary)
@@ -90,11 +91,16 @@ class Evilution::Reporter::CLI
90
91
  "Efficiency: #{pct} killtime, #{rate} mutations/s"
91
92
  end
92
93
 
93
- def format_survived(result)
94
- mutation = result.mutation
95
- location = "#{mutation.file_path}:#{mutation.line}"
96
- diff_lines = mutation.diff.split("\n").map { |l| " #{l}" }.join("\n")
97
- " #{mutation.operator_name}: #{location} (#{mutation.subject.name})\n#{diff_lines}"
94
+ def format_coverage_gap(gap)
95
+ location = "#{gap.file_path}:#{gap.line}"
96
+ header = if gap.single?
97
+ " #{gap.primary_operator}: #{location} (#{gap.subject_name})"
98
+ else
99
+ operators = gap.operator_names.join(", ")
100
+ " #{location} (#{gap.subject_name}) [#{gap.count} mutations: #{operators}]"
101
+ end
102
+ diff_lines = gap.primary_diff.split("\n").map { |l| " #{l}" }.join("\n")
103
+ "#{header}\n#{diff_lines}"
98
104
  end
99
105
 
100
106
  def format_neutral(result)
@@ -4,6 +4,7 @@ require "cgi"
4
4
  require_relative "suggestion"
5
5
 
6
6
  require_relative "../reporter"
7
+ require_relative "../result/coverage_gap_grouper"
7
8
 
8
9
  class Evilution::Reporter::HTML
9
10
  def initialize(baseline: nil)
@@ -156,15 +157,39 @@ class Evilution::Reporter::HTML
156
157
  def build_survived_details(survived)
157
158
  return "" if survived.empty?
158
159
 
159
- entries = survived.map { |r| build_survived_entry(r) }.join("\n")
160
+ gaps = Evilution::Result::CoverageGapGrouper.new.call(survived)
161
+ entries = gaps.map { |gap| build_gap_entry(gap) }.join("\n")
160
162
  <<~HTML
161
163
  <div class="survived-details">
162
- <h3>Survived Mutations</h3>
164
+ <h3>Coverage Gaps (#{gaps.length})</h3>
163
165
  #{entries}
164
166
  </div>
165
167
  HTML
166
168
  end
167
169
 
170
+ def build_gap_entry(gap)
171
+ if gap.single?
172
+ build_survived_entry(gap.mutation_results.first)
173
+ else
174
+ build_grouped_gap_entry(gap)
175
+ end
176
+ end
177
+
178
+ def build_grouped_gap_entry(gap)
179
+ operator_tags = gap.operator_names.map { |op| %(<span class="operator-tag">#{h(op)}</span>) }.join(" ")
180
+ entries_html = gap.mutation_results.map { |r| build_survived_entry(r) }.join("\n")
181
+ <<~HTML
182
+ <div class="coverage-gap">
183
+ <div class="gap-header">
184
+ <span class="location">#{h(gap.file_path)}:#{gap.line} (#{h(gap.subject_name)})</span>
185
+ <span class="gap-count">#{gap.count} mutations</span>
186
+ #{operator_tags}
187
+ </div>
188
+ #{entries_html}
189
+ </div>
190
+ HTML
191
+ end
192
+
168
193
  def build_survived_entry(result)
169
194
  mutation = result.mutation
170
195
  suggestion_text = @suggestion.suggestion_for(mutation)
@@ -307,6 +332,11 @@ class Evilution::Reporter::HTML
307
332
  .diff-removed { color: #f85149; display: block; }
308
333
  .diff-added { color: #3fb950; display: block; }
309
334
  .suggestion { color: #d29922; font-size: 0.8rem; margin-top: 0.5rem; font-style: italic; }
335
+ .coverage-gap { border: 1px solid #30363d; border-radius: 6px; padding: 0.75rem; margin-bottom: 0.75rem; background: #161b22; }
336
+ .gap-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; font-size: 0.85rem; padding-bottom: 0.5rem; border-bottom: 1px solid #21262d; }
337
+ .gap-header .location { color: #58a6ff; font-family: monospace; }
338
+ .gap-count { background: #4a1a1a; color: #f85149; font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 10px; font-weight: bold; }
339
+ .operator-tag { background: #21262d; color: #8b949e; font-size: 0.7rem; padding: 0.1rem 0.4rem; border-radius: 4px; font-family: monospace; }
310
340
  .empty { color: #8b949e; text-align: center; padding: 2rem; }
311
341
  .baseline-comparison { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1rem; margin-bottom: 2rem; }
312
342
  .baseline-comparison h2 { font-size: 1rem; color: #f0f6fc; margin-bottom: 0.75rem; }
@@ -24,6 +24,7 @@ class Evilution::Reporter::JSON
24
24
  timestamp: Time.now.iso8601,
25
25
  summary: build_summary(summary),
26
26
  survived: summary.survived_results.map { |r| build_mutation_detail(r) },
27
+ coverage_gaps: build_coverage_gaps(summary),
27
28
  killed: summary.killed_results.map { |r| build_mutation_detail(r) },
28
29
  neutral: summary.neutral_results.map { |r| build_mutation_detail(r) },
29
30
  timed_out: summary.results.select(&:timeout?).map { |r| build_mutation_detail(r) },
@@ -81,6 +82,19 @@ class Evilution::Reporter::JSON
81
82
  detail
82
83
  end
83
84
 
85
+ def build_coverage_gaps(summary)
86
+ summary.coverage_gaps.map do |gap|
87
+ {
88
+ file: gap.file_path,
89
+ subject: gap.subject_name,
90
+ line: gap.line,
91
+ operators: gap.operator_names,
92
+ count: gap.count,
93
+ mutations: gap.mutation_results.map { |r| build_mutation_detail(r) }
94
+ }
95
+ end
96
+ end
97
+
84
98
  def build_disabled_detail(mutation)
85
99
  {
86
100
  operator: mutation.operator_name,
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../result"
4
+
5
+ class Evilution::Result::CoverageGap
6
+ attr_reader :file_path, :subject_name, :line, :mutation_results
7
+
8
+ def initialize(file_path:, subject_name:, line:, mutation_results:)
9
+ @file_path = file_path
10
+ @subject_name = subject_name
11
+ @line = line
12
+ @mutation_results = mutation_results.dup.freeze
13
+ freeze
14
+ end
15
+
16
+ def operator_names
17
+ mutation_results.map { |r| r.mutation.operator_name }.uniq
18
+ end
19
+
20
+ def primary_operator
21
+ mutation_results.first.mutation.operator_name
22
+ end
23
+
24
+ def primary_diff
25
+ mutation_results.first.mutation.diff
26
+ end
27
+
28
+ def count
29
+ mutation_results.length
30
+ end
31
+
32
+ def single?
33
+ count == 1
34
+ end
35
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "coverage_gap"
4
+
5
+ class Evilution::Result::CoverageGapGrouper
6
+ def call(survived_results)
7
+ grouped = survived_results.group_by do |r|
8
+ [r.mutation.file_path, r.mutation.subject.name, r.mutation.line]
9
+ end
10
+
11
+ gaps = grouped.map do |(file_path, subject_name, line), results|
12
+ Evilution::Result::CoverageGap.new(
13
+ file_path: file_path,
14
+ subject_name: subject_name,
15
+ line: line,
16
+ mutation_results: results
17
+ )
18
+ end
19
+
20
+ gaps.sort_by { |gap| [gap.file_path, gap.line, gap.subject_name] }
21
+ end
22
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../result"
4
+ require_relative "coverage_gap_grouper"
4
5
 
5
6
  class Evilution::Result::Summary
6
7
  attr_reader :results, :duration, :skipped, :disabled_mutations
@@ -73,6 +74,10 @@ class Evilution::Result::Summary
73
74
  results.select(&:equivalent?)
74
75
  end
75
76
 
77
+ def coverage_gaps
78
+ Evilution::Result::CoverageGapGrouper.new.call(survived_results)
79
+ end
80
+
76
81
  def killtime
77
82
  results.sum(0.0, &:duration)
78
83
  end
@@ -65,6 +65,7 @@ class Evilution::Session::Store
65
65
  git: git_context,
66
66
  summary: build_summary(summary),
67
67
  survived: summary.survived_results.map { |r| build_mutation_detail(r) },
68
+ coverage_gaps: build_coverage_gaps(summary),
68
69
  killed_count: summary.killed,
69
70
  timed_out_count: summary.timed_out,
70
71
  error_count: summary.errors,
@@ -101,6 +102,18 @@ class Evilution::Session::Store
101
102
  }
102
103
  end
103
104
 
105
+ def build_coverage_gaps(summary)
106
+ summary.coverage_gaps.map do |gap|
107
+ {
108
+ file: gap.file_path,
109
+ subject: gap.subject_name,
110
+ line: gap.line,
111
+ operators: gap.operator_names,
112
+ count: gap.count
113
+ }
114
+ end
115
+ end
116
+
104
117
  def git_context
105
118
  sha = `git rev-parse HEAD 2>/dev/null`.strip
106
119
  branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.19.0"
4
+ VERSION = "0.20.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -81,6 +81,15 @@ require_relative "evilution/mutator/operator/yield_statement"
81
81
  require_relative "evilution/mutator/operator/splat_operator"
82
82
  require_relative "evilution/mutator/operator/defined_check"
83
83
  require_relative "evilution/mutator/operator/regex_capture"
84
+ require_relative "evilution/mutator/operator/loop_flip"
85
+ require_relative "evilution/mutator/operator/string_interpolation"
86
+ require_relative "evilution/mutator/operator/retry_removal"
87
+ require_relative "evilution/mutator/operator/case_when"
88
+ require_relative "evilution/mutator/operator/predicate_replacement"
89
+ require_relative "evilution/mutator/operator/equality_to_identity"
90
+ require_relative "evilution/mutator/operator/lambda_body"
91
+ require_relative "evilution/mutator/operator/begin_unwrap"
92
+ require_relative "evilution/mutator/operator/block_param_removal"
84
93
  require_relative "evilution/mutator/registry"
85
94
  require_relative "evilution/equivalent"
86
95
  require_relative "evilution/equivalent/heuristic"