evilution 0.25.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 +15 -0
- data/.claude/prompts/architect.md +14 -1
- data/.claude/skills/create-issue/SKILL.md +55 -0
- data/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +38 -0
- data/README.md +57 -3
- data/lib/evilution/ast/constant_names.rb +34 -0
- 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/compare/invalid_input.rb +12 -0
- data/lib/evilution/compare.rb +1 -10
- 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/base.rb +4 -155
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +49 -0
- data/lib/evilution/integration/loading/constant_pinner.rb +24 -0
- data/lib/evilution/integration/loading/mutation_applier.rb +52 -0
- data/lib/evilution/integration/loading/redefinition_recovery.rb +54 -0
- data/lib/evilution/integration/loading/source_evaluator.rb +15 -0
- data/lib/evilution/integration/loading/syntax_validator.rb +19 -0
- data/lib/evilution/integration/loading.rb +6 -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/load_path/subpath_resolver.rb +25 -0
- data/lib/evilution/load_path.rb +4 -0
- 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 +20 -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/subject_pipeline.rb +18 -8
- data/lib/evilution/runner.rb +21 -0
- data/lib/evilution/version.rb +1 -1
- metadata +125 -5
- data/lib/evilution/mcp/session_diff_tool.rb +0 -63
- data/lib/evilution/mcp/session_list_tool.rb +0 -50
- data/lib/evilution/mcp/session_show_tool.rb +0 -57
|
@@ -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
|
|
@@ -20,13 +20,7 @@ class Evilution::Runner::SubjectPipeline
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def target_files
|
|
23
|
-
@target_files ||=
|
|
24
|
-
resolve_source_glob
|
|
25
|
-
elsif !config.target_files.empty?
|
|
26
|
-
config.target_files
|
|
27
|
-
else
|
|
28
|
-
Evilution::Git::ChangedFiles.new.call
|
|
29
|
-
end
|
|
23
|
+
@target_files ||= resolve_target_files
|
|
30
24
|
end
|
|
31
25
|
|
|
32
26
|
private
|
|
@@ -88,11 +82,27 @@ class Evilution::Runner::SubjectPipeline
|
|
|
88
82
|
|
|
89
83
|
def filter_by_target(subjects)
|
|
90
84
|
matched = subjects.select(&target_matcher)
|
|
91
|
-
raise Evilution::Error,
|
|
85
|
+
raise Evilution::Error, build_no_match_error if matched.empty?
|
|
92
86
|
|
|
93
87
|
matched
|
|
94
88
|
end
|
|
95
89
|
|
|
90
|
+
def resolve_target_files
|
|
91
|
+
return resolve_source_glob if source_glob_target?
|
|
92
|
+
return config.target_files unless config.target_files.empty?
|
|
93
|
+
|
|
94
|
+
@used_git_fallback = true
|
|
95
|
+
Evilution::Git::ChangedFiles.new.call
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_no_match_error
|
|
99
|
+
base = "no subject matched '#{config.target}'"
|
|
100
|
+
return base unless @used_git_fallback
|
|
101
|
+
|
|
102
|
+
"#{base}; scanned git-changed files only. Pass file paths or " \
|
|
103
|
+
"--target source:<glob> to scan the full codebase."
|
|
104
|
+
end
|
|
105
|
+
|
|
96
106
|
def target_matcher
|
|
97
107
|
target = config.target
|
|
98
108
|
if target.end_with?("*")
|
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