evilution 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +10 -0
  3. data/.rubocop_todo.yml +7 -0
  4. data/CHANGELOG.md +22 -0
  5. data/README.md +57 -3
  6. data/lib/evilution/cache.rb +2 -0
  7. data/lib/evilution/child_output.rb +24 -0
  8. data/lib/evilution/cli/commands/run.rb +9 -0
  9. data/lib/evilution/cli/commands/version.rb +2 -0
  10. data/lib/evilution/cli/parser/options_builder.rb +16 -2
  11. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  12. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  13. data/lib/evilution/config/builders.rb +4 -0
  14. data/lib/evilution/config/env_loader.rb +12 -0
  15. data/lib/evilution/config/file_loader.rb +22 -0
  16. data/lib/evilution/config/sources.rb +14 -0
  17. data/lib/evilution/config/validators/base.rb +37 -0
  18. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  19. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  20. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  21. data/lib/evilution/config/validators/hooks.rb +12 -0
  22. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  23. data/lib/evilution/config/validators/integration.rb +11 -0
  24. data/lib/evilution/config/validators/isolation.rb +19 -0
  25. data/lib/evilution/config/validators/jobs.rb +9 -0
  26. data/lib/evilution/config/validators/preload.rb +13 -0
  27. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  28. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  29. data/lib/evilution/config/validators.rb +4 -0
  30. data/lib/evilution/config.rb +78 -268
  31. data/lib/evilution/feedback/detector.rb +15 -0
  32. data/lib/evilution/feedback/messages.rb +42 -0
  33. data/lib/evilution/feedback.rb +5 -0
  34. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  35. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  36. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  37. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  38. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  39. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  40. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  41. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +35 -0
  42. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  43. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  44. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  45. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  46. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  47. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  48. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  49. data/lib/evilution/integration/rspec.rb +61 -232
  50. data/lib/evilution/isolation/fork.rb +7 -2
  51. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  52. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  53. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  54. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  55. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  56. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  57. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  58. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  59. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  60. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  61. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  62. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  63. data/lib/evilution/mcp/info_tool.rb +43 -261
  64. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  65. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  66. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  67. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  68. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  69. data/lib/evilution/parallel/work_queue/channel/frame.rb +21 -0
  70. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  71. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  72. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  73. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  74. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  75. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  76. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  77. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  78. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  79. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  80. data/lib/evilution/parallel/work_queue.rb +42 -327
  81. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  82. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  83. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  84. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  85. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  86. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  87. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  88. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  89. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  90. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  91. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  92. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  93. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  94. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  95. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  96. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  97. data/lib/evilution/reporter/cli/pct.rb +9 -0
  98. data/lib/evilution/reporter/cli/section.rb +13 -0
  99. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  100. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  101. data/lib/evilution/reporter/cli.rb +79 -162
  102. data/lib/evilution/runner/isolation_resolver.rb +9 -2
  103. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +32 -0
  104. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +16 -0
  105. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +46 -0
  106. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +75 -0
  107. data/lib/evilution/runner/mutation_executor/result_cache.rb +69 -0
  108. data/lib/evilution/runner/mutation_executor/result_notifier.rb +48 -0
  109. data/lib/evilution/runner/mutation_executor/result_packer.rb +39 -0
  110. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +80 -0
  111. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +34 -0
  112. data/lib/evilution/runner/mutation_executor.rb +58 -289
  113. data/lib/evilution/runner.rb +21 -0
  114. data/lib/evilution/version.rb +1 -1
  115. metadata +113 -2
