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.
- checksums.yaml +4 -4
- data/.beads/interactions.jsonl +23 -0
- data/.rubocop_todo.yml +6 -0
- data/CHANGELOG.md +54 -0
- data/README.md +76 -3
- data/lib/evilution/baseline.rb +5 -4
- 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 +23 -2
- data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
- data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
- data/lib/evilution/compare/diff_extractor.rb +6 -0
- data/lib/evilution/compare/fingerprint.rb +15 -72
- data/lib/evilution/compare/line_normalizer.rb +72 -0
- data/lib/evilution/compare/normalizer.rb +17 -4
- 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/profile.rb +11 -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 +93 -266
- 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/crash_detector.rb +2 -2
- data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
- data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
- 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 +43 -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 +23 -13
- data/lib/evilution/isolation/in_process.rb +10 -6
- 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 -263
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
- data/lib/evilution/mcp/mutate_tool.rb +5 -2
- data/lib/evilution/mcp/session_tool.rb +0 -2
- data/lib/evilution/mutation.rb +47 -27
- data/lib/evilution/mutator/base.rb +8 -8
- data/lib/evilution/mutator/operator/block_removal.rb +1 -1
- data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
- data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
- data/lib/evilution/mutator/registry.rb +20 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -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/process_cleanup.rb +19 -0
- 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/reporter/html/baseline_keys.rb +1 -1
- data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
- data/lib/evilution/reporter/html/escape.rb +1 -1
- data/lib/evilution/reporter/html/section.rb +1 -1
- data/lib/evilution/reporter/html/sections.rb +4 -2
- data/lib/evilution/reporter/html/stylesheet.rb +1 -1
- data/lib/evilution/reporter/html.rb +8 -3
- data/lib/evilution/reporter/suggestion/registry.rb +1 -5
- data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
- data/lib/evilution/reporter/suggestion/templates.rb +6 -0
- data/lib/evilution/result/error_info.rb +20 -0
- data/lib/evilution/result/memory_stats.rb +20 -0
- data/lib/evilution/result/mutation_result.rb +30 -14
- data/lib/evilution/runner/baseline_runner.rb +1 -2
- data/lib/evilution/runner/diagnostics.rb +1 -2
- data/lib/evilution/runner/isolation_resolver.rb +10 -4
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +53 -292
- data/lib/evilution/runner/mutation_planner.rb +1 -2
- data/lib/evilution/runner/report_publisher.rb +1 -2
- data/lib/evilution/runner/subject_pipeline.rb +1 -2
- data/lib/evilution/runner.rb +53 -30
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- data/script/memory_check +3 -1
- metadata +125 -3
- 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
|
-
|
|
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(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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 @
|
|
59
|
+
return if @framework_loader.loaded?
|
|
50
60
|
|
|
51
61
|
fire_hook(:setup_integration_pre, integration: :rspec)
|
|
52
|
-
|
|
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
|
-
|
|
67
|
+
files = @test_file_resolver.call(mutation)
|
|
68
|
+
return @result_builder.unresolved(mutation) if files.nil?
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
return
|
|
70
|
+
targets = @example_filter_applier.call(mutation, files)
|
|
71
|
+
return @result_builder.unresolved_example(mutation) if targets.nil?
|
|
66
72
|
|
|
67
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
257
|
-
|
|
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"
|