diff --git a/lib/wp/collection-manager.c b/lib/wp/collection-manager.c new file mode 100644 index 00000000..a7e76534 --- /dev/null +++ b/lib/wp/collection-manager.c @@ -0,0 +1,907 @@ +/* WirePlumber + * + * Copyright © 2025 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include "error.h" +#include "core.h" +#include "collection-manager.h" +#include "metadata.h" +#include "log.h" +#include "object-manager.h" + +#include "private/collection.h" + +WP_DEFINE_LOCAL_LOG_TOPIC ("wp-collection-manager") + +/*! \defgroup wpcollectionmanager WpCollectionManager */ +/*! + * \struct WpCollectionManager + * + * The WpCollectionManager class is the object charged to create and destroy + * the collections in WirePlumber. + * + * WpCollection API. + */ + +struct _WpCollectionManager +{ + WpObject parent; + + /* Props */ + gchar *metadata_name; + + /* Metadata */ + WpObjectManager *metadata_om; + GWeakRef metadata; + + /* Collections info */ + guint pending_collections; + guint failed_collections; + GHashTable *collections; + GHashTable *create_collection_tasks; + + /* Globals info */ + WpObjectManager *global_proxy_om; + GHashTable *collected_globals; +}; + +enum { + PROP_0, + PROP_METADATA_NAME, +}; + +enum { + SIGNAL_GLOBAL_COLLECTED, + SIGNAL_GLOBAL_DROPPED, + LAST_SIGNAL, +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE (WpCollectionManager, wp_collection_manager, WP_TYPE_OBJECT) + +static void +wp_collection_manager_init (WpCollectionManager * self) +{ + self->collections = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + self->create_collection_tasks = g_hash_table_new_full (g_str_hash, + g_str_equal, g_free, g_object_unref); + self->collected_globals = g_hash_table_new_full (g_direct_hash, + g_direct_equal, NULL, g_free); +} + +static void +wp_collection_manager_set_property (GObject * object, guint property_id, + const GValue * value, GParamSpec * pspec) +{ + WpCollectionManager *self = WP_COLLECTION_MANAGER (object); + + switch (property_id) { + case PROP_METADATA_NAME: + g_clear_pointer (&self->metadata_name, g_free); + self->metadata_name = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +wp_collection_manager_get_property (GObject * object, guint property_id, + GValue * value, GParamSpec * pspec) +{ + WpCollectionManager *self = WP_COLLECTION_MANAGER (object); + + switch (property_id) { + case PROP_METADATA_NAME: + g_value_set_string (value, self->metadata_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +enum { + STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START, +}; + +static WpObjectFeatures +wp_collection_manager_get_supported_features (WpObject * self) +{ + return WP_COLLECTION_MANAGER_LOADED; +} + +static guint +wp_collection_manager_activate_get_next_step (WpObject * self, + WpFeatureActivationTransition * transition, guint step, + WpObjectFeatures missing) +{ + g_return_val_if_fail (missing == WP_COLLECTION_MANAGER_LOADED, + WP_TRANSITION_STEP_ERROR); + + return STEP_LOAD; +} + +static void +on_global_proxy_removed (WpObjectManager *om, WpGlobalProxy *global, gpointer d) +{ + WpCollectionManager * self = WP_COLLECTION_MANAGER (d); + + /* Drop global from the collection if global is removed */ + wp_collection_manager_drop_global (self, global); +} + +static void +on_collection_activated (GObject * o, GAsyncResult *res, + WpTransition * transition) +{ + WpCollectionManager * self = wp_transition_get_source_object (transition); + g_autoptr (WpCollection) collection = WP_COLLECTION (o); + g_autoptr (GError) error = NULL; + const gchar *name; + + /* Get the task name */ + name = wp_collection_get_name (collection); + g_return_if_fail (name); + + /* Add collection to hash table if it was activated successfully */ + if (!wp_object_activate_finish (WP_OBJECT (collection), res, &error)) { + self->failed_collections++; + wp_warning_object (self, "Failed to activate collection '%s': %s", name, + error->message); + } else { + g_autoptr (WpMetadata) m = wp_collection_get_metadata (collection); + g_autoptr (WpIterator) it = NULL; + g_auto (GValue) item = G_VALUE_INIT; + + /* Collect the globals */ + it = wp_metadata_new_iterator (m, PW_ID_ANY); + for (; wp_iterator_next (it, &item); g_value_unset (&item)) { + WpMetadataItem *mi = g_value_get_boxed (&item); + const gchar *key = wp_metadata_item_get_key (mi); + guint32 global_id = SPA_ID_INVALID; + + /* Parse global Id */ + if (!spa_atou32 (key, &global_id, 10)) { + wp_warning_object (self, + "Failed to parse global Id from metadata key '%s'. Ignoring...", + key); + continue; + } + + g_autoptr (WpGlobalProxy) global = wp_object_manager_lookup ( + self->global_proxy_om, WP_TYPE_GLOBAL_PROXY, + WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", global_id, NULL); + if (global) + g_hash_table_insert (self->collected_globals, global, g_strdup (name)); + else + wp_warning_object (self, + "Could not find global with Id %u for collection %s. Not adding...", + global_id, name); + } + + /* Add the collection */ + g_hash_table_insert (self->collections, g_strdup (name), + g_object_ref (collection)); + wp_info_object (self, "Added collection '%s' successfully", name); + } + + g_return_if_fail (self->pending_collections > 0); + self->pending_collections--; + + /* Set loaded feature if no pending collections */ + if (self->pending_collections == 0) { + /* Check for errors */ + if (self->failed_collections > 0) { + wp_transition_return_error (transition, g_error_new ( + WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, + "%d collections failed to activate", + self->failed_collections)); + } else { + wp_object_update_features (WP_OBJECT (self), WP_COLLECTION_MANAGER_LOADED, 0); + } + } +} + +static void +on_collection_ready (GObject * o, GAsyncResult *res, gpointer d) +{ + WpCollectionManager * self = WP_COLLECTION_MANAGER (d); + g_autoptr (WpCollection) collection = WP_COLLECTION (o); + g_autoptr (GError) error = NULL; + g_autoptr (GTask) t = NULL; + g_autofree gchar *k = NULL; + const gchar *name; + + /* Get the task name */ + name = wp_collection_get_name (collection); + g_return_if_fail (name); + + /* Get the pending task if any */ + g_hash_table_steal_extended (self->create_collection_tasks, name, + (gpointer *)&k, (gpointer *)&t); + + /* Add collection to hash table if it was activated successfully */ + name = wp_collection_get_name (collection); + if (!wp_object_activate_finish (WP_OBJECT (collection), res, &error)) { + wp_warning_object (self, "Failed to activate collection '%s': %s", name, + error->message); + if (t) + g_task_return_new_error (t, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT, + "Could not add collection '%s' as its activation failed: %s", name, + error->message); + return; + } + + /* Add collection to the hash table */ + g_hash_table_insert (self->collections, g_strdup (name), + g_object_ref (collection)); + wp_info_object (self, "Added collection '%s' successfully", name); + + /* Notify pending task */ + if (t) + g_task_return_pointer (t, collection, NULL); +} + +static void +on_metadata_changed (WpMetadata *m, guint32 subject, + const gchar *key, const gchar *type, const gchar *value, gpointer d) +{ + WpCollectionManager *self = WP_COLLECTION_MANAGER (d); + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); + + if (key) { + if (value) { + WpCollection *collection; + wp_info_object (self, "Adding collection '%s'", key); + collection = wp_collection_new (core, key); + wp_object_activate (WP_OBJECT (collection), WP_OBJECT_FEATURES_ALL, NULL, + on_collection_ready, self); + } else { + g_hash_table_remove (self->collections, key); + wp_info_object (self, "Removed collection '%s'", key); + } + } else { + g_hash_table_remove_all (self->collections); + wp_info_object (self, "Removed all collections"); + } +} + +static void +on_metadata_added (WpObjectManager *om, WpMetadata *m, gpointer d) +{ + WpTransition * transition = WP_TRANSITION (d); + WpCollectionManager * self = wp_transition_get_source_object (transition); + g_autoptr (WpProperties) props = NULL; + const gchar *metadata_name = NULL; + g_autoptr (WpCore) core = NULL; + g_autoptr (WpIterator) it = NULL; + g_auto (GValue) item = G_VALUE_INIT; + + /* Make sure the metadata has a name */ + props = wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (m)); + g_return_if_fail (props); + metadata_name = wp_properties_get (props, "metadata.name"); + g_return_if_fail (metadata_name); + g_return_if_fail (g_str_equal (metadata_name, self->metadata_name)); + + /* Set the metadata weak ref */ + g_weak_ref_set (&self->metadata, m); + + /* Listen for metadata changes */ + g_signal_connect_object (m, "changed", G_CALLBACK (on_metadata_changed), + self, 0); + + /* Check if there are collections, and activate them if so */ + self->pending_collections = 0; + self->failed_collections = 0; + core = wp_object_get_core (WP_OBJECT (self)); + it = wp_metadata_new_iterator (m, 0); + for (; wp_iterator_next (it, &item); g_value_unset (&item)) { + WpMetadataItem *mi = g_value_get_boxed (&item); + const gchar *key = wp_metadata_item_get_key (mi); + WpCollection *collection = wp_collection_new (core, key); + self->pending_collections++; + wp_object_activate_closure (WP_OBJECT (collection), + WP_OBJECT_FEATURES_ALL, NULL, + g_cclosure_new_object ( + (GCallback) on_collection_activated, G_OBJECT (transition))); + } + + /* Otherwise just set loaded feature */ + if (self->pending_collections == 0) + wp_object_update_features (WP_OBJECT (self), WP_COLLECTION_MANAGER_LOADED, 0); +} + +static void +on_global_proxy_om_installed (WpObjectManager *om, gpointer d) +{ + WpTransition * transition = WP_TRANSITION (d); + WpCollectionManager * self = wp_transition_get_source_object (transition); + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); + + /* Install metadata object manager */ + g_clear_object (&self->metadata_om); + self->metadata_om = wp_object_manager_new (); + wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA, + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, + "metadata.name", "=s", self->metadata_name, + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, + "wireplumber.collection-manager", "=b", TRUE, + NULL); + wp_object_manager_request_object_features (self->metadata_om, + WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL); + g_signal_connect_object (self->metadata_om, "object-added", + G_CALLBACK (on_metadata_added), transition, 0); + wp_core_install_object_manager (core, self->metadata_om); + + wp_info_object (self, "looking for metadata object named %s", + self->metadata_name); +} + +static void +wp_collection_manager_activate_execute_step (WpObject * object, + WpFeatureActivationTransition * transition, guint step, + WpObjectFeatures missing) +{ + WpCollectionManager *self = WP_COLLECTION_MANAGER (object); + g_autoptr (WpCore) core = wp_object_get_core (object); + + switch (step) { + case STEP_LOAD: { + /* Install global proxy object manager */ + g_clear_object (&self->global_proxy_om); + self->global_proxy_om = wp_object_manager_new (); + wp_object_manager_add_interest (self->global_proxy_om, + WP_TYPE_GLOBAL_PROXY, NULL); + wp_object_manager_request_object_features (self->global_proxy_om, + WP_TYPE_GLOBAL_PROXY, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL); + g_signal_connect_object (self->global_proxy_om, "object-removed", + G_CALLBACK (on_global_proxy_removed), self, 0); + g_signal_connect_object (self->global_proxy_om, "installed", + G_CALLBACK (on_global_proxy_om_installed), transition, 0); + wp_core_install_object_manager (core, self->global_proxy_om); + break; + } + case WP_TRANSITION_STEP_ERROR: + break; + default: + g_assert_not_reached (); + } +} + +static void +finish_task (gpointer key, gpointer value, gpointer user_data) +{ + const char *collection_name = key; + GTask *t = G_TASK (value); + g_task_return_new_error (t, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT, + "Deactivated collection manager before collection '%s' was created", + collection_name); +} + +static void +wp_collection_manager_deactivate (WpObject * object, WpObjectFeatures features) +{ + WpCollectionManager *self = WP_COLLECTION_MANAGER (object); + + /* Globals info */ + g_hash_table_remove_all (self->collected_globals); + g_clear_object (&self->global_proxy_om); + + /* Collections info */ + g_hash_table_foreach (self->create_collection_tasks, finish_task, NULL); + g_hash_table_remove_all (self->create_collection_tasks); + g_hash_table_remove_all (self->collections); + + /* Metadata */ + g_weak_ref_set (&self->metadata, NULL); + g_clear_object (&self->metadata_om); + + wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL); +} + +static void +wp_collection_manager_finalize (GObject * object) +{ + WpCollectionManager *self = WP_COLLECTION_MANAGER (object); + + /* Globals info */ + g_clear_pointer (&self->collected_globals, g_hash_table_unref); + g_clear_object (&self->global_proxy_om); + + /* Collections info */ + g_hash_table_foreach (self->create_collection_tasks, finish_task, NULL); + g_clear_pointer (&self->create_collection_tasks, g_hash_table_unref); + g_clear_pointer (&self->collections, g_hash_table_unref); + + /* Metadata */ + g_weak_ref_clear (&self->metadata); + g_clear_object (&self->metadata_om); + + /* Props */ + g_clear_pointer (&self->metadata_name, g_free); + + G_OBJECT_CLASS (wp_collection_manager_parent_class)->finalize (object); +} + +static void +wp_collection_manager_class_init (WpCollectionManagerClass * klass) +{ + GObjectClass * object_class = (GObjectClass *) klass; + WpObjectClass *wpobject_class = (WpObjectClass *) klass; + + object_class->finalize = wp_collection_manager_finalize; + object_class->set_property = wp_collection_manager_set_property; + object_class->get_property = wp_collection_manager_get_property; + + wpobject_class->get_supported_features = + wp_collection_manager_get_supported_features; + wpobject_class->activate_get_next_step = + wp_collection_manager_activate_get_next_step; + wpobject_class->activate_execute_step = + wp_collection_manager_activate_execute_step; + wpobject_class->deactivate = wp_collection_manager_deactivate; + + g_object_class_install_property (object_class, PROP_METADATA_NAME, + g_param_spec_string ("metadata-name", "metadata-name", + "The metadata object to look after", NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + signals[SIGNAL_GLOBAL_COLLECTED] = g_signal_new ( + "global-collected", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 2, WP_TYPE_GLOBAL_PROXY, G_TYPE_STRING); + + signals[SIGNAL_GLOBAL_DROPPED] = g_signal_new ( + "global-dropped", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 2, WP_TYPE_GLOBAL_PROXY, G_TYPE_STRING); +} + +/*! + * \brief Creates a new WpCollectionManager object + * + * \ingroup wpcollectionmanager + * \param core the WpCore + * \param metadata_name (nullable): the name of the metadata object to + * associate with the collection manager object; NULL means the default + * "sm-collection-manager" + * \returns (transfer full): a new WpCollectionManager object + */ +WpCollectionManager * +wp_collection_manager_new (WpCore * core, const gchar * metadata_name) +{ + return g_object_new (WP_TYPE_COLLECTION_MANAGER, + "core", core, + "metadata-name", metadata_name ? metadata_name : "sm-collection-manager", + NULL); +} + +static gboolean +find_collection_manager_func (gpointer g_object, gpointer metadata_name) +{ + if (!WP_IS_COLLECTION_MANAGER (g_object)) + return FALSE; + + if (metadata_name) + return g_str_equal (((WpCollectionManager *) g_object)->metadata_name, + (gchar *) metadata_name); + else + return TRUE; +} + +/*! + * \brief Finds a registered WpCollectionManager object by its metadata name + * + * \ingroup wpcollectionmanager + * \param core the WpCore + * \param metadata_name (nullable): the name of the metadata object that the + * collection manager object is associated with; NULL returns the first + * collection manager object that is found + * \returns (transfer full) (nullable): the WpCollectionManager object, or NULL + * if not found + */ +WpCollectionManager * +wp_collection_manager_find (WpCore * core, const gchar *metadata_name) +{ + g_return_val_if_fail (WP_IS_CORE (core), NULL); + + GObject *cm = wp_core_find_object (core, + (GEqualFunc) find_collection_manager_func, metadata_name); + return cm ? WP_COLLECTION_MANAGER (cm) : NULL; +} + +/*! + * \brief Callback version of wp_collection_manager_create_collection_closure() + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \param collection_name the new collection name to create + * \param cancellable (nullable): a cancellable for the async operation + * \param callback (scope async)(closure user_data): a function to call when + * the collection was created + * \param user_data data for \a callback + */ +void +wp_collection_manager_create_collection (WpCollectionManager *self, + const gchar *collection_name, GCancellable * cancellable, + GAsyncReadyCallback callback, gpointer user_data) +{ + g_return_if_fail (WP_IS_COLLECTION_MANAGER (self)); + + GClosure *closure = g_cclosure_new (G_CALLBACK (callback), user_data, NULL); + + wp_collection_manager_create_collection_closure (self, collection_name, + cancellable, closure); +} + +static void +invoke_closure (GObject * obj, GAsyncResult * res, gpointer data) +{ + GClosure *closure = data; + GValue values[2] = { G_VALUE_INIT, G_VALUE_INIT }; + g_value_init (&values[0], G_TYPE_OBJECT); + g_value_init (&values[1], G_TYPE_OBJECT); + g_value_set_object (&values[0], obj); + g_value_set_object (&values[1], res); + g_closure_invoke (closure, NULL, 2, values, NULL); + g_value_unset (&values[0]); + g_value_unset (&values[1]); + g_closure_unref (closure); +} + +/*! + * \brief Creates a new collection into the manager + * + * \note \a closure may be invoked in sync while this method is being called, + * if the collection already exists. + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \param collection_name the new collection name to create + * \param cancellable (nullable): a cancellable for the async operation + * \param closure (transfer full): the closure to use when the collection was + * created + */ +void +wp_collection_manager_create_collection_closure (WpCollectionManager *self, + const gchar *collection_name, GCancellable * cancellable, GClosure *closure) +{ + g_autoptr (GTask) t = NULL; + g_autoptr (WpMetadata) m = NULL; + WpCollection *c = NULL; + g_autoptr (WpSpaJson) json_value = NULL; + + g_return_if_fail (WP_IS_COLLECTION_MANAGER (self)); + + /* Create the task from the closure */ + if (G_CLOSURE_NEEDS_MARSHAL (closure)) + g_closure_set_marshal (closure, g_cclosure_marshal_VOID__OBJECT); + t = g_task_new (self, cancellable, invoke_closure, closure); + + /* Just return if the collection already exists */ + c = g_hash_table_lookup (self->collections, collection_name); + if (c) { + g_task_return_pointer (t, c, NULL); + return; + } + + /* Make sure the new collection is not being created already */ + if (g_hash_table_contains (self->create_collection_tasks, collection_name)) { + g_task_return_new_error (t, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT, + "Cannot create collection '%s' as another task is already creating it", + collection_name); + return; + } + + /* Get the metadata */ + m = g_weak_ref_get (&self->metadata); + if (!m) { + g_task_return_new_error (t, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT, + "Collection manager metadata not found, cannot create collection '%s'", + collection_name); + return; + } + + /* Request new collection */ + g_hash_table_insert (self->create_collection_tasks, + g_strdup (collection_name), g_object_ref (t)); + json_value = wp_spa_json_new_boolean (TRUE); + wp_metadata_set (m, 0, collection_name, "Spa:String:JSON", + wp_spa_json_get_data (json_value)); +} + +/*! + * \brief Finishes the async operation that was started with + * wp_collection_manager_create_collection() + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \param res the async operation result + * \param error (out) (optional): the error of the operation, if any + * \returns (nullable) (transfer none): The newly created WpCollection, or NULL + * if an error happened. + */ +WpCollection * +wp_collection_manager_create_collection_finish (WpCollectionManager * self, + GAsyncResult * res, GError ** error) +{ + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE); + g_return_val_if_fail (g_task_is_valid (res, self), FALSE); + + return g_task_propagate_pointer (G_TASK (res), error); +} + +/*! + * \brief Destroys a collection from the manager + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \param collection_name the collection name to destroy + * \returns TRUE if the collection was destroyed, FALSE otherwise + */ +gboolean +wp_collection_manager_destroy_collection (WpCollectionManager *self, + const gchar *collection_name) +{ + g_autoptr (WpMetadata) m = NULL; + + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE); + + m = g_weak_ref_get (&self->metadata); + if (!m) + return FALSE; + + g_hash_table_remove (self->collections, collection_name); + wp_metadata_set (m, 0, collection_name, "Spa:String:JSON", NULL); + return TRUE; +} + +/*! + * \brief Checks whether the collection manager has a collection or not + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \param collection_name the collection name to check + * \returns TRUE if the collection exists, FALSE otherwise + */ +gboolean +wp_collection_manager_has_collection (WpCollectionManager *self, + const gchar *collection_name) +{ + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE); + + return g_hash_table_contains (self->collections, collection_name); +} + +/*! + * \brief Get the collection for the given collection name + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \param collection_name the collection name to get + * \returns (nullable) (transfer full): the collection for the given name + */ +WpCollection * +wp_collection_manager_get_collection (WpCollectionManager *self, + const gchar *collection_name) +{ + WpCollection *c = NULL; + + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), NULL); + + c = g_hash_table_lookup (self->collections, collection_name); + return c ? g_object_ref (c) : NULL; +} + +/*! + * \brief Gets the total number of collections the manager has + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \returns the total number of collections the manager has + */ +guint +wp_collection_manager_get_collection_count (WpCollectionManager *self) +{ + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), 0); + + return g_hash_table_size (self->collections); +} + +/*! + * \brief Iterates over all the collections that the manager has + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \returns (transfer full): an iterator that iterates over the collections + */ +WpIterator * +wp_collection_manager_new_collection_iterator (WpCollectionManager *self) +{ + GPtrArray *collections; + GHashTableIter iter; + WpCollection *c; + + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), NULL); + + collections = g_ptr_array_new_with_free_func (g_object_unref); + g_hash_table_iter_init (&iter, self->collections); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&c)) + g_ptr_array_add (collections, g_object_ref (c)); + + return wp_iterator_new_ptr_array (collections, WP_TYPE_COLLECTION); +} + +/*! + * \brief Collects the bound global into a collection. + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \param global (transfer none): the global to collect + * \param collection_name: the name of the collection to collect the global into + * \returns TRUE if the global could be collected, FALSE otherwise + */ +gboolean +wp_collection_manager_collect_global (WpCollectionManager *self, + WpGlobalProxy *global, const gchar *collection_name) +{ + const gchar *cn; + WpCollection *c; + g_autoptr (WpMetadata) c_meta = NULL; + + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE); + g_return_val_if_fail (wp_object_test_active_features ( + WP_OBJECT (global), WP_PROXY_FEATURE_BOUND), FALSE); + g_return_val_if_fail (collection_name, FALSE); + + /* Just return if this global is already collected */ + cn = g_hash_table_lookup (self->collected_globals, global); + if (cn) + return g_str_equal (cn, collection_name); + + /* Get the collection */ + c = g_hash_table_lookup (self->collections, collection_name); + if (!c) + return FALSE; + c_meta = wp_collection_get_metadata (c); + g_return_val_if_fail (c_meta, FALSE); + + /* Make sure we are not collecting a collection into itself */ + if (WP_GLOBAL_PROXY (c_meta) == global) { + wp_warning_object (self, "Cannot collect a collection into itself"); + return FALSE; + } + + if (!wp_collection_collect_global (c, global)) + return FALSE; + + g_hash_table_insert (self->collected_globals, global, + g_strdup (collection_name)); + + g_signal_emit (self, signals[SIGNAL_GLOBAL_COLLECTED], 0, global, + collection_name); + return TRUE; +} + +/*! + * \brief Drops the bound global from its collection + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \param global (transfer none): the global to drop + * \returns TRUE if the global could be dropped, FALSE otherwise + */ +gboolean +wp_collection_manager_drop_global (WpCollectionManager *self, + WpGlobalProxy *global) +{ + g_autofree gchar *cn = NULL; + WpCollection *c; + + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE); + g_return_val_if_fail (wp_object_test_active_features ( + WP_OBJECT (global), WP_PROXY_FEATURE_BOUND), FALSE); + + /* Just return if this global is not collected */ + if (!g_hash_table_steal_extended (self->collected_globals, global, NULL, + (gpointer *)&cn)) + return TRUE; + + /* Get the collection */ + c = g_hash_table_lookup (self->collections, cn); + g_return_val_if_fail (c, FALSE); + + if (!wp_collection_drop_global (c, global)) + return FALSE; + + /* Destroy the collection if it is empty */ + if (wp_collection_get_global_count (c) == 0) + wp_collection_manager_destroy_collection (self, cn); + + g_signal_emit (self, signals[SIGNAL_GLOBAL_DROPPED], 0, global, cn); + return TRUE; +} + +/*! + * \brief Gets the collection that a given global belongs to + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \param global (transfer none): the global to get the collection from + * \returns (nullable) (transfer full): the collection that the given global + * belongs too + */ +WpCollection * +wp_collection_manager_get_global_collection (WpCollectionManager *self, + WpGlobalProxy *global) +{ + const gchar *cn; + + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), NULL); + + cn = g_hash_table_lookup (self->collected_globals, global); + return cn ? wp_collection_manager_get_collection (self, cn) : NULL; +} + +/*! + * \brief Convenience method to checks whether a global is collected into a + * collection or not + * + * \ingroup wpcollectionmanager + * \param self the WpCollectionManager + * \param global (transfer none): the global to check if it is collected or not + * \param collection_name (nullable): the collection name + * \returns TRUE if the global is collected, FALSE otherwise + * belongs too + */ +gboolean +wp_collection_manager_is_global_collected (WpCollectionManager *self, + WpGlobalProxy *global, const gchar *collection_name) +{ + const gchar *cn; + + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE); + + cn = g_hash_table_lookup (self->collected_globals, global); + if (!cn) + return FALSE; + + return collection_name ? g_str_equal (cn, collection_name) : TRUE; +} + +/*! + * \brief Iterates over all globals that a collection has + * + * \ingroup wpcollection + * \param self the WpCollection + * \param collection_name (nullable): the collection name + * \returns (transfer full): an iterator that iterates over the globals. + */ +WpIterator * +wp_collection_manager_new_global_iterator (WpCollectionManager *self, + const gchar *collection_name) +{ + GPtrArray *globals; + GHashTableIter iter; + WpGlobalProxy *g = NULL; + const gchar *cn = NULL; + + g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), NULL); + + globals = g_ptr_array_new_with_free_func (g_object_unref); + g_hash_table_iter_init (&iter, self->collected_globals); + while (g_hash_table_iter_next (&iter, (gpointer *)&g, (gpointer *)&cn)) + if (!collection_name || g_str_equal (cn, collection_name)) + g_ptr_array_add (globals, g_object_ref (g)); + + return wp_iterator_new_ptr_array (globals, WP_TYPE_GLOBAL_PROXY); +} diff --git a/lib/wp/collection-manager.h b/lib/wp/collection-manager.h new file mode 100644 index 00000000..e30294b2 --- /dev/null +++ b/lib/wp/collection-manager.h @@ -0,0 +1,105 @@ +/* WirePlumber + * + * Copyright © 2025 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#ifndef __WIREPLUMBER_COLLECTION_MANAGER_H__ +#define __WIREPLUMBER_COLLECTION_MANAGER_H__ + +#include "object.h" +#include "collection.h" + +G_BEGIN_DECLS + +/*! + * \brief Flags to be used as WpObjectFeatures for WpCollectionManager. + * \ingroup wpcollectionmanager + */ +typedef enum { /*< flags >*/ + /*! Loads the collection manager */ + WP_COLLECTION_MANAGER_LOADED = (1 << 0), +} WpCollectionManagerFeatures; + +/*! + * \brief The WpCollectionManager GType + * \ingroup wpcollectionmanager + */ +#define WP_TYPE_COLLECTION_MANAGER (wp_collection_manager_get_type ()) + +WP_API +G_DECLARE_FINAL_TYPE (WpCollectionManager, wp_collection_manager, WP, + COLLECTION_MANAGER, WpObject) + +WP_API +WpCollectionManager * wp_collection_manager_new (WpCore * core, + const gchar * metadata_name); + +WP_API +WpCollectionManager * wp_collection_manager_find (WpCore * core, + const gchar *metadata_name); + + +/* Collection */ + +WP_API +void wp_collection_manager_create_collection (WpCollectionManager *self, + const gchar *collection_name, GCancellable * cancellable, + GAsyncReadyCallback callback, gpointer user_data); + +WP_API +void wp_collection_manager_create_collection_closure (WpCollectionManager *self, + const gchar *collection_name, GCancellable * cancellable, + GClosure *closure); + +WP_API +WpCollection *wp_collection_manager_create_collection_finish ( + WpCollectionManager * self, GAsyncResult * res, GError ** error); + +WP_API +gboolean wp_collection_manager_destroy_collection (WpCollectionManager *self, + const gchar *collection_name); + +WP_API +gboolean wp_collection_manager_has_collection (WpCollectionManager *self, + const gchar *collection_name); + +WP_API +WpCollection * wp_collection_manager_get_collection ( + WpCollectionManager *self, const gchar *collection_name); + +WP_API +guint wp_collection_manager_get_collection_count (WpCollectionManager *self); + +WP_API +WpIterator * wp_collection_manager_new_collection_iterator ( + WpCollectionManager *self); + + +/* Global */ + +WP_API +gboolean wp_collection_manager_collect_global (WpCollectionManager *self, + WpGlobalProxy *global, const gchar *collection_name); + +WP_API +gboolean wp_collection_manager_drop_global (WpCollectionManager *self, + WpGlobalProxy *global); + +WP_API +WpCollection * wp_collection_manager_get_global_collection ( + WpCollectionManager *self, WpGlobalProxy *global); + +WP_API +gboolean wp_collection_manager_is_global_collected (WpCollectionManager *self, + WpGlobalProxy *global, const gchar *collection_name); + +WP_API +WpIterator * wp_collection_manager_new_global_iterator ( + WpCollectionManager *self, const gchar *collection_name); + +G_END_DECLS + +#endif diff --git a/lib/wp/meson.build b/lib/wp/meson.build index 092a9d13..6c01a515 100644 --- a/lib/wp/meson.build +++ b/lib/wp/meson.build @@ -2,6 +2,7 @@ wp_lib_sources = files( 'base-dirs.c', 'client.c', 'collection.c', + 'collection-manager.c', 'component-loader.c', 'conf.c', 'core.c', @@ -51,6 +52,7 @@ wp_lib_headers = files( 'base-dirs.h', 'client.h', 'collection.h', + 'collection-manager.h', 'component-loader.h', 'conf.h', 'core.h', diff --git a/lib/wp/wp.h b/lib/wp/wp.h index 4169343a..66f09c89 100644 --- a/lib/wp/wp.h +++ b/lib/wp/wp.h @@ -49,6 +49,7 @@ #include "settings.h" #include "permission-manager.h" #include "collection.h" +#include "collection-manager.h" G_BEGIN_DECLS diff --git a/tests/wp/collection-manager.c b/tests/wp/collection-manager.c new file mode 100644 index 00000000..6752297a --- /dev/null +++ b/tests/wp/collection-manager.c @@ -0,0 +1,446 @@ +/* WirePlumber + * + * Copyright © 2025 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include "../common/base-test-fixture.h" + +#define COLLECTION_A_NAME "sm-collection-a" +#define COLLECTION_B_NAME "sm-collection-b" +#define COLLECTION_C_NAME "sm-collection-c" +#define COLLECTION_MANAGER_METADATA_NAME "sm-collection-manager" + +typedef struct { + WpBaseTestFixture base; + WpImplMetadata *collection_a_metadata; + WpImplMetadata *collection_b_metadata; + WpImplMetadata *collection_manager_metadata; + WpCollectionManager *collection_manager; + WpCollection *collection_a; + WpCollection *collection_b; +} TestCollectionManagerFixture; + +static void +on_metadata_activated (WpMetadata * m, GAsyncResult * res, + gpointer d) +{ + TestCollectionManagerFixture *self = d; + g_assert_true (wp_object_activate_finish (WP_OBJECT (m), res, NULL)); + g_main_loop_quit (self->base.loop); +} + +static void +on_collection_manager_metadata_activated (WpMetadata * m, GAsyncResult * res, + gpointer d) +{ + TestCollectionManagerFixture *self = d; + g_assert_true (wp_object_activate_finish (WP_OBJECT (m), res, NULL)); + + /* Add collections A and B */ + wp_metadata_set (m, 0, COLLECTION_A_NAME, "Spa:String:JSON", "1"); + wp_metadata_set (m, 0, COLLECTION_B_NAME, "Spa:String:JSON", "1"); + + g_main_loop_quit (self->base.loop); +} + +static void +on_collection_manager_ready (WpCollectionManager *s, GAsyncResult *res, + gpointer d) +{ + TestCollectionManagerFixture *self = d; + + g_assert_true (wp_object_activate_finish (WP_OBJECT (s), res, NULL)); + + wp_core_register_object (self->base.core, g_object_ref (s)); + + g_main_loop_quit (self->base.loop); +} + +static void +collection_manager_setup (TestCollectionManagerFixture *self, gconstpointer d) +{ + wp_base_test_fixture_setup (&self->base, WP_BASE_TEST_FLAG_CLIENT_CORE); + + /* Create collection A metadata and activate it */ + self->collection_a_metadata = wp_impl_metadata_new_full ( + self->base.core, COLLECTION_A_NAME, + wp_properties_new ("wireplumber.collection", "true", NULL)); + wp_object_activate (WP_OBJECT (self->collection_a_metadata), + WP_OBJECT_FEATURES_ALL, NULL, + (GAsyncReadyCallback)on_metadata_activated, self); + g_main_loop_run (self->base.loop); + + /* Create collection B metadata and activate it */ + self->collection_b_metadata = wp_impl_metadata_new_full ( + self->base.core, COLLECTION_B_NAME, + wp_properties_new ("wireplumber.collection", "true", NULL)); + wp_object_activate (WP_OBJECT (self->collection_b_metadata), + WP_OBJECT_FEATURES_ALL, NULL, + (GAsyncReadyCallback)on_metadata_activated, self); + g_main_loop_run (self->base.loop); + + /* Create collection manager metadata and activate it */ + self->collection_manager_metadata = wp_impl_metadata_new_full ( + self->base.core, COLLECTION_MANAGER_METADATA_NAME, + wp_properties_new ("wireplumber.collection-manager", "true", NULL)); + wp_object_activate (WP_OBJECT (self->collection_manager_metadata), + WP_OBJECT_FEATURES_ALL, NULL, + (GAsyncReadyCallback)on_collection_manager_metadata_activated, self); + g_main_loop_run (self->base.loop); + + /* Create the collection manager and activate it */ + self->collection_manager = wp_collection_manager_new (self->base.core, + COLLECTION_MANAGER_METADATA_NAME); + wp_object_activate (WP_OBJECT (self->collection_manager), + WP_OBJECT_FEATURES_ALL, + NULL, + (GAsyncReadyCallback)on_collection_manager_ready, + self); + g_main_loop_run (self->base.loop); + + /* Get collections A and B */ + self->collection_a = wp_collection_manager_get_collection ( + self->collection_manager, COLLECTION_A_NAME); + g_assert_nonnull (self->collection_a); + self->collection_b = wp_collection_manager_get_collection ( + self->collection_manager, COLLECTION_B_NAME); + g_assert_nonnull (self->collection_b); +} + +static void +collection_manager_teardown (TestCollectionManagerFixture *self, + gconstpointer d) +{ + g_clear_object (&self->collection_b); + g_clear_object (&self->collection_a); + g_clear_object (&self->collection_manager); + g_clear_object (&self->collection_manager_metadata); + g_clear_object (&self->collection_b_metadata); + g_clear_object (&self->collection_a_metadata); + wp_base_test_fixture_teardown (&self->base); +} + +static void +on_collection_created (WpCollectionManager * cm, GAsyncResult * res, gpointer d) +{ + TestCollectionManagerFixture *self = d; + WpCollection *c = wp_collection_manager_create_collection_finish (cm, res, + NULL); + g_assert_nonnull (c); + g_assert_true (WP_IS_COLLECTION (c)); + g_main_loop_quit (self->base.loop); +} + +static void +test_collection_manager_collection (TestCollectionManagerFixture *self, + gconstpointer d) +{ + g_autoptr (WpImplMetadata) collection_c_metadata; + + /* Make sure the manager has 2 collections */ + g_assert_cmpuint (wp_collection_manager_get_collection_count ( + self->collection_manager), ==, 2); + + /* Make sure collections A and B were added */ + g_assert_true (wp_collection_manager_has_collection (self->collection_manager, + COLLECTION_A_NAME)); + g_assert_true (wp_collection_manager_has_collection (self->collection_manager, + COLLECTION_B_NAME)); + + /* Make sure collection C was not added */ + g_assert_false (wp_collection_manager_has_collection (self->collection_manager, + COLLECTION_C_NAME)); + + /* Create collection C metadata and activate it */ + collection_c_metadata = wp_impl_metadata_new_full ( + self->base.core, COLLECTION_C_NAME, + wp_properties_new ("wireplumber.collection", "true", NULL)); + wp_object_activate (WP_OBJECT (collection_c_metadata), + WP_OBJECT_FEATURES_ALL, NULL, + (GAsyncReadyCallback)on_metadata_activated, self); + g_main_loop_run (self->base.loop); + + /* Make sure collection C is still not added */ + g_assert_false (wp_collection_manager_has_collection (self->collection_manager, + COLLECTION_C_NAME)); + + /* Create collection C */ + wp_collection_manager_create_collection (self->collection_manager, + COLLECTION_C_NAME, NULL, (GAsyncReadyCallback)on_collection_created, self); + g_main_loop_run (self->base.loop); + + /* Make sure the manager has 3 collections */ + g_assert_cmpuint (wp_collection_manager_get_collection_count ( + self->collection_manager), ==, 3); + + /* Iterate collections */ + { + g_autoptr (WpIterator) iter = NULL; + g_auto (GValue) val = G_VALUE_INIT; + gboolean has_collection_a = FALSE; + gboolean has_collection_b = FALSE; + gboolean has_collection_c = FALSE; + guint count = 0; + + iter = wp_collection_manager_new_collection_iterator ( + self->collection_manager); + g_assert_nonnull (iter); + + while (wp_iterator_next (iter, &val)) { + WpCollection *c = g_value_get_object (&val); + g_assert_nonnull (c); + if (g_str_equal (wp_collection_get_name (c), COLLECTION_A_NAME)) + has_collection_a = TRUE; + if (g_str_equal (wp_collection_get_name (c), COLLECTION_B_NAME)) + has_collection_b = TRUE; + if (g_str_equal (wp_collection_get_name (c), COLLECTION_C_NAME)) + has_collection_c = TRUE; + count++; + g_value_unset (&val); + } + + g_assert_true (has_collection_a); + g_assert_true (has_collection_b); + g_assert_true (has_collection_c); + g_assert_cmpuint (count, ==, 3); + } + + /* Make sure collection C was added */ + g_assert_true (wp_collection_manager_has_collection (self->collection_manager, + COLLECTION_C_NAME)); + + /* Destroy collection C */ + g_assert_true (wp_collection_manager_destroy_collection ( + self->collection_manager, COLLECTION_C_NAME)); + + /* Make sure the manager has 2 collections */ + g_assert_cmpuint (wp_collection_manager_get_collection_count ( + self->collection_manager), ==, 2); + + /* Make sure collection C was removed */ + g_assert_false (wp_collection_manager_has_collection (self->collection_manager, + COLLECTION_C_NAME)); + + /* Remove collections A and B */ + g_assert_true (wp_collection_manager_destroy_collection ( + self->collection_manager, COLLECTION_A_NAME)); + g_assert_true (wp_collection_manager_destroy_collection ( + self->collection_manager, COLLECTION_B_NAME)); + + /* Make sure the manager does not have any collections */ + g_assert_cmpuint (wp_collection_manager_get_collection_count ( + self->collection_manager), ==, 0); + + /* Make sure collections A and B were removed */ + g_assert_false (wp_collection_manager_has_collection (self->collection_manager, + COLLECTION_A_NAME)); + g_assert_false (wp_collection_manager_has_collection (self->collection_manager, + COLLECTION_B_NAME)); +} + +static void +test_collection_manager_global (TestCollectionManagerFixture *self, + gconstpointer d) +{ + g_autoptr (WpGlobalProxy) global_a = NULL; + g_autoptr (WpGlobalProxy) global_b = NULL; + + /* Activate global A */ + global_a = WP_GLOBAL_PROXY (wp_impl_metadata_new_full (self->base.core, + "global-a", NULL)); + wp_object_activate (WP_OBJECT (global_a), WP_OBJECT_FEATURES_ALL, NULL, + (GAsyncReadyCallback)on_metadata_activated, self); + g_main_loop_run (self->base.loop); + + /* Activate global B */ + global_b = WP_GLOBAL_PROXY (wp_impl_metadata_new_full (self->base.core, + "global-a", NULL)); + wp_object_activate (WP_OBJECT (global_b), WP_OBJECT_FEATURES_ALL, NULL, + (GAsyncReadyCallback)on_metadata_activated, self); + g_main_loop_run (self->base.loop); + + /* Make sure collection A and B don't have any globals */ + g_assert_cmpuint (wp_collection_get_global_count (self->collection_a), ==, 0); + g_assert_cmpuint (wp_collection_get_global_count (self->collection_b), ==, 0); + + /* Make sure global A and B don't belong to any collection */ + g_assert_false (wp_collection_manager_is_global_collected ( + self->collection_manager, global_a, NULL)); + g_assert_false (wp_collection_manager_is_global_collected ( + self->collection_manager, global_b, NULL)); + + /* Collect global A into collection A and global B into collection B */ + g_assert_true (wp_collection_manager_collect_global (self->collection_manager, + global_a, COLLECTION_A_NAME)); + g_assert_true (wp_collection_manager_collect_global (self->collection_manager, + global_b, COLLECTION_B_NAME)); + + /* Make sure collection A and B have only one global */ + g_assert_cmpuint (wp_collection_get_global_count (self->collection_a), ==, 1); + g_assert_cmpuint (wp_collection_get_global_count (self->collection_b), ==, 1); + + /* Iterate collection A */ + { + g_autoptr (WpIterator) iter = NULL; + g_auto (GValue) val = G_VALUE_INIT; + guint count = 0; + + iter = wp_collection_manager_new_global_iterator (self->collection_manager, + COLLECTION_A_NAME); + g_assert_nonnull (iter); + + while (wp_iterator_next (iter, &val)) { + WpGlobalProxy *g = g_value_get_object (&val); + g_assert_nonnull (g); + g_assert_true (g == global_a); + count++; + g_value_unset (&val); + } + + g_assert_cmpuint (count, ==, 1); + } + + /* Iterate collection B */ + { + g_autoptr (WpIterator) iter = NULL; + g_auto (GValue) val = G_VALUE_INIT; + guint count = 0; + + iter = wp_collection_manager_new_global_iterator (self->collection_manager, + COLLECTION_B_NAME); + g_assert_nonnull (iter); + + while (wp_iterator_next (iter, &val)) { + WpGlobalProxy *g = g_value_get_object (&val); + g_assert_nonnull (g); + g_assert_true (g == global_b); + count++; + g_value_unset (&val); + } + + g_assert_cmpuint (count, ==, 1); + } + + /* Make sure global A and global B are in their respective collection */ + g_assert_true (wp_collection_manager_is_global_collected ( + self->collection_manager, global_a, COLLECTION_A_NAME)); + g_assert_true (wp_collection_manager_is_global_collected ( + self->collection_manager, global_b, COLLECTION_B_NAME)); + { + g_autoptr (WpCollection) ca = NULL; + g_autoptr (WpCollection) cb = NULL; + ca = wp_collection_manager_get_global_collection (self->collection_manager, + global_a); + g_assert_true (ca == self->collection_a); + cb = wp_collection_manager_get_global_collection (self->collection_manager, + global_b); + g_assert_true (cb == self->collection_b); + } + + g_assert_true (WP_IS_COLLECTION (self->collection_a)); + + /* Make sure we cannot add global A into collection B and vice versa */ + g_assert_false (wp_collection_manager_collect_global ( + self->collection_manager, global_a, COLLECTION_B_NAME)); + g_assert_false (wp_collection_manager_collect_global ( + self->collection_manager, global_b, COLLECTION_A_NAME)); + + /* Drop global A from collection A */ + g_assert_true (wp_collection_manager_drop_global (self->collection_manager, + global_a)); + + /* Make sure collection A does not have any globals */ + g_assert_cmpuint (wp_collection_get_global_count (self->collection_a), ==, 0); + + /* Make sure global A does not belong to any collection */ + g_assert_false (wp_collection_manager_is_global_collected ( + self->collection_manager, global_a, NULL)); + + /* Make sure collection A was automatically removed */ + g_assert_false (wp_collection_manager_has_collection ( + self->collection_manager, COLLECTION_A_NAME)); + + /* Collect global A into collection B */ + g_assert_true (wp_collection_manager_collect_global (self->collection_manager, + global_a, COLLECTION_B_NAME)); + + /* Make sure global A is in collection B */ + g_assert_true (wp_collection_manager_is_global_collected ( + self->collection_manager, global_a, COLLECTION_B_NAME)); + + /* Make sure collection B has 2 globals */ + g_assert_cmpuint (wp_collection_get_global_count (self->collection_b), ==, 2); + + /* Iterate collection B */ + { + g_autoptr (WpIterator) iter = NULL; + g_auto (GValue) val = G_VALUE_INIT; + gboolean has_global_a = FALSE; + gboolean has_global_b = FALSE; + guint count = 0; + + iter = wp_collection_manager_new_global_iterator (self->collection_manager, + COLLECTION_B_NAME); + g_assert_nonnull (iter); + + while (wp_iterator_next (iter, &val)) { + WpGlobalProxy *g = g_value_get_object (&val); + g_assert_nonnull (g); + if (g == global_a) + has_global_a = TRUE; + if (g == global_b) + has_global_b = TRUE; + count++; + g_value_unset (&val); + } + + g_assert_true (has_global_a); + g_assert_true (has_global_b); + g_assert_cmpuint (count, ==, 2); + } + + /* Drop global B from collection B */ + g_assert_true (wp_collection_manager_drop_global (self->collection_manager, + global_b)); + + /* Make sure collection B has only one global */ + g_assert_cmpuint (wp_collection_get_global_count (self->collection_b), ==, 1); + + /* Make sure global B does not belong to any collection */ + g_assert_false (wp_collection_manager_is_global_collected ( + self->collection_manager, global_b, NULL)); + + /* Drop global A from collection B */ + g_assert_true (wp_collection_manager_drop_global (self->collection_manager, + global_a)); + + /* Make sure collection B does not have any globals */ + g_assert_cmpuint (wp_collection_get_global_count (self->collection_b), ==, 0); + + /* Make sure global A does not belong to any collection */ + g_assert_false (wp_collection_manager_is_global_collected ( + self->collection_manager, global_a, NULL)); + + /* Make sure collection B was automatically removed */ + g_assert_false (wp_collection_manager_has_collection ( + self->collection_manager, COLLECTION_B_NAME)); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + wp_init (WP_INIT_ALL); + + g_test_add ("/wp/collection-manager/collection", + TestCollectionManagerFixture, NULL, collection_manager_setup, + test_collection_manager_collection, collection_manager_teardown); + g_test_add ("/wp/collection-manager/global", + TestCollectionManagerFixture, NULL, collection_manager_setup, + test_collection_manager_global, collection_manager_teardown); + + return g_test_run (); +} diff --git a/tests/wp/meson.build b/tests/wp/meson.build index cc0b9ac8..686d871c 100644 --- a/tests/wp/meson.build +++ b/tests/wp/meson.build @@ -3,6 +3,13 @@ common_env = common_test_env common_env.set('G_TEST_SRCDIR', meson.current_source_dir()) common_env.set('G_TEST_BUILDDIR', meson.current_build_dir()) +test( + 'test-collection-manager', + executable('test-collection-manager', 'collection-manager.c', + dependencies: common_deps), + env: common_env, +) + test( 'test-component-loader', executable('test-component-loader', 'component-loader.c',