mutineer-mongoid 2.0.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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1194 -0
  3. data/LICENSE +21 -0
  4. data/README.md +659 -0
  5. data/bin/mutineer +6 -0
  6. data/lib/mutineer/baseline.rb +185 -0
  7. data/lib/mutineer/changed_lines.rb +136 -0
  8. data/lib/mutineer/child_stdout.rb +35 -0
  9. data/lib/mutineer/cli.rb +681 -0
  10. data/lib/mutineer/config.rb +372 -0
  11. data/lib/mutineer/coverage_map.rb +1366 -0
  12. data/lib/mutineer/daemon_backend.rb +351 -0
  13. data/lib/mutineer/daemon_client.rb +407 -0
  14. data/lib/mutineer/daemon_server.rb +351 -0
  15. data/lib/mutineer/external_backend.rb +288 -0
  16. data/lib/mutineer/file_swap.rb +194 -0
  17. data/lib/mutineer/isolation.rb +321 -0
  18. data/lib/mutineer/job_plan.rb +352 -0
  19. data/lib/mutineer/kill_channel.rb +284 -0
  20. data/lib/mutineer/kill_matrix.rb +88 -0
  21. data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
  22. data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
  23. data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +145 -0
  24. data/lib/mutineer/minitest_integration.rb +137 -0
  25. data/lib/mutineer/mongoid_failures.rb +82 -0
  26. data/lib/mutineer/mongoid_worker_db.rb +97 -0
  27. data/lib/mutineer/mutant_id.rb +126 -0
  28. data/lib/mutineer/mutation.rb +27 -0
  29. data/lib/mutineer/mutator_registry.rb +109 -0
  30. data/lib/mutineer/mutators/arithmetic.rb +35 -0
  31. data/lib/mutineer/mutators/array_literal.rb +44 -0
  32. data/lib/mutineer/mutators/base.rb +105 -0
  33. data/lib/mutineer/mutators/boolean_connector.rb +52 -0
  34. data/lib/mutineer/mutators/boolean_literal.rb +56 -0
  35. data/lib/mutineer/mutators/chain_link.rb +132 -0
  36. data/lib/mutineer/mutators/collection_method.rb +44 -0
  37. data/lib/mutineer/mutators/comparison.rb +35 -0
  38. data/lib/mutineer/mutators/condition_forcing.rb +134 -0
  39. data/lib/mutineer/mutators/condition_negation.rb +49 -0
  40. data/lib/mutineer/mutators/literal_mutation.rb +51 -0
  41. data/lib/mutineer/mutators/negation_removal.rb +44 -0
  42. data/lib/mutineer/mutators/operand_removal.rb +58 -0
  43. data/lib/mutineer/mutators/operator_assignment.rb +113 -0
  44. data/lib/mutineer/mutators/range_literal.rb +54 -0
  45. data/lib/mutineer/mutators/regex_literal.rb +72 -0
  46. data/lib/mutineer/mutators/return_nil.rb +72 -0
  47. data/lib/mutineer/mutators/safe_navigation.rb +76 -0
  48. data/lib/mutineer/mutators/statement_removal.rb +33 -0
  49. data/lib/mutineer/mutators/string_literal.rb +34 -0
  50. data/lib/mutineer/orphan_guard.rb +28 -0
  51. data/lib/mutineer/pairing.rb +298 -0
  52. data/lib/mutineer/parser.rb +45 -0
  53. data/lib/mutineer/progress.rb +46 -0
  54. data/lib/mutineer/project.rb +506 -0
  55. data/lib/mutineer/project_path.rb +57 -0
  56. data/lib/mutineer/rails_worker_db.rb +212 -0
  57. data/lib/mutineer/reporter.rb +926 -0
  58. data/lib/mutineer/result.rb +240 -0
  59. data/lib/mutineer/runner.rb +418 -0
  60. data/lib/mutineer/statement_lines.rb +183 -0
  61. data/lib/mutineer/subject.rb +38 -0
  62. data/lib/mutineer/test_runners/minitest.rb +22 -0
  63. data/lib/mutineer/test_runners/rspec.rb +330 -0
  64. data/lib/mutineer/test_runners.rb +30 -0
  65. data/lib/mutineer/version.rb +6 -0
  66. data/lib/mutineer/worker_pool.rb +147 -0
  67. data/lib/mutineer.rb +48 -0
  68. metadata +143 -0
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Mutineer
6
+ # Which tests kill which mutants, read from the {Kills} rows of a `--matrix`
7
+ # run. Pure: it reads results and runs nothing, and it never touches the score.
8
+ #
9
+ # A test is a `[file, name, id]` triple (see {Kills}); file and id identify it,
10
+ # and every row names a test the same way (see {Runner.share_tests}). The tests it knows are
11
+ # the ones that ran against at least one mutant of this run, so every answer
12
+ # is relative to the run's mutants: a test of code outside the run's sources
13
+ # kills nothing here.
14
+ #
15
+ # - A blind test ran in at least one complete row and killed no mutant. A
16
+ # test seen only in incomplete rows is never called blind.
17
+ # - A redundant test killed at least one mutant, and each mutant it killed has
18
+ # another killer. Each test is judged alone: two redundant tests can be the
19
+ # only killers of one mutant, so delete them one at a time. Finding the
20
+ # smallest set of tests that keeps every kill is a set-cover problem, out of
21
+ # scope here.
22
+ #
23
+ # Kills in incomplete rows count, since they did happen. An incomplete row can
24
+ # miss kills, so when {#complete?} is false a blind test may have killed one
25
+ # of those mutants.
26
+ class KillMatrix
27
+ # The results that carry a {Kills} row, in run order.
28
+ #
29
+ # @return [Array<Mutineer::Result>]
30
+ attr_reader :rows
31
+
32
+ # @param results [Array<Mutineer::Result>] every result of the run.
33
+ def initialize(results)
34
+ @rows = results.select(&:kills)
35
+ @kill_counts = Hash.new(0)
36
+ @rows.each { |r| r.kills.killed_by.each { |test| @kill_counts[test] += 1 } }
37
+ end
38
+
39
+ # Every test that ran against at least one mutant, sorted by file, name and id.
40
+ #
41
+ # @return [Array<Array(String, String, String)>]
42
+ def tests
43
+ @tests ||= @rows.flat_map { |r| r.kills.ran + r.kills.killed_by }.uniq.sort
44
+ end
45
+
46
+ # True when every row is complete.
47
+ #
48
+ # @return [Boolean]
49
+ def complete?
50
+ @rows.all? { |r| r.kills.complete }
51
+ end
52
+
53
+ # The rows that are not complete.
54
+ #
55
+ # @return [Array<Mutineer::Result>]
56
+ def incomplete_rows
57
+ @rows.reject { |r| r.kills.complete }
58
+ end
59
+
60
+ # How many mutants `test` killed.
61
+ #
62
+ # @param test [Array(String, String, String)] a `[file, name, id]` test.
63
+ # @return [Integer]
64
+ def kill_count(test)
65
+ @kill_counts[test]
66
+ end
67
+
68
+ # Tests that ran in a complete row and killed no mutant.
69
+ #
70
+ # @return [Array<Array(String, String, String)>] sorted.
71
+ def blind
72
+ @blind ||= begin
73
+ seen = @rows.select { |r| r.kills.complete }.flat_map { |r| r.kills.ran }.to_set
74
+ tests.select { |test| kill_count(test).zero? && seen.include?(test) }
75
+ end
76
+ end
77
+
78
+ # Tests that killed a mutant, where every mutant they killed has another killer.
79
+ #
80
+ # @return [Array<Array(String, String, String)>] sorted.
81
+ def redundant
82
+ @redundant ||= begin
83
+ sole = @rows.filter_map { |r| r.kills.killed_by.first if r.kills.killed_by.size == 1 }.to_set
84
+ tests.select { |test| kill_count(test).positive? && !sole.include?(test) }
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "outer_reporter"
4
+ require_relative "../kill_channel"
5
+
6
+ module Mutineer
7
+ class MinitestIntegration
8
+ # Sends the outcome of each test to the parent of a `--matrix` mutant run,
9
+ # through {KillChannel}. It never stops the run: every covering test runs,
10
+ # so the parent learns exactly which tests kill the mutant. Child-process
11
+ # only, like MinitestIntegration.
12
+ #
13
+ # Only the outer reporter counts (see {OuterReporter}): a test can build and
14
+ # record on its own reporter, and those results are not the run's.
15
+ module KillRecorder
16
+ extend OuterReporter::Client
17
+
18
+ # Serializes the counting and the writing of results, which Minitest
19
+ # records from several threads in a parallel class.
20
+ LOCK = Mutex.new
21
+
22
+ # Prepended on each loaded test class: one test's run. Serial and parallel
23
+ # runs both call it, in Minitest 5 and 6.
24
+ module MarkSkippedTest
25
+ # Runs the test inside a `skip` region once a serial test has killed.
26
+ #
27
+ # @return [Minitest::Result]
28
+ def run
29
+ KillRecorder.skipping { super }
30
+ end
31
+ end
32
+
33
+ # Prepended on each loaded test class's singleton under Minitest 6: one
34
+ # class's run and each test's class-level run, user wrappers included.
35
+ # A run without `--matrix` skips both after a stop (see
36
+ # StopAtFirstFailure::SkipAfterStop6).
37
+ module MarkSkippedClass6
38
+ # Runs the class inside a `skip` region once a serial test has killed.
39
+ #
40
+ # @param reporter [Minitest::CompositeReporter]
41
+ # @param options [Hash]
42
+ # @return [Object]
43
+ def run_suite(reporter, options = {})
44
+ KillRecorder.class_starting(self, reporter)
45
+ KillRecorder.skipping { super }
46
+ end
47
+
48
+ # Runs one test's class-level step inside a `skip` region once a
49
+ # serial test has killed.
50
+ #
51
+ # @param klass [Class]
52
+ # @param method_name [String]
53
+ # @param reporter [Minitest::CompositeReporter]
54
+ # @return [Object]
55
+ def run(klass, method_name, reporter)
56
+ KillRecorder.skipping { super }
57
+ end
58
+ end
59
+
60
+ # The Minitest 5 form of {MarkSkippedClass6}.
61
+ module MarkSkippedClass5
62
+ # Runs the class inside a `skip` region once a serial test has killed.
63
+ #
64
+ # @param reporter [Minitest::CompositeReporter]
65
+ # @param options [Hash]
66
+ # @return [Object]
67
+ def run(reporter, options = {})
68
+ KillRecorder.class_starting(self, reporter)
69
+ KillRecorder.skipping { super }
70
+ end
71
+
72
+ # Runs one test's class-level step inside a `skip` region once a
73
+ # serial test has killed.
74
+ #
75
+ # @param klass [Class]
76
+ # @param method_name [String]
77
+ # @param reporter [Minitest::CompositeReporter]
78
+ # @return [Object]
79
+ def run_one_method(klass, method_name, reporter)
80
+ KillRecorder.skipping { super }
81
+ end
82
+ end
83
+
84
+ # Prepended on `Minitest::CompositeReporter`.
85
+ module Record
86
+ # Records the result, then sends it to the channel.
87
+ #
88
+ # @param result [Minitest::Result] the result of one test.
89
+ # @return [void]
90
+ def record(result)
91
+ super
92
+ KillRecorder.record(self, result)
93
+ end
94
+ end
95
+
96
+ class << self
97
+ # The write end of the channel of the armed run.
98
+ #
99
+ # @return [IO, nil]
100
+ attr_accessor :channel
101
+
102
+ # True once the `parallel` line has gone out.
103
+ #
104
+ # @return [Boolean, nil]
105
+ attr_accessor :parallel_marked
106
+
107
+ # The loaded test classes of the armed run.
108
+ #
109
+ # @return [Array<Class>, nil]
110
+ attr_accessor :runnables
111
+
112
+ # How many results the outer reporter has recorded, skips included.
113
+ #
114
+ # @return [Integer, nil]
115
+ attr_accessor :seen
116
+
117
+ # True once a serial test killed: from then on, a run without `--matrix`
118
+ # skips each later test and class.
119
+ #
120
+ # @return [Boolean, nil]
121
+ attr_accessor :serial_killed
122
+
123
+ # Installs the hooks, arms the recorder for this process, and writes the
124
+ # channel's `start` line. Call it after the test files load, so the
125
+ # recorder knows the test classes. An unknown Minitest shape arms
126
+ # nothing and writes nothing, so the parent sees no `start` and keeps
127
+ # the row incomplete.
128
+ #
129
+ # @param channel [IO] the write end of the channel.
130
+ # @param runnables [Array<Class>] the loaded test classes.
131
+ # @return [Boolean] false when the Minitest shape is unknown.
132
+ def arm!(channel, runnables)
133
+ outer = OuterReporter.hook_for_loaded_minitest
134
+ return false unless outer
135
+
136
+ OuterReporter.prepend_once(::Minitest::CompositeReporter, Record)
137
+ OuterReporter.prepend_once(::Minitest.singleton_class, outer)
138
+ class_hook = outer.equal?(OuterReporter::Hook6) ? MarkSkippedClass6 : MarkSkippedClass5
139
+ runnables.each do |klass|
140
+ OuterReporter.prepend_once(klass, MarkSkippedTest)
141
+ OuterReporter.prepend_once(klass.singleton_class, class_hook)
142
+ end
143
+ OuterReporter.register(self)
144
+ self.armed_pid = Process.pid
145
+ self.armed_reporter = nil
146
+ self.channel = channel
147
+ self.parallel_marked = false
148
+ # A copy: Minitest's list is live, and a class a test defines during
149
+ # the run joins it without running, which would break the count.
150
+ self.runnables = runnables.dup
151
+ self.seen = 0
152
+ self.serial_killed = false
153
+ KillChannel.write_start(channel)
154
+ true
155
+ end
156
+
157
+ # Writes the channel's `end` line once the suite has returned and the
158
+ # outer reporter has seen every test the loaded classes run. A return
159
+ # alone proves nothing: Minitest catches an Interrupt in a test and
160
+ # returns after the tests so far. Does nothing unless the recorder is
161
+ # armed in this process.
162
+ #
163
+ # @return [void]
164
+ def finish!
165
+ return unless channel && armed_here?
166
+
167
+ KillChannel.write_end(channel) if seen == planned_tests
168
+ end
169
+
170
+ # True when the test class runs its tests in parallel threads
171
+ # (`parallelize_me!`). Minitest runs these after every serial class, and
172
+ # the stop at the first failure cannot skip the tests it already queued.
173
+ # Minitest 6 names the order `run_order`; Minitest 5 names it `test_order`.
174
+ #
175
+ # @param klass [Class] a loaded test class.
176
+ # @return [Boolean]
177
+ def parallel?(klass)
178
+ %i[run_order test_order].any? { |order| klass.respond_to?(order) && klass.public_send(order) == :parallel }
179
+ end
180
+
181
+ # Clears the armed state.
182
+ #
183
+ # @return [void]
184
+ def disarm!
185
+ self.armed_pid = nil
186
+ self.armed_reporter = nil
187
+ self.channel = nil
188
+ self.runnables = nil
189
+ self.serial_killed = false
190
+ end
191
+
192
+ # Writes the `parallel` line when the first parallel class starts on the
193
+ # outer reporter. The class object is known here, so an anonymous class
194
+ # (which Minitest records with no name) counts too. Minitest runs every
195
+ # serial class first, so a kill before this line came from a serial
196
+ # test. A test that runs a class on its own reporter is not the run's
197
+ # parallel phase, so it marks nothing.
198
+ #
199
+ # @param klass [Class] the test class starting its run.
200
+ # @param reporter [Minitest::CompositeReporter] the reporter it runs on.
201
+ # @return [void]
202
+ def class_starting(klass, reporter)
203
+ return unless channel && outer_reporter?(reporter) && parallel?(klass)
204
+
205
+ LOCK.synchronize { mark_parallel }
206
+ end
207
+
208
+ # Runs the block between `skip` and `unskip` when a serial test has
209
+ # killed in this armed process, else just runs it. No `ensure`: a block
210
+ # that never returns (an exit, a crash) must leave its region open, so
211
+ # the parent knows the child ended where a run without `--matrix`
212
+ # would not have been.
213
+ #
214
+ # @yieldreturn [Object]
215
+ # @return [Object] the block's value.
216
+ def skipping
217
+ open = channel && serial_killed && armed_here?
218
+ KillChannel.write_skip(channel) if open
219
+ value = yield
220
+ KillChannel.write_unskip(channel) if open
221
+ value
222
+ end
223
+
224
+ # Sends one result recorded on `reporter`: a pass, or a kill for a
225
+ # failure or an error. Skips send no test line, though they count as
226
+ # seen, and results on other reporters count for nothing. The
227
+ # `parallel` line goes out when the first parallel class starts (see
228
+ # {.class_starting}), so a kill that precedes it came from a serial test.
229
+ #
230
+ # @param reporter [Minitest::CompositeReporter] the recording reporter.
231
+ # @param result [Minitest::Result] the result of one test.
232
+ # @return [void]
233
+ def record(reporter, result)
234
+ return unless channel && outer_reporter?(reporter)
235
+
236
+ LOCK.synchronize do
237
+ self.seen += 1
238
+ send_result(result)
239
+ end
240
+ end
241
+
242
+ private
243
+
244
+ # Writes one result's line. A recorder that raised would change the
245
+ # verdict, so a result it cannot describe (a Minitest without
246
+ # `source_location` or `klass` on its results) is a `lost` line, which
247
+ # keeps the row incomplete, as on the RSpec side.
248
+ #
249
+ # @param result [Minitest::Result] the result of one test.
250
+ # @return [void]
251
+ def send_result(result)
252
+ return if result.skipped?
253
+
254
+ event = result.passed? ? KillChannel::PASS : KillChannel::KILL
255
+ self.serial_killed = true if event == KillChannel::KILL && !parallel_marked
256
+ file, line = Array(result.source_location)
257
+ KillChannel.write(channel, event, file, *name_and_id(result, line))
258
+ rescue StandardError
259
+ KillChannel.write_lost(channel)
260
+ end
261
+
262
+ # The test's name and id. `Class#method` is unique within a run, so the
263
+ # name is also the id. An anonymous class has no name (Minitest records
264
+ # nil), so its tests would all merge into `#method`; their id adds the
265
+ # line that defines the method, which no mutant changes. The test's file
266
+ # is already part of its identity, so the id holds no path.
267
+ #
268
+ # @param result [Minitest::Result] the result of one test.
269
+ # @param line [Integer, nil] the line that defines the test method.
270
+ # @return [Array(String, String)] name and id.
271
+ def name_and_id(result, line)
272
+ return ["#{result.klass}##{result.name}"] * 2 unless result.klass.to_s.empty?
273
+
274
+ name = "(anonymous)##{result.name}"
275
+ [name, "#{name}@#{line}"]
276
+ end
277
+
278
+ # Writes the `parallel` line, once.
279
+ #
280
+ # @return [void]
281
+ def mark_parallel
282
+ return if parallel_marked
283
+
284
+ self.parallel_marked = true
285
+ KillChannel.write_parallel(channel)
286
+ end
287
+
288
+ # How many tests the loaded classes ran, or nil when it cannot be told.
289
+ # Asked after the run, because `runnable_methods` shuffles with the
290
+ # seed that `Minitest.run` sets.
291
+ #
292
+ # @return [Integer, nil]
293
+ def planned_tests
294
+ runnables.sum { |klass| klass.runnable_methods.size }
295
+ rescue StandardError
296
+ nil
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutineer
4
+ class MinitestIntegration
5
+ # Finds the outer reporter of a Minitest run, for the hooks that act only on
6
+ # the run's own results. A test can build a reporter of its own and record
7
+ # on it, or start a nested run, so the first reporter the run hands to its
8
+ # suites is the one that counts. Child-process only, like
9
+ # MinitestIntegration.
10
+ #
11
+ # A client module extends {Client}, registers here, and sets its armed pid.
12
+ # The hook then gives the outer reporter to each client armed in this
13
+ # process. Minitest 5 and 6 need different hook points; an unknown shape
14
+ # gets no hook.
15
+ module OuterReporter
16
+ # Prepended on the `Minitest` singleton class for Minitest 6.
17
+ module Hook6
18
+ # Hands the outer reporter to the armed clients. Only the first call
19
+ # counts, because a test can call this method with its own reporter.
20
+ #
21
+ # @param reporter [Minitest::CompositeReporter] the outer reporter.
22
+ # @param options [Hash] the Minitest options.
23
+ # @return [Object] whatever Minitest returns.
24
+ def run_all_suites(reporter, options)
25
+ OuterReporter.seen(reporter)
26
+ super
27
+ end
28
+ end
29
+
30
+ # Prepended on the `Minitest` singleton class for Minitest 5.
31
+ module Hook5
32
+ # The Minitest 5 form of Hook6#run_all_suites.
33
+ #
34
+ # @param reporter [Minitest::CompositeReporter] the outer reporter.
35
+ # @param options [Hash] the Minitest options.
36
+ # @return [Object] whatever Minitest returns.
37
+ def __run(reporter, options)
38
+ OuterReporter.seen(reporter)
39
+ super
40
+ end
41
+ end
42
+
43
+ # The per-process state a client keeps. Forked workers inherit it, and
44
+ # the pid keeps them from acting on it.
45
+ module Client
46
+ # The pid of the armed process.
47
+ #
48
+ # @return [Integer, nil]
49
+ attr_accessor :armed_pid
50
+
51
+ # The outer reporter of the armed run.
52
+ #
53
+ # @return [Minitest::CompositeReporter, nil]
54
+ attr_accessor :armed_reporter
55
+
56
+ # True when the client is armed in this process.
57
+ #
58
+ # @return [Boolean]
59
+ def armed_here?
60
+ armed_pid == Process.pid
61
+ end
62
+
63
+ # True when `reporter` is the outer reporter of the run armed in this
64
+ # process.
65
+ #
66
+ # @param reporter [Minitest::CompositeReporter] the recording reporter.
67
+ # @return [Boolean]
68
+ def outer_reporter?(reporter)
69
+ armed_here? && reporter.equal?(armed_reporter)
70
+ end
71
+ end
72
+
73
+ class << self
74
+ # The registered clients, in registration order.
75
+ #
76
+ # @return [Array<Module>]
77
+ def clients
78
+ @clients ||= []
79
+ end
80
+
81
+ # Registers a client once.
82
+ #
83
+ # @param client [Module] a module that extends {Client}.
84
+ # @return [void]
85
+ def register(client)
86
+ clients << client unless clients.include?(client)
87
+ end
88
+
89
+ # Gives `reporter` to every client armed in this process that has no
90
+ # outer reporter yet.
91
+ #
92
+ # @param reporter [Minitest::CompositeReporter] a reporter a run uses.
93
+ # @return [void]
94
+ def seen(reporter)
95
+ clients.each do |client|
96
+ client.armed_reporter ||= reporter if client.armed_here?
97
+ end
98
+ end
99
+
100
+ # The outer-reporter hook for the loaded Minitest.
101
+ #
102
+ # @return [Module, nil] {Hook6}, {Hook5}, or nil for an unknown shape.
103
+ def hook_for_loaded_minitest
104
+ runnable = ::Minitest::Runnable
105
+ if ::Minitest.respond_to?(:run_all_suites) && runnable.respond_to?(:run_suite)
106
+ Hook6
107
+ elsif ::Minitest.respond_to?(:__run) && runnable.respond_to?(:run_one_method)
108
+ Hook5
109
+ end
110
+ end
111
+
112
+ # Prepends `mod` on `target` once. A copy on a superclass does not
113
+ # count, because it comes after the own methods of `target`.
114
+ #
115
+ # @param target [Module] the class or singleton class.
116
+ # @param mod [Module] the module to prepend.
117
+ # @return [void]
118
+ def prepend_once(target, mod)
119
+ return if target.ancestors.take_while { |a| !a.equal?(target) }.include?(mod)
120
+
121
+ target.prepend(mod)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "outer_reporter"
4
+
5
+ module Mutineer
6
+ class MinitestIntegration
7
+ # Stops a Minitest run at the first failing test: one failure already
8
+ # kills the mutant. Child-process only, like MinitestIntegration.
9
+ #
10
+ # It sets a flag instead of unwinding the stack, so that class-level
11
+ # wrappers (`after_all`, a block-form transaction) still finish. The
12
+ # remaining tests and classes then return before they start.
13
+ #
14
+ # Minitest 5 and 6 need different hook points. An unknown shape installs
15
+ # no hook, and the run is a full run. The outer reporter comes from
16
+ # {OuterReporter}, shared with {KillRecorder}.
17
+ module StopAtFirstFailure
18
+ extend OuterReporter::Client
19
+
20
+ # Prepended on `Minitest::CompositeReporter`.
21
+ module RecordFailure
22
+ # Records the result, then sets the stop flag on a failure or an
23
+ # error. Only the outer reporter counts: a test can build and record
24
+ # on its own reporter.
25
+ #
26
+ # @param result [Minitest::Result] the result of one test.
27
+ # @return [void]
28
+ def record(result)
29
+ super
30
+ return if result.passed? || result.skipped?
31
+ return unless StopAtFirstFailure.outer_reporter?(self)
32
+
33
+ StopAtFirstFailure.stopped = true
34
+ end
35
+ end
36
+
37
+ # Prepended on the singleton class of each test class for Minitest 6.
38
+ module SkipAfterStop6
39
+ # Skips the test class after a stop.
40
+ #
41
+ # @param reporter [Minitest::CompositeReporter] the reporter.
42
+ # @param options [Hash] the Minitest options.
43
+ # @return [Object, nil] whatever Minitest returns, or nil when skipped.
44
+ def run_suite(reporter, options = {})
45
+ return if StopAtFirstFailure.stopped_here?
46
+
47
+ super
48
+ end
49
+
50
+ # Skips one test after a stop, without a record.
51
+ #
52
+ # @param klass [Class] the test class.
53
+ # @param method_name [String] the test method.
54
+ # @param reporter [Minitest::CompositeReporter] the reporter.
55
+ # @return [Object, nil] whatever Minitest returns, or nil when skipped.
56
+ def run(klass, method_name, reporter)
57
+ return if StopAtFirstFailure.stopped_here?
58
+
59
+ super
60
+ end
61
+ end
62
+
63
+ # Prepended on the singleton class of each test class for Minitest 5.
64
+ module SkipAfterStop5
65
+ # Skips the test class after a stop.
66
+ #
67
+ # @param reporter [Minitest::CompositeReporter] the reporter.
68
+ # @param options [Hash] the Minitest options.
69
+ # @return [Object, nil] whatever Minitest returns, or nil when skipped.
70
+ def run(reporter, options = {})
71
+ return if StopAtFirstFailure.stopped_here?
72
+
73
+ super
74
+ end
75
+
76
+ # Skips one test after a stop, without a record.
77
+ #
78
+ # @param klass [Class] the test class.
79
+ # @param method_name [String] the test method.
80
+ # @param reporter [Minitest::CompositeReporter] the reporter.
81
+ # @return [Object, nil] whatever Minitest returns, or nil when skipped.
82
+ def run_one_method(klass, method_name, reporter)
83
+ return if StopAtFirstFailure.stopped_here?
84
+
85
+ super
86
+ end
87
+ end
88
+
89
+ class << self
90
+ # True after the outer reporter records a failure or an error.
91
+ #
92
+ # @return [Boolean, nil]
93
+ attr_accessor :stopped
94
+
95
+ # Installs the hooks and arms the stop for this process. Call it after
96
+ # the test files load, so that every test class gets its prepend.
97
+ #
98
+ # @param runnables [Array<Class>] the loaded test classes.
99
+ # @return [Boolean] false when the Minitest shape is unknown.
100
+ def arm!(runnables)
101
+ outer, skip = hooks_for_loaded_minitest
102
+ return false unless outer
103
+
104
+ OuterReporter.prepend_once(::Minitest::CompositeReporter, RecordFailure)
105
+ OuterReporter.prepend_once(::Minitest.singleton_class, outer)
106
+ runnables.each { |klass| OuterReporter.prepend_once(klass.singleton_class, skip) }
107
+ OuterReporter.register(self)
108
+ self.armed_pid = Process.pid
109
+ self.armed_reporter = nil
110
+ self.stopped = false
111
+ true
112
+ end
113
+
114
+ # Clears the armed state.
115
+ #
116
+ # @return [void]
117
+ def disarm!
118
+ self.armed_pid = nil
119
+ self.armed_reporter = nil
120
+ self.stopped = false
121
+ end
122
+
123
+ # True when the armed run in this process has stopped.
124
+ #
125
+ # @return [Boolean]
126
+ def stopped_here?
127
+ stopped == true && armed_here?
128
+ end
129
+
130
+ private
131
+
132
+ # Picks the hook modules for the loaded Minitest.
133
+ #
134
+ # @return [Array(Module, Module), nil] the outer reporter hook and the
135
+ # skip hook, or nil for an unknown shape.
136
+ def hooks_for_loaded_minitest
137
+ outer = OuterReporter.hook_for_loaded_minitest
138
+ if outer.equal?(OuterReporter::Hook6) then [outer, SkipAfterStop6]
139
+ elsif outer.equal?(OuterReporter::Hook5) then [outer, SkipAfterStop5]
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end