evilution 1.2.0 → 1.4.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 (213) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +100 -0
  3. data/.rubocop_todo.yml +14 -0
  4. data/CHANGELOG.md +124 -0
  5. data/README.md +142 -28
  6. data/docs/architecture.md +123 -9
  7. data/docs/ast_pattern_syntax.md +50 -5
  8. data/docs/isolation.md +39 -2
  9. data/docs/migration-from-mutant.md +1 -1
  10. data/lib/evilution/ast/aasm_declaration.rb +133 -0
  11. data/lib/evilution/ast/callback_declaration.rb +91 -0
  12. data/lib/evilution/ast/included_block.rb +27 -0
  13. data/lib/evilution/ast/literal_callable.rb +21 -0
  14. data/lib/evilution/ast/parser.rb +120 -16
  15. data/lib/evilution/ast/pattern/filter.rb +1 -0
  16. data/lib/evilution/ast/pattern/method_name.rb +63 -0
  17. data/lib/evilution/ast/pattern/parser.rb +15 -15
  18. data/lib/evilution/ast/pattern.rb +2 -0
  19. data/lib/evilution/ast/regexp_pattern.rb +104 -0
  20. data/lib/evilution/ast/scope_declaration.rb +45 -0
  21. data/lib/evilution/ast/sorbet_sig_detector.rb +1 -0
  22. data/lib/evilution/ast/uncovered_code.rb +88 -0
  23. data/lib/evilution/ast/value_object_definition.rb +25 -0
  24. data/lib/evilution/baseline/failure_formatter.rb +27 -0
  25. data/lib/evilution/baseline/report.rb +66 -0
  26. data/lib/evilution/baseline/spec_failure.rb +38 -0
  27. data/lib/evilution/baseline.rb +101 -42
  28. data/lib/evilution/child_output.rb +1 -1
  29. data/lib/evilution/cli/command.rb +1 -0
  30. data/lib/evilution/cli/commands/tests_list.rb +4 -3
  31. data/lib/evilution/cli/commands.rb +2 -0
  32. data/lib/evilution/cli/dispatcher.rb +2 -0
  33. data/lib/evilution/cli/exit_guard.rb +2 -0
  34. data/lib/evilution/cli/parsed_args.rb +2 -0
  35. data/lib/evilution/cli/parser/command_extractor.rb +2 -0
  36. data/lib/evilution/cli/parser/file_args.rb +4 -1
  37. data/lib/evilution/cli/parser/options_builder.rb +4 -1
  38. data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
  39. data/lib/evilution/cli/parser.rb +1 -0
  40. data/lib/evilution/cli/printers/tests_list.rb +5 -4
  41. data/lib/evilution/cli/printers.rb +2 -0
  42. data/lib/evilution/cli/result.rb +2 -0
  43. data/lib/evilution/cli.rb +8 -2
  44. data/lib/evilution/config/builders.rb +2 -0
  45. data/lib/evilution/config/env_loader.rb +2 -0
  46. data/lib/evilution/config/file_loader.rb +1 -0
  47. data/lib/evilution/config/sources.rb +1 -0
  48. data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
  49. data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
  50. data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
  51. data/lib/evilution/config/validators/fail_fast.rb +1 -0
  52. data/lib/evilution/config/validators/hooks.rb +1 -0
  53. data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
  54. data/lib/evilution/config/validators/integration.rb +1 -0
  55. data/lib/evilution/config/validators/isolation.rb +1 -0
  56. data/lib/evilution/config/validators/jobs.rb +1 -0
  57. data/lib/evilution/config/validators/preload.rb +1 -0
  58. data/lib/evilution/config/validators/profile.rb +1 -0
  59. data/lib/evilution/config/validators/spec_mappings.rb +3 -1
  60. data/lib/evilution/config/validators/spec_pattern.rb +1 -0
  61. data/lib/evilution/config/validators/warmup.rb +17 -0
  62. data/lib/evilution/config/validators.rb +2 -0
  63. data/lib/evilution/config.rb +7 -5
  64. data/lib/evilution/coverage.rb +1 -1
  65. data/lib/evilution/coverage_example_filter.rb +1 -1
  66. data/lib/evilution/diagnostic.rb +1 -1
  67. data/lib/evilution/equivalent/detector.rb +3 -1
  68. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/condition.rb +66 -0
  69. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/disturbance.rb +54 -0
  70. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/early_exit.rb +73 -0
  71. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/guard.rb +36 -0
  72. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/index_read.rb +82 -0
  73. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/node_path.rb +43 -0
  74. data/lib/evilution/equivalent/heuristic/guarded_index_fetch.rb +65 -0
  75. data/lib/evilution/example_filter.rb +46 -1
  76. data/lib/evilution/hooks/registry.rb +2 -1
  77. data/lib/evilution/integration/base.rb +2 -0
  78. data/lib/evilution/integration/known_failures.rb +26 -0
  79. data/lib/evilution/integration/loading/body_call_neutralizer.rb +139 -12
  80. data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
  81. data/lib/evilution/integration/loading/concern_redeclaration.rb +61 -0
  82. data/lib/evilution/integration/loading/concern_state_cleaner.rb +19 -12
  83. data/lib/evilution/integration/loading/mutation_applier.rb +16 -0
  84. data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
  85. data/lib/evilution/integration/loading/test_load_path.rb +21 -13
  86. data/lib/evilution/integration/minitest/test_ids.rb +21 -0
  87. data/lib/evilution/integration/minitest.rb +45 -6
  88. data/lib/evilution/integration/rspec/baseline_runner.rb +47 -3
  89. data/lib/evilution/integration/rspec/example_ids.rb +35 -0
  90. data/lib/evilution/integration/rspec/result_builder.rb +6 -1
  91. data/lib/evilution/integration/rspec/state_guard/anonymous_example_group_examples.rb +36 -0
  92. data/lib/evilution/integration/rspec/state_guard/configuration_state.rb +1 -0
  93. data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +1 -0
  94. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
  95. data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
  96. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
  97. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
  98. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
  99. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
  100. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
  101. data/lib/evilution/integration/rspec/state_guard.rb +10 -1
  102. data/lib/evilution/integration/rspec.rb +22 -4
  103. data/lib/evilution/integration/test_unit/result_builder.rb +6 -0
  104. data/lib/evilution/integration/test_unit/test_ids.rb +18 -0
  105. data/lib/evilution/integration/test_unit.rb +37 -9
  106. data/lib/evilution/isolation/fork.rb +2 -1
  107. data/lib/evilution/isolation/in_process.rb +21 -4
  108. data/lib/evilution/mcp/info_tool/actions/environment.rb +1 -0
  109. data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
  110. data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
  111. data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
  112. data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
  113. data/lib/evilution/mcp/info_tool/status_glossary.rb +2 -2
  114. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +2 -1
  115. data/lib/evilution/memory/leak_check.rb +27 -3
  116. data/lib/evilution/mutation.rb +8 -3
  117. data/lib/evilution/mutator/base.rb +49 -5
  118. data/lib/evilution/mutator/operator/alias_removal.rb +126 -0
  119. data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
  120. data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
  121. data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
  122. data/lib/evilution/mutator/operator/argument_removal.rb +9 -1
  123. data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
  124. data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
  125. data/lib/evilution/mutator/operator/bang_method.rb +11 -3
  126. data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
  127. data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
  128. data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
  129. data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
  130. data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
  131. data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
  132. data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
  133. data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
  134. data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
  135. data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
  136. data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
  137. data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
  138. data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
  139. data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
  140. data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
  141. data/lib/evilution/mutator/operator/format_specifier_swap.rb +100 -0
  142. data/lib/evilution/mutator/operator/forwarded_argument_drop.rb +142 -0
  143. data/lib/evilution/mutator/operator/inequality_to_negated_identity.rb +43 -0
  144. data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
  145. data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
  146. data/lib/evilution/mutator/operator/keyword_argument_removal.rb +47 -0
  147. data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -0
  148. data/lib/evilution/mutator/operator/no_matching_pattern_else.rb +47 -0
  149. data/lib/evilution/mutator/operator/numbered_parameter_swap.rb +78 -0
  150. data/lib/evilution/mutator/operator/off_by_one_boundary.rb +68 -0
  151. data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +14 -3
  152. data/lib/evilution/mutator/operator/pattern_matching_array.rb +12 -1
  153. data/lib/evilution/mutator/operator/pattern_wildcard_widening.rb +117 -0
  154. data/lib/evilution/mutator/operator/pin_operator_removal.rb +48 -0
  155. data/lib/evilution/mutator/operator/proc_to_lambda.rb +43 -0
  156. data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
  157. data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
  158. data/lib/evilution/mutator/operator/regex_simplification.rb +53 -132
  159. data/lib/evilution/mutator/operator/regexp_alternation_branch_deletion.rb +54 -0
  160. data/lib/evilution/mutator/operator/regexp_anchor_promotion.rb +43 -0
  161. data/lib/evilution/mutator/operator/regexp_anchor_to_predicate.rb +136 -0
  162. data/lib/evilution/mutator/operator/regexp_capture_to_passive.rb +61 -0
  163. data/lib/evilution/mutator/operator/regexp_character_type_complement.rb +54 -0
  164. data/lib/evilution/mutator/operator/regexp_named_group_rename.rb +106 -0
  165. data/lib/evilution/mutator/operator/regexp_option_removal.rb +51 -0
  166. data/lib/evilution/mutator/operator/regexp_quantifier_minimum_swap.rb +45 -0
  167. data/lib/evilution/mutator/operator/rescue_else_concatenation.rb +62 -0
  168. data/lib/evilution/mutator/operator/rescue_handler_concatenation.rb +69 -0
  169. data/lib/evilution/mutator/operator/rescue_handler_promotion.rb +65 -0
  170. data/lib/evilution/mutator/operator/return_keyword_removal.rb +79 -0
  171. data/lib/evilution/mutator/operator/rightward_assignment.rb +46 -0
  172. data/lib/evilution/mutator/operator/safe_navigation_removal.rb +67 -0
  173. data/lib/evilution/mutator/operator/send_mutation.rb +44 -6
  174. data/lib/evilution/mutator/operator/splat_operator.rb +38 -13
  175. data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
  176. data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
  177. data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
  178. data/lib/evilution/mutator/primitives.rb +25 -0
  179. data/lib/evilution/mutator/registry.rb +54 -2
  180. data/lib/evilution/mutator/rescue_handlers.rb +81 -0
  181. data/lib/evilution/parallel/pool.rb +1 -0
  182. data/lib/evilution/parallel_db_warning.rb +1 -1
  183. data/lib/evilution/process_supervisor.rb +3 -2
  184. data/lib/evilution/rails_warmup.rb +61 -0
  185. data/lib/evilution/reporter/cli/line_formatters/baseline_neutralized_notice.rb +55 -0
  186. data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
  187. data/lib/evilution/reporter/html/report.rb +1 -0
  188. data/lib/evilution/reporter/json/baseline.rb +15 -0
  189. data/lib/evilution/reporter/json.rb +7 -2
  190. data/lib/evilution/result/baseline_neutralization.rb +10 -0
  191. data/lib/evilution/result/coverage_gap_grouper.rb +1 -0
  192. data/lib/evilution/result/mutation_result.rb +9 -1
  193. data/lib/evilution/result/subject_scorer.rb +1 -0
  194. data/lib/evilution/result/summary.rb +42 -2
  195. data/lib/evilution/runner/baseline_runner.rb +26 -11
  196. data/lib/evilution/runner/canary.rb +2 -57
  197. data/lib/evilution/runner/canary_failure_message.rb +101 -0
  198. data/lib/evilution/runner/isolation_resolver.rb +23 -2
  199. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +35 -14
  200. data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -0
  201. data/lib/evilution/runner/mutation_executor/result_packer.rb +4 -2
  202. data/lib/evilution/runner/mutation_executor.rb +1 -1
  203. data/lib/evilution/runner/mutation_planner.rb +11 -2
  204. data/lib/evilution/runner/report_publisher.rb +3 -2
  205. data/lib/evilution/runner/subject_pipeline.rb +46 -1
  206. data/lib/evilution/runner.rb +4 -1
  207. data/lib/evilution/source_ast_cache.rb +1 -1
  208. data/lib/evilution/spec_ast_cache.rb +1 -1
  209. data/lib/evilution/subject.rb +8 -2
  210. data/lib/evilution/version.rb +1 -1
  211. data/lib/evilution.rb +51 -0
  212. data/script/memory_check +62 -16
  213. metadata +108 -4
