mutant 0.7.3 → 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.travis.yml +3 -2
  4. data/Changelog.md +7 -2
  5. data/Gemfile +0 -1
  6. data/Gemfile.devtools +9 -37
  7. data/README.md +1 -1
  8. data/circle.yml +1 -1
  9. data/config/flay.yml +1 -1
  10. data/config/reek.yml +6 -19
  11. data/config/rubocop.yml +58 -63
  12. data/lib/mutant.rb +8 -4
  13. data/lib/mutant/ast.rb +1 -1
  14. data/lib/mutant/cli.rb +12 -6
  15. data/lib/mutant/env.rb +17 -1
  16. data/lib/mutant/isolation.rb +4 -2
  17. data/lib/mutant/loader.rb +4 -0
  18. data/lib/mutant/matcher/compiler.rb +2 -0
  19. data/lib/mutant/mutation.rb +2 -0
  20. data/lib/mutant/mutator/node/const.rb +1 -1
  21. data/lib/mutant/mutator/node/generic.rb +1 -1
  22. data/lib/mutant/mutator/node/if.rb +2 -0
  23. data/lib/mutant/parallel.rb +93 -0
  24. data/lib/mutant/{runner → parallel}/master.rb +90 -45
  25. data/lib/mutant/parallel/source.rb +73 -0
  26. data/lib/mutant/{runner → parallel}/worker.rb +13 -30
  27. data/lib/mutant/reporter/cli.rb +8 -11
  28. data/lib/mutant/reporter/cli/printer.rb +14 -8
  29. data/lib/mutant/result.rb +0 -10
  30. data/lib/mutant/runner.rb +49 -43
  31. data/lib/mutant/runner/{scheduler.rb → sink.rb} +9 -68
  32. data/lib/mutant/version.rb +1 -1
  33. data/lib/mutant/zombifier/file.rb +28 -9
  34. data/meta/if.rb +8 -0
  35. data/meta/match_current_line.rb +1 -0
  36. data/spec/integration/mutant/corpus_spec.rb +1 -1
  37. data/spec/integration/mutant/null_spec.rb +1 -1
  38. data/spec/integration/mutant/rspec_spec.rb +1 -1
  39. data/spec/integration/mutant/test_mutator_handles_types_spec.rb +1 -1
  40. data/spec/integration/mutant/zombie_spec.rb +1 -1
  41. data/spec/support/corpus.rb +2 -0
  42. data/spec/support/fake_actor.rb +20 -10
  43. data/spec/support/mutation_verifier.rb +2 -0
  44. data/spec/support/shared_context.rb +12 -19
  45. data/spec/unit/mutant/env_spec.rb +20 -2
  46. data/spec/unit/mutant/expression_spec.rb +4 -1
  47. data/spec/unit/mutant/parallel/master_spec.rb +339 -0
  48. data/spec/unit/mutant/parallel/source/array_spec.rb +47 -0
  49. data/spec/unit/mutant/{runner → parallel}/worker_spec.rb +23 -26
  50. data/spec/unit/mutant/parallel_spec.rb +16 -0
  51. data/spec/unit/mutant/reporter/cli_spec.rb +1 -1
  52. data/spec/unit/mutant/reporter/trace_spec.rb +9 -0
  53. data/spec/unit/mutant/result/env_spec.rb +0 -55
  54. data/spec/unit/mutant/runner/driver_spec.rb +26 -0
  55. data/spec/unit/mutant/runner/sink_spec.rb +162 -0
  56. data/spec/unit/mutant/runner_spec.rb +60 -63
  57. data/spec/unit/mutant/warning_filter_spec.rb +2 -1
  58. data/test_app/Gemfile.devtools +9 -37
  59. metadata +21 -11
  60. data/spec/unit/mutant/runner/master_spec.rb +0 -199
  61. data/spec/unit/mutant/runner/scheduler_spec.rb +0 -161
@@ -1,4 +1,4 @@
1
1
  module Mutant
2
2
  # The current mutant version
3
- VERSION = '0.7.3'.freeze
3
+ VERSION = '0.7.4'.freeze
4
4
  end # Mutant
