diff --git a/qubes/tests/integ/vm_qrexec_gui.py b/qubes/tests/integ/vm_qrexec_gui.py index 5619a62d..5ed61f91 100644 --- a/qubes/tests/integ/vm_qrexec_gui.py +++ b/qubes/tests/integ/vm_qrexec_gui.py @@ -36,6 +36,8 @@ import qubes.tests import qubes.vm.appvm import qubes.vm.templatevm +import numpy as np + class TC_00_AppVMMixin(object): def setUp(self): @@ -421,17 +423,14 @@ class TC_00_AppVMMixin(object): # then wait for the stream to appear in dom0 local_user = grp.getgrnam('qubes').gr_mem[0] p = self.loop.run_until_complete(asyncio.create_subprocess_shell( - "sudo -E -u {} timeout 30s sh -c '" - "while ! pactl list sink-inputs | grep -q :{}; do sleep 1; done'".format( - local_user, vm.name))) + "sudo -E -u {} timeout 60s sh -c '" + "while ! pactl list sink-inputs | grep -q :{}; do sleep 1; done'" + .format(local_user, vm.name))) self.loop.run_until_complete(p.wait()) # and some more... self.loop.run_until_complete(asyncio.sleep(1)) - - @unittest.skipUnless(spawn.find_executable('parecord'), - "pulseaudio-utils not installed in dom0") - def test_220_audio_playback(self): + def prepare_audio_vm(self): if 'whonix-gw' in self.template: self.skipTest('whonix-gw have no audio') self.loop.run_until_complete(self.testvm1.start()) @@ -440,22 +439,46 @@ class TC_00_AppVMMixin(object): self.testvm1.run_for_stdio('which parecord')) except subprocess.CalledProcessError: self.skipTest('pulseaudio-utils not installed in VM') - self.wait_for_pulseaudio_startup(self.testvm1) - # generate some "audio" data - audio_in = b'\x20' * 44100 + + def check_audio_sample(self, sample, sfreq): + rec = np.fromstring(sample, dtype=np.float32) + # determine sample size using silence threshold + threshold = 10**-3 + rec_size = np.count_nonzero((rec > threshold) | (rec < -threshold)) + if not rec_size: + self.fail('only silence detected, no useful audio data') + # find zero crossings + crossings = np.nonzero((rec[1:] > threshold) & + (rec[:-1] < -threshold))[0] + np.seterr('raise') + # compare against sine wave frequency + rec_freq = rec_size/np.mean(np.diff(crossings)) + if not sfreq*0.8 < rec_freq < sfreq*1.2: + self.fail('frequency {} not in specified range' + .format(rec_freq)) + + def common_audio_playback(self): + # sine frequency + sfreq = 4400 + # generate signal + audio_in = np.sin(2*np.pi*np.arange(44100)*sfreq/44100) self.loop.run_until_complete( - self.testvm1.run_for_stdio('cat > audio_in.raw', input=audio_in)) + self.testvm1.run_for_stdio('cat > audio_in.raw', + input=audio_in.astype(np.float32).tobytes())) local_user = grp.getgrnam('qubes').gr_mem[0] with tempfile.NamedTemporaryFile() as recorded_audio: os.chmod(recorded_audio.name, 0o666) # FIXME: -d 0 assumes only one audio device p = subprocess.Popen(['sudo', '-E', '-u', local_user, - 'parecord', '-d', '0', '--raw', recorded_audio.name], - stdout=subprocess.PIPE) + 'parecord', '-d', '0', '--raw', + '--format=float32le', '--rate=44100', '--channels=1', + recorded_audio.name], stdout=subprocess.PIPE) try: self.loop.run_until_complete( - self.testvm1.run_for_stdio('paplay --raw audio_in.raw')) + self.testvm1.run_for_stdio( + 'paplay --format=float32le --rate=44100 \ + --channels=1 --raw audio_in.raw')) except subprocess.CalledProcessError as err: self.fail('{} stderr: {}'.format(str(err), err.stderr)) # wait for possible parecord buffering @@ -464,15 +487,7 @@ class TC_00_AppVMMixin(object): # for some reason sudo do not relay SIGTERM sent above subprocess.check_call(['pkill', 'parecord']) p.wait() - # allow up to 20ms missing, don't use assertIn, to avoid printing - # the whole data in error message - recorded_audio = recorded_audio.file.read() - if audio_in[:-3528] not in recorded_audio: - found_bytes = recorded_audio.count(audio_in[0]) - all_bytes = len(audio_in) - self.fail('played sound not found in dom0, ' - 'missing {} bytes out of {}'.format( - all_bytes-found_bytes, all_bytes)) + self.check_audio_sample(recorded_audio.file.read(), sfreq) def _configure_audio_recording(self, vm): '''Connect VM's output-source to sink monitor instead of mic''' @@ -497,19 +512,7 @@ class TC_00_AppVMMixin(object): subprocess.check_call(sudo + ['pacmd', 'move-source-output', last_index, '0']) - @unittest.skipUnless(spawn.find_executable('parecord'), - "pulseaudio-utils not installed in dom0") - def test_221_audio_record_muted(self): - if 'whonix-gw' in self.template: - self.skipTest('whonix-gw have no audio') - self.loop.run_until_complete(self.testvm1.start()) - try: - self.loop.run_until_complete( - self.testvm1.run_for_stdio('which parecord')) - except subprocess.CalledProcessError: - self.skipTest('pulseaudio-utils not installed in VM') - - self.wait_for_pulseaudio_startup(self.testvm1) + def common_audio_record_muted(self): # connect VM's recording source output monitor (instead of mic) self._configure_audio_recording(self.testvm1) @@ -535,36 +538,25 @@ class TC_00_AppVMMixin(object): if audio_in[:32] in recorded_audio: self.fail('VM recorded something, even though mic disabled') - @unittest.skipUnless(spawn.find_executable('parecord'), - "pulseaudio-utils not installed in dom0") - def test_222_audio_record_unmuted(self): - if 'whonix-gw' in self.template: - self.skipTest('whonix-gw have no audio') - self.loop.run_until_complete(self.testvm1.start()) - try: - self.loop.run_until_complete( - self.testvm1.run_for_stdio('which parecord')) - except subprocess.CalledProcessError: - self.skipTest('pulseaudio-utils not installed in VM') - - self.wait_for_pulseaudio_startup(self.testvm1) - da = qubes.devices.DeviceAssignment(self.app.domains[0], 'mic') + def common_audio_record_unmuted(self): + deva = qubes.devices.DeviceAssignment(self.app.domains[0], 'mic') self.loop.run_until_complete( - self.testvm1.devices['mic'].attach(da)) + self.testvm1.devices['mic'].attach(deva)) # connect VM's recording source output monitor (instead of mic) self._configure_audio_recording(self.testvm1) - - # generate some "audio" data - audio_in = b'\x20' * 44100 + sfreq = 4400 + audio_in = np.sin(2*np.pi*np.arange(44100)*sfreq/44100) local_user = grp.getgrnam('qubes').gr_mem[0] - record = self.loop.run_until_complete( - self.testvm1.run('parecord --raw audio_rec.raw')) + record = self.loop.run_until_complete(self.testvm1.run( + 'parecord --raw --format=float32le --rate=44100 \ + --channels=1 audio_rec.raw')) # give it time to start recording self.loop.run_until_complete(asyncio.sleep(0.5)) p = subprocess.Popen(['sudo', '-E', '-u', local_user, - 'paplay', '--raw'], + 'paplay', '--raw', '--format=float32le', + '--rate=44100', '--channels=1'], stdin=subprocess.PIPE) - p.communicate(audio_in) + p.communicate(audio_in.astype(np.float32).tobytes()) # wait for possible parecord buffering self.loop.run_until_complete(asyncio.sleep(1)) self.loop.run_until_complete( @@ -573,15 +565,60 @@ class TC_00_AppVMMixin(object): if record_stderr: self.fail('parecord printed something on stderr: {}'.format( record_stderr)) + recorded_audio, _ = self.loop.run_until_complete( self.testvm1.run_for_stdio('cat audio_rec.raw')) - # allow up to 20ms to be missing - if audio_in[:-3528] not in recorded_audio: - found_bytes = recorded_audio.count(audio_in[0]) - all_bytes = len(audio_in) - self.fail('VM not recorded expected data, ' - 'missing {} bytes out of {}'.format( - all_bytes-found_bytes, all_bytes)) + self.check_audio_sample(recorded_audio, sfreq) + + @unittest.skipUnless(spawn.find_executable('parecord'), + "pulseaudio-utils not installed in dom0") + def test_220_audio_play(self): + self.prepare_audio_vm() + self.common_audio_playback() + + @unittest.skipUnless(spawn.find_executable('parecord'), + "pulseaudio-utils not installed in dom0") + def test_221_audio_rec_muted(self): + self.prepare_audio_vm() + self.common_audio_record_muted() + + @unittest.skipUnless(spawn.find_executable('parecord'), + "pulseaudio-utils not installed in dom0") + def test_222_audio_rec_unmuted(self): + self.prepare_audio_vm() + self.common_audio_record_unmuted() + + @unittest.skipUnless(spawn.find_executable('parecord'), + "pulseaudio-utils not installed in dom0") + def test_223_audio_play_hvm(self): + self.testvm1.virt_mode = 'hvm' + self.testvm1.features['audio-model'] = 'ich6' + self.prepare_audio_vm() + self.loop.run_until_complete( + self.testvm1.run_for_stdio('pacmd unload-module module-vchan-sink')) + self.common_audio_playback() + + @unittest.skipUnless(spawn.find_executable('parecord'), + "pulseaudio-utils not installed in dom0") + def test_224_audio_rec_muted_hvm(self): + self.testvm1.virt_mode = 'hvm' + self.testvm1.features['audio-model'] = 'ich6' + self.prepare_audio_vm() + self.loop.run_until_complete( + self.testvm1.run_for_stdio('pacmd unload-module module-vchan-sink')) + self.common_audio_record_muted() + + @unittest.skipUnless(spawn.find_executable('parecord'), + "pulseaudio-utils not installed in dom0") + def test_225_audio_rec_unmuted_hvm(self): + self.testvm1.virt_mode = 'hvm' + self.testvm1.features['audio-model'] = 'ich6' + self.prepare_audio_vm() + self.loop.run_until_complete( + self.testvm1.run_for_stdio('pacmd set-sink-volume 1 0x10000')) + self.loop.run_until_complete( + self.testvm1.run_for_stdio('pacmd unload-module module-vchan-sink')) + self.common_audio_record_unmuted() def test_250_resize_private_img(self): """