mrjoy-launchpad 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,456 @@
1
+ require 'helper'
2
+ require 'timeout'
3
+
4
+ class BreakError < StandardError; end
5
+
6
+ describe Launchpad::Interaction do
7
+
8
+ # returns true/false whether the operation ended or the timeout was hit
9
+ def timeout(timeout = 0.02, &block)
10
+ Timeout.timeout(timeout, &block)
11
+ true
12
+ rescue Timeout::Error
13
+ false
14
+ end
15
+
16
+ def press(interaction, type, opts = nil)
17
+ interaction.respond_to(type, :down, opts)
18
+ interaction.respond_to(type, :up, opts)
19
+ end
20
+
21
+
22
+ def press_all(interaction)
23
+ %w(up down left right session user1 user2 mixer).each do |type|
24
+ press(interaction, type.to_sym)
25
+ end
26
+ 8.times do |y|
27
+ 8.times do |x|
28
+ press(interaction, :grid, :x => x, :y => y)
29
+ end
30
+ press(interaction, :"scene#{y + 1}")
31
+ end
32
+ end
33
+
34
+ describe '#initialize' do
35
+
36
+ it 'creates device if not given' do
37
+ device = Launchpad::Device.new
38
+ Launchpad::Device.expects(:new).
39
+ with(:input => true, :output => true, :logger => nil).
40
+ returns(device)
41
+ interaction = Launchpad::Interaction.new
42
+ assert_same device, interaction.device
43
+ end
44
+
45
+ it 'creates device with given device_name' do
46
+ device = Launchpad::Device.new
47
+ Launchpad::Device.expects(:new).
48
+ with(:device_name => 'device', :input => true, :output => true, :logger => nil).
49
+ returns(device)
50
+ interaction = Launchpad::Interaction.new(:device_name => 'device')
51
+ assert_same device, interaction.device
52
+ end
53
+
54
+ it 'creates device with given input_device_id' do
55
+ device = Launchpad::Device.new
56
+ Launchpad::Device.expects(:new).
57
+ with(:input_device_id => 'in', :input => true, :output => true, :logger => nil).
58
+ returns(device)
59
+ interaction = Launchpad::Interaction.new(:input_device_id => 'in')
60
+ assert_same device, interaction.device
61
+ end
62
+
63
+ it 'creates device with given output_device_id' do
64
+ device = Launchpad::Device.new
65
+ Launchpad::Device.expects(:new).
66
+ with(:output_device_id => 'out', :input => true, :output => true, :logger => nil).
67
+ returns(device)
68
+ interaction = Launchpad::Interaction.new(:output_device_id => 'out')
69
+ assert_same device, interaction.device
70
+ end
71
+
72
+ it 'creates device with given input_device_id/output_device_id' do
73
+ device = Launchpad::Device.new
74
+ Launchpad::Device.expects(:new).
75
+ with(:input_device_id => 'in', :output_device_id => 'out', :input => true, :output => true, :logger => nil).
76
+ returns(device)
77
+ interaction = Launchpad::Interaction.new(:input_device_id => 'in', :output_device_id => 'out')
78
+ assert_same device, interaction.device
79
+ end
80
+
81
+ it 'initializes device if given' do
82
+ device = Launchpad::Device.new
83
+ interaction = Launchpad::Interaction.new(:device => device)
84
+ assert_same device, interaction.device
85
+ end
86
+
87
+ it 'stores the logger given' do
88
+ logger = Logger.new(nil)
89
+ interaction = Launchpad::Interaction.new(:logger => logger)
90
+ assert_same logger, interaction.logger
91
+ assert_same logger, interaction.device.logger
92
+ end
93
+
94
+ it 'doesn\'t activate the interaction' do
95
+ assert !Launchpad::Interaction.new.active
96
+ end
97
+
98
+ end
99
+
100
+ describe '#logger=' do
101
+
102
+ it 'stores the logger and passes it to the device as well' do
103
+ logger = Logger.new(nil)
104
+ interaction = Launchpad::Interaction.new
105
+ interaction.logger = logger
106
+ assert_same logger, interaction.logger
107
+ assert_same logger, interaction.device.logger
108
+ end
109
+
110
+ end
111
+
112
+ describe '#close' do
113
+
114
+ it 'stops the interaction' do
115
+ interaction = Launchpad::Interaction.new
116
+ interaction.expects(:stop)
117
+ interaction.close
118
+ end
119
+
120
+ it 'closes the device' do
121
+ interaction = Launchpad::Interaction.new
122
+ interaction.device.expects(:close)
123
+ interaction.close
124
+ end
125
+
126
+ end
127
+
128
+ describe '#closed?' do
129
+
130
+ it 'returns false on a newly created interaction, but true after closing' do
131
+ interaction = Launchpad::Interaction.new
132
+ assert !interaction.closed?
133
+ interaction.close
134
+ assert interaction.closed?
135
+ end
136
+
137
+ end
138
+
139
+ describe '#start' do
140
+
141
+ before do
142
+ @interaction = Launchpad::Interaction.new
143
+ end
144
+
145
+ after do
146
+ mocha_teardown # so that expectations on Thread.join don't fail in here
147
+ begin
148
+ @interaction.close
149
+ rescue
150
+ # ignore, should be handled in tests, this is just to close all the spawned threads
151
+ end
152
+ end
153
+
154
+ it 'sets active to true in blocking mode' do
155
+ refute @interaction.active
156
+ erg = timeout { @interaction.start }
157
+ refute erg, 'there was no timeout'
158
+ assert @interaction.active
159
+ end
160
+
161
+ it 'sets active to true in detached mode' do
162
+ refute @interaction.active
163
+ @interaction.start(:detached => true)
164
+ assert @interaction.active
165
+ end
166
+
167
+ it 'blocks in blocking mode' do
168
+ erg = timeout { @interaction.start }
169
+ refute erg, 'there was no timeout'
170
+ end
171
+
172
+ it 'returns immediately in detached mode' do
173
+ erg = timeout { @interaction.start(:detached => true) }
174
+ assert erg, 'there was a timeout'
175
+ end
176
+
177
+ it 'raises CommunicationError when Portmidi::DeviceError occurs' do
178
+ @interaction.device.stubs(:read_pending_actions).raises(Portmidi::DeviceError.new(0))
179
+ assert_raises Launchpad::CommunicationError do
180
+ @interaction.start
181
+ end
182
+ end
183
+
184
+ describe 'action handling' do
185
+
186
+ before do
187
+ @interaction.response_to(:mixer, :down) { @mixer_down = true }
188
+ @interaction.response_to(:mixer, :up) do |i,a|
189
+ sleep 0.001 # sleep to make "sure" :mixer :down has been processed
190
+ i.stop
191
+ end
192
+ @interaction.device.expects(:read_pending_actions).
193
+ at_least_once.
194
+ returns([
195
+ {
196
+ :timestamp => 0,
197
+ :state => :down,
198
+ :type => :mixer
199
+ },
200
+ {
201
+ :timestamp => 0,
202
+ :state => :up,
203
+ :type => :mixer
204
+ }
205
+ ])
206
+ end
207
+
208
+ it 'calls respond_to_action with actions from respond_to_action in blocking mode' do
209
+ erg = timeout(0.5) { @interaction.start }
210
+ assert erg, 'the actions weren\'t called'
211
+ assert @mixer_down, 'the mixer button wasn\'t pressed'
212
+ end
213
+
214
+ it 'calls respond_to_action with actions from respond_to_action in detached mode' do
215
+ @interaction.start(:detached => true)
216
+ erg = timeout(0.5) { while @interaction.active; sleep 0.01; end }
217
+ assert erg, 'there was a timeout'
218
+ assert @mixer_down, 'the mixer button wasn\'t pressed'
219
+ end
220
+
221
+ end
222
+
223
+ describe 'latency' do
224
+
225
+ before do
226
+ @device = @interaction.device
227
+ @times = []
228
+ @device.instance_variable_set("@test_interaction_latency_times", @times)
229
+ def @device.read_pending_actions
230
+ @test_interaction_latency_times << Time.now.to_f
231
+ []
232
+ end
233
+ end
234
+
235
+ it 'sleeps with default latency of 0.001s when none given' do
236
+ timeout { @interaction.start }
237
+ assert @times.size > 1
238
+ @times.each_cons(2) do |a,b|
239
+ assert_in_delta 0.001, b - a, 0.01
240
+ end
241
+ end
242
+
243
+ it 'sleeps with given latency' do
244
+ @interaction = Launchpad::Interaction.new(:latency => 0.5, :device => @device)
245
+ timeout(0.55) { @interaction.start }
246
+ assert @times.size > 1
247
+ @times.each_cons(2) do |a,b|
248
+ assert_in_delta 0.5, b - a, 0.01
249
+ end
250
+ end
251
+
252
+ it 'sleeps with absolute value of given negative latency' do
253
+ @interaction = Launchpad::Interaction.new(:latency => -0.1, :device => @device)
254
+ timeout(0.15) { @interaction.start }
255
+ assert @times.size > 1
256
+ @times.each_cons(2) do |a,b|
257
+ assert_in_delta 0.1, b - a, 0.01
258
+ end
259
+ end
260
+
261
+ it 'does not sleep when latency is 0' do
262
+ @interaction = Launchpad::Interaction.new(:latency => 0, :device => @device)
263
+ timeout(0.001) { @interaction.start }
264
+ assert @times.size > 1
265
+ @times.each_cons(2) do |a,b|
266
+ assert_in_delta 0, b - a, 0.1
267
+ end
268
+ end
269
+
270
+ end
271
+
272
+ it 'resets the device after the loop' do
273
+ @interaction.device.expects(:reset)
274
+ @interaction.start(:detached => true)
275
+ @interaction.stop
276
+ end
277
+
278
+ it 'raises NoOutputAllowedError on closed interaction' do
279
+ @interaction.close
280
+ assert_raises Launchpad::NoOutputAllowedError do
281
+ @interaction.start
282
+ end
283
+ end
284
+
285
+ end
286
+
287
+ describe '#stop' do
288
+
289
+ before do
290
+ @interaction = Launchpad::Interaction.new
291
+ end
292
+
293
+ it 'sets active to false in blocking mode' do
294
+ erg = timeout { @interaction.start }
295
+ refute erg, 'there was no timeout'
296
+ assert @interaction.active
297
+ @interaction.stop
298
+ assert !@interaction.active
299
+ end
300
+
301
+ it 'sets active to false in detached mode' do
302
+ @interaction.start(:detached => true)
303
+ assert @interaction.active
304
+ @interaction.stop
305
+ assert !@interaction.active
306
+ end
307
+
308
+ it 'is callable anytime' do
309
+ @interaction.stop
310
+ @interaction.start(:detached => true)
311
+ @interaction.stop
312
+ @interaction.stop
313
+ end
314
+
315
+ # this is kinda greybox tested, since I couldn't come up with another way to test tread handling [thomas, 2010-01-24]
316
+ it 'raises pending exceptions in detached mode' do
317
+ t = Thread.new {raise BreakError}
318
+ Thread.expects(:new).returns(t)
319
+ @interaction.start(:detached => true)
320
+ assert_raises BreakError do
321
+ @interaction.stop
322
+ end
323
+ end
324
+
325
+ end
326
+
327
+ describe '#response_to/#no_response_to/#respond_to' do
328
+
329
+ before do
330
+ @interaction = Launchpad::Interaction.new
331
+ end
332
+
333
+ it 'calls all responses that match, and not others' do
334
+ @interaction.response_to(:mixer, :down) {|i, a| @mixer_down = true}
335
+ @interaction.response_to(:all, :down) {|i, a| @all_down = true}
336
+ @interaction.response_to(:all, :up) {|i, a| @all_up = true}
337
+ @interaction.response_to(:grid, :down) {|i, a| @grid_down = true}
338
+ @interaction.respond_to(:mixer, :down)
339
+ assert @mixer_down
340
+ assert @all_down
341
+ assert !@all_up
342
+ assert !@grid_down
343
+ end
344
+
345
+ it 'does not call responses when they are deregistered' do
346
+ @interaction.response_to(:mixer, :down) {|i, a| @mixer_down = true}
347
+ @interaction.response_to(:mixer, :up) {|i, a| @mixer_up = true}
348
+ @interaction.response_to(:all, :both) {|i, a| @all_down = a[:state] == :down}
349
+ @interaction.no_response_to(:mixer, :down)
350
+ @interaction.respond_to(:mixer, :down)
351
+ assert !@mixer_down
352
+ assert !@mixer_up
353
+ assert @all_down
354
+ @interaction.respond_to(:mixer, :up)
355
+ assert !@mixer_down
356
+ assert @mixer_up
357
+ assert !@all_down
358
+ end
359
+
360
+ it 'does not call responses registered for both when removing for one of both states' do
361
+ @interaction.response_to(:mixer, :both) {|i, a| @mixer = true}
362
+ @interaction.no_response_to(:mixer, :down)
363
+ @interaction.respond_to(:mixer, :down)
364
+ assert !@mixer
365
+ @interaction.respond_to(:mixer, :up)
366
+ assert @mixer
367
+ end
368
+
369
+ it 'removes other responses when adding a new exclusive response' do
370
+ @interaction.response_to(:mixer, :both) {|i, a| @mixer = true}
371
+ @interaction.response_to(:mixer, :down, :exclusive => true) {|i, a| @exclusive_mixer = true}
372
+ @interaction.respond_to(:mixer, :down)
373
+ assert !@mixer
374
+ assert @exclusive_mixer
375
+ @interaction.respond_to(:mixer, :up)
376
+ assert @mixer
377
+ assert @exclusive_mixer
378
+ end
379
+
380
+ it 'allows for multiple types' do
381
+ @downs = []
382
+ @interaction.response_to([:up, :down], :down) {|i, a| @downs << a[:type]}
383
+ @interaction.respond_to(:up, :down)
384
+ @interaction.respond_to(:down, :down)
385
+ @interaction.respond_to(:up, :down)
386
+ assert_equal [:up, :down, :up], @downs
387
+ end
388
+
389
+ describe 'allows to bind to specific grid buttons' do
390
+
391
+ before do
392
+ @downs = []
393
+ @action = lambda {|i, a| @downs << [a[:x], a[:y]]}
394
+ end
395
+
396
+ it 'one specific grid button' do
397
+ @interaction.response_to(:grid, :down, :x => 4, :y => 2, &@action)
398
+ press_all @interaction
399
+ assert_equal [[4, 2]], @downs
400
+ end
401
+
402
+ it 'a complete row of grid buttons' do
403
+ @interaction.response_to(:grid, :down, :y => 2, &@action)
404
+ press_all @interaction
405
+ assert_equal [[0, 2], [1, 2], [2, 2], [3, 2], [4, 2], [5, 2], [6, 2], [7, 2]], @downs
406
+ end
407
+
408
+ it 'a complete column of grid buttons' do
409
+ @interaction.response_to(:grid, :down, :x => 3, &@action)
410
+ press_all @interaction
411
+ assert_equal [[3, 0], [3, 1], [3, 2], [3, 3], [3, 4], [3, 5], [3, 6], [3, 7]], @downs
412
+ end
413
+
414
+ it 'a complex range of grid buttons' do
415
+ @interaction.response_to(:grid, :down, :x => [1,[2]], :y => [1, 3..5], &@action)
416
+ press_all @interaction
417
+ assert_equal [[1, 1], [2, 1], [1, 3], [2, 3], [1, 4], [2, 4], [1, 5], [2, 5]], @downs
418
+ end
419
+
420
+ it 'a specific grid buttons, a column, a row, all grid buttons and all buttons' do
421
+ @interaction.response_to(:all, :down) {|i, a| @downs << [a[:x], a[:y], :all]}
422
+ @interaction.response_to(:grid, :down) {|i, a| @downs << [a[:x], a[:y], :grid]}
423
+ @interaction.response_to(:grid, :down, :x => 0) {|i, a| @downs << [a[:x], a[:y], :col]}
424
+ @interaction.response_to(:grid, :down, :y => 0) {|i, a| @downs << [a[:x], a[:y], :row]}
425
+ @interaction.response_to(:grid, :down, :x => 0, :y => 0, &@action)
426
+ press @interaction, :grid, :x => 0, :y => 0
427
+ assert_equal [[0, 0], [0, 0, :col], [0, 0, :row], [0, 0, :grid], [0, 0, :all]], @downs
428
+ end
429
+
430
+ end
431
+
432
+ end
433
+
434
+ describe 'regression tests' do
435
+
436
+ it 'does not raise an exception or write an error to the logger when calling stop within a response in attached mode' do
437
+ log = StringIO.new
438
+ logger = Logger.new(log)
439
+ logger.level = Logger::ERROR
440
+ i = Launchpad::Interaction.new(:logger => logger)
441
+ i.response_to(:mixer, :down) {|i,a| i.stop}
442
+ i.device.expects(:read_pending_actions).
443
+ at_least_once.
444
+ returns([{
445
+ :timestamp => 0,
446
+ :state => :down,
447
+ :type => :mixer
448
+ }])
449
+ erg = timeout { i.start }
450
+ # assert erg, 'the actions weren\'t called'
451
+ assert_equal '', log.string
452
+ end
453
+
454
+ end
455
+
456
+ end