Skip to content

Commit a4d8a83

Browse files
committed
fixes
1 parent 93fe5d0 commit a4d8a83

File tree

8 files changed

+328
-201
lines changed

8 files changed

+328
-201
lines changed

conftest.py

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import nowplaying.bootstrap
1919
import nowplaying.config
20-
import nowplaying.db
2120
import nowplaying.apicache
2221

2322
# if sys.platform == 'darwin':
@@ -98,43 +97,53 @@ def bootstrap(getroot): # pylint: disable=redefined-outer-name
9897
# don't stick around
9998
#
10099
@pytest.fixture(autouse=True, scope="function")
101-
def clear_old_testsuite():
100+
def clear_old_testsuite(request):
102101
''' clear out old testsuite configs '''
102+
# Skip Qt app creation for tests that need their own QApplication (like singleinstance tests)
103+
skip_qt_cleanup = 'qapp' in request.fixturenames
104+
103105
if sys.platform == "win32":
104106
qsettingsformat = QSettings.IniFormat
105107
else:
106108
qsettingsformat = QSettings.NativeFormat
107109

108110
nowplaying.bootstrap.set_qt_names(appname='testsuite')
109-
config = QSettings(qsettingsformat, QSettings.SystemScope, QCoreApplication.organizationName(),
110-
QCoreApplication.applicationName())
111-
config.clear()
112-
config.sync()
113-
114-
cachedir = pathlib.Path(QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0])
115-
if 'testsuite' in cachedir.name and cachedir.exists():
116-
logging.info('Removing %s', cachedir)
117-
shutil.rmtree(cachedir)
118-
119-
config = QSettings(qsettingsformat, QSettings.UserScope, QCoreApplication.organizationName(),
120-
QCoreApplication.applicationName())
121-
config.clear()
122-
config.sync()
123-
filename = pathlib.Path(config.fileName())
124-
del config
125-
if filename.exists():
111+
112+
# Only create QCoreApplication if test doesn't need QApplication
113+
if not skip_qt_cleanup:
114+
config = QSettings(qsettingsformat, QSettings.SystemScope, QCoreApplication.organizationName(),
115+
QCoreApplication.applicationName())
116+
config.clear()
117+
config.sync()
118+
119+
cachedir = pathlib.Path(QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0])
120+
if 'testsuite' in cachedir.name and cachedir.exists():
121+
logging.info('Removing %s', cachedir)
122+
shutil.rmtree(cachedir)
123+
124+
config = QSettings(qsettingsformat, QSettings.UserScope, QCoreApplication.organizationName(),
125+
QCoreApplication.applicationName())
126+
config.clear()
127+
config.sync()
128+
filename = pathlib.Path(config.fileName())
129+
del config
130+
else:
131+
# For Qt app tests, just set up the filename for cleanup
132+
filename = None
133+
134+
if filename and filename.exists():
126135
filename.unlink()
127136
reboot_macosx_prefs()
128-
if filename.exists():
137+
if filename and filename.exists():
129138
filename.unlink()
130139
reboot_macosx_prefs()
131-
if filename.exists():
140+
if filename and filename.exists():
132141
logging.error('Still exists, wtf?')
133142
yield filename
134-
if filename.exists():
143+
if filename and filename.exists():
135144
filename.unlink()
136145
reboot_macosx_prefs()
137-
if filename.exists():
146+
if filename and filename.exists():
138147
filename.unlink()
139148
reboot_macosx_prefs()
140149

@@ -155,8 +164,8 @@ async def temp_api_cache():
155164
await cache.close()
156165

157166

158-
@pytest.fixture(autouse=True)
159-
def auto_temp_api_cache_for_artistextras(request, temp_api_cache): # pylint: disable=redefined-outer-name
167+
@pytest_asyncio.fixture(autouse=True)
168+
async def auto_temp_api_cache_for_artistextras(request, temp_api_cache): # pylint: disable=redefined-outer-name
160169
"""Automatically use temp API cache for artistextras tests to prevent random CI failures."""
161170
# Only auto-apply to artistextras tests, not apicache tests
162171
if 'test_artistextras' in request.module.__name__:

nowplaying/subprocesses.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ def _start_process(self, processname):
7979
))
8080
self.processes[processname]['process'].start()
8181

82-
def _stop_process_parallel(self, processname):
83-
''' Stop a process - designed for parallel execution '''
82+
def _shutdown_process_common(self, processname: str, graceful_timeout: int = 8) -> None:
83+
''' Common shutdown logic for both parallel and sequential process stopping '''
8484
if not self.processes[processname]['process']:
8585
return
8686

