evilution 1.0.0 → 1.2.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +41 -0
  3. data/CHANGELOG.md +65 -0
  4. data/README.md +149 -13
  5. data/docs/architecture.md +20 -5
  6. data/docs/isolation.md +3 -4
  7. data/exe/evil +8 -1
  8. data/exe/evilution +8 -1
  9. data/lib/evilution/ast/local_reads.rb +43 -0
  10. data/lib/evilution/cli/exit_guard.rb +59 -0
  11. data/lib/evilution/cli/parser/options_builder.rb +1 -0
  12. data/lib/evilution/cli.rb +1 -0
  13. data/lib/evilution/config.rb +3 -2
  14. data/lib/evilution/diagnostic.rb +22 -0
  15. data/lib/evilution/integration/loading/concern_state_cleaner.rb +20 -4
  16. data/lib/evilution/integration/loading/redefinition_recovery.rb +1 -1
  17. data/lib/evilution/integration/loading/reeval_warning_filter.rb +72 -0
  18. data/lib/evilution/integration/loading/source_evaluator.rb +4 -1
  19. data/lib/evilution/integration/minitest.rb +2 -1
  20. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +9 -1
  21. data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +4 -1
  22. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +2 -1
  23. data/lib/evilution/integration/rspec.rb +48 -2
  24. data/lib/evilution/integration/test_unit/test_file_resolver.rb +2 -1
  25. data/lib/evilution/isolation/fork.rb +29 -8
  26. data/lib/evilution/mcp/complete_result_server.rb +41 -0
  27. data/lib/evilution/mcp/server.rb +2 -1
  28. data/lib/evilution/mutator/base.rb +14 -2
  29. data/lib/evilution/mutator/operator/block_destructuring_expansion.rb +85 -0
  30. data/lib/evilution/mutator/operator/block_parameter_drop.rb +96 -0
  31. data/lib/evilution/mutator/operator/boolean_expression_to_nil.rb +22 -0
  32. data/lib/evilution/mutator/operator/boolean_operand_promotion.rb +31 -0
  33. data/lib/evilution/mutator/operator/case_in.rb +64 -0
  34. data/lib/evilution/mutator/operator/case_when.rb +72 -1
  35. data/lib/evilution/mutator/operator/conditional_branch.rb +20 -7
  36. data/lib/evilution/mutator/operator/forwarding_super_to_explicit.rb +71 -0
  37. data/lib/evilution/mutator/operator/if_branch_swap.rb +48 -0
  38. data/lib/evilution/mutator/operator/loop_body_to_raise.rb +68 -0
  39. data/lib/evilution/mutator/operator/method_body_replacement.rb +10 -1
  40. data/lib/evilution/mutator/operator/method_body_to_raise.rb +59 -0
  41. data/lib/evilution/mutator/operator/method_body_to_super.rb +150 -0
  42. data/lib/evilution/mutator/operator/optional_default_injection.rb +71 -0
  43. data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +41 -0
  44. data/lib/evilution/mutator/operator/pattern_predicate.rb +28 -0
  45. data/lib/evilution/mutator/operator/typed_default_return.rb +84 -0
  46. data/lib/evilution/mutator/primitives.rb +52 -0
  47. data/lib/evilution/mutator/registry.rb +14 -0
  48. data/lib/evilution/process_supervisor.rb +20 -8
  49. data/lib/evilution/reporter/cli/item_formatters/neutral_group.rb +23 -0
  50. data/lib/evilution/reporter/cli/item_formatters/subject_score.rb +42 -0
  51. data/lib/evilution/reporter/cli/item_formatters/subject_score_group.rb +16 -0
  52. data/lib/evilution/reporter/cli/line_formatters/infra_retry_notice.rb +19 -0
  53. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +27 -3
  54. data/lib/evilution/reporter/cli/line_formatters/score.rb +19 -1
  55. data/lib/evilution/reporter/cli/line_formatters/unresolved_targets.rb +35 -0
  56. data/lib/evilution/reporter/cli/metrics_block.rb +4 -0
  57. data/lib/evilution/reporter/cli/trailer.rb +11 -7
  58. data/lib/evilution/reporter/cli.rb +20 -4
  59. data/lib/evilution/reporter/json/subjects.rb +29 -0
  60. data/lib/evilution/reporter/json.rb +23 -1
  61. data/lib/evilution/result/mutation_result.rb +3 -2
  62. data/lib/evilution/result/neutral_reason.rb +35 -0
  63. data/lib/evilution/result/subject_score.rb +28 -0
  64. data/lib/evilution/result/subject_scorer.rb +37 -0
  65. data/lib/evilution/result/summary.rb +44 -2
  66. data/lib/evilution/runner/canary.rb +52 -5
  67. data/lib/evilution/runner/mutation_executor/infra_retry.rb +54 -0
  68. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +9 -3
  69. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +18 -1
  70. data/lib/evilution/runner/mutation_executor/result_cache.rb +11 -0
  71. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +19 -1
  72. data/lib/evilution/runner/mutation_executor.rb +32 -4
  73. data/lib/evilution/runner/report_publisher.rb +31 -9
  74. data/lib/evilution/runner/target_spec_audit.rb +43 -0
  75. data/lib/evilution/runner.rb +10 -1
  76. data/lib/evilution/version.rb +1 -1
  77. data/lib/evilution.rb +18 -0
  78. data/scripts/compare_targeting +4 -2
  79. metadata +33 -2
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../neutralizer"
4
4
  require_relative "../../../result/mutation_result"