@@ -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
@@ -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::WorldFilteredExamples
7
+ def snapshot
8
+ fe = Evilution::Integration::RSpec::StateGuard::Internals.world_ivar(:@filtered_examples)
9
+ fe ? Set.new(fe.keys.map(&:object_id)) : nil
10
+ end
11
+
12
+ def release(snapshot_keys)
13
+ fe = Evilution::Integration::RSpec::StateGuard::Internals.world_ivar(:@filtered_examples)
14
+ return unless fe && snapshot_keys
15
+
16
+ fe.each_key.to_a.each do |k|
17
+ fe.delete(k) unless snapshot_keys.include?(k.object_id)
18
+ end
19
+ end
20
+ 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::WorldSourcesByPath
7
+ def snapshot
8
+ src = Evilution::Integration::RSpec::StateGuard::Internals.world_ivar(:@sources_by_path)
9
+ src ? Set.new(src.keys) : nil
10
+ end
11
+
12
+ def release(before)
13
+ return unless before
14
+
15
+ src = Evilution::Integration::RSpec::StateGuard::Internals.world_ivar(:@sources_by_path)
16
+ return unless src
17
+
18
+ src.delete_if { |k, _v| !before.include?(k) }
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rspec"
4
+ require_relative "state_guard/object_space_example_groups"
5
+ require_relative "state_guard/world_example_groups"
6
+ require_relative "state_guard/world_sources_by_path"
7
+ require_relative "state_guard/world_filtered_examples"
8
+ require_relative "state_guard/reporter_arrays"
9
+ require_relative "state_guard/example_groups_constants"
10
+
11
+ class Evilution::Integration::RSpec::StateGuard
12
+ DEFAULT_STRATEGIES = [
13
+ ObjectSpaceExampleGroups.new,
14
+ WorldExampleGroups.new,
15
+ WorldSourcesByPath.new,
16
+ WorldFilteredExamples.new,
17
+ ReporterArrays.new,
18
+ ExampleGroupsConstants.new
19
+ ].freeze
20
+
21
+ def initialize(strategies: DEFAULT_STRATEGIES)
22
+ @strategies = strategies
23
+ end
24
+
25
+ def snapshot
26
+ @strategies.map { |s| [s, s.snapshot] }
27
+ end
28
+
29
+ def release(token)
30
+ token.reverse_each { |strategy, captured| release_one(strategy, captured) }
31
+ end
32
+
33
+ private
34
+
35
+ def release_one(strategy, captured)
36
+ strategy.release(captured)
37
+ rescue StandardError => e
38
+ warn "[evilution] state release failed for #{strategy.class.name}: #{e.class}: #{e.message}"
39
+ end
40
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rspec"
4
+
5
+ class Evilution::Integration::RSpec::TestFileResolver
6
+ def initialize(test_files:, spec_selector:, related_spec_heuristic:,
7
+ related_specs_heuristic_enabled:, fallback_to_full_suite:, warner:)
8
+ @test_files = test_files
9
+ @spec_selector = spec_selector
10
+ @related_spec_heuristic = related_spec_heuristic
11
+ @related_specs_heuristic_enabled = related_specs_heuristic_enabled
12
+ @fallback_to_full_suite = fallback_to_full_suite
13
+ @warner = warner
14
+ end
15
+
16
+ def call(mutation)
17
+ return @test_files if @test_files
18
+
19
+ resolved = Array(@spec_selector.call(mutation.file_path))
20
+ if resolved.empty?
21
+ @warner.call(mutation.file_path, fallback_to_full_suite: @fallback_to_full_suite)
22
+ return @fallback_to_full_suite ? ["spec"] : nil
23
+ end
24
+
25
+ return resolved unless @related_specs_heuristic_enabled
26
+
27
+ related = @related_spec_heuristic.call(mutation)
28
+ (resolved + related).uniq
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rspec"
4
+
5
+ class Evilution::Integration::RSpec::UnresolvedSpecWarner
6
+ def initialize
7
+ @warned = Set.new
8
+ end
9
+
10
+ def call(file_path, fallback_to_full_suite:)
11
+ return if @warned.include?(file_path)
12
+
13
+ @warned << file_path
14
+ action = fallback_to_full_suite ? "running full suite" : "marking mutation unresolved"
15
+ warn "[evilution] No matching spec found for #{file_path}, #{action}. " \
16
+ "Use --spec to specify the spec file."
17
+ end
18
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "stringio"
4
4
  require_relative "base"
5
- require_relative "crash_detector"
6
5
  require_relative "../spec_resolver"
7
6
  require_relative "../spec_selector"
8
7
  require_relative "../related_spec_heuristic"
@@ -11,262 +10,92 @@ require_relative "../integration"
11
10
 
12
11
  class Evilution::Integration::RSpec < Evilution::Integration::Base
13
12
  def self.baseline_runner
14
- lambda { |spec_file|
15
- require "rspec/core"
16
- spec_dir = File.expand_path("spec")
17
- $LOAD_PATH.unshift(spec_dir) unless $LOAD_PATH.include?(spec_dir)
18
- ::RSpec.reset
19
- status = ::RSpec::Core::Runner.run(
20
- ["--format", "progress", "--no-color", "--order", "defined", spec_file]
21
- )
22
- status.zero?
23
- }
13
+ BaselineRunner.new
24
14
  end
