evilution 1.1.0 → 1.3.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 (161) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +58 -0
  3. data/.rubocop_todo.yml +9 -0
  4. data/CHANGELOG.md +86 -0
  5. data/README.md +202 -14
  6. data/docs/architecture.md +54 -5
  7. data/docs/isolation.md +3 -4
  8. data/exe/evil +8 -1
  9. data/exe/evilution +8 -1
  10. data/lib/evilution/ast/local_reads.rb +43 -0
  11. data/lib/evilution/ast/parser.rb +1 -0
  12. data/lib/evilution/ast/pattern/filter.rb +1 -0
  13. data/lib/evilution/ast/pattern/parser.rb +1 -0
  14. data/lib/evilution/ast/pattern.rb +2 -0
  15. data/lib/evilution/ast/sorbet_sig_detector.rb +1 -0
  16. data/lib/evilution/baseline.rb +37 -15
  17. data/lib/evilution/child_output.rb +1 -1
  18. data/lib/evilution/cli/command.rb +1 -0
  19. data/lib/evilution/cli/commands/tests_list.rb +4 -3
  20. data/lib/evilution/cli/commands.rb +2 -0
  21. data/lib/evilution/cli/dispatcher.rb +2 -0
  22. data/lib/evilution/cli/exit_guard.rb +61 -0
  23. data/lib/evilution/cli/parsed_args.rb +2 -0
  24. data/lib/evilution/cli/parser/command_extractor.rb +2 -0
  25. data/lib/evilution/cli/parser/file_args.rb +2 -0
  26. data/lib/evilution/cli/parser/options_builder.rb +4 -0
  27. data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
  28. data/lib/evilution/cli/parser.rb +1 -0
  29. data/lib/evilution/cli/printers/tests_list.rb +5 -4
  30. data/lib/evilution/cli/printers.rb +2 -0
  31. data/lib/evilution/cli/result.rb +2 -0
  32. data/lib/evilution/cli.rb +6 -0
  33. data/lib/evilution/config/builders.rb +2 -0
  34. data/lib/evilution/config/env_loader.rb +2 -0
  35. data/lib/evilution/config/file_loader.rb +1 -0
  36. data/lib/evilution/config/sources.rb +1 -0
  37. data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
  38. data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
  39. data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
  40. data/lib/evilution/config/validators/fail_fast.rb +1 -0
  41. data/lib/evilution/config/validators/hooks.rb +1 -0
  42. data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
  43. data/lib/evilution/config/validators/integration.rb +1 -0
  44. data/lib/evilution/config/validators/isolation.rb +1 -0
  45. data/lib/evilution/config/validators/jobs.rb +1 -0
  46. data/lib/evilution/config/validators/preload.rb +1 -0
  47. data/lib/evilution/config/validators/profile.rb +1 -0
  48. data/lib/evilution/config/validators/spec_mappings.rb +1 -0
  49. data/lib/evilution/config/validators/spec_pattern.rb +1 -0
  50. data/lib/evilution/config/validators/warmup.rb +17 -0
  51. data/lib/evilution/config/validators.rb +2 -0
  52. data/lib/evilution/config.rb +7 -5
  53. data/lib/evilution/coverage.rb +1 -1
  54. data/lib/evilution/coverage_example_filter.rb +1 -1
  55. data/lib/evilution/diagnostic.rb +22 -0
  56. data/lib/evilution/example_filter.rb +1 -1
  57. data/lib/evilution/integration/loading/body_call_neutralizer.rb +10 -2
  58. data/lib/evilution/integration/loading/concern_state_cleaner.rb +20 -4
  59. data/lib/evilution/integration/loading/reeval_warning_filter.rb +72 -0
  60. data/lib/evilution/integration/loading/source_evaluator.rb +4 -1
  61. data/lib/evilution/integration/loading/test_load_path.rb +21 -13
  62. data/lib/evilution/integration/minitest.rb +2 -1
  63. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +9 -1
  64. data/lib/evilution/integration/rspec/state_guard/configuration_state.rb +1 -0
  65. data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +5 -1
  66. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
  67. data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
  68. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
  69. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
  70. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
  71. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
  72. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
  73. data/lib/evilution/integration/rspec/state_guard.rb +6 -0
  74. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +2 -1
  75. data/lib/evilution/integration/rspec.rb +48 -2
  76. data/lib/evilution/integration/test_unit/test_file_resolver.rb +2 -1
  77. data/lib/evilution/isolation/fork.rb +29 -8
  78. data/lib/evilution/mcp/info_tool/actions/environment.rb +1 -0
  79. data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
  80. data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
  81. data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
  82. data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
  83. data/lib/evilution/mutation.rb +1 -1
  84. data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
  85. data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
  86. data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
  87. data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
  88. data/lib/evilution/mutator/operator/bang_method.rb +11 -3
  89. data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
  90. data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
  91. data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
  92. data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
  93. data/lib/evilution/mutator/operator/block_destructuring_expansion.rb +85 -0
  94. data/lib/evilution/mutator/operator/block_parameter_drop.rb +96 -0
  95. data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
  96. data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
  97. data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
  98. data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
  99. data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
  100. data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
  101. data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
  102. data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
  103. data/lib/evilution/mutator/operator/forwarding_super_to_explicit.rb +71 -0
  104. data/lib/evilution/mutator/operator/inequality_to_negated_identity.rb +43 -0
  105. data/lib/evilution/mutator/operator/keyword_argument_removal.rb +47 -0
  106. data/lib/evilution/mutator/operator/method_body_replacement.rb +10 -1
  107. data/lib/evilution/mutator/operator/method_body_to_raise.rb +59 -0
  108. data/lib/evilution/mutator/operator/method_body_to_super.rb +150 -0
  109. data/lib/evilution/mutator/operator/optional_default_injection.rb +71 -0
  110. data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +41 -0
  111. data/lib/evilution/mutator/operator/proc_to_lambda.rb +43 -0
  112. data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
  113. data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
  114. data/lib/evilution/mutator/operator/regexp_anchor_to_predicate.rb +136 -0
  115. data/lib/evilution/mutator/operator/safe_navigation_removal.rb +67 -0
  116. data/lib/evilution/mutator/operator/send_mutation.rb +42 -6
  117. data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
  118. data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
  119. data/lib/evilution/mutator/operator/typed_default_return.rb +84 -0
  120. data/lib/evilution/mutator/primitives.rb +25 -0
  121. data/lib/evilution/mutator/registry.rb +32 -1
  122. data/lib/evilution/parallel/pool.rb +1 -0
  123. data/lib/evilution/parallel_db_warning.rb +1 -1
  124. data/lib/evilution/process_supervisor.rb +20 -8
  125. data/lib/evilution/rails_warmup.rb +61 -0
  126. data/lib/evilution/reporter/cli/item_formatters/neutral_group.rb +23 -0
  127. data/lib/evilution/reporter/cli/item_formatters/subject_score.rb +42 -0
  128. data/lib/evilution/reporter/cli/item_formatters/subject_score_group.rb +16 -0
  129. data/lib/evilution/reporter/cli/line_formatters/infra_retry_notice.rb +19 -0
  130. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +27 -3
  131. data/lib/evilution/reporter/cli/line_formatters/score.rb +19 -1
  132. data/lib/evilution/reporter/cli/line_formatters/unresolved_targets.rb +35 -0
  133. data/lib/evilution/reporter/cli/metrics_block.rb +4 -0
  134. data/lib/evilution/reporter/cli/trailer.rb +11 -7
  135. data/lib/evilution/reporter/cli.rb +20 -4
  136. data/lib/evilution/reporter/html/report.rb +1 -0
  137. data/lib/evilution/reporter/json/subjects.rb +29 -0
  138. data/lib/evilution/reporter/json.rb +23 -1
  139. data/lib/evilution/result/coverage_gap_grouper.rb +1 -0
  140. data/lib/evilution/result/mutation_result.rb +3 -2
  141. data/lib/evilution/result/neutral_reason.rb +35 -0
  142. data/lib/evilution/result/subject_score.rb +28 -0
  143. data/lib/evilution/result/subject_scorer.rb +38 -0
  144. data/lib/evilution/result/summary.rb +44 -2
  145. data/lib/evilution/runner/baseline_runner.rb +17 -7
  146. data/lib/evilution/runner/canary.rb +52 -5
  147. data/lib/evilution/runner/isolation_resolver.rb +23 -2
  148. data/lib/evilution/runner/mutation_executor/infra_retry.rb +54 -0
  149. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +23 -10
  150. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +18 -1
  151. data/lib/evilution/runner/mutation_executor/result_cache.rb +11 -0
  152. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +19 -1
  153. data/lib/evilution/runner/mutation_executor.rb +32 -4
  154. data/lib/evilution/runner/report_publisher.rb +31 -9
  155. data/lib/evilution/runner/target_spec_audit.rb +43 -0
  156. data/lib/evilution/runner.rb +10 -1
  157. data/lib/evilution/source_ast_cache.rb +1 -1
  158. data/lib/evilution/spec_ast_cache.rb +1 -1
  159. data/lib/evilution/version.rb +1 -1
  160. data/lib/evilution.rb +35 -0
  161. metadata +50 -2
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../pattern"
3
4
  require_relative "matcher"