@@ -10,6 +10,10 @@ module Mutant
10
10
  #
11
11
  # @api private
12
12
  #
13
+ # Probably one of the only valid uses of eval.
14
+ #
15
+ # rubocop:disable Lint/Eval
16
+ #
13
17
  def zombify(namespace)
14
18
  $stderr.puts("Zombifying #{path}")
15
19
  eval(
@@ -33,15 +37,7 @@ module Mutant
33
37
  # @api private
34
38
  #
35
39
  def self.find(logical_name)
36
- file_name =
37
- case ::File.extname(logical_name)
38
- when '.so'
39
- return
40
- when '.rb'
41
- logical_name
42
- else
43
- "#{logical_name}.rb"
44
- end
40
+ file_name = expand_file_name(logical_name)
45
41
 
46
42
  $LOAD_PATH.each do |path|
47
43
  path = Pathname.new(path).join(file_name)
@@ -52,6 +48,29 @@ module Mutant
52
48
  nil
53
49
  end
54
50
 
51
+ # Return expanded file name
52
+ #
53
+ # @param [String] logical_name
54
+ #
55
+ # @return [nil]
56
+ # if no expansion is possible
57
+ #
58
+ # @return [String]
59
+ #
60
+ # @api private
61
+ #
62
+ def self.expand_file_name(logical_name)
63
+ case ::File.extname(logical_name)
64
+ when '.so'
65
+ return
66
+ when '.rb'
67
+ logical_name
68
+ else
69
+ "#{logical_name}.rb"
70
+ end
71
+ end
72
+ private_class_method :expand_file_name
73
+
55
74
  private
56
75
 
57
76
  # Return node
data/meta/if.rb CHANGED
@@ -14,6 +14,12 @@ Mutant::Meta::Example.add do
14
14
  # Deleted else branch
15
15
  mutation 'if condition; true end'
16
16
 
17
+ # Promote if branch
18
+ mutation 'true'
19
+
20
+ # Promote else branch
21
+ mutation 'false'
22
+
17
23
  # Deleted if branch resulting in unless rendering
18
24
  mutation 'unless condition; false; end'
19
25
 
@@ -39,6 +45,7 @@ Mutant::Meta::Example.add do
39
45
  mutation 'if true; true; end'
40
46
  mutation 'if false; true; end'
41
47
  mutation 'if nil; true; end'
48
+ mutation 'true'
42
49
  end
43
50
 
44
51
  Mutant::Meta::Example.add do
@@ -52,4 +59,5 @@ Mutant::Meta::Example.add do
52
59
  mutation 'unless condition; false; end'
53
60
  mutation 'unless condition; nil; end'
54
61
  mutation 'if condition; true; end'
62
+ mutation 'true'
55
63
  end
@@ -11,4 +11,5 @@ Mutant::Meta::Example.add do
11
11
  mutation 'true if false'
12
12
  mutation 'true if nil'
13
13
  mutation 'true if /a\A/'
14
+ mutation 'true'
14
15
  end
@@ -1,4 +1,4 @@
1
- RSpec.describe 'Mutant on ruby corpus' do
1
+ RSpec.describe 'Mutant on ruby corpus', mutant: false do
2
2
 
3
3
  before do
4
4
  skip 'Corpus test is deactivated on < 2.1' if RUBY_VERSION < '2.1'
@@ -1,4 +1,4 @@
1
- RSpec.describe 'null integration' do
1
+ RSpec.describe 'null integration', mutant: false do
2
2
 
3
3
  let(:base_cmd) { 'bundle exec mutant -I lib --require test_app "TestApp*"' }
4
4
 
@@ -1,4 +1,4 @@
1
- RSpec.describe 'rspec integration' do
1
+ RSpec.describe 'rspec integration', mutant: false do
2
2
 
3
3
  let(:base_cmd) { 'bundle exec mutant -I lib --require test_app --use rspec' }
4
4
 
@@ -1,4 +1,4 @@
1
- RSpec.describe do
1
+ RSpec.describe 'AST type coverage', mutant: false do
2
2
 
3
3
  specify 'mutant should not crash for any node parser can generate' do
4
4
  Mutant::AST::Types::ALL.each do |type|
@@ -1,4 +1,4 @@
1
- RSpec.describe 'as a zombie' do
1
+ RSpec.describe 'as a zombie', mutant: false do
2
2
  specify 'it allows to create zombie from mutant' do
3
3
  expect { Mutant.zombify }.to change { defined?(Zombie) }.from(nil).to('constant')
4
4
  expect(Zombie.constants).to include(:Mutant)
@@ -47,6 +47,8 @@ module Corpus
47
47
  # otherwise
48
48
  #
49
49
  # rubocop:disable MethodLength
50
+ # rubocop:disable AbcSize
51
+ #
50
52
  def verify_mutation_generation
51
53
  checkout
52
54
  start = Time.now
@@ -3,34 +3,44 @@ require 'mutant/actor'
3
3
  # A fake actor used from specs
4
4
  module FakeActor
5
5
  class Expectation
6
- include Concord::Public.new(:name, :message)
6
+ include Concord::Public.new(:name, :message, :block)
7
+ include Equalizer.new(:name, :message)
8
+
9
+ def self.new(_name, _message, _block = nil)
10
+ super
11
+ end
12
+
13
+ def verify(other)
14
+ unless eql?(other)
15
+ fail "Got:\n#{other.inspect}\nExpected:\n#{inspect}"
16
+ end
17
+ block.call(other.message) if block
18
+ end
7
19
  end
8
20
 
9
21
  class MessageSequence
10
- include Adamantium::Flat, Concord.new(:messages)
22
+ include Adamantium::Flat, Concord::Public.new(:messages)
11
23
 
12
24
  def self.new
13
25
  super([])
14
26
  end
15
27
 
16
- def add(name, *message_arguments)
17
- messages << Expectation.new(name, Mutant::Actor::Message.new(*message_arguments))
28
+ def add(name, *message_arguments, &block)
29
+ messages << Expectation.new(name, Mutant::Actor::Message.new(*message_arguments), block)
18
30
  self
19
31
  end
20
32
 
21
33
  def sending(expectation)
22
- raise "Unexpected send: #{expectation.inspect}" if messages.empty?
34
+ fail "Unexpected send: #{expectation.inspect}" if messages.empty?
23
35
  expected = messages.shift
24
- unless expectation.eql?(expected)
25
- raise "Got:\n#{expectation.inspect}\nExpected:\n#{expected.inspect}"
26
- end
36
+ expected.verify(expectation)
27
37
  self
28
38
  end
29
39
 
30
40
  def receiving(name)
31
- raise "No message to read for #{name.inspect}" if messages.empty?
41
+ fail "No message to read for #{name.inspect}" if messages.empty?
32
42
  expected = messages.shift
33
- raise "Unexpected message #{expected.inspect} for #{name.inspect}" unless expected.name.eql?(name)
43
+ fail "Unexpected message #{expected.inspect} for #{name.inspect}" unless expected.name.eql?(name)
34
44
  expected.message
35
45
  end
36
46
 
@@ -43,6 +43,8 @@ private
43
43
  #
44
44
  # @api private
45
45
  #
46
+ # rubocop:disable AbcSize
47
+ #
46
48
  def mutation_report
47
49
  message = ['Original-AST:', original_node.inspect, 'Original-Source:', Unparser.unparse(original_node)]
48
50
  if missing.any?
@@ -8,16 +8,17 @@ module SharedContext
8
8
  def messages(&block)
9
9
  let(:message_sequence) do
10
10
  FakeActor::MessageSequence.new.tap do |sequence|
11
- sequence.instance_eval(&block)
11
+ sequence.instance_eval(&block)
12
12
  end
13
13
  end
14
14
  end
15
15
 
16
16
  # rubocop:disable MethodLength
17
+ # rubocop:disable AbcSize
17
18
  def setup_shared_context
18
19
  let(:env) { double('env', config: config, subjects: [subject_a], mutations: mutations) }
19
- let(:job_a) { Mutant::Runner::Job.new(index: 0, mutation: mutation_a) }
20
- let(:job_b) { Mutant::Runner::Job.new(index: 1, mutation: mutation_b) }
20
+ let(:job_a) { Mutant::Parallel::Job.new(index: 0, payload: mutation_a) }
21
+ let(:job_b) { Mutant::Parallel::Job.new(index: 1, payload: mutation_b) }
21
22
  let(:job_a_result) { Mutant::Runner::JobResult.new(job: job_a, result: mutation_a_result) }
22
23
  let(:job_b_result) { Mutant::Runner::JobResult.new(job: job_b, result: mutation_b_result) }
23
24
  let(:mutations) { [mutation_a, mutation_b] }
@@ -27,6 +28,14 @@ module SharedContext
27
28
  let(:actor_names) { [] }
28
29
  let(:message_sequence) { FakeActor::MessageSequence.new }
29
30
 
31
+ let(:status) do
32
+ Mutant::Parallel::Status.new(
33
+ active_jobs: [].to_set,
34
+ payload: env_result,
35
+ done: true
36
+ )
37
+ end
38
+
30
39
  let(:config) do
31
40
  Mutant::Config::DEFAULT.update(
32
41
  actor_env: actor_env,
@@ -53,22 +62,6 @@ module SharedContext
53
62
  allow(subject_a).to receive(:mutations).and_return([mutation_a, mutation_b])
54
63
  end
55
64
 
56
- let(:empty_status) do
57
- Mutant::Runner::Status.new(
58
- active_jobs: Set.new,
59
- env_result: env_result.update(subject_results: [], runtime: 0.0),
60
- done: false
61
- )
62
- end
63
-
64
- let(:status) do
65
- Mutant::Runner::Status.new(
66
- active_jobs: Set.new,
67
- env_result: env_result,
68
- done: true
69
- )
70
- end
71
-
72
65
  let(:env_result) do
73
66
  Mutant::Result::Env.new(
74
67
  env: env,
@@ -8,11 +8,13 @@ RSpec.describe Mutant::Env do
8
8
  it 'warns via reporter' do
9
9
  klass = Class.new do
10
10
  def self.name
11
- raise
11
+ fail
12
12
  end
13
13
  end
14
14
 
15
- expected_warnings = ["Class#name from: #{klass} raised an error: RuntimeError. #{Mutant::Env::SEMANTICS_MESSAGE}"]
15
+ expected_warnings = [
16
+ "Class#name from: #{klass} raised an error: RuntimeError. #{Mutant::Env::SEMANTICS_MESSAGE}"
17
+ ]
16
18
 
17
19
  expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings)
18
20
 
@@ -46,4 +48,20 @@ RSpec.describe Mutant::Env do
46
48
  end
47
49
  end
48
50
  end
51
+
52
+ context '#kill_mutation' do
53
+ let(:object) { described_class.new(config) }
54
+ let(:result) { double('Result') }
55
+ let(:mutation) { double('Mutation') }
56
+
57
+ subject { object.kill_mutation(mutation) }
58
+
59
+ before do
60
+ expect(mutation).to receive(:kill).with(config.isolation, config.integration).and_return(result)
61
+ end
62
+
63
+ it 'uses the configured integration and isolation to kill mutation' do
64
+ should eql(Mutant::Result::Mutation.new(mutation: mutation, test_result: result))
65
+ end
66
+ end
49
67
  end
@@ -51,7 +51,10 @@ RSpec.describe Mutant::Expression do
51
51
  let(:input) { 'foo bar' }
52
52
 
53
53
  it 'raises an exception' do
54
- expect { subject }.to raise_error(Mutant::Expression::InvalidExpressionError, 'Expression: "foo bar" is not valid')
54
+ expect { subject }.to raise_error(
55
+ Mutant::Expression::InvalidExpressionError,
56
+ 'Expression: "foo bar" is not valid'
57
+ )
55
58
  end
56
59
  end
57
60
 
@@ -0,0 +1,339 @@
1
+ RSpec.describe Mutant::Parallel::Master do
2
+ let(:message_sequence) { FakeActor::MessageSequence.new }
3
+ let(:actor_names) { [:master, :worker_a, :worker_b] }
4
+ let(:status) { double('Status') }
5
+ let(:sink) { FakeSink.new }
6
+ let(:processor) { double('Processor') }
7
+ let(:worker_a) { actor_env.mailbox(:worker_a).sender }
8
+ let(:worker_b) { actor_env.mailbox(:worker_b).sender }
9
+ let(:parent) { actor_env.mailbox(:parent).sender }
10
+ let(:job_payload_a) { double('Job Payload A') }
11
+ let(:job_payload_b) { double('Job Payload B') }
12
+ let(:job_result_payload_a) { double('Job Result Payload A') }
13
+ let(:job_result_payload_b) { double('Job Result Payload B') }
14
+ let(:job_a) { Mutant::Parallel::Job.new(index: 0, payload: job_payload_a) }
15
+ let(:job_b) { Mutant::Parallel::Job.new(index: 1, payload: job_payload_b) }
16
+ let(:job_result_a) { Mutant::Parallel::JobResult.new(job: job_a, payload: job_result_payload_a) }
17
+ let(:job_result_b) { Mutant::Parallel::JobResult.new(job: job_b, payload: job_result_payload_b) }
18
+
19
+ let(:actor_env) do
20
+ FakeActor::Env.new(message_sequence, actor_names)
21
+ end
22
+
23
+ shared_examples_for 'master behavior' do
24
+ it { should eql(actor_env.mailbox(:master).sender) }
25
+
26
+ it 'has expected results in sink' do
27
+ subject
28
+ expect(sink.results).to eql(expected_results)
29
+ end
30
+
31
+ it 'consumes all messages' do
32
+ subject
33
+ expect(message_sequence.messages).to eql([])
34
+ end
35
+ end
36
+
37
+ let(:config) do
38
+ Mutant::Parallel::Config.new(
39
+ jobs: 1,
40
+ env: actor_env,
41
+ source: Mutant::Parallel::Source::Array.new([job_payload_a, job_payload_b]),
42
+ sink: sink,
43
+ processor: processor
44
+ )
45
+ end
46
+
47
+ class FakeSink
48
+ def initialize
49
+ @results = []
50
+ @stop = false
51
+ end
52
+
53
+ attr_reader :results
54
+
55
+ def status
56
+ @results.length
57
+ end
58
+
59
+ def result(result)
60
+ @results << result
61
+ end
62
+
63
+ def stop
64
+ @stop = true
65
+ self
66
+ end
67
+
68
+ def stop?
69
+ @stop
70
+ end
71
+ end
72
+
73
+ # Needed because of rubies undefined-ivar-read-is-nil stuff
74
+ describe 'object initialization' do
75
+ let(:object) { described_class.send(:new, config, actor_env.mailbox(:master)) }
76
+
77
+ it 'initializes falsy ivars'do
78
+ expect(object.instance_variable_get(:@stop)).to be(false)
79
+ end
80
+ end
81
+
82
+ describe '.call' do
83
+
84
+ before do
85
+ expect(Mutant::Parallel::Worker).to receive(:run).with(
86
+ actor: actor_env.mailbox(:worker_a),
87
+ processor: processor,
88
+ parent: actor_env.mailbox(:master).sender
89
+ ).and_return(worker_a)
90
+ end
91
+
92
+ subject { described_class.call(config) }
93
+
94
+ context 'with multiple workers configured' do
95
+ let(:config) { super().update(jobs: 2) }
96
+ let(:expected_results) { [] }
97
+
98
+ before do
99
+ expect(Mutant::Parallel::Worker).to receive(:run).with(
100
+ actor: actor_env.mailbox(:worker_b),
101
+ processor: processor,
102
+ parent: actor_env.mailbox(:master).sender
103
+ ).and_return(worker_b)
104
+
105
+ sink.stop
106
+
107
+ message_sequence.add(:master, :ready, worker_a)
108
+ message_sequence.add(:worker_a, :stop)
109
+
110
+ message_sequence.add(:master, :ready, worker_b)
111
+ message_sequence.add(:worker_b, :stop)
112
+
113
+ message_sequence.add(:master, :stop, parent)
114
+ message_sequence.add(:parent, :stop)
115
+ end
116
+
117
+ include_examples 'master behavior'
118
+ end
119
+
120
+ context 'explicit stop by scheduler state' do
121
+ context 'before jobs are processed' do
122
+ let(:expected_results) { [] }
123
+
124
+ before do
125
+ sink.stop
126
+
127
+ message_sequence.add(:master, :ready, worker_a)
128
+ message_sequence.add(:worker_a, :stop)
129
+
130
+ message_sequence.add(:master, :stop, parent)
131
+ message_sequence.add(:parent, :stop)
132
+ end
133
+
134
+ include_examples 'master behavior'
135
+ end
136
+
137
+ context 'while jobs are processed' do
138
+ let(:expected_results) { [job_result_payload_a] }
139
+
140
+ let(:sink) do
141
+ super().instance_eval do
142
+ def stop?
143
+ @results.length.equal?(1)
144
+ end
145
+ self
146
+ end
147
+ end
148
+
149
+ before do
150
+ message_sequence.add(:master, :ready, worker_a)
151
+ message_sequence.add(:worker_a, :job, job_a)
152
+ message_sequence.add(:master, :result, job_result_a)
153
+
154
+ message_sequence.add(:master, :ready, worker_a)
155
+ message_sequence.add(:worker_a, :stop)
156
+
157
+ message_sequence.add(:master, :stop, parent)
158
+ message_sequence.add(:parent, :stop)
159
+ end
160
+
161
+ it { should eql(actor_env.mailbox(:master).sender) }
162
+
163
+ it 'consumes all messages' do
164
+ expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
165
+ end
166
+ end
167
+ end
168
+
169
+ context 'external stop' do
170
+ context 'after jobs are done' do
171
+ let(:expected_results) { [job_result_payload_a, job_result_payload_b] }
172
+
173
+ before do
174
+ message_sequence.add(:master, :ready, worker_a)
175
+ message_sequence.add(:worker_a, :job, job_a)
176
+ message_sequence.add(:master, :result, job_result_a)
177
+
178
+ message_sequence.add(:master, :ready, worker_a)
179
+ message_sequence.add(:worker_a, :job, job_b)
180
+ message_sequence.add(:master, :result, job_result_b)
181
+
182
+ message_sequence.add(:master, :ready, worker_a)
183
+ message_sequence.add(:worker_a, :stop)
184
+
185
+ message_sequence.add(:master, :stop, parent)
186
+ message_sequence.add(:parent, :stop)
187
+ end
188
+
189
+ include_examples 'master behavior'
190
+ end
191
+
192
+ context 'when no jobs are active' do
193
+ let(:expected_results) { [job_result_payload_a] }
194
+
195
+ before do
196
+ message_sequence.add(:master, :ready, worker_a)
197
+ message_sequence.add(:worker_a, :job, job_a)
198
+ message_sequence.add(:master, :stop, parent)
199
+ message_sequence.add(:master, :result, job_result_a)
200
+
201
+ message_sequence.add(:master, :ready, worker_a)
202
+ message_sequence.add(:worker_a, :stop)
203
+
204
+ message_sequence.add(:parent, :stop)
205
+ end
206
+
207
+ include_examples 'master behavior'
208
+ end
209
+
210
+ context 'before any job got processed' do
211
+ let(:expected_results) { [] }
212
+
213
+ before do
214
+ message_sequence.add(:master, :stop, parent)
215
+ message_sequence.add(:master, :ready, worker_a)
216
+ message_sequence.add(:worker_a, :stop)
217
+ message_sequence.add(:parent, :stop)
218
+ end
219
+
220
+ include_examples 'master behavior'
221
+ end
222
+ end
223
+
224
+ context 'requesting status' do
225
+ context 'when jobs are done' do
226
+ let(:expected_status) { Mutant::Parallel::Status.new(payload: 2, active_jobs: Set.new, done: true) }
227
+ let(:expected_results) { [job_result_payload_a, job_result_payload_b] }
228
+
229
+ before do
230
+ message_sequence.add(:master, :ready, worker_a)
231
+ message_sequence.add(:worker_a, :job, job_a)
232
+ message_sequence.add(:master, :result, job_result_a)
233
+
234
+ message_sequence.add(:master, :ready, worker_a)
235
+ message_sequence.add(:worker_a, :job, job_b)
236
+ message_sequence.add(:master, :result, job_result_b)
237
+
238
+ message_sequence.add(:master, :ready, worker_a)
239
+ message_sequence.add(:worker_a, :stop)
240
+
241
+ message_sequence.add(:master, :status, parent)
242
+
243
+ # Special bit to kill a mutation that results in mutable active_jobs being passed.
244
+ message_sequence.add(:parent, :status, expected_status) do |message|
245
+ expect(message.payload.active_jobs.frozen?).to be(true)
246
+ end
247
+ message_sequence.add(:master, :stop, parent)
248
+ message_sequence.add(:parent, :stop)
249
+ end
250
+
251
+ include_examples 'master behavior'
252
+ end
253
+
254
+ context 'just after scheduler stops' do
255
+ let(:expected_status) { Mutant::Parallel::Status.new(payload: 1, active_jobs: [].to_set, done: true) }
256
+ let(:expected_results) { [job_result_payload_a] }
257
+
258
+ let(:sink) do
259
+ super().instance_eval do
260
+ def stop?
261
+ @results.length.equal?(1)
262
+ end
263
+ self
264
+ end
265
+ end
266
+
267
+ before do
268
+ message_sequence.add(:master, :ready, worker_a)
269
+ message_sequence.add(:worker_a, :job, job_a)
270
+ message_sequence.add(:master, :result, job_result_a)
271
+
272
+ message_sequence.add(:master, :status, parent)
273
+ message_sequence.add(:parent, :status, expected_status)
274
+
275
+ message_sequence.add(:master, :ready, worker_a)
276
+ message_sequence.add(:worker_a, :stop)
277
+
278
+ message_sequence.add(:master, :stop, parent)
279
+ message_sequence.add(:parent, :stop)
280
+ end
281
+
282
+ include_examples 'master behavior'
283
+ end
284
+
285
+ context 'when jobs are active' do
286
+ let(:expected_status) { Mutant::Parallel::Status.new(payload: 1, active_jobs: [job_b].to_set, done: false) }
287
+ let(:expected_results) { [job_result_payload_a, job_result_payload_b] }
288
+
289
+ before do
290
+ message_sequence.add(:master, :ready, worker_a)
291
+ message_sequence.add(:worker_a, :job, job_a)
292
+ message_sequence.add(:master, :result, job_result_a)
293
+
294
+ message_sequence.add(:master, :ready, worker_a)
295
+ message_sequence.add(:worker_a, :job, job_b)
296
+
297
+ message_sequence.add(:master, :status, parent)
298
+ message_sequence.add(:parent, :status, expected_status)
299
+
300
+ message_sequence.add(:master, :result, job_result_b)
301
+
302
+ message_sequence.add(:master, :ready, worker_a)
303
+ message_sequence.add(:worker_a, :stop)
304
+
305
+ message_sequence.add(:master, :stop, parent)
306
+ message_sequence.add(:parent, :stop)
307
+ end
308
+
309
+ include_examples 'master behavior'
310
+ end
311
+
312
+ context 'before jobs are done' do
313
+ let(:expected_status) { Mutant::Parallel::Status.new(payload: 0, active_jobs: Set.new, done: false) }
314
+ let(:expected_results) { [] }
315
+
316
+ before do
317
+ message_sequence.add(:master, :status, parent)
318
+ message_sequence.add(:parent, :status, expected_status)
319
+ message_sequence.add(:master, :stop, parent)
320
+ message_sequence.add(:master, :ready, worker_a)
321
+ message_sequence.add(:worker_a, :stop)
322
+ message_sequence.add(:parent, :stop)
323
+ end
324
+
325
+ include_examples 'master behavior'
326
+ end
327
+ end
328
+
329
+ context 'unhandled message received' do
330
+ before do
331
+ message_sequence.add(:master, :foo, parent)
332
+ end
333
+
334
+ it 'raises message' do
335
+ expect { subject }.to raise_error(Mutant::Actor::ProtocolError, 'Unexpected message: :foo')
336
+ end
337
+ end
338
+ end
339
+ end