Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
2 : : *
3 : : * Copyright © 2018 Endless Mobile, Inc.
4 : : *
5 : : * This library is free software; you can redistribute it and/or
6 : : * modify it under the terms of the GNU Lesser General Public
7 : : * License as published by the Free Software Foundation; either
8 : : * version 2.1 of the License, or (at your option) any later version.
9 : : *
10 : : * This library is distributed in the hope that it will be useful,
11 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 : : * Lesser General Public License for more details.
14 : : *
15 : : * You should have received a copy of the GNU Lesser General Public
16 : : * License along with this library; if not, write to the Free Software
17 : : * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 : : *
19 : : * Authors:
20 : : * - Philip Withnall <withnall@endlessm.com>
21 : : */
22 : :
23 : : #include "config.h"
24 : :
25 : : #include <glib.h>
26 : : #include <glib-object.h>
27 : : #include <glib/gi18n-lib.h>
28 : : #include <gio/gio.h>
29 : : #include <libmogwai-schedule/clock.h>
30 : : #include <libmogwai-schedule/tests/clock-dummy.h>
31 : :
32 : :
33 : : static void mws_clock_dummy_clock_init (MwsClockInterface *iface);
34 : :
35 : : static void mws_clock_dummy_finalize (GObject *obj);
36 : :
37 : : static GDateTime *mws_clock_dummy_get_now_local (MwsClock *clock);
38 : : static guint mws_clock_dummy_add_alarm (MwsClock *clock,
39 : : GDateTime *alarm_time,
40 : : GSourceFunc alarm_func,
41 : : gpointer user_data,
42 : : GDestroyNotify destroy_func);
43 : : static void mws_clock_dummy_remove_alarm (MwsClock *clock,
44 : : guint id);
45 : :
46 : : typedef struct
47 : : {
48 : : GDateTime *alarm_time; /* (owned) */
49 : : GSourceFunc alarm_func; /* (nullable) */
50 : : gpointer user_data; /* (nullable) */
51 : : GDestroyNotify destroy_func; /* (nullable) */
52 : : } AlarmData;
53 : :
54 : : static void
55 : 91 : alarm_data_free (AlarmData *data)
56 : : {
57 : : /* FIXME: In order to be able to steal elements from the alarms array, we need
58 : : * to gracefully ignore NULL entries. */
59 [ + + ]: 91 : if (data == NULL)
60 : 11 : return;
61 : :
62 [ + - ]: 80 : g_clear_pointer (&data->alarm_time, g_date_time_unref);
63 [ - + - - ]: 80 : if (data->destroy_func != NULL && data->user_data != NULL)
64 : 0 : data->destroy_func (data->user_data);
65 : 80 : g_free (data);
66 : : }
67 : :
68 [ + + ]: 246 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (AlarmData, alarm_data_free)
69 : :
70 : : static gint
71 : 166 : alarm_data_compare (const AlarmData *a,
72 : : const AlarmData *b)
73 : : {
74 [ + + ]: 166 : if (!g_date_time_equal (a->alarm_time, b->alarm_time))
75 : 21 : return g_date_time_compare (a->alarm_time, b->alarm_time);
76 : :
77 : : /* Tie break using the pointer values, so the sort is stable. */
78 [ - + ]: 145 : if (a == b)
79 : 0 : return 0;
80 [ + + ]: 145 : else if (a < b)
81 : 98 : return -1;
82 : : else
83 : 47 : return 1;
84 : : }
85 : :
86 : : static void
87 : 11 : alarm_data_invoke (const AlarmData *data)
88 : : {
89 [ + - ]: 11 : if (data->alarm_func != NULL)
90 : 11 : data->alarm_func (data->user_data);
91 : 11 : }
92 : :
93 : : /**
94 : : * MwsClockDummy:
95 : : *
96 : : * An implementation of the #MwsClock interface which is not tied to any
97 : : * real-world clock. Its time transitions are entirely programmatically driven
98 : : * by calling mws_clock_dummy_set_time() and mws_clock_dummy_set_time_zone(),
99 : : * which will typically be done by a test harness. Its internal clock will not
100 : : * progress automatically at all.
101 : : *
102 : : * This is intended for testing code which uses the #MwsClock interface.
103 : : *
104 : : * The time in the dummy clock may move forwards or backwards and its timezone
105 : : * can be changed (which results in the #MwsClock::offset-changed signal being
106 : : * emitted).
107 : : *
108 : : * The clock starts at 2000-01-01T00:00:00Z.
109 : : *
110 : : * Since: 0.1.0
111 : : */
112 : : struct _MwsClockDummy
113 : : {
114 : : GObject parent;
115 : :
116 : : /* Main context from construction time. */
117 : : GMainContext *context; /* (owned) */
118 : : GSource *add_alarm_source; /* (owned) (nullable) */
119 : :
120 : : /* The current time. This only changes when the test harness explicitly
121 : : * advances it. It’s always maintained in @tz. */
122 : : GDateTime *now; /* (owned) */
123 : : GTimeZone *tz; /* (owned) */
124 : :
125 : : /* Array of pending alarms, sorted by increasing date/time. */
126 : : GPtrArray *alarms; /* (owned) (element-type AlarmData) */
127 : : };
128 : :
129 [ + + + - : 551 : G_DEFINE_TYPE_WITH_CODE (MwsClockDummy, mws_clock_dummy, G_TYPE_OBJECT,
+ + ]
130 : : G_IMPLEMENT_INTERFACE (MWS_TYPE_CLOCK,
131 : : mws_clock_dummy_clock_init))
132 : : static void
133 : 2 : mws_clock_dummy_class_init (MwsClockDummyClass *klass)
134 : : {
135 : 2 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
136 : :
137 : 2 : object_class->finalize = mws_clock_dummy_finalize;
138 : 2 : }
139 : :
140 : : static void
141 : 2 : mws_clock_dummy_clock_init (MwsClockInterface *iface)
142 : : {
143 : 2 : iface->get_now_local = mws_clock_dummy_get_now_local;
144 : 2 : iface->add_alarm = mws_clock_dummy_add_alarm;
145 : 2 : iface->remove_alarm = mws_clock_dummy_remove_alarm;
146 : 2 : }
147 : :
148 : : static void
149 : 48 : mws_clock_dummy_init (MwsClockDummy *self)
150 : : {
151 : : /* Start at the year 2000 UTC, so things are reproducible. We can’t start at
152 : : * the year 1, as then the initial timezone transition might not be possible. */
153 : 48 : self->tz = g_time_zone_new_utc ();
154 : 48 : self->now = g_date_time_new (self->tz, 2000, 1, 1, 0, 0, 0);
155 [ - + ]: 48 : g_assert (self->now != NULL);
156 : :
157 : 48 : self->context = g_main_context_ref_thread_default ();
158 : :
159 : 48 : self->alarms = g_ptr_array_new_with_free_func ((GDestroyNotify) alarm_data_free);
160 : 48 : }
161 : :
162 : : static void
163 : 47 : mws_clock_dummy_finalize (GObject *obj)
164 : : {
165 : 47 : MwsClockDummy *self = MWS_CLOCK_DUMMY (obj);
166 : :
167 [ + - ]: 47 : g_clear_pointer (&self->alarms, g_ptr_array_unref);
168 [ + - ]: 47 : g_clear_pointer (&self->tz, g_time_zone_unref);
169 [ + - ]: 47 : g_clear_pointer (&self->now, g_date_time_unref);
170 : :
171 [ + + ]: 47 : if (self->add_alarm_source != NULL)
172 : : {
173 : 17 : g_source_destroy (self->add_alarm_source);
174 [ + - ]: 17 : g_clear_pointer (&self->add_alarm_source, g_source_unref);
175 : : }
176 [ + - ]: 47 : g_clear_pointer (&self->context, g_main_context_unref);
177 : :
178 : 47 : G_OBJECT_CLASS (mws_clock_dummy_parent_class)->finalize (obj);
179 : 47 : }
180 : :
181 : : static GDateTime *
182 : 168 : mws_clock_dummy_get_now_local (MwsClock *clock)
183 : : {
184 : 168 : MwsClockDummy *self = MWS_CLOCK_DUMMY (clock);
185 : 168 : return g_date_time_ref (self->now);
186 : : }
187 : :
188 : : static gint
189 : 166 : alarm_data_compare_cb (gconstpointer a,
190 : : gconstpointer b)
191 : : {
192 : 166 : return alarm_data_compare (*((const AlarmData **) a),
193 : : *((const AlarmData **) b));
194 : : }
195 : :
196 : : static gboolean
197 : 0 : add_alarm_cb (gpointer user_data)
198 : : {
199 : 0 : MwsClockDummy *self = MWS_CLOCK_DUMMY (user_data);
200 : :
201 : 0 : g_autoptr(GDateTime) now = mws_clock_dummy_get_now_local (MWS_CLOCK (self));
202 : 0 : mws_clock_dummy_set_time (self, now);
203 [ # # ]: 0 : g_clear_pointer (&self->add_alarm_source, g_source_unref);
204 : :
205 : 0 : return G_SOURCE_REMOVE;
206 : : }
207 : :
208 : : static guint
209 : 80 : mws_clock_dummy_add_alarm (MwsClock *clock,
210 : : GDateTime *alarm_time,
211 : : GSourceFunc alarm_func,
212 : : gpointer user_data,
213 : : GDestroyNotify destroy_func)
214 : : {
215 : 80 : MwsClockDummy *self = MWS_CLOCK_DUMMY (clock);
216 : :
217 : 80 : g_autoptr(AlarmData) data = g_new0 (AlarmData, 1);
218 : 80 : data->alarm_time = g_date_time_ref (alarm_time);
219 : 80 : data->alarm_func = alarm_func;
220 : 80 : data->user_data = user_data;
221 : 80 : data->destroy_func = destroy_func;
222 : :
223 : 80 : guint id = GPOINTER_TO_UINT (data);
224 : 80 : g_ptr_array_add (self->alarms, g_steal_pointer (&data));
225 : 80 : g_ptr_array_sort (self->alarms, alarm_data_compare_cb);
226 : :
227 : : /* Ensure the alarm is triggered if it’s in the past, but don’t trigger it
228 : : * immediately to avoid re-entrancy issues. */
229 [ + + ]: 80 : if (self->add_alarm_source == NULL)
230 : : {
231 : 17 : self->add_alarm_source = g_idle_source_new ();
232 : 17 : g_source_set_callback (self->add_alarm_source, add_alarm_cb, self, NULL);
233 : 17 : g_source_attach (self->add_alarm_source, self->context);
234 : : }
235 : :
236 : 80 : return id;
237 : : }
238 : :
239 : : static void
240 : 0 : mws_clock_dummy_remove_alarm (MwsClock *clock,
241 : : guint id)
242 : : {
243 : 0 : MwsClockDummy *self = MWS_CLOCK_DUMMY (clock);
244 : 0 : GSource *source = GUINT_TO_POINTER (id);
245 : :
246 : : /* The source will be destroyed by the free function set up on the array. */
247 [ # # ]: 0 : g_return_if_fail (g_ptr_array_remove_fast (self->alarms, source));
248 : : }
249 : :
250 : : /**
251 : : * mws_clock_dummy_new:
252 : : *
253 : : * Create a #MwsClockDummy object which gets wall clock time from the dummy
254 : : * clock.
255 : : *
256 : : * Returns: (transfer full): a new #MwsClockDummy
257 : : * Since: 0.1.0
258 : : */
259 : : MwsClockDummy *
260 : 48 : mws_clock_dummy_new (void)
261 : : {
262 : 48 : return g_object_new (MWS_TYPE_CLOCK_DUMMY, NULL);
263 : : }
264 : :
265 : : /**
266 : : * mws_clock_dummy_set_time:
267 : : * @self: a #MwsClockDummy
268 : : * @now: new time for the clock
269 : : *
270 : : * Set the clock to consider ‘now’ to be @now. This will result in any alarms
271 : : * whose trigger times are earlier than or equal to @now being triggered. Any
272 : : * new alarms added from alarm callbacks are also invoked if their trigger times
273 : : * meet the same criterion.
274 : : *
275 : : * The time zone for @now is not used to change the time zone for the clock;
276 : : * @now is converted to the current time zone in use by the clock. To change the
277 : : * clock’s time zone, call mws_clock_dummy_set_time_zone().
278 : : *
279 : : * Since: 0.1.0
280 : : */
281 : : void
282 : 51 : mws_clock_dummy_set_time (MwsClockDummy *self,
283 : : GDateTime *now)
284 : : {
285 [ - + ]: 51 : g_return_if_fail (MWS_IS_CLOCK_DUMMY (self));
286 [ - + ]: 51 : g_return_if_fail (now != NULL);
287 : :
288 : 51 : g_autofree gchar *now_str = g_date_time_format (now, "%FT%T%:::z");
289 : 51 : g_debug ("%s: Setting time to %s; %u alarms to check",
290 : : G_STRFUNC, now_str, self->alarms->len);
291 : :
292 : : /* Trigger all the alarms before the new @now (inclusive).
293 : : * Since other methods on #MwsClockDummy may be called by the user function
294 : : * in alarm_data_invoke(), which may modify the @self->alarms array, we need
295 : : * to be careful to re-read the array on each loop iteration, and to remove
296 : : * invoked alarms individually. */
297 [ + + ]: 62 : while (self->alarms->len > 0)
298 : : {
299 [ + + ]: 43 : g_autoptr(AlarmData) owned_data = NULL;
300 : 43 : const AlarmData *data = g_ptr_array_index (self->alarms, 0);
301 : :
302 [ + + ]: 86 : g_autofree gchar *alarm_time_str = g_date_time_format (data->alarm_time, "%FT%T%:::z");
303 : 43 : g_debug ("%s: Comparing alarm %s to now %s",
304 : : G_STRFUNC, alarm_time_str, now_str);
305 : :
306 [ + + ]: 43 : if (g_date_time_compare (data->alarm_time, now) > 0)
307 : 32 : break;
308 : :
309 : : /* Steal the alarm from the array.
310 : : * FIXME: Use g_ptr_array_steal_index() when we can depend on a suitable GLib version. */
311 : 11 : owned_data = g_steal_pointer (&self->alarms->pdata[0]);
312 : 11 : g_ptr_array_remove_index (self->alarms, 0);
313 : :
314 : : /* Set the current time to what the alarm expects. */
315 : 11 : g_date_time_unref (self->now);
316 : 11 : self->now = g_date_time_to_timezone (owned_data->alarm_time, self->tz);
317 : :
318 : 11 : alarm_data_invoke (owned_data);
319 : : }
320 : :
321 : : /* Convert the final time to our local timezone. */
322 : 51 : g_date_time_unref (self->now);
323 : 51 : self->now = g_date_time_to_timezone (now, self->tz);
324 : : }
325 : :
326 : : /**
327 : : * mws_clock_dummy_set_time_zone:
328 : : * @self: a #MwsClockDummy
329 : : * @tz: new time zone for the clock
330 : : *
331 : : * Set the clock’s time zone to @tz, and convert its current ‘now’ time to be
332 : : * the same instant as before, but in @tz.
333 : : *
334 : : * If the time zone has changed, this results in the #MwsClock::offset-changed
335 : : * signal being emitted.
336 : : *
337 : : * Since: 0.1.0
338 : : */
339 : : void
340 : 51 : mws_clock_dummy_set_time_zone (MwsClockDummy *self,
341 : : GTimeZone *tz)
342 : : {
343 [ - + ]: 81 : g_return_if_fail (MWS_IS_CLOCK_DUMMY (self));
344 [ - + ]: 51 : g_return_if_fail (tz != NULL);
345 : :
346 : : /* FIXME: Do we need a g_time_zone_equal() function to compare these? This
347 : : * is a terrible way of comparing them. */
348 [ + + ]: 51 : if (tz == self->tz)
349 : 30 : return;
350 : :
351 : 21 : g_debug ("%s: Setting time zone to %s (%p)",
352 : : G_STRFUNC, g_time_zone_get_abbreviation (tz, 0), tz);
353 : :
354 [ + - ]: 42 : g_autoptr(GDateTime) now_new_tz = g_date_time_to_timezone (self->now, tz);
355 [ - + ]: 21 : g_return_if_fail (now_new_tz != NULL);
356 : 21 : g_date_time_unref (self->now);
357 : 21 : self->now = g_steal_pointer (&now_new_tz);
358 : :
359 : 21 : g_time_zone_unref (self->tz);
360 : 21 : self->tz = g_time_zone_ref (tz);
361 : :
362 : 21 : g_signal_emit_by_name (self, "offset-changed");
363 : : }
364 : :
365 : : /**
366 : : * mws_clock_dummy_get_next_alarm_time:
367 : : * @self: a #MwsClockDummy
368 : : *
369 : : * Get the time when the next alarm will be triggered.
370 : : *
371 : : * Returns: time of the next alarm, or %NULL if there are currently no alarms
372 : : * scheduled
373 : : * Since: 0.1.0
374 : : */
375 : : GDateTime *
376 : 0 : mws_clock_dummy_get_next_alarm_time (MwsClockDummy *self)
377 : : {
378 [ # # ]: 0 : g_return_val_if_fail (MWS_IS_CLOCK_DUMMY (self), NULL);
379 : :
380 [ # # ]: 0 : if (self->alarms->len == 0)
381 : 0 : return NULL;
382 : :
383 : 0 : const AlarmData *data = g_ptr_array_index (self->alarms, 0);
384 : 0 : return data->alarm_time;
385 : : }
386 : :
387 : : /**
388 : : * mws_clock_dummy_next_alarm:
389 : : * @self: a #MwsClockDummy
390 : : *
391 : : * Advance the clock to the time of the next alarm (as determined by
392 : : * mws_clock_dummy_get_next_alarm_time()) using mws_clock_dummy_set_time().
393 : : *
394 : : * If there are no alarms scheduled, this function is a no-op and returns
395 : : * %FALSE.
396 : : *
397 : : * Returns: %TRUE if there was a next alarm, %FALSE otherwise
398 : : * Since: 0.1.0
399 : : */
400 : : gboolean
401 : 0 : mws_clock_dummy_next_alarm (MwsClockDummy *self)
402 : : {
403 [ # # ]: 0 : g_return_val_if_fail (MWS_IS_CLOCK_DUMMY (self), FALSE);
404 : :
405 : 0 : GDateTime *next_alarm_time = mws_clock_dummy_get_next_alarm_time (self);
406 [ # # ]: 0 : if (next_alarm_time == NULL)
407 : 0 : return FALSE;
408 : :
409 : 0 : mws_clock_dummy_set_time (self, next_alarm_time);
410 : :
411 : 0 : return TRUE;
412 : : }
|