4
5
 
5
6
  class Evilution::AST::Pattern::Parser
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../ast"
4
+
3
5
  module Evilution::AST::Pattern
4
6
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
+ require_relative "../ast"
4
5
 
5
6
  class Evilution::AST::SorbetSigDetector
6
7
  def call(source)
@@ -15,13 +15,24 @@ class Evilution::Baseline
15
15
  end
16
16
  end
17
17
 
18
+ # spec_selector: the object `run` resolves with (integration layout plus
19
+ # spec_mappings / spec_pattern), returning every spec covering a source.
20
+ # Without one, the bare spec_resolver is used. spec_resolver also supplies
21
+ # the best-guess hint for an unresolved source.
22
+ #
23
+ # fallback_to_full_suite: whether an unresolved source runs the whole
24
+ # fallback_dir. When false its mutations are reported unresolved, so the
25
+ # baseline skips it rather than running -- and often timing out on -- the
26
+ # entire test directory.
18
27
  def initialize(spec_resolver: Evilution::SpecResolver.new, timeout: 30, runner: nil,
19
- fallback_dir: "spec", test_files: nil)
28
+ fallback_dir: "spec", test_files: nil, spec_selector: nil, fallback_to_full_suite: true)
20
29
  @spec_resolver = spec_resolver
21
30
  @timeout = timeout
