Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
449d990
feat: automatically iterate audio sinks
tomli380576 Jan 28, 2026
1cadb69
doc: types
tomli380576 Apr 1, 2026
c787a34
refactor: remove Popen
tomli380576 Apr 1, 2026
65607f4
fix: flush the dividers
tomli380576 Apr 1, 2026
6041007
fix: correctly map node to device
tomli380576 Apr 1, 2026
d80c129
feat: update test plan
tomli380576 Apr 1, 2026
2821b11
refactor: move sink search to a diff function
tomli380576 Apr 7, 2026
b7f903d
feat: don't go full auto
tomli380576 Apr 7, 2026
2679dd0
style: prettier prompts
tomli380576 Apr 7, 2026
9bb0e6e
style: include product name to mimic what gnome has
tomli380576 Apr 7, 2026
fc478cf
fix: wrong args
tomli380576 Apr 7, 2026
b12562c
style: minor output changes
tomli380576 Apr 7, 2026
19f51c5
test: add happy path test
tomli380576 Apr 8, 2026
7ee4b56
fix: copilot suggestions
tomli380576 Apr 8, 2026
e9779a5
fix: use shlex.split and add early exit tests
tomli380576 Apr 8, 2026
5766afa
test: test N==0 case
tomli380576 Apr 8, 2026
3af48ab
test: trigger bad input path
tomli380576 Apr 8, 2026
4bd3570
fix: formatting
tomli380576 Apr 8, 2026
2d50858
style: output style
tomli380576 Apr 8, 2026
3407900
fix: copilot suggestion
tomli380576 Apr 9, 2026
2c6b005
fix: comment
tomli380576 Apr 9, 2026
7095b85
test: coverage
tomli380576 Apr 9, 2026
07b9ee1
3.5
tomli380576 Apr 9, 2026
3ac2c22
fix: 3.5 methods
tomli380576 Apr 9, 2026
01b396a
fix: catch sp exceptions
tomli380576 Apr 9, 2026
b31e680
fix: formatting
tomli380576 Apr 9, 2026
33f4415
test: coverage
tomli380576 Apr 9, 2026
418d42f
fix: flush the input prompt
tomli380576 Apr 9, 2026
a44c2ef
fix: extra dash when name is empty
tomli380576 Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 123 additions & 45 deletions checkbox-support/checkbox_support/scripts/pipewire_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class PipewireTest:

logger = logging.getLogger()

def _get_pw_type(self, media_class) -> str:
def _get_pw_type(self, media_class: str) -> str:
"""
convert sink to Output and source to Input

Expand All @@ -96,7 +96,9 @@ def _get_pw_type(self, media_class) -> str:
self.logger.info("Media class:[{}] is unknown".format(media_class))
return "UNKNOWN CLASS"

def _get_pw_dump(self, p_type) -> dict:
def _get_pw_dump(
self, p_type: 't.Literal["Device", "Node"]'
) -> "list[dict[str, t.Any]]":
"""
Use to convert the json output of pw-dump to dict object

Expand All @@ -113,7 +115,7 @@ def _get_pw_dump(self, p_type) -> dict:
return json.loads(pw_dump)
except (json.decoder.JSONDecodeError, TypeError):
self.logger.error("pw-dump {} failed !!!".format(p_type))
return {}
return []

def generate_pw_media_class(self, media_type, media_class) -> str:
"""
Expand Down Expand Up @@ -319,7 +321,7 @@ def gst_pipeline(self, pipe, timeout, device) -> int:

return PipewireTestError.NO_ERROR

def _get_audio_config(self, mode) -> set:
def _get_audio_config(self, mode):
"""
Get simple audio configuration
This function parse output of pw-dump to find the device type
Expand All @@ -331,7 +333,7 @@ def _get_audio_config(self, mode) -> set:
:type mode: str
"""
clients = self._get_pw_dump("Device")
cfg = set()
cfg = set() # type: set[tuple[str, str, str]]
for client in clients:
active_ports = None
mclass = client["info"]["props"].get("media.class")
Expand Down Expand Up @@ -380,7 +382,7 @@ def monitor_active_port_change(self, timeout, mode) -> int:
self.logger.info("Couldn't detect active port change!")
return PipewireTestError.NO_CHANGE_DETECTED

def go_through_ports(self, cmd, mode):
def go_through_ports(self, cmd: str, mode):
"""
Go through available ports for testing
This script checks if the ports on either sinks
Expand All @@ -395,46 +397,109 @@ def go_through_ports(self, cmd, mode):
"""
clients = self._get_pw_dump("Device")
for client in clients:
ports = None
ports = []
mclass = client["info"]["props"].get("media.class")
if mclass == "Audio/Device":
ports = client["info"]["params"]["EnumRoute"]
if ports:
for p in ports:
chosen = None
if p["direction"] == self._get_pw_type(mode) and p[
"available"
] in [
"yes",
"unknown",
]:
while chosen != "yes":
self.logger.info(
"Please select [{}] for "
"testing (if selected, "
"please enter 'yes')".format(p["description"])
)
assert type(ports) is list
Comment thread
tomli380576 marked this conversation as resolved.

for p in ports:
chosen = None
if p["direction"] == self._get_pw_type(mode) and p[
"available"
] in [
"yes",
"unknown",
]:
while chosen != "yes":
self.logger.info(
"Please select [{}] for "
"testing (if selected, "
"please enter 'yes')".format(p["description"])
)

chosen = input()
checked = None
while checked != "yes":
with subprocess.Popen(
cmd,
shell=True,
stdout=subprocess.PIPE,
universal_newlines=True,
) as p:
while p.poll() is None:
line = p.stdout.readline().strip()
self.logger.info(line)
p.kill()
self.logger.info(
"Is working ? please enter 'yes' to leave"
)
chosen = input()
checked = None
while checked != "yes":
# check_call will print to stdout for us
subprocess.check_call(cmd, shell=True)
self.logger.info(
"Is working ? please enter 'yes' to leave"
)
checked = input()

checked = input()
def iter_audio_sinks(self, cmd: str) -> None:
"""Execute the cmd for each audio sink discovered by pipewire

