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
@@ -4,6 +4,8 @@ require_relative "../work_queue"
4
4
  require_relative "collection_state"
5
5
 
6
6
  class Evilution::Parallel::WorkQueue::Dispatcher
7
+ RunResult = Data.define(:results, :retired)
8
+
7
9
  attr_reader :first_error
8
10
 
9
11
  def initialize(workers:, items:, prefetch:, item_timeout:, worker_max_items:, recycle_factory:)
@@ -21,7 +23,7 @@ class Evilution::Parallel::WorkQueue::Dispatcher
21
23
  seed
22
24
  collect
23
25
  @first_error = @state.first_error
24
- [@state.results, @retired]
26
+ RunResult.new(results: @state.results, retired: @retired)
25
27
  end
26
28
 
27
29
  private
@@ -42,20 +44,25 @@ class Evilution::Parallel::WorkQueue::Dispatcher
42
44
 
43
45
  while @state.in_flight.positive?
44
46
  readable, = IO.select(result_ios, nil, nil, @item_timeout)
45
-
46
47
  if readable.nil?
47
- terminate_stuck
48
- @state.first_error ||= Evilution::Error.new("worker timed out after #{@item_timeout}s")
48
+ record_timeout
49
49
  break
50
50
  end
51
51
 
52
- readable.each do |io|
53
- alive = handle(io_to_worker[io], io_to_worker, result_ios)
54
- result_ios.delete(io) unless alive
55
- end
52
+ readable.each { |io| process_readable(io, io_to_worker, result_ios) }
56
53
  end
57
54
  end
58
55
 
56
+ def record_timeout
57
+ terminate_stuck
58
+ @state.first_error ||= Evilution::Error.new("worker timed out after #{@item_timeout}s")
59
+ end
60
+
61
+ def process_readable(io, io_to_worker, result_ios)
62
+ alive = handle(io_to_worker[io], io_to_worker, result_ios)
63
+ result_ios.delete(io) unless alive
64
+ end
65
+
59
66
  def handle(worker, io_to_worker, result_ios)
60
67
  message = worker.read_result
61
68
  return handle_dead(worker) if message.nil?
@@ -36,7 +36,7 @@ module Evilution::Parallel::WorkQueue::Worker::Loop
36
36
  result = block.call(item)
37
37
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
38
38
  Evilution::Parallel::WorkQueue::Channel.write(res_io, [index, :ok, result])
39
- rescue Exception => e # rubocop:disable Lint/RescueException
39
+ rescue StandardError, ScriptError, SystemStackError, NoMemoryError, SecurityError => e
40
40
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
41
41
  Evilution::Parallel::WorkQueue::Channel.write(res_io, [index, :error, e])
42
42
  end
@@ -6,6 +6,8 @@ require_relative "channel"
6
6
  require_relative "channel/frame"
7
7
 
8
8
  class Evilution::Parallel::WorkQueue::Worker
9
+ Timing = Data.define(:busy, :wall)
10
+
9
11
  attr_reader :pid, :worker_index
10
12
  attr_accessor :items_completed, :pending, :busy_time, :wall_time
11
13
 
@@ -84,11 +86,11 @@ class Evilution::Parallel::WorkQueue::Worker
84
86
 
85
87
  def retire
86
88
  shutdown
87
- busy, wall = drain_stats
89
+ timing = drain_stats
88
90
  close_pipes
89
91
  reap
90
- @busy_time = busy
91
- @wall_time = wall
92
+ @busy_time = timing.busy
93
+ @wall_time = timing.wall
92
94
  to_stat
93
95
  end
94
96
 
@@ -101,14 +103,15 @@ class Evilution::Parallel::WorkQueue::Worker
101
103
  private
102
104
 
103
105
  def drain_stats
104
- return [0.0, 0.0] unless @res_read.wait_readable(Evilution::Parallel::WorkQueue::TIMING_GRACE_PERIOD)
106
+ zero = Timing.new(busy: 0.0, wall: 0.0)
107
+ return zero unless @res_read.wait_readable(Evilution::Parallel::WorkQueue::TIMING_GRACE_PERIOD)
105
108
 
106
109
  message = read_result
107
- return [0.0, 0.0] if message.nil?
110
+ return zero if message.nil?
108
111
 
109
112
  tag, busy, wall = message
110
- return [0.0, 0.0] unless tag == Evilution::Parallel::WorkQueue::STATS
113
+ return zero unless tag == Evilution::Parallel::WorkQueue::STATS
111
114
 
112
- [busy, wall]
115
+ Timing.new(busy: busy, wall: wall)
113
116
  end
114
117
  end