22
31
  @runner = runner
23
32
  @fallback_dir = fallback_dir
24
33
  @test_files = test_files
34
+ @spec_selector = spec_selector
35
+ @fallback_to_full_suite = fallback_to_full_suite
25
36
  end
26
37
 
27
38
  def call(subjects)
@@ -43,7 +54,7 @@ class Evilution::Baseline
43
54
  read_io, write_io = IO.pipe
44
55
  pid = fork_spec_runner(spec_file, read_io, write_io)
45
56
  write_io.close
46
- read_result(read_io, pid)
57
+ read_result(read_io, pid, spec_file)
47
58
  rescue Evilution::Error
48
59
  raise
49
60
  rescue StandardError
@@ -69,7 +80,7 @@ class Evilution::Baseline
69
80
 
70
81
  GRACE_PERIOD = 0.5
71
82
 
72
- def read_result(read_io, pid)
83
+ def read_result(read_io, pid, spec_file)
73
84
  if read_io.wait_readable(@timeout)
74
85
  data = read_io.read
75
86
  Process.wait(pid)
@@ -79,10 +90,15 @@ class Evilution::Baseline
79
90
  result[:passed]
80
91
  else
81
92
  terminate_child(pid)
93
+ warn_timeout(spec_file)
82
94
  false
83
95
  end
84
96
  end
85
97
 
98
+ def warn_timeout(spec_file)
99
+ warn "[evilution] Baseline for #{spec_file} timed out after #{@timeout}s; treating it as failing."
100
+ end
101
+
86
102
  def terminate_child(pid)
87
103
  Evilution::ProcessCleanup.safe_kill("TERM", pid)
88
104
  _, status = Process.waitpid2(pid, Process::WNOHANG)
@@ -98,11 +114,6 @@ class Evilution::Baseline
98
114
 
99
115
  private
100
116
 
