listen 2.7.5 → 2.7.6

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/.gitignore +0 -0
  3. data/.rspec +0 -0
  4. data/.rubocop.yml +0 -0
  5. data/.travis.yml +0 -1
  6. data/.yardopts +0 -0
  7. data/CHANGELOG.md +0 -0
  8. data/CONTRIBUTING.md +0 -0
  9. data/Gemfile +25 -4
  10. data/Guardfile +0 -0
  11. data/LICENSE.txt +0 -0
  12. data/README.md +18 -10
  13. data/Rakefile +0 -0
  14. data/lib/listen.rb +2 -4
  15. data/lib/listen/adapter.rb +13 -4
  16. data/lib/listen/adapter/base.rb +33 -16
  17. data/lib/listen/adapter/bsd.rb +21 -38
  18. data/lib/listen/adapter/darwin.rb +17 -25
  19. data/lib/listen/adapter/linux.rb +34 -52
  20. data/lib/listen/adapter/polling.rb +9 -25
  21. data/lib/listen/adapter/tcp.rb +27 -14
  22. data/lib/listen/adapter/windows.rb +67 -23
  23. data/lib/listen/change.rb +26 -23
  24. data/lib/listen/cli.rb +0 -0
  25. data/lib/listen/directory.rb +47 -58
  26. data/lib/listen/file.rb +66 -101
  27. data/lib/listen/listener.rb +214 -155
  28. data/lib/listen/queue_optimizer.rb +104 -0
  29. data/lib/listen/record.rb +15 -5
  30. data/lib/listen/silencer.rb +14 -10
  31. data/lib/listen/tcp.rb +0 -1
  32. data/lib/listen/tcp/broadcaster.rb +31 -26
  33. data/lib/listen/tcp/message.rb +2 -2
  34. data/lib/listen/version.rb +1 -1
  35. data/listen.gemspec +1 -1
  36. data/spec/acceptance/listen_spec.rb +151 -239
  37. data/spec/acceptance/tcp_spec.rb +125 -134
  38. data/spec/lib/listen/adapter/base_spec.rb +13 -30
  39. data/spec/lib/listen/adapter/bsd_spec.rb +7 -35
  40. data/spec/lib/listen/adapter/darwin_spec.rb +18 -30
  41. data/spec/lib/listen/adapter/linux_spec.rb +49 -55
  42. data/spec/lib/listen/adapter/polling_spec.rb +20 -35
  43. data/spec/lib/listen/adapter/tcp_spec.rb +25 -27
  44. data/spec/lib/listen/adapter/windows_spec.rb +7 -33
  45. data/spec/lib/listen/adapter_spec.rb +10 -10
  46. data/spec/lib/listen/change_spec.rb +55 -57
  47. data/spec/lib/listen/directory_spec.rb +105 -155
  48. data/spec/lib/listen/file_spec.rb +186 -73
  49. data/spec/lib/listen/listener_spec.rb +233 -216
  50. data/spec/lib/listen/record_spec.rb +60 -22
  51. data/spec/lib/listen/silencer_spec.rb +48 -75
  52. data/spec/lib/listen/tcp/broadcaster_spec.rb +78 -69
  53. data/spec/lib/listen/tcp/listener_spec.rb +28 -71
  54. data/spec/lib/listen/tcp/message_spec.rb +48 -14
  55. data/spec/lib/listen_spec.rb +3 -3
  56. data/spec/spec_helper.rb +6 -3
  57. data/spec/support/acceptance_helper.rb +250 -31
  58. data/spec/support/fixtures_helper.rb +6 -4
  59. data/spec/support/platform_helper.rb +2 -2
  60. metadata +5 -5
  61. data/lib/listen/tcp/listener.rb +0 -108
@@ -1,29 +1,32 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Listen::Record do
4
- let(:registry) { double(Celluloid::Registry) }
5
- let(:listener) { double(Listen::Listener, registry: registry, options: {}) }
4
+ let(:registry) { instance_double(Celluloid::Registry) }
5
+
6
+ let(:listener) do
7
+ instance_double(Listen::Listener, registry: registry, options: {})
8
+ end
9
+
6
10
  let(:record) { Listen::Record.new(listener) }
7
11
  let(:path) { '/dir/path/file.rb' }
8
- let(:data) { { type: 'File' } }
9
12
 