@@ -2,10 +2,11 @@
2
2
 
3
3
  require_relative "spec_resolver"
4
4
  require_relative "process_cleanup"
5
+ require_relative "diagnostic"
5
6
 
6
7
  class Evilution::Baseline
7
- Result = Struct.new(:failed_spec_files, :duration) do
8
- def initialize(**)
8
+ Result = Struct.new(:failed_spec_files, :duration, :failures) do
9
+ def initialize(failed_spec_files:, duration:, failures: [])
9
10
  super
10
11
  freeze
11
12
  end
@@ -13,44 +14,62 @@ class Evilution::Baseline
13
14
  def failed?
14
15
  !failed_spec_files.empty?
15
16
  end
17
+
18
+ # Every example known to have failed before any mutation ran.
19
+ def failed_example_ids
20
+ failures.flat_map(&:failed_ids).to_set
21
+ end
16
22
  end
17
23
 
24
+ # spec_selector: the object `run` resolves with (integration layout plus
25
+ # spec_mappings / spec_pattern), returning every spec covering a source.
26
+ # Without one, the bare spec_resolver is used. spec_resolver also supplies
27
+ # the best-guess hint for an unresolved source.
28
+ #
29
+ # fallback_to_full_suite: whether an unresolved source runs the whole
30
+ # fallback_dir. When false its mutations are reported unresolved, so the
31
+ # baseline skips it rather than running -- and often timing out on -- the
32
+ # entire test directory.
18
33
  def initialize(spec_resolver: Evilution::SpecResolver.new, timeout: 30, runner: nil,
19
- fallback_dir: "spec", test_files: nil)
34
+ fallback_dir: "spec", test_files: nil, spec_selector: nil, fallback_to_full_suite: true)
20
35
  @spec_resolver = spec_resolver