@@ -92,11 +92,13 @@ def _stop_process_parallel(self, processname):
9292
try:
9393
func = getattr(self.processes[processname]['module'], 'stop')
9494
func(process.pid)
95-
except Exception as error: # pylint: disable=broad-exception-caught
95+
except (AttributeError, TypeError) as error:
9696
logging.error('Error calling stop function for %s: %s', processname, error)
97+
except Exception as error: # pylint: disable=broad-exception-caught
98+
logging.exception('Unexpected error calling stop function for %s: %s', processname, error)
9799

98-
# Wait for graceful shutdown (reduced since we're parallel)
99-
process.join(8)
100+
# Wait for graceful shutdown
101+
process.join(graceful_timeout)
100102

101103
# Force termination if still alive
102104
if process.is_alive():
@@ -108,19 +110,25 @@ def _stop_process_parallel(self, processname):
108110
# Cleanup - be defensive on Windows
109111
try:
110112
process.close()
111-
except Exception as error: # pylint: disable=broad-exception-caught
113+
except (OSError, AttributeError) as error:
112114
logging.debug('Error closing process %s: %s', processname, error)
115+
except Exception as error: # pylint: disable=broad-exception-caught
116+
logging.exception('Unexpected error closing process %s: %s', processname, error)
113117

114118
del self.processes[processname]['process']
115119
self.processes[processname]['process'] = None
116120
logging.debug('%s stopped successfully', processname)
117121

118-
def _stop_process(self, processname):
122+
def _stop_process_parallel(self, processname: str) -> None:
123+
''' Stop a process - designed for parallel execution '''
124+
self._shutdown_process_common(processname, graceful_timeout=8)
125+
126+
def _stop_process(self, processname: str) -> None:
119127
''' Stop a process - sequential version for individual stops '''
120128
if self.processes[processname]['process']:
121129
logging.debug('Notifying %s', processname)
122130
self.processes[processname]['stopevent'].set()
123-
self._stop_process_parallel(processname)
131+
self._shutdown_process_common(processname, graceful_timeout=15)
124132
logging.debug('%s should be stopped', processname)
125133

126134
def start_process(self, processname):

nowplaying/systemtray.py

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
''' system tray '''
33

44
import logging
5+
import sqlite3
56

67
from PySide6.QtWidgets import ( # pylint: disable=no-name-in-module
78
QApplication, QErrorMessage, QMenu, QMessageBox, QSystemTrayIcon)
89
from PySide6.QtGui import QAction, QActionGroup, QIcon # pylint: disable=no-name-in-module
9-
from PySide6.QtCore import QFileSystemWatcher # pylint: disable=no-name-in-module
10+
from PySide6.QtCore import QFileSystemWatcher, QThread # pylint: disable=no-name-in-module
1011

1112
import nowplaying.apicache
1213
import nowplaying.config
@@ -19,6 +20,49 @@
1920
LASTANNOUNCED: dict[str,str|None] = {'artist': None, 'title': None}
2021

2122

23+
class DatabaseVacuumThread(QThread): # pylint: disable=too-few-public-methods
24+
''' Background thread for database vacuum operations '''
25+
26+
def __init__(self, requestswindow=None) -> None:
27+
super().__init__()
28+
self.requestswindow = requestswindow
29+
30+
def run(self) -> None:
31+
''' Perform database vacuum operations in background '''
32+
logging.debug("Starting database vacuum operations in background thread")
33+
34+
# Vacuum API cache database
35+
try:
36+
nowplaying.apicache.APIResponseCache.vacuum_database_file()
37+
logging.debug("API cache database vacuumed successfully")
38+
except (sqlite3.Error, OSError) as error:
39+
logging.error("Error vacuuming API cache: %s", error)
40+
except Exception as error: # pylint: disable=broad-exception-caught
41+
logging.exception("Unexpected error vacuuming API cache: %s", error)
42+
43+
# Vacuum metadata database
44+
try:
45+
metadb = nowplaying.db.MetadataDB()
46+
metadb.vacuum_database()
47+
logging.debug("Metadata database vacuumed successfully")
48+
except (sqlite3.Error, OSError) as error:
49+
logging.error("Error vacuuming metadata database: %s", error)
50+
except Exception as error: # pylint: disable=broad-exception-caught
51+
logging.exception("Unexpected error vacuuming metadata database: %s", error)
52+
53+
# Vacuum requests database (if available)
54+
try:
55+
if self.requestswindow:
56+
self.requestswindow.vacuum_database()
57+
logging.debug("Requests database vacuumed successfully")
58+
except (sqlite3.Error, OSError) as error:
59+
logging.error("Error vacuuming requests database: %s", error)
60+
except Exception as error: # pylint: disable=broad-exception-caught
61+
logging.exception("Unexpected error vacuuming requests database: %s", error)
62+
63+
logging.debug("Database vacuum operations completed in background thread")
64+
65+
2266
class Tray: # pylint: disable=too-many-instance-attributes
2367
''' System Tray object '''
2468