5
+ require_relative "../../../result/neutral_reason"
5
6
 
6
7
  class Evilution::Runner::MutationExecutor::Neutralizer::BaselineFailed
7
8
  def initialize(config:, spec_resolver:, fallback_dir:)
@@ -15,25 +16,30 @@ class Evilution::Runner::MutationExecutor::Neutralizer::BaselineFailed
15
16
 
16
17
  if @config.spec_files.any?
17
18
  should_neutralize = true
19
+ spec_file = nil
18
20
  else
19
21
  spec_file = @spec_resolver.call(result.mutation.file_path) || @fallback_dir
20
22
  should_neutralize = baseline_result.failed_spec_files.include?(spec_file)
21
23
  end
22
24
  return result unless should_neutralize
23
25
 
24
- neutralize(result)
26
+ neutralize(result, spec_file)
25
27
  end
26
28
 
27
29
  private
28
30
 
29
- def neutralize(result)
31
+ # The spec that was already red is the fact a reader needs: it says what to
32
+ # fix before the run can say anything about these mutations
33
+ # (EV-5pob / GH #1606).
34
+ def neutralize(result, spec_file)
30
35
  Evilution::Result::MutationResult.new(
31
36
  mutation: result.mutation,
32
37
  status: :neutral,
33
38
  duration: result.duration,
34
39
  test_command: result.test_command,
35
40
  memory: result.memory,
36
- error: result.error
41
+ error: result.error,
42
+ neutral_reason: Evilution::Result::NeutralReason.baseline_failure(spec_file)
37
43
  )
38
44
  end
39
45
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../neutralizer"
4
4
  require_relative "../../../result/mutation_result"
5
+ require_relative "../../../result/neutral_reason"
5
6
 
6
7
  # Reclassify results as :neutral when the failure was caused by test
7
8
  # infrastructure rather than by the mutation. Two independent paths:
@@ -33,6 +34,21 @@ class Evilution::Runner::MutationExecutor::Neutralizer::InfraError
33
34
  ].freeze
34
35
  private_constant :INFRA_ERROR_CLASSES, :INFRA_BACKTRACE_PATHS, :INFRA_CRASH_CLASSES
35
36
 
37
+ # Whether a crash class is one of the infrastructure failures that say
38
+ # nothing about the mutation. The parallel strategy asks this before the
39
+ # neutralisation pipeline has run, while the result is still a `:killed`
40
+ # crash (EV-j0bv / GH #1607).
41
+ def self.infra_crash_class?(error_class)
42
+ INFRA_CRASH_CLASSES.include?(error_class)
43
+ end
44
+
45
+ # Whether this result is a kill that was demoted because the test process
46
+ # crashed on infrastructure rather than on the mutation. Such a mutation got
47
+ # no verdict, so a parallel run can re-run it once the contention is over.
48
+ def self.infra_neutral?(result)
49
+ result.neutral? && infra_crash_class?(result.error_class)
50
+ end
51
+
36
52
  def call(result, **_ctx)
37
53
  return neutralize(result) if infra_crash?(result)
38
54
  return result unless result.error?
@@ -62,7 +78,8 @@ class Evilution::Runner::MutationExecutor::Neutralizer::InfraError
62
78
  duration: result.duration,
63
79
  test_command: result.test_command,
64
80
  memory: result.memory,
65
- error: result.error
81
+ error: result.error,
82
+ neutral_reason: Evilution::Result::NeutralReason.infra_error(result.error_class)
66
83
  )
67
84
  end
68
85
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../mutation_executor"
4
4
  require_relative "../../result/mutation_result"
5
+ require_relative "neutralizer/infra_error"
5
6
 
6
7
  class Evilution::Runner::MutationExecutor::ResultCache
7
8
  CACHEABLE_STATUSES = %i[killed timeout].freeze