10
13
  describe '#set_path' do
11
- it 'sets path by spliting direname and basename' do
12
- record.set_path(path, data)
13
- expect(record.paths).to eq('/dir/path' => { 'file.rb' => data })
14
+ it 'sets path by spliting dirname and basename' do
15
+ record.set_path(:file, path)
16
+ expect(record.paths['/dir/path']).to eq('file.rb' => { type: :file })
14
17
  end
15
18
 
16
19
  it 'sets path and keeps old data not overwritten' do
17
- record.set_path(path, data.merge(foo: 1, bar: 2))
18
- record.set_path(path, data.merge(foo: 3))
19
- expected = { '/dir/path' => { 'file.rb' => data.merge(foo: 3, bar: 2) } }
20
- expect(record.paths).to eq(expected)
20
+ record.set_path(:file, path, foo: 1, bar: 2)
21
+ record.set_path(:file, path, foo: 3)
22
+ file_data = record.paths['/dir/path']['file.rb']
23
+ expect(file_data).to eq(foo: 3, bar: 2, type: :file)
21
24
  end
22
25
  end
23
26
 
24
27
  describe '#unset_path' do
25
28
  context 'path is present' do
26
- before { record.set_path(path, data) }
29
+ before { record.set_path(:file, path) }
27
30
 
28
31
  it 'unsets path' do
29
32
  record.unset_path(path)
@@ -41,10 +44,10 @@ describe Listen::Record do
41
44
 
42
45
  describe '#file_data' do
43
46
  context 'path is present' do
44
- before { record.set_path(path, data) }
47
+ before { record.set_path(:file, path) }
45
48
 
46
49
  it 'returns file data' do
47
- expect(record.file_data(path)).to eq data
50
+ expect(record.file_data(path)).to eq(type: :file)
48
51
  end
49
52
  end
50
53
 
@@ -57,10 +60,11 @@ describe Listen::Record do
57
60
 
58
61
  describe '#dir_entries' do
59
62
  context 'path is present' do
60
- before { record.set_path(path, data) }
63
+ before { record.set_path(:file, path) }
61
64
 
62
65
  it 'returns file path' do
63
- expect(record.dir_entries('/dir/path')).to eq('file.rb' => data)
66
+ entries = record.dir_entries('/dir/path')
67
+ expect(entries).to eq('file.rb' => { type: :file })
64
68
  end
65
69
  end
66
70
 
@@ -73,24 +77,58 @@ describe Listen::Record do
73
77
 
74
78
  describe '#build' do
75
79
  let(:directories) { ['dir_path'] }
76
- let(:change_pool) { double(Listen::Change, terminate: true) }
80
+
81
+ let(:actor) do
82
+ instance_double(Listen::Change, change: nil, terminate: true)
83
+ end
84
+
77
85
  before do
78
- change_pool.stub(:change)
79
- registry.stub(:[]).with(:change_pool) { change_pool }
80
- listener.stub(:directories) { directories }
86
+ allow(listener).to receive(:directories) { directories }
87
+ allow(listener).to receive(:sync).with(:change_pool) { actor }
81
88
  end
82
89
 
83
90
  it 're-inits paths' do
84
- record.set_path(path, data)
91
+ record.set_path(:file, path)
85
92
  record.build
86
93
  expect(record.file_data(path)).to be_empty
87
94
  end
88
95
 
89
96
  it 'calls change asynchronously on all directories to build record' do
90
- expect(change_pool).to receive(:change).
91
- with('dir_path', type: 'Dir', recursive: true, silence: true)
97
+ expect(actor).to receive(:change).
98
+ with(:dir, 'dir_path', recursive: true, silence: true, build: true)
99
+ record.build
100
+ end
101
+ end
102
+
103
+ describe '#still_building!' do
104
+ let(:directories) { ['dir_path'] }
92
105
 
106
+ let(:actor) do
107
+ instance_double(Listen::Change, change: nil, terminate: true)
108
+ end
109
+
110
+ before do
111
+ allow(listener).to receive(:directories) { directories }
112
+ allow(listener).to receive(:sync).with(:change_pool) { actor }
113
+ end
114
+
115
+ it 'keeps the build blocking longer' do
116
+ record # To avoid initializing record in thread
117
+
118
+ th = Thread.new do
119
+ 10.times do
120
+ sleep 0.05
121
+ record.still_building!
122
+ end
123
+ end
124
+
125
+ started = Time.now
93
126
  record.build
