evilution 0.26.0 → 0.28.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 +23 -0
- data/.rubocop_todo.yml +6 -0
- data/CHANGELOG.md +54 -0
- data/README.md +76 -3
- data/lib/evilution/baseline.rb +5 -4
- data/lib/evilution/cache.rb +2 -0
- data/lib/evilution/child_output.rb +24 -0
- data/lib/evilution/cli/commands/run.rb +9 -0
- data/lib/evilution/cli/commands/version.rb +2 -0
- data/lib/evilution/cli/parser/options_builder.rb +23 -2
- 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 +17 -4
- data/lib/evilution/config/builders/spec_resolver.rb +15 -0
- data/lib/evilution/config/builders/spec_selector.rb +16 -0
- data/lib/evilution/config/builders.rb +4 -0
- data/lib/evilution/config/env_loader.rb +12 -0
- data/lib/evilution/config/file_loader.rb +22 -0
- data/lib/evilution/config/sources.rb +14 -0
- data/lib/evilution/config/validators/base.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
- data/lib/evilution/config/validators/fail_fast.rb +11 -0
- data/lib/evilution/config/validators/hooks.rb +12 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
- data/lib/evilution/config/validators/integration.rb +11 -0
- data/lib/evilution/config/validators/isolation.rb +19 -0
- data/lib/evilution/config/validators/jobs.rb +9 -0
- data/lib/evilution/config/validators/preload.rb +13 -0
- data/lib/evilution/config/validators/profile.rb +11 -0
- data/lib/evilution/config/validators/spec_mappings.rb +56 -0
- data/lib/evilution/config/validators/spec_pattern.rb +12 -0
- data/lib/evilution/config/validators.rb +4 -0
- data/lib/evilution/config.rb +93 -266
- data/lib/evilution/feedback/detector.rb +15 -0
- data/lib/evilution/feedback/messages.rb +42 -0
- data/lib/evilution/feedback.rb +5 -0
- data/lib/evilution/integration/crash_detector.rb +2 -2
- data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
- data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
- data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
- data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
- data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
- data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
- data/lib/evilution/integration/rspec/result_builder.rb +40 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
- data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard.rb +40 -0
- data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
- data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
- data/lib/evilution/integration/rspec.rb +61 -232
- data/lib/evilution/isolation/fork.rb +23 -13
- data/lib/evilution/isolation/in_process.rb +10 -6
- data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
- data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
- data/lib/evilution/mcp/info_tool/actions.rb +16 -0
- data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
- data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
- data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
- data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
- data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
- data/lib/evilution/mcp/info_tool.rb +43 -263
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
- data/lib/evilution/mcp/mutate_tool.rb +5 -2
- data/lib/evilution/mcp/session_tool.rb +0 -2
- data/lib/evilution/mutation.rb +47 -27
- data/lib/evilution/mutator/base.rb +8 -8
- data/lib/evilution/mutator/operator/block_removal.rb +1 -1
- data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
- data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
- data/lib/evilution/mutator/registry.rb +20 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -0
- data/lib/evilution/parallel/work_queue/channel.rb +23 -0
- data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators.rb +6 -0
- data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
- data/lib/evilution/parallel/work_queue/worker.rb +114 -0
- data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
- data/lib/evilution/parallel/work_queue.rb +42 -327
- data/lib/evilution/process_cleanup.rb +19 -0
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
- data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
- data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
- data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
- data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
- data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
- data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
- data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
- data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
- data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
- data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
- data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
- data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
- data/lib/evilution/reporter/cli/pct.rb +9 -0
- data/lib/evilution/reporter/cli/section.rb +13 -0
- data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
- data/lib/evilution/reporter/cli/trailer.rb +22 -0
- data/lib/evilution/reporter/cli.rb +79 -162
- 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/suggestion/registry.rb +1 -5
- data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
- 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 +1 -2
- data/lib/evilution/runner/diagnostics.rb +1 -2
- data/lib/evilution/runner/isolation_resolver.rb +10 -4
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +53 -292
- data/lib/evilution/runner/mutation_planner.rb +1 -2
- data/lib/evilution/runner/report_publisher.rb +1 -2
- data/lib/evilution/runner/subject_pipeline.rb +1 -2
- data/lib/evilution/runner.rb +53 -30
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- data/script/memory_check +3 -1
- metadata +125 -3
- data/lib/evilution/reporter/html/namespace.rb +0 -11
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../result"
|
|
4
|
+
|
|
5
|
+
class Evilution::Result::ErrorInfo
|
|
6
|
+
attr_reader :message, :klass, :backtrace
|
|
7
|
+
|
|
8
|
+
def self.from_fields(message: nil, klass: nil, backtrace: nil)
|
|
9
|
+
return nil if message.nil? && klass.nil? && backtrace.nil?
|
|
10
|
+
|
|
11
|
+
new(message: message, klass: klass, backtrace: backtrace)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(message: nil, klass: nil, backtrace: nil)
|
|
15
|
+
@message = message
|
|
16
|
+
@klass = klass
|
|
17
|
+
@backtrace = backtrace.nil? ? nil : backtrace.dup.freeze
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../result"
|
|
4
|
+
|
|
5
|
+
class Evilution::Result::MemoryStats
|
|
6
|
+
attr_reader :child_rss_kb, :memory_delta_kb, :parent_rss_kb
|
|
7
|
+
|
|
8
|
+
def self.from_fields(child_rss_kb: nil, memory_delta_kb: nil, parent_rss_kb: nil)
|
|
9
|
+
return nil if child_rss_kb.nil? && memory_delta_kb.nil? && parent_rss_kb.nil?
|
|
10
|
+
|
|
11
|
+
new(child_rss_kb: child_rss_kb, memory_delta_kb: memory_delta_kb, parent_rss_kb: parent_rss_kb)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(child_rss_kb: nil, memory_delta_kb: nil, parent_rss_kb: nil)
|
|
15
|
+
@child_rss_kb = child_rss_kb
|
|
16
|
+
@memory_delta_kb = memory_delta_kb
|
|
17
|
+
@parent_rss_kb = parent_rss_kb
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../result"
|
|
4
|
+
require_relative "error_info"
|
|
5
|
+
require_relative "memory_stats"
|
|
4
6
|
|
|
5
7
|
class Evilution::Result::MutationResult
|
|
6
8
|
STATUSES = %i[killed survived timeout error neutral equivalent unresolved unparseable].freeze
|
|
7
9
|
|
|
8
|
-
attr_reader :mutation, :status, :duration, :killing_test, :test_command,
|
|
9
|
-
:child_rss_kb, :memory_delta_kb, :parent_rss_kb,
|
|
10
|
-
:error_message, :error_class, :error_backtrace
|
|
10
|
+
attr_reader :mutation, :status, :duration, :killing_test, :test_command, :memory, :error
|
|
11
11
|
|
|
12
|
-
# rubocop:disable Metrics/ParameterLists
|
|
13
12
|
def initialize(mutation:, status:, duration: 0.0, killing_test: nil,
|
|
14
|
-
test_command: nil,
|
|
15
|
-
parent_rss_kb: nil, error_message: nil, error_class: nil,
|
|
16
|
-
error_backtrace: nil)
|
|
17
|
-
# rubocop:enable Metrics/ParameterLists
|
|
13
|
+
test_command: nil, memory: nil, error: nil)
|
|
18
14
|
raise ArgumentError, "invalid status: #{status}" unless STATUSES.include?(status)
|
|
19
15
|
|
|
20
16
|
@mutation = mutation
|
|
@@ -22,15 +18,35 @@ class Evilution::Result::MutationResult
|
|
|
22
18
|
@duration = duration
|
|
23
19
|
@killing_test = killing_test
|
|
24
20
|
@test_command = test_command
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
@parent_rss_kb = parent_rss_kb
|
|
28
|
-
@error_message = error_message
|
|
29
|
-
@error_class = error_class
|
|
30
|
-
@error_backtrace = error_backtrace.nil? ? nil : error_backtrace.dup.freeze
|
|
21
|
+
@memory = memory
|
|
22
|
+
@error = error
|
|
31
23
|
freeze
|
|
32
24
|
end
|
|
33
25
|
|
|
26
|
+
def child_rss_kb
|
|
27
|
+
@memory.nil? ? nil : @memory.child_rss_kb
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def memory_delta_kb
|
|
31
|
+
@memory.nil? ? nil : @memory.memory_delta_kb
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parent_rss_kb
|
|
35
|
+
@memory.nil? ? nil : @memory.parent_rss_kb
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def error_message
|
|
39
|
+
@error.nil? ? nil : @error.message
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def error_class
|
|
43
|
+
@error.nil? ? nil : @error.klass
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def error_backtrace
|
|
47
|
+
@error.nil? ? nil : @error.backtrace
|
|
48
|
+
end
|
|
49
|
+
|
|
34
50
|
def killed?
|
|
35
51
|
status == :killed
|
|
36
52
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../runner"
|
|
3
4
|
require_relative "../baseline"
|
|
4
5
|
require_relative "../spec_resolver"
|
|
5
6
|
require_relative "../integration/rspec"
|
|
@@ -8,8 +9,6 @@ require_relative "../example_filter"
|
|
|
8
9
|
require_relative "../spec_ast_cache"
|
|
9
10
|
require_relative "../source_ast_cache"
|
|
10
11
|
|
|
11
|
-
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
12
|
-
|
|
13
12
|
unless defined?(Evilution::Runner::INTEGRATIONS)
|
|
14
13
|
Evilution::Runner::INTEGRATIONS = {
|
|
15
14
|
rspec: Evilution::Integration::RSpec,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../runner"
|
|
3
4
|
require_relative "../memory"
|
|
4
5
|
require_relative "../parallel/pool"
|
|
5
6
|
|
|
6
|
-
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
7
|
-
|
|
8
7
|
class Evilution::Runner::Diagnostics
|
|
9
8
|
def initialize(config, stderr: $stderr)
|
|
10
9
|
@config = config
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../runner"
|
|
3
4
|
require_relative "../isolation/fork"
|
|
4
5
|
require_relative "../isolation/in_process"
|
|
5
6
|
require_relative "../rails_detector"
|
|
6
7
|
|
|
7
|
-
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
8
|
-
|
|
9
8
|
class Evilution::Runner::IsolationResolver
|
|
10
9
|
PRELOAD_CANDIDATES = [
|
|
11
10
|
File.join("spec", "rails_helper.rb"),
|
|
11
|
+
File.join("spec", "spec_helper.rb"),
|
|
12
12
|
File.join("test", "test_helper.rb")
|
|
13
13
|
].freeze
|
|
14
14
|
|
|
@@ -30,13 +30,15 @@ class Evilution::Runner::IsolationResolver
|
|
|
30
30
|
|
|
31
31
|
def perform_preload
|
|
32
32
|
return if config.preload == false
|
|
33
|
+
return unless should_preload?
|
|
33
34
|
|
|
34
35
|
path = resolve_preload_path
|
|
35
36
|
return unless path
|
|
36
|
-
return unless should_preload?
|
|
37
37
|
|
|
38
38
|
prepare_load_path_for_preload
|
|
39
39
|
require File.expand_path(path)
|
|
40
|
+
rescue Evilution::ConfigError
|
|
41
|
+
raise
|
|
40
42
|
rescue ScriptError, StandardError => e
|
|
41
43
|
raise Evilution::ConfigError.new(
|
|
42
44
|
"failed to preload #{path.inspect}: #{e.class}: #{e.message}",
|
|
@@ -123,7 +125,11 @@ class Evilution::Runner::IsolationResolver
|
|
|
123
125
|
abs = File.join(root, rel)
|
|
124
126
|
return abs if File.file?(abs)
|
|
125
127
|
end
|
|
126
|
-
|
|
128
|
+
|
|
129
|
+
raise Evilution::ConfigError,
|
|
130
|
+
"Preload file not found. Tried: [#{PRELOAD_CANDIDATES.join(", ")}]. " \
|
|
131
|
+
"Pass --preload <file> or set preload: in .evilution.yml. " \
|
|
132
|
+
"Use --no-preload (or preload: false) to disable preloading entirely."
|
|
127
133
|
end
|
|
128
134
|
|
|
129
135
|
# When the user explicitly requests InProcess on a Rails project, warn once
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../mutation_executor"
|
|
4
|
+
require_relative "../../result/mutation_result"
|
|
5
|
+
|
|
6
|
+
class Evilution::Runner::MutationExecutor::MutationRunner
|
|
7
|
+
def initialize(config:, cache:, isolator:)
|
|
8
|
+
@config = config
|
|
9
|
+
@cache = cache
|
|
10
|
+
@isolator = isolator
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(mutation, integration:)
|
|
14
|
+
return unparseable_result(mutation) if mutation.unparseable?
|
|
15
|
+
|
|
16
|
+
cached = @cache.fetch(mutation)
|
|
17
|
+
return cached if cached
|
|
18
|
+
|
|
19
|
+
test_command = ->(m) { integration.call(m) }
|
|
20
|
+
result = @isolator.call(mutation: mutation, test_command: test_command, timeout: @config.timeout)
|
|
21
|
+
@cache.store(mutation, result)
|
|
22
|
+
result
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def unparseable_result(mutation)
|
|
28
|
+
Evilution::Result::MutationResult.new(mutation: mutation, status: :unparseable)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../mutation_executor"
|
|
4
|
+
|
|
5
|
+
class Evilution::Runner::MutationExecutor::NeutralizationPipeline
|
|
6
|
+
def initialize(neutralizers)
|
|
7
|
+
@neutralizers = neutralizers
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(result, **ctx)
|
|
11
|
+
@neutralizers.reduce(result) do |acc, nz|
|
|
12
|
+
ctx.empty? ? nz.call(acc) : nz.call(acc, **ctx)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../neutralizer"
|
|
4
|
+
require_relative "../../../result/mutation_result"
|
|
5
|
+
|
|
6
|
+
class Evilution::Runner::MutationExecutor::Neutralizer::BaselineFailed
|
|
7
|
+
def initialize(config:, spec_resolver:, fallback_dir:)
|
|
8
|
+
@config = config
|
|
9
|
+
@spec_resolver = spec_resolver
|
|
10
|
+
@fallback_dir = fallback_dir
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(result, baseline_result:)
|
|
14
|
+
return result unless result.survived? && baseline_result && baseline_result.failed?
|
|
15
|
+
|
|
16
|
+
if @config.spec_files.any?
|
|
17
|
+
should_neutralize = true
|
|
18
|
+
else
|
|
19
|
+
spec_file = @spec_resolver.call(result.mutation.file_path) || @fallback_dir
|
|
20
|
+
should_neutralize = baseline_result.failed_spec_files.include?(spec_file)
|
|
21
|
+
end
|
|
22
|
+
return result unless should_neutralize
|
|
23
|
+
|
|
24
|
+
neutralize(result)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def neutralize(result)
|
|
30
|
+
Evilution::Result::MutationResult.new(
|
|
31
|
+
mutation: result.mutation,
|
|
32
|
+
status: :neutral,
|
|
33
|
+
duration: result.duration,
|
|
34
|
+
test_command: result.test_command,
|
|
35
|
+
memory: result.memory,
|
|
36
|
+
error: result.error
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../neutralizer"
|
|
4
|
+
require_relative "../../../result/mutation_result"
|
|
5
|
+
|
|
6
|
+
# Reclassify results as :neutral when the failure was caused by test
|
|
7
|
+
# infrastructure rather than by the mutation. Two independent paths:
|
|
8
|
+
#
|
|
9
|
+
# 1) :error from a missing require / spec_helper / rails_helper / spec/support
|
|
10
|
+
# initialization — detected by error_class ∈ INFRA_ERROR_CLASSES and
|
|
11
|
+
# first backtrace frame matching INFRA_BACKTRACE_PATHS. Origin-only match
|
|
12
|
+
# (not `any?`): Ruby backtraces typically carry spec_helper frames below
|
|
13
|
+
# mutation-caused errors, so matching any frame would misclassify real
|
|
14
|
+
# mutation NameError/LoadError as :neutral.
|
|
15
|
+
#
|
|
16
|
+
# 2) :killed from a CrashDetector test_crashed whose sole crash class is in
|
|
17
|
+
# INFRA_CRASH_CLASSES (ActiveRecord::StatementTimeout, Timeout::Error,
|
|
18
|
+
# etc.). These surface under parallel workers sharing a DB file or on a
|
|
19
|
+
# slow CI; fork.rb initially reports them as :killed, and without this
|
|
20
|
+
# demotion the kill count inflates with infra noise. No backtrace check:
|
|
21
|
+
# the single-class signal from CrashDetector already rules out mixed
|
|
22
|
+
# mutation-caused failures. See EV-toid / GH #814.
|
|
23
|
+
class Evilution::Runner::MutationExecutor::Neutralizer::InfraError
|
|
24
|
+
INFRA_ERROR_CLASSES = %w[LoadError NameError].freeze
|
|
25
|
+
INFRA_BACKTRACE_PATHS = %r{(?:^|/)(?:spec_helper\.rb|rails_helper\.rb|spec/support/)}
|
|
26
|
+
INFRA_CRASH_CLASSES = %w[
|
|
27
|
+
Timeout::Error
|
|
28
|
+
ActiveRecord::StatementTimeout
|
|
29
|
+
ActiveRecord::Deadlocked
|
|
30
|
+
ActiveRecord::ConnectionTimeoutError
|
|
31
|
+
ActiveRecord::LockWaitTimeout
|
|
32
|
+
SQLite3::BusyException
|
|
33
|
+
].freeze
|
|
34
|
+
private_constant :INFRA_ERROR_CLASSES, :INFRA_BACKTRACE_PATHS, :INFRA_CRASH_CLASSES
|
|
35
|
+
|
|
36
|
+
def call(result, **_ctx)
|
|
37
|
+
return neutralize(result) if infra_crash?(result)
|
|
38
|
+
return result unless result.error?
|
|
39
|
+
return result unless INFRA_ERROR_CLASSES.include?(result.error_class)
|
|
40
|
+
return result unless infra_origin?(result.error_backtrace)
|
|
41
|
+
|
|
42
|
+
neutralize(result)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def infra_crash?(result)
|
|
48
|
+
result.killed? && INFRA_CRASH_CLASSES.include?(result.error_class)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def infra_origin?(backtrace)
|
|
52
|
+
frames = Array(backtrace)
|
|
53
|
+
return false if frames.empty?
|
|
54
|
+
|
|
55
|
+
frames.first =~ INFRA_BACKTRACE_PATHS ? true : false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def neutralize(result)
|
|
59
|
+
Evilution::Result::MutationResult.new(
|
|
60
|
+
mutation: result.mutation,
|
|
61
|
+
status: :neutral,
|
|
62
|
+
duration: result.duration,
|
|
63
|
+
test_command: result.test_command,
|
|
64
|
+
memory: result.memory,
|
|
65
|
+
error: result.error
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../mutation_executor"
|
|
4
|
+
|
|
5
|
+
# Namespace for MutationExecutor's neutralization rules (InfraError,
|
|
6
|
+
# BaselineFailed). Concrete neutralizer classes live in neutralizer/*.rb and
|
|
7
|
+
# are autoloaded on first reference.
|
|
8
|
+
module Evilution::Runner::MutationExecutor::Neutralizer
|
|
9
|
+
autoload :InfraError, File.expand_path("neutralizer/infra_error", __dir__)
|
|
10
|
+
autoload :BaselineFailed, File.expand_path("neutralizer/baseline_failed", __dir__)
|
|
11
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../mutation_executor"
|
|
4
|
+
require_relative "../../result/mutation_result"
|
|
5
|
+
|
|
6
|
+
class Evilution::Runner::MutationExecutor::ResultCache
|
|
7
|
+
CACHEABLE_STATUSES = %i[killed timeout].freeze
|
|
8
|
+
private_constant :CACHEABLE_STATUSES
|
|
9
|
+
|
|
10
|
+
def initialize(backend)
|
|
11
|
+
@backend = backend
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def fetch(mutation)
|
|
15
|
+
return nil unless @backend
|
|
16
|
+
|
|
17
|
+
data = @backend.fetch(mutation)
|
|
18
|
+
return nil unless data
|
|
19
|
+
return nil unless CACHEABLE_STATUSES.include?(data[:status])
|
|
20
|
+
|
|
21
|
+
Evilution::Result::MutationResult.new(
|
|
22
|
+
mutation: mutation,
|
|
23
|
+
status: data[:status],
|
|
24
|
+
duration: data[:duration],
|
|
25
|
+
killing_test: data[:killing_test],
|
|
26
|
+
test_command: data[:test_command]
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def store(mutation, result)
|
|
31
|
+
return unless @backend
|
|
32
|
+
return unless result.killed? || result.timeout?
|
|
33
|
+
|
|
34
|
+
@backend.store(mutation,
|
|
35
|
+
status: result.status,
|
|
36
|
+
duration: result.duration,
|
|
37
|
+
killing_test: result.killing_test,
|
|
38
|
+
test_command: result.test_command)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def partition(batch, packer:)
|
|
42
|
+
uncached_indices = []
|
|
43
|
+
cached_results = {}
|
|
44
|
+
|
|
45
|
+
batch.each_with_index do |mutation, i|
|
|
46
|
+
if mutation.unparseable?
|
|
47
|
+
cached_results[i] = packer.compact(unparseable_result(mutation))
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
cached = fetch(mutation)
|
|
52
|
+
if cached
|
|
53
|
+
cached_results[i] = packer.compact(cached)
|
|
54
|
+
else
|
|
55
|
+
uncached_indices << i
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
[uncached_indices, cached_results]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def unparseable_result(mutation)
|
|
65
|
+
Evilution::Result::MutationResult.new(mutation: mutation, status: :unparseable)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../mutation_executor"
|
|
4
|
+
require_relative "../../reporter/progress_bar"
|
|
5
|
+
|
|
6
|
+
class Evilution::Runner::MutationExecutor::ResultNotifier
|
|
7
|
+
def initialize(config, diagnostics:, on_result:)
|
|
8
|
+
@config = config
|
|
9
|
+
@diagnostics = diagnostics
|
|
10
|
+
@on_result = on_result
|
|
11
|
+
@survived_count = 0
|
|
12
|
+
@progress_bar = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :survived_count
|
|
16
|
+
|
|
17
|
+
def start(total)
|
|
18
|
+
@survived_count = 0
|
|
19
|
+
@progress_bar = build_progress_bar(total)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def notify(result, index)
|
|
23
|
+
@on_result.call(result) if @on_result
|
|
24
|
+
@progress_bar.tick(status: result.status) if @progress_bar
|
|
25
|
+
@diagnostics.log_progress(index, result.status)
|
|
26
|
+
@diagnostics.log_mutation_diagnostics(result)
|
|
27
|
+
@survived_count += 1 if result.survived?
|
|
28
|
+
truncate? ? :truncate : :continue
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def finish
|
|
32
|
+
@progress_bar.finish if @progress_bar
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def truncate?
|
|
38
|
+
@config.fail_fast? && @survived_count >= @config.fail_fast
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_progress_bar(total)
|
|
42
|
+
return nil if !@config.progress? || @config.quiet || @config.verbose || !@config.text? || !$stderr.tty?
|
|
43
|
+
|
|
44
|
+
Evilution::Reporter::ProgressBar.new(total: total, output: $stderr)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../mutation_executor"
|
|
4
|
+
require_relative "../../result/mutation_result"
|
|
5
|
+
|
|
6
|
+
class Evilution::Runner::MutationExecutor::ResultPacker
|
|
7
|
+
def compact(result)
|
|
8
|
+
{
|
|
9
|
+
status: result.status,
|
|
10
|
+
duration: result.duration,
|
|
11
|
+
killing_test: result.killing_test,
|
|
12
|
+
test_command: result.test_command,
|
|
13
|
+
child_rss_kb: result.child_rss_kb,
|
|
14
|
+
memory_delta_kb: result.memory_delta_kb,
|
|
15
|
+
parent_rss_kb: result.parent_rss_kb,
|
|
16
|
+
error_message: result.error_message,
|
|
17
|
+
error_class: result.error_class,
|
|
18
|
+
error_backtrace: result.error_backtrace
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def rebuild(mutation, data)
|
|
23
|
+
Evilution::Result::MutationResult.new(
|
|
24
|
+
mutation: mutation,
|
|
25
|
+
status: data[:status],
|
|
26
|
+
duration: data[:duration],
|
|
27
|
+
killing_test: data[:killing_test],
|
|
28
|
+
test_command: data[:test_command],
|
|
29
|
+
memory: Evilution::Result::MemoryStats.from_fields(
|
|
30
|
+
child_rss_kb: data[:child_rss_kb],
|
|
31
|
+
memory_delta_kb: data[:memory_delta_kb],
|
|
32
|
+
parent_rss_kb: data[:parent_rss_kb]
|
|
33
|
+
),
|
|
34
|
+
error: Evilution::Result::ErrorInfo.from_fields(
|
|
35
|
+
message: data[:error_message],
|
|
36
|
+
klass: data[:error_class],
|
|
37
|
+
backtrace: data[:error_backtrace]
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../strategy"
|
|
4
|
+
|
|
5
|
+
class Evilution::Runner::MutationExecutor::Strategy::Parallel
|
|
6
|
+
def initialize(cache:, isolator:, packer:, pipeline:, notifier:, pool_factory:, config:, diagnostics: nil)
|
|
7
|
+
@cache = cache
|
|
8
|
+
@isolator = isolator
|
|
9
|
+
@packer = packer
|
|
10
|
+
@pipeline = pipeline
|
|
11
|
+
@notifier = notifier
|
|
12
|
+
@pool_factory = pool_factory
|
|
13
|
+
@diagnostics = diagnostics
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(mutations, baseline_result:, integration:)
|
|
18
|
+
@notifier.start(mutations.length)
|
|
19
|
+
pool = @pool_factory.call
|
|
20
|
+
state = { results: [], truncated: false, completed: 0 }
|
|
21
|
+
all_worker_stats = []
|
|
22
|
+
|
|
23
|
+
mutations.each_slice(@config.jobs) do |batch|
|
|
24
|
+
break if state[:truncated]
|
|
25
|
+
|
|
26
|
+
batch_results = run_batch(batch, pool, integration)
|
|
27
|
+
all_worker_stats.concat(pool.worker_stats)
|
|
28
|
+
process_batch(batch_results, baseline_result, state)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@diagnostics.log_worker_stats(@diagnostics.aggregate_worker_stats(all_worker_stats)) if @diagnostics
|
|
32
|
+
@notifier.finish
|
|
33
|
+
[state[:results], state[:truncated]]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def run_batch(batch, pool, integration)
|
|
39
|
+
uncached_indices, cached_results = @cache.partition(batch, packer: @packer)
|
|
40
|
+
worker_results = run_uncached(batch, uncached_indices, pool, integration)
|
|
41
|
+
compact_results = merge(batch, uncached_indices, cached_results, worker_results)
|
|
42
|
+
batch_results = batch.zip(compact_results).map { |m, h| @packer.rebuild(m, h) }
|
|
43
|
+
uncached_indices.each { |i| @cache.store(batch_results[i].mutation, batch_results[i]) }
|
|
44
|
+
batch.each(&:strip_sources!)
|
|
45
|
+
batch_results
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def run_uncached(batch, uncached_indices, pool, integration)
|
|
49
|
+
return [] if uncached_indices.empty?
|
|
50
|
+
|
|
51
|
+
uncached = uncached_indices.map { |i| batch[i] }
|
|
52
|
+
pool.map(uncached) do |mutation|
|
|
53
|
+
test_command = ->(m) { integration.call(m) }
|
|
54
|
+
result = @isolator.call(mutation: mutation, test_command: test_command, timeout: @config.timeout)
|
|
55
|
+
@packer.compact(result)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def merge(batch, uncached_indices, cached_results, worker_results)
|
|
60
|
+
result_map = cached_results.dup
|
|
61
|
+
uncached_indices.each_with_index { |batch_idx, worker_idx| result_map[batch_idx] = worker_results[worker_idx] }
|
|
62
|
+
batch.each_index.map { |i| result_map[i] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def process_batch(batch_results, baseline_result, state)
|
|
66
|
+
batch_results.each do |result|
|
|
67
|
+
result = @pipeline.call(result, baseline_result: baseline_result)
|
|
68
|
+
state[:results] << result
|
|
69
|
+
state[:completed] += 1
|
|
70
|
+
if @notifier.notify(result, state[:completed]) == :truncate
|
|
71
|
+
state[:truncated] = true
|
|
72
|
+
break
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@diagnostics.log_memory("after batch", "#{state[:completed]} complete") if @diagnostics
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../strategy"
|
|
4
|
+
|
|
5
|
+
class Evilution::Runner::MutationExecutor::Strategy::Sequential
|
|
6
|
+
def initialize(runner:, pipeline:, notifier:)
|
|
7
|
+
@runner = runner
|
|
8
|
+
@pipeline = pipeline
|
|
9
|
+
@notifier = notifier
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(mutations, baseline_result:, integration:)
|
|
13
|
+
@notifier.start(mutations.length)
|
|
14
|
+
results = []
|
|
15
|
+
truncated = false
|
|
16
|
+
|
|
17
|
+
mutations.each_with_index do |mutation, index|
|
|
18
|
+
result = @runner.call(mutation, integration: integration)
|
|
19
|
+
mutation.strip_sources!
|
|
20
|
+
result = @pipeline.call(result, baseline_result: baseline_result)
|
|
21
|
+
results << result
|
|
22
|
+
|
|
23
|
+
if @notifier.notify(result, index + 1) == :truncate
|
|
24
|
+
truncated = true
|
|
25
|
+
break
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@notifier.finish
|
|
30
|
+
[results, truncated]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../mutation_executor"
|
|
4
|
+
|
|
5
|
+
# Namespace for MutationExecutor's execution strategies (Sequential, Parallel).
|
|
6
|
+
# Concrete strategy classes live in strategy/{sequential,parallel}.rb and are
|
|
7
|
+
# autoloaded on first reference.
|
|
8
|
+
module Evilution::Runner::MutationExecutor::Strategy
|
|
9
|
+
autoload :Sequential, File.expand_path("strategy/sequential", __dir__)
|
|
10
|
+
autoload :Parallel, File.expand_path("strategy/parallel", __dir__)
|
|
11
|
+
end
|