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.
- checksums.yaml +4 -4
- data/.beads/interactions.jsonl +65 -0
- data/.rubocop_todo.yml +0 -1
- data/CHANGELOG.md +39 -0
- data/README.md +19 -0
- data/lib/evilution/ast/constant_names.rb +28 -11
- data/lib/evilution/ast/pattern/parser.rb +29 -17
- data/lib/evilution/baseline.rb +5 -4
- data/lib/evilution/cli/commands/session_diff.rb +6 -4
- data/lib/evilution/cli/commands/subjects.rb +6 -3
- data/lib/evilution/cli/commands/util_mutation.rb +24 -19
- data/lib/evilution/cli/parser/command_extractor.rb +9 -11
- data/lib/evilution/cli/parser/file_args.rb +3 -1
- data/lib/evilution/cli/parser/options_builder.rb +36 -1
- data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
- data/lib/evilution/cli/parser.rb +18 -20
- data/lib/evilution/cli/printers/environment.rb +19 -19
- data/lib/evilution/cli/printers/session_diff.rb +8 -8
- data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
- data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
- data/lib/evilution/compare/diff_extractor.rb +6 -0
- data/lib/evilution/compare/fingerprint.rb +15 -72
- data/lib/evilution/compare/line_normalizer.rb +72 -0
- data/lib/evilution/compare/normalizer.rb +27 -9
- data/lib/evilution/config/validators/profile.rb +11 -0
- data/lib/evilution/config.rb +49 -32
- data/lib/evilution/disable_comment.rb +21 -12
- data/lib/evilution/integration/crash_detector.rb +2 -2
- data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
- data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
- data/lib/evilution/integration/minitest.rb +25 -16
- data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +11 -3
- data/lib/evilution/integration/rspec.rb +4 -0
- data/lib/evilution/isolation/fork.rb +43 -28
- data/lib/evilution/isolation/in_process.rb +10 -6
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
- data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
- data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
- data/lib/evilution/mcp/info_tool.rb +7 -3
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
- data/lib/evilution/mcp/mutate_tool.rb +27 -14
- data/lib/evilution/mcp/session_tool.rb +27 -20
- data/lib/evilution/mutation.rb +60 -42
- data/lib/evilution/mutator/base.rb +23 -21
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
- data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
- data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
- data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
- data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
- data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
- data/lib/evilution/mutator/operator/case_when.rb +7 -5
- data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
- data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
- data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
- data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
- data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
- data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
- data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
- data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
- data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
- data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
- data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
- data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
- data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
- data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
- data/lib/evilution/mutator/registry.rb +20 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +5 -1
- data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
- data/lib/evilution/parallel/work_queue/worker/loop.rb +1 -1
- data/lib/evilution/parallel/work_queue/worker.rb +10 -7
- data/lib/evilution/parallel/work_queue.rb +35 -18
- data/lib/evilution/process_cleanup.rb +19 -0
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
- data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
- data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
- data/lib/evilution/reporter/html/escape.rb +1 -1
- data/lib/evilution/reporter/html/section.rb +1 -1
- data/lib/evilution/reporter/html/sections.rb +4 -2
- data/lib/evilution/reporter/html/stylesheet.rb +1 -1
- data/lib/evilution/reporter/html.rb +8 -3
- data/lib/evilution/reporter/json.rb +52 -18
- data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
- data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
- data/lib/evilution/reporter/suggestion/registry.rb +1 -5
- data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +361 -649
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +362 -603
- data/lib/evilution/reporter/suggestion/templates.rb +6 -0
- data/lib/evilution/result/error_info.rb +20 -0
- data/lib/evilution/result/memory_stats.rb +20 -0
- data/lib/evilution/result/mutation_result.rb +30 -14
- data/lib/evilution/runner/baseline_runner.rb +16 -10
- data/lib/evilution/runner/diagnostics.rb +14 -11
- data/lib/evilution/runner/isolation_resolver.rb +12 -11
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +1 -3
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +1 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +3 -10
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +3 -10
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +4 -4
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +1 -3
- data/lib/evilution/runner/mutation_executor/result_packer.rb +11 -9
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +33 -13
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +2 -4
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +14 -20
- data/lib/evilution/runner/mutation_planner.rb +38 -19
- data/lib/evilution/runner/report_publisher.rb +1 -2
- data/lib/evilution/runner/subject_pipeline.rb +22 -13
- data/lib/evilution/runner.rb +36 -34
- data/lib/evilution/session/diff.rb +15 -6
- data/lib/evilution/spec_ast_cache.rb +26 -12
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- data/script/memory_check +14 -6
- data/scripts/benchmark_density +10 -9
- data/scripts/compare_mutations +38 -21
- data/scripts/mutant_json_adapter +7 -4
- metadata +15 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
110
|
+
return zero if message.nil?
|
|
108
111
|
|
|
109
112
|
tag, busy, wall = message
|
|
110
|
-
return
|
|
113
|
+
return zero unless tag == Evilution::Parallel::WorkQueue::STATS
|
|
111
114
|
|
|
112
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
72
|
+
deadline = monotonic_now + TIMING_GRACE_PERIOD
|
|
64
73
|
|
|
65
74
|
until io_to_worker.empty?
|
|
66
|
-
remaining = deadline -
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
@@ -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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
detail[:
|
|
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
|
|