127
+ ended = Time.now
128
+
129
+ th.join
130
+
131
+ expect(ended - started).to be > 0.5
94
132
  end
95
133
  end
96
134
  end
@@ -4,10 +4,11 @@ describe Listen::Silencer do
4
4
  let(:options) { {} }
5
5
 
6
6
  let(:listener) do
7
- double(Listen::Listener,
8
- directories: [Pathname.pwd, Pathname.new('/Users/Shared/')],
9
- options: options
10
- )
7
+ instance_double(
8
+ Listen::Listener,
9
+ directories: [Pathname.pwd, Pathname.new('/Users/Shared/')],
10
+ options: options
11
+ )
11
12
  end
12
13
 
13
14
  let(:silencer) { Listen::Silencer.new(listener) }
@@ -19,46 +20,42 @@ describe Listen::Silencer do
19
20
  hidden_dirs = %w(.git .svn .hg .rbx .bundle)
20
21
  other_dirs = %w(bundle vendor/bundle log tmp vendor/ruby)
21
22
  (hidden_dirs + other_dirs).each do |dir|
22
- describe do
23
- let(:path) { pwd.join(dir) }
23
+ it "silences #{dir}" do
24
+ expect(silencer.silenced?(pwd + dir, :dir)).to be_truthy
25
+ end
24
26
 
25
- it "silences default ignored directory: #{dir}" do
26
- expect(silencer.silenced?(path)).to be_true
27
- end
27
+ it "doesn't silence #{dir}foo" do
28
+ expect(silencer.silenced?(pwd + "#{dir}foo", :dir)).to be_falsey
29
+ end
28
30
 
29
- context 'with a directory beginning with the same name' do
30
- let(:path) { pwd.join("#{dir}foo") }
31
+ it "doesn't silence foo#{dir}" do
32
+ expect(silencer.silenced?(pwd + "foo#{dir}", :dir)).to be_falsey
33
+ end
34
+ end
31
35
 
32
- it "doesn't silences default ignored directory: #{dir}foo" do
33
- expect(silencer.silenced?(path)).to be_false
34
- end
35
- end
36
+ all_files = %w(.DS_Store foo.tmp foo~)
36
37
 
37
- context 'with a directory ending with the same name' do
38
- let(:path) { pwd.join("foo#{dir}") }
38
+ # Gedit swap files
39
+ all_files += %w(.goutputstream-S3FBGX)
39
40
 
40
- it "doesn't silences default ignored directory: foo#{dir}" do
41
- expect(silencer.silenced?(path)).to be_false
42
- end
43
- end
44
- end
45
- end
41
+ # Kate editor swap files
42
+ all_files += %w(foo.rbo54321.new foo.rbB22583.new foo.rb.kate-swp)
46
43
 
47
- gedit_files = %w(.goutputstream-S3FBGX)
48
- kate_files = %w(foo.rbo54321.new foo.rbB22583.new foo.rb.kate-swp)
49
- (%w(.DS_Store foo.tmp foo~) + kate_files + gedit_files).each do |path|
50
- describe do
51
- it "by default silences files like: #{path}" do
52
- expect(silencer.silenced?(pwd + path)).to be_true
53
- end
44
+ # Intellij swap files
45
+ all_files += %w(foo.rb___jb_bak___ foo.rb___jb_old___)
46
+
47
+ # Vim swap files
48
+ all_files += %w(foo.swp foo.swx foo.swpx 4913)
49
+
50
+ all_files.each do |path|
51
+ it "silences #{path}" do
52
+ expect(silencer.silenced?(pwd + path, :file)).to be_truthy
54
53
  end
55
54
  end
56
55
 
57
- %w(foo.tmpl file.new file54321.new).each do |path|
58
- describe do
59
- it "by default does not silence files like: #{path}" do
60
- expect(silencer.silenced?(pwd + path)).to be_false
61
- end
56
+ %w(foo.tmpl file.new file54321.new a.swf 14913 49131).each do |path|
57
+ it "does not silence #{path}" do
58
+ expect(silencer.silenced?(pwd + path, :file)).to be_falsey
62
59
  end
63
60
  end
64
61
  end
