require "helper" require "timeout" class BreakError < StandardError; end describe SurfaceMaster::Launchpad::Interaction do # returns true/false whether the operation ended or the timeout was hit def timeout(timeout = 0.02, &block) Timeout.timeout(timeout, &block) true rescue Timeout::Error false end def press(interaction, type, opts = nil) interaction.respond_to(type, :down, opts) interaction.respond_to(type, :up, opts) end def press_all(interaction) %w(up down left right session user1 user2 mixer).each do |type| press(interaction, type.to_sym) end 8.times do |y| 8.times do |x| press(interaction, :grid, x: x, y: y) end press(interaction, :"scene#{y + 1}") end end describe '#initialize' do it "creates device if not given" do device = SurfaceMaster::Launchpad::Device.new SurfaceMaster::Launchpad::Device.expects(:new) .with(input: true, output: true, logger: nil) .returns(device) interaction = SurfaceMaster::Launchpad::Interaction.new assert_same device, interaction.device end it "creates device with given device_name" do device = SurfaceMaster::Launchpad::Device.new SurfaceMaster::Launchpad::Device.expects(:new) .with(device_name: "device", input: true, output: true, logger: nil) .returns(device) interaction = SurfaceMaster::Launchpad::Interaction.new(device_name: "device") assert_same device, interaction.device end it "creates device with given input_device_id" do device = SurfaceMaster::Launchpad::Device.new SurfaceMaster::Launchpad::Device.expects(:new) .with(input_device_id: "in", input: true, output: true, logger: nil) .returns(device) interaction = SurfaceMaster::Launchpad::Interaction.new(input_device_id: "in") assert_same device, interaction.device end it "creates device with given output_device_id" do device = SurfaceMaster::Launchpad::Device.new SurfaceMaster::Launchpad::Device.expects(:new) .with(output_device_id: "out", input: true, output: true, logger: nil) .returns(device) interaction = SurfaceMaster::Launchpad::Interaction.new(output_device_id: "out") assert_same device, interaction.device end it "creates device with given input_device_id/output_device_id" do device = SurfaceMaster::Launchpad::Device.new SurfaceMaster::Launchpad::Device .expects(:new) .with(input_device_id: "in", output_device_id: "out", input: true, output: true, logger: nil) .returns(device) interaction = SurfaceMaster::Launchpad::Interaction.new(input_device_id: "in", output_device_id: "out") assert_same device, interaction.device end it "initializes device if given" do device = SurfaceMaster::Launchpad::Device.new interaction = SurfaceMaster::Launchpad::Interaction.new(device: device) assert_same device, interaction.device end it "stores the logger given" do logger = Logger.new(nil) interaction = SurfaceMaster::Launchpad::Interaction.new(logger: logger) assert_same logger, interaction.logger assert_same logger, interaction.device.logger end it 'doesn\'t activate the interaction' do assert !SurfaceMaster::Launchpad::Interaction.new.active end end describe '#logger=' do it "stores the logger and passes it to the device as well" do logger = Logger.new(nil) interaction = SurfaceMaster::Launchpad::Interaction.new(logger: logger) assert_same logger, interaction.logger assert_same logger, interaction.device.logger end end describe '#close' do it "stops the interaction" do interaction = SurfaceMaster::Launchpad::Interaction.new interaction.expects(:stop) interaction.close end it "closes the device" do interaction = SurfaceMaster::Launchpad::Interaction.new interaction.device.expects(:close) interaction.close end end describe '#closed?' do it "returns false on a newly created interaction, but true after closing" do interaction = SurfaceMaster::Launchpad::Interaction.new assert !interaction.closed? interaction.close assert interaction.closed? end end describe '#start' do before do @interaction = SurfaceMaster::Launchpad::Interaction.new end after do mocha_teardown # so that expectations on Thread.join don't fail in here begin @interaction.close # rubocop:disable Lint/HandleExceptions rescue # rubocop:enable Lint/HandleExceptions # ignore, should be handled in tests, this is just to close all the spawned threads end end it "sets active to true in blocking mode" do refute @interaction.active erg = timeout { @interaction.start } refute erg, "there was no timeout" assert @interaction.active end it "sets active to true in detached mode" do refute @interaction.active @interaction.start(detached: true) assert @interaction.active end it "blocks in blocking mode" do erg = timeout { @interaction.start } refute erg, "there was no timeout" end it "returns immediately in detached mode" do erg = timeout { @interaction.start(detached: true) } assert erg, "there was a timeout" end it "raises CommunicationError when Portmidi::DeviceError occurs" do @interaction.device.stubs(:read).raises(Portmidi::DeviceError.new(0)) assert_raises SurfaceMaster::CommunicationError do @interaction.start end end describe "action handling" do before do @interaction.response_to(:mixer, :down) { @mixer_down = true } @interaction.response_to(:mixer, :up) do |i, _a| sleep 0.001 # sleep to make "sure" :mixer :down has been processed i.stop end @interaction.device.expects(:read) .at_least_once .returns([ { timestamp: 0, state: :down, type: :mixer }, { timestamp: 0, state: :up, type: :mixer }, ]) end it "calls respond_to_action with actions from respond_to_action in blocking mode" do erg = timeout(0.5) { @interaction.start } assert erg, 'the actions weren\'t called' assert @mixer_down, 'the mixer button wasn\'t pressed' end it "calls respond_to_action with actions from respond_to_action in detached mode" do @interaction.start(detached: true) erg = timeout(0.5) { sleep 0.01 while @interaction.active } assert erg, "there was a timeout" assert @mixer_down, 'the mixer button wasn\'t pressed' end end describe "latency" do # TODO: This seems like a REALLY janky way to handle these tests... # TODO: Also, at best the flow of which-code-creates-which-instance is # TODO: confusing here, and at worst it's doing something unexpected # TODO: that only works by accident. before do @device = @interaction.device @times = [] @device.instance_variable_set("@test_interaction_latency_times", @times) def @device.read @test_interaction_latency_times << Time.now.to_f [] end end it "sleeps with default latency of 0.001s when none given" do @interaction = SurfaceMaster::Launchpad::Interaction.new(device: @device) timeout { @interaction.start } assert @times.size > 1 @times.each_cons(2) do |a, b| # TODO: This.. this is meaningless. WTF. assert_in_delta 0.001, b - a, 0.01 end end it "sleeps with given latency" do @interaction = SurfaceMaster::Launchpad::Interaction.new(latency: 0.5, device: @device) timeout(0.55) { @interaction.start } assert @times.size > 1 @times.each_cons(2) do |a, b| assert_in_delta 0.5, b - a, 0.01 end end it "sleeps with absolute value of given negative latency" do @interaction = SurfaceMaster::Launchpad::Interaction.new(latency: -0.1, device: @device) timeout(0.15) { @interaction.start } assert @times.size > 1 @times.each_cons(2) do |a, b| assert_in_delta 0.1, b - a, 0.11 end end it "does not sleep when latency is 0" do @interaction = SurfaceMaster::Launchpad::Interaction.new(latency: 0, device: @device) timeout(0.01) { @interaction.start } assert @times.size > 1 @interaction.stop @times.each_cons(2) do |a, b| assert_in_delta 0, b - a, 0.01 end end end # TODO: This semantic doesn't appear to be working, and begs the question of whether # TODO: that semantics is DESIRABLE. # it 'resets the device after the loop' do # @interaction.device.expects(:reset) # @interaction.start(detached: true) # @interaction.stop # end it "raises NoInputAllowedError on closed interaction" do @interaction.close assert_raises SurfaceMaster::NoInputAllowedError do @interaction.start end end end describe '#stop' do before do @interaction = SurfaceMaster::Launchpad::Interaction.new end it "sets active to false in blocking mode" do erg = timeout { @interaction.start } refute erg, "there was no timeout" assert @interaction.active @interaction.stop assert !@interaction.active end it "sets active to false in detached mode" do @interaction.start(detached: true) assert @interaction.active @interaction.stop assert !@interaction.active end it "is callable anytime" do @interaction.stop @interaction.start(detached: true) @interaction.stop @interaction.stop end # this is kinda greybox tested, since I couldn't come up with another way to test thread # handling [thomas, 2010-01-24] it "raises pending exceptions in detached mode" do t = Thread.new { fail BreakError } Thread.expects(:new).returns(t) @interaction.start(detached: true) assert_raises BreakError do @interaction.stop end end end describe '#response_to/#no_response_to/#respond_to' do before do @interaction = SurfaceMaster::Launchpad::Interaction.new end it "calls all responses that match, and not others" do @interaction.response_to(:mixer, :down) { |_i, _a| @mixer_down = true } @interaction.response_to(:all, :down) { |_i, _a| @all_down = true } @interaction.response_to(:all, :up) { |_i, _a| @all_up = true } @interaction.response_to(:grid, :down) { |_i, _a| @grid_down = true } @interaction.respond_to(:mixer, :down) assert @mixer_down assert @all_down assert !@all_up assert !@grid_down end it "does not call responses when they are deregistered" do @interaction.response_to(:mixer, :down) { |_i, _a| @mixer_down = true } @interaction.response_to(:mixer, :up) { |_i, _a| @mixer_up = true } @interaction.response_to(:all, :both) { |_i, a| @all_down = a[:state] == :down } @interaction.no_response_to(:mixer, :down) @interaction.respond_to(:mixer, :down) assert !@mixer_down assert !@mixer_up assert @all_down @interaction.respond_to(:mixer, :up) assert !@mixer_down assert @mixer_up assert !@all_down end it "does not call responses registered for both when removing for one of both states" do @interaction.response_to(:mixer, :both) { |_i, _a| @mixer = true } @interaction.no_response_to(:mixer, :down) @interaction.respond_to(:mixer, :down) assert !@mixer @interaction.respond_to(:mixer, :up) assert @mixer end it "removes other responses when adding a new exclusive response" do @interaction.response_to(:mixer, :both) { |_i, _a| @mixer = true } @interaction.response_to(:mixer, :down, exclusive: true) { |_i, _a| @exclusive_mixer = true } @interaction.respond_to(:mixer, :down) assert !@mixer assert @exclusive_mixer @interaction.respond_to(:mixer, :up) assert @mixer assert @exclusive_mixer end it "allows for multiple types" do @downs = [] @interaction.response_to([:up, :down], :down) { |_i, a| @downs << a[:type] } @interaction.respond_to(:up, :down) @interaction.respond_to(:down, :down) @interaction.respond_to(:up, :down) assert_equal [:up, :down, :up], @downs end describe "allows to bind to specific grid buttons" do before do @downs = [] @action = ->(_i, a) { @downs << [a[:x], a[:y]] } end it "one specific grid button" do @interaction.response_to(:grid, :down, x: 4, y: 2, &@action) press_all @interaction assert_equal [[4, 2]], @downs end it "a complete row of grid buttons" do @interaction.response_to(:grid, :down, y: 2, &@action) press_all @interaction assert_equal [[0, 2], [1, 2], [2, 2], [3, 2], [4, 2], [5, 2], [6, 2], [7, 2]], @downs end it "a complete column of grid buttons" do @interaction.response_to(:grid, :down, x: 3, &@action) press_all @interaction assert_equal [[3, 0], [3, 1], [3, 2], [3, 3], [3, 4], [3, 5], [3, 6], [3, 7]], @downs end it "a complex range of grid buttons" do @interaction.response_to(:grid, :down, x: [1, [2]], y: [1, 3..5], &@action) press_all @interaction assert_equal [[1, 1], [2, 1], [1, 3], [2, 3], [1, 4], [2, 4], [1, 5], [2, 5]], @downs end it "a specific grid buttons, a column, a row, all grid buttons and all buttons" do @interaction.response_to(:all, :down) { |_i, a| @downs << [a[:x], a[:y], :all] } @interaction.response_to(:grid, :down) { |_i, a| @downs << [a[:x], a[:y], :grid] } @interaction.response_to(:grid, :down, x: 0) { |_i, a| @downs << [a[:x], a[:y], :col] } @interaction.response_to(:grid, :down, y: 0) { |_i, a| @downs << [a[:x], a[:y], :row] } @interaction.response_to(:grid, :down, x: 0, y: 0, &@action) press @interaction, :grid, x: 0, y: 0 assert_equal [[0, 0], [0, 0, :col], [0, 0, :row], [0, 0, :grid], [0, 0, :all]], @downs end end end describe "regression tests" do it "doesn't kvetch when calling stop in response in attached mode" do log = StringIO.new logger = Logger.new(log) logger.level = Logger::ERROR inter = SurfaceMaster::Launchpad::Interaction.new(logger: logger) inter.response_to(:mixer, :down) { |i, _a| i.stop } inter.device.expects(:read) .at_least_once .returns([{ timestamp: 0, state: :down, type: :mixer }]) # erg = timeout { inter.start } # assert erg, 'the actions weren\'t called' assert_equal "", log.string end end end