Преглед изворни кода

Merge remote-tracking branch 'origin/pr/352'

* origin/pr/352:
  audio: set sink volume to workaround alsa save/restore issue
  audio: increase timeout to match hvm loading
  audio: auxiliary pauses should be harmless now, place them back just in case
  audio: add silence threshold
  audio: unload guest' module-vchan-sink in hvm tests
  audio: fix prepare_audio_vm
  audio: do not use pacat on copying audio_in.raw
  Audio: rework audio tests
Marek Marczykowski-Górecki пре 3 година
родитељ
комит
7dfaef35cd
1 измењених фајлова са 102 додато и 65 уклоњено
  1. 102 65
      qubes/tests/integ/vm_qrexec_gui.py

+ 102 - 65
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):
         """