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