Files
cthulhu/tests/native_audacious.py

112 lines
5.3 KiB
Python

#!/usr/bin/env python3
# Copyright (c) 2026 Storm Dragon
# Authors: Storm Dragon, Angus Kola
# SPDX-License-Identifier: GPL-3.0-or-later
"""Exercise real Audacious GUIs without the desktop player's session or files."""
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import wave
sys.dont_write_bytecode = True
from native_test_session import run_private_session
from native_x11 import wait_for
def run_inside():
root = Path(__file__).resolve().parents[1]
temporary = Path(os.environ['XDG_CONFIG_HOME'])
configPath = temporary / 'i3.conf'
configPath.write_text('font pango:monospace 10\nfocus_follows_mouse no\nipc-socket ' +
str(temporary / 'i3.sock') + '\n', encoding='utf-8')
manager = subprocess.Popen(['i3', '-c', str(configPath)], stdout=subprocess.DEVNULL)
process = None
try:
wait_for(lambda: subprocess.run(['i3-msg', '-t', 'get_version'],
capture_output=True).returncode == 0, 'private i3 missing')
tracks = []
trackDirectory = temporary / 'Example Artist' / 'Example Album'
trackDirectory.mkdir(parents=True)
for name in ('Alpha', 'Beta', 'Gamma'):
path = trackDirectory / (name + '.wav')
with wave.open(str(path), 'wb') as track:
track.setparams((1, 2, 8000, 0, 'NONE', 'not compressed'))
track.writeframes(b'\0\0' * 8000)
tracks.append(str(path))
process = subprocess.Popen(['audacious', '-2', os.environ['CTHULHU_NATIVE_AUDACIOUS'], '-e', *tracks])
def find_window():
result = subprocess.run(['xdotool', 'search', '--onlyvisible', '--name', '^Audacious \\(2\\)$'],
capture_output=True, text=True)
return result.stdout.strip().splitlines()[0] if result.stdout.strip() else None
wait_for(find_window, 'Audacious window missing')
import pyatspi
def find_playlist(node):
if node.getRoleName() in ('tree', 'table'):
table = node.queryTable()
if table.nRows == 3:
return table
for child in node:
result = find_playlist(child)
if result is not None:
return result
return None
playlist = wait_for(lambda: find_playlist(pyatspi.Registry.getDesktop(0)),
'Audacious playlist missing')
assert playlist.addRowSelection(0), 'could not select first fixture row'
if os.environ['CTHULHU_NATIVE_AUDACIOUS'] == '-G':
assert playlist.getAccessibleAt(0, 0).queryComponent().grabFocus(), 'could not focus first fixture cell'
# Selecting through AT-SPI does not initialize Audacious's GTK
# keyboard cursor. The first Down establishes its first row.
subprocess.run(['xdotool', 'key', '--clearmodifiers', 'Down'], check=True)
wait_for(lambda: playlist.getSelectedRows() == [0], 'GTK cursor did not select first row')
subprocess.run(['go', 'test', './internal/app', '-run', '^TestNativeAudacious$',
'-count=1', '-v', '-timeout=90s'], cwd=root, check=True, timeout=120)
finally:
if process is not None:
process.terminate()
process.wait(timeout=5)
manager.terminate()
manager.wait(timeout=5)
if __name__ == '__main__':
if '--diagnose' in sys.argv:
import pyatspi
def describe(node, depth=0):
if depth > 8:
return
if node.getRoleName() in ('tree', 'table', 'frame', 'table cell'):
print(node.getRoleName(), repr(node.name), node.getState().getStates(), flush=True)
if node.getRoleName() in ('tree', 'table'):
table = node.queryTable()
print('ROWS', table.getSelectedRows(), flush=True)
for row in range(table.nRows):
cell = table.getAccessibleAt(row, 0)
print('CELL', repr(cell.name), cell.getState().getStates(), flush=True)
for child in node:
describe(child, depth+1)
for app in pyatspi.Registry.getDesktop(0):
if app.name.lower() == 'audacious':
describe(app)
elif '--inside' in sys.argv:
run_inside()
else:
moduleCache = subprocess.check_output(['go', 'env', 'GOMODCACHE'], text=True).strip()
for mode in ('-Q', '-G'):
with tempfile.TemporaryDirectory(prefix='cthulhu-audacious-') as temporary:
environment = dict(os.environ, XDG_CONFIG_HOME=temporary,
XDG_CACHE_HOME=temporary, XDG_DATA_HOME=temporary, XDG_STATE_HOME=temporary,
GOMODCACHE=moduleCache, GOCACHE='/tmp/cthulhu-go-cache',
GOPROXY='off', QT_QPA_PLATFORM='xcb',
QT_LINUX_ACCESSIBILITY_ALWAYS_ON='1', NO_AT_BRIDGE='0',
CTHULHU_NATIVE_AUDACIOUS=mode)
environment['I3SOCK'] = str(Path(temporary) / 'i3.sock')
run_private_session(['xvfb-run', '-a', 'dbus-run-session', '--',
sys.executable, __file__, '--inside'], env=environment, check=True)