@@ -32,6 +33,7 @@ class Evilution::Runner::MutationExecutor::ResultCache
32
33
  def store(mutation, result)
33
34
  return unless @backend
34
35
  return unless result.killed? || result.timeout?
36
+ return if infra_crash?(result)
35
37
 
36
38
  @backend.store(mutation,
37
39
  status: result.status,
@@ -40,6 +42,15 @@ class Evilution::Runner::MutationExecutor::ResultCache
40
42
  test_command: result.test_command)
41
43
  end
42
44
 
45
+ # A kill that came from an infrastructure crash is not a verdict about the
46
+ # mutation, and the cache keeps no error class to tell them apart later. It
47
+ # is left out so the next fetch does not hand back a plain `:killed`, which
48
+ # would defeat both the demotion to neutral and the serial retry
49
+ # (EV-j0bv / GH #1607).
50
+ def infra_crash?(result)
51
+ Evilution::Runner::MutationExecutor::Neutralizer::InfraError.infra_crash_class?(result.error_class)
52
+ end
53
+
43
54
  def partition(batch, packer:)
44
55
  uncached_indices = []
45
56
  cached_results = {}
@@ -65,10 +65,28 @@ class Evilution::Runner::MutationExecutor::Strategy::Parallel
65
65
  compact_results = merge(batch, partition.uncached_indices, partition.cached_results, worker_results)
66
66
  batch_results = rebuild_results(batch, compact_results)
67
67
  cache_results(batch_results, partition.uncached_indices)
68
- batch.each(&:strip_sources!)
68
+ release_sources(batch, batch_results)
69
69
  batch_results
70
70
  end
71
71
 
72
+ # Sources are dropped as soon as a mutation has been judged, to keep a long
73
+ # run's memory flat. A mutation the workers could not judge because of
74
+ # infrastructure contention is re-run serially once the pool is done
75
+ # (InfraRetry), which needs its sources, so it keeps them until then. The
76
+ # neutralisation pipeline has not run yet here, so the crash is still a kill.
77
+ def release_sources(batch, batch_results)
78
+ batch.each_with_index do |mutation, index|
79
+ result = batch_results[index]
80
+ next if result && infra_crash?(result)
81
+
82
+ mutation.strip_sources!
83
+ end
84
+ end
85
+
86
+ def infra_crash?(result)
87
+ Evilution::Runner::MutationExecutor::Neutralizer::InfraError.infra_crash_class?(result.error_class)
88
+ end
89
+
72
90
  def rebuild_results(batch, compact_results)
73
91
  batch.zip(compact_results).map { |m, h| @packer.rebuild(m, h) }
74
92
  end
@@ -3,13 +3,18 @@
3
3
  require_relative "../runner"
4
4
 
5
5
  class Evilution::Runner::MutationExecutor
6
- ExecutionResult = Data.define(:results, :truncated)
6
+ ExecutionResult = Data.define(:results, :truncated, :infra_retried) do
7
+ def initialize(results:, truncated:, infra_retried: 0)
8
+ super
9
+ end
10
+ end
7
11
 
8
12
  autoload :ResultCache, File.expand_path("mutation_executor/result_cache", __dir__)
9
13
  autoload :ResultPacker, File.expand_path("mutation_executor/result_packer", __dir__)
10
14
  autoload :ResultNotifier, File.expand_path("mutation_executor/result_notifier", __dir__)
11
15
  autoload :MutationRunner, File.expand_path("mutation_executor/mutation_runner", __dir__)
12
16
  autoload :NeutralizationPipeline, File.expand_path("mutation_executor/neutralization_pipeline", __dir__)
17
+ autoload :InfraRetry, File.expand_path("mutation_executor/infra_retry", __dir__)
13
18
  autoload :Strategy, File.expand_path("mutation_executor/strategy", __dir__)
14
19
  autoload :Neutralizer, File.expand_path("mutation_executor/neutralizer", __dir__)
15
20
 
@@ -29,13 +34,36 @@ class Evilution::Runner::MutationExecutor
29
34
  spec_resolver = baseline_failed?(baseline_result) ? @baseline_runner.neutralization_resolver : nil
30
35
  notifier = build_notifier
31
36
  pipeline = build_pipeline(spec_resolver)
32
- strategy = @config.jobs > 1 ? build_parallel(notifier, pipeline) : build_sequential(notifier, pipeline)
37
+ return sequential_run(notifier, pipeline, mutations, baseline_result, integration) if @config.jobs <= 1
33
38
 
34
- strategy.call(mutations, baseline_result: baseline_result, integration: integration)
39
+ parallel_run(notifier, pipeline, mutations, baseline_result, integration)
35
40
  end
36
41
 