25
15
 
26
16
  def self.baseline_options
27
17
  { runner: baseline_runner }
28
18
  end
29
19
 
30
- def initialize(test_files: nil, hooks: nil, related_specs_heuristic: false, fallback_to_full_suite: false,
31
- spec_selector: nil, example_filter: nil)
32
- @test_files = test_files
33
- @rspec_loaded = false
34
- @spec_selector = spec_selector || Evilution::SpecSelector.new
35
- @related_spec_heuristic = Evilution::RelatedSpecHeuristic.new
36
- @related_specs_heuristic_enabled = related_specs_heuristic
37
- @fallback_to_full_suite = fallback_to_full_suite
38
- @example_filter = example_filter
39
- @crash_detector = nil
40
- @warned_files = Set.new
20
+ def initialize(
21
+ test_files: nil,
22
+ hooks: nil,
23
+ related_specs_heuristic: false,
24
+ fallback_to_full_suite: false,
25
+ spec_selector: nil,
26
+ example_filter: nil,
27
+ framework_loader: FrameworkLoader.new,
28
+ test_file_resolver: nil,
29
+ example_filter_applier: nil,
30
+ crash_detector_lifecycle: CrashDetectorLifecycle.new,
31
+ result_builder: ResultBuilder.new,
32
+ state_guard: StateGuard.new
33
+ )
34
+ @framework_loader = framework_loader
35
+ @test_file_resolver = test_file_resolver || TestFileResolver.new(
36
+ test_files: test_files,
37
+ spec_selector: spec_selector || Evilution::SpecSelector.new,
38
+ related_spec_heuristic: Evilution::RelatedSpecHeuristic.new,
39
+ related_specs_heuristic_enabled: related_specs_heuristic,
40
+ fallback_to_full_suite: fallback_to_full_suite,
41
+ warner: UnresolvedSpecWarner.new
42
+ )
43
+ @example_filter_applier = example_filter_applier || build_example_filter_applier(example_filter)
44
+ @crash_detector_lifecycle = crash_detector_lifecycle
45
+ @result_builder = result_builder
46
+ @state_guard = state_guard
41
47
  super(hooks: hooks)
42
48
  end
43
49
 
44
50
  private
45
51
 
46
- attr_reader :test_files
52
+ def build_example_filter_applier(example_filter)
53
+ return ExampleFilterApplier::Identity.new unless example_filter
54
+
55
+ ExampleFilterApplier::Custom.new(example_filter)
56
+ end
47
57
 
48
58
  def ensure_framework_loaded
49
- return if @rspec_loaded
59
+ return if @framework_loader.loaded?
50
60
 
51
61
  fire_hook(:setup_integration_pre, integration: :rspec)
52
- require "rspec/core"
53
- add_spec_load_path
54
- Evilution::Integration::CrashDetector.register_with_rspec
55
- @rspec_loaded = true
62
+ @framework_loader.call
56
63
  fire_hook(:setup_integration_post, integration: :rspec)
57
- rescue LoadError => e
58
- raise Evilution::Error, "rspec-core is required but not available: #{e.message}"
59
64
  end
60
65
 
61
66
  def run_tests(mutation)
62
- reset_state
67
+ files = @test_file_resolver.call(mutation)
68
+ return @result_builder.unresolved(mutation) if files.nil?
63
69
 
64
- files = resolve_test_files(mutation)
65
- return unresolved_result(mutation) if files.nil?
70
+ targets = @example_filter_applier.call(mutation, files)
71
+ return @result_builder.unresolved_example(mutation) if targets.nil?
66
72
 
67
- targets = apply_example_filter(mutation, files)
68
- return unresolved_example_result(mutation) if targets.nil?
69
-
70
- out = StringIO.new
71
- err = StringIO.new
72
- args = build_args(targets)
73
+ args = ["--format", "progress", "--no-color", "--order", "defined", *targets]
73
74
  command = "rspec #{args.join(" ")}"
74
75
 
