evilution 0.26.0 → 0.27.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 +10 -0
- data/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +22 -0
- data/README.md +57 -3
- 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 +16 -2
- 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/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 +78 -268
- 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/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 +35 -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 +7 -2
- 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 -261
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
- data/lib/evilution/mcp/mutate_tool.rb +5 -2
- data/lib/evilution/mutator/operator/block_removal.rb +1 -1
- data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
- data/lib/evilution/parallel/work_queue/channel/frame.rb +21 -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/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/runner/isolation_resolver.rb +9 -2
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +32 -0
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +16 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +46 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +75 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +69 -0
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +48 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +39 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +80 -0
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +34 -0
- data/lib/evilution/runner/mutation_executor.rb +58 -289
- data/lib/evilution/runner.rb +21 -0
- data/lib/evilution/version.rb +1 -1
- metadata +113 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
4
|
+
class Evilution::Runner::MutationExecutor; end unless defined?(Evilution::Runner::MutationExecutor) # rubocop:disable Lint/EmptyClass
|
|
5
|
+
module Evilution::Runner::MutationExecutor::Strategy; end unless defined?(Evilution::Runner::MutationExecutor::Strategy)
|
|
6
|
+
|
|
7
|
+
class Evilution::Runner::MutationExecutor::Strategy::Parallel
|
|
8
|
+
def initialize(cache:, isolator:, packer:, pipeline:, notifier:, pool_factory:, config:, diagnostics: nil)
|
|
9
|
+
@cache = cache
|
|
10
|
+
@isolator = isolator
|
|
11
|
+
@packer = packer
|
|
12
|
+
@pipeline = pipeline
|
|
13
|
+
@notifier = notifier
|
|
14
|
+
@pool_factory = pool_factory
|
|
15
|
+
@diagnostics = diagnostics
|
|
16
|
+
@config = config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(mutations, baseline_result:, integration:)
|
|
20
|
+
@notifier.start(mutations.length)
|
|
21
|
+
pool = @pool_factory.call
|
|
22
|
+
state = { results: [], truncated: false, completed: 0 }
|
|
23
|
+
all_worker_stats = []
|
|
24
|
+
|
|
25
|
+
mutations.each_slice(@config.jobs) do |batch|
|
|
26
|
+
break if state[:truncated]
|
|
27
|
+
|
|
28
|
+
batch_results = run_batch(batch, pool, integration)
|
|
29
|
+
all_worker_stats.concat(pool.worker_stats)
|
|
30
|
+
process_batch(batch_results, baseline_result, state)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@diagnostics.log_worker_stats(@diagnostics.aggregate_worker_stats(all_worker_stats)) if @diagnostics
|
|
34
|
+
@notifier.finish
|
|
35
|
+
[state[:results], state[:truncated]]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def run_batch(batch, pool, integration)
|
|
41
|
+
uncached_indices, cached_results = @cache.partition(batch, packer: @packer)
|
|
42
|
+
worker_results = run_uncached(batch, uncached_indices, pool, integration)
|
|
43
|
+
compact_results = merge(batch, uncached_indices, cached_results, worker_results)
|
|
44
|
+
batch_results = batch.zip(compact_results).map { |m, h| @packer.rebuild(m, h) }
|
|
45
|
+
uncached_indices.each { |i| @cache.store(batch_results[i].mutation, batch_results[i]) }
|
|
46
|
+
batch.each(&:strip_sources!)
|
|
47
|
+
batch_results
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def run_uncached(batch, uncached_indices, pool, integration)
|
|
51
|
+
return [] if uncached_indices.empty?
|
|
52
|
+
|
|
53
|
+
uncached = uncached_indices.map { |i| batch[i] }
|
|
54
|
+
pool.map(uncached) do |mutation|
|
|
55
|
+
test_command = ->(m) { integration.call(m) }
|
|
56
|
+
result = @isolator.call(mutation: mutation, test_command: test_command, timeout: @config.timeout)
|
|
57
|
+
@packer.compact(result)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def merge(batch, uncached_indices, cached_results, worker_results)
|
|
62
|
+
result_map = cached_results.dup
|
|
63
|
+
uncached_indices.each_with_index { |batch_idx, worker_idx| result_map[batch_idx] = worker_results[worker_idx] }
|
|
64
|
+
batch.each_index.map { |i| result_map[i] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def process_batch(batch_results, baseline_result, state)
|
|
68
|
+
batch_results.each do |result|
|
|
69
|
+
result = @pipeline.call(result, baseline_result: baseline_result)
|
|
70
|
+
state[:results] << result
|
|
71
|
+
state[:completed] += 1
|
|
72
|
+
if @notifier.notify(result, state[:completed]) == :truncate
|
|
73
|
+
state[:truncated] = true
|
|
74
|
+
break
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@diagnostics.log_memory("after batch", "#{state[:completed]} complete") if @diagnostics
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
4
|
+
class Evilution::Runner::MutationExecutor; end unless defined?(Evilution::Runner::MutationExecutor) # rubocop:disable Lint/EmptyClass
|
|
5
|
+
module Evilution::Runner::MutationExecutor::Strategy; end unless defined?(Evilution::Runner::MutationExecutor::Strategy)
|
|
6
|
+
|
|
7
|
+
class Evilution::Runner::MutationExecutor::Strategy::Sequential
|
|
8
|
+
def initialize(runner:, pipeline:, notifier:)
|
|
9
|
+
@runner = runner
|
|
10
|
+
@pipeline = pipeline
|
|
11
|
+
@notifier = notifier
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(mutations, baseline_result:, integration:)
|
|
15
|
+
@notifier.start(mutations.length)
|
|
16
|
+
results = []
|
|
17
|
+
truncated = false
|
|
18
|
+
|
|
19
|
+
mutations.each_with_index do |mutation, index|
|
|
20
|
+
result = @runner.call(mutation, integration: integration)
|
|
21
|
+
mutation.strip_sources!
|
|
22
|
+
result = @pipeline.call(result, baseline_result: baseline_result)
|
|
23
|
+
results << result
|
|
24
|
+
|
|
25
|
+
if @notifier.notify(result, index + 1) == :truncate
|
|
26
|
+
truncated = true
|
|
27
|
+
break
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@notifier.finish
|
|
32
|
+
[results, truncated]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -1,325 +1,94 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../parallel/pool"
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "
|
|
4
|
+
require_relative "mutation_executor/result_cache"
|
|
5
|
+
require_relative "mutation_executor/result_packer"
|
|
6
|
+
require_relative "mutation_executor/result_notifier"
|
|
7
|
+
require_relative "mutation_executor/mutation_runner"
|
|
8
|
+
require_relative "mutation_executor/neutralization_pipeline"
|
|
9
|
+
require_relative "mutation_executor/neutralizer/infra_error"
|
|
10
|
+
require_relative "mutation_executor/neutralizer/baseline_failed"
|
|
11
|
+
require_relative "mutation_executor/strategy/sequential"
|
|
12
|
+
require_relative "mutation_executor/strategy/parallel"
|
|
6
13
|
|
|
7
14
|
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
8
15
|
|
|
9
16
|
class Evilution::Runner::MutationExecutor
|
|
17
|
+
InfraError = Neutralizer::InfraError
|
|
18
|
+
BaselineFailed = Neutralizer::BaselineFailed
|
|
19
|
+
Sequential = Strategy::Sequential
|
|
20
|
+
Parallel = Strategy::Parallel
|
|
21
|
+
|
|
10
22
|
def initialize(config, isolator:, baseline_runner:, cache:, hooks:, diagnostics:, on_result: nil)
|
|
11
23
|
@config = config
|
|
12
24
|
@isolator = isolator
|
|
13
25
|
@baseline_runner = baseline_runner
|
|
14
|
-
@cache = cache
|
|
26
|
+
@cache = ResultCache.new(cache)
|
|
27
|
+
@packer = ResultPacker.new
|
|
15
28
|
@hooks = hooks
|
|
16
29
|
@diagnostics = diagnostics
|
|
17
30
|
@on_result = on_result
|
|
18
31
|
end
|
|
19
32
|
|
|
20
33
|
def call(mutations, baseline_result = nil)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
result
|
|
34
|
+
integration = @baseline_runner.build_integration
|
|
35
|
+
spec_resolver = baseline_failed?(baseline_result) ? @baseline_runner.neutralization_resolver : nil
|
|
36
|
+
notifier = build_notifier
|
|
37
|
+
pipeline = build_pipeline(spec_resolver)
|
|
38
|
+
strategy = @config.jobs > 1 ? build_parallel(notifier, pipeline) : build_sequential(notifier, pipeline)
|
|
39
|
+
|
|
40
|
+
strategy.call(mutations, baseline_result: baseline_result, integration: integration)
|
|
29
41
|
end
|
|
30
42
|
|
|
31
43
|
private
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def run_sequential(mutations, baseline_result)
|
|
36
|
-
integration = baseline_runner.build_integration
|
|
37
|
-
spec_resolver = baseline_result&.failed? ? baseline_runner.neutralization_resolver : nil
|
|
38
|
-
results = []
|
|
39
|
-
survived_count = 0
|
|
40
|
-
truncated = false
|
|
41
|
-
|
|
42
|
-
mutations.each_with_index do |mutation, index|
|
|
43
|
-
result = execute_one(mutation, integration)
|
|
44
|
-
mutation.strip_sources!
|
|
45
|
-
result = neutralize_if_infra_error(result)
|
|
46
|
-
result = neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
|
|
47
|
-
results << result
|
|
48
|
-
survived_count += 1 if result.survived?
|
|
49
|
-
notify_result(result, index + 1)
|
|
50
|
-
|
|
51
|
-
if config.fail_fast? && survived_count >= config.fail_fast
|
|
52
|
-
truncated = true
|
|
53
|
-
break
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
[results, truncated]
|
|
45
|
+
def baseline_failed?(baseline_result)
|
|
46
|
+
baseline_result && baseline_result.failed?
|
|
58
47
|
end
|
|
59
48
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
pool = build_pool
|
|
63
|
-
spec_resolver = baseline_result&.failed? ? baseline_runner.neutralization_resolver : nil
|
|
64
|
-
state = { results: [], survived_count: 0, truncated: false, completed: 0 }
|
|
65
|
-
all_worker_stats = []
|
|
66
|
-
|
|
67
|
-
mutations.each_slice(config.jobs) do |batch|
|
|
68
|
-
break if state[:truncated]
|
|
69
|
-
|
|
70
|
-
batch_results = run_parallel_batch(batch, pool, isolator, integration)
|
|
71
|
-
all_worker_stats.concat(pool.worker_stats)
|
|
72
|
-
process_batch(batch_results, baseline_result, spec_resolver, state)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
diagnostics.log_worker_stats(diagnostics.aggregate_worker_stats(all_worker_stats))
|
|
76
|
-
[state[:results], state[:truncated]]
|
|
49
|
+
def build_notifier
|
|
50
|
+
ResultNotifier.new(@config, diagnostics: @diagnostics, on_result: @on_result)
|
|
77
51
|
end
|
|
78
52
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
53
|
+
def build_pipeline(spec_resolver)
|
|
54
|
+
NeutralizationPipeline.new(
|
|
55
|
+
[
|
|
56
|
+
InfraError.new,
|
|
57
|
+
BaselineFailed.new(
|
|
58
|
+
config: @config,
|
|
59
|
+
spec_resolver: spec_resolver || ->(_f) {},
|
|
60
|
+
fallback_dir: @baseline_runner.neutralization_fallback_dir
|
|
61
|
+
)
|
|
62
|
+
]
|
|
84
63
|
)
|
|
85
64
|
end
|
|
86
65
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
batch_results = rebuild_results(batch, compact_results)
|
|
93
|
-
batch_results.each { |r| store_cached_result(r.mutation, r) }
|
|
94
|
-
batch_results
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def run_uncached_workers(batch, uncached_indices, pool, worker_isolator, integration)
|
|
98
|
-
return [] if uncached_indices.empty?
|
|
99
|
-
|
|
100
|
-
uncached = uncached_indices.map { |i| batch[i] }
|
|
101
|
-
pool.map(uncached) do |mutation|
|
|
102
|
-
test_command = ->(m) { integration.call(m) }
|
|
103
|
-
result = worker_isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
|
|
104
|
-
compact_result(result)
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def process_batch(batch_results, baseline_result, spec_resolver, state)
|
|
109
|
-
batch_results.each do |result|
|
|
110
|
-
result = neutralize_if_infra_error(result)
|
|
111
|
-
result = neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
|
|
112
|
-
state[:results] << result
|
|
113
|
-
state[:survived_count] += 1 if result.survived?
|
|
114
|
-
state[:completed] += 1
|
|
115
|
-
notify_result(result, state[:completed])
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
diagnostics.log_memory("after batch", "#{state[:completed]} complete")
|
|
119
|
-
state[:truncated] = true if should_truncate?(state[:survived_count])
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Reclassify results as :neutral when the failure was caused by test
|
|
123
|
-
# infrastructure rather than by the mutation. Two independent paths:
|
|
124
|
-
#
|
|
125
|
-
# 1) :error from a missing require / spec_helper / rails_helper / spec/support
|
|
126
|
-
# initialization — detected by error_class ∈ INFRA_ERROR_CLASSES and
|
|
127
|
-
# first backtrace frame matching INFRA_BACKTRACE_PATHS. Origin-only match
|
|
128
|
-
# (not `any?`): Ruby backtraces typically carry spec_helper frames below
|
|
129
|
-
# mutation-caused errors, so matching any frame would misclassify real
|
|
130
|
-
# mutation NameError/LoadError as :neutral.
|
|
131
|
-
#
|
|
132
|
-
# 2) :killed from a CrashDetector test_crashed whose sole crash class is in
|
|
133
|
-
# INFRA_CRASH_CLASSES (ActiveRecord::StatementTimeout, Timeout::Error,
|
|
134
|
-
# etc.). These surface under parallel workers sharing a DB file or on a
|
|
135
|
-
# slow CI; fork.rb initially reports them as :killed, and without this
|
|
136
|
-
# demotion the kill count inflates with infra noise. No backtrace check:
|
|
137
|
-
# the single-class signal from CrashDetector already rules out mixed
|
|
138
|
-
# mutation-caused failures. See EV-toid / GH #814.
|
|
139
|
-
INFRA_ERROR_CLASSES = %w[LoadError NameError].freeze
|
|
140
|
-
INFRA_BACKTRACE_PATHS = %r{(?:^|/)(?:spec_helper\.rb|rails_helper\.rb|spec/support/)}
|
|
141
|
-
INFRA_CRASH_CLASSES = %w[
|
|
142
|
-
Timeout::Error
|
|
143
|
-
ActiveRecord::StatementTimeout
|
|
144
|
-
ActiveRecord::Deadlocked
|
|
145
|
-
ActiveRecord::ConnectionTimeoutError
|
|
146
|
-
ActiveRecord::LockWaitTimeout
|
|
147
|
-
SQLite3::BusyException
|
|
148
|
-
].freeze
|
|
149
|
-
private_constant :INFRA_ERROR_CLASSES, :INFRA_BACKTRACE_PATHS, :INFRA_CRASH_CLASSES
|
|
150
|
-
|
|
151
|
-
def neutralize_if_infra_error(result)
|
|
152
|
-
return neutralize(result) if infra_crash?(result)
|
|
153
|
-
return result unless result.error?
|
|
154
|
-
return result unless INFRA_ERROR_CLASSES.include?(result.error_class)
|
|
155
|
-
return result unless infra_origin?(result.error_backtrace)
|
|
156
|
-
|
|
157
|
-
neutralize(result)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def infra_crash?(result)
|
|
161
|
-
result.killed? && INFRA_CRASH_CLASSES.include?(result.error_class)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def infra_origin?(backtrace)
|
|
165
|
-
frames = Array(backtrace)
|
|
166
|
-
return false if frames.empty?
|
|
167
|
-
|
|
168
|
-
frames.first =~ INFRA_BACKTRACE_PATHS ? true : false
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def neutralize(result)
|
|
172
|
-
Evilution::Result::MutationResult.new(
|
|
173
|
-
mutation: result.mutation,
|
|
174
|
-
status: :neutral,
|
|
175
|
-
duration: result.duration,
|
|
176
|
-
test_command: result.test_command,
|
|
177
|
-
child_rss_kb: result.child_rss_kb,
|
|
178
|
-
memory_delta_kb: result.memory_delta_kb,
|
|
179
|
-
parent_rss_kb: result.parent_rss_kb,
|
|
180
|
-
error_message: result.error_message,
|
|
181
|
-
error_class: result.error_class,
|
|
182
|
-
error_backtrace: result.error_backtrace
|
|
66
|
+
def build_sequential(notifier, pipeline)
|
|
67
|
+
Sequential.new(
|
|
68
|
+
runner: MutationRunner.new(config: @config, cache: @cache, isolator: @isolator),
|
|
69
|
+
pipeline: pipeline,
|
|
70
|
+
notifier: notifier
|
|
183
71
|
)
|
|
184
72
|
end
|
|
185
73
|
|
|
186
|
-
def
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
neutralize(result)
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def compact_result(result)
|
|
201
|
-
{
|
|
202
|
-
status: result.status,
|
|
203
|
-
duration: result.duration,
|
|
204
|
-
killing_test: result.killing_test,
|
|
205
|
-
test_command: result.test_command,
|
|
206
|
-
child_rss_kb: result.child_rss_kb,
|
|
207
|
-
memory_delta_kb: result.memory_delta_kb,
|
|
208
|
-
parent_rss_kb: result.parent_rss_kb,
|
|
209
|
-
error_message: result.error_message,
|
|
210
|
-
error_class: result.error_class,
|
|
211
|
-
error_backtrace: result.error_backtrace
|
|
212
|
-
}
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def rebuild_results(batch, compact_results)
|
|
216
|
-
batch.zip(compact_results).map do |mutation, data|
|
|
217
|
-
Evilution::Result::MutationResult.new(
|
|
218
|
-
mutation: mutation,
|
|
219
|
-
status: data[:status],
|
|
220
|
-
duration: data[:duration],
|
|
221
|
-
killing_test: data[:killing_test],
|
|
222
|
-
test_command: data[:test_command],
|
|
223
|
-
child_rss_kb: data[:child_rss_kb],
|
|
224
|
-
memory_delta_kb: data[:memory_delta_kb],
|
|
225
|
-
parent_rss_kb: data[:parent_rss_kb],
|
|
226
|
-
error_message: data[:error_message],
|
|
227
|
-
error_class: data[:error_class],
|
|
228
|
-
error_backtrace: data[:error_backtrace]
|
|
229
|
-
)
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def should_truncate?(survived_count)
|
|
234
|
-
config.fail_fast? && survived_count >= config.fail_fast
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def partition_cached(batch)
|
|
238
|
-
uncached_indices = []
|
|
239
|
-
cached_results = {}
|
|
240
|
-
|
|
241
|
-
batch.each_with_index do |mutation, i|
|
|
242
|
-
if mutation.unparseable?
|
|
243
|
-
cached_results[i] = compact_result(build_unparseable_result(mutation))
|
|
244
|
-
next
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
cached = fetch_cached_result(mutation)
|
|
248
|
-
if cached
|
|
249
|
-
cached_results[i] = compact_result(cached)
|
|
250
|
-
else
|
|
251
|
-
uncached_indices << i
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
[uncached_indices, cached_results]
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def execute_one(mutation, integration)
|
|
259
|
-
return build_unparseable_result(mutation) if mutation.unparseable?
|
|
260
|
-
|
|
261
|
-
execute_or_fetch(mutation) do
|
|
262
|
-
test_command = ->(m) { integration.call(m) }
|
|
263
|
-
isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def build_unparseable_result(mutation)
|
|
268
|
-
Evilution::Result::MutationResult.new(mutation: mutation, status: :unparseable)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def merge_parallel_results(batch, uncached_indices, cached_results, worker_results)
|
|
272
|
-
result_map = cached_results.dup
|
|
273
|
-
uncached_indices.each_with_index { |batch_idx, worker_idx| result_map[batch_idx] = worker_results[worker_idx] }
|
|
274
|
-
batch.each_index.map { |i| result_map[i] }
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def execute_or_fetch(mutation)
|
|
278
|
-
cached = fetch_cached_result(mutation)
|
|
279
|
-
return cached if cached
|
|
280
|
-
|
|
281
|
-
result = yield
|
|
282
|
-
store_cached_result(mutation, result)
|
|
283
|
-
result
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def fetch_cached_result(mutation)
|
|
287
|
-
return nil unless cache
|
|
288
|
-
|
|
289
|
-
data = cache.fetch(mutation)
|
|
290
|
-
return nil unless data
|
|
291
|
-
return nil unless %i[killed timeout].include?(data[:status])
|
|
292
|
-
|
|
293
|
-
Evilution::Result::MutationResult.new(
|
|
294
|
-
mutation: mutation,
|
|
295
|
-
status: data[:status],
|
|
296
|
-
duration: data[:duration],
|
|
297
|
-
killing_test: data[:killing_test],
|
|
298
|
-
test_command: data[:test_command]
|
|
74
|
+
def build_parallel(notifier, pipeline)
|
|
75
|
+
Parallel.new(
|
|
76
|
+
cache: @cache,
|
|
77
|
+
isolator: @isolator,
|
|
78
|
+
packer: @packer,
|
|
79
|
+
pipeline: pipeline,
|
|
80
|
+
notifier: notifier,
|
|
81
|
+
pool_factory: -> { build_pool },
|
|
82
|
+
diagnostics: @diagnostics,
|
|
83
|
+
config: @config
|
|
299
84
|
)
|
|
300
85
|
end
|
|
301
86
|
|
|
302
|
-
def
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
duration: result.duration,
|
|
309
|
-
killing_test: result.killing_test,
|
|
310
|
-
test_command: result.test_command)
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def notify_result(result, index)
|
|
314
|
-
on_result&.call(result)
|
|
315
|
-
@progress_bar&.tick(status: result.status)
|
|
316
|
-
diagnostics.log_progress(index, result.status)
|
|
317
|
-
diagnostics.log_mutation_diagnostics(result)
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
def build_progress_bar(total)
|
|
321
|
-
return nil if !config.progress? || config.quiet || config.verbose || !config.text? || !$stderr.tty?
|
|
322
|
-
|
|
323
|
-
Evilution::Reporter::ProgressBar.new(total: total, output: $stderr)
|
|
87
|
+
def build_pool
|
|
88
|
+
Evilution::Parallel::Pool.new(
|
|
89
|
+
size: @config.jobs,
|
|
90
|
+
hooks: @hooks,
|
|
91
|
+
item_timeout: @config.timeout ? @config.timeout * 2 : nil
|
|
92
|
+
)
|
|
324
93
|
end
|
|
325
94
|
end
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
3
4
|
require_relative "config"
|
|
4
5
|
require_relative "ast/parser"
|
|
5
6
|
require_relative "memory"
|
|
@@ -22,6 +23,7 @@ require_relative "session/store"
|
|
|
22
23
|
require_relative "temp_dir_tracker"
|
|
23
24
|
require_relative "rails_detector"
|
|
24
25
|
require_relative "parallel_db_warning"
|
|
26
|
+
require_relative "child_output"
|
|
25
27
|
require_relative "runner/subject_pipeline"
|
|
26
28
|
require_relative "runner/mutation_planner"
|
|
27
29
|
require_relative "runner/isolation_resolver"
|
|
@@ -44,6 +46,7 @@ class Evilution::Runner
|
|
|
44
46
|
|
|
45
47
|
def call
|
|
46
48
|
install_signal_handlers
|
|
49
|
+
configure_child_output
|
|
47
50
|
emit_parallel_db_warning
|
|
48
51
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
49
52
|
|
|
@@ -156,6 +159,24 @@ class Evilution::Runner
|
|
|
156
159
|
Evilution::ParallelDbWarning.warn_if_sqlite_parallel(config)
|
|
157
160
|
end
|
|
158
161
|
|
|
162
|
+
def configure_child_output
|
|
163
|
+
unless config.quiet_children
|
|
164
|
+
Evilution::ChildOutput.log_dir = nil
|
|
165
|
+
return
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
dir = config.quiet_children_dir
|
|
169
|
+
begin
|
|
170
|
+
FileUtils.rm_rf(dir)
|
|
171
|
+
FileUtils.mkdir_p(dir)
|
|
172
|
+
rescue SystemCallError => e
|
|
173
|
+
raise Evilution::ConfigError,
|
|
174
|
+
"quiet_children_dir #{dir.inspect} is not writable: #{e.class}: #{e.message}. " \
|
|
175
|
+
"Pass --quiet-children-dir <writable path> or drop --quiet-children."
|
|
176
|
+
end
|
|
177
|
+
Evilution::ChildOutput.log_dir = dir
|
|
178
|
+
end
|
|
179
|
+
|
|
159
180
|
def install_signal_handlers
|
|
160
181
|
%w[INT TERM].each { |sig| install_signal_handler(sig) }
|
|
161
182
|
end
|
data/lib/evilution/version.rb
CHANGED