mutant 0.10.6 → 0.10.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mutant.rb +4 -3
  3. data/lib/mutant/cli/command/run.rb +5 -2
  4. data/lib/mutant/config.rb +59 -34
  5. data/lib/mutant/env.rb +14 -4
  6. data/lib/mutant/integration.rb +7 -10
  7. data/lib/mutant/integration/null.rb +0 -1
  8. data/lib/mutant/isolation.rb +11 -48
  9. data/lib/mutant/isolation/fork.rb +107 -40
  10. data/lib/mutant/isolation/none.rb +18 -5
  11. data/lib/mutant/license/subscription/commercial.rb +2 -3
  12. data/lib/mutant/license/subscription/opensource.rb +0 -1
  13. data/lib/mutant/matcher/method/instance.rb +0 -2
  14. data/lib/mutant/mutator/node/send.rb +1 -1
  15. data/lib/mutant/parallel.rb +0 -1
  16. data/lib/mutant/parallel/worker.rb +0 -2
  17. data/lib/mutant/reporter/cli.rb +0 -2
  18. data/lib/mutant/reporter/cli/printer/config.rb +9 -5
  19. data/lib/mutant/reporter/cli/printer/coverage_result.rb +19 -0
  20. data/lib/mutant/reporter/cli/printer/env_progress.rb +2 -0
  21. data/lib/mutant/reporter/cli/printer/isolation_result.rb +19 -35
  22. data/lib/mutant/reporter/cli/printer/mutation_result.rb +4 -9
  23. data/lib/mutant/reporter/cli/printer/subject_result.rb +2 -2
  24. data/lib/mutant/result.rb +81 -30
  25. data/lib/mutant/runner/sink.rb +12 -5
  26. data/lib/mutant/timer.rb +60 -11
  27. data/lib/mutant/transform.rb +25 -21
  28. data/lib/mutant/version.rb +1 -1
  29. data/lib/mutant/warnings.rb +0 -1
  30. data/lib/mutant/world.rb +15 -0
  31. metadata +3 -5
  32. data/lib/mutant/reporter/cli/printer/mutation_progress_result.rb +0 -28
  33. data/lib/mutant/reporter/cli/printer/subject_progress.rb +0 -58
  34. data/lib/mutant/reporter/cli/printer/test_result.rb +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b5a7d1620b6792fa1cef90ead01124cf50287a165a0bdd415fbc9aab9ecfde8
4
- data.tar.gz: 758609938988d4d8de30a6f95a47c603fc4d5365ba98ad1a113a6de11f8e074f
3
+ metadata.gz: defe26105634d28a81b10e876713ab4e29a53a5d78ac8fe7e2d8a44d32b644c6
4
+ data.tar.gz: fad9645931798ed165a249e8e8d4a1c032896ceba6b823a1464ff2f60a7cadbb
5
5
  SHA512:
6
- metadata.gz: 3ecd6aea62e2d01cb68cc9c9c651190305459b7f1b796f4360ce872ce0dbb1871cb40821744d1c09a029a80f39a93cbda9a242591ceae405e47ed971d3d861f9
7
- data.tar.gz: '09e132e435163308309c0185abdd39ea9b6be0d2bd0efa0956049c366f0b521df866a3b7fd04d2e13550bd4f2b09993c659530f86b00142adff69ebc14af3a16'
6
+ metadata.gz: 79da9f75f18510bbf81e43e2b98e43c4b92ddf2e6cb68b66828f9f4240f7d3e8747d6e475130f83a2d1edec8c1dda43a34f48524bd7439372e545a046099c28d
7
+ data.tar.gz: 7d8aac2776eca84aff051b7a146863bb271c5df4ec7dc344f041d3a22a17b4d87f9f48d5201e2dceec30f548adc7135d443ee78bc00764a9399fb677f2cf0aa5
@@ -179,16 +179,14 @@ require 'mutant/reporter/sequence'
179
179
  require 'mutant/reporter/cli'
180
180
  require 'mutant/reporter/cli/printer'
181
181
  require 'mutant/reporter/cli/printer/config'
182
+ require 'mutant/reporter/cli/printer/coverage_result'
182
183
  require 'mutant/reporter/cli/printer/env'