21
36
  @timeout = timeout
22
37
  @runner = runner
23
38
  @fallback_dir = fallback_dir
24
39
  @test_files = test_files
40
+ @spec_selector = spec_selector
41
+ @fallback_to_full_suite = fallback_to_full_suite
25
42
  end
26
43
 
27
44
  def call(subjects)
28
45
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
- spec_files = baseline_spec_files(subjects)
30
- failed = Set.new
31
-
32
- spec_files.each do |spec_file|
33
- failed.add(spec_file) unless run_spec_file(spec_file)
34
- end
46
+ failures = baseline_spec_files(subjects).filter_map { |spec_file| check_spec_file(spec_file) }
35
47
 
36
48
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
37
- Result.new(failed_spec_files: failed, duration: duration)
49
+ Result.new(failed_spec_files: failures.to_set(&:spec_file), duration: duration, failures: failures)
38
50
  end
39
51
 
40
52
  def run_spec_file(spec_file)
53
+ check_spec_file(spec_file).nil?
54
+ end
55
+
56
+ # nil when the spec file passed, otherwise why it did not. Anything that
57
+ # stops the baseline from getting an answer counts as failing, and is
58
+ # recorded as the reason.
59
+ def check_spec_file(spec_file)
41
60
  raise Evilution::Error, "no baseline runner configured" unless @runner
