mutant 0.7.3 → 0.7.4

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 (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