mrjoy-launchpad 0.4.0
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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +8 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +136 -0
- data/Rakefile +10 -0
- data/examples/binary_clock.rb +29 -0
- data/examples/color_picker.rb +96 -0
- data/examples/colors.rb +21 -0
- data/examples/doodle.rb +68 -0
- data/examples/double_buffering.rb +104 -0
- data/examples/drawing_board.rb +25 -0
- data/examples/feedback.rb +34 -0
- data/examples/reset.rb +6 -0
- data/launchpad.gemspec +34 -0
- data/lib/launchpad.rb +34 -0
- data/lib/launchpad/device.rb +574 -0
- data/lib/launchpad/errors.rb +37 -0
- data/lib/launchpad/interaction.rb +336 -0
- data/lib/launchpad/logging.rb +27 -0
- data/lib/launchpad/midi_codes.rb +53 -0
- data/lib/launchpad/version.rb +3 -0
- data/monitor.rb +88 -0
- data/test/helper.rb +44 -0
- data/test/test_device.rb +530 -0
- data/test/test_interaction.rb +456 -0
- data/testbed.rb +48 -0
- metadata +146 -0
@@ -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
|