75
- detector = reset_crash_detector
76
- eg_before = snapshot_example_groups
77
- fe_before = snapshot_filtered_examples_keys
78
- rep_before = snapshot_reporter_lengths
79
- status = ::RSpec::Core::Runner.run(args, out, err)
80
-
81
- build_rspec_result(status, command, detector)
82
- rescue StandardError => e
83
- { passed: false, error: e.message, test_command: command }
84
- ensure
85
- release_rspec_state(eg_before)
86
- release_filtered_examples(fe_before)
87
- release_reporter_state(rep_before)
88
- end
89
-
90
- def build_args(files)
91
- ["--format", "progress", "--no-color", "--order", "defined", *files]
92
- end
93
-
94
- def apply_example_filter(mutation, files)
95
- return files unless @example_filter
96
-
97
- @example_filter.call(mutation, files)
98
- end
99
-
100
- def unresolved_result(mutation)
101
- {
102
- passed: false,
103
- unresolved: true,
104
- error: "no matching spec resolved for #{mutation.file_path}",
105
- test_command: "rspec (skipped: no spec resolved for #{mutation.file_path})"
106
- }
107
- end
108
-
109
- def unresolved_example_result(mutation)
110
- {
111
- passed: false,
112
- unresolved: true,
113
- error: "no matching example found for #{mutation.file_path}",
114
- test_command: "rspec (skipped: no matching example for #{mutation.file_path})"
115
- }
116
- end
117
-
118
- def reset_state
119
- if ::RSpec.respond_to?(:clear_examples)
120
- ::RSpec.clear_examples
121
- else
122
- ::RSpec.reset
123
- end
124
- end
125
-
126
- def snapshot_example_groups
127
- groups = Set.new
128
- ObjectSpace.each_object(Class) do |klass|
129
- groups << klass.object_id if klass < ::RSpec::Core::ExampleGroup
130
- rescue TypeError # rubocop:disable Lint/SuppressedException
131
- end
132
- groups
133
- end
134
-
135
- def release_rspec_state(eg_before)
136
- release_example_groups(eg_before)
137
- ::RSpec::ExampleGroups.remove_all_constants if defined?(::RSpec::ExampleGroups)
138
- release_world_example_groups
139
- end
140
-
141
- def release_example_groups(eg_before)
142
- return unless eg_before
143
-
144
- ObjectSpace.each_object(Class) do |klass|
145
- next unless klass < ::RSpec::Core::ExampleGroup
146
- next if eg_before.include?(klass.object_id)
147
-
148
- klass.constants(false).each do |const|
149
- klass.send(:remove_const, const)
150
- rescue NameError # rubocop:disable Lint/SuppressedException
151
- end
152
-
153
- klass.instance_variables.each do |ivar|
154
- klass.remove_instance_variable(ivar)
155
- end
156
- rescue TypeError # rubocop:disable Lint/SuppressedException
157
- end
158
- end
159
-
160
- def release_world_example_groups
161
- world = ::RSpec.world
162
- world.instance_variable_get(:@example_groups).clear if world.instance_variable_defined?(:@example_groups)
163
- world.instance_variable_set(:@sources_by_path, {}) if world.instance_variable_defined?(:@sources_by_path)
164
- end
165
-
166
- def snapshot_filtered_examples_keys
167
- fe = rspec_world_ivar(:@filtered_examples)
168
- fe ? Set.new(fe.keys.map(&:object_id)) : nil
169
- end
170
-
171
- def snapshot_reporter_lengths
172
- reporter = rspec_config_ivar(:@reporter)
173
- return nil unless reporter
174
-
175
- %i[@examples @failed_examples @pending_examples].each_with_object({}) do |ivar, acc|
176
- next unless reporter.instance_variable_defined?(ivar)
177
-
178
- arr = reporter.instance_variable_get(ivar)
179
- acc[ivar] = arr.length if arr.is_a?(Array)
180
- end
181
- end
182
-
183
- def release_filtered_examples(snapshot_keys)
184
- fe = rspec_world_ivar(:@filtered_examples)
185
- return unless fe && snapshot_keys
186
-
187
- fe.each_key.to_a.each do |k|
188
- fe.delete(k) unless snapshot_keys.include?(k.object_id)
189
- end
190
- end
191
-
192
- def release_reporter_state(lengths)
193
- return unless lengths
194
-
195
- reporter = rspec_config_ivar(:@reporter)
196
- return unless reporter
197
-
198
- lengths.each do |ivar, length|
199
- arr = reporter.instance_variable_get(ivar)
200
- arr.slice!(length..) if arr.is_a?(Array) && arr.length > length
201
- end
202
- end
203
-
204
- def rspec_world_ivar(ivar)
205
- world = ::RSpec.world
206
- world.instance_variable_defined?(ivar) ? world.instance_variable_get(ivar) : nil
207
- end
208
-
209
- def rspec_config_ivar(ivar)
210
- config = ::RSpec.configuration
211
- config.instance_variable_defined?(ivar) ? config.instance_variable_get(ivar) : nil
212
- end
213
-
214
- def reset_crash_detector
215
- if @crash_detector
216
- @crash_detector.reset
217
- else
218
- @crash_detector = Evilution::Integration::CrashDetector.new(StringIO.new)
219
- ::RSpec.configuration.add_formatter(@crash_detector)
220
- end
221
- @crash_detector
222
- end
223
-
224
- def build_rspec_result(status, command, detector)
225
- if status.zero?
226
- { passed: true, test_command: command }
227
- elsif detector.only_crashes?
228
- classes = detector.unique_crash_classes
229
- {
230
- passed: false,
231
- test_crashed: true,
232
- error: "test crashes: #{detector.crash_summary}",
233
- error_class: (classes.first if classes.length == 1),
234
- test_command: command
235
- }
236
- else
237
- { passed: false, test_command: command }
238
- end
239
- end
240
-
241
- def resolve_test_files(mutation)
242
- return test_files if test_files
243
-
244
- resolved = Array(@spec_selector.call(mutation.file_path))
245
- if resolved.empty?
246
- warn_unresolved_spec(mutation.file_path)
247
- return @fallback_to_full_suite ? ["spec"] : nil
76
+ reset_examples
77
+ detector = @crash_detector_lifecycle.current
78
+ snapshot = @state_guard.snapshot
79
+ begin
80
+ status = ::RSpec::Core::Runner.run(args, StringIO.new, StringIO.new)
81
+ @result_builder.from_run(status, command, detector)
82
+ rescue StandardError => e
83
+ { passed: false, error: e.message, test_command: command }
84
+ ensure
85
+ @state_guard.release(snapshot)
248
86
  end