def _get_node_description(self, properties) -> str:
:param cmd: the command to run
"""
total_sinks_tested = 0

pw_audio_devices = [
device
for device in self._get_pw_dump("Device")
if device["info"]["props"].get("media.class") == "Audio/Device"
]
pw_sink_nodes = [
node
for node in self._get_pw_dump("Node")
if node["info"]["props"].get("media.class") == "Audio/Sink"
]

for node in pw_sink_nodes:
# IDs of these "nodes" can be passed to wpctl set-default
node_id = int(node["id"])

device_id = int(node["info"]["props"]["device.id"])
device = None # type: dict[str, t.Any] | None
for dev in pw_audio_devices:
if dev["id"] == device_id:
device = dev
break
assert device, "Could not find device {}".format(device_id)

# now check if the device has at least 1 available route
enum_routes = device["info"]["params"]["EnumRoute"]
assert type(enum_routes) is list
Comment thread
tomli380576 marked this conversation as resolved.
Outdated
testable = False
for route in enum_routes:
# try to match the device to this node
Comment thread
tomli380576 marked this conversation as resolved.
if (
route["devices"][0] # this is an array with just 1 value
!= node["info"]["props"]["card.profile.device"]
):
continue
if route["direction"] != "Output":
print(
"Skipping '{}'".format(route["description"]),
"because it's not a sink",
)
continue
if route["available"] not in ("yes", "unknown"):
print(
"Skipping '{}'".format(route["description"]),
"because it's unavailable",
)
continue
# correct direction + at least 1 available route
testable = True

if testable:
print("=" * 80, flush=True)
total_sinks_tested += 1
# now switch to this sink
subprocess.check_call(["wpctl", "set-default", str(node_id)])
print(
Comment thread
tomli380576 marked this conversation as resolved.
Outdated
"Testing sink '{}', node id = '{}'".format(
node["info"]["props"]["node.description"], node_id
),
"with command '{}'".format(cmd),
)
subprocess.check_call(cmd, shell=True)
print("=" * 80, flush=True)
print("Tested", total_sinks_tested, "audio sinks in total")

def _get_node_description(self, properties) -> "str | None":
"""
Get node description from the output of wpctl inspect

Expand Down Expand Up @@ -490,7 +555,7 @@ def show_default_device(self, device_type):
except subprocess.CalledProcessError as e:
raise RuntimeError("Show default device error {}".format(repr(e)))

def _sort_wpctl_status(self, lines: list) -> list:
def _sort_wpctl_status(self, lines: "list[str]") -> "list[str]":
"""
This method will sort wpctl status for sub-items under catalog only

Expand Down Expand Up @@ -533,10 +598,10 @@ def compare_wpctl_status(self, status_1: str, status_2: str):
:param status_2: path to second wpctl status
"""
with open(status_1, "r") as s1, open(status_2, "r") as s2:
status_1 = s1.readlines()
status_2 = s2.readlines()
sorted_status_1 = self._sort_wpctl_status(status_1)
sorted_status_2 = self._sort_wpctl_status(status_2)
status_1_lines = s1.readlines()
status_2_lines = s2.readlines()
sorted_status_1 = self._sort_wpctl_status(status_1_lines)
sorted_status_2 = self._sort_wpctl_status(status_2_lines)
Comment thread
tomli380576 marked this conversation as resolved.
delta = difflib.unified_diff(sorted_status_1, sorted_status_2, n=0)
diff = "".join(delta)
if diff:
Expand Down Expand Up @@ -753,6 +818,17 @@ def _args_parsing(self, args=sys.argv[1:]):
"-m", "--mode", type=str, help="Either sinks or sources"
)

parser_iter_sink = subparsers.add_parser(
"iter-audio-sinks", help="Iterate all available audio sinks"
)
parser_iter_sink.add_argument(
"-c",
"--command",
type=str,
required=True,
help="command for testing",
)

# Add parser for show default device function
parser_show = subparsers.add_parser(
"show", help="show the default device"
Expand Down Expand Up @@ -823,6 +899,8 @@ def function_select(self, args):
return PipewireTestError.NO_ERROR
else:
return PipewireTestError.NOT_REAL_DEVICE
elif args.test_type == "iter-audio-sinks":
return self.iter_audio_sinks(args.command)


def main():
Expand Down
2 changes: 1 addition & 1 deletion providers/base/units/dock/jobs.pxu
Original file line number Diff line number Diff line change
Expand Up @@ -2598,7 +2598,7 @@ plugin: user-interact-verify
estimated_duration: 60.0
command:
if check_audio_daemon.sh ; then
checkbox-support-pipewire-utils through -m sink -c "speaker-test -c 2 -l 1 -t wav"
checkbox-support-pipewire-utils iter-audio-sinks -m sink -c "speaker-test -c 2 -l 1 -t wav"
Comment thread
tomli380576 marked this conversation as resolved.
Outdated
else
indexes=$(pacmd list-sinks | grep -e 'index' -e 'available' | grep -B 1 -e 'available: unknown' -e 'available: yes' | grep index | awk '{print $NF}')
for index in $indexes
Expand Down
Loading