101
- # When --spec was provided, run those files only. Auto-discovery is skipped
102
- # entirely — the user has declared what covers their subjects and any
103
- # mismatch between auto-discovery and their declaration is what produced
104
- # the misleading "No matching test found" warning users have reported even
105
- # while passing --spec.
106
117
  def baseline_spec_files(subjects)
107
118
  return Array(@test_files).uniq if @test_files && !@test_files.empty?
108
119
 
@@ -111,20 +122,31 @@ class Evilution::Baseline
111
122
 
112
123
  def resolve_unique_spec_files(subjects)
113
124
  warned = Set.new
114
- subjects.map do |s|
115
- resolved = @spec_resolver.call(s.file_path)
116
- warn_no_matching_test(s.file_path) if resolved.nil? && warned.add?(s.file_path)
117
- resolved || @fallback_dir
125
+ subjects.flat_map do |s|
126
+ specs = specs_for(s.file_path)
127
+ next specs unless specs.empty?
128
+
129
+ warn_no_matching_test(s.file_path) if warned.add?(s.file_path)
130
+ @fallback_to_full_suite ? [@fallback_dir] : []
118
131
  end.uniq
119
132
  end
120
133
 
134
+ def specs_for(file_path)
135
+ Array(@spec_selector ? @spec_selector.call(file_path) : @spec_resolver.call(file_path))
136
+ end
137
+
121
138
  def warn_no_matching_test(file_path)
139
+ action = @fallback_to_full_suite ? "running full suite" : "marking its mutations unresolved"
140
+ warn "[evilution] No matching test found for #{file_path}, #{action}. #{no_match_hint(file_path)}"
141
+ end
142
+
143
+ def no_match_hint(file_path)
122
144
  suggestion = @spec_resolver.suggest(file_path)
123
145
  hint = if suggestion
124
- "Pass --spec #{suggestion} (best guess) or the correct test file."
146
+ "Pass --spec #{suggestion} (best guess) or the correct test file"
125
147
  else
126
- "Use --spec to specify the test file."
148
+ "Use --spec to specify the test file"
127
149
  end
128
- warn "[evilution] No matching test found for #{file_path}, running full suite. #{hint}"
150
+ @fallback_to_full_suite ? "#{hint}." : "#{hint}, or --fallback-full-suite to run the whole suite."
129
151
  end
130
152
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../evilution"
3
+ require_relative "version"
4
4
 
5
5
  module Evilution::ChildOutput
6
6
  module_function
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
3
4
  require_relative "result"
4
5
 
5
6
  class Evilution::CLI::Command
@@ -5,7 +5,6 @@ require_relative "../command"
5
5
  require_relative "../dispatcher"
6
6
  require_relative "../printers/tests_list"
7
7
  require_relative "../../config"
8
- require_relative "../../spec_resolver"
9
8
  require_relative "../../git/changed_files"
10
9
 
11
10
  class Evilution::CLI::Commands::TestsList < Evilution::CLI::Command
@@ -25,8 +24,10 @@ class Evilution::CLI::Commands::TestsList < Evilution::CLI::Command
25
24
  return 0
26
25
  end
27
26
 
28
- resolver = Evilution::SpecResolver.new
29
- entries = source_files.map { |source| { source: source, spec: resolver.call(source) } }
27
+ # The same selector `run` uses: the integration's test layout (test/*_test.rb
28
+ # for minitest and test-unit) plus spec_mappings and spec_pattern, so the
29
+ # two commands cannot disagree about which specs cover a source (GH #1596).
30
+ entries = source_files.map { |source| { source: source, specs: Array(config.spec_selector.call(source)) } }
30
31
  Evilution::CLI::Printers::TestsList.new(mode: :resolved, entries: entries).render(@stdout)
31
32
  0
32
33
  end
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
4
+
3
5
  module Evilution::CLI::Commands
4
6
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
4
+
3
5
  module Evilution::CLI::Dispatcher
4
6
  @commands = {}
