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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "prism"
|
|
4
3
|
require_relative "../integration"
|
|
4
|
+
require_relative "loading/mutation_applier"
|
|
5
5
|
|
|
6
6
|
class Evilution::Integration::Base
|
|
7
7
|
def self.baseline_runner
|
|
@@ -12,14 +12,15 @@ class Evilution::Integration::Base
|
|
|
12
12
|
raise NotImplementedError, "#{name}.baseline_options must be implemented"
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def initialize(hooks: nil)
|
|
15
|
+
def initialize(hooks: nil, mutation_applier: Evilution::Integration::Loading::MutationApplier.new)
|
|
16
16
|
@hooks = hooks
|
|
17
|
+
@mutation_applier = mutation_applier
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def call(mutation)
|
|
20
21
|
ensure_framework_loaded
|
|
21
22
|
fire_hook(:mutation_insert_pre, mutation: mutation, file_path: mutation.file_path)
|
|
22
|
-
load_error =
|
|
23
|
+
load_error = @mutation_applier.call(mutation)
|
|
23
24
|
return load_error if load_error
|
|
24
25
|
|
|
25
26
|
fire_hook(:mutation_insert_post, mutation: mutation, file_path: mutation.file_path)
|
|
@@ -47,156 +48,4 @@ class Evilution::Integration::Base
|
|
|
47
48
|
def fire_hook(event, **payload)
|
|
48
49
|
@hooks.fire(event, **payload) if @hooks
|
|
49
50
|
end
|
|
50
|
-
|
|
51
|
-
def apply_mutation(mutation)
|
|
52
|
-
prism_error = validate_mutated_syntax(mutation.mutated_source)
|
|
53
|
-
return prism_error if prism_error
|
|
54
|
-
|
|
55
|
-
pin_autoloaded_constants(mutation.original_source)
|
|
56
|
-
clear_concern_state(mutation.file_path)
|
|
57
|
-
with_redefinition_recovery(mutation.original_source) do
|
|
58
|
-
eval_mutated_source(mutation)
|
|
59
|
-
end
|
|
60
|
-
nil
|
|
61
|
-
rescue SyntaxError => e
|
|
62
|
-
{
|
|
63
|
-
passed: false,
|
|
64
|
-
error: "syntax error in mutated source: #{e.message}",
|
|
65
|
-
error_class: e.class.name,
|
|
66
|
-
error_backtrace: Array(e.backtrace).first(5)
|
|
67
|
-
}
|
|
68
|
-
rescue ScriptError, StandardError => e
|
|
69
|
-
{
|
|
70
|
-
passed: false,
|
|
71
|
-
error: "#{e.class}: #{e.message}",
|
|
72
|
-
error_class: e.class.name,
|
|
73
|
-
error_backtrace: Array(e.backtrace).first(5)
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def validate_mutated_syntax(source)
|
|
78
|
-
return nil if Prism.parse(source).success?
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
passed: false,
|
|
82
|
-
error: "mutated source has syntax errors",
|
|
83
|
-
error_class: "SyntaxError",
|
|
84
|
-
error_backtrace: []
|
|
85
|
-
}
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Evaluate the mutated source with __FILE__ set to the original path so
|
|
89
|
-
# that `require_relative` and `__dir__` resolve against the real source
|
|
90
|
-
# tree, where sibling files actually exist.
|
|
91
|
-
def eval_mutated_source(mutation)
|
|
92
|
-
absolute = File.expand_path(mutation.file_path)
|
|
93
|
-
# rubocop:disable Security/Eval
|
|
94
|
-
eval(mutation.mutated_source, TOPLEVEL_BINDING, absolute, 1)
|
|
95
|
-
# rubocop:enable Security/Eval
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def with_redefinition_recovery(original_source)
|
|
99
|
-
yield
|
|
100
|
-
rescue ArgumentError => e
|
|
101
|
-
raise unless redefinition_conflict?(e)
|
|
102
|
-
|
|
103
|
-
remove_defined_constants(original_source)
|
|
104
|
-
yield
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def redefinition_conflict?(error)
|
|
108
|
-
error.message.include?("already defined")
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def pin_autoloaded_constants(source)
|
|
112
|
-
collect_constant_names(Prism.parse(source).value).each do |name|
|
|
113
|
-
Object.const_get(name) if Object.const_defined?(name, false)
|
|
114
|
-
rescue NameError # :nodoc:
|
|
115
|
-
nil
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def collect_constant_names(node, nesting = [])
|
|
120
|
-
names = []
|
|
121
|
-
case node
|
|
122
|
-
when Prism::ModuleNode, Prism::ClassNode
|
|
123
|
-
const = node.constant_path.full_name
|
|
124
|
-
qualified = nesting.any? && !const.include?("::") ? "#{nesting.join("::")}::#{const}" : const
|
|
125
|
-
names << qualified
|
|
126
|
-
names.concat(collect_constant_names(node.body, nesting + [const])) if node.body
|
|
127
|
-
when Prism::ProgramNode
|
|
128
|
-
names.concat(collect_constant_names(node.statements, nesting)) if node.statements
|
|
129
|
-
when Prism::StatementsNode
|
|
130
|
-
node.body.each { |child| names.concat(collect_constant_names(child, nesting)) }
|
|
131
|
-
end
|
|
132
|
-
names
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def remove_defined_constants(source)
|
|
136
|
-
collect_constant_names(Prism.parse(source).value).reverse_each do |name|
|
|
137
|
-
parent_name, _, local_name = name.rpartition("::")
|
|
138
|
-
parent = resolve_loaded_constant_parent(parent_name)
|
|
139
|
-
next unless parent
|
|
140
|
-
next unless parent.const_defined?(local_name, false)
|
|
141
|
-
next if parent.autoload?(local_name)
|
|
142
|
-
|
|
143
|
-
parent.send(:remove_const, local_name.to_sym)
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def resolve_loaded_constant_parent(parent_name)
|
|
148
|
-
return Object if parent_name.empty?
|
|
149
|
-
|
|
150
|
-
parent_name.split("::").reduce(Object) do |mod, part|
|
|
151
|
-
return nil unless mod.const_defined?(part, false)
|
|
152
|
-
return nil if mod.autoload?(part)
|
|
153
|
-
|
|
154
|
-
resolved = mod.const_get(part, false)
|
|
155
|
-
return nil unless resolved.is_a?(Module)
|
|
156
|
-
|
|
157
|
-
resolved
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def clear_concern_state(file_path)
|
|
162
|
-
return unless defined?(ActiveSupport::Concern)
|
|
163
|
-
|
|
164
|
-
absolute = File.expand_path(file_path)
|
|
165
|
-
subpath = resolve_require_subpath(file_path)
|
|
166
|
-
|
|
167
|
-
ObjectSpace.each_object(Module) do |mod|
|
|
168
|
-
next unless mod.singleton_class.ancestors.include?(ActiveSupport::Concern)
|
|
169
|
-
|
|
170
|
-
%i[@_included_block @_prepended_block].each do |ivar|
|
|
171
|
-
next unless mod.instance_variable_defined?(ivar)
|
|
172
|
-
|
|
173
|
-
block = mod.instance_variable_get(ivar)
|
|
174
|
-
block_file = block.source_location&.first
|
|
175
|
-
next unless block_file
|
|
176
|
-
|
|
177
|
-
expanded = File.expand_path(block_file)
|
|
178
|
-
mod.remove_instance_variable(ivar) if source_matches?(expanded, absolute, subpath)
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def source_matches?(block_path, absolute, subpath)
|
|
184
|
-
block_path == absolute || (subpath && block_path.end_with?("/#{subpath}"))
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def resolve_require_subpath(file_path)
|
|
188
|
-
absolute = File.expand_path(file_path)
|
|
189
|
-
best_subpath = nil
|
|
190
|
-
|
|
191
|
-
$LOAD_PATH.each do |entry|
|
|
192
|
-
dir = File.expand_path(entry)
|
|
193
|
-
prefix = dir.end_with?("/") ? dir : "#{dir}/"
|
|
194
|
-
next unless absolute.start_with?(prefix)
|
|
195
|
-
|
|
196
|
-
candidate = absolute.delete_prefix(prefix)
|
|
197
|
-
best_subpath = candidate if best_subpath.nil? || candidate.length < best_subpath.length
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
best_subpath
|
|
201
|
-
end
|
|
202
51
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
require_relative "../../load_path/subpath_resolver"
|
|
5
|
+
|
|
6
|
+
# Re-evaluating an `ActiveSupport::Concern` module raises
|
|
7
|
+
# "MultipleIncludedBlocks" because AS::Concern records the block source
|
|
8
|
+
# location on the first include/prepend call. Before a re-eval we clear the
|
|
9
|
+
# `@_included_block` / `@_prepended_block` ivar on modules whose block came
|
|
10
|
+
# from the file we're about to re-eval.
|
|
11
|
+
class Evilution::Integration::Loading::ConcernStateCleaner
|
|
12
|
+
IVARS = %i[@_included_block @_prepended_block].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(subpath_resolver: Evilution::LoadPath::SubpathResolver.new)
|
|
15
|
+
@subpath_resolver = subpath_resolver
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(file_path)
|
|
19
|
+
return unless defined?(ActiveSupport::Concern)
|
|
20
|
+
|
|
21
|
+
absolute = File.expand_path(file_path)
|
|
22
|
+
subpath = @subpath_resolver.call(file_path)
|
|
23
|
+
|
|
24
|
+
ObjectSpace.each_object(Module) do |mod|
|
|
25
|
+
next unless mod.singleton_class.ancestors.include?(ActiveSupport::Concern)
|
|
26
|
+
|
|
27
|
+
clear_concern_ivars(mod, absolute, subpath)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def clear_concern_ivars(mod, absolute, subpath)
|
|
34
|
+
IVARS.each do |ivar|
|
|
35
|
+
next unless mod.instance_variable_defined?(ivar)
|
|
36
|
+
|
|
37
|
+
block = mod.instance_variable_get(ivar)
|
|
38
|
+
block_file = block.source_location&.first
|
|
39
|
+
next unless block_file
|
|
40
|
+
|
|
41
|
+
expanded = File.expand_path(block_file)
|
|
42
|
+
mod.remove_instance_variable(ivar) if source_matches?(expanded, absolute, subpath)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def source_matches?(block_path, absolute, subpath)
|
|
47
|
+
block_path == absolute || (subpath && block_path.end_with?("/#{subpath}"))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
require_relative "../../ast/constant_names"
|
|
5
|
+
|
|
6
|
+
# Defeat Zeitwerk's re-autoload hook when we re-eval a file in place. Walking
|
|
7
|
+
# the source AST for top-level class/module names and calling `const_get` on
|
|
8
|
+
# each tells Zeitwerk "this constant is loaded" so our re-eval does not lose
|
|
9
|
+
# state (e.g. `@_included_block`) to a follow-up autoload.
|
|
10
|
+
class Evilution::Integration::Loading::ConstantPinner
|
|
11
|
+
def initialize(constant_names: Evilution::AST::ConstantNames.new)
|
|
12
|
+
@constant_names = constant_names
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(source)
|
|
16
|
+
names = @constant_names.call(source)
|
|
17
|
+
names.each do |name|
|
|
18
|
+
Object.const_get(name) if Object.const_defined?(name, false)
|
|
19
|
+
rescue NameError # :nodoc:
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
names
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
require_relative "syntax_validator"
|
|
5
|
+
require_relative "constant_pinner"
|
|
6
|
+
require_relative "concern_state_cleaner"
|
|
7
|
+
require_relative "source_evaluator"
|
|
8
|
+
require_relative "redefinition_recovery"
|
|
9
|
+
|
|
10
|
+
# Composes the load-time pipeline that applies a mutation's new source to the
|
|
11
|
+
# running VM: syntax-validate -> pin top-level constants (beats Zeitwerk) ->
|
|
12
|
+
# clear AS::Concern state -> eval inside a redefinition-recovery wrapper.
|
|
13
|
+
# Returns nil on success or a failure-shaped hash on any error.
|
|
14
|
+
class Evilution::Integration::Loading::MutationApplier
|
|
15
|
+
def initialize(syntax_validator: Evilution::Integration::Loading::SyntaxValidator.new,
|
|
16
|
+
constant_pinner: Evilution::Integration::Loading::ConstantPinner.new,
|
|
17
|
+
concern_state_cleaner: Evilution::Integration::Loading::ConcernStateCleaner.new,
|
|
18
|
+
source_evaluator: Evilution::Integration::Loading::SourceEvaluator.new,
|
|
19
|
+
redefinition_recovery: Evilution::Integration::Loading::RedefinitionRecovery.new)
|
|
20
|
+
@syntax_validator = syntax_validator
|
|
21
|
+
@constant_pinner = constant_pinner
|
|
22
|
+
@concern_state_cleaner = concern_state_cleaner
|
|
23
|
+
@source_evaluator = source_evaluator
|
|
24
|
+
@redefinition_recovery = redefinition_recovery
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(mutation)
|
|
28
|
+
syntax_error = @syntax_validator.call(mutation.mutated_source)
|
|
29
|
+
return syntax_error if syntax_error
|
|
30
|
+
|
|
31
|
+
@constant_pinner.call(mutation.original_source)
|
|
32
|
+
@concern_state_cleaner.call(mutation.file_path)
|
|
33
|
+
@redefinition_recovery.call(mutation.original_source) do
|
|
34
|
+
@source_evaluator.call(mutation.mutated_source, mutation.file_path)
|
|
35
|
+
end
|
|
36
|
+
nil
|
|
37
|
+
rescue SyntaxError => e
|
|
38
|
+
{
|
|
39
|
+
passed: false,
|
|
40
|
+
error: "syntax error in mutated source: #{e.message}",
|
|
41
|
+
error_class: e.class.name,
|
|
42
|
+
error_backtrace: Array(e.backtrace).first(5)
|
|
43
|
+
}
|
|
44
|
+
rescue ScriptError, StandardError => e
|
|
45
|
+
{
|
|
46
|
+
passed: false,
|
|
47
|
+
error: "#{e.class}: #{e.message}",
|
|
48
|
+
error_class: e.class.name,
|
|
49
|
+
error_backtrace: Array(e.backtrace).first(5)
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
require_relative "../../ast/constant_names"
|
|
5
|
+
|
|
6
|
+
# Some DSLs (Rails 8 enum, define_method guards) raise ArgumentError on
|
|
7
|
+
# re-declaration. On such a conflict we strip constants declared in the source
|
|
8
|
+
# and retry the load once against a fresh namespace.
|
|
9
|
+
class Evilution::Integration::Loading::RedefinitionRecovery
|
|
10
|
+
def initialize(constant_names: Evilution::AST::ConstantNames.new)
|
|
11
|
+
@constant_names = constant_names
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(source, &block)
|
|
15
|
+
block.call
|
|
16
|
+
rescue ArgumentError => e
|
|
17
|
+
raise unless redefinition_conflict?(e)
|
|
18
|
+
|
|
19
|
+
remove_defined_constants(source)
|
|
20
|
+
block.call
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def redefinition_conflict?(error)
|
|
26
|
+
error.message.include?("already defined")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def remove_defined_constants(source)
|
|
30
|
+
@constant_names.call(source).reverse_each do |name|
|
|
31
|
+
parent_name, _, local_name = name.rpartition("::")
|
|
32
|
+
parent = resolve_loaded_constant_parent(parent_name)
|
|
33
|
+
next unless parent
|
|
34
|
+
next unless parent.const_defined?(local_name, false)
|
|
35
|
+
next if parent.autoload?(local_name)
|
|
36
|
+
|
|
37
|
+
parent.send(:remove_const, local_name.to_sym)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def resolve_loaded_constant_parent(parent_name)
|
|
42
|
+
return Object if parent_name.empty?
|
|
43
|
+
|
|
44
|
+
parent_name.split("::").reduce(Object) do |mod, part|
|
|
45
|
+
return nil unless mod.const_defined?(part, false)
|
|
46
|
+
return nil if mod.autoload?(part)
|
|
47
|
+
|
|
48
|
+
resolved = mod.const_get(part, false)
|
|
49
|
+
return nil unless resolved.is_a?(Module)
|
|
50
|
+
|
|
51
|
+
resolved
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
|
|
5
|
+
# Evaluate source with __FILE__ set to the absolute original path so that
|
|
6
|
+
# `require_relative` and `__dir__` resolve against the real source tree, where
|
|
7
|
+
# sibling files actually exist.
|
|
8
|
+
class Evilution::Integration::Loading::SourceEvaluator
|
|
9
|
+
def call(source, file_path)
|
|
10
|
+
absolute = File.expand_path(file_path)
|
|
11
|
+
# rubocop:disable Security/Eval
|
|
12
|
+
eval(source, TOPLEVEL_BINDING, absolute, 1)
|
|
13
|
+
# rubocop:enable Security/Eval
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../loading"
|
|
5
|
+
|
|
6
|
+
class Evilution::Integration::Loading::SyntaxValidator
|
|
7
|
+
ERROR_MESSAGE = "mutated source has syntax errors"
|
|
8
|
+
|
|
9
|
+
def call(source)
|
|
10
|
+
return nil if Prism.parse(source).success?
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
passed: false,
|
|
14
|
+
error: ERROR_MESSAGE,
|
|
15
|
+
error_class: "SyntaxError",
|
|
16
|
+
error_backtrace: []
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rspec"
|
|
4
|
+
|
|
5
|
+
class Evilution::Integration::RSpec::BaselineRunner
|
|
6
|
+
def call(spec_file)
|
|
7
|
+
require "rspec/core"
|
|
8
|
+
spec_dir = File.expand_path("spec")
|
|
9
|
+
$LOAD_PATH.unshift(spec_dir) unless $LOAD_PATH.include?(spec_dir)
|
|
10
|
+
::RSpec.reset
|
|
11
|
+
status = ::RSpec::Core::Runner.run(
|
|
12
|
+
["--format", "progress", "--no-color", "--order", "defined", spec_file]
|
|
13
|
+
)
|
|
14
|
+
status.zero?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../rspec"
|
|
5
|
+
require_relative "../crash_detector"
|
|
6
|
+
|
|
7
|
+
class Evilution::Integration::RSpec::CrashDetectorLifecycle
|
|
8
|
+
def current
|
|
9
|
+
if @detector
|
|
10
|
+
@detector.reset
|
|
11
|
+
else
|
|
12
|
+
@detector = Evilution::Integration::CrashDetector.new(StringIO.new)
|
|
13
|
+
::RSpec.configuration.add_formatter(@detector)
|
|
14
|
+
end
|
|
15
|
+
@detector
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rspec"
|
|
4
|
+
|
|
5
|
+
module Evilution::Integration::RSpec::ExampleFilterApplier
|
|
6
|
+
class Identity
|
|
7
|
+
def call(_mutation, files)
|
|
8
|
+
files
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Custom
|
|
13
|
+
def initialize(filter)
|
|
14
|
+
@filter = filter
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(mutation, files)
|
|
18
|
+
@filter.call(mutation, files)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rspec"
|
|
4
|
+
require_relative "../crash_detector"
|
|
5
|
+
|
|
6
|
+
class Evilution::Integration::RSpec::FrameworkLoader
|
|
7
|
+
def loaded?
|
|
8
|
+
@loaded == true
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
return if @loaded
|
|
13
|
+
|
|
14
|
+
require "rspec/core"
|
|
15
|
+
add_spec_load_path
|
|
16
|
+
Evilution::Integration::CrashDetector.register_with_rspec
|
|
17
|
+
@loaded = true
|
|
18
|
+
rescue LoadError => e
|
|
19
|
+
raise Evilution::Error, "rspec-core is required but not available: #{e.message}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def add_spec_load_path
|
|
25
|
+
spec_dir = File.expand_path("spec")
|
|
26
|
+
$LOAD_PATH.unshift(spec_dir) unless $LOAD_PATH.include?(spec_dir)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rspec"
|
|
4
|
+
|
|
5
|
+
class Evilution::Integration::RSpec::ResultBuilder
|
|
6
|
+
def unresolved(mutation)
|
|
7
|
+
{
|
|
8
|
+
passed: false,
|
|
9
|
+
unresolved: true,
|
|
10
|
+
error: "no matching spec resolved for #{mutation.file_path}",
|
|
11
|
+
test_command: "rspec (skipped: no spec resolved for #{mutation.file_path})"
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def unresolved_example(mutation)
|
|
16
|
+
{
|
|
17
|
+
passed: false,
|
|
18
|
+
unresolved: true,
|
|
19
|
+
error: "no matching example found for #{mutation.file_path}",
|
|
20
|
+
test_command: "rspec (skipped: no matching example for #{mutation.file_path})"
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def from_run(status, command, detector)
|
|
25
|
+
return { passed: true, test_command: command } if status.zero?
|
|
26
|
+
|
|
27
|
+
if detector.only_crashes?
|
|
28
|
+
classes = detector.unique_crash_classes
|
|
29
|
+
return {
|
|
30
|
+
passed: false,
|
|
31
|
+
test_crashed: true,
|
|
32
|
+
error: "test crashes: #{detector.crash_summary}",
|
|
33
|
+
error_class: (classes.first if classes.length == 1),
|
|
34
|
+
test_command: command
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{ passed: false, test_command: command }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../rspec"
|
|
4
|
+
|
|
5
|
+
class Evilution::Integration::RSpec::StateGuard; end unless defined?(Evilution::Integration::RSpec::StateGuard) # rubocop:disable Lint/EmptyClass
|
|
6
|
+
|
|
7
|
+
class Evilution::Integration::RSpec::StateGuard::ExampleGroupsConstants
|
|
8
|
+
def snapshot
|
|
9
|
+
return nil unless defined?(::RSpec::ExampleGroups)
|
|
10
|
+
|
|
11
|
+
Set.new(::RSpec::ExampleGroups.constants(false))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def release(before)
|
|
15
|
+
return unless before
|
|
16
|
+
return unless defined?(::RSpec::ExampleGroups)
|
|
17
|
+
|
|
18
|
+
::RSpec::ExampleGroups.constants(false).each do |c|
|
|
19
|
+
next if before.include?(c)
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
::RSpec::ExampleGroups.send(:remove_const, c)
|
|
23
|
+
rescue NameError
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../rspec"
|
|
4
|
+
|
|
5
|
+
class Evilution::Integration::RSpec::StateGuard; end unless defined?(Evilution::Integration::RSpec::StateGuard) # rubocop:disable Lint/EmptyClass
|
|
6
|
+
|
|
7
|
+
module Evilution::Integration::RSpec::StateGuard::Internals
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def world_ivar(name)
|
|
11
|
+
world = ::RSpec.world
|
|
12
|
+
world.instance_variable_defined?(name) ? world.instance_variable_get(name) : nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def config_ivar(name)
|
|
16
|
+
config = ::RSpec.configuration
|
|
17
|
+
config.instance_variable_defined?(name) ? config.instance_variable_get(name) : nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../rspec"
|
|
4
|
+
|
|
5
|
+
class Evilution::Integration::RSpec::StateGuard; end unless defined?(Evilution::Integration::RSpec::StateGuard) # rubocop:disable Lint/EmptyClass
|
|
6
|
+
|
|
7
|
+
class Evilution::Integration::RSpec::StateGuard::ObjectSpaceExampleGroups
|
|
8
|
+
def snapshot
|
|
9
|
+
groups = Set.new
|
|
10
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
11
|
+
groups << klass.object_id if klass < ::RSpec::Core::ExampleGroup
|
|
12
|
+
rescue TypeError # rubocop:disable Lint/SuppressedException
|
|
13
|
+
end
|
|
14
|
+
groups
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def release(eg_before)
|
|
18
|
+
return unless eg_before
|
|
19
|
+
|
|
20
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
21
|
+
next unless klass < ::RSpec::Core::ExampleGroup
|
|
22
|
+
next if eg_before.include?(klass.object_id)
|
|
23
|
+
|
|
24
|
+
klass.constants(false).each do |const|
|
|
25
|
+
klass.send(:remove_const, const)
|
|
26
|
+
rescue NameError # rubocop:disable Lint/SuppressedException
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
klass.instance_variables.each do |ivar|
|
|
30
|
+
klass.remove_instance_variable(ivar)
|
|
31
|
+
end
|
|
32
|
+
rescue TypeError # rubocop:disable Lint/SuppressedException
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../rspec"
|
|
4
|
+
require_relative "internals"
|
|
5
|
+
|
|
6
|
+
class Evilution::Integration::RSpec::StateGuard::ReporterArrays
|
|
7
|
+
IVARS = %i[@examples @failed_examples @pending_examples].freeze
|
|
8
|
+
|
|
9
|
+
def snapshot
|
|
10
|
+
reporter = Evilution::Integration::RSpec::StateGuard::Internals.config_ivar(:@reporter)
|
|
11
|
+
return nil unless reporter
|
|
12
|
+
|
|
13
|
+
IVARS.each_with_object({}) do |ivar, acc|
|
|
14
|
+
next unless reporter.instance_variable_defined?(ivar)
|
|
15
|
+
|
|
16
|
+
arr = reporter.instance_variable_get(ivar)
|
|
17
|
+
acc[ivar] = arr.length if arr.is_a?(Array)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def release(lengths)
|
|
22
|
+
return unless lengths
|
|
23
|
+
|
|
24
|
+
reporter = Evilution::Integration::RSpec::StateGuard::Internals.config_ivar(:@reporter)
|
|
25
|
+
return unless reporter
|
|
26
|
+
|
|
27
|
+
lengths.each do |ivar, length|
|
|
28
|
+
arr = reporter.instance_variable_get(ivar)
|
|
29
|
+
arr.slice!(length..) if arr.is_a?(Array) && arr.length > length
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../rspec"
|
|
4
|
+
require_relative "internals"
|
|
5
|
+
|
|
6
|
+
class Evilution::Integration::RSpec::StateGuard::WorldExampleGroups
|
|
7
|
+
def snapshot
|
|
8
|
+
groups = Evilution::Integration::RSpec::StateGuard::Internals.world_ivar(:@example_groups)
|
|
9
|
+
groups ? groups.dup.freeze : nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def release(before)
|
|
13
|
+
return unless before
|
|
14
|
+
|
|
15
|
+
groups = Evilution::Integration::RSpec::StateGuard::Internals.world_ivar(:@example_groups)
|
|
16
|
+
return unless groups
|
|
17
|
+
|
|
18
|
+
groups.select! { |g| before.include?(g) }
|
|
19
|
+
end
|
|
20
|
+
end
|