@@ -67,7 +64,7 @@ describe Listen::Silencer do
67
64
  let(:options) { { ignore: /\.pid$/ } }
68
65
 
69
66
  it 'silences path matching custom ignore regex' do
70
- expect(silencer.silenced?(pwd + 'foo.pid')).to be_true
67
+ expect(silencer.silenced?(pwd + 'foo.pid', :file)).to be_truthy
71
68
  end
72
69
  end
73
70
 
@@ -75,8 +72,8 @@ describe Listen::Silencer do
75
72
  let(:options) { { ignore: [%r{^foo/bar}, /\.pid$/] } }
76
73
 
77
74
  it 'silences paths matching custom ignore regexes' do
78
- expect(silencer.silenced?(pwd + 'foo/bar/baz')).to be_true
79
- expect(silencer.silenced?(pwd + 'foo.pid')).to be_true
75
+ expect(silencer.silenced?(pwd + 'foo/bar/baz', :file)).to be_truthy
76
+ expect(silencer.silenced?(pwd + 'foo.pid', :file)).to be_truthy
80
77
  end
81
78
  end
82
79
 
@@ -84,90 +81,66 @@ describe Listen::Silencer do
84
81
  let(:options) { { ignore!: /\.pid$/ } }
85
82
 
86
83
  it 'silences custom ignored directory' do
87
- expect(silencer.silenced?(pwd + 'foo.pid')).to be_true
84
+ expect(silencer.silenced?(pwd + 'foo.pid', :file)).to be_truthy
88
85
  end
89
86
 
90
87
  it "doesn't silence default ignored directory" do
91
- expect(silencer.silenced?(pwd + '.git')).to be_false
88
+ expect(silencer.silenced?(pwd + '.git', :file)).to be_falsey
92
89
  end
93
90
  end
94
91
 
95
92
  context 'with only options (regexp)' do
96
93
  let(:options) { { only: %r{foo} } }
97
94
 
98
- it 'do not take only regex in account if type is Unknown' do
99
- expect(silencer.silenced?(pwd + 'baz')).to be_false
100
- end
101
-
102
95
  it 'do not silence path matches only regex if type is File' do
103
- expect(silencer.silenced?(pwd + 'foo', 'File')).to be_false
96
+ expect(silencer.silenced?(pwd + 'foo', :file)).to be_falsey
104
97
  end
105
98
 
106
99
  it 'silences other directory' do
107
- expect(silencer.silenced?(pwd + 'bar', 'File')).to be_true
100
+ expect(silencer.silenced?(pwd + 'bar', :file)).to be_truthy
108
101
  end
109
102
  end
110
103
 
111
104
  context 'with only options (array)' do
112
105
  let(:options) { { only: [%r{^foo/}, %r{\.txt$}] } }
113
106
 
114
- it 'do not take only regex in account if type is Unknown' do
115
- expect(silencer.silenced?(pwd + 'baz')).to be_false
116
- end
117
-
118
107
  it "doesn't silence good directory" do
119
- expect(silencer.silenced?(pwd + 'foo/bar.rb', 'File')).to be_false
108
+ expect(silencer.silenced?(pwd + 'foo/bar.rb', :file)).to be_falsey
120
109
  end
121
110
 
122
111
  it "doesn't silence good file" do
123
- expect(silencer.silenced?(pwd + 'bar.txt', 'File')).to be_false
112
+ expect(silencer.silenced?(pwd + 'bar.txt', :file)).to be_falsey
124
113
  end
125
114
 
126
115
  it 'silences other directory' do
127
- expect(silencer.silenced?(pwd + 'bar/baz.rb', 'File')).to be_true
116
+ expect(silencer.silenced?(pwd + 'bar/baz.rb', :file)).to be_truthy
128
117
  end
129
118
 
130
119
  it 'silences other file' do
131
- expect(silencer.silenced?(pwd + 'bar.rb', 'File')).to be_true
120
+ expect(silencer.silenced?(pwd + 'bar.rb', :file)).to be_truthy
132
121
  end
133
122
  end
134
123
 
135
124
  context 'with ignore and only options' do
136
125
  let(:options) { { only: /\.pid$/, ignore: %r{^bar} } }
137
126
 