@@ -27,24 +27,17 @@ class Evilution::Parallel::WorkQueue
27
27
  return [] if items.empty?
28
28
 
29
29
  workers = (0...[@size, items.length].min).map { |i| spawn_one(i, &block) }
30
- dispatcher = Dispatcher.new(
31
- workers: workers, items: items, prefetch: @prefetch,
32
- item_timeout: @item_timeout, worker_max_items: @worker_max_items,
33
- recycle_factory: ->(old) { spawn_one(old.worker_index, &block) }
34
- )
30
+ dispatcher = build_dispatcher(workers, items, &block)
35
31
 
36
32
  retired = []
37
33
  begin
38
- results, retired = dispatcher.run
34
+ run_result = dispatcher.run
35
+ retired = run_result.retired
39
36
  raise dispatcher.first_error if dispatcher.first_error
40
37
 
41
- results
38
+ run_result.results
42
39
  ensure
43
- workers.each(&:shutdown)
44
- collect_final_timings(workers)
45
- workers.each(&:close_pipes)
46
- workers.each(&:reap)
47
- @worker_stats = retired + workers.map(&:to_stat)
40
+ cleanup_workers(workers, retired)
48
41
  end
49
42
  end
50
43
 
@@ -58,19 +51,43 @@ class Evilution::Parallel::WorkQueue
58
51
  Worker.spawn(worker_index: worker_index, hooks: @hooks, &)
59
52
  end
60
53
 
54
+ def build_dispatcher(workers, items, &block)
55
+ Dispatcher.new(
56
+ workers: workers, items: items, prefetch: @prefetch,
57
+ item_timeout: @item_timeout, worker_max_items: @worker_max_items,
58
+ recycle_factory: ->(old) { spawn_one(old.worker_index, &block) }
59
+ )
60
+ end
61
+
62
+ def cleanup_workers(workers, retired)
63
+ workers.each(&:shutdown)
64
+ collect_final_timings(workers)
65
+ workers.each(&:close_pipes)
66
+ workers.each(&:reap)
67
+ @worker_stats = retired + workers.map(&:to_stat)
68
+ end
69
+
61
70
  def collect_final_timings(workers)
62
71
  io_to_worker = workers.reject { |w| w.res_io.closed? }.to_h { |w| [w.res_io, w] }
63
- deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + TIMING_GRACE_PERIOD
72
+ deadline = monotonic_now + TIMING_GRACE_PERIOD
64
73
 
65
74
  until io_to_worker.empty?
66
- remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
75
+ remaining = deadline - monotonic_now
67
76
  break if remaining <= 0
77
+ break unless poll_and_apply(io_to_worker, remaining)
78
+ end
79
+ end
68
80
 
69
- readable, = IO.select(io_to_worker.keys, nil, nil, remaining)
70
- break unless readable
81
+ def poll_and_apply(io_to_worker, remaining)
82
+ readable, = IO.select(io_to_worker.keys, nil, nil, remaining)
83
+ return false unless readable
71
84
 
72
- readable.each { |io| apply_final_timing(io_to_worker.delete(io), io) }
73
- end
85
+ readable.each { |io| apply_final_timing(io_to_worker.delete(io), io) }
86
+ true
87
+ end
88
+
89
+ def monotonic_now
90
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
74
91
  end
75
92
 
76
93
  def apply_final_timing(worker, io)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version"
4
+
5
+ module Evilution::ProcessCleanup
6
+ module_function
7
+
8
+ def safe_kill(signal, pid)
9
+ ::Process.kill(signal, pid)
10
+ rescue Errno::ESRCH
11
+ nil
12
+ end
13
+
14
+ def safe_wait(pid)
15
+ ::Process.wait(pid)
16
+ rescue Errno::ECHILD
17
+ nil
18
+ end
19
+ end
@@ -5,14 +5,19 @@ require_relative "../item_formatters"
5
5
  class Evilution::Reporter::CLI::ItemFormatters::CoverageGap
6
6
  def format(gap)
7
7
  location = "#{gap.file_path}:#{gap.line}"
8
- header = if gap.single?
9
- " #{gap.primary_operator}: #{location} (#{gap.subject_name})"
10
- else
11
- operators = gap.operator_names.join(", ")
12
- " #{location} (#{gap.subject_name}) [#{gap.count} mutations: #{operators}]"
13
- end
8
+ "#{format_header(gap, location)}\n#{format_body(gap)}"
9
+ end
10
+
11
+ private
12
+
13
+ def format_header(gap, location)
14
+ return " #{gap.primary_operator}: #{location} (#{gap.subject_name})" if gap.single?
15
+
16
+ " #{location} (#{gap.subject_name}) [#{gap.count} mutations: #{gap.operator_names.join(", ")}]"
17
+ end
18
+
19
+ def format_body(gap)
14
20
  body = gap.mutation_results.first.mutation.unified_diff || gap.primary_diff
