evilution 0.26.0 → 0.28.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 (159) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +23 -0
  3. data/.rubocop_todo.yml +6 -0
  4. data/CHANGELOG.md +54 -0
  5. data/README.md +76 -3
  6. data/lib/evilution/baseline.rb +5 -4
  7. data/lib/evilution/cache.rb +2 -0
  8. data/lib/evilution/child_output.rb +24 -0
  9. data/lib/evilution/cli/commands/run.rb +9 -0
  10. data/lib/evilution/cli/commands/version.rb +2 -0
  11. data/lib/evilution/cli/parser/options_builder.rb +23 -2
  12. data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
  13. data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
  14. data/lib/evilution/compare/diff_extractor.rb +6 -0
  15. data/lib/evilution/compare/fingerprint.rb +15 -72
  16. data/lib/evilution/compare/line_normalizer.rb +72 -0
  17. data/lib/evilution/compare/normalizer.rb +17 -4
  18. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  19. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  20. data/lib/evilution/config/builders.rb +4 -0
  21. data/lib/evilution/config/env_loader.rb +12 -0
  22. data/lib/evilution/config/file_loader.rb +22 -0
  23. data/lib/evilution/config/sources.rb +14 -0
  24. data/lib/evilution/config/validators/base.rb +37 -0
  25. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  26. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  27. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  28. data/lib/evilution/config/validators/hooks.rb +12 -0
  29. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  30. data/lib/evilution/config/validators/integration.rb +11 -0
  31. data/lib/evilution/config/validators/isolation.rb +19 -0
  32. data/lib/evilution/config/validators/jobs.rb +9 -0
  33. data/lib/evilution/config/validators/preload.rb +13 -0
  34. data/lib/evilution/config/validators/profile.rb +11 -0
  35. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  36. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  37. data/lib/evilution/config/validators.rb +4 -0
  38. data/lib/evilution/config.rb +93 -266
  39. data/lib/evilution/feedback/detector.rb +15 -0
  40. data/lib/evilution/feedback/messages.rb +42 -0
  41. data/lib/evilution/feedback.rb +5 -0
  42. data/lib/evilution/integration/crash_detector.rb +2 -2
  43. data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
  44. data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
  45. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  46. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  47. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  48. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  49. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  50. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  51. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  52. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
  53. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  54. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  55. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  56. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  57. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  58. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  59. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  60. data/lib/evilution/integration/rspec.rb +61 -232
  61. data/lib/evilution/isolation/fork.rb +23 -13
  62. data/lib/evilution/isolation/in_process.rb +10 -6
  63. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  64. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  65. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  66. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  67. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  68. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  69. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  70. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  71. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  72. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  73. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  74. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  75. data/lib/evilution/mcp/info_tool.rb +43 -263
  76. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  77. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
  78. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  79. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  80. data/lib/evilution/mcp/session_tool.rb +0 -2
  81. data/lib/evilution/mutation.rb +47 -27
  82. data/lib/evilution/mutator/base.rb +8 -8
  83. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  84. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  85. data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
  86. data/lib/evilution/mutator/registry.rb +20 -0
  87. data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -0
  88. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  89. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  90. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  91. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  92. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  93. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  94. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  95. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  96. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  97. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  98. data/lib/evilution/parallel/work_queue.rb +42 -327
  99. data/lib/evilution/process_cleanup.rb +19 -0
  100. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  101. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  102. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  103. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  104. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  105. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  106. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  107. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  108. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  109. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  110. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  111. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  112. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  113. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  114. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  115. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  116. data/lib/evilution/reporter/cli/pct.rb +9 -0
  117. data/lib/evilution/reporter/cli/section.rb +13 -0
  118. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  119. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  120. data/lib/evilution/reporter/cli.rb +79 -162
  121. data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
  122. data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
  123. data/lib/evilution/reporter/html/escape.rb +1 -1
  124. data/lib/evilution/reporter/html/section.rb +1 -1
  125. data/lib/evilution/reporter/html/sections.rb +4 -2
  126. data/lib/evilution/reporter/html/stylesheet.rb +1 -1
  127. data/lib/evilution/reporter/html.rb +8 -3
  128. data/lib/evilution/reporter/suggestion/registry.rb +1 -5
  129. data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
  130. data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
  131. data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
  132. data/lib/evilution/reporter/suggestion/templates.rb +6 -0
  133. data/lib/evilution/result/error_info.rb +20 -0
  134. data/lib/evilution/result/memory_stats.rb +20 -0
  135. data/lib/evilution/result/mutation_result.rb +30 -14
  136. data/lib/evilution/runner/baseline_runner.rb +1 -2
  137. data/lib/evilution/runner/diagnostics.rb +1 -2
  138. data/lib/evilution/runner/isolation_resolver.rb +10 -4
  139. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
  140. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
  141. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
  142. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
  143. data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
  144. data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
  145. data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
  146. data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
  147. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
  148. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
  149. data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
  150. data/lib/evilution/runner/mutation_executor.rb +53 -292
  151. data/lib/evilution/runner/mutation_planner.rb +1 -2
  152. data/lib/evilution/runner/report_publisher.rb +1 -2
  153. data/lib/evilution/runner/subject_pipeline.rb +1 -2
  154. data/lib/evilution/runner.rb +53 -30
  155. data/lib/evilution/version.rb +1 -1
  156. data/lib/evilution.rb +1 -0
  157. data/script/memory_check +3 -1
  158. metadata +125 -3
  159. data/lib/evilution/reporter/html/namespace.rb +0 -11
@@ -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,43 @@
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
13
+ # ObjectSpace iteration may surface partially-initialized or anonymous
14
+ # classes whose `<` comparison raises. Skipping them is safe — they
15
+ # cannot be ExampleGroup descendants we need to track.
16
+ end
17
+ groups
18
+ end
19
+
20
+ def release(eg_before)
21
+ return unless eg_before
22
+
23
+ ObjectSpace.each_object(Class) do |klass|
24
+ next unless klass < ::RSpec::Core::ExampleGroup
25
+ next if eg_before.include?(klass.object_id)
26
+
27
+ klass.constants(false).each do |const|
28
+ klass.send(:remove_const, const)
29
+ rescue NameError
30
+ # Constant may have been removed concurrently (e.g. via autoload
31
+ # reload) between #constants(false) and #remove_const. Best-effort
32
+ # cleanup — nothing to do if it's already gone.
33
+ end
34
+
35
+ klass.instance_variables.each do |ivar|
36
+ klass.remove_instance_variable(ivar)
37
+ end
38
+ rescue TypeError
39
+ # Same defensive case as #snapshot: skip classes whose `<` raises
40
+ # mid-iteration.
41
+ end
42
+ end
43
+ 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
@@ -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"