#!/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)