37
42
  private
38
43
 
44
+ def sequential_run(notifier, pipeline, mutations, baseline_result, integration)
45
+ build_sequential(notifier, pipeline)
46
+ .call(mutations, baseline_result: baseline_result, integration: integration)
47
+ end
48
+
49
+ # Infrastructure contention only exists while the pool is running, so the
50
+ # mutations it left without a verdict are re-run once it is done. Without
51
+ # this the neutral bucket swings with --jobs on identical input
52
+ # (EV-j0bv / GH #1607).
53
+ def parallel_run(notifier, pipeline, mutations, baseline_result, integration)
54
+ execution = build_parallel(notifier, pipeline)
55
+ .call(mutations, baseline_result: baseline_result, integration: integration)
56
+ retry_pass = InfraRetry.new(runner: build_mutation_runner, pipeline: pipeline)
57
+ results = retry_pass.call(execution.results, baseline_result: baseline_result, integration: integration)
58
+
59
+ ExecutionResult.new(results: results, truncated: execution.truncated,
60
+ infra_retried: retry_pass.retried_count)
61
+ end
62
+
63
+ def build_mutation_runner
64
+ MutationRunner.new(config: @config, cache: @cache, isolator: @isolator)
65
+ end
66
+
39
67
  def baseline_failed?(baseline_result)
40
68
  baseline_result && baseline_result.failed?
41
69
  end
@@ -59,7 +87,7 @@ class Evilution::Runner::MutationExecutor
59
87
 
60
88
  def build_sequential(notifier, pipeline)
61
89
  Strategy::Sequential.new(
62
- runner: MutationRunner.new(config: @config, cache: @cache, isolator: @isolator),
90
+ runner: build_mutation_runner,
63
91
  pipeline: pipeline,
64
92
  notifier: notifier
65
93
  )
@@ -11,20 +11,42 @@ class Evilution::Runner::ReportPublisher
11
11
  @config = config
12
12
  end
13
13
 
14
- def publish(summary)
14
+ def publish(summary, stdout: $stdout)
15
15
  reporter = build_reporter
16
16
  return unless reporter
17
17
 
18
18
  output = reporter.call(summary)
19
19
  return if config.quiet
20
20
 
21
- if config.html?
22
- path = "evilution-report.html"
23
- File.write(path, output)
24
- warn "HTML report written to #{path}"
25
- else
26
- $stdout.puts(output)
27
- end
21
+ deliver(output, stdout)
22
+ end
23
+
24
+ def deliver(output, stdout)
25
+ return publish_html(output) if config.html?
26
+ return File.write(config.output, output) if config.output
27
+
28
+ stdout.puts(output)
29
+ claim_stdout(stdout) if config.json?
30
+ end
31
+
32
+ # `--preload` brings the project's at_exit hooks into this process, and
33
+ # SimpleCov's prints its coverage report on the way out. Once the document is
34
+ # written, stdout is pointed at stderr so nothing can append to it and leave a
35
+ # consumer with unparseable JSON (EV-g8ya / GH #1608). Text reports are read
36
+ # by people, not parsers, so they keep the stream as it was.
37
+ def claim_stdout(stdout)
38
+ stdout.flush
39
+ stdout.reopen($stderr)
40
+ rescue TypeError
41
+ # A harness that captures output swaps $stdout for a StringIO, which cannot
42
+ # be reopened onto an IO. There is no file descriptor to protect there.
43
+ nil
44
+ end
45
+
46
+ def publish_html(output)
47
+ path = "evilution-report.html"
48
+ File.write(path, output)
49
+ warn "HTML report written to #{path}"
28
50
  end
29
51
 
30
52
  def save_session(summary)
@@ -44,7 +66,7 @@ class Evilution::Runner::ReportPublisher
44
66
  when :json
45
67
  Evilution::Reporter::JSON.new(integration: config.integration)
46
68
  when :text
47
- Evilution::Reporter::CLI.new
69
+ Evilution::Reporter::CLI.new(min_score: config.min_score)
48
70
  when :html
49
71
  Evilution::Reporter::HTML.new(baseline: load_baseline_session, integration: config.integration)
