1
0
Fork 0
gnome-settings-daemon/tests/gsdtestcase.py
Daniel Baumann 18b565039d
Adding upstream version 48.1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 20:20:27 +02:00

364 lines
13 KiB
Python

'''GNOME settings daemon test base class'''
__author__ = 'Martin Pitt <martin.pitt@ubuntu.com>'
__copyright__ = '(C) 2013 Canonical Ltd.'
__license__ = 'GPL v2 or later'
import subprocess
import time
import os
import os.path
import tempfile
import fcntl
import shutil
import sys
from glob import glob
import signal
from output_checker import OutputChecker
from gi.repository import GLib
try:
import dbusmock
except ImportError:
sys.stderr.write('You need python-dbusmock (http://pypi.python.org/pypi/python-dbusmock) for this test suite.\n')
sys.exit(77)
from dbusmock import DBusTestCase
try:
from gi.repository import Gio
except ImportError:
sys.stderr.write('You need pygobject and the Gio GIR for this test suite.\n')
sys.exit(77)
if subprocess.call(['which', 'gnome-session'], stdout=subprocess.DEVNULL) != 0:
sys.stderr.write('You need gnome-session for this test suite.\n')
sys.exit(77)
top_builddir = os.environ.get('TOP_BUILDDIR',
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def set_nonblock(fd):
'''Set a file object to non-blocking'''
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
class GSDTestCase(DBusTestCase):
'''Base class for settings daemon tests
This redirects the XDG directories to temporary directories, and runs local
session and system D-BUSes with a minimal GNOME session and a mock
notification daemon. It also provides common functionality for plugin
tests.
'''
@classmethod
def setUpClass(klass):
os.environ['GIO_USE_VFS'] = 'local'
os.environ['GVFS_DISABLE_FUSE'] = '1'
# we do some string checks, disable translations
os.environ['LC_MESSAGES'] = 'C'
klass.workdir = tempfile.mkdtemp(prefix='gsd-plugin-test')
klass.addClassCleanup(shutil.rmtree, klass.workdir)
# X11 display tracking
klass.display_name_fifo = None
klass.x11_display = None
# Prevent applications from accessing an outside session manager
os.environ['SESSION_MANAGER'] = ''
os.environ['XDG_SESSION_TYPE'] = 'wayland'
os.environ['G_MESSAGES_DEBUG'] = 'all'
# tell dconf and friends to use our config/runtime directories
os.environ['XDG_CONFIG_HOME'] = os.path.join(klass.workdir, 'config')
os.environ['XDG_DATA_HOME'] = os.path.join(klass.workdir, 'data')
os.environ['XDG_RUNTIME_DIR'] = os.path.join(klass.workdir, 'runtime')
# Copy gschema file into XDG_DATA_HOME
gschema_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'glib-2.0', 'schemas')
os.makedirs(gschema_dir)
shutil.copy(os.path.join(top_builddir, 'data', 'gschemas.compiled'), gschema_dir)
# work around https://bugzilla.gnome.org/show_bug.cgi?id=689136
os.makedirs(os.path.join(os.environ['XDG_CONFIG_HOME'], 'dconf'))
os.makedirs(os.environ['XDG_RUNTIME_DIR'], mode=0o700)
# Starts dbus busses
DBusTestCase.setUpClass()
klass.start_system_bus()
klass.start_session_bus()
# Make dconf discoverable (requires newer dbusmock API, is not needed otherwise)
if hasattr(klass, 'enable_service'):
klass.enable_service('ca.desrt.dconf')
klass.system_bus_con = klass.get_dbus(True)
klass.session_bus_con = klass.get_dbus(False)
klass.addClassCleanup(klass.system_bus_con.close)
klass.addClassCleanup(klass.session_bus_con.close)
# we never want to cause notifications on the actual GUI
klass.p_notify_log = OutputChecker()
klass.p_notify = klass.spawn_server_template(
'notification_daemon', {}, stdout=klass.p_notify_log.fd)[0]
klass.p_notify_log.writer_attached()
klass.addClassCleanup(klass.stop_process, klass.p_notify)
klass.configure_session()
klass.start_monitor()
klass.addClassCleanup(klass.stop_monitor)
# Reset between tests in tearDown
klass.settings_session = Gio.Settings(schema_id='org.gnome.desktop.session')
# Make sure we get a backtrace when meson kills after a timeout
def r(*args):
raise KeyboardInterrupt()
signal.signal(signal.SIGTERM, r)
@classmethod
def tearDownClass(klass):
#if hasattr(klass, 'disable_service'):
# klass.disable_service('ca.desrt.dconf')
signal.signal(signal.SIGTERM, signal.SIG_IGN)
DBusTestCase.tearDownClass()
signal.signal(signal.SIGTERM, signal.SIG_DFL)
def setUp(self):
self.daemon_death_expected = False
def tearDown(self):
# we check this at the end so that the other cleanup always happens
daemon_running = self.daemon.poll() == None
self.assertTrue(daemon_running or self.daemon_death_expected, 'daemon died during the test')
self.reset_settings(self.settings_session)
def run(self, result=None):
'''Show log files on failed tests
If the environment variable $SHELL_ON_FAIL is set, this runs bash in
the work directory; exit the shell to continue the tests. Otherwise it
shows all log files.
'''
if result:
orig_err_fail = len(result.errors) + len(result.failures)
super(GSDTestCase, self).run(result)
if result and len(result.errors) + len(result.failures) > orig_err_fail:
if 'SHELL_ON_FAIL' in os.environ:
subprocess.call(['bash', '-i'], cwd=self.workdir)
else:
for log_file in glob(os.path.join(self.workdir, '*.log')):
with open(log_file) as f:
print('\n----- %s -----\n%s\n------\n'
% (log_file, f.read()))
@classmethod
def configure_session(klass):
'''Configure minimal GNOME session'''
# create dummy session type and component
d = os.path.join(klass.workdir, 'config', 'gnome-session', 'sessions')
if not os.path.isdir(d):
os.makedirs(d)
shutil.copy(os.path.join(os.path.dirname(__file__), 'dummy.session'), d)
d = os.path.join(klass.workdir, 'data', 'applications')
if not os.path.isdir(d):
os.makedirs(d)
shutil.copy(os.path.join(os.path.dirname(__file__), 'dummyapp.desktop'), d)
def start_session(self):
self.session_log = OutputChecker()
self.session = subprocess.Popen(['gnome-session', '-f',
'-a', os.path.join(self.workdir, 'autostart'),
'--session=dummy', '--debug'],
stdout=self.session_log.fd,
stderr=subprocess.STDOUT)
self.session_log.writer_attached()
# wait until the daemon is on the bus
self.wait_for_bus_object('org.gnome.SessionManager',
'/org/gnome/SessionManager',
timeout=100)
self.session_log.check_line(b'fill: *** Done adding required components')
def stop_session(self):
'''Stop GNOME session'''
assert self.session
self.stop_process(self.session)
# dummyapp.desktop survives the session. This keeps the FD open in the
# CI environment when gnome-session fails to redirect the child output
# to journald.
# Though, gnome-session should probably kill the child anyway.
#self.session_log.assert_closed()
self.session_log.force_close()
@classmethod
def start_monitor(klass):
'''Start dbus-monitor'''
# You can rename the log file to *.log if you want to see it on test
# case failures
klass.monitor_log = open(os.path.join(klass.workdir, 'dbus-monitor.out'), 'wb', buffering=0)
klass.monitor = subprocess.Popen(['dbus-monitor', '--monitor'],
stdout=klass.monitor_log,
stderr=subprocess.STDOUT)
@classmethod
def stop_monitor(klass):
'''Stop dbus-monitor'''
assert klass.monitor
klass.stop_process(klass.monitor)
klass.monitor_log.flush()
klass.monitor_log.close()
def start_logind(self, parameters=None):
'''start mock logind'''
if parameters is None:
parameters = {}
self.logind_log = OutputChecker()
self.logind, self.logind_obj = self.spawn_server_template('logind',
parameters,
stdout=self.logind_log.fd)
self.logind_log.writer_attached()
# Monkey patch SuspendThenHibernate functions in for dbusmock <= 0.17.2
# This should be removed once we can depend on dbusmock 0.17.3
self.logind_obj.AddMethod('org.freedesktop.login1.Manager', 'SuspendThenHibernate', 'b', '', '')
self.logind_obj.AddMethod('org.freedesktop.login1.Manager', 'CanSuspendThenHibernate', '', 's', 'ret = "%s"' % parameters.get('CanSuspendThenHibernate', 'yes'))
self.logind_obj.AddMethod('org.freedesktop.login1.Session', 'SetBrightness', 'ssu', '', '')
def stop_logind(self):
'''stop mock logind'''
self.stop_process(self.logind)
self.logind_log.assert_closed()
@classmethod
def start_mutter(klass, needs_x11=False):
''' start mutter '''
if needs_x11:
tempdir = tempfile.mkdtemp()
display_name_fifo = os.path.join(tempdir, 'display-name')
os.mkfifo(display_name_fifo)
extra_mutter_args = [
'--',
os.path.join(project_root, 'tests', 'get-display-name.sh'),
display_name_fifo,
]
else:
extra_mutter_args = []
os.environ['MUTTER_DEBUG_RESET_IDLETIME']='1'
klass.mutter = subprocess.Popen(['mutter', '--headless',
'--virtual-monitor', '800x600'] +
extra_mutter_args)
if needs_x11:
with open(display_name_fifo) as f:
klass.display_name_fifo = display_name_fifo
klass.x11_display = f.readline().strip()
klass.xauth = f.readline().strip()
print("Using X11 display %s" % (klass.x11_display))
klass.wait_for_bus_object('org.gnome.Mutter.IdleMonitor',
'/org/gnome/Mutter/IdleMonitor/Core',
timeout=100)
@classmethod
def stop_mutter(klass):
'''stop mutter'''
assert klass.monitor
if klass.display_name_fifo:
with open(klass.display_name_fifo, 'w') as f:
f.write('\n')
os.remove(klass.display_name_fifo)
os.rmdir(os.path.dirname(klass.display_name_fifo))
klass.stop_process(klass.mutter, timeout=2)
def start_plugin(self, env):
self.plugin_death_expected = False
# We need to redirect stdout to grab the debug messages.
# stderr is not needed by the testing infrastructure but is useful to
# see warnings and errors.
self.plugin_log = OutputChecker()
if self.__class__.x11_display:
klass = self.__class__
extra_env = {
'GNOME_SETUP_DISPLAY': klass.x11_display,
'XAUTHORITY': klass.xauth,
}
else:
extra_env = {}
self.daemon = subprocess.Popen(
[os.path.join(top_builddir, 'plugins', self.gsd_plugin, 'gsd-' + self.gsd_plugin), '--verbose'],
stdout=self.plugin_log.fd,
stderr=subprocess.STDOUT,
env=env | extra_env)
self.plugin_log.writer_attached()
bus = self.get_dbus(False)
timeout = 100
while timeout > 0:
if bus.name_has_owner('org.gnome.SettingsDaemon.' + self.gsd_plugin_case):
break
timeout -= 1
time.sleep(0.1)
if timeout <= 0:
assert timeout > 0, 'timed out waiting for plugin startup: %s' % (self.gsd_plugin_case)
def stop_plugin(self):
daemon_running = self.daemon.poll() == None
if daemon_running:
self.stop_process(self.daemon)
self.plugin_log.assert_closed()
def reset_settings(self, schema):
# reset all changed gsettings, so that tests are independent from each
# other
for k in schema.list_keys():
schema.reset(k)
Gio.Settings.sync()
@classmethod
def stop_process(cls, proc, timeout=1):
proc.terminate()
try:
proc.wait(timeout)
except:
print("Killing %d (%s) after timeout of %f seconds" % (proc.pid, proc.args[0], timeout))
proc.kill()
proc.wait()
@classmethod
def reset_idle_timer(klass):
'''trigger activity to reset idle timer'''
obj_mutter_idlemonitor = klass.session_bus_con.get_object(
'org.gnome.Mutter.IdleMonitor', '/org/gnome/Mutter/IdleMonitor/Core')
obj_mutter_idlemonitor.ResetIdletime(dbus_interface='org.gnome.Mutter.IdleMonitor')