183
184
  require 'mutant/reporter/cli/printer/env_progress'
184
185
  require 'mutant/reporter/cli/printer/env_result'
185
186
  require 'mutant/reporter/cli/printer/isolation_result'
186
- require 'mutant/reporter/cli/printer/mutation_progress_result'
187
187
  require 'mutant/reporter/cli/printer/mutation_result'
188
188
  require 'mutant/reporter/cli/printer/status_progressive'
189
- require 'mutant/reporter/cli/printer/subject_progress'
190
189
  require 'mutant/reporter/cli/printer/subject_result'
191
- require 'mutant/reporter/cli/printer/test_result'
192
190
  require 'mutant/reporter/cli/format'
193
191
  require 'mutant/repository'
194
192
  require 'mutant/repository/diff'
@@ -219,12 +217,14 @@ module Mutant
219
217
  stderr: $stderr,
220
218
  stdout: $stdout,
221
219
  thread: Thread,
220
+ timer: Timer.new(Process),
222
221
  warnings: Warnings.new(Warning)
223
222
  )
224
223
 
225
224
  # Reopen class to initialize constant to avoid dep circle
226
225
  class Config
227
226
  DEFAULT = new(
227
+ coverage_criteria: Config::CoverageCriteria::DEFAULT,
228
228
  expression_parser: Expression::Parser.new([
229
229
  Expression::Method,
230
230
  Expression::Methods,
@@ -237,6 +237,7 @@ module Mutant
237
237
  isolation: Mutant::Isolation::Fork.new(WORLD),
238
238
  jobs: nil,
239
239
  matcher: Matcher::Config::DEFAULT,
240
+ mutation_timeout: nil,
240
241
  reporter: Reporter::CLI.build(WORLD.stdout),
241
242
  requires: EMPTY_ARRAY,
242
243
  zombie: false
@@ -35,7 +35,7 @@ module Mutant
35
35
 
36
36
  def initialize(attributes)
37
37
  super(attributes)
38
- @config = Config::DEFAULT
38
+ @config = Config.env
39
39
  end
40
40
 
41
41
  def execute
@@ -49,7 +49,7 @@ module Mutant
49
49
  end
50
50
 
51
51
  def expand(file_config)
52
- @config = Config.env.merge(file_config).merge(@config)
52
+ @config = @config.merge(file_config)
53
53
  end
54
54
 
55
55
  def soft_fail(result)
@@ -153,6 +153,9 @@ module Mutant
153
153
  parser.on('-j', '--jobs NUMBER', 'Number of kill jobs. Defaults to number of processors.') do |number|
154
154
  set(jobs: Integer(number))
155
155
  end
156
+ parser.on('-t', '--mutation-timeout NUMBER', 'Per mutation analysis timeout') do |number|
157
+ set(mutation_timeout: Float(number))
158
+ end
156
159
  end
157
160
  end # Run
158
161
  # rubocop:enable Metrics/ClassLength
@@ -7,6 +7,7 @@ module Mutant
7
7
  # to current environment is being represented by the Mutant::Env object.
8
8
  class Config
9
9
  include Adamantium::Flat, Anima.new(
10
+ :coverage_criteria,
10
11
  :expression_parser,
11
12
  :fail_fast,
12
13
  :includes,
@@ -14,6 +15,7 @@ module Mutant
14
15
  :isolation,
15
16
  :jobs,
16
17
  :matcher,
18
+ :mutation_timeout,
17
19
  :reporter,
18
20
  :requires,
19
21
  :zombie
@@ -23,30 +25,6 @@ module Mutant
23
25
  define_method(:"#{name}?") { public_send(name) }
24
26
  end
25
27
 
26
- boolean = Transform::Boolean.new
27
- integer = Transform::Primitive.new(Integer)
28
- string = Transform::Primitive.new(String)
29
-
30
- string_array = Transform::Array.new(string)
31
-
32
- TRANSFORM = Transform::Sequence.new(
33
- [
34
- Transform::Exception.new(SystemCallError, :read.to_proc),
35
- Transform::Exception.new(YAML::SyntaxError, YAML.method(:safe_load)),
36
- Transform::Hash.new(
37
- optional: [
38
- Transform::Hash::Key.new('fail_fast', boolean),
39
- Transform::Hash::Key.new('includes', string_array),
40
- Transform::Hash::Key.new('integration', string),
41
- Transform::Hash::Key.new('jobs', integer),
42
- Transform::Hash::Key.new('requires', string_array)
43
- ],
44
- required: []
45
- ),
46
- Transform::Hash::Symbolize.new
47
- ]
48
- )
49
-
50
28
  MORE_THAN_ONE_CONFIG_FILE = <<~'MESSAGE'
51
29
  Found more than one candidate for use as implicit config file: %s
52
30
  MESSAGE
@@ -57,6 +35,32 @@ module Mutant
57
35
  mutant.yml
58
36
  ].freeze
59
37
 
38
+ private_constant(*constants(false))
39
+
40
+ class CoverageCriteria
41
+ include Anima.new(:timeout, :test_result)
42
+
43
+ DEFAULT = new(
44
+ timeout: false,
45
+ test_result: true
46
+ )
47
+
48
+ TRANSFORM =
49
+ Transform::Sequence.new(
50
+ [
51
+ Transform::Hash.new(
52
+ optional: [
53
+ Transform::Hash::Key.new('timeout', Transform::BOOLEAN),
54
+ Transform::Hash::Key.new('test_result', Transform::BOOLEAN)
55
+ ],
56
+ required: []
57
+ ),
58
+ Transform::Hash::Symbolize.new,
59
+ ->(value) { Either::Right.new(DEFAULT.with(**value)) }
60
+ ]
61
+ )
62
+ end # CoverageCriteria
63
+
60
64
  # Merge with other config
61
65
  #
62
66
  # @param [Config] other
@@ -64,18 +68,17 @@ module Mutant
64
68
  # @return [Config]
65
69
  def merge(other)
66
70
  other.with(
67
- fail_fast: fail_fast || other.fail_fast,
68
- includes: includes + other.includes,
69
- jobs: other.jobs || jobs,
70
- integration: other.integration || integration,
71
- matcher: matcher.merge(other.matcher),
72
- requires: requires + other.requires,
73
- zombie: zombie || other.zombie
71
+ fail_fast: fail_fast || other.fail_fast,
72
+ includes: other.includes + includes,
73
+ jobs: other.jobs || jobs,
74
+ integration: other.integration || integration,
75
+ mutation_timeout: other.mutation_timeout || mutation_timeout,
76
+ matcher: matcher.merge(other.matcher),
77
+ requires: other.requires + requires,
78
+ zombie: zombie || other.zombie
74
79
  )
75
80
  end
76
81
 
77
- private_constant(*constants(false))
78
-
79
82
  # Load config file
80
83
  #
81
84
  # @param [World] world
@@ -98,7 +101,7 @@ module Mutant
98
101
  def self.load_contents(path)
99
102
  Transform::Named
100
103
  .new(path.to_s, TRANSFORM)
101
- .apply(path)
104
+ .call(path)
102
105
  .lmap(&:compact_message)
103
106
  end
104
107
  private_class_method :load_contents
@@ -109,5 +112,27 @@ module Mutant
109
112
  def self.env
110
113
  DEFAULT.with(jobs: Etc.nprocessors)
111
114
  end
115
+
116
+ TRANSFORM = Transform::Sequence.new(
117
+ [
118
+ Transform::Exception.new(SystemCallError, :read.to_proc),
119
+ Transform::Exception.new(YAML::SyntaxError, YAML.method(:safe_load)),
120
+ Transform::Hash.new(
121
+ optional: [
122
+ Transform::Hash::Key.new('coverage_criteria', CoverageCriteria::TRANSFORM),
123
+ Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
124
+ Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
125
+ Transform::Hash::Key.new('integration', Transform::STRING),
126
+ Transform::Hash::Key.new('jobs', Transform::INTEGER),
127
+ Transform::Hash::Key.new('mutation_timeout', Transform::FLOAT),
128
+ Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
129
+ ],
130
+ required: []
131
+ ),
132
+ Transform::Hash::Symbolize.new
133
+ ]
134
+ )
135
+
136
+ private_constant(:TRANSFORM)
112
137
  end # Config
113
138
  end # Mutant
@@ -24,10 +24,15 @@ module Mutant
24
24
  # @param [Config] config
25
25
  #
26
26
  # @return [Env]
27
+ #
28
+ # rubocop:disable Metrics/MethodLength
27
29
  def self.empty(world, config)
28
30
  new(
29
31
  config: config,
30
- integration: Integration::Null.new(config),
32
+ integration: Integration::Null.new(
33
+ expression_parser: config.expression_parser,
34
+ timer: world.timer
35
+ ),
31
36
  matchable_scopes: EMPTY_ARRAY,
32
37
  mutations: EMPTY_ARRAY,
33
38
  parser: Parser.new,
@@ -36,6 +41,7 @@ module Mutant
36
41
  world: world
37
42
  )
38
43
  end
44
+ # rubocop:enable Metrics/MethodLength
39
45
 
40
46
  # Kill mutation
41
47
  #
@@ -43,14 +49,14 @@ module Mutant
43
49
  #
44
50
  # @return [Result::Mutation]
45
51
  def kill(mutation)
46
- start = Timer.now
52
+ start = timer.now
47
53
 
48
54
  tests = selections.fetch(mutation.subject)
49
55
 
50
56
  Result::Mutation.new(
51
57
  isolation_result: run_mutation_tests(mutation, tests),
52
58
  mutation: mutation,
53
- runtime: Timer.now - start
59
+ runtime: timer.now - start
54
60
  )
55
61
  end
56
62
 
@@ -127,7 +133,7 @@ module Mutant
127
133
  private
128
134
 
129
135
  def run_mutation_tests(mutation, tests)
130
- config.isolation.call do
136
+ config.isolation.call(config.mutation_timeout) do
131
137
  result = mutation.insert(world.kernel)
132
138
 
133
139
  if result.equal?(Loader::Result::VoidValue.instance)
@@ -138,5 +144,9 @@ module Mutant
138
144
  end
139
145
  end
140
146
 
147
+ def timer
148
+ world.timer
149
+ end
150
+
141
151
  end # Env
142
152
  end # Mutant
@@ -4,7 +4,7 @@ module Mutant
4
4
 
5
5
  # Abstract base class mutant test framework integrations
6
6
  class Integration
7
- include AbstractType, Adamantium::Flat, Concord.new(:config)
7
+ include AbstractType, Adamantium::Flat, Anima.new(:expression_parser, :timer)
8
8
 
9
9
  LOAD_MESSAGE = <<~'MESSAGE'
10
10
  Unable to load integration mutant-%<integration_name>s:
@@ -27,9 +27,12 @@ module Mutant
27
27
  #
28
28
  # @return [Either<String, Integration>]
29
29
  def self.setup(env)
30
- attempt_require(env)
31
- .bind { attempt_const_get(env) }
32
- .fmap { |klass| klass.new(env.config).setup }
30
+ attempt_require(env).bind { attempt_const_get(env) }.fmap do |klass|
31
+ klass.new(
32
+ expression_parser: env.config.expression_parser,
33
+ timer: env.world.timer
34
+ ).setup
35
+ end
33
36
  end
34
37
 
35
38
  # rubocop:disable Style/MultilineBlockChain
@@ -80,11 +83,5 @@ module Mutant
80
83
  #
81
84
  # @return [Enumerable<Test>]
82
85
  abstract_method :all_tests
83
-
84
- private
85
-
86
- def expression_parser
87
- config.expression_parser
88
- end
89
86
  end # Integration
90
87
  end # Mutant
@@ -18,7 +18,6 @@ module Mutant
18
18
  # @return [Result::Test]
19
19
  def call(tests)
20
20
  Result::Test.new(
21
- output: '',
22
21
  passed: true,
23
22
  runtime: 0.0,
24
23
  tests: tests
@@ -7,57 +7,20 @@ module Mutant
7
7
 
8
8
  # Isolated computation result
9
9
  class Result
10
- include AbstractType, Adamantium
11
-
12
- NULL_LOG = ''
13
-
14
- private_constant(*constants(false))
15
-
16
- abstract_method :error
17
- abstract_method :next
18
- abstract_method :value
19
-
20
- # Add error on top of current result
21
- #
22
- # @param [Result] error
23
- #
24
- # @return [Result]
25
- def add_error(error)
26
- ErrorChain.new(error, self)
27
- end
28
-
29
- # The log captured from integration
30
- #
31
- # @return [String]
32
- def log
33
- NULL_LOG
34
- end
35
-
36
- # Test for success
10
+ include Anima.new(
11
+ :exception,
12
+ :log,
13
+ :process_status,
14
+ :timeout,
15
+ :value
16
+ )
17
+
18
+ # Test for successful result
37
19
  #
38
20
  # @return [Boolean]
39
- def success?
40
- instance_of?(Success)
21
+ def valid_value?
22
+ timeout.nil? && exception.nil? && (process_status.nil? || process_status.success?)
41
23
  end
42
-
43
- # Successful result producing value
44
- class Success < self
45
- include Concord::Public.new(:value, :log)
46
-
47
- def self.new(_value, _log = '')
48
- super
49
- end
50
- end # Success
51
-
52
- # Unsuccessful result by unexpected exception
53
- class Exception < self
54
- include Concord::Public.new(:value)
55
- end # Error
56
-
57
- # Result when there where many results
58
- class ErrorChain < Result
59
- include Concord::Public.new(:value, :next)
60
- end # ChainError
61
24
  end # Result
62
25
 
63
26
  # Call block in isolation
@@ -3,22 +3,36 @@
3
3
  module Mutant
4
4
  class Isolation
5
5
  # Isolation via the fork(2) systemcall.
6
+ #
7
+ # Communication between parent and child process is done
8
+ # via anonymous pipes.
9
+ #
10
+ # Timeouts are initially handled relatively efficiently via IO.select
11
+ # but once the child process pipes are on eof via busy looping on
12
+ # waitpid2 with Process::WNOHANG set.
13
+ #
14
+ # Handling timeouts this way is not the conceptually most
15
+ # efficient solution. But its cross platform.
16
+ #
17
+ # Design constraints:
18
+ #
19
+ # * Support Linux
20
+ # * Support MacOSX
21
+ # * Avoid platform specific APIs and code.
22
+ # * Only use ruby corelib.
23
+ # * Do not use any named resource.
24
+ # * Never block on latency inducing systemcall without a
25
+ # timeout.
26
+ # * Child process freezing before closing the pipes needs to
27
+ # be detected by parent process.
28
+ # * Child process freezing after closing the pipes needs to be
29
+ # detected by parent process.
6
30
  class Fork < self
7
31
  include(Adamantium::Flat, Concord.new(:world))
8
32
 
9
33
  READ_SIZE = 4096
10
34
 
11
- ATTRIBUTES = %i[block log_pipe result_pipe world].freeze
12
-
13
- # Unsuccessful result as child exited nonzero
14
- class ChildError < Result
15
- include Concord::Public.new(:value, :log)
16
- end # ChildError
17
-
18
- # Unsuccessful result as fork failed
19
- class ForkError < Result
20
- include Equalizer.new
21
- end # ForkError
35
+ ATTRIBUTES = %i[block deadline log_pipe result_pipe world].freeze
22
36
 
23
37
  # Pipe abstraction
24
38
  class Pipe
@@ -50,7 +64,6 @@ module Mutant
50
64
  end
51
65
  end # Pipe
52
66
 
53
- # ignore :reek:InstanceVariableAssumption
54
67
  class Parent
55
68
  include(
56
69
  Anima.new(*ATTRIBUTES),
@@ -64,15 +77,28 @@ module Mutant
64
77
  #
65
78
  # @return [Result]
66
79
  def call
67
- pid = start_child or return ForkError.new
68
-
69
- read_child_result(pid)
70
-
71
- @result
80
+ @exception = nil
81
+ @log_fragments = []
82
+ @timeout = nil
83
+ @value = nil
84
+ @pid = start_child
85
+
86
+ read_child_result
87
+ result
72
88
  end
73
89
 
74
90
  private
75
91
 
92
+ def result
93
+ Result.new(
94
+ exception: @exception,
95
+ log: @log_fragments.join,
96
+ process_status: @process_status,
97
+ timeout: @timeout,
98
+ value: @value
99
+ )
100
+ end
101
+
76
102
  def start_child
77
103
  world.process.fork do
78
104
  Child.call(
@@ -85,30 +111,43 @@ module Mutant
85
111
  end
86
112
 
87
113
  # rubocop:disable Metrics/MethodLength
88
- def read_child_result(pid)
114
+ def read_child_result
89
115
  result_fragments = []
90
- log_fragments = []
91
116
 
92
- read_fragments(
93
- log_pipe.parent => log_fragments,
94
- result_pipe.parent => result_fragments
95
- )
117
+ targets =
118
+ {
119
+ log_pipe.parent => @log_fragments,
120
+ result_pipe.parent => result_fragments
121
+ }
96
122
 
97
- begin
98
- result = world.marshal.load(result_fragments.join)
99
- rescue ArgumentError => exception
100
- add_result(Result::Exception.new(exception))
123
+ read_targets(targets)
124
+
125
+ if targets.empty?
126
+ load_result(result_fragments)
127
+ terminate_graceful
101
128
  else
102
- add_result(Result::Success.new(result, log_fragments.join))
129
+ @timeout = deadline.allowed_time
130
+ terminate_ungraceful
103
131
  end
104
- ensure
105
- wait_child(pid, log_fragments)
106
132
  end
107
133
  # rubocop:enable Metrics/MethodLength
108
134
 
109
- def read_fragments(targets)
135
+ def load_result(result_fragments)
136
+ @value = world.marshal.load(result_fragments.join)
137
+ rescue ArgumentError => exception
138
+ @exception = exception
139
+ end
140
+
141
+ # rubocop:disable Metrics/MethodLength
142
+ def read_targets(targets)
110
143
  until targets.empty?
111
- ready, = world.io.select(targets.keys)
144
+ status = deadline.status
145
+
146
+ break unless status.ok?
147
+
148
+ ready, = world.io.select(targets.keys, [], [], status.time_left)
149
+
150
+ break unless ready
112
151
 
113
152
  ready.each do |fd|
114
153
  if fd.eof?
@@ -119,14 +158,42 @@ module Mutant
119
158
  end
120
159
  end
121
160
  end
161
+ # rubocop:enable Metrics/MethodLength
122
162
 
123
- def wait_child(pid, log_fragments)
124
- _pid, status = world.process.wait2(pid)
163
+ # rubocop:disable Metrics/MethodLength
164
+ def terminate_graceful
165
+ status = nil
166
+
167
+ loop do
168
+ status = peek_child
169
+ break if status || deadline.expired?
170
+ world.kernel.sleep(0.1)
171
+ end
125
172
 
126
- unless status.success? # rubocop:disable Style/GuardClause
127
- add_result(ChildError.new(status, log_fragments.join))
173
+ if status
174
+ handle_status(status)
175
+ else
176
+ terminate_ungraceful
128
177
  end
129
178
  end
179
+ # rubocop:enable Metrics/MethodLength
180
+
181
+ def terminate_ungraceful
182
+ world.process.kill('KILL', @pid)
183
+
184
+ _pid, status = world.process.wait2(@pid)
185
+
186
+ handle_status(status)
187
+ end
188
+
189
+ def handle_status(status)
190
+ @process_status = status
191
+ end
192
+
193
+ def peek_child
194
+ _pid, status = world.process.wait2(@pid, Process::WNOHANG)
195
+ status
196
+ end
130
197
 
131
198
  def add_result(result)
132
199
  @result = defined?(@result) ? @result.add_error(result) : result
@@ -157,17 +224,16 @@ module Mutant
157
224
  # Call block in isolation
158
225
  #
159
226
  # @return [Result]
160
- # execution result
161
- #
162
- # ignore :reek:NestedIterators
163
227
  #
164
228
  # rubocop:disable Metrics/MethodLength
165
- def call(&block)
229
+ def call(timeout, &block)
230
+ deadline = world.deadline(timeout)
166
231
  io = world.io
167
232
  Pipe.with(io) do |result|
168
233
  Pipe.with(io) do |log|
169
234
  Parent.call(
170
235
  block: block,
236
+ deadline: deadline,
171
237
  log_pipe: log,
172
238
  result_pipe: result,
173
239
  world: world
@@ -176,6 +242,7 @@ module Mutant
176
242
  end
177
243
  end
178
244
  # rubocop:enable Metrics/MethodLength
245
+
179
246
  end # Fork
180
247
  end # Isolation
181
248
  end # Mutant