50
72
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../runner"
4
+
5
+ # Answers, in the parent process and before any mutation runs, which of the
6
+ # files evilution was pointed at resolve to no test file at all.
7
+ #
8
+ # The integrations resolve specs per mutation inside the worker, which is what
9
+ # produces `:unresolved` results, but that is a per-mutation fact diluted by
10
+ # every other file in the run: a target with no spec at all contributes a
11
+ # handful of unresolved mutations and disappears behind a healthy score
12
+ # (EV-p4sm / GH #1603). Asking the same selector once per file, up front, keeps
13
+ # the stronger fact — "you named this file and it was never tested" — intact.
14
+ #
15
+ # Only files that produced a subject are audited: a file evilution found
16
+ # nothing to mutate in — a constants-only file, say — has no untested behaviour
17
+ # to report, and failing a run over one would be noise.
18
+ #
19
+ # With `fallback_to_full_suite` a file without its own spec is run against the
20
+ # whole suite rather than skipped, so nothing goes untested and there is
21
+ # nothing to report.
22
+ class Evilution::Runner::TargetSpecAudit
23
+ def initialize(config)
24
+ @config = config
25
+ end
26
+
27
+ def call(file_paths)
28
+ return [] if @config.fallback_to_full_suite?
29
+
30
+ file_paths.uniq.sort.reject { |path| resolved?(path) }
31
+ end
32
+
33
+ private
34
+
35
+ # SpecSelector answers nil when nothing resolves; a custom resolver plugged
36
+ # in through it may answer an empty list instead.
37
+ def resolved?(path)
38
+ specs = @config.spec_selector.call(path)
39
+ return false if specs.nil?
40
+
41
+ !specs.empty?
42
+ end
43
+ end
@@ -46,9 +46,13 @@ class Evilution::Runner
46
46
 
47
47
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
48
48
 
49
+ audited_files = subjects.map(&:file_path).uniq.sort
49
50
  summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: execution.truncated,
50
51
  skipped: plan.skipped_count,
51
- disabled_mutations: plan.disabled_mutations)
52
+ disabled_mutations: plan.disabled_mutations,
53
+ unresolved_target_files: target_spec_audit.call(audited_files),
54
+ target_file_count: audited_files.length,
55
+ infra_retried: execution.infra_retried)
52
56
  output_report(summary)
53
57
  save_session(summary)
54
58
 
@@ -71,6 +75,10 @@ class Evilution::Runner
71
75
  @mutation_planner ||= Evilution::Runner::MutationPlanner.new(config, registry: registry)
72
76
  end
73
77
 
78
+ def target_spec_audit
79
+ @target_spec_audit ||= Evilution::Runner::TargetSpecAudit.new(config)
80
+ end
81
+
74
82
  def isolation_resolver
