Thu, 24 Jul 2025 23:35:13 -0500
Create Purple.Scheduler
This manages Purple.ScheduledTask instances and propagates their execute
signals.
This can be used for stuff like reconnecting accounts, scheduling messages
regardless of protocol support, changing avatars at a set time, really
whatever you can think of and write a plugin to implement.
Testing Done:
Ran the unit tests under valgrind and called in the turtles.
Also run in a dev environment and verified that there were no weird error messages.
Bugs closed: PIDGIN-18105
Reviewed at https://reviews.imfreedom.org/r/4073/
--- a/libpurple/core.c Thu Jul 24 23:33:18 2025 -0500 +++ b/libpurple/core.c Thu Jul 24 23:35:13 2025 -0500 @@ -49,6 +49,7 @@ #include "purplepath.h" #include "purplepresencemanagerprivate.h" #include "purpleprotocolmanagerprivate.h" +#include "purpleschedulerprivate.h" #include "purplewhiteboardmanagerprivate.h" #include "util.h" #ifdef _WIN32 @@ -127,6 +128,7 @@ purple_ui_prefs_init(core->ui); + purple_scheduler_startup(); purple_notification_manager_startup(); purple_protocol_manager_startup(); @@ -234,6 +236,7 @@ purple_notification_manager_shutdown(); purple_history_manager_shutdown(); + purple_scheduler_shutdown(); /* Everything after util_uninit cannot try to write things to the * confdir.
--- a/libpurple/meson.build Thu Jul 24 23:33:18 2025 -0500 +++ b/libpurple/meson.build Thu Jul 24 23:35:13 2025 -0500 @@ -71,6 +71,7 @@ 'purpleproxyinfo.c', 'purplesavedpresence.c', 'purplescheduledtask.c', + 'purplescheduler.c', 'purplesqlitehistoryadapter.c', 'purpletags.c', 'purpleui.c', @@ -167,6 +168,7 @@ 'purpleproxyinfo.h', 'purplesavedpresence.h', 'purplescheduledtask.h', + 'purplescheduler.h', 'purplesqlitehistoryadapter.h', 'purpletags.h', 'purpletyping.h', @@ -195,6 +197,7 @@ 'purplenotificationmanagerprivate.h', 'purplepresencemanagerprivate.h', 'purpleprotocolmanagerprivate.h', + 'purpleschedulerprivate.h', 'purplewhiteboardmanagerprivate.h', ]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purplescheduler.c Thu Jul 24 23:35:13 2025 -0500 @@ -0,0 +1,437 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this library; if not, see <https://www.gnu.org/licenses/>. + */ + +#ifdef G_LOG_DOMAIN +# undef G_LOG_DOMAIN +#endif +#define G_LOG_DOMAIN "PurpleScheduler" + +#include <birb.h> + +#include "purplescheduler.h" +#include "purpleschedulerprivate.h" + +struct _PurpleScheduler { + GObject parent; + + GPtrArray *tasks; +}; + +enum { + PROP_0, + PROP_ITEM_TYPE, + PROP_N_ITEMS, + N_PROPERTIES, +}; +static GParamSpec *properties[N_PROPERTIES] = {NULL, }; + +enum { + SIG_EXECUTE_TASK, + N_SIGNALS, +}; +static guint signals[N_SIGNALS] = {0, }; + +G_DEFINE_QUARK(purple-scheduler-error, purple_scheduler_error) + +static PurpleScheduler *default_scheduler = NULL; + +/****************************************************************************** + * Helpers + *****************************************************************************/ +/** + * purple_scheduler_find_task_with_id: (skip) + * @id: the id of the task to search for + * @position: (out) (nullable): a return address for the position of the item + * + * Looks for a task with the given id. + * + * If the task is found it will be returned as well as it's position. + * + * Returns: (transfer none) (nullable): The task if found. + * + * Since: 3.0 + */ +static PurpleScheduledTask * +purple_scheduler_find_task_with_id(PurpleScheduler *scheduler, + const char *id, + guint *position) +{ + g_return_val_if_fail(PURPLE_IS_SCHEDULER(scheduler), NULL); + + for(guint i = 0; i < scheduler->tasks->len; i++) { + PurpleScheduledTask *task = NULL; + const char *task_id = NULL; + + task = g_ptr_array_index(scheduler->tasks, i); + task_id = purple_scheduled_task_get_id(task); + + if(birb_str_equal(id, task_id)) { + if(position != NULL) { + *position = i; + } + + return task; + } + } + + return NULL; +} + +/** + * purple_scheduler_unref_task: (skip) + * @task: the task to cancel and unref + * + * Cancels a task if necessary before unreferencing it. + * + * Since: 3.0 + */ +static void +purple_scheduler_unref_task(PurpleScheduledTask *task) { + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + + g_return_if_fail(PURPLE_IS_SCHEDULED_TASK(task)); + + state = purple_scheduled_task_get_state(task); + if(state == PURPLE_SCHEDULED_TASK_STATE_SCHEDULED) { + purple_scheduled_task_cancel(task); + } + + g_object_unref(task); +} + +/****************************************************************************** + * Callbacks + *****************************************************************************/ +static void +purple_scheduler_task_execute_cb(PurpleScheduledTask *task, gpointer data) { + PurpleScheduler *scheduler = data; + const char *task_type = NULL; + + task_type = purple_scheduled_task_get_task_type(task); + g_signal_emit(scheduler, + signals[SIG_EXECUTE_TASK], + g_quark_from_string(task_type), + task, + task_type); +} + +static void +purple_scheduler_task_notify_state_cb(GObject *obj, + G_GNUC_UNUSED GParamSpec *pspec, + gpointer data) +{ + PurpleScheduledTask *task = PURPLE_SCHEDULED_TASK(obj); + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + PurpleScheduler *scheduler = data; + + state = purple_scheduled_task_get_state(task); + if(state == PURPLE_SCHEDULED_TASK_STATE_EXECUTED) { + purple_scheduler_remove_task(scheduler, + purple_scheduled_task_get_id(task)); + } +} + +/****************************************************************************** + * GListModel Implementation + *****************************************************************************/ +static GType +purple_scheduler_get_item_type(G_GNUC_UNUSED GListModel *model) { + return PURPLE_TYPE_SCHEDULED_TASK; +} + +static guint +purple_scheduler_get_n_items(GListModel *list) { + PurpleScheduler *scheduler = PURPLE_SCHEDULER(list); + + return scheduler->tasks->len; +} + +static gpointer +purple_scheduler_get_item(GListModel *list, guint position) { + PurpleScheduler *scheduler = PURPLE_SCHEDULER(list); + PurpleScheduledTask *task = NULL; + + if(position < scheduler->tasks->len) { + task = g_ptr_array_index(scheduler->tasks, position); + g_object_ref(task); + } + + return task; +} + +static void +purple_scheduler_list_model_init(GListModelInterface *iface) { + iface->get_item_type = purple_scheduler_get_item_type; + iface->get_n_items = purple_scheduler_get_n_items; + iface->get_item = purple_scheduler_get_item; +} + +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +G_DEFINE_FINAL_TYPE_WITH_CODE(PurpleScheduler, purple_scheduler, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL, + purple_scheduler_list_model_init)); + +static void +purple_scheduler_finalize(GObject *obj) { + PurpleScheduler *scheduler = PURPLE_SCHEDULER(obj); + + g_clear_pointer(&scheduler->tasks, g_ptr_array_unref); + + G_OBJECT_CLASS(purple_scheduler_parent_class)->finalize(obj); +} + +static void +purple_scheduler_get_property(GObject *obj, guint param_id, GValue *value, + GParamSpec *pspec) +{ + GListModel *model = G_LIST_MODEL(obj); + + switch(param_id) { + case PROP_ITEM_TYPE: + g_value_set_gtype(value, g_list_model_get_item_type(model)); + break; + case PROP_N_ITEMS: + g_value_set_uint(value, g_list_model_get_n_items(model)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); + break; + } +} + +static void +purple_scheduler_init(PurpleScheduler *scheduler) { + scheduler->tasks = g_ptr_array_new_full(10, + (GDestroyNotify)purple_scheduler_unref_task); +} + +static void +purple_scheduler_class_init(PurpleSchedulerClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + + obj_class->finalize = purple_scheduler_finalize; + obj_class->get_property = purple_scheduler_get_property; + + /** + * PurpleScheduler:item-type: + * + * The type of items. See [vfunc@Gio.ListModel.get_item_type]. + * + * Since: 3.0 + */ + properties[PROP_ITEM_TYPE] = g_param_spec_gtype( + "item-type", NULL, NULL, + G_TYPE_OBJECT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * PurpleScheduler:n-items: + * + * The number of items. See [vfunc@Gio.ListModel.get_n_items]. + * + * Since: 3.0 + */ + properties[PROP_N_ITEMS] = g_param_spec_uint( + "n-items", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(obj_class, N_PROPERTIES, properties); + + /** + * PurpleScheduler::execute-task: + * @scheduler: the instance + * @task: the task being executed + * @task_type: the task type + * + * Emitted when a task is being executed. + * + * This signal supports details on [property@ScheduledTask:task-type] to + * make it easier to listen for specific task types being executed. + * + * Since: 3.0 + */ + signals[SIG_EXECUTE_TASK] = g_signal_new_class_handler( + "execute-task", + G_OBJECT_CLASS_TYPE(klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + NULL, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + PURPLE_TYPE_SCHEDULED_TASK, + G_TYPE_STRING); +} + +/****************************************************************************** + * Private API + *****************************************************************************/ +void +purple_scheduler_startup(void) { + if(!PURPLE_IS_SCHEDULER(default_scheduler)) { + default_scheduler = purple_scheduler_new(); + g_object_add_weak_pointer(G_OBJECT(default_scheduler), + (gpointer *)&default_scheduler); + } +} + +void +purple_scheduler_shutdown(void) { + g_clear_object(&default_scheduler); +} + +/****************************************************************************** + * Public API + *****************************************************************************/ +gboolean +purple_scheduler_add_task(PurpleScheduler *scheduler, + PurpleScheduledTask *task, + GDateTime *execute_at, + GError **error) +{ + PurpleScheduledTask *existing = NULL; + GError *local_error = NULL; + const char *id = NULL; + + g_return_val_if_fail(PURPLE_IS_SCHEDULER(scheduler), FALSE); + g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), FALSE); + g_return_val_if_fail(execute_at != NULL, FALSE); + + id = purple_scheduled_task_get_id(task); + existing = purple_scheduler_find_task_with_id(scheduler, id, NULL); + if(PURPLE_IS_SCHEDULED_TASK(existing)) { + g_set_error(error, + PURPLE_SCHEDULER_ERROR, + PURPLE_SCHEDULER_ERROR_TASK_EXISTS, + "a task with id %s already exists", + id); + return FALSE; + } + + if(!purple_scheduled_task_schedule(task, execute_at, &local_error)) { + if(local_error != NULL) { + g_propagate_error(error, local_error); + } else { + g_set_error_literal(error, + PURPLE_SCHEDULER_ERROR, + PURPLE_SCHEDULER_ERROR_FAILED_TO_SCHEDULE, + "the task failed to schedule for an unknown " + "reason"); + } + + return FALSE; + } + + /* Connect to the execute signal so we can propagate the signal. */ + g_signal_connect_object(task, + "execute", + G_CALLBACK(purple_scheduler_task_execute_cb), + scheduler, + G_CONNECT_DEFAULT); + + /* Add a handler to remove the task after it's been executed. */ + g_signal_connect_object(task, + "notify::state", + G_CALLBACK(purple_scheduler_task_notify_state_cb), + scheduler, + G_CONNECT_DEFAULT); + + /* Finally add the item and emit the items-changed signal. */ + g_ptr_array_add(scheduler->tasks, g_object_ref(task)); + g_list_model_items_changed(G_LIST_MODEL(scheduler), + scheduler->tasks->len - 1, 0, 1); + + return TRUE; +} + +gboolean +purple_scheduler_add_task_relative(PurpleScheduler *scheduler, + PurpleScheduledTask *task, + GTimeSpan when, + GError **error) +{ + GDateTime *now = NULL; + GDateTime *execute_at = NULL; + gboolean ret = FALSE; + + g_return_val_if_fail(PURPLE_IS_SCHEDULER(scheduler), FALSE); + g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), FALSE); + + now = g_date_time_new_now_local(); + execute_at = g_date_time_add(now, when); + g_date_time_unref(now); + + ret = purple_scheduler_add_task(scheduler, task, execute_at, error); + + g_date_time_unref(execute_at); + + return ret; +} + +PurpleScheduler * +purple_scheduler_get_default(void) { + return default_scheduler; +} + +GListModel * +purple_scheduler_get_default_as_model(void) { + if(G_IS_LIST_MODEL(default_scheduler)) { + return G_LIST_MODEL(default_scheduler); + } + + return NULL; +} + +PurpleScheduler * +purple_scheduler_new(void) { + return g_object_new(PURPLE_TYPE_SCHEDULER, NULL); +} + +gboolean +purple_scheduler_remove_task(PurpleScheduler *scheduler, const char *id) { + PurpleScheduledTask *task = NULL; + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + guint position = 0; + + g_return_val_if_fail(PURPLE_IS_SCHEDULER(scheduler), FALSE); + g_return_val_if_fail(!birb_str_is_empty(id), FALSE); + + task = purple_scheduler_find_task_with_id(scheduler, id, &position); + if(!PURPLE_IS_SCHEDULED_TASK(task)) { + return FALSE; + } + + state = purple_scheduled_task_get_state(task); + if(state == PURPLE_SCHEDULED_TASK_STATE_SCHEDULED) { + purple_scheduled_task_cancel(task); + } + + g_ptr_array_remove_index(scheduler->tasks, position); + + g_list_model_items_changed(G_LIST_MODEL(scheduler), position, 1, 0); + + return TRUE; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purplescheduler.h Thu Jul 24 23:35:13 2025 -0500 @@ -0,0 +1,185 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this library; if not, see <https://www.gnu.org/licenses/>. + */ + +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <purple.h> may be included directly" +#endif + +#ifndef PURPLE_SCHEDULER_H +#define PURPLE_SCHEDULER_H + +#include <glib.h> +#include <glib-object.h> + +#include "purplescheduledtask.h" +#include "purpleversion.h" + +G_BEGIN_DECLS + +#define PURPLE_SCHEDULER_ERROR purple_scheduler_error_quark() + +/** + * PurpleSchedulerError: + * @PURPLE_SCHEDULER_ERROR_TASK_EXISTS: the task has already been added to the + * scheduler + * @PURPLE_SCHEDULER_ERROR_FAILED_TO_SCHEDULE: the task failed to schedule for + * an unknown reason + * + * Error codes returned by the scheduler. + * + * Since: 3.0 + */ +typedef enum { + PURPLE_SCHEDULER_ERROR_TASK_EXISTS PURPLE_AVAILABLE_ENUMERATOR_IN_3_0, + PURPLE_SCHEDULER_ERROR_FAILED_TO_SCHEDULE PURPLE_AVAILABLE_ENUMERATOR_IN_3_0, +} PurpleSchedulerError; + +/** + * purple_scheduler_error_quark: + * + * The error domain to identify errors with the scheduler. + * + * Returns: The error domain. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +GQuark purple_scheduler_error_quark(void); + +/** + * PurpleScheduler: + * + * An object that manages a collection of [class@ScheduledTask]. + * + * Since: 3.0 + */ + +#define PURPLE_TYPE_SCHEDULER (purple_scheduler_get_type()) + +PURPLE_AVAILABLE_IN_3_0 +G_DECLARE_FINAL_TYPE(PurpleScheduler, purple_scheduler, PURPLE, SCHEDULER, + GObject) + +/** + * purple_scheduler_add_task: + * @task: (transfer none): the task to add + * @execute_at: (transfer none): the date time for when the task should be + * executed. + * @error: (out) (nullable): a return address for a #GError + * + * Adds a new task to the scheduler. + * + * If the task has already been added, false will be returned and @error will + * be set. + * + * If @task is already scheduled, it will be rescheduled to run at @execute_at. + * + * Returns: true if the task was added successfully. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_scheduler_add_task(PurpleScheduler *scheduler, PurpleScheduledTask *task, GDateTime *execute_at, GError **error); + +/** + * purple_scheduler_add_task_relative: + * @task: (transfer none): the task to add + * @when: the relative time from now to execute the task + * @error: (out) (nullable): a return address for a #GError + * + * Adds a new task to the scheduler. + * + * If the task has already been added, false will be returned and @error will + * be set. + * + * If @task is already scheduled, it will be rescheduled to run at @when. + * + * This is a wrapper around [method@Scheduler.add_task] that will add @when to + * the current time for you. + * + * Returns: true if the task was added successfully. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_scheduler_add_task_relative(PurpleScheduler *scheduler, PurpleScheduledTask *task, GTimeSpan when, GError **error); + +/** + * purple_scheduler_get_default: + * + * Gets the default scheduler. + * + * This can return %NULL if libpurple has not been initialized. + * + * Returns: (transfer none) (nullable): The default scheduler. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleScheduler *purple_scheduler_get_default(void); + +/** + * purple_scheduler_get_default_as_model: + * + * Gets the default scheduler cast to a [iface@Gio.ListModel]. + * + * This can return %NULL if libpurple has not been initialized. + * + * Returns: (transfer none) (nullable): The default scheduler as a list model. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +GListModel *purple_scheduler_get_default_as_model(void); + +/** + * purple_scheduler_new: + * + * Creates a new scheduler. + * + * This is typically only used internally but may be useful to others. + * + * Returns: (transfer full): The new instance. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleScheduler *purple_scheduler_new(void); + +/** + * purple_scheduler_remove_task: + * @id: the id of the task + * + * Removes a task from the scheduler. + * + * If the task is found and currently scheduled, it will be cancelled. + * + * Returns: true if the task was found and removed. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_scheduler_remove_task(PurpleScheduler *scheduler, const char *id); + +G_END_DECLS + +#endif /* PURPLE_SCHEDULER_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purpleschedulerprivate.h Thu Jul 24 23:35:13 2025 -0500 @@ -0,0 +1,56 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this library; if not, see <https://www.gnu.org/licenses/>. + */ + +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <purple.h> may be included directly" +#endif + +#ifndef PURPLE_SCHEDULER_PRIVATE_H +#define PURPLE_SCHEDULER_PRIVATE_H + +#include <glib.h> + +G_BEGIN_DECLS + +/** + * purple_scheduler_startup: (skip) + * + * Starts up the scheduler by creating the default instance. + * + * Since: 3.0 + */ +G_GNUC_INTERNAL +void purple_scheduler_startup(void); + +/** + * purple_scheduler_shutdown: (skip) + * + * Shuts down the scheduler by destroying the default instance. + * + * Since: 3.0 + */ +G_GNUC_INTERNAL +void purple_scheduler_shutdown(void); + +G_END_DECLS + +#endif /* PURPLE_SCHEDULER_PRIVATE_H */
--- a/libpurple/tests/meson.build Thu Jul 24 23:33:18 2025 -0500 +++ b/libpurple/tests/meson.build Thu Jul 24 23:35:13 2025 -0500 @@ -54,6 +54,7 @@ 'request_page', 'saved_presence', 'scheduled_task', + 'scheduler', 'str', 'tags', 'util',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/tests/test_scheduler.c Thu Jul 24 23:35:13 2025 -0500 @@ -0,0 +1,489 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <https://www.gnu.org/licenses/>. + */ + +#include <glib.h> + +#include <birb.h> + +#include <purple.h> + +/****************************************************************************** + * Tests + *****************************************************************************/ +static void +test_purple_scheduler_new(void) { + PurpleScheduler *scheduler = NULL; + + scheduler = purple_scheduler_new(); + birb_assert_type(scheduler, PURPLE_TYPE_SCHEDULER); + g_assert_true(G_IS_LIST_MODEL(scheduler)); + + g_assert_finalize_object(scheduler); +} + +static void +test_purple_scheduler_properties(void) { + PurpleScheduler *scheduler = NULL; + GType item_type = G_TYPE_INVALID; + guint n_items = 0; + + scheduler = g_object_new( + PURPLE_TYPE_SCHEDULER, + NULL); + + g_object_get( + G_OBJECT(scheduler), + "item-type", &item_type, + "n-items", &n_items, + NULL); + + g_assert_cmpuint(item_type, ==, PURPLE_TYPE_SCHEDULED_TASK); + + g_assert_cmpuint(n_items, ==, 0); + + g_assert_finalize_object(scheduler); +} + +static void +test_purple_scheduler_add_remove(void) { + PurpleScheduledTask *task = NULL; + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + PurpleScheduler *scheduler = NULL; + GDateTime *execute_at = NULL; + GDateTime *now = NULL; + GError *error = NULL; + guint counter = 0; + gboolean result = FALSE; + + scheduler = purple_scheduler_new(); + birb_assert_type(scheduler, PURPLE_TYPE_SCHEDULER); + g_assert_true(G_IS_LIST_MODEL(scheduler)); + + /* Create our execute_at time. */ + now = g_date_time_new_now_local(); + execute_at = g_date_time_add(now, 10 * G_TIME_SPAN_MINUTE); + g_clear_pointer(&now, g_date_time_unref); + + /* Wire up our signals. */ + birb_count_list_model_items_changed(G_LIST_MODEL(scheduler), &counter); + + /* Create the task. */ + task = purple_scheduled_task_new("test-scheduler", "Scheduler Tests", + TRUE); + birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK); + + /* Make sure the task is unscheduled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED); + + /* Add the task to the scheduler. */ + counter = 0; + result = purple_scheduler_add_task(scheduler, task, execute_at, &error); + + g_assert_no_error(error); + g_assert_true(result); + + /* Make sure items changed was called once and that we have 1 item in the + * list model. + */ + g_assert_cmpuint(counter, ==, 1); + birb_assert_list_model_n_items(scheduler, 1); + + /* Make sure that the task got scheduled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED); + + /* Remove the task. */ + counter = 0; + + result = purple_scheduler_remove_task(scheduler, + purple_scheduled_task_get_id(task)); + g_assert_true(result); + + /* Make sure items changed was called once and that the model empty. */ + g_assert_cmpuint(counter, ==, 1); + birb_assert_list_model_n_items(scheduler, 0); + + /* Make sure the that the task got cancelled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_CANCELLED); + + /* After removal from the scheduler, only our reference should exist. */ + g_assert_finalize_object(task); + + /* Clean up the scheduler. */ + g_assert_finalize_object(scheduler); + + g_clear_pointer(&execute_at, g_date_time_unref); +} + +static void +test_purple_scheduler_add_already_scheduled(void) { + PurpleScheduledTask *task = NULL; + PurpleScheduler *scheduler = NULL; + GDateTime *original_execute_at = NULL; + GDateTime *updated_execute_at = NULL; + GError *error = NULL; + gboolean result = FALSE; + + scheduler = purple_scheduler_new(); + + /* Create the task. */ + task = purple_scheduled_task_new("test-scheduler", "Scheduler Tests", + TRUE); + + /* Schedule the task and store the original execute_at. */ + result = purple_scheduled_task_schedule_relative(task, + 10 * G_TIME_SPAN_MILLISECOND, + &error); + g_assert_no_error(error); + g_assert_true(result); + + original_execute_at = purple_scheduled_task_get_execute_at(task); + if(original_execute_at != NULL) { + g_date_time_ref(original_execute_at); + } + + /* Add the task to the scheduler. */ + result = purple_scheduler_add_task_relative(scheduler, + task, + 100 * G_TIME_SPAN_MILLISECOND, + &error); + g_assert_no_error(error); + g_assert_true(result); + + /* Get the execute time of the task and verify that it is not the same as + * the original time. + */ + updated_execute_at = purple_scheduled_task_get_execute_at(task); + g_assert_false(birb_date_time_equal(original_execute_at, + updated_execute_at)); + + /* Clean up everything. */ + g_assert_finalize_object(scheduler); + g_assert_finalize_object(task); + g_clear_pointer(&original_execute_at, g_date_time_unref); +} + +static void +test_purple_scheduler_double_add(void) { + PurpleScheduledTask *task = NULL; + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + PurpleScheduler *scheduler = NULL; + GDateTime *execute_at = NULL; + GDateTime *now = NULL; + GError *error = NULL; + guint counter = 0; + gboolean result = FALSE; + + scheduler = purple_scheduler_new(); + birb_assert_type(scheduler, PURPLE_TYPE_SCHEDULER); + g_assert_true(G_IS_LIST_MODEL(scheduler)); + + /* Create our execute_at time. */ + now = g_date_time_new_now_local(); + execute_at = g_date_time_add(now, 10 * G_TIME_SPAN_MINUTE); + g_clear_pointer(&now, g_date_time_unref); + + /* Wire up our signals. */ + birb_count_list_model_items_changed(G_LIST_MODEL(scheduler), &counter); + + /* Create the task. */ + task = purple_scheduled_task_new("test-scheduler", "Scheduler Tests", + TRUE); + birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK); + + /* Make sure the task is unscheduled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED); + + /* Add the task to the scheduler. */ + counter = 0; + result = purple_scheduler_add_task(scheduler, task, execute_at, &error); + + g_assert_no_error(error); + g_assert_true(result); + + /* Make sure items changed was called once and that we have 1 item in the + * list model. + */ + g_assert_cmpuint(counter, ==, 1); + birb_assert_list_model_n_items(scheduler, 1); + + /* Make sure that the task got scheduled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED); + + /* Now add the task again. */ + counter = 0; + result = purple_scheduler_add_task(scheduler, task, execute_at, &error); + g_assert_error(error, + PURPLE_SCHEDULER_ERROR, + PURPLE_SCHEDULER_ERROR_TASK_EXISTS); + g_clear_error(&error); + g_assert_false(result); + + /* Make sure the items-changed signal wasn't called and that we still only + * have one item in the list. + */ + g_assert_cmpuint(counter, ==, 0); + birb_assert_list_model_n_items(scheduler, 1); + + /* Cleanup. We don't remove the task because we want to make sure the + * scheduler will cancel it when it shuts down. Also the task is last as + * it's still known to the scheduler. + */ + g_assert_finalize_object(scheduler); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_CANCELLED); + + g_assert_finalize_object(task); + + g_clear_pointer(&execute_at, g_date_time_unref); +} + +static void +test_purple_scheduler_double_remove(void) { + PurpleScheduledTask *task = NULL; + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + PurpleScheduler *scheduler = NULL; + GDateTime *execute_at = NULL; + GDateTime *now = NULL; + GError *error = NULL; + guint counter = 0; + gboolean result = FALSE; + + scheduler = purple_scheduler_new(); + birb_assert_type(scheduler, PURPLE_TYPE_SCHEDULER); + g_assert_true(G_IS_LIST_MODEL(scheduler)); + + /* Create our execute_at time. */ + now = g_date_time_new_now_local(); + execute_at = g_date_time_add(now, 10 * G_TIME_SPAN_MINUTE); + g_clear_pointer(&now, g_date_time_unref); + + /* Wire up our signals. */ + birb_count_list_model_items_changed(G_LIST_MODEL(scheduler), &counter); + + /* Create the task. */ + task = purple_scheduled_task_new("test-scheduler", "Scheduler Tests", + TRUE); + birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK); + + /* Make sure the task is unscheduled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED); + + /* Add the task to the scheduler. */ + counter = 0; + result = purple_scheduler_add_task(scheduler, task, execute_at, &error); + + g_assert_no_error(error); + g_assert_true(result); + + /* Make sure items changed was called once and that we have 1 item in the + * list model. + */ + g_assert_cmpuint(counter, ==, 1); + birb_assert_list_model_n_items(scheduler, 1); + + /* Make sure that the task got scheduled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED); + + /* Remove the task. */ + counter = 0; + result = purple_scheduler_remove_task(scheduler, + purple_scheduled_task_get_id(task)); + g_assert_true(result); + + /* Make sure the items-changed signal got called once and that we no longer + * have any items in the model. + */ + g_assert_cmpuint(counter, ==, 1); + birb_assert_list_model_n_items(scheduler, 0); + + /* Attempt to remove the task again. */ + counter = 0; + result = purple_scheduler_remove_task(scheduler, + purple_scheduled_task_get_id(task)); + g_assert_false(result); + + /* Make sure the items-changed signal wasn't called and that we still don't + * have any items in the model. + */ + g_assert_cmpuint(counter, ==, 0); + birb_assert_list_model_n_items(scheduler, 0); + + /* Cleanup. We remove task first because the scheduler shouldn't know about + * it anymore. + */ + g_assert_finalize_object(task); + g_assert_finalize_object(scheduler); + + g_clear_pointer(&execute_at, g_date_time_unref); +} + +/****************************************************************************** + * Main + *****************************************************************************/ +static void +test_purple_scheduler_execute_task_cb(PurpleScheduler *scheduler, + PurpleScheduledTask *task, + const char *task_type, + gpointer data) +{ + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + const char *actual_task_type = NULL; + guint *counter = data; + + birb_assert_type(scheduler, PURPLE_TYPE_SCHEDULER); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_EXECUTING); + + actual_task_type = purple_scheduled_task_get_task_type(task); + g_assert_cmpstr(actual_task_type, ==, task_type); + + *counter = *counter + 1; +} + +static void +test_purple_scheduler_signals_quit_cb(G_GNUC_UNUSED PurpleScheduler *scheduler, + G_GNUC_UNUSED PurpleScheduledTask *task, + G_GNUC_UNUSED const char *task_type, + gpointer data) +{ + g_main_loop_quit(data); +} + +static void +test_purple_scheduler_signals_timeout_cb(gpointer data) { + g_main_loop_quit(data); + + g_assert_not_reached(); +} + +static void +test_purple_scheduler_signals(void) { + PurpleScheduledTask *task1 = NULL; + PurpleScheduledTask *task2 = NULL; + PurpleScheduler *scheduler = NULL; + GError *error = NULL; + GMainLoop *loop = NULL; + guint all_counter = 0; + guint detailed_counter = 0; + gboolean result = FALSE; + + /* This test creates 2 tasks, one for 10ms from now and the second for 20ms + * from now. When the 20ms task is executed the main loop will be quit and + * allow the rest of the test to finish. + * + * There is a 2 second timeout to make sure we don't hang the unit tests if + * something unexpected happens. We use 2 seconds because internally the + * tasks are scheduled with g_timeout_add_seconds which tries to schedule + * timeouts together to avoid excessive CPU wake ups, so 2 seconds should + * cover that. + */ + + /* Create the counter and add our signal handlers with and without the + * detail. + */ + scheduler = purple_scheduler_new(); + g_signal_connect(scheduler, + "execute-task", + G_CALLBACK(test_purple_scheduler_execute_task_cb), + &all_counter); + g_signal_connect(scheduler, + "execute-task::scheduler-test-2", + G_CALLBACK(test_purple_scheduler_execute_task_cb), + &detailed_counter); + + /* Add the first task. */ + task1 = purple_scheduled_task_new("scheduler-test-1", "Scheduler Test 1", + TRUE); + result = purple_scheduler_add_task_relative(scheduler, + task1, + 10 * G_TIME_SPAN_MILLISECOND, + &error); + g_assert_no_error(error); + g_assert_true(result); + + /* Add the second task. */ + task2 = purple_scheduled_task_new("scheduler-test-2", "Scheduler Test 2", + FALSE); + result = purple_scheduler_add_task_relative(scheduler, + task2, + 20 * G_TIME_SPAN_MILLISECOND, + &error); + g_assert_no_error(error); + g_assert_true(result); + + /* Create the main loop to run the tasks. */ + loop = g_main_loop_new(NULL, FALSE); + + /* Add a handler to quit the main loop when the second task is executed. */ + g_signal_connect(scheduler, + "execute-task::scheduler-test-2", + G_CALLBACK(test_purple_scheduler_signals_quit_cb), + loop); + + /* Add a timeout to avoid hangs on unexpected behavior. */ + g_timeout_add_seconds_once(2, test_purple_scheduler_signals_timeout_cb, + loop); + + /* Run the main loop. */ + g_main_loop_run(loop); + + g_clear_pointer(&loop, g_main_loop_unref); + + /* Make sure the counters are correct. */ + g_assert_cmpuint(all_counter, ==, 2); + g_assert_cmpuint(detailed_counter, ==, 1); + + /* Make sure the scheduler is empty. */ + birb_assert_list_model_n_items(scheduler, 0); + + g_assert_finalize_object(scheduler); + g_assert_finalize_object(task1); + g_assert_finalize_object(task2); +} + +/****************************************************************************** + * Main + *****************************************************************************/ +int +main(int argc, char *argv[]) { + g_test_init(&argc, &argv, NULL); + g_test_set_nonfatal_assertions(); + + g_test_add_func("/scheduler/new", test_purple_scheduler_new); + g_test_add_func("/scheduler/properties", test_purple_scheduler_properties); + + g_test_add_func("/scheduler/add-remove", test_purple_scheduler_add_remove); + g_test_add_func("/scheduler/add-already-scheduled", + test_purple_scheduler_add_already_scheduled); + g_test_add_func("/scheduler/double-add", test_purple_scheduler_double_add); + g_test_add_func("/scheduler/double-remove", + test_purple_scheduler_double_remove); + + g_test_add_func("/scheduler/signals", test_purple_scheduler_signals); + + return g_test_run(); +}