5
7
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cli"
4
+
5
+ # Gives evilution the final word on the process exit status.
6
+ #
7
+ # `--preload` loads the project's own spec helper into the parent process, and
8
+ # whatever that helper installs comes along with it. SimpleCov's at_exit hook
9
+ # calls `exit` with its own status when coverage is below the configured
10
+ # minimum, which replaces the status evilution computed and breaks a CI step
11
+ # reading it (EV-g8ya / GH #1608).
12
+ #
13
+ # at_exit hooks run last-registered-first, so a hook installed before the
14
+ # preload is the last one standing. It exits with `exit!`, which is immediate
15
+ # and cannot be overridden by anything left in the queue — by then every other
16
+ # hook, evilution's own temp-directory cleanup included, has already run.
17
+ #
18
+ # With no status recorded, evilution did not finish normally: an exception is on
19
+ # its way out, and Ruby's own handling decides the status instead.
20
+ #
21
+ # The hook belongs to the process that installed it. Evilution forks workers,
22
+ # and a fork inherits its parent's at_exit hooks; a worker running this one
23
+ # would exit with the parent's status and skip its own ending. The guard
24
+ # therefore remembers its process and stands down anywhere else.
25
+ class Evilution::CLI::ExitGuard
26
+ attr_accessor :status
27
+
28
+ def initialize(register: nil, exiter: nil, pid_source: nil, streams: nil)
29
+ @register = register || ->(&hook) { at_exit(&hook) }
30
+ @exiter = exiter || ->(code) { exit!(code) }
31
+ @streams = streams || -> { [$stdout, $stderr] }
32
+ @pid_source = pid_source || -> { Process.pid }
33
+ @status = nil
34
+ end
35
+
36
+ def install
37
+ @pid = @pid_source.call
38
+ @register.call { fire }
39
+ self
40
+ end
41
+
42
+ private
43
+
44
+ def fire
45
+ return if @status.nil?
46
+ return unless @pid_source.call == @pid
47
+
48
+ flush_streams
49
+ @exiter.call(@status)
50
+ end
51
+
52
+ # `exit!` leaves buffered output where it stands, so anything written by the
53
+ # report, or by a hook that ran before this one, has to be pushed out first.
54
+ def flush_streams
55
+ @streams.call.each do |stream|
56
+ stream.flush
57
+ rescue IOError, Errno::EBADF
58
+ nil
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
4
+
3
5
  class Evilution::CLI