75
83
  @isolation_resolver ||= Evilution::Runner::IsolationResolver.new(
76
84
  config,
@@ -240,6 +248,7 @@ require_relative "rails_detector"
240
248
  require_relative "parallel_db_warning"
241
249
  require_relative "child_output"
242
250
  require_relative "runner/subject_pipeline"
251
+ require_relative "runner/target_spec_audit"
243
252
  require_relative "runner/mutation_planner"
244
253
  require_relative "runner/isolation_resolver"
245
254
  require_relative "runner/baseline_runner"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "1.0.0"
4
+ VERSION = "1.2.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -13,6 +13,7 @@ require_relative "evilution/ast/parser"
13
13
  require_relative "evilution/ast/inheritance_scanner"
14
14
  require_relative "evilution/ast/pattern"
15
15
  require_relative "evilution/ast/sorbet_sig_detector"
16
+ require_relative "evilution/ast/local_reads"
16
17
  require_relative "evilution/ast/pattern/matcher"
17
18
  require_relative "evilution/ast/pattern/parser"
18
19
  require_relative "evilution/hooks"
@@ -27,11 +28,14 @@ require_relative "evilution/mutator/operator/integer_literal"
27
28
  require_relative "evilution/mutator/operator/float_literal"
28
29
  require_relative "evilution/mutator/operator/nil_replacement"
29
30
  require_relative "evilution/mutator/operator/boolean_operator_replacement"
31
+ require_relative "evilution/mutator/operator/boolean_operand_promotion"
32
+ require_relative "evilution/mutator/operator/boolean_expression_to_nil"
30
33
  require_relative "evilution/mutator/operator/arithmetic_replacement"
31
34
  require_relative "evilution/mutator/operator/string_literal"
32
35
  require_relative "evilution/mutator/operator/array_literal"
33
36
  require_relative "evilution/mutator/operator/hash_literal"
34
37
  require_relative "evilution/mutator/operator/conditional_branch"
38
+ require_relative "evilution/mutator/operator/if_branch_swap"
35
39
  require_relative "evilution/mutator/operator/symbol_literal"
36
40
  require_relative "evilution/mutator/operator/conditional_negation"
37
41
  require_relative "evilution/mutator/operator/negation_insertion"
@@ -85,9 +89,20 @@ require_relative "evilution/mutator/operator/splat_operator"
85
89
  require_relative "evilution/mutator/operator/defined_check"
86
90
  require_relative "evilution/mutator/operator/regex_capture"
87
91
  require_relative "evilution/mutator/operator/loop_flip"
92
+ require_relative "evilution/mutator/operator/loop_body_to_raise"
93
+ require_relative "evilution/mutator/operator/method_body_to_raise"
94
+ require_relative "evilution/mutator/operator/method_body_to_super"
95
+ require_relative "evilution/mutator/operator/typed_default_return"
96
+ require_relative "evilution/mutator/operator/block_parameter_drop"
97
+ require_relative "evilution/mutator/operator/optional_parameter_to_required"
98
+ require_relative "evilution/mutator/operator/optional_default_injection"
99
+ require_relative "evilution/mutator/operator/block_destructuring_expansion"
100
+ require_relative "evilution/mutator/operator/forwarding_super_to_explicit"
88
101
  require_relative "evilution/mutator/operator/string_interpolation"
89
102
  require_relative "evilution/mutator/operator/retry_removal"
90
103
  require_relative "evilution/mutator/operator/case_when"
104
+ require_relative "evilution/mutator/operator/case_in"
105
+ require_relative "evilution/mutator/operator/pattern_predicate"
91
106
  require_relative "evilution/mutator/operator/predicate_replacement"
92
107
  require_relative "evilution/mutator/operator/predicate_to_nil"
93
108
  require_relative "evilution/mutator/operator/equality_to_identity"
@@ -115,6 +130,9 @@ require_relative "evilution/integration/base"
115
130
  require_relative "evilution/integration/rspec"
116
131
  require_relative "evilution/integration/minitest"
117
132
  require_relative "evilution/result/mutation_result"
133
+ require_relative "evilution/result/neutral_reason"
134
+ require_relative "evilution/result/subject_score"
135
+ require_relative "evilution/result/subject_scorer"
118
136
  require_relative "evilution/result/summary"
119
137
  require_relative "evilution/reporter"
120
138
  require_relative "evilution/reporter/json"
@@ -128,8 +128,10 @@ module CompareTargeting
128
128
  def lost_kills(mode = "coverage")
129
129
  full = @modes.fetch("full_file")
130
130
  other = @modes.fetch(mode)
131
- full.keys.select { |key| full.status(key) == "killed" && other.status(key) != "killed" }
132
- .map { |key| full.label(key) }
131
+ full
132
+ .keys
133
+ .select { |key| full.status(key) == "killed" && other.status(key) != "killed" }
134
+ .map { |key| full.label(key) }
133
135
  end
134
136
 
135
137
  def to_row(repo)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evilution
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Kiselev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-15 00:00:00.000000000 Z
11
+ date: 2026-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -123,6 +123,7 @@ files:
123
123
  - lib/evilution/ast/constant_names.rb
124
124
  - lib/evilution/ast/heredoc_span.rb
125
125
  - lib/evilution/ast/inheritance_scanner.rb
126
+ - lib/evilution/ast/local_reads.rb
126
127
  - lib/evilution/ast/parser.rb
127
128
  - lib/evilution/ast/pattern.rb
128
129
  - lib/evilution/ast/pattern/filter.rb
@@ -150,6 +151,7 @@ files:
150
151
  - lib/evilution/cli/commands/util_mutation.rb
151
152
  - lib/evilution/cli/commands/version.rb
152
153
  - lib/evilution/cli/dispatcher.rb
154
+ - lib/evilution/cli/exit_guard.rb
153
155
  - lib/evilution/cli/parsed_args.rb
154
156
  - lib/evilution/cli/parser.rb
155
157
  - lib/evilution/cli/parser/command_extractor.rb
@@ -206,6 +208,7 @@ files:
206
208
  - lib/evilution/coverage/map_store.rb
207
209
  - lib/evilution/coverage/recorder.rb
208
210
  - lib/evilution/coverage_example_filter.rb
211
+ - lib/evilution/diagnostic.rb
209
212
  - lib/evilution/disable_comment.rb
210
213
  - lib/evilution/equivalent.rb
211
214
  - lib/evilution/equivalent/detector.rb
@@ -237,6 +240,7 @@ files:
237
240
  - lib/evilution/integration/loading/constant_pinner.rb
238
241
  - lib/evilution/integration/loading/mutation_applier.rb
239
242
  - lib/evilution/integration/loading/redefinition_recovery.rb
243
+ - lib/evilution/integration/loading/reeval_warning_filter.rb
240
244
  - lib/evilution/integration/loading/source_evaluator.rb
241
245
  - lib/evilution/integration/loading/syntax_validator.rb
242
246
  - lib/evilution/integration/loading/test_load_path.rb
@@ -273,6 +277,7 @@ files:
273
277
  - lib/evilution/load_path.rb
274
278
  - lib/evilution/load_path/subpath_resolver.rb
275
279
  - lib/evilution/mcp.rb
280
+ - lib/evilution/mcp/complete_result_server.rb
276
281
  - lib/evilution/mcp/info_tool.rb
277
282
  - lib/evilution/mcp/info_tool/actions.rb
278
283
  - lib/evilution/mcp/info_tool/actions/base.rb
@@ -310,12 +315,17 @@ files:
310
315
  - lib/evilution/mutator/operator/begin_unwrap.rb
311
316
  - lib/evilution/mutator/operator/bitwise_complement.rb
312
317
  - lib/evilution/mutator/operator/bitwise_replacement.rb
318
+ - lib/evilution/mutator/operator/block_destructuring_expansion.rb
313
319
  - lib/evilution/mutator/operator/block_param_removal.rb
320
+ - lib/evilution/mutator/operator/block_parameter_drop.rb
314
321
  - lib/evilution/mutator/operator/block_pass_removal.rb
315
322
  - lib/evilution/mutator/operator/block_removal.rb
323
+ - lib/evilution/mutator/operator/boolean_expression_to_nil.rb
316
324
  - lib/evilution/mutator/operator/boolean_literal_replacement.rb
325
+ - lib/evilution/mutator/operator/boolean_operand_promotion.rb
317
326
  - lib/evilution/mutator/operator/boolean_operator_replacement.rb
318
327
  - lib/evilution/mutator/operator/break_statement.rb
328
+ - lib/evilution/mutator/operator/case_in.rb
319
329
  - lib/evilution/mutator/operator/case_when.rb
320
330
  - lib/evilution/mutator/operator/class_variable_write.rb
321
331
  - lib/evilution/mutator/operator/collection_replacement.rb
@@ -330,8 +340,10 @@ files:
330
340
  - lib/evilution/mutator/operator/equality_to_identity.rb
331
341
  - lib/evilution/mutator/operator/explicit_super_mutation.rb
332
342
  - lib/evilution/mutator/operator/float_literal.rb
343
+ - lib/evilution/mutator/operator/forwarding_super_to_explicit.rb
333
344
  - lib/evilution/mutator/operator/global_variable_write.rb
334
345
  - lib/evilution/mutator/operator/hash_literal.rb
346
+ - lib/evilution/mutator/operator/if_branch_swap.rb
335
347
  - lib/evilution/mutator/operator/index_assignment_removal.rb
336
348
  - lib/evilution/mutator/operator/index_to_at.rb
337
349
  - lib/evilution/mutator/operator/index_to_dig.rb
@@ -343,17 +355,23 @@ files:
343
355
  - lib/evilution/mutator/operator/lambda_body.rb
344
356
  - lib/evilution/mutator/operator/last_expression_removal.rb
345
357
  - lib/evilution/mutator/operator/local_variable_assignment.rb
358
+ - lib/evilution/mutator/operator/loop_body_to_raise.rb
346
359
  - lib/evilution/mutator/operator/loop_flip.rb
347
360
  - lib/evilution/mutator/operator/method_body_replacement.rb
361
+ - lib/evilution/mutator/operator/method_body_to_raise.rb
362
+ - lib/evilution/mutator/operator/method_body_to_super.rb
348
363
  - lib/evilution/mutator/operator/method_call_removal.rb
349
364
  - lib/evilution/mutator/operator/mixin_removal.rb
350
365
  - lib/evilution/mutator/operator/multiple_assignment.rb
351
366
  - lib/evilution/mutator/operator/negation_insertion.rb
352
367
  - lib/evilution/mutator/operator/next_statement.rb
353
368
  - lib/evilution/mutator/operator/nil_replacement.rb
369
+ - lib/evilution/mutator/operator/optional_default_injection.rb
370
+ - lib/evilution/mutator/operator/optional_parameter_to_required.rb
354
371
  - lib/evilution/mutator/operator/pattern_matching_alternative.rb
355
372
  - lib/evilution/mutator/operator/pattern_matching_array.rb
356
373
  - lib/evilution/mutator/operator/pattern_matching_guard.rb
374
+ - lib/evilution/mutator/operator/pattern_predicate.rb
357
375
  - lib/evilution/mutator/operator/predicate_replacement.rb
358
376
  - lib/evilution/mutator/operator/predicate_to_nil.rb
359
377
  - lib/evilution/mutator/operator/range_replacement.rb
@@ -374,8 +392,10 @@ files:
374
392
  - lib/evilution/mutator/operator/string_literal.rb
375
393
  - lib/evilution/mutator/operator/superclass_removal.rb
376
394
  - lib/evilution/mutator/operator/symbol_literal.rb
395
+ - lib/evilution/mutator/operator/typed_default_return.rb
377
396
  - lib/evilution/mutator/operator/yield_statement.rb
378
397
  - lib/evilution/mutator/operator/zsuper_removal.rb
398
+ - lib/evilution/mutator/primitives.rb
379
399
  - lib/evilution/mutator/registry.rb
380
400
  - lib/evilution/parallel.rb
381
401
  - lib/evilution/parallel/pool.rb
@@ -403,19 +423,24 @@ files:
403
423
  - lib/evilution/reporter/cli/item_formatters/coverage_gap.rb
404
424
  - lib/evilution/reporter/cli/item_formatters/disabled.rb
405
425
  - lib/evilution/reporter/cli/item_formatters/error.rb
426
+ - lib/evilution/reporter/cli/item_formatters/neutral_group.rb
406
427
  - lib/evilution/reporter/cli/item_formatters/result_location.rb
428
+ - lib/evilution/reporter/cli/item_formatters/subject_score.rb
429
+ - lib/evilution/reporter/cli/item_formatters/subject_score_group.rb
407
430
  - lib/evilution/reporter/cli/line_formatters.rb
408
431
  - lib/evilution/reporter/cli/line_formatters/duration.rb
409
432
  - lib/evilution/reporter/cli/line_formatters/efficiency.rb
410
433
  - lib/evilution/reporter/cli/line_formatters/error_rate_warning.rb
411
434
  - lib/evilution/reporter/cli/line_formatters/feedback_footer.rb
412
435
  - lib/evilution/reporter/cli/line_formatters/header.rb
436
+ - lib/evilution/reporter/cli/line_formatters/infra_retry_notice.rb
413
437
  - lib/evilution/reporter/cli/line_formatters/mutations.rb
414
438
  - lib/evilution/reporter/cli/line_formatters/peak_memory.rb
415
439
  - lib/evilution/reporter/cli/line_formatters/result_line.rb
416
440
  - lib/evilution/reporter/cli/line_formatters/score.rb
417
441
  - lib/evilution/reporter/cli/line_formatters/truncation_notice.rb
418
442
  - lib/evilution/reporter/cli/line_formatters/unresolved_rate_warning.rb
443
+ - lib/evilution/reporter/cli/line_formatters/unresolved_targets.rb
419
444
  - lib/evilution/reporter/cli/metrics_block.rb
420
445
  - lib/evilution/reporter/cli/pct.rb
421
446
  - lib/evilution/reporter/cli/section.rb
@@ -458,6 +483,7 @@ files:
458
483
  - lib/evilution/reporter/html/templates/unparseable_details.html.erb
459
484
  - lib/evilution/reporter/html/templates/unresolved_details.html.erb
460
485
  - lib/evilution/reporter/json.rb
486
+ - lib/evilution/reporter/json/subjects.rb
461
487
  - lib/evilution/reporter/progress_bar.rb
462
488
  - lib/evilution/reporter/suggestion.rb
463
489
  - lib/evilution/reporter/suggestion/diff_helpers.rb
@@ -473,6 +499,9 @@ files:
473
499
  - lib/evilution/result/error_info.rb
474
500
  - lib/evilution/result/memory_stats.rb
475
501
  - lib/evilution/result/mutation_result.rb
502
+ - lib/evilution/result/neutral_reason.rb
503
+ - lib/evilution/result/subject_score.rb
504
+ - lib/evilution/result/subject_scorer.rb
476
505
  - lib/evilution/result/summary.rb
477
506
  - lib/evilution/runner.rb
478
507
  - lib/evilution/runner/baseline_runner.rb
@@ -480,6 +509,7 @@ files:
480
509
  - lib/evilution/runner/diagnostics.rb
481
510
  - lib/evilution/runner/isolation_resolver.rb
482
511
  - lib/evilution/runner/mutation_executor.rb
512
+ - lib/evilution/runner/mutation_executor/infra_retry.rb
483
513
  - lib/evilution/runner/mutation_executor/mutation_runner.rb
484
514
  - lib/evilution/runner/mutation_executor/neutralization_pipeline.rb
485
515
  - lib/evilution/runner/mutation_executor/neutralizer.rb
@@ -494,6 +524,7 @@ files:
494
524
  - lib/evilution/runner/mutation_planner.rb
495
525
  - lib/evilution/runner/report_publisher.rb
496
526
  - lib/evilution/runner/subject_pipeline.rb
527
+ - lib/evilution/runner/target_spec_audit.rb
497
528
  - lib/evilution/session.rb
498
529
  - lib/evilution/session/diff.rb
499
530
  - lib/evilution/session/schema.rb