groem 0.0.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.
@@ -0,0 +1,140 @@
1
+ require 'eventmachine'
2
+ require 'uuidtools'
3
+
4
+ module Groem
5
+
6
+ class Notification < Struct.new(:environment,
7
+ :application_name,
8
+ :name,
9
+ :display_name,
10
+ :enabled,
11
+ :title,
12
+ :text,
13
+ :sticky,
14
+ :priority,
15
+ :coalescing_id,
16
+ :headers
17
+ )
18
+ include Groem::Marshal::Request
19
+
20
+ DEFAULT_ENV = {'protocol' => 'GNTP', 'version' => '1.0',
21
+ 'request_method' => 'NOTIFY', 'encryption_id' => 'NONE'
22
+ }
23
+
24
+ def initialize(name, *args)
25
+ opts = args.last.is_a?(Hash) ? args.pop : {}
26
+ title = args.shift
27
+ self.environment, self.headers, @callback = {}, {}, {}
28
+ self.environment = DEFAULT_ENV.merge(opts.delete(:environment) || {})
29
+ self.name = name.to_s if name
30
+ self.title = title.to_s if title
31
+ self.enabled = 'True'
32
+ self.headers = opts.delete(:headers) || {}
33
+ opts.each_pair do |opt, val|
34
+ if self.respond_to?(:"#{opt}=")
35
+ self.__send__ :"#{opt}=", val.to_s
36
+ else
37
+ header(opt, val.to_s)
38
+ end
39
+ end
40
+ reset!
41
+ end
42
+
43
+ def [](key)
44
+ to_request[key]
45
+ end
46
+
47
+ def reset!
48
+ @to_request = nil
49
+ self
50
+ end
51
+
52
+ def reset_callback!
53
+ @callback = {}
54
+ end
55
+
56
+ def dup
57
+ attrs = {}; self.each_pair {|k, v| attrs[k] = v.dup if v}
58
+ n = self.class.new(self.name, attrs)
59
+ n.callback(:context => callback_context,
60
+ :type => callback_type,
61
+ :target => callback_target) if @callback
62
+ n
63
+ end
64
+
65
+ def header key, value
66
+ reset!
67
+ self.headers[growlify_key(key)] = value
68
+ end
69
+
70
+ def icon(uri_or_file)
71
+ # TODO if not uri
72
+ reset!
73
+ header GNTP_NOTIFICATION_ICON_KEY, uri_or_file
74
+ end
75
+
76
+ # Note defaults name and type to notification name
77
+ def callback *args
78
+ opts = ((Hash === args.last) ? args.pop : {})
79
+ name = args.shift || opts[:context] || self.name
80
+ type = opts[:type] || self.name
81
+ target = opts[:target]
82
+ reset!
83
+ reset_callback!
84
+ @callback[GNTP_NOTIFICATION_CALLBACK_CONTEXT_KEY] = name if name
85
+ @callback[GNTP_NOTIFICATION_CALLBACK_CONTEXT_TYPE_KEY] = type if type
86
+ @callback[GNTP_NOTIFICATION_CALLBACK_TARGET_KEY] = target if target
87
+ @callback
88
+ end
89
+
90
+ def callback_context
91
+ @callback[GNTP_NOTIFICATION_CALLBACK_CONTEXT_KEY]
92
+ end
93
+
94
+ def callback_type
95
+ @callback[GNTP_NOTIFICATION_CALLBACK_CONTEXT_TYPE_KEY]
96
+ end
97
+ alias_method :callback_context_type, :callback_type
98
+
99
+ def callback_target
100
+ @callback[GNTP_NOTIFICATION_CALLBACK_TARGET_KEY]
101
+ end
102
+
103
+ def to_register
104
+ %w{display_name enabled}.inject({}) do |memo, attr|
105
+ if val = self.__send__(:"#{attr}")
106
+ memo["Notification-#{growlify_key(attr)}"] = val
107
+ end
108
+ memo
109
+ end.merge(self.headers)
110
+ end
111
+
112
+ def to_notify
113
+ %w{name title text sticky priority coalescing_id}.inject({}) do |memo, attr|
114
+ if val = self.__send__(:"#{attr}")
115
+ memo["Notification-#{growlify_key(attr)}"] = val
116
+ end
117
+ memo
118
+ end.merge({GNTP_APPLICATION_NAME_KEY => self.application_name}).
119
+ merge({GNTP_NOTIFICATION_ID_KEY => unique_id}).
120
+ merge(@callback).
121
+ merge(self.headers)
122
+ end
123
+
124
+ def to_request
125
+ @to_request ||= \
126
+ {'environment' => environment,
127
+ 'headers' => to_notify,
128
+ 'notifications' => {}
129
+ }
130
+ end
131
+
132
+ protected
133
+
134
+ def unique_id
135
+ UUIDTools::UUID.timestamp_create.to_s
136
+ end
137
+
138
+ end
139
+
140
+ end
@@ -0,0 +1,86 @@
1
+ require 'forwardable'
2
+
3
+ module Groem
4
+ class Response < Struct.new(:status,
5
+ :method,
6
+ :action,
7
+ :notification_id,
8
+ :context,
9
+ :context_type,
10
+ :callback_result,
11
+ :headers)
12
+ include Groem::Marshal::Response
13
+ extend Forwardable
14
+
15
+ def_delegator :@raw, :[]
16
+
17
+ def initialize(resp)
18
+ @raw = resp
19
+ self.status = resp[0]
20
+ self.headers = resp[1]
21
+ self.action = resp[1][GNTP_RESPONSE_ACTION_KEY]
22
+ self.notification_id = resp[1][GNTP_NOTIFICATION_ID_KEY]
23
+ self.context = resp[2][GNTP_NOTIFICATION_CALLBACK_CONTEXT_KEY]
24
+ self.context_type = resp[2][GNTP_NOTIFICATION_CALLBACK_CONTEXT_TYPE_KEY]
25
+ self.callback_result = resp[2][GNTP_NOTIFICATION_CALLBACK_RESULT_KEY]
26
+ self.method = if self.status.to_i == 0
27
+ if self.callback_result
28
+ GNTP_CALLBACK_RESPONSE
29
+ else
30
+ GNTP_OK_RESPONSE
31
+ end
32
+ else
33
+ GNTP_ERROR_RESPONSE
34
+ end
35
+ end
36
+
37
+ def callback_route
38
+ [self.callback_result, self.context, self.context_type]
39
+ end
40
+
41
+ def to_register? &blk
42
+ yield_and_return_if self.action == GNTP_REGISTER_METHOD, &blk
43
+ end
44
+
45
+ def to_notify? &blk
46
+ yield_and_return_if self.action == GNTP_NOTIFY_METHOD, &blk
47
+ end
48
+
49
+ def ok? &blk
50
+ yield_and_return_if self.method == GNTP_OK_RESPONSE, &blk
51
+ end
52
+
53
+ def callback? &blk
54
+ yield_and_return_if self.method == GNTP_CALLBACK_RESPONSE, &blk
55
+ end
56
+
57
+ def error?(code=nil, &blk)
58
+ yield_and_return_if (self.method == GNTP_ERROR_RESPONSE && \
59
+ code.nil? || self.status == code), &blk
60
+ end
61
+
62
+ def clicked? &blk
63
+ yield_and_return_if self.callback_result == GNTP_CLICK_CALLBACK_RESULT, &blk
64
+ end
65
+
66
+ def closed? &blk
67
+ yield_and_return_if self.callback_result == GNTP_CLOSE_CALLBACK_RESULT, &blk
68
+ end
69
+
70
+ def timedout? &blk
71
+ yield_and_return_if self.callback_result == GNTP_TIMEDOUT_CALLBACK_RESULT, &blk
72
+ end
73
+
74
+ alias_method :click?, :clicked?
75
+ alias_method :close?, :closed?
76
+ alias_method :timeout?, :timedout?
77
+
78
+ protected
79
+
80
+ def yield_and_return_if(cond)
81
+ yield if block_given? && cond
82
+ cond
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,37 @@
1
+
2
+ module Groem
3
+ class Route
4
+
5
+ class << self
6
+ def parse action, *path
7
+ [action] + Array.new(2).fill {|i| path[i] }
8
+ end
9
+
10
+ def matches? pattern, parts
11
+ parts = [parts] unless Array === parts
12
+ pattern.zip(parts).all? do |exp, act|
13
+ exp.nil? || exp == act
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ attr_reader :pattern
20
+
21
+ def initialize action, *path
22
+ path = path.flatten
23
+ @pattern = self.class.parse action, *path
24
+ end
25
+
26
+ def matches?(*args)
27
+ self.class.matches?(pattern, args.flatten)
28
+ end
29
+
30
+
31
+ # sort nil parts after named parts (unless named parts begin with ~)
32
+ def <=>(other)
33
+ pattern.map {|it| it || '~'} <=> other.pattern.map {|it| it || '~'}
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module Groem
2
+ VERSION = '0.0.4'
3
+ end
@@ -0,0 +1,73 @@
1
+ require File.join(File.dirname(__FILE__),'..','spec_helper')
2
+
3
+ describe 'Groem::App #notify with ad-hoc callback' do
4
+
5
+ before do
6
+ @p_svr = DummyServerHelper.fork_server(:callback => ['CLICK', 2])
7
+ Groem::Client.response_class = MarshalHelper.dummy_response_class
8
+ @subject = Groem::App.new('test', :port => DummyServerHelper::DEFAULT_PORT)
9
+ end
10
+
11
+ after do
12
+ DummyServerHelper.kill_server(@p_svr)
13
+ end
14
+
15
+ it 'should receive CLICK callback matching ad-hoc callback spec' do
16
+ ok_count = 0
17
+ cb_count = 0
18
+
19
+ app = @subject
20
+ app.register do
21
+ notification 'Foo' do |n|
22
+ n.display_name = 'Hoo'
23
+ n.callback 'First', :type => '1'
24
+ end
25
+ end
26
+
27
+ app.when_click :type => '2' do |resp|
28
+ cb_count += 1
29
+ end
30
+
31
+ app.when_click 'Second' do |resp|
32
+ cb_count += 1
33
+ end
34
+
35
+ app.when_click 'First' do |resp|
36
+ flunk 'Expected callback context \'Second\', got \'First\''
37
+ end
38
+
39
+ app.when_click :type => '1' do |resp|
40
+ flunk 'Expected callback context type \'2\', got \'1\''
41
+ end
42
+
43
+ app.notify('Foo', :callback => {:context => 'Second', :type => '2'})
44
+
45
+ cb_count.must_equal 2
46
+ end
47
+
48
+ it 'should trigger callback with ad-hoc state, but not change state of registered callback' do
49
+
50
+ app = @subject
51
+ app.register do
52
+ notification 'Foo' do |n|
53
+ n.display_name = 'Hoo'
54
+ n.callback 'First', :type => '1'
55
+ end
56
+ end
57
+
58
+ app.when_click do |resp|
59
+ puts resp[0..2].inspect
60
+ resp.context.must_equal 'Second'
61
+ resp.context_type.must_equal '2'
62
+ end
63
+
64
+ app.notify('Foo', :display_name => 'Who',
65
+ :callback => {:context => 'Second', :type => '2'})
66
+
67
+ app.notifications['Foo'].display_name.must_equal 'Hoo'
68
+ app.notifications['Foo'].callback_context.must_equal 'First'
69
+ app.notifications['Foo'].callback_type.must_equal '1'
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,390 @@
1
+ require File.join(File.dirname(__FILE__),'..','spec_helper')
2
+
3
+ describe 'Groem::App #notify without callbacks' do
4
+
5
+ describe 'with one notification' do
6
+
7
+ before do
8
+ @p_svr = DummyServerHelper.fork_server(:notify => '-OK')
9
+ Groem::Client.response_class = MarshalHelper.dummy_response_class
10
+ @subject = Groem::App.new('test', :port => DummyServerHelper::DEFAULT_PORT)
11
+ end
12
+
13
+ after do
14
+ DummyServerHelper.kill_server(@p_svr)
15
+ end
16
+
17
+ it 'should receive back one OK response from notify' do
18
+ count = 0
19
+
20
+ @subject.register do
21
+ notification 'hello' do end
22
+ end
23
+
24
+ @subject.notify('hello') do |resp|
25
+ count += 1
26
+ resp[0].to_i.must_equal 0
27
+ resp[2].must_be_empty
28
+ count.must_equal 1
29
+ end
30
+
31
+ end
32
+
33
+ it 'should return OK response from notify' do
34
+
35
+ @subject.register do
36
+ notification 'hello' do end
37
+ end
38
+
39
+ ret = @subject.notify('hello')
40
+ ret.class.must_be_same_as Groem::Response
41
+ ret[0].to_i.must_equal 0
42
+ ret[2].must_be_empty
43
+
44
+ end
45
+
46
+ end
47
+
48
+ describe 'when notification hasnt been specified' do
49
+
50
+ before do
51
+ @p_svr = DummyServerHelper.fork_server(:notify => '-OK')
52
+ Groem::Client.response_class = MarshalHelper.dummy_response_class
53
+ @subject = Groem::App.new('test', :port => DummyServerHelper::DEFAULT_PORT)
54
+ end
55
+
56
+ after do
57
+ DummyServerHelper.kill_server(@p_svr)
58
+ end
59
+
60
+ it 'should return nil' do
61
+
62
+ @subject.register do
63
+ notification 'hello' do end
64
+ end
65
+
66
+ @subject.notify('goodbye') do end.must_be_nil
67
+
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+
74
+ module AppNotifyCallbacksHelper
75
+
76
+
77
+ def should_receive_ok_and_callback(app, rslt, timeout, opts = {})
78
+ click_name = opts[:click] || 'CLICK'
79
+ close_name = opts[:close] || 'CLOSE'
80
+ timedout_name = opts[:timedout] || 'TIMEDOUT'
81
+ ok_count = 0
82
+ click_count = 0
83
+ close_count = 0
84
+ timedout_count = 0
85
+
86
+ EM.run {
87
+ app.register do
88
+ notification 'Foo' do |n|
89
+ n.callback 'You', :type => 'shiny'
90
+ end
91
+ end
92
+
93
+ app.when_callback click_name do |resp|
94
+ puts "App received callback: #{resp[2]['Notification-Callback-Result']}"
95
+ click_count += 1
96
+ ['CLICK', 'CLICKED'].must_include resp[2]['Notification-Callback-Result']
97
+ end
98
+
99
+ app.when_callback close_name do |resp|
100
+ puts "App received callback: #{resp[2]['Notification-Callback-Result']}"
101
+ close_count += 1
102
+ ['CLOSE', 'CLOSED'].must_include resp[2]['Notification-Callback-Result']
103
+ end
104
+
105
+ app.when_callback timedout_name do |resp|
106
+ puts "App received callback: #{resp[2]['Notification-Callback-Result']}"
107
+ timedout_count += 1
108
+ ['TIMEDOUT', 'TIMEOUT'].must_include resp[2]['Notification-Callback-Result']
109
+ end
110
+
111
+ app.notify('Foo') do |resp|
112
+ ok_count += 1 if resp[0].to_i == 0
113
+ resp[0].to_i.must_equal 0
114
+ end
115
+
116
+ EM.add_timer(timeout + 1) {
117
+ ok_count.must_equal 1
118
+ case rslt
119
+ when 'CLICK'
120
+ click_count.must_equal 1
121
+ close_count.must_equal 0
122
+ timedout_count.must_equal 0
123
+ when 'CLOSE'
124
+ click_count.must_equal 0
125
+ close_count.must_equal 1
126
+ timedout_count.must_equal 0
127
+ when 'TIMEDOUT'
128
+ click_count.must_equal 0
129
+ close_count.must_equal 0
130
+ timedout_count.must_equal 1
131
+ end
132
+ EM.stop
133
+ }
134
+
135
+ }
136
+ end
137
+
138
+ def should_receive_ok_and_callback_outside_reactor(app, rslt, timeout, opts = {})
139
+ click_name = opts[:click] || 'CLICK'
140
+ close_name = opts[:close] || 'CLOSE'
141
+ timedout_name = opts[:timedout] || 'TIMEDOUT'
142
+ ok_count = 0
143
+ click_count = 0
144
+ close_count = 0
145
+ timedout_count = 0
146
+
147
+ app.register do
148
+ notification 'Foo' do |n|
149
+ n.callback 'You', :type => 'shiny'
150
+ end
151
+ end
152
+
153
+ app.when_callback click_name do |resp|
154
+ puts "App received callback: #{resp[2]['Notification-Callback-Result']}"
155
+ click_count += 1
156
+ ['CLICK', 'CLICKED'].must_include resp[2]['Notification-Callback-Result']
157
+ end
158
+
159
+ app.when_callback close_name do |resp|
160
+ puts "App received callback: #{resp[2]['Notification-Callback-Result']}"
161
+ close_count += 1
162
+ ['CLOSE', 'CLOSED'].must_include resp[2]['Notification-Callback-Result']
163
+ end
164
+
165
+ app.when_callback timedout_name do |resp|
166
+ puts "App received callback: #{resp[2]['Notification-Callback-Result']}"
167
+ timedout_count += 1
168
+ ['TIMEDOUT', 'TIMEOUT'].must_include resp[2]['Notification-Callback-Result']
169
+ end
170
+
171
+ app.notify('Foo') do |resp|
172
+ ok_count += 1 if resp[0].to_i == 0
173
+ resp[0].to_i.must_equal 0
174
+ end
175
+
176
+ ok_count.must_equal 1
177
+ case rslt
178
+ when 'CLICK'
179
+ click_count.must_equal 1
180
+ close_count.must_equal 0
181
+ timedout_count.must_equal 0
182
+ when 'CLOSE'
183
+ click_count.must_equal 0
184
+ close_count.must_equal 1
185
+ timedout_count.must_equal 0
186
+ when 'TIMEDOUT'
187
+ click_count.must_equal 0
188
+ close_count.must_equal 0
189
+ timedout_count.must_equal 1
190
+ end
191
+
192
+ end
193
+
194
+
195
+ def should_return_ok_response(app)
196
+
197
+ app.register do
198
+ notification 'Foo' do |n|
199
+ n.callback 'You', :type => 'shiny'
200
+ end
201
+ end
202
+
203
+ ret = app.notify('Foo')
204
+ ret.class.must_be_same_as Groem::Response
205
+ ret[0].to_i.must_equal 0
206
+ ret[2].must_be_empty
207
+ end
208
+
209
+
210
+ end
211
+
212
+
213
+ describe 'Groem::App #notify with simple callbacks' do
214
+
215
+
216
+ describe 'when CLICK callback returned' do
217
+ include AppNotifyCallbacksHelper
218
+
219
+ before do
220
+ @p_svr = DummyServerHelper.fork_server(:callback => ['CLICK', 2])
221
+ Groem::Client.response_class = MarshalHelper.dummy_response_class
222
+ @subject = Groem::App.new('test', :port => DummyServerHelper::DEFAULT_PORT)
223
+ end
224
+
225
+ after do
226
+ DummyServerHelper.kill_server(@p_svr)
227
+ end
228
+
229
+ it 'should receive ok and CLICK callback' do
230
+ should_receive_ok_and_callback(@subject, 'CLICK', 2)
231
+ end
232
+
233
+ it 'should receive ok and CLICK callback outside reactor' do
234
+ should_receive_ok_and_callback_outside_reactor(@subject, 'CLICK', 2)
235
+ end
236
+
237
+ it 'should receive ok and CLICK callback using symbolized actions' do
238
+ should_receive_ok_and_callback(@subject, 'CLICK', 2,
239
+ :click => :click,
240
+ :close => :closed,
241
+ :timedout => :timedout)
242
+ end
243
+
244
+ it 'should receive ok and CLICK callback with alternate action names' do
245
+ should_receive_ok_and_callback(@subject, 'CLICK', 2,
246
+ :click => 'CLICKED',
247
+ :close => 'CLOSED',
248
+ :timedout => 'TIMEOUT')
249
+ end
250
+
251
+ it 'should return ok response' do
252
+ should_return_ok_response(@subject)
253
+ end
254
+
255
+ end
256
+
257
+ describe 'when CLOSE callback returned' do
258
+ include AppNotifyCallbacksHelper
259
+
260
+ before do
261
+ @p_svr = DummyServerHelper.fork_server(:callback => ['CLOSE', 2])
262
+ Groem::Client.response_class = MarshalHelper.dummy_response_class
263
+ @subject = Groem::App.new('test', :port => DummyServerHelper::DEFAULT_PORT)
264
+ end
265
+
266
+ after do
267
+ DummyServerHelper.kill_server(@p_svr)
268
+ end
269
+
270
+ it 'should receive ok and CLOSE callback' do
271
+ should_receive_ok_and_callback(@subject, 'CLOSE', 2)
272
+ end
273
+
274
+ it 'should receive ok and CLOSE callback using symbolized actions' do
275
+ should_receive_ok_and_callback(@subject, 'CLOSE', 2,
276
+ :click => :click,
277
+ :close => :closed,
278
+ :timedout => :timedout)
279
+ end
280
+
281
+ it 'should receive ok and CLOSE callback with alternate action names' do
282
+ should_receive_ok_and_callback(@subject, 'CLOSE', 2,
283
+ :click => 'CLICKED',
284
+ :close => 'CLOSED',
285
+ :timedout => 'TIMEOUT')
286
+ end
287
+
288
+ it 'should return ok response' do
289
+ should_return_ok_response(@subject)
290
+ end
291
+
292
+ end
293
+
294
+ describe 'when TIMEDOUT callback returned' do
295
+ include AppNotifyCallbacksHelper
296
+
297
+ before do
298
+ @p_svr = DummyServerHelper.fork_server(:callback => ['TIMEDOUT', 2])
299
+ Groem::Client.response_class = MarshalHelper.dummy_response_class
300
+ @subject = Groem::App.new('test', :port => DummyServerHelper::DEFAULT_PORT)
301
+ end
302
+
303
+ after do
304
+ DummyServerHelper.kill_server(@p_svr)
305
+ end
306
+
307
+ it 'should receive ok and TIMEDOUT callback' do
308
+ should_receive_ok_and_callback(@subject, 'TIMEDOUT', 2)
309
+ end
310
+
311
+ it 'should receive ok and TIMEDOUT callback using symbolized actions' do
312
+ should_receive_ok_and_callback(@subject, 'TIMEDOUT', 2,
313
+ :click => :click,
314
+ :close => :closed,
315
+ :timedout => :timedout)
316
+ end
317
+
318
+ it 'should receive ok and TIMEDOUT callback with alternate action names' do
319
+ should_receive_ok_and_callback(@subject, 'TIMEDOUT', 2,
320
+ :click => 'CLICKED',
321
+ :close => 'CLOSED',
322
+ :timedout => 'TIMEOUT')
323
+ end
324
+
325
+ it 'should return ok response' do
326
+ should_return_ok_response(@subject)
327
+ end
328
+
329
+ end
330
+
331
+ end
332
+
333
+
334
+ describe 'Groem::App #notify with routed callbacks' do
335
+
336
+ before do
337
+ @p_svr = DummyServerHelper.fork_server(:callback => ['CLICK', 2])
338
+ Groem::Client.response_class = MarshalHelper.dummy_response_class
339
+ @subject = Groem::App.new('test', :port => DummyServerHelper::DEFAULT_PORT)
340
+ end
341
+
342
+ after do
343
+ DummyServerHelper.kill_server(@p_svr)
344
+ end
345
+
346
+ it 'should receive ok and CLICK callback matching multiple routes' do
347
+ ok_count = 0
348
+ cb_count = 0
349
+
350
+ app = @subject
351
+ app.register do
352
+ notification 'Foo' do |n|
353
+ n.callback 'You', :type => 'shiny'
354
+ end
355
+ end
356
+
357
+ app.when_click 'You' do |resp|
358
+ cb_count.must_equal 1
359
+ cb_count += 1
360
+ end
361
+
362
+ app.when_click :context => 'You', :type => 'shiny' do |resp|
363
+ cb_count.must_equal 0
364
+ cb_count += 1
365
+ end
366
+
367
+ app.when_click do |resp|
368
+ cb_count.must_equal 3
369
+ cb_count += 1
370
+ end
371
+
372
+ app.when_click :type => 'shiny' do |resp|
373
+ cb_count.must_equal 2
374
+ cb_count += 1
375
+ end
376
+
377
+ app.notify('Foo') do |resp|
378
+ ok_count += 1 if resp[0].to_i == 0
379
+ resp[0].to_i.must_equal 0
380
+ end
381
+
382
+ ok_count.must_equal 1
383
+ cb_count.must_equal 4
384
+ end
385
+
386
+
387
+ end
388
+
389
+
390
+