42
61
 
43
62
  read_io, write_io = IO.pipe
44
63
  pid = fork_spec_runner(spec_file, read_io, write_io)
45
64
  write_io.close
46
- read_result(read_io, pid)
65
+ read_result(read_io, pid, spec_file)
47
66
  rescue Evilution::Error
48
67
  raise
49
- rescue StandardError
50
- false
68
+ rescue StandardError => e
69
+ warned(SpecFailure.new(spec_file: spec_file, error: "#{e.class}: #{e.message}"))
51
70
  ensure
52
- read_io&.close
53
- write_io&.close
71
+ read_io.close if read_io
72
+ write_io.close if write_io
54
73
  end
55
74
 
56
75
  def fork_spec_runner(spec_file, read_io, write_io)
@@ -60,27 +79,28 @@ class Evilution::Baseline
60
79
  $stdout.reopen(File::NULL, "w")
61
80
  $stderr.reopen(File::NULL, "w")
62
81
 
63
- passed = runner.call(spec_file)
64
- Marshal.dump({ passed: passed }, write_io)
82
+ report = child_report(runner, spec_file)
83
+ Marshal.dump(report, write_io)
65
84
  write_io.close
66
- exit!(passed ? 0 : 1)
85
+ exit!(report[:passed] ? 0 : 1)
67
86
  end