249
-
250
- return resolved unless @related_specs_heuristic_enabled
251
-
252
- related = @related_spec_heuristic.call(mutation)
253
- (resolved + related).uniq
254
87
  end
255
88
 
256
- def warn_unresolved_spec(file_path)
257
- return if @warned_files.include?(file_path)
258
-
259
- @warned_files << file_path
260
- action = @fallback_to_full_suite ? "running full suite" : "marking mutation unresolved"
261
- warn "[evilution] No matching spec found for #{file_path}, #{action}. " \
262
- "Use --spec to specify the spec file."
263
- end
264
-
265
- # RSpec's CLI adds spec/ to $LOAD_PATH so that `--require spec_helper`
266
- # (commonly in .rspec) resolves. We call Runner.run directly, bypassing
267
- # the CLI, so we must replicate this.
268
- def add_spec_load_path
269
- spec_dir = File.expand_path("spec")
270
- $LOAD_PATH.unshift(spec_dir) unless $LOAD_PATH.include?(spec_dir)
89
+ def reset_examples
90
+ ::RSpec.respond_to?(:clear_examples) ? ::RSpec.clear_examples : ::RSpec.reset
271
91
  end
272
92
  end
93
+
94
+ require_relative "rspec/framework_loader"
95
+ require_relative "rspec/test_file_resolver"
96
+ require_relative "rspec/unresolved_spec_warner"
97
+ require_relative "rspec/example_filter_applier"
98
+ require_relative "rspec/crash_detector_lifecycle"
99
+ require_relative "rspec/result_builder"
100
+ require_relative "rspec/baseline_runner"
101
+ require_relative "rspec/state_guard"
@@ -4,6 +4,7 @@ require "fileutils"
4
4
  require "tmpdir"
5
5
  require_relative "../memory"
6
6
  require_relative "../temp_dir_tracker"
7
+ require_relative "../child_output"
7
8
 
8
9
  require_relative "../isolation"
9
10
 
@@ -59,8 +60,12 @@ class Evilution::Isolation::Fork
59
60
  end
60
61
 
61
62
  def suppress_child_output
62
- $stdout.reopen(File::NULL, "w")
63
- $stderr.reopen(File::NULL, "w")
63
+ if Evilution::ChildOutput.log_dir
64
+ Evilution::ChildOutput.redirect!
65
+ else
66
+ $stdout.reopen(File::NULL, "w")
67
+ $stderr.reopen(File::NULL, "w")
68
+ end
64
69
  end
65
70
 
66
71
  def execute_in_child(mutation, test_command)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../actions"