15
- indented = body.split("\n").map { |l| " #{l}" }.join("\n")
16
- "#{header}\n#{indented}"
21
+ body.split("\n").map { |l| " #{l}" }.join("\n")
17
22
  end
18
23
  end
@@ -3,14 +3,23 @@
3
3
  require_relative "../line_formatters"
4
4
 
5
5
  class Evilution::Reporter::CLI::LineFormatters::Mutations
6
+ OPTIONAL_FIELDS = %i[neutral equivalent unresolved unparseable skipped].freeze
7
+
6
8
  def format(summary)
7
- parts = "Mutations: #{summary.total} total, #{summary.killed} killed, " \
8
- "#{summary.survived} survived, #{summary.timed_out} timed out"
9
- parts += ", #{summary.neutral} neutral" if summary.neutral.positive?
10
- parts += ", #{summary.equivalent} equivalent" if summary.equivalent.positive?
11
- parts += ", #{summary.unresolved} unresolved" if summary.unresolved.positive?
12
- parts += ", #{summary.unparseable} unparseable" if summary.unparseable.positive?
13
- parts += ", #{summary.skipped} skipped" if summary.skipped.positive?
14
- parts
9
+ base_line(summary) + optional_sections(summary)
10
+ end
11
+
12
+ private
13
+
14
+ def base_line(summary)
15
+ "Mutations: #{summary.total} total, #{summary.killed} killed, " \
16
+ "#{summary.survived} survived, #{summary.timed_out} timed out"
17
+ end
18
+
19
+ def optional_sections(summary)
20
+ OPTIONAL_FIELDS.filter_map do |field|
21
+ count = summary.public_send(field)
22
+ ", #{count} #{field}" if count.positive?
23
+ end.join
15
24
  end
16
25
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "namespace"
3
+ require_relative "../html"
4
4
 
5
5
  class Evilution::Reporter::HTML::BaselineKeys
6
6
  def initialize(baseline)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "namespace"
3
+ require_relative "../html"
4
4
  require_relative "escape"
5
5
 
6
6
  module Evilution::Reporter::HTML::DiffFormatter
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "cgi"
4
- require_relative "namespace"
4
+ require_relative "../html"
5
5
 
6
6
  module Evilution::Reporter::HTML::Escape
7
7
  module_function
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
- require_relative "namespace"
4
+ require_relative "../html"
5
5
  require_relative "escape"
6
6
 
7
7
  class Evilution::Reporter::HTML::Section
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "namespace"
4
- require_relative "section"
3
+ require_relative "../html"
4
+
5
+ module Evilution::Reporter::HTML::Sections
6
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "namespace"
3
+ require_relative "../html"
4
4
 
5
5
  module Evilution::Reporter::HTML::Stylesheet
6
6
  PATH = File.expand_path("assets/style.css", __dir__)
@@ -2,11 +2,16 @@
2
2
 
3
3
  require_relative "../reporter"
4
4
  require_relative "suggestion"
5
- require_relative "html/escape"
6
- require_relative "html/baseline_keys"
7
- require_relative "html/report"
8
5
 
9
6
  class Evilution::Reporter::HTML
7
+ autoload :Escape, "evilution/reporter/html/escape"
8
+ autoload :BaselineKeys, "evilution/reporter/html/baseline_keys"
9
+ autoload :Section, "evilution/reporter/html/section"
10
+ autoload :Sections, "evilution/reporter/html/sections"
11
+ autoload :Stylesheet, "evilution/reporter/html/stylesheet"
12
+ autoload :DiffFormatter, "evilution/reporter/html/diff_formatter"
13
+ autoload :Report, "evilution/reporter/html/report"
14
+
10
15
  def initialize(baseline: nil, integration: :rspec)
11
16
  @suggestion = Evilution::Reporter::Suggestion.new(integration: integration)
12
17
  @baseline = baseline
@@ -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
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../suggestion"
4
-
5
- # rubocop:disable Style/OneClassPerFile
6
- module Evilution::Reporter::Suggestion::Templates
7
- end
4
+ require_relative "templates"
8
5
 
9
6
  class Evilution::Reporter::Suggestion::Registry
10
- # rubocop:enable Style/OneClassPerFile
11
7
  def self.default
12
8
  return @default if @default
13
9
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../registry"
3
+ require_relative "../templates"
4
4
 
5
5
  module Evilution::Reporter::Suggestion::Templates::Generic
6
6
  GENERIC_ENTRIES = {