@@ -43,14 +87,17 @@ def __init__(self, beam: bool = False) -> None: #pylint: disable=too-many-state
4387
self.about_action.setEnabled(True)
4488
self.about_action.triggered.connect(self.aboutwindow.show)
4589

46-
# Vacuum databases on startup to reclaim space from previous session
47-
self._vacuum_databases_on_startup()
90+
# Vacuum databases will be started in background after requestswindow is created
4891

4992
self.subprocesses = nowplaying.subprocesses.SubprocessManager(self.config)
5093
try:
5194
self.settingswindow = nowplaying.settingsui.SettingsUI(tray=self, beam=beam)
95+
except (FileNotFoundError, ImportError) as error:
96+
logging.error("Failed to create settings window - missing UI files: %s", error)
97+
self._show_installation_error('settings UI files')
98+
return
5299
except Exception as error: # pylint: disable=broad-exception-caught
53-
logging.error("Failed to create settings window: %s", error)
100+
logging.exception("Unexpected error creating settings window: %s", error)
54101
self._show_installation_error('settings UI files')
55102
return
56103

@@ -99,6 +146,9 @@ def __init__(self, beam: bool = False) -> None: #pylint: disable=too-many-state
99146
self.requestswindow = None
100147
self._configure_twitchrequests()
101148

149+
# Start database vacuum operations in background thread now that requestswindow is created
150+
self._vacuum_databases_on_startup()
151+
102152
def _configure_beamstatus(self, beam: bool) -> None:
103153
self.config.cparser.setValue('control/beam', beam)
104154
# these will get filled in by their various subsystems as required
@@ -125,38 +175,16 @@ def _show_installation_error(ui_file: str) -> None:
125175
app.exit(1)
126176

127177
def _vacuum_databases_on_startup(self) -> None:
128-
''' Vacuum databases on startup to reclaim space from previous session '''
129-
logging.debug("Starting database vacuum operations on startup")
178+
''' Start database vacuum operations in background thread '''
179+
logging.debug("Starting database vacuum operations in background thread")
130180

131-
# Vacuum API cache database
132-
try:
133-
nowplaying.apicache.APIResponseCache.vacuum_database_file()
134-
logging.debug("API cache database vacuumed successfully")
135-
except Exception as error: # pylint: disable=broad-exception-caught
136-
logging.error("Error vacuuming API cache: %s", error)
137-
138-
# Vacuum metadata database
139-
try:
140-
metadb = nowplaying.db.MetadataDB()
141-
metadb.vacuum_database()
142-
logging.debug("Metadata database vacuumed successfully")
143-
except Exception as error: # pylint: disable=broad-exception-caught
144-
logging.error("Error vacuuming metadata database: %s", error)
145-
146-
# Vacuum requests database (will be created later, so check if available)
147-
try:
148-
if hasattr(self, 'requestswindow') and self.requestswindow:
149-
self.requestswindow.vacuum_database()
150-
logging.debug("Requests database vacuumed successfully")
151-
except Exception as error: # pylint: disable=broad-exception-caught
152-
logging.error("Error vacuuming requests database: %s", error)
153-
154-
logging.debug("Database vacuum operations completed")
181+
# Create and start vacuum thread
182+
self.vacuum_thread = DatabaseVacuumThread(requestswindow=self.requestswindow)
183+
self.vacuum_thread.start()
155184

156185
def _requestswindow(self) -> None:
157-
if self.config.cparser.value('settings/requests', type=bool):
158-
if self.requestswindow:
159-
self.requestswindow.raise_window()
186+
if self.config.cparser.value('settings/requests', type=bool) and self.requestswindow:
187+
self.requestswindow.raise_window()
160188

161189
def _configure_newold_menu(self) -> None:
162190
self.action_newestmode.setCheckable(True)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ testpaths = ["tests-qt", "tests"]
8989
markers = [
9090
"seratosettings: custom settings for serato ",
9191
"templatesettings: custom settings for templates",
92+
"performance: performance-sensitive tests that may be flaky in CI",
9293
]
9394
qt_api = "pyside6"
9495
#log_cli = true

0 commit comments

Comments
 (0)