138
- context 'with Unknown type' do
139
- context 'when not matching :only' do
140
- context 'when not matching :ignore' do
141
- it 'does not silence' do
142
- expect(silencer.silenced?(pwd + 'baz')).to be_false
143
- end
144
- end
145
-
146
- context 'when matching :ignore' do
147
- it 'silences' do
148
- expect(silencer.silenced?(pwd + 'bar')).to be_true
149
- end
150
- end
151
- end
152
- end
153
-
154
127
  context 'with File type' do
155
128
  context 'when not matching :only' do
156
129
  it 'silences' do
157
- expect(silencer.silenced?(pwd + 'foo.rb', 'File')).to be_true
130
+ expect(silencer.silenced?(pwd + 'foo.rb', :file)).to be_truthy
158
131
  end
159
132
  end
160
133
 
161
134
  context 'when matching :only' do
162
135
  context 'when matching :ignore' do
163
136
  it 'silences' do
164
- expect(silencer.silenced?(pwd + 'bar.pid', 'File')).to be_true
137
+ expect(silencer.silenced?(pwd + 'bar.pid', :file)).to be_truthy
165
138
  end
166
139
  end
167
140
 
168
141
  context 'when not matching :ignore' do
169
142
  it 'does not silence' do
170
- expect(silencer.silenced?(pwd + 'foo.pid', 'File')).to be_false
143
+ expect(silencer.silenced?(pwd + 'foo.pid', :file)).to be_falsey
171
144
  end
172
145
  end
173
146
  end
@@ -175,8 +148,8 @@ describe Listen::Silencer do
175
148
  end
176
149
 
177
150
  it "doesn't silence normal path" do
178
- path = pwd + 'some_dir' + 'some_file.rb'
179
- expect(silencer.silenced?(path)).to be_false
151
+ path = (pwd + 'some_dir') + 'some_file.rb'
152
+ expect(silencer.silenced?(path, :file)).to be_falsey
180
153
  end
181
154
  end
182
155
 
@@ -1,115 +1,124 @@
1
1
  require 'spec_helper'
2
2
 
3
+ require 'listen/tcp/broadcaster'
4
+
3
5
  describe Listen::TCP::Broadcaster do
4
6
 
5
7
  let(:host) { '10.0.0.2' }
6
8
  let(:port) { 4000 }
7
9
 
8
10
  subject { described_class.new(host, port) }
9
- let(:server) { double(described_class::TCPServer, close: true, accept: nil) }
10
- let(:socket) { double(described_class::TCPSocket, write: true) }
11
+
12
+ let(:server) do
13
+ instance_double(described_class::TCPServer, close: true, accept: nil)
14
+ end
15
+
16
+ let(:socket) do
17
+ instance_double(described_class::TCPSocket, close: true, write: true)
18
+ end
19
+
20
+ let(:socket2) do
21
+ instance_double(described_class::TCPSocket, close: true, write: true)
22
+ end
23
+
11
24
  let(:payload) { Listen::TCP::Message.new.payload }
12
25
 
13
26
  before do
14
27
  expect(described_class::TCPServer).to receive(:new).
15
28
  with(host, port).and_return server
29
+ allow(server).to receive(:accept).and_raise('stub called')
16
30
  end
17
31
 
18
32
  after do
19
33
  subject.terminate
20
34
  end
21
35
 
22
- describe '#initialize' do
23
- it 'initializes and exposes a server' do
24
- expect(subject.server).to be server
25
- end
26
-
27
- it 'initializes and exposes a list of sockets' do
28
- expect(subject.sockets).to eq []
29
- end
30
- end
31
-
32
36
  describe '#start' do
33
- let(:async) { double('TCP-listener async') }
34
-
35
37
  it 'invokes run loop asynchronously' do
36
- subject.stub(:async).and_return async
37
- expect(async).to receive(:run)
38
+ expect_any_instance_of(described_class).to receive(:run)
38
39
  subject.start
39
40
  end
40
41
  end
41
42
 
42
43
  describe '#finalize' do
43
- it 'clears sockets' do
44
- expect(subject.sockets).to receive(:clear)
45
- subject.finalize
46
- end
44
+ before { allow(server).to receive(:accept).and_return nil }
47
45
 
48
46
  it 'closes server' do
49
- expect(subject.server).to receive(:close)
47
+ expect(server).to receive(:close)
50
48
  subject.finalize
51
- expect(subject.server).to be_nil
52
49
  end