4
+ require_relative "../response_formatter"
5
+
6
+ class Evilution::MCP::InfoTool::Actions::Base
7
+ def self.call(**)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ class << self
12
+ private
13
+
14
+ def success(payload)
15
+ Evilution::MCP::InfoTool::ResponseFormatter.success(payload)
16
+ end
17
+
18
+ def config_error(message)
19
+ Evilution::MCP::InfoTool::ResponseFormatter.error("config_error", message)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../../../config"
5
+ require_relative "../../../version"
6
+
7
+ class Evilution::MCP::InfoTool::Actions::Environment < Evilution::MCP::InfoTool::Actions::Base
8
+ def self.call(**)
9
+ config = Evilution::Config.new(skip_config_file: false)
10
+ config_file = Evilution::Config::CONFIG_FILES.find { |path| File.exist?(path) }
11
+
12
+ success(
13
+ "version" => Evilution::VERSION,
14
+ "ruby" => RUBY_VERSION,
15
+ "config_file" => config_file,
16
+ "settings" => settings(config)
17
+ )
18
+ end
19
+
20
+ class << self
21
+ private
22
+
23
+ def settings(config)
24
+ {
25
+ "timeout" => config.timeout,
26
+ "format" => config.format,
27
+ "integration" => config.integration,
28
+ "jobs" => config.jobs,
29
+ "isolation" => config.isolation,
30
+ "baseline" => config.baseline,
31
+ "incremental" => config.incremental,
32
+ "fail_fast" => config.fail_fast,
33
+ "min_score" => config.min_score,
34
+ "suggest_tests" => config.suggest_tests,
35
+ "save_session" => config.save_session,
36
+ "target" => config.target,
37
+ "skip_heredoc_literals" => config.skip_heredoc_literals,
38
+ "ignore_patterns" => config.ignore_patterns
39
+ }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../../../version"
5
+ require_relative "../../../feedback"
6
+ require_relative "../../../feedback/messages"
7
+
8
+ class Evilution::MCP::InfoTool::Actions::Feedback < Evilution::MCP::InfoTool::Actions::Base
9
+ def self.call(**)
10
+ success(
11
+ "discussion_url" => Evilution::Feedback::DISCUSSION_URL,
12
+ "version" => Evilution::VERSION,
13
+ "guidance_for_agent" => Evilution::Feedback::Messages.info_guidance
14
+ )
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../status_glossary"
5
+
6
+ class Evilution::MCP::InfoTool::Actions::Statuses < Evilution::MCP::InfoTool::Actions::Base
7
+ def self.call(**)
8
+ success("statuses" => Evilution::MCP::InfoTool::StatusGlossary.entries)
9
+ end
10
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../config_factory"
5
+ require_relative "../../../runner"
6
+ require_relative "../../../mutator/registry"
7
+ require_relative "../../../ast/pattern/filter"
8
+
9
+ class Evilution::MCP::InfoTool::Actions::Subjects < Evilution::MCP::InfoTool::Actions::Base
10
+ def self.call(files: nil, line_ranges: nil, target: nil, integration: nil, skip_config: nil, **)
11
+ return config_error("files is required") if files.nil? || files.empty?
12
+
13
+ config = Evilution::MCP::InfoTool::ConfigFactory.subjects(
14
+ files: files, line_ranges: line_ranges,
15
+ target: target, integration: integration, skip_config: skip_config
16
+ )
17
+ runner = Evilution::Runner.new(config: config)
18
+ subjects = runner.parse_and_filter_subjects
19
+
20
+ registry = Evilution::Mutator::Registry.default
21
+ filter = build_subject_filter(config)
22
+ operator_options = { skip_heredoc_literals: config.skip_heredoc_literals? }
23
+
24
+ entries = subjects.map do |subj|
25
+ count = registry.mutations_for(subj, filter: filter, operator_options: operator_options).length
26
+ { "name" => subj.name, "file" => subj.file_path, "line" => subj.line_number, "mutations" => count }
27
+ ensure
28
+ subj.release_node!
29
+ end
30
+
31
+ success(
32
+ "subjects" => entries,
33
+ "total_subjects" => entries.length,
34
+ "total_mutations" => entries.sum { |e| e["mutations"] }
35
+ )
36
+ end
37
+
38
+ class << self
39
+ private
40
+
41
+ def build_subject_filter(config)
42
+ return nil if config.ignore_patterns.empty?
43
+
44
+ Evilution::AST::Pattern::Filter.new(config.ignore_patterns)
45
+ end
46
+ end
47
+ end