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.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +15 -0
  3. data/.claude/prompts/architect.md +14 -1
  4. data/.claude/skills/create-issue/SKILL.md +55 -0
  5. data/.rubocop_todo.yml +7 -0
  6. data/CHANGELOG.md +38 -0
  7. data/README.md +57 -3
  8. data/lib/evilution/ast/constant_names.rb +34 -0
  9. data/lib/evilution/cache.rb +2 -0
  10. data/lib/evilution/child_output.rb +24 -0
  11. data/lib/evilution/cli/commands/run.rb +9 -0
  12. data/lib/evilution/cli/commands/version.rb +2 -0
  13. data/lib/evilution/cli/parser/options_builder.rb +16 -2
  14. data/lib/evilution/compare/invalid_input.rb +12 -0
  15. data/lib/evilution/compare.rb +1 -10
  16. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  17. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  18. data/lib/evilution/config/builders.rb +4 -0
  19. data/lib/evilution/config/env_loader.rb +12 -0
  20. data/lib/evilution/config/file_loader.rb +22 -0
  21. data/lib/evilution/config/sources.rb +14 -0
  22. data/lib/evilution/config/validators/base.rb +37 -0
  23. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  24. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  25. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  26. data/lib/evilution/config/validators/hooks.rb +12 -0
  27. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  28. data/lib/evilution/config/validators/integration.rb +11 -0
  29. data/lib/evilution/config/validators/isolation.rb +19 -0
  30. data/lib/evilution/config/validators/jobs.rb +9 -0
  31. data/lib/evilution/config/validators/preload.rb +13 -0
  32. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  33. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  34. data/lib/evilution/config/validators.rb +4 -0
  35. data/lib/evilution/config.rb +78 -268
  36. data/lib/evilution/feedback/detector.rb +15 -0
  37. data/lib/evilution/feedback/messages.rb +42 -0
  38. data/lib/evilution/feedback.rb +5 -0
  39. data/lib/evilution/integration/base.rb +4 -155
  40. data/lib/evilution/integration/loading/concern_state_cleaner.rb +49 -0
  41. data/lib/evilution/integration/loading/constant_pinner.rb +24 -0
  42. data/lib/evilution/integration/loading/mutation_applier.rb +52 -0
  43. data/lib/evilution/integration/loading/redefinition_recovery.rb +54 -0
  44. data/lib/evilution/integration/loading/source_evaluator.rb +15 -0
  45. data/lib/evilution/integration/loading/syntax_validator.rb +19 -0
  46. data/lib/evilution/integration/loading.rb +6 -0
  47. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  48. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  49. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  50. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  51. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  52. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  53. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  54. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +35 -0
  55. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  56. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  57. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  58. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  59. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  60. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  61. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  62. data/lib/evilution/integration/rspec.rb +61 -232
  63. data/lib/evilution/isolation/fork.rb +7 -2
  64. data/lib/evilution/load_path/subpath_resolver.rb +25 -0
  65. data/lib/evilution/load_path.rb +4 -0
  66. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  67. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  68. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  69. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  70. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  71. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  72. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  73. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  74. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  75. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  76. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  77. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  78. data/lib/evilution/mcp/info_tool.rb +43 -261
  79. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  80. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  81. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  82. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  83. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  84. data/lib/evilution/parallel/work_queue/channel/frame.rb +21 -0
  85. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  86. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  87. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  88. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  89. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  90. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  91. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  92. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  93. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  94. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  95. data/lib/evilution/parallel/work_queue.rb +42 -327
  96. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  97. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  98. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  99. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  100. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  101. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  102. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  103. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  104. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  105. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  106. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  107. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  108. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  109. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  110. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  111. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  112. data/lib/evilution/reporter/cli/pct.rb +9 -0
  113. data/lib/evilution/reporter/cli/section.rb +13 -0
  114. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  115. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  116. data/lib/evilution/reporter/cli.rb +79 -162
  117. data/lib/evilution/runner/isolation_resolver.rb +20 -2
  118. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +32 -0
  119. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +16 -0
  120. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +46 -0
  121. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +75 -0
  122. data/lib/evilution/runner/mutation_executor/result_cache.rb +69 -0
  123. data/lib/evilution/runner/mutation_executor/result_notifier.rb +48 -0
  124. data/lib/evilution/runner/mutation_executor/result_packer.rb +39 -0
  125. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +80 -0
  126. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +34 -0
  127. data/lib/evilution/runner/mutation_executor.rb +58 -289
  128. data/lib/evilution/runner/subject_pipeline.rb +18 -8
  129. data/lib/evilution/runner.rb +21 -0
  130. data/lib/evilution/version.rb +1 -1
  131. metadata +125 -5
  132. data/lib/evilution/mcp/session_diff_tool.rb +0 -63
  133. data/lib/evilution/mcp/session_list_tool.rb +0 -50
  134. 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 = apply_mutation(mutation)
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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../integration"
4
+
5
+ module Evilution::Integration::Loading
6
+ 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