53
50
  end
54
51
 
55
52
  describe '#broadcast' do
56
- it 'unicasts to connected sockets' do
57
- subject.handle_connection socket
58
- expect(subject.wrapped_object).to receive(:unicast).with socket, payload
59
- subject.broadcast payload
60
- end
61
- end
62
-
63
- describe '#unicast' do
64
- before do
65
- subject.handle_connection socket
66
- end
53
+ context 'with active socket' do
54
+ before { allow(server).to receive(:accept).and_return socket, nil }
67
55
 
68
- context 'when succesful' do
69
- it 'returns true and leaves socket untouched' do
70
- expect(subject.unicast(socket, payload)).to be_true
71
- expect(subject.sockets).to include socket
56
+ it 'should broadcast payload' do
57
+ expect(socket).to receive(:write).with(payload)
58
+ subject.run
59
+ subject.broadcast payload
72
60
  end
73
- end
74
61
 
75
- context 'on IO errors' do
76
- it 'returns false and removes socket from list' do
77
- socket.stub(:write).and_raise IOError
78
- expect(subject.unicast(socket, payload)).to be_false
79
- expect(subject.sockets).not_to include socket
62
+ it 'should keep socket' do
63
+ expect(socket).to receive(:write).twice.with(payload)
64
+ subject.run
65
+ 2.times { subject.broadcast payload }
80
66
  end
81
- end
82
67
 
83
- context 'on connection reset by peer' do
84
- it 'returns false and removes socket from list' do
85
- socket.stub(:write).and_raise Errno::ECONNRESET
86
- expect(subject.unicast(socket, payload)).to be_false
87
- expect(subject.sockets).not_to include socket
68
+ context 'with IOError' do
69
+ it 'should remove socket from list' do
70
+ allow(socket).to receive(:write).once.and_raise IOError
71
+ subject.run
72
+ 2.times { subject.broadcast payload }
73
+ end
88
74
  end
89
- end
90
75
 
91
- context 'on broken pipe' do
92
- it 'returns false and removes socket from list' do
93
- socket.stub(:write).and_raise Errno::EPIPE
94
- expect(subject.unicast(socket, payload)).to be_false
95
- expect(subject.sockets).not_to include socket
76
+ context 'when reset by peer' do
77
+ it 'should remove socket from list' do
78
+ allow(socket).to receive(:write).once.and_raise Errno::ECONNRESET
79
+ subject.run
80
+ 2.times { subject.broadcast payload }
81
+ end
96
82
  end
97
- end
98
- end
99
83
 
100
- describe '#run' do
101
- it 'handles incoming connections' do
102
- server.stub(:accept).and_return socket, nil
103
- expect(subject.wrapped_object).to receive(:handle_connection).with socket
104
- subject.run
105
- end
106
- end
84
+ context 'when broken pipe' do
85
+ it 'should remove socket from list' do
86
+ allow(socket).to receive(:write).once.and_raise Errno::EPIPE
87
+ subject.run
88
+ 2.times { subject.broadcast payload }
89
+ end
90
+ end
107
91
 
108
- describe '#handle_connection' do
109
- it 'adds socket to list' do
110
- subject.handle_connection socket
111
- expect(subject.sockets).to include socket
92
+ context 'with another active socket' do
93
+ before do
94
+ allow(server).to receive(:accept).and_return socket, socket2, nil
95
+ end
96
+
97
+ it 'should broadcast payload to both' do
98
+ expect(socket).to receive(:write).with(payload)
99
+ expect(socket2).to receive(:write).with(payload)
100
+ subject.run
101
+ subject.broadcast payload
102
+ end
103
+
104
+ context 'with a failure in first socket' do
105
+ before do
106
+ allow(socket).to receive(:write).once.and_raise Errno::EPIPE
107
+ end
108
+
109
+ it 'should still broadcast to remaining socket' do
110
+ expect(socket2).to receive(:write).with(payload)
111
+ subject.run
112
+ subject.broadcast payload
113
+ end
114
+
115
+ it 'should broadcast to only remaining socket' do
116
+ expect(socket2).to receive(:write).twice.with(payload)
117
+ subject.run
118
+ 2.times { subject.broadcast payload }
119
+ end
120
+ end
121
+ end
112
122
  end
113
123
  end
114
-
115
124
  end