diff --git a/test/common/microphone_utils.py b/test/common/microphone_utils.py new file mode 100644 index 0000000..70bcb84 --- /dev/null +++ b/test/common/microphone_utils.py @@ -0,0 +1,97 @@ +import random +import sys +from StringIO import StringIO + +import mock + +from robot_interface.utils.microphone import choose_mic_default, choose_mic_interactive, get_microphones + + +class MicrophoneUtils(object): + """Shared tests for any PyAudio-like implementation, e.g. mock and real.""" + + def test_choose_mic_default(self, pyaudio_instance): + """ + The result must contain at least "index", as this is used to identify the microphone. + The "name" is used for logging, so it should also exist. + It must have one or more channels. + Lastly it must be capable of sending at least 16000 samples per second. + """ + result = choose_mic_default(pyaudio_instance) + assert "index" in result + assert isinstance(result["index"], (int, long)) + + assert "name" in result + assert isinstance(result["name"], (str, unicode)) + + assert "maxInputChannels" in result + assert isinstance(result["maxInputChannels"], (int, long)) + assert result["maxInputChannels"] > 0 + + assert "defaultSampleRate" in result + assert isinstance(result["defaultSampleRate"], float) + assert result["defaultSampleRate"] >= 16000 + + def test_choose_mic_interactive_input_not_int(self, pyaudio_instance, mocker): + """ + First mock an input that's not an integer, then a valid integer. There should be no errors. + """ + mock_input = mocker.patch("__builtin__.raw_input", side_effect=["not an integer", "0"]) + fake_out = StringIO() + mocker.patch.object(sys, "stdout", fake_out) + + result = choose_mic_interactive(pyaudio_instance) + assert "index" in result + assert isinstance(result["index"], (int, long)) + assert result["index"] == 0 + + assert mock_input.called + + assert any(p.startswith("Please enter a number") for p in fake_out.getvalue().splitlines()) + + def test_choose_mic_interactive_negative_index(self, pyaudio_instance, mocker): + """ + Make sure that the interactive method does not allow negative integers as input. + """ + mock_input = mocker.patch("__builtin__.raw_input", side_effect=["-1", "0"]) + fake_out = StringIO() + mocker.patch.object(sys, "stdout", fake_out) + + result = choose_mic_interactive(pyaudio_instance) + assert "index" in result + assert isinstance(result["index"], (int, long)) + assert result["index"] == 0 + + assert mock_input.called + + assert any(p.startswith("Please enter a number") for p in fake_out.getvalue().splitlines()) + + def test_choose_mic_interactive_index_too_high(self, pyaudio_instance, mocker): + """ + Make sure that the interactive method does not allow indices higher than the highest mic index. + """ + real_count = len(list(get_microphones(pyaudio_instance))) + mock_input = mocker.patch("__builtin__.raw_input", side_effect=[str(real_count), "0"]) + fake_out = StringIO() + mocker.patch.object(sys, "stdout", fake_out) + + result = choose_mic_interactive(pyaudio_instance) + assert "index" in result + assert isinstance(result["index"], (int, long)) + + assert mock_input.called + + assert any(p.startswith("Please enter a number") for p in fake_out.getvalue().splitlines()) + + def test_choose_mic_interactive_random_index(self, pyaudio_instance, mocker): + """ + Get a random index from the list of available mics, make sure it's correct. + """ + microphones = list(get_microphones(pyaudio_instance)) + random_index = random.randrange(len(microphones)) + mocker.patch("__builtin__.raw_input", side_effect=[str(random_index)]) + + result = choose_mic_interactive(pyaudio_instance) + assert "index" in result + assert isinstance(result["index"], (int, long)) + assert result["index"] == microphones[random_index]["index"] diff --git a/test/integration/test_microphone_utils.py b/test/integration/test_microphone_utils.py new file mode 100644 index 0000000..a857498 --- /dev/null +++ b/test/integration/test_microphone_utils.py @@ -0,0 +1,20 @@ +import pyaudio + +import pytest + +from common.microphone_utils import MicrophoneUtils + + +@pytest.fixture +def pyaudio_instance(): + audio = pyaudio.PyAudio() + try: + audio.get_default_input_device_info() + return audio + except IOError: + pytest.skip("No microphone available to test with.") + + +class TestAudioIntegration(MicrophoneUtils): + """Run shared audio behavior tests with the mock implementation.""" + pass diff --git a/test/unit/test_microphone_utils.py b/test/unit/test_microphone_utils.py index 0114f73..5ad551d 100644 --- a/test/unit/test_microphone_utils.py +++ b/test/unit/test_microphone_utils.py @@ -1,107 +1,85 @@ -import functools -import random -from StringIO import StringIO -import sys - -import pyaudio +# coding=utf-8 +import mock import pytest +from common.microphone_utils import MicrophoneUtils from robot_interface.utils.microphone import choose_mic_default, choose_mic_interactive +class MockPyAudio: + def __init__(self): + # You can predefine fake device info here + self.devices = [ + { + "index": 0, + "name": u"Someone’s Microphone", # Using a Unicode ’ character + "maxInputChannels": 2, + "maxOutputChannels": 0, + "defaultSampleRate": 44100.0, + "defaultLowInputLatency": 0.01, + "defaultLowOutputLatency": 0.01, + "defaultHighInputLatency": 0.1, + "defaultHighOutputLatency": 0.1, + "hostApi": 0, + }, + { + "index": 1, + "name": u"Mock Speaker 1", + "maxInputChannels": 0, + "maxOutputChannels": 2, + "defaultSampleRate": 48000.0, + "defaultLowInputLatency": 0.01, + "defaultLowOutputLatency": 0.01, + "defaultHighInputLatency": 0.1, + "defaultHighOutputLatency": 0.1, + "hostApi": 0, + }, + ] + + def get_device_count(self): + """Return the number of available mock devices.""" + return len(self.devices) + + def get_device_info_by_index(self, index): + """Return information for a given mock device index.""" + if 0 <= index < len(self.devices): + return self.devices[index] + else: + raise IOError("Invalid device index: {}".format(index)) + + def get_default_input_device_info(self): + """Return info for a default mock input device.""" + for device in self.devices: + if device.get("maxInputChannels", 0) > 0: + return device + raise IOError("No default input device found") + + @pytest.fixture def pyaudio_instance(): - audio = pyaudio.PyAudio() - yield audio + return MockPyAudio() -def test_choose_mic_default(pyaudio_instance): - """ - The result must contain at least "index", as this is used to identify the microphone. - The "name" is used for logging, so it should also exist. - It must have one or more channels. - Lastly it must be capable of sending at least 16000 samples per second. - """ - result = choose_mic_default(pyaudio_instance) - assert "index" in result - assert isinstance(result["index"], long) - - assert "name" in result - assert isinstance(result["name"], (str, unicode)) - - assert "maxInputChannels" in result - assert isinstance(result["maxInputChannels"], long) - assert result["maxInputChannels"] > 0 - - assert "defaultSampleRate" in result - assert isinstance(result["defaultSampleRate"], float) - assert result["defaultSampleRate"] >= 16000 +def _raise_io_error(): + raise IOError() -def test_choose_mic_interactive_input_not_int(pyaudio_instance, mocker): - """ - First mock an input that's not an integer, then a valid integer. There should be no errors. - """ - mock_input = mocker.patch("__builtin__.raw_input", side_effect=["not an integer", "0"]) - fake_out = StringIO() - mocker.patch.object(sys, "stdout", fake_out) +class TestAudioUnit(MicrophoneUtils): + """Run shared audio behavior tests with the mock implementation.""" + def test_choose_mic_default_no_mic(self): + mock_pyaudio = mock.Mock() + mock_pyaudio.get_device_count = mock.Mock(return_value=0L) + mock_pyaudio.get_default_input_device_info = _raise_io_error - result = choose_mic_interactive(pyaudio_instance) - assert "index" in result - assert isinstance(result["index"], (int, long)) - assert result["index"] == 0 + result = choose_mic_default(mock_pyaudio) - assert mock_input.called + assert result is None - assert any(p.startswith("Please enter a number") for p in fake_out.getvalue().splitlines()) + def test_choose_mic_interactive_no_mic(self): + mock_pyaudio = mock.Mock() + mock_pyaudio.get_device_count = mock.Mock(return_value=0L) + mock_pyaudio.get_default_input_device_info = _raise_io_error + result = choose_mic_interactive(mock_pyaudio) -def test_choose_mic_interactive_negative_index(pyaudio_instance, mocker): - """ - Make sure that the interactive method does not allow negative integers as input. - """ - mock_input = mocker.patch("__builtin__.raw_input", side_effect=["-1", "0"]) - fake_out = StringIO() - mocker.patch.object(sys, "stdout", fake_out) - - result = choose_mic_interactive(pyaudio_instance) - assert "index" in result - assert isinstance(result["index"], (int, long)) - assert result["index"] == 0 - - assert mock_input.called - - assert any(p.startswith("Please enter a number") for p in fake_out.getvalue().splitlines()) - - -def test_choose_mic_interactive_index_too_high(pyaudio_instance, mocker): - """ - Make sure that the interactive method does not allow indices higher than the highest mic index. - """ - real_count = pyaudio_instance.get_device_count() - mock_input = mocker.patch("__builtin__.raw_input", side_effect=[str(real_count), "0"]) - fake_out = StringIO() - mocker.patch.object(sys, "stdout", fake_out) - - result = choose_mic_interactive(pyaudio_instance) - assert "index" in result - assert isinstance(result["index"], (int, long)) - assert result["index"] == 0 - - assert mock_input.called - - assert any(p.startswith("Please enter a number") for p in fake_out.getvalue().splitlines()) - - -def test_choose_mic_interactive_random_index(pyaudio_instance, mocker): - """ - Get a random index from the list of available mics, make sure it's correct. - """ - real_count = pyaudio_instance.get_device_count() - random_index = random.randrange(real_count) - mocker.patch("__builtin__.raw_input", side_effect=[str(random_index)]) - - result = choose_mic_interactive(pyaudio_instance) - assert "index" in result - assert isinstance(result["index"], (int, long)) - assert result["index"] == random_index + assert result is None