68
87
  end
69
88
 
70
89
  GRACE_PERIOD = 0.5
71
90
 
72
- def read_result(read_io, pid)
73
- if read_io.wait_readable(@timeout)
74
- data = read_io.read
75
- Process.wait(pid)
76
- return false if data.empty?
77
-
78
- result = Marshal.load(data)
79
- result[:passed]
80
- else
81
- terminate_child(pid)
82
- false
83
- end
91
+ def read_result(read_io, pid, spec_file)
92
+ return timed_out(pid, spec_file) unless read_io.wait_readable(@timeout)
93
+
94
+ data = read_io.read
95
+ _, status = Process.wait2(pid)
96
+ return warned(SpecFailure.new(spec_file: spec_file, error: unreported_error(status))) if data.empty?
97
+
98
+ report = Marshal.load(data)
99
+ report[:passed] ? nil : warned(SpecFailure.from_report(spec_file, report))
100
+ end
101
+
102
+ def warn_timeout(spec_file)
103
+ Evilution::Diagnostic.warn("[evilution] Baseline for #{spec_file} timed out after #{@timeout}s; treating it as failing.")
84
104
  end
85
105
 
86
106
  def terminate_child(pid)
@@ -98,11 +118,35 @@ class Evilution::Baseline
98
118
 
99
119
  private
100
120
 
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.
121
+ # Runs in the child. A runner that raises -- a spec helper that cannot load,
122
+ # a framework that is not there -- is a failure with a reason, not a child
123
+ # that dies without a word.
124
+ def child_report(runner, spec_file)
125
+ Report.from(runner.call(spec_file))
126
+ rescue StandardError, ScriptError => e
127
+ Report.build(passed: false, error: "#{e.class}: #{e.message}")
128
+ end
129
+
130
+ def timed_out(pid, spec_file)
131
+ terminate_child(pid)
132
+ warn_timeout(spec_file)
133
+ SpecFailure.new(spec_file: spec_file, error: "timed out after #{@timeout}s")
134
+ end
135
+
136
+ def unreported_error(status)
137
+ cause = status.signaled? ? "signal #{status.termsig}" : "exit status #{status.exitstatus}"
138
+ "baseline process ended without reporting (#{cause})"
139
+ end
140
+
141
+ def warned(failure)
142
+ detail = FailureFormatter.new.call(failure).map { |line| " #{line}" }
143
+ Evilution::Diagnostic.warn(
144
+ ["[evilution] Baseline failed for #{failure.spec_file}; " \
145
+ "a mutation that fails only its already-failing examples will be reported neutral.", *detail].join("\n")
146
+ )
147
+ failure
148
+ end
149
+
106
150
  def baseline_spec_files(subjects)
107
151
  return Array(@test_files).uniq if @test_files && !@test_files.empty?
108
152
 
@@ -111,20 +155,35 @@ class Evilution::Baseline
111
155
 
112
156
  def resolve_unique_spec_files(subjects)
113
157
  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
158
+ subjects.flat_map do |s|
159
+ specs = specs_for(s.file_path)
160
+ next specs unless specs.empty?
161
+
162
+ warn_no_matching_test(s.file_path) if warned.add?(s.file_path)
163
+ @fallback_to_full_suite ? [@fallback_dir] : []
118
164
  end.uniq
119
165
  end
120
166
 
167
+ def specs_for(file_path)
168
+ Array(@spec_selector ? @spec_selector.call(file_path) : @spec_resolver.call(file_path))
169
+ end
170
+
121
171
  def warn_no_matching_test(file_path)
172
+ action = @fallback_to_full_suite ? "running full suite" : "marking its mutations unresolved"
173
+ Evilution::Diagnostic.warn("[evilution] No matching test found for #{file_path}, #{action}. #{no_match_hint(file_path)}")
174
+ end
175
+
176
+ def no_match_hint(file_path)
122
177
  suggestion = @spec_resolver.suggest(file_path)
123
178
  hint = if suggestion
124
- "Pass --spec #{suggestion} (best guess) or the correct test file."
179
+ "Pass --spec #{suggestion} (best guess) or the correct test file"
125
180
  else
