Skip to content

Commit f46bd65

Browse files
authored
Merge pull request #1094 from Lyokone/release/10.0.0
release: prepare location 10.0.0, location_platform_interface 7.0.0, location_web 7.0.0
2 parents f45a75c + cbc7da5 commit f46bd65

34 files changed

Lines changed: 1363 additions & 42 deletions

.github/scripts/fake_geoclue2.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env python3
2+
"""A minimal fake org.freedesktop.GeoClue2 D-Bus service for CI.
3+
4+
Implements just enough of the real GeoClue2 D-Bus protocol (as used by
5+
packages/location/linux/location_plugin.cc) to drive the Linux e2e tests
6+
without a real GeoClue2 daemon or GPS hardware:
7+
8+
- org.freedesktop.GeoClue2.Manager.GetClient() -> client object path
9+
- org.freedesktop.GeoClue2.Client.{Start,Stop}()
10+
- org.freedesktop.DBus.Properties.{Get,Set,GetAll} on the client
11+
(DesktopId, RequestedAccuracyLevel)
12+
- org.freedesktop.GeoClue2.Client.LocationUpdated(old_path, new_path) signal
13+
- org.freedesktop.GeoClue2.Location.{Latitude,Longitude,Accuracy,Altitude,
14+
Speed,Heading} properties on the location object the signal points to
15+
16+
Must run on the SYSTEM bus (that's what real GeoClue2 uses, and what the
17+
plugin connects to) -- see the e2e workflow for the D-Bus policy/ownership
18+
setup this requires.
19+
20+
The mock coordinates are read from a JSON file (path given as argv[1]) that
21+
the CI script can rewrite at any time; this process polls it and emits a
22+
fresh LocationUpdated signal whenever the content changes, which is how the
23+
listen_location_test.dart "second, distinct fix mid-test" assertion works.
24+
"""
25+
26+
import json
27+
import sys
28+
import time
29+
30+
import dbus
31+
import dbus.service
32+
from dbus.mainloop.glib import DBusGMainLoop
33+
from gi.repository import GLib
34+
35+
BUS_NAME = "org.freedesktop.GeoClue2"
36+
MANAGER_PATH = "/org/freedesktop/GeoClue2/Manager"
37+
MANAGER_IFACE = "org.freedesktop.GeoClue2.Manager"
38+
CLIENT_IFACE = "org.freedesktop.GeoClue2.Client"
39+
LOCATION_IFACE = "org.freedesktop.GeoClue2.Location"
40+
CLIENT_PATH = "/org/freedesktop/GeoClue2/Client/0"
41+
42+
POLL_INTERVAL_SECONDS = 0.5
43+
44+
45+
class Location(dbus.service.Object):
46+
def __init__(self, bus, path, lat, lon):
47+
super().__init__(bus, path)
48+
self._props = {
49+
"Latitude": dbus.Double(lat),
50+
"Longitude": dbus.Double(lon),
51+
"Accuracy": dbus.Double(5.0),
52+
"Altitude": dbus.Double(0.0),
53+
"Speed": dbus.Double(0.0),
54+
"Heading": dbus.Double(0.0),
55+
}
56+
57+
@dbus.service.method(
58+
"org.freedesktop.DBus.Properties", in_signature="ss", out_signature="v"
59+
)
60+
def Get(self, interface, name):
61+
return self._props[name]
62+
63+
@dbus.service.method(
64+
"org.freedesktop.DBus.Properties", in_signature="s", out_signature="a{sv}"
65+
)
66+
def GetAll(self, interface):
67+
return dbus.Dictionary(self._props, signature="sv")
68+
69+
70+
class Client(dbus.service.Object):
71+
def __init__(self, bus, mock_file):
72+
super().__init__(bus, CLIENT_PATH)
73+
self._bus = bus
74+
self._mock_file = mock_file
75+
self._started = False
76+
self._location_index = 0
77+
self._current_location_path = None
78+
self._props = {
79+
"DesktopId": dbus.String(""),
80+
"RequestedAccuracyLevel": dbus.UInt32(8),
81+
}
82+
self._last_seen = None
83+
# dbus.service.Object instances must stay referenced to remain
84+
# exported on the bus -- without this, each Location object
85+
# created in _maybe_publish was eligible for garbage collection
86+
# the moment the function returned, since nothing held onto it.
87+
# Worked for the very first one seemingly by luck (GC hadn't run
88+
# yet by the time it was queried); broke once a second/third
89+
# Location object was created shortly after in the same run.
90+
#
91+
# NOT named `_locations`: dbus.service.Object already uses that
92+
# exact attribute internally (its list of (connection, path) pairs
93+
# this object is exported at, consulted by signal emission).
94+
# Reusing the name silently replaced that bookkeeping list with
95+
# this one, so emitting LocationUpdated crashed inside dbus-python
96+
# with "'Location' object is not subscriptable" -- confirmed via
97+
# CI logging every Client method call, which caught the traceback.
98+
self._published_locations = []
99+
100+
@dbus.service.method(CLIENT_IFACE)
101+
def Start(self):
102+
print("Client.Start() called", flush=True)
103+
self._started = True
104+
try:
105+
self._maybe_publish(force=True)
106+
except Exception:
107+
import traceback
108+
109+
traceback.print_exc()
110+
raise
111+
112+
@dbus.service.method(CLIENT_IFACE)
113+
def Stop(self):
114+
print("Client.Stop() called", flush=True)
115+
self._started = False
116+
117+
@dbus.service.method(
118+
"org.freedesktop.DBus.Properties", in_signature="ss", out_signature="v"
119+
)
120+
def Get(self, interface, name):
121+
print(f"Client.Get({interface!r}, {name!r}) called", flush=True)
122+
return self._props[name]
123+
124+
@dbus.service.method(
125+
"org.freedesktop.DBus.Properties", in_signature="ssv"
126+
)
127+
def Set(self, interface, name, value):
128+
print(f"Client.Set({interface!r}, {name!r}, {value!r}) called", flush=True)
129+
self._props[name] = value
130+
131+
@dbus.service.method(
132+
"org.freedesktop.DBus.Properties", in_signature="s", out_signature="a{sv}"
133+
)
134+
def GetAll(self, interface):
135+
return dbus.Dictionary(self._props, signature="sv")
136+
137+
@dbus.service.signal(CLIENT_IFACE, signature="oo")
138+
def LocationUpdated(self, old_path, new_path):
139+
pass
140+
141+
def poll(self):
142+
self._maybe_publish(force=False)
143+
return True # keep the GLib timeout running
144+
145+
def _maybe_publish(self, force):
146+
try:
147+
with open(self._mock_file) as f:
148+
mock = json.load(f)
149+
except (FileNotFoundError, json.JSONDecodeError):
150+
return
151+
152+
key = (mock.get("latitude"), mock.get("longitude"))
153+
if not self._started or (not force and key == self._last_seen):
154+
return
155+
self._last_seen = key
156+
157+
self._location_index += 1
158+
new_path = f"{CLIENT_PATH}/Location/{self._location_index}"
159+
self._published_locations.append(Location(self._bus, new_path, key[0], key[1]))
160+
161+
old_path = self._current_location_path or "/"
162+
self._current_location_path = new_path
163+
self.LocationUpdated(old_path, new_path)
164+
165+
166+
class Manager(dbus.service.Object):
167+
def __init__(self, bus, client):
168+
super().__init__(bus, MANAGER_PATH)
169+
self._client = client
170+
171+
@dbus.service.method(MANAGER_IFACE, out_signature="o")
172+
def GetClient(self):
173+
print("Manager.GetClient() called", flush=True)
174+
return CLIENT_PATH
175+
176+
177+
def main():
178+
if len(sys.argv) != 2:
179+
print("usage: fake_geoclue2.py <mock-location-json-file>", file=sys.stderr)
180+
sys.exit(1)
181+
mock_file = sys.argv[1]
182+
183+
DBusGMainLoop(set_as_default=True)
184+
bus = dbus.SystemBus()
185+
bus_name = dbus.service.BusName(BUS_NAME, bus)
186+
187+
client = Client(bus, mock_file)
188+
Manager(bus, client)
189+
190+
GLib.timeout_add(int(POLL_INTERVAL_SECONDS * 1000), client.poll)
191+
192+
print("fake GeoClue2 ready", flush=True)
193+
GLib.MainLoop().run()
194+
195+
196+
if __name__ == "__main__":
197+
main()

0 commit comments

Comments
 (0)