test: unit test mock PyAudio, integration test use real

Make unit tests use a mock version of PyAudio, while making integration tests using the real version. If no real microphone is available, these integration tests are skipped.

ref: N25B-119
This commit is contained in:
Twirre Meulenbelt
2025-10-22 13:27:35 +02:00
parent 1e3e077029
commit f8db719bfa
3 changed files with 186 additions and 91 deletions

View File

@@ -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"]

View File

@@ -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

View File

@@ -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"Someones 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