blob: d9675eafff2cc0a5bbd8177f98bce32c72740d83 [file] [edit]
#!/usr/bin/env python3
#
# Copyright (C) 2026 Igalia S.L.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.
"""End-to-end driver for WTF/glib/TimeZoneGLib.cpp: spawns a private bus,
owns timedate1 on it, and drives WTF_TimeZoneGLib_External.* across a real
process boundary. See TEST_FILTER."""
import argparse
import os
import shutil
import subprocess
import sys
import time
import gi
gi.require_version('Gio', '2.0')
from gi.repository import Gio, GLib # noqa: E402
TIMEDATE1_NAME = 'org.freedesktop.timedate1'
TIMEDATE1_PATH = '/org/freedesktop/timedate1'
PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties'
TEST_FILTER = 'WTF_TimeZoneGLib_External.*'
EMIT_INTERVAL_SECONDS = 0.2
# Keep below the gtest-side timeout (30s) so the test's own deadline is the
# one reported on failure.
OVERALL_TIMEOUT_SECONDS = 60
def find_testwtf(explicit_path):
if explicit_path:
if not os.path.isfile(explicit_path):
sys.exit(f'TestWTF not found at {explicit_path}')
return explicit_path
script_dir = os.path.dirname(os.path.abspath(__file__))
webkit_root = os.path.abspath(os.path.join(script_dir, '..', '..'))
candidates = [
os.path.join(webkit_root, 'WebKitBuild', 'GTK', 'Release', 'bin', 'TestWebKitAPI', 'TestWTF'),
os.path.join(webkit_root, 'WebKitBuild', 'GTK', 'Debug', 'bin', 'TestWebKitAPI', 'TestWTF'),
os.path.join(webkit_root, 'WebKitBuild', 'WPE', 'Release', 'bin', 'TestWebKitAPI', 'TestWTF'),
os.path.join(webkit_root, 'WebKitBuild', 'WPE', 'Debug', 'bin', 'TestWebKitAPI', 'TestWTF'),
]
for c in candidates:
if os.path.isfile(c):
return c
sys.exit('Could not locate TestWTF; pass --testwtf <path> or build the GTK/WPE port first.\n'
'Searched:\n ' + '\n '.join(candidates))
def spawn_private_bus():
if not shutil.which('dbus-daemon'):
sys.exit('dbus-daemon not found in PATH; install dbus or run inside the wkdev-sdk container.')
daemon = subprocess.Popen(
['dbus-daemon', '--session', '--nofork', '--print-address'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
line = daemon.stdout.readline()
if not line:
stderr = daemon.stderr.read().decode(errors='replace') if daemon.stderr else ''
daemon.kill()
sys.exit(f'dbus-daemon failed to print bus address. stderr:\n{stderr}')
return daemon, line.decode().strip()
def own_timedate1(address):
flags = Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT | Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION
connection = Gio.DBusConnection.new_for_address_sync(address, flags, None, None)
reply = connection.call_sync(
'org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus',
'RequestName',
GLib.Variant('(su)', (TIMEDATE1_NAME, 0)),
GLib.VariantType('(u)'),
Gio.DBusCallFlags.NONE,
-1,
None)
code = reply.unpack()[0]
# DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER == 1; any other value on a fresh
# private bus means our setup is wrong, not a flake worth retrying.
if code != 1:
sys.exit(f'RequestName({TIMEDATE1_NAME}) returned {code}, expected 1 (PRIMARY_OWNER)')
return connection
def emit_timezone_changed(connection, timezone):
# The '@' format placeholder is C-only; PyGObject auto-wraps a Python
# dict/list into a{sv}/as as long as dict values are already GLib.Variant.
params = GLib.Variant(
'(sa{sv}as)',
(TIMEDATE1_NAME, {'Timezone': GLib.Variant('s', timezone)}, []))
connection.emit_signal(None, TIMEDATE1_PATH, PROPERTIES_IFACE, 'PropertiesChanged', params)
connection.flush_sync(None)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--testwtf', help='Path to the TestWTF binary')
parser.add_argument('--timezone', default='Europe/Madrid',
help='Timezone value to send in PropertiesChanged (default: Europe/Madrid)')
parser.add_argument('--verbose', action='store_true')
args = parser.parse_args()
testwtf = find_testwtf(args.testwtf)
if args.verbose:
print(f'[driver] TestWTF: {testwtf}')
daemon, bus_address = spawn_private_bus()
if args.verbose:
print(f'[driver] bus address: {bus_address}')
try:
owner_connection = own_timedate1(bus_address)
if args.verbose:
print(f'[driver] owning {TIMEDATE1_NAME}')
env = os.environ.copy()
env['WEBKIT_TIMEDATE1_TEST_BUS'] = bus_address
child = subprocess.Popen(
[testwtf, f'--gtest_filter={TEST_FILTER}'],
env=env)
deadline = time.monotonic() + OVERALL_TIMEOUT_SECONDS
while True:
rc = child.poll()
if rc is not None:
if args.verbose:
print(f'[driver] TestWTF exited rc={rc}')
return rc
if time.monotonic() > deadline:
print(f'[driver] timed out after {OVERALL_TIMEOUT_SECONDS}s; killing TestWTF', file=sys.stderr)
child.kill()
child.wait()
return 1
# The child's subscription becomes live only after g_bus_get +
# signal_subscribe finish asynchronously; emits before then are
# silently dropped. Re-emit on every tick until the child observes
# the bump and exits PASS.
emit_timezone_changed(owner_connection, args.timezone)
time.sleep(EMIT_INTERVAL_SECONDS)
finally:
daemon.terminate()
try:
daemon.wait(timeout=5)
except subprocess.TimeoutExpired:
daemon.kill()
if __name__ == '__main__':
sys.exit(main())