Skip to content

Commit

Permalink
faking pyalsaaudio
Browse files Browse the repository at this point in the history
  • Loading branch information
sezanzeb committed Oct 29, 2020
1 parent 0f9d41d commit 8b0194d
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 34 deletions.
22 changes: 15 additions & 7 deletions alsacontrol/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,23 @@
import alsaaudio

from alsacontrol.alsa import play_silence, record_to_nowhere
from alsacontrol.services import is_jack_running
from alsacontrol import services
from alsacontrol.logger import logger
from alsacontrol.config import get_config


def input_exists(func, testcard=True, testmixer=True):
"""Check if the configured input card and mixer is available."""
"""Check if the configured input card and mixer is available.
Returns None if no card is configured, because the existance of 'no' card
cannot be determined.
"""
# might be a pcm name with plugin and device
card = get_card(get_config().get('pcm_input'))
if testcard:
if card is None:
logger.debug('%s, No input selected', func)
return False
return None
if not card in alsaaudio.cards():
logger.error('%s, Could not find the input card "%s"', func, card)
return False
Expand All @@ -53,9 +57,13 @@ def output_exists(func, testcard=True, testmixer=True):
"""Check if the configured output card and mixer is available."""
# might be a pcm name with plugin and device
card = get_card(get_config().get('pcm_output'))
if testcard and not card in get_cards():
logger.error('%s, Could not find the output card "%s"', func, card)
return False
if testcard:
if card is None:
logger.debug('%s, No output selected', func)
return None
if not card in get_cards():
logger.error('%s, Could not find the output card "%s"', func, card)
return False
if testmixer and get_config().get('output_use_softvol'):
if 'alsacontrol-output-volume' not in alsaaudio.mixers():
logger.error('%s, Could not find the output softvol mixer', func)
Expand Down Expand Up @@ -107,7 +115,7 @@ def get_card(pcm):
def get_cards():
"""List all cards, including options such as jack."""
cards = alsaaudio.cards()
if is_jack_running():
if services.is_jack_running():
cards.append('jack')
if len(cards) == 0:
logger.error('Could not find any card')
Expand Down
14 changes: 9 additions & 5 deletions alsacontrol/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@


def _modify_config(config_contents, key, value):
"""Write settings into a config files contents.
"""Return a string representing the modified contents of the config file.
Parameters
----------
config_contents : string
Contents of the config file in ~/.config/alsacontrol/config
Contents of the config file in ~/.config/alsacontrol/config.
It is not edited in place and the config file is not overwritten.
key : string
Settings key that should be modified
value : string, int
Expand Down Expand Up @@ -97,15 +98,18 @@ def __init__(self, path=None):
self._config = {}
self.mtime = 0

# create an empty config if it doesn't exist
self.create_config_file()

self.load_config()

def create_config_file(self):
"""Create an empty config if it doesn't exist."""
if not os.path.exists(os.path.dirname(self._path)):
os.makedirs(os.path.dirname(self._path))
if not os.path.exists(self._path):
logger.info('Creating config file "%s"', self._path)
os.mknod(self._path)

self.load_config()

def load_config(self):
"""Read the config file."""
logger.debug('Loading configuration')
Expand Down
23 changes: 13 additions & 10 deletions bin/alsacontrol-gtk
Original file line number Diff line number Diff line change
Expand Up @@ -482,17 +482,19 @@ class ALSAControlWindow:

def on_output_card_selected(self, card_dropdown):
"""Handler for when a card is selected or the list just changed."""
if not output_exists('on_output_card_selected'):
play_silence()

card_name = card_dropdown.get_active_text()
if card_name is None:
return
if get_current_card('pcm_output')[1] == card_name:
# already at the correct selection
return
select_output_pcm(card_name)
setup_asoundrc()

if get_current_card('pcm_output')[1] != card_name:
# card was changed
select_output_pcm(card_name)
setup_asoundrc()

# in any case, if a card is selected but the mixers are not found,
# try to find them.
if not output_exists('on_output_card_selected'):
play_silence()

@only_with_existing_output
def on_output_volume_change(self, gtk_range):
Expand Down Expand Up @@ -582,7 +584,7 @@ class ALSAControlWindow:
setup_asoundrc()
self.display_input()

if not input_exists('on_input_card_selected'):
if card is not None and not input_exists('on_input_card_selected'):
record_to_nowhere()

@only_with_existing_input
Expand Down Expand Up @@ -612,7 +614,8 @@ class ALSAControlWindow:
setup_asoundrc()
self.display_input()

if not input_exists('on_input_card_selected'):
card = get_current_card('pcm_input')[1]
if card is not None and not input_exists('on_input_card_selected'):
record_to_nowhere()

def display_input(self):
Expand Down
136 changes: 136 additions & 0 deletions tests/fakes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ALSA-Control - ALSA configuration interface
# Copyright (C) 2020 sezanzeb <[email protected]>
#
# This file is part of ALSA-Control.
#
# ALSA-Control is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ALSA-Control is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ALSA-Control. If not, see <https://www.gnu.org/licenses/>.


"""Patch alsaaudio to get reproducible tests."""


from unittest.mock import patch

import alsaaudio

from alsacontrol import services
from alsacontrol.config import get_config
from alsacontrol.cards import get_card


fake_config_path = '/tmp/alsacontrol-test-config'


class FakeMixer:
"""Fake mixer object."""
def __init__(self, name):
self.name = name
self.mute = False
self.volume = 50
self.channels = 2

def getmute(self):
return [self.mute] * self.channels

def getvolume(self, pcm):
"""The direction arguments are unused, because I have one mixer
for each direction instead of unidirectional ones.
"""
# two channels
return [self.volume] * self.channels

def setmute(self, mute):
self.mute = mute

def setvolume(self, volume):
self.volume = volume


class FakePCM:
def __init__(self, type, device, *args, **kwargs):
self.type = type
config_key = {
alsaaudio.PCM_CAPTURE: 'pcm_input',
alsaaudio.PCM_PLAYBACK: 'pcm_output'
}[type]
if device is None or device == 'default':
self.card = get_card(get_config().get(config_key))
if self.card is None:
raise ValueError(
'I don\'t think the PCM constructor should '
'be called when no device is set'
)
else:
self.card = device

def write(self, data):
if 'FakeCard2' in self.card:
raise alsaaudio.ALSAAudioError()
if self.type == alsaaudio.PCM_CAPTURE:
raise ValueError('tried to read on a capture PCM')

def read(self):
if 'FakeCard2' in self.card:
raise alsaaudio.ALSAAudioError()
if self.type == alsaaudio.PCM_PLAYBACK:
raise ValueError('tried to write on a playback PCM')
# this is a subset of some random stuff that this function
# returned at some point.
return 3, b'\x01\x00\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff'


class UseFakes:
"""Provides fake functionality for alsaaudio and some services."""
def __init__(self):
self.patches = []

def patch(self):
"""Replace the functions of alsaaudio with various fakes."""
# alsaaudio patches
self.patches.append(patch.object(alsaaudio, 'cards', self.cards))
self.patches.append(patch.object(alsaaudio, 'PCM', FakePCM))
self.patches.append(patch.object(alsaaudio, 'mixers', self.mixers))
self.patches.append(patch.object(alsaaudio, 'Mixer', FakeMixer))

# service patches
self.patches.append(
patch.object(services, 'is_jack_running', lambda: True)
)

for p in self.patches:
p.__enter__()

def restore(self):
"""Restore alsaaudios functionality."""
for p in self.patches:
p.__exit__(None, None, None)
self.patches = []

@staticmethod
def mixers():
config = get_config()
mixers = []
if config.get('input_use_softvol'):
mixers.append('alsacontrol-input-volume')
mixers.append('alsacontrol-input-mute')
if config.get('output_use_softvol'):
mixers.append('alsacontrol-output-volume')
mixers.append('alsacontrol-output-mute')
return mixers

@staticmethod
def cards():
return ['FakeCard1', 'FakeCard2']
9 changes: 4 additions & 5 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,17 @@

from alsacontrol.config import get_config
from alsacontrol.logger import update_verbosity
from fakes import fake_config_path


if __name__ == "__main__":
update_verbosity(debug=True)

config_path = '/tmp/alsacontrol-test-config'

if os.path.exists(config_path):
os.remove(config_path)
if os.path.exists(fake_config_path):
os.remove(fake_config_path)

# don't overwrite the users settings in unittests
get_config(config_path)
get_config(fake_config_path)

modules = sys.argv[1:]
# discoverer is really convenient, but it can't find a specific test
Expand Down
33 changes: 26 additions & 7 deletions tests/testcases/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
# along with ALSA-Control. If not, see <https://www.gnu.org/licenses/>.


import os
import sys
import unittest
from unittest.mock import patch
Expand All @@ -31,6 +32,8 @@
from gi.repository import Gtk

from alsacontrol.config import get_config
from alsacontrol import services
from fakes import UseFakes, fake_config_path


def gtk_iteration():
Expand Down Expand Up @@ -68,14 +71,29 @@ def setUpClass(cls):
# there and just continue to the tests while the UI becomes
# unresponsive
Gtk.main = gtk_iteration

# doesn't do much except avoid some Gtk assertion error, whatever:
Gtk.main_quit = lambda: None

def setUp(self):
self.fakes = UseFakes()
self.fakes.patch()
self.window = launch()

def tearDown(self):
self.window.on_close()
self.window.window.destroy()
gtk_iteration()
self.fakes.restore()
if os.path.exists(fake_config_path):
os.remove(fake_config_path)
config = get_config()
config.create_config_file()
config.load_config()

def test_fakes(self):
self.assertEqual(alsaaudio.cards(), ['FakeCard1', 'FakeCard2'])
self.assertTrue(services.is_jack_running())

def test_can_start(self):
self.assertIsNotNone(self.window)
Expand Down Expand Up @@ -143,29 +161,30 @@ def test_select_input(self):

def test_go_to_input_page(self):
# should start at the output page, no monitoring should be active now
self.assertNotIn(True, [
self.assertEqual([
input_row._input_level_monitor.running
for input_row
in self.window.input_rows
])
], [False, False, False])

notebook = self.window.get('tabs')

notebook.set_current_page(1)
# at least one should be monitoring now
self.assertIn(True, [
# at least one should be monitoring now. The second card is
# configured to raise an error for this test.
self.assertEqual([
input_row._input_level_monitor.running
for input_row
in self.window.input_rows
])
], [True, False, True])

notebook.set_current_page(0)
# and back to stopping them all
self.assertNotIn(True, [
self.assertEqual([
input_row._input_level_monitor.running
for input_row
in self.window.input_rows
])
], [False, False, False])


if __name__ == "__main__":
Expand Down

0 comments on commit 8b0194d

Please sign in to comment.