4
6
  ParsedArgs = Struct.new(
5
7
  :command, :options, :files, :line_ranges, :stdin_error, :parse_error,
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../parser"
4
+
3
5
  class Evilution::CLI::Parser::CommandExtractor
4
6
  SIMPLE_COMMANDS = {
5
7
  "version" => :version,
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../parser"
4
+
3
5
  module Evilution::CLI::Parser::FileArgs
4
6
  ParsedPaths = Data.define(:files, :ranges)
5
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "optparse"
4
+ require_relative "../parser"
4
5
  require_relative "../../version"
5
6
  require_relative "file_args"
6
7
 
@@ -55,6 +56,7 @@ class Evilution::CLI::Parser::OptionsBuilder
55
56
 
56
57
  def add_spec_filter_options(opts)
57
58
  opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
59
+ opts.on("--output FILE", "Write the report to FILE instead of stdout") { |f| @options[:output] = f }
58
60
  opts.on("--spec FILES", Array, "Spec files to run (comma-separated)") { |f| @options[:spec_files] = f }
59
61
  opts.on("--spec-dir DIR", "Include all specs in DIR") { |d| expand_spec_dir(d) }
60
62
  opts.on("--spec-pattern GLOB",
@@ -118,6 +120,8 @@ class Evilution::CLI::Parser::OptionsBuilder
118
120
  "(default: auto-detect spec/rails_helper.rb -> spec/spec_helper.rb -> " \
119
121
  "test/test_helper.rb for Rails projects)") { |f| @options[:preload] = f }
120
122
  opts.on("--no-preload", "Disable parent-process preload even for Rails projects") { @options[:preload] = false }
123
+ opts.on("--warmup NAME", "After preload, warm lazily-initialised framework state once " \
124
+ "in the parent: none, rails (default: none)") { |w| @options[:warmup] = w }
121
125
  end
122
126
 
123
127
  def add_output_options(opts)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../parser"
3
4
  require_relative "file_args"
4
5
 
5
6
  class Evilution::CLI::Parser::StdinReader
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
3
4
  require_relative "parsed_args"
4
5
 
5
6
  class Evilution::CLI::Parser
@@ -24,16 +24,17 @@ class Evilution::CLI::Printers::TestsList
24
24
  io.puts(@specs.length == 1 ? "1 spec file" : "#{@specs.length} spec files")
25
25
  end
26
26
 
27
+ # Each entry is `{ source:, specs: [...] }`; a source can resolve to several
28
+ # spec files (spec_mappings), and each gets its own line.
27
29
  def render_resolved(io)
28
30
  unique_specs = []
29
31
  @entries.each do |entry|
30
32
  source = entry[:source]
31
- spec = entry[:spec]
32
- if spec
33
+ specs = entry[:specs]
34
+ io.puts(" #{source} (no spec found)") if specs.empty?
35
+ specs.each do |spec|
33
36
  unique_specs << spec
34
37
  io.puts(" #{spec} (#{source})")
35
- else
36
- io.puts(" #{source} (no spec found)")
37
38
  end
38
39
  end
39
40
 
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
4
+
3
5
  module Evilution::CLI::Printers
4
6
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
4
+
3
5
  class Evilution::CLI
4
6
  Result = Struct.new(:exit_code, :error, :error_rendered, keyword_init: true) do
5
7
  def initialize(exit_code:, error: nil, error_rendered: false)
data/lib/evilution/cli.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "version"
4
+
5
+ # Declared before the children are required: each child requires this file
6
+ # and nests under Evilution::CLI, so the namespace must exist first.
7
+ class Evilution::CLI; end # rubocop:disable Lint/EmptyClass
8
+
4
9
  require_relative "cli/parser"
5
10
  require_relative "cli/parsed_args"
6
11
  require_relative "cli/printers/subjects"
@@ -18,6 +23,7 @@ require_relative "cli/commands/session_diff"
18
23
  require_relative "cli/commands/session_gc"
19
24
  require_relative "cli/commands/compare"
20
25
  require_relative "cli/commands/run"
26
+ require_relative "cli/exit_guard"
21
27
 
22
28
  # @api private
23
29
  class Evilution::CLI
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../config"
4
+
3
5
  module Evilution::Config::Builders
4
6
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../config"
4
+
3
5
  module Evilution::Config::EnvLoader
4
6
  module_function
5
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
+ require_relative "../config"
4
5
 
5
6
  module Evilution::Config::FileLoader
6
7
  module_function
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../config"
3
4
  require_relative "file_loader"
4
5
  require_relative "env_loader"
5
6
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::ExampleTargetingCache < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::ExampleTargetingFallback < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::ExampleTargetingStrategy < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::FailFast < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Hooks < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::IgnorePatterns < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Integration < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Isolation < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Jobs < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Preload < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Profile < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::SpecMappings < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::SpecPattern < Evilution::Config::Validators::Base
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../validators"
4
+ require_relative "base"
5
+
6
+ # `warmup: rails` warms Rails' lazily-initialised state in the parent after
7
+ # the preload; `none` (the default) skips it. `false` is accepted as `none`
8
+ # so `warmup: false` reads naturally in .evilution.yml.
9
+ class Evilution::Config::Validators::Warmup < Evilution::Config::Validators::Base
10
+ ALLOWED = %i[none rails].freeze
11
+
12
+ def self.call(value)
13
+ return :none if value == false
14
+
15
+ coerce_symbol!(value, allowed: ALLOWED, name: "warmup")
16
+ end
17
+ end
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../config"
4
+
3
5
  module Evilution::Config::Validators
4
6
  end
@@ -17,15 +17,15 @@ class Evilution::Config
17
17
  save_session: false, line_ranges: {}, spec_files: [], ignore_patterns: [],
18
18
  show_disabled: false, baseline_session: nil, skip_heredoc_literals: false,
19
19
  related_specs_heuristic: false, fallback_to_full_suite: false, preload: nil,
20
- spec_mappings: {}, spec_pattern: nil, example_targeting: true,
20
+ output: nil, spec_mappings: {}, spec_pattern: nil, example_targeting: true,
21
21
  example_targeting_fallback: :full_file, example_targeting_strategy: :lexical,
22
22
  example_targeting_cache: { max_files: 50, max_blocks: 10_000 },
23
23
  quiet_children: false, quiet_children_dir: "tmp/evilution_children",
24
- profile: :default, canary: true
24
+ profile: :default, canary: true, warmup: :none
25
25
  }.freeze
26
26
 
27
27
  attr_reader :target_files, :schema_version, :timeout, :format,
28
- :target, :min_score, :integration, :verbose, :quiet,
28
+ :target, :min_score, :integration, :verbose, :quiet, :output,
29
29
  :jobs, :fail_fast, :baseline, :isolation, :incremental, :suggest_tests,
30
30
  :progress, :save_session, :line_ranges, :spec_files, :hooks,
31
31
  :ignore_patterns, :show_disabled, :baseline_session,
@@ -33,7 +33,7 @@ class Evilution::Config
33
33
  :fallback_to_full_suite, :preload, :spec_mappings, :spec_pattern,
34
34
  :example_targeting, :example_targeting_fallback, :example_targeting_strategy,
35
35
  :example_targeting_cache,
36
- :spec_selector, :quiet_children, :quiet_children_dir, :profile, :canary
36
+ :spec_selector, :quiet_children, :quiet_children_dir, :profile, :canary, :warmup
37
37
 
38
38
  def initialize(**options)
39
39
  skip_file = options.delete(:skip_config_file) ? true : false
@@ -243,6 +243,7 @@ class Evilution::Config
243
243
  format: :to_sym.to_proc,
244
244
  target: nil,
245
245
  min_score: :to_f.to_proc,
246
+ output: nil,
246
247
  verbose: nil,
247
248
  quiet: nil,
248
249
  baseline: nil,
@@ -273,7 +274,7 @@ class Evilution::Config
273
274
 
274
275
  VALIDATED_ATTRS = %i[
275
276
  integration jobs fail_fast isolation ignore_patterns
276
- hooks preload spec_mappings spec_pattern profile
277
+ hooks preload spec_mappings spec_pattern profile warmup
277
278
  ].freeze
278
279
  private_constant :VALIDATED_ATTRS
279
280
 
@@ -310,6 +311,7 @@ require_relative "config/validators/example_targeting_fallback"
310
311
  require_relative "config/validators/example_targeting_strategy"
311
312
  require_relative "config/validators/example_targeting_cache"
312
313
  require_relative "config/validators/profile"
314
+ require_relative "config/validators/warmup"
313
315
  require_relative "config/builders"
314
316
  require_relative "config/builders/spec_resolver"
315
317
  require_relative "config/builders/spec_selector"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../evilution"
3
+ require_relative "version"
4
4
 
5
5
  # Per-example line-coverage support: build a `source file:line -> [examples]`
6
6
  # map so mutation targeting can run exactly the examples that execute a line.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../evilution"
3
+ require_relative "version"
4
4
  require_relative "coverage/map"
5
5
 
6
6
  # Per-mutation example targeting backed by a real line-coverage Map (EV-ndjd).
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version"
4
+
5
+ # Evilution's own advisory messages -- "no matching spec found", and the like.
6
+ #
7
+ # These deliberately bypass Kernel#warn. That routes through Warning.warn,
8
+ # which a project is free to replace: the `warning` gem with a raising handler
9
+ # turns every Ruby warning into an exception, and both dry-schema and
10
+ # dry-monads configure exactly that in their spec_helper. Under such a project
11
+ # our advisory message would be promoted to a fatal error, so a mutation we
12
+ # meant to classify :unresolved would be reported :error instead -- 239 of
13
+ # dry-monads' 240 mutations, in the run that found this.
14
+ #
15
+ # A message about how evilution is behaving must never be able to fail a
16
+ # user's mutation. Writing straight to the stream keeps it a diagnostic.
17
+ # EV-df7u / GH #1588.
18
+ module Evilution::Diagnostic
19
+ def self.warn(message, stream: $stderr)
20
+ stream.puts(message)
21
+ end
22
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
- require_relative "../evilution"
4
+ require_relative "version"
5
5
  require_relative "spec_ast_cache"
6
6
 
7
7
  class Evilution::ExampleFilter