126
- "Use --spec to specify the test file."
181
+ "Use --spec to specify the test file"
127
182
  end
128
- warn "[evilution] No matching test found for #{file_path}, running full suite. #{hint}"
183
+ @fallback_to_full_suite ? "#{hint}." : "#{hint}, or --fallback-full-suite to run the whole suite."
129
184
  end
130
185
  end
186
+
187
+ require_relative "baseline/report"
188
+ require_relative "baseline/spec_failure"
189
+ require_relative "baseline/failure_formatter"
@@ -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
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
4
+
3
5
  # Gives evilution the final word on the process exit status.
4
6
  #
5
7
  # `--preload` loads the project's own spec helper into the parent process, and
@@ -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,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../parser"
4
+ require_relative "../../diagnostic"
5
+
3
6
  module Evilution::CLI::Parser::FileArgs
4
7
  ParsedPaths = Data.define(:files, :ranges)
5
8
 
@@ -22,7 +25,7 @@ module Evilution::CLI::Parser::FileArgs
22
25
 
23
26
  def expand_spec_dir(dir)
24
27
  unless File.directory?(dir)
25
- warn("Error: #{dir} is not a directory")
28
+ Evilution::Diagnostic.warn("Error: #{dir} is not a directory")
26
29
  return []
27
30
  end
28
31
 
@@ -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
 
@@ -119,6 +120,8 @@ class Evilution::CLI::Parser::OptionsBuilder
119
120
  "(default: auto-detect spec/rails_helper.rb -> spec/spec_helper.rb -> " \
120
121
  "test/test_helper.rb for Rails projects)") { |f| @options[:preload] = f }
121
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 }
122
125
  end
123
126
 
124
127
  def add_output_options(opts)
@@ -137,7 +140,7 @@ class Evilution::CLI::Parser::OptionsBuilder
137
140
 
138
141
  def add_profile_options(opts)
139
142
  opts.on("--profile NAME", "Operator profile: default, strict (default: default). " \
140
- "strict adds aggressive truthiness mutators for pre-merge audits.") { |p| @options[:profile] = p }
143
+ "strict adds aggressive mutators for pre-merge audits.") { |p| @options[:profile] = p }
141
144
  opts.on("--strict", "Shortcut for --profile=strict") { @options[:profile] = "strict" }
142
145
  end
143
146
 
@@ -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"
@@ -19,6 +24,7 @@ require_relative "cli/commands/session_gc"
19
24
  require_relative "cli/commands/compare"
20
25
  require_relative "cli/commands/run"
21
26
  require_relative "cli/exit_guard"
27
+ require_relative "diagnostic"
22
28
 
23
29
  # @api private
24
30
  class Evilution::CLI
@@ -37,14 +43,14 @@ class Evilution::CLI
37
43
  return run_subcommand_error(@parse_error) if @command == :parse_error
38
44
 
39
45
  result = Dispatcher.lookup(@command).new(@parsed, stdout: $stdout, stderr: $stderr).call
40
- warn("Error: #{result.error.message}") if result.error && !result.error_rendered
46
+ Evilution::Diagnostic.warn("Error: #{result.error.message}") if result.error && !result.error_rendered
41
47
  result.exit_code
42
48
  end
43
49
 
44
50
  private
45
51
 
46
52
  def run_subcommand_error(message)
47
- warn("Error: #{message}")
53
+ Evilution::Diagnostic.warn("Error: #{message}")
48
54
  2
49
55
  end
50
56
  end
@@ -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,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
5
+ require_relative "../../diagnostic"
4
6
 
5
7
  class Evilution::Config::Validators::SpecMappings < Evilution::Config::Validators::Base
6
8
  def self.call(value)
@@ -48,7 +50,7 @@ class Evilution::Config::Validators::SpecMappings < Evilution::Config::Validator
48
50
  specs.each do |spec_path|
49
51
  next if File.exist?(spec_path)
50
52
 
51
- warn "[evilution] spec_mappings[#{source.inspect}]: #{spec_path} not found, skipping"
53
+ Evilution::Diagnostic.warn("[evilution] spec_mappings[#{source.inspect}]: #{spec_path} not found, skipping")
52
54
  end
53
55
  end
54
56
  end
@@ -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