mieps_http-2 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,581 @@
1
+ require 'helper'
2
+
3
+ RSpec.describe HTTP2::Connection do
4
+ before(:each) do
5
+ @conn = Client.new
6
+ end
7
+
8
+ let(:f) { Framer.new }
9
+
10
+ context 'initialization and settings' do
11
+ it 'should raise error if first frame is not SETTINGS' do
12
+ (FRAME_TYPES - [SETTINGS]).each do |frame|
13
+ frame = set_stream_id(f.generate(frame.deep_dup), 0x0)
14
+ expect { @conn.dup << frame }.to raise_error(ProtocolError)
15
+ end
16
+
17
+ expect { @conn << f.generate(SETTINGS.dup) }.to_not raise_error
18
+ expect(@conn.state).to eq :connected
19
+ end
20
+
21
+ it 'should raise error if SETTINGS stream != 0' do
22
+ frame = set_stream_id(f.generate(SETTINGS.dup), 0x1)
23
+ expect { @conn << frame }.to raise_error(ProtocolError)
24
+ end
25
+ end
26
+
27
+ context 'settings synchronization' do
28
+ it 'should reflect outgoing settings when ack is received' do
29
+ expect(@conn.local_settings[:settings_header_table_size]).to eq 4096
30
+ @conn.settings(settings_header_table_size: 256)
31
+ expect(@conn.local_settings[:settings_header_table_size]).to eq 4096
32
+
33
+ ack = { type: :settings, stream: 0, payload: [], flags: [:ack] }
34
+ @conn << f.generate(ack)
35
+
36
+ expect(@conn.local_settings[:settings_header_table_size]).to eq 256
37
+ end
38
+
39
+ it 'should reflect incoming settings when SETTINGS is received' do
40
+ expect(@conn.remote_settings[:settings_header_table_size]).to eq 4096
41
+ settings = SETTINGS.dup
42
+ settings[:payload] = [[:settings_header_table_size, 256]]
43
+
44
+ @conn << f.generate(settings)
45
+
46
+ expect(@conn.remote_settings[:settings_header_table_size]).to eq 256
47
+ end
48
+
49
+ it 'should send SETTINGS ACK when SETTINGS is received' do
50
+ settings = SETTINGS.dup
51
+ settings[:payload] = [[:settings_header_table_size, 256]]
52
+
53
+ expect(@conn).to receive(:send) do |frame|
54
+ expect(frame[:type]).to eq :settings
55
+ expect(frame[:flags]).to eq [:ack]
56
+ expect(frame[:payload]).to eq []
57
+ end
58
+
59
+ @conn << f.generate(settings)
60
+ end
61
+ end
62
+
63
+ context 'stream management' do
64
+ it 'should initialize to default stream limit (100)' do
65
+ expect(@conn.local_settings[:settings_max_concurrent_streams]).to eq 100
66
+ end
67
+
68
+ it 'should change stream limit to received SETTINGS value' do
69
+ @conn << f.generate(SETTINGS.dup)
70
+ expect(@conn.remote_settings[:settings_max_concurrent_streams]).to eq 10
71
+ end
72
+
73
+ it 'should count open streams against stream limit' do
74
+ s = @conn.new_stream
75
+ expect(@conn.active_stream_count).to eq 0
76
+ s.receive HEADERS
77
+ expect(@conn.active_stream_count).to eq 1
78
+ end
79
+
80
+ it 'should not count reserved streams against stream limit' do
81
+ s1 = @conn.new_stream
82
+ s1.receive PUSH_PROMISE
83
+ expect(@conn.active_stream_count).to eq 0
84
+
85
+ s2 = @conn.new_stream
86
+ s2.send PUSH_PROMISE.deep_dup
87
+ expect(@conn.active_stream_count).to eq 0
88
+
89
+ # transition to half closed
90
+ s1.receive HEADERS
91
+ s2.send HEADERS.deep_dup
92
+ expect(@conn.active_stream_count).to eq 2
93
+
94
+ # transition to closed
95
+ s1.receive DATA
96
+ s2.send DATA.dup
97
+ expect(@conn.active_stream_count).to eq 0
98
+ end
99
+
100
+ it 'should not exceed stream limit set by peer' do
101
+ @conn << f.generate(SETTINGS.dup)
102
+
103
+ expect do
104
+ 10.times do
105
+ s = @conn.new_stream
106
+ s.send HEADERS.deep_dup
107
+ end
108
+ end.to_not raise_error
109
+
110
+ expect { @conn.new_stream }.to raise_error(StreamLimitExceeded)
111
+ end
112
+
113
+ it 'should initialize stream with HEADERS priority value' do
114
+ @conn << f.generate(SETTINGS.dup)
115
+
116
+ stream, headers = nil, HEADERS.dup
117
+ headers[:weight] = 20
118
+ headers[:stream_dependency] = 0
119
+ headers[:exclusive] = false
120
+
121
+ @conn.on(:stream) { |s| stream = s }
122
+ @conn << f.generate(headers)
123
+
124
+ expect(stream.weight).to eq 20
125
+ end
126
+
127
+ it 'should initialize idle stream on PRIORITY frame' do
128
+ @conn << f.generate(SETTINGS.dup)
129
+
130
+ stream = nil
131
+ @conn.on(:stream) { |s| stream = s }
132
+ @conn << f.generate(PRIORITY.dup)
133
+
134
+ expect(stream.state).to eq :idle
135
+ end
136
+ end
137
+
138
+ context 'Headers pre/post processing' do
139
+ it 'should not concatenate multiple occurences of a header field with the same name' do
140
+ input = [
141
+ ['Content-Type', 'text/html'],
142
+ ['Cache-Control', 'max-age=60, private'],
143
+ ['Cache-Control', 'must-revalidate'],
144
+ ]
145
+ expected = [
146
+ ['content-type', 'text/html'],
147
+ ['cache-control', 'max-age=60, private'],
148
+ ['cache-control', 'must-revalidate'],
149
+ ]
150
+ headers = []
151
+ @conn.on(:frame) do |bytes|
152
+ # bytes[3]: frame's type field
153
+ headers << f.parse(bytes) if [1, 5, 9].include?(bytes[3].ord)
154
+ end
155
+
156
+ stream = @conn.new_stream
157
+ stream.headers(input)
158
+
159
+ expect(headers.size).to eq 1
160
+ emitted = Decompressor.new.decode(headers.first[:payload])
161
+ expect(emitted).to match_array(expected)
162
+ end
163
+
164
+ it 'should not split zero-concatenated header field values' do
165
+ input = [
166
+ ['cache-control', "max-age=60, private\0must-revalidate"],
167
+ ['content-type', 'text/html'],
168
+ ['cookie', "a=b\0c=d; e=f"],
169
+ ]
170
+ expected = [
171
+ ['cache-control', "max-age=60, private\0must-revalidate"],
172
+ ['content-type', 'text/html'],
173
+ ['cookie', "a=b\0c=d; e=f"],
174
+ ]
175
+
176
+ result = nil
177
+ @conn.on(:stream) do |stream|
178
+ stream.on(:headers) { |h| result = h }
179
+ end
180
+
181
+ srv = Server.new
182
+ srv.on(:frame) { |bytes| @conn << bytes }
183
+ stream = srv.new_stream
184
+ stream.headers(input)
185
+
186
+ expect(result).to eq expected
187
+ end
188
+ end
189
+
190
+ context 'flow control' do
191
+ it 'should initialize to default flow window' do
192
+ expect(@conn.remote_window).to eq DEFAULT_FLOW_WINDOW
193
+ end
194
+
195
+ it 'should update connection and stream windows on SETTINGS' do
196
+ settings, data = SETTINGS.dup, DATA.dup
197
+ settings[:payload] = [[:settings_initial_window_size, 1024]]
198
+ data[:payload] = 'x' * 2048
199
+
200
+ stream = @conn.new_stream
201
+
202
+ stream.send HEADERS.deep_dup
203
+ stream.send data
204
+ expect(stream.remote_window).to eq(DEFAULT_FLOW_WINDOW - 2048)
205
+ expect(@conn.remote_window).to eq(DEFAULT_FLOW_WINDOW - 2048)
206
+
207
+ @conn << f.generate(settings)
208
+ expect(@conn.remote_window).to eq(-1024)
209
+ expect(stream.remote_window).to eq(-1024)
210
+ end
211
+
212
+ it 'should initialize streams with window specified by peer' do
213
+ settings = SETTINGS.dup
214
+ settings[:payload] = [[:settings_initial_window_size, 1024]]
215
+
216
+ @conn << f.generate(settings)
217
+ expect(@conn.new_stream.remote_window).to eq 1024
218
+ end
219
+
220
+ it 'should observe connection flow control' do
221
+ settings, data = SETTINGS.dup, DATA.dup
222
+ settings[:payload] = [[:settings_initial_window_size, 1000]]
223
+
224
+ @conn << f.generate(settings)
225
+ s1 = @conn.new_stream
226
+ s2 = @conn.new_stream
227
+
228
+ s1.send HEADERS.deep_dup
229
+ s1.send data.merge(payload: 'x' * 900)
230
+ expect(@conn.remote_window).to eq 100
231
+
232
+ s2.send HEADERS.deep_dup
233
+ s2.send data.merge(payload: 'x' * 200)
234
+ expect(@conn.remote_window).to eq 0
235
+ expect(@conn.buffered_amount).to eq 100
236
+
237
+ @conn << f.generate(WINDOW_UPDATE.merge(stream: 0, increment: 1000))
238
+ expect(@conn.buffered_amount).to eq 0
239
+ expect(@conn.remote_window).to eq 900
240
+ end
241
+ end
242
+
243
+ context 'framing' do
244
+ it 'should buffer incomplete frames' do
245
+ settings = SETTINGS.dup
246
+ settings[:payload] = [[:settings_initial_window_size, 1000]]
247
+ @conn << f.generate(settings)
248
+
249
+ frame = f.generate(WINDOW_UPDATE.merge(stream: 0, increment: 1000))
250
+ @conn << frame
251
+ expect(@conn.remote_window).to eq 2000
252
+
253
+ @conn << frame.slice!(0, 1)
254
+ @conn << frame
255
+ expect(@conn.remote_window).to eq 3000
256
+ end
257
+
258
+ it 'should decompress header blocks regardless of stream state' do
259
+ req_headers = [
260
+ ['content-length', '20'],
261
+ ['x-my-header', 'first'],
262
+ ]
263
+
264
+ cc = Compressor.new
265
+ headers = HEADERS.dup
266
+ headers[:payload] = cc.encode(req_headers)
267
+
268
+ @conn << f.generate(SETTINGS.dup)
269
+ @conn.on(:stream) do |stream|
270
+ expect(stream).to receive(:<<) do |frame|
271
+ expect(frame[:payload]).to eq req_headers
272
+ end
273
+ end
274
+
275
+ @conn << f.generate(headers)
276
+ end
277
+
278
+ it 'should decode non-contiguous header blocks' do
279
+ req_headers = [
280
+ ['content-length', '15'],
281
+ ['x-my-header', 'first'],
282
+ ]
283
+
284
+ cc = Compressor.new
285
+ h1, h2 = HEADERS.dup, CONTINUATION.dup
286
+
287
+ # Header block fragment might not complete for decompression
288
+ payload = cc.encode(req_headers)
289
+ h1[:payload] = payload.slice!(0, payload.size / 2) # first half
290
+ h1[:stream] = 5
291
+ h1[:flags] = []
292
+
293
+ h2[:payload] = payload # the remaining
294
+ h2[:stream] = 5
295
+
296
+ @conn << f.generate(SETTINGS.dup)
297
+ @conn.on(:stream) do |stream|
298
+ expect(stream).to receive(:<<) do |frame|
299
+ expect(frame[:payload]).to eq req_headers
300
+ end
301
+ end
302
+
303
+ @conn << f.generate(h1)
304
+ @conn << f.generate(h2)
305
+ end
306
+
307
+ it 'should require that split header blocks are a contiguous sequence' do
308
+ headers = HEADERS.dup
309
+ headers[:flags] = []
310
+
311
+ @conn << f.generate(SETTINGS.dup)
312
+ @conn << f.generate(headers)
313
+ (FRAME_TYPES - [CONTINUATION]).each do |frame|
314
+ expect { @conn << f.generate(frame.deep_dup) }.to raise_error(ProtocolError)
315
+ end
316
+ end
317
+
318
+ it 'should raise compression error on encode of invalid frame' do
319
+ @conn << f.generate(SETTINGS.dup)
320
+ stream = @conn.new_stream
321
+
322
+ expect do
323
+ stream.headers('name' => Float::INFINITY)
324
+ end.to raise_error(CompressionError)
325
+ end
326
+
327
+ it 'should raise connection error on decode of invalid frame' do
328
+ @conn << f.generate(SETTINGS.dup)
329
+ frame = f.generate(DATA.dup) # Receiving DATA on unopened stream 1 is an error.
330
+ # Connection errors emit protocol error frames
331
+ expect { @conn << frame }.to raise_error(ProtocolError)
332
+ end
333
+
334
+ it 'should emit encoded frames via on(:frame)' do
335
+ bytes = nil
336
+ @conn.on(:frame) { |d| bytes = d }
337
+ @conn.settings(settings_max_concurrent_streams: 10,
338
+ settings_initial_window_size: 0x7fffffff)
339
+
340
+ expect(bytes).to eq f.generate(SETTINGS.dup)
341
+ end
342
+
343
+ it 'should compress stream headers' do
344
+ @conn.on(:frame) do |bytes|
345
+ expect(bytes).not_to include('get')
346
+ expect(bytes).not_to include('http')
347
+ expect(bytes).not_to include('www.example.org') # should be huffman encoded
348
+ end
349
+
350
+ stream = @conn.new_stream
351
+ stream.headers(':method' => 'get',
352
+ ':scheme' => 'http',
353
+ ':authority' => 'www.example.org',
354
+ ':path' => '/resource')
355
+ end
356
+
357
+ it 'should generate CONTINUATION if HEADERS is too long' do
358
+ headers = []
359
+ @conn.on(:frame) do |bytes|
360
+ # bytes[3]: frame's type field
361
+ headers << f.parse(bytes) if [1, 5, 9].include?(bytes[3].ord)
362
+ end
363
+
364
+ stream = @conn.new_stream
365
+ stream.headers({
366
+ ':method' => 'get',
367
+ ':scheme' => 'http',
368
+ ':authority' => 'www.example.org',
369
+ ':path' => '/resource',
370
+ 'custom' => 'q' * 44_000,
371
+ }, end_stream: true)
372
+ expect(headers.size).to eq 3
373
+ expect(headers[0][:type]).to eq :headers
374
+ expect(headers[1][:type]).to eq :continuation
375
+ expect(headers[2][:type]).to eq :continuation
376
+ expect(headers[0][:flags]).to eq [:end_stream]
377
+ expect(headers[1][:flags]).to eq []
378
+ expect(headers[2][:flags]).to eq [:end_headers]
379
+ end
380
+
381
+ it 'should not generate CONTINUATION if HEADERS fits exactly in a frame' do
382
+ headers = []
383
+ @conn.on(:frame) do |bytes|
384
+ # bytes[3]: frame's type field
385
+ headers << f.parse(bytes) if [1, 5, 9].include?(bytes[3].ord)
386
+ end
387
+
388
+ stream = @conn.new_stream
389
+ stream.headers({
390
+ ':method' => 'get',
391
+ ':scheme' => 'http',
392
+ ':authority' => 'www.example.org',
393
+ ':path' => '/resource',
394
+ 'custom' => 'q' * 18_682, # this number should be updated when Huffman table is changed
395
+ }, end_stream: true)
396
+ expect(headers[0][:length]).to eq @conn.remote_settings[:settings_max_frame_size]
397
+ expect(headers.size).to eq 1
398
+ expect(headers[0][:type]).to eq :headers
399
+ expect(headers[0][:flags]).to include(:end_headers)
400
+ expect(headers[0][:flags]).to include(:end_stream)
401
+ end
402
+
403
+ it 'should not generate CONTINUATION if HEADERS fits exactly in a frame' do
404
+ headers = []
405
+ @conn.on(:frame) do |bytes|
406
+ # bytes[3]: frame's type field
407
+ headers << f.parse(bytes) if [1, 5, 9].include?(bytes[3].ord)
408
+ end
409
+
410
+ stream = @conn.new_stream
411
+ stream.headers({
412
+ ':method' => 'get',
413
+ ':scheme' => 'http',
414
+ ':authority' => 'www.example.org',
415
+ ':path' => '/resource',
416
+ 'custom' => 'q' * 18_682, # this number should be updated when Huffman table is changed
417
+ }, end_stream: true)
418
+ expect(headers[0][:length]).to eq @conn.remote_settings[:settings_max_frame_size]
419
+ expect(headers.size).to eq 1
420
+ expect(headers[0][:type]).to eq :headers
421
+ expect(headers[0][:flags]).to include(:end_headers)
422
+ expect(headers[0][:flags]).to include(:end_stream)
423
+ end
424
+
425
+ it 'should generate CONTINUATION if HEADERS exceed the max payload by one byte' do
426
+ headers = []
427
+ @conn.on(:frame) do |bytes|
428
+ headers << f.parse(bytes) if [1, 5, 9].include?(bytes[3].ord)
429
+ end
430
+
431
+ stream = @conn.new_stream
432
+ stream.headers({
433
+ ':method' => 'get',
434
+ ':scheme' => 'http',
435
+ ':authority' => 'www.example.org',
436
+ ':path' => '/resource',
437
+ 'custom' => 'q' * 18_683, # this number should be updated when Huffman table is changed
438
+ }, end_stream: true)
439
+ expect(headers[0][:length]).to eq @conn.remote_settings[:settings_max_frame_size]
440
+ expect(headers[1][:length]).to eq 1
441
+ expect(headers.size).to eq 2
442
+ expect(headers[0][:type]).to eq :headers
443
+ expect(headers[1][:type]).to eq :continuation
444
+ expect(headers[0][:flags]).to eq [:end_stream]
445
+ expect(headers[1][:flags]).to eq [:end_headers]
446
+ end
447
+ end
448
+
449
+ context 'connection management' do
450
+ it 'should raise error on invalid connection header' do
451
+ srv = Server.new
452
+ expect { srv << f.generate(SETTINGS.dup) }.to raise_error(HandshakeError)
453
+
454
+ srv = Server.new
455
+ expect do
456
+ srv << CONNECTION_PREFACE_MAGIC
457
+ srv << f.generate(SETTINGS.dup)
458
+ end.to_not raise_error
459
+ end
460
+
461
+ it 'should respond to PING frames' do
462
+ @conn << f.generate(SETTINGS.dup)
463
+ expect(@conn).to receive(:send) do |frame|
464
+ expect(frame[:type]).to eq :ping
465
+ expect(frame[:flags]).to eq [:ack]
466
+ expect(frame[:payload]).to eq '12345678'
467
+ end
468
+
469
+ @conn << f.generate(PING.dup)
470
+ end
471
+
472
+ it 'should fire callback on PONG' do
473
+ @conn << f.generate(SETTINGS.dup)
474
+
475
+ pong = nil
476
+ @conn.ping('12345678') { |d| pong = d }
477
+ @conn << f.generate(PONG.dup)
478
+ expect(pong).to eq '12345678'
479
+ end
480
+
481
+ it 'should fire callback on receipt of GOAWAY' do
482
+ last_stream, payload, error = nil
483
+ @conn << f.generate(SETTINGS.dup)
484
+ @conn.on(:goaway) do |s, e, p|
485
+ last_stream = s
486
+ error = e
487
+ payload = p
488
+ end
489
+ @conn << f.generate(GOAWAY.merge(last_stream: 17, payload: 'test'))
490
+
491
+ expect(last_stream).to eq 17
492
+ expect(error).to eq :no_error
493
+ expect(payload).to eq 'test'
494
+ end
495
+
496
+ it 'should raise error when opening new stream after sending GOAWAY' do
497
+ @conn.goaway
498
+ expect { @conn.new_stream }.to raise_error(ConnectionClosed)
499
+ end
500
+
501
+ it 'should raise error when opening new stream after receiving GOAWAY' do
502
+ @conn << f.generate(SETTINGS.dup)
503
+ @conn << f.generate(GOAWAY.dup)
504
+ expect { @conn.new_stream }.to raise_error(ConnectionClosed)
505
+ end
506
+
507
+ it 'should process connection management frames after GOAWAY' do
508
+ @conn << f.generate(SETTINGS.dup)
509
+ @conn << f.generate(HEADERS.dup)
510
+ @conn << f.generate(GOAWAY.dup)
511
+ @conn << f.generate(HEADERS.merge(stream: 7))
512
+ @conn << f.generate(PUSH_PROMISE.dup)
513
+
514
+ expect(@conn.active_stream_count).to eq 1
515
+ end
516
+
517
+ it 'should raise error on frame for invalid stream ID' do
518
+ @conn << f.generate(SETTINGS.dup)
519
+
520
+ expect do
521
+ @conn << f.generate(DATA.dup.merge(stream: 31))
522
+ end.to raise_error(ProtocolError)
523
+ end
524
+
525
+ it 'should send GOAWAY frame on connection error' do
526
+ stream = @conn.new_stream
527
+
528
+ expect(@conn).to receive(:encode) do |frame|
529
+ expect(frame[:type]).to eq :settings
530
+ [frame]
531
+ end
532
+ expect(@conn).to receive(:encode) do |frame|
533
+ expect(frame[:type]).to eq :goaway
534
+ expect(frame[:last_stream]).to eq stream.id
535
+ expect(frame[:error]).to eq :protocol_error
536
+ [frame]
537
+ end
538
+
539
+ expect { @conn << f.generate(DATA.dup) }.to raise_error(ProtocolError)
540
+ end
541
+ end
542
+
543
+ context 'API' do
544
+ it '.settings should emit SETTINGS frames' do
545
+ expect(@conn).to receive(:send) do |frame|
546
+ expect(frame[:type]).to eq :settings
547
+ expect(frame[:payload]).to eq([
548
+ [:settings_max_concurrent_streams, 10],
549
+ [:settings_initial_window_size, 0x7fffffff],
550
+ ])
551
+ expect(frame[:stream]).to eq 0
552
+ end
553
+
554
+ @conn.settings(settings_max_concurrent_streams: 10,
555
+ settings_initial_window_size: 0x7fffffff)
556
+ end
557
+
558
+ it '.ping should generate PING frames' do
559
+ expect(@conn).to receive(:send) do |frame|
560
+ expect(frame[:type]).to eq :ping
561
+ expect(frame[:payload]).to eq 'somedata'
562
+ end
563
+
564
+ @conn.ping('somedata')
565
+ end
566
+
567
+ it '.goaway should generate GOAWAY frame with last processed stream ID' do
568
+ @conn << f.generate(SETTINGS.dup)
569
+ @conn << f.generate(HEADERS.merge(stream: 17))
570
+
571
+ expect(@conn).to receive(:send) do |frame|
572
+ expect(frame[:type]).to eq :goaway
573
+ expect(frame[:last_stream]).to eq 17
574
+ expect(frame[:error]).to eq :internal_error
575
+ expect(frame[:payload]).to eq 'payload'
576
+ end
577
+
578
+ @conn.goaway(:internal_error, 'payload')
579
+ end
580
+ end
581
+ end
@@ -0,0 +1,54 @@
1
+ require 'helper'
2
+
3
+ RSpec.describe HTTP2::Emitter do
4
+ class Worker
5
+ include Emitter
6
+ end
7
+
8
+ before(:each) do
9
+ @w = Worker.new
10
+ @cnt = 0
11
+ end
12
+
13
+ it 'should raise error on missing callback' do
14
+ expect { @w.on(:a) {} }.to_not raise_error
15
+ expect { @w.on(:a) }.to raise_error
16
+ end
17
+
18
+ it 'should allow multiple callbacks on single event' do
19
+ @w.on(:a) { @cnt += 1 }
20
+ @w.on(:a) { @cnt += 1 }
21
+ @w.emit(:a)
22
+
23
+ expect(@cnt).to eq 2
24
+ end
25
+
26
+ it 'should execute callback with optional args' do
27
+ args = nil
28
+ @w.on(:a) { |a| args = a }
29
+ @w.emit(:a, 123)
30
+
31
+ expect(args).to eq 123
32
+ end
33
+
34
+ it 'should pass emitted callbacks to listeners' do
35
+ @w.on(:a) { |&block| block.call }
36
+ @w.once(:a) { |&block| block.call }
37
+ @w.emit(:a) { @cnt += 1 }
38
+
39
+ expect(@cnt).to eq 2
40
+ end
41
+
42
+ it 'should allow events with no callbacks' do
43
+ expect { @w.emit(:missing) }.to_not raise_error
44
+ end
45
+
46
+ it 'should execute callback exactly once' do
47
+ @w.on(:a) { @cnt += 1 }
48
+ @w.once(:a) { @cnt += 1 }
49
+ @w.emit(:a)
50
+ @w.emit(:a)
51
+
52
+ expect(@cnt).to eq 3
53
+ end
54
+ end