Add web mixer with native input volume and output selection
This commit is contained in:
+25
-1
@@ -40,7 +40,7 @@ class ManagerTests(unittest.TestCase):
|
||||
self.assertIn('"radxa_mix"',f['/etc/shairport-sync.conf'])
|
||||
self.assertIn('--pcm=radxa_mix',f['/etc/systemd/system/bluealsa-aplay.service.d/10-radxa-mixer.conf'])
|
||||
self.assertNotIn('--single-audio', ''.join(f.values()))
|
||||
self.assertIn('ttable { 0.0 0.5 1.1 0.5 }',f['/etc/asound.conf'])
|
||||
self.assertIn('ttable { 0.0 0.707945784 1.1 0.707945784 }',f['/etc/asound.conf'])
|
||||
self.assertIn('rate 44100',f['/etc/asound.conf'])
|
||||
|
||||
def test_output_device_is_not_hardcoded(self):
|
||||
@@ -51,6 +51,8 @@ class ManagerTests(unittest.TestCase):
|
||||
self.assertNotIn('E4:58',unit)
|
||||
self.assertIn('Loopback', f['/etc/asound.conf'])
|
||||
self.assertIn('rate 48000',f['/etc/asound.conf'])
|
||||
self.assertIn('--sbc-quality=medium',f['/etc/systemd/system/bluealsa.service.d/10-radxa-sink.conf'])
|
||||
self.assertIn('-t 500000',unit)
|
||||
|
||||
def test_name_quotes_are_escaped(self):
|
||||
f=m.templates(self.config(name='Desk "Two"'))
|
||||
@@ -64,6 +66,28 @@ class ManagerTests(unittest.TestCase):
|
||||
self.assertFalse(any('pair' in call.args for call in run.call_args_list))
|
||||
self.assertTrue(any('trust' in call.args for call in run.call_args_list))
|
||||
|
||||
def test_bond_request_must_not_report_temporary_pair_as_success(self):
|
||||
def fake(*args, **kwargs):
|
||||
return subprocess.CompletedProcess(args,0,stdout='Paired: yes\nBonded: no')
|
||||
with patch.object(m,'root'),patch.object(m,'run',side_effect=fake) as run:
|
||||
with self.assertRaises(ValueError):m.pair('AA:BB:CC:DD:EE:FF',output=True,bond=True)
|
||||
self.assertFalse(any('trust' in call.args for call in run.call_args_list))
|
||||
|
||||
def test_bond_request_keeps_existing_bond(self):
|
||||
def fake(*args, **kwargs):
|
||||
return subprocess.CompletedProcess(args,0,stdout='Paired: yes\nBonded: yes')
|
||||
with patch.object(m,'root'),patch.object(m,'run',side_effect=fake) as run:
|
||||
m.pair('AA:BB:CC:DD:EE:FF',bond=True)
|
||||
self.assertFalse(any('pair' in call.args for call in run.call_args_list))
|
||||
|
||||
def test_reconfigure_does_not_require_headphone_to_be_on(self):
|
||||
c=self.config(output='bluetooth',bluetooth_device='AA:BB:CC:DD:EE:FF')
|
||||
with patch.object(m,'root'), patch.object(m,'run') as run, patch.object(m,'load',return_value=c), \
|
||||
patch.object(m,'snapshot',return_value=Path('/mock')), patch.object(m,'save'), \
|
||||
patch.object(m,'active',return_value=True),patch.object(m,'enabled',return_value=True):
|
||||
m.apply(c,reconfigure=True)
|
||||
self.assertFalse(any('connect' in call.args for call in run.call_args_list))
|
||||
|
||||
def test_failed_service_restores_configuration(self):
|
||||
c=self.config()
|
||||
with patch.object(m, 'root'), patch.object(m, 'run') as run, \
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0,str(Path(__file__).parents[1]/'scripts'))
|
||||
import input_controls as a
|
||||
import web_server as w
|
||||
|
||||
|
||||
class InputTests(unittest.TestCase):
|
||||
def test_bus_property_scalar_and_method_tuple(self):
|
||||
with patch.object(a,'command',return_value='{"type":"d","data":-15.0}'):
|
||||
self.assertEqual(a.bus('get-property','service','path','interface','Volume'),-15.0)
|
||||
with patch.object(a,'command',return_value='{"type":"a{sv}","data":[{"Volume":-15.0}]}'):
|
||||
self.assertEqual(a.bus('call','service','path','interface','GetAll'),{'Volume':-15.0})
|
||||
|
||||
def test_headphone_volume_write_is_rejected(self):
|
||||
props={'Device':'/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF','Mode':'sink','Transport':'A2DP-source','Volume':0x7f7f}
|
||||
with patch.object(a,'pcms',return_value={'/output':props}), patch.object(a,'command') as command:
|
||||
with self.assertRaises(ValueError):a.set_input('AA:BB:CC:DD:EE:FF',volume=20)
|
||||
command.assert_not_called()
|
||||
|
||||
def test_bluetooth_only_writes_input_pcm(self):
|
||||
source={'Device':'/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF','Mode':'source','Transport':'A2DP-sink','Volume':0x7f7f}
|
||||
sink={**source,'Mode':'sink','Transport':'A2DP-source'}
|
||||
with patch.object(a,'pcms',return_value={'/input':source,'/output':sink}),patch.object(a,'command') as command:
|
||||
a.set_input('AA:BB:CC:DD:EE:FF',volume=40)
|
||||
self.assertEqual(command.call_count,2)
|
||||
self.assertTrue(all('/input' in call.args for call in command.call_args_list))
|
||||
self.assertTrue(all('/output' not in call.args for call in command.call_args_list))
|
||||
|
||||
def test_airplay_uses_existing_native_volume(self):
|
||||
with patch.object(a,'airplay',return_value={'volume':80,'muted':False}),patch.object(a,'remember'),patch.object(a,'command') as command:
|
||||
a.set_input('airplay',volume=50)
|
||||
self.assertEqual(command.call_args.args,('busctl','--system','--','set-property',a.SPS,a.SPS_PATH,a.SPS,'Volume','d','-15.0'))
|
||||
a.set_input('airplay',mute=True)
|
||||
self.assertEqual(command.call_args.args[-1],'-144.0')
|
||||
|
||||
def test_volume_validation_rejects_outside_range_and_bool(self):
|
||||
for level in (-1,101,True,'50'):
|
||||
with self.subTest(level=level), self.assertRaises(ValueError): a.set_input('airplay',volume=level)
|
||||
|
||||
def test_packed_bluetooth_stereo_mute(self):
|
||||
value=a.encode_volume(60,True)
|
||||
level,muted=a.decode_volume(value)
|
||||
self.assertEqual(level,60);self.assertTrue(muted)
|
||||
self.assertEqual((value>>8)&255,value&255)
|
||||
|
||||
|
||||
class WebTests(unittest.TestCase):
|
||||
def test_dac_selection_is_one_atomic_cli_command(self):
|
||||
self.assertEqual(w.validate_action({'action':'output','mode':'dac','pcm':'hw:CARD=Audio,DEV=0'}),
|
||||
['output','dac','--pcm','hw:CARD=Audio,DEV=0'])
|
||||
|
||||
def test_no_output_volume_api(self):
|
||||
for data in [{'action':'output-volume','volume':20},{'action':'volume','input':'dac','volume':20},
|
||||
{'action':'volume','input':'airplay','volume':101},
|
||||
{'action':'output','mode':'bluetooth','address':'AA:BB:CC:DD:EE:FF;reboot'}]:
|
||||
with self.subTest(data=data),self.assertRaises(ValueError):w.validate_action(data)
|
||||
|
||||
def test_http_requires_token_and_same_origin(self):
|
||||
import threading,json,urllib.request,urllib.error
|
||||
server=w.ThreadingHTTPServer(('127.0.0.1',0),w.Handler)
|
||||
thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start()
|
||||
url='http://127.0.0.1:'+str(server.server_address[1])+'/api/action'
|
||||
try:
|
||||
with patch.object(w,'allowed_host',return_value=True),patch.object(w,'launch',return_value='mockjob'):
|
||||
body=json.dumps({'action':'scan'}).encode()
|
||||
for headers in [{'Content-Type':'application/json'}, {'Content-Type':'application/json','X-Mixer-Token':w.TOKEN,'Origin':'http://other.site'}]:
|
||||
req=urllib.request.Request(url,body,headers=headers)
|
||||
with self.assertRaises(urllib.error.HTTPError) as error:urllib.request.urlopen(req,timeout=5)
|
||||
self.assertEqual(error.exception.code,403);error.exception.close()
|
||||
req=urllib.request.Request(url,body,headers={'Content-Type':'application/json','X-Mixer-Token':w.TOKEN})
|
||||
with urllib.request.urlopen(req,timeout=5) as response:self.assertEqual(response.status,202)
|
||||
finally:server.shutdown();server.server_close();thread.join()
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in New Issue
Block a user