From d5b9b6c308d755cd64c17f644ef38b8af3156f1f Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Sun, 21 Dec 2025 22:06:49 +0330 Subject: [PATCH 1/2] Integration tests for `audio-initial-volume` feature for: https://github.com/QubesOS/qubes-issues/issues/2724 --- qubes/tests/integ/audio.py | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/qubes/tests/integ/audio.py b/qubes/tests/integ/audio.py index a3f575235..9885c1302 100644 --- a/qubes/tests/integ/audio.py +++ b/qubes/tests/integ/audio.py @@ -125,6 +125,8 @@ def create_audio_vm(self, backend, start=True): admin.vm.property.Get +is_preload {vm} @tag:audiovm-{vm} allow target=dom0 admin.vm.feature.CheckWithTemplate +audio {vm} @tag:audiovm-{vm} allow target=dom0 admin.vm.feature.CheckWithTemplate +audio-model {vm} @tag:audiovm-{vm} allow target=dom0 +admin.vm.feature.CheckWithTemplate +audio-initial-volume {vm} @tag:audiovm-{vm} allow target=dom0 +admin.vm.feature.CheckWithTemplate +audio-initial-volume {vm} {vm} allow target=dom0 """.format( vm=self.audiovm.name ) @@ -368,6 +370,32 @@ def _configure_audio_recording(self, vm, expect_stream=True): self.assertGreater(attempts_left, 0, "Failed to move-source-output") + def _get_sink_volume(self, vm) -> str: + """Return VM's sink-input volume percent or `mute` if mute""" + audiovm = vm.audiovm + + sinks = json.loads( + self._call_in_audiovm( + audiovm, ["pactl", "-f", "json", "list", "sink-inputs"] + ) + ) + + if not sinks: + self.fail("no sink-inputs found in {}".format(audiovm.name)) + assert False + + for sink in sinks: + if sink["properties"]["application.name"] == vm.name: + if sink["mute"]: + return "mute" + vol_l = sink["volume"]["front-left"]["value_percent"] + vol_r = sink["volume"]["front-right"]["value_percent"] + assert vol_l == vol_r + return vol_l + + self.fail("{} sink-input not found in {}".format(vm.name, audiovm.name)) + assert False + async def retrieve_audio_input(self, vm, status): try: await asyncio.wait_for( @@ -748,6 +776,26 @@ def test_260_audio_mic_enabled_switch_audiovm(self): self.assert_pacat_running(self.audiovm, self.testvm1, True) self.common_audio_record_muted() + @unittest.skipUnless( + spawn.find_executable("pactl"), + "pulseaudio-utils not installed in dom0", + ) + def test_261_audio_initial_volume_42_percent(self): + self.testvm1.features["audio-initial-volume"] = "42" + self.loop.run_until_complete(self.testvm1.start()) + self.wait_for_pulseaudio_startup(self.testvm1) + assert self._get_sink_volume(self.testvm1) == "42%" + + @unittest.skipUnless( + spawn.find_executable("pactl"), + "pulseaudio-utils not installed in dom0", + ) + def test_262_audio_initial_volume_mute(self): + self.testvm1.features["audio-initial-volume"] = "mute" + self.loop.run_until_complete(self.testvm1.start()) + self.wait_for_pulseaudio_startup(self.testvm1) + assert self._get_sink_volume(self.testvm1) == "mute" + def create_testcases_for_templates(): yield from qubes.tests.create_testcases_for_templates( From 6a38ab0bc0d2514222592cb94f1ec8185c4cd71c Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Sat, 27 Dec 2025 02:27:54 +0330 Subject: [PATCH 2/2] Integration tests: Reset volume/mute settings Reset testvm1 audio volume and clear mute setting after each test. To avoid conflicts with subsequent tests --- qubes/tests/integ/audio.py | 41 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/qubes/tests/integ/audio.py b/qubes/tests/integ/audio.py index 9885c1302..39e59076b 100644 --- a/qubes/tests/integ/audio.py +++ b/qubes/tests/integ/audio.py @@ -396,6 +396,33 @@ def _get_sink_volume(self, vm) -> str: self.fail("{} sink-input not found in {}".format(vm.name, audiovm.name)) assert False + def _reset_sink_volume(self, vm) -> None: + """Reset VM's sink-input volume to 100% and unmuted""" + audiovm = vm.audiovm + + sinks = json.loads( + self._call_in_audiovm( + audiovm, ["pactl", "-f", "json", "list", "sink-inputs"] + ) + ) + + if not sinks: + self.fail("no sink-inputs found in {}".format(audiovm.name)) + assert False + + for sink in sinks: + if sink["properties"]["application.name"] == vm.name: + index = str(sink["index"]) + self._call_in_audiovm( + audiovm, + ["pactl", "set-sink-input-volume", index, "100%"], + ) + self._call_in_audiovm( + audiovm, + ["pactl", "set-sink-input-mute", index, "0"], + ) + break + async def retrieve_audio_input(self, vm, status): try: await asyncio.wait_for( @@ -784,7 +811,12 @@ def test_261_audio_initial_volume_42_percent(self): self.testvm1.features["audio-initial-volume"] = "42" self.loop.run_until_complete(self.testvm1.start()) self.wait_for_pulseaudio_startup(self.testvm1) - assert self._get_sink_volume(self.testvm1) == "42%" + try: + assert self._get_sink_volume(self.testvm1) == "42%" + except AssertionError: + raise + finally: + self._reset_sink_volume(self.testvm1) @unittest.skipUnless( spawn.find_executable("pactl"), @@ -794,7 +826,12 @@ def test_262_audio_initial_volume_mute(self): self.testvm1.features["audio-initial-volume"] = "mute" self.loop.run_until_complete(self.testvm1.start()) self.wait_for_pulseaudio_startup(self.testvm1) - assert self._get_sink_volume(self.testvm1) == "mute" + try: + assert self._get_sink_volume(self.testvm1) == "mute" + except AssertionError: + raise + finally: + self._reset_sink_volume(self.testvm1) def create_testcases_for_templates():