From be6ab166b7452d9e6dd9255f195236a8efaa0e62 Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Tue, 21 Jul 2026 09:39:52 +0200 Subject: [PATCH] feat: add asynchronous investigation graph loading --- Makefile | 30 +- include/core/investigation_graph_load_task.h | 176 ++ .../core/investigation_graph_load_task_test.h | 57 + src/core/investigation_graph_load_task.c | 1612 ++++++++++++++ tests/test_investigation_graph_load_task.c | 1878 +++++++++++++++++ 5 files changed, 3751 insertions(+), 2 deletions(-) create mode 100644 include/core/investigation_graph_load_task.h create mode 100644 include/core/investigation_graph_load_task_test.h create mode 100644 src/core/investigation_graph_load_task.c create mode 100644 tests/test_investigation_graph_load_task.c diff --git a/Makefile b/Makefile index 0c2a248..f10f692 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,10 @@ RELATION_SERVICE_TEST_CFLAGS := \ -DRELATION_SERVICE_ENABLE_TEST_HOOKS INVESTIGATION_GRAPH_MODEL_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic INVESTIGATION_GRAPH_LOADER_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic +INVESTIGATION_GRAPH_LOAD_TASK_TEST_CFLAGS := \ + $(TEST_CFLAGS) \ + -Wpedantic \ + -DINVESTIGATION_GRAPH_LOAD_TASK_ENABLE_TEST_HOOKS SRC := $(shell find src -name "*.c") @@ -102,6 +106,7 @@ TEST_RELATION_EVIDENCE_DAO := tests/test_relation_evidence_dao TEST_RELATION_SERVICE := tests/test_relation_service TEST_INVESTIGATION_GRAPH_MODEL := tests/test_investigation_graph_model TEST_INVESTIGATION_GRAPH_LOADER := tests/test_investigation_graph_loader +TEST_INVESTIGATION_GRAPH_LOAD_TASK := tests/test_investigation_graph_load_task all: $(TARGET) @@ -505,6 +510,24 @@ $(TEST_INVESTIGATION_GRAPH_LOADER): \ $(CC) $(INVESTIGATION_GRAPH_LOADER_TEST_CFLAGS) $^ -o $@ \ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \ + tests/test_investigation_graph_load_task.c \ + src/core/investigation_graph_load_task.c \ + src/core/investigation_graph_loader.c \ + src/core/background_task.c \ + src/dao/entity_dao.c \ + src/dao/relation_dao.c \ + src/models/investigation_graph_model.c \ + src/models/entity_record.c \ + src/models/relation_record.c \ + src/database/database.c \ + src/database/schema.c \ + src/database/statement.c \ + src/database/transaction.c \ + src/database/error.c + $(CC) $(INVESTIGATION_GRAPH_LOAD_TASK_TEST_CFLAGS) $^ -o $@ \ + $(TEST_LDFLAGS) -lsqlite3 + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -549,7 +572,8 @@ test: \ $(TEST_RELATION_EVIDENCE_DAO) \ $(TEST_RELATION_SERVICE) \ $(TEST_INVESTIGATION_GRAPH_MODEL) \ - $(TEST_INVESTIGATION_GRAPH_LOADER) + $(TEST_INVESTIGATION_GRAPH_LOADER) \ + $(TEST_INVESTIGATION_GRAPH_LOAD_TASK) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -595,6 +619,7 @@ test: \ @$(TEST_RELATION_SERVICE) @$(TEST_INVESTIGATION_GRAPH_MODEL) @$(TEST_INVESTIGATION_GRAPH_LOADER) + @$(TEST_INVESTIGATION_GRAPH_LOAD_TASK) @echo "Tous les tests sont valides." %.o: %.c @@ -646,6 +671,7 @@ clean: $(TEST_RELATION_EVIDENCE_DAO) \ $(TEST_RELATION_SERVICE) \ $(TEST_INVESTIGATION_GRAPH_MODEL) \ - $(TEST_INVESTIGATION_GRAPH_LOADER) + $(TEST_INVESTIGATION_GRAPH_LOADER) \ + $(TEST_INVESTIGATION_GRAPH_LOAD_TASK) .PHONY: clean run test diff --git a/include/core/investigation_graph_load_task.h b/include/core/investigation_graph_load_task.h new file mode 100644 index 0000000..0695c7d --- /dev/null +++ b/include/core/investigation_graph_load_task.h @@ -0,0 +1,176 @@ +/****************************************************************************** + * @file investigation_graph_load_task.h + * @brief Chargement asynchrone du graphe métier d'une enquête. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LOAD_TASK_H +#define LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LOAD_TASK_H + +#include "models/investigation_graph_model.h" + +#include + +G_BEGIN_DECLS + +/** + * @brief Tâche opaque de chargement asynchrone d'un graphe d'enquête. + * + * Chaque exécution ouvre sa propre connexion SQLite dans le worker. + * + * L'objet peut être réutilisé après la finalisation complète d'une exécution. + */ +typedef struct InvestigationGraphLoadTask InvestigationGraphLoadTask; + +/** + * @brief Catégories d'erreurs de la tâche de chargement. + */ +typedef enum +{ + /** + * Un argument public est invalide. + */ + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INVALID_ARGUMENT, + + /** + * Une allocation mémoire a échoué. + */ + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_MEMORY, + + /** + * Une exécution est déjà active. + */ + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_ALREADY_RUNNING, + + /** + * La connexion SQLite propre au worker n'a pas pu être ouverte. + */ + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_DATABASE_OPEN, + + /** + * Le graphe n'a pas pu être construit depuis SQLite. + */ + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_LOAD, + + /** + * Le chargement a été annulé. + */ + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_CANCELLED, + + /** + * Une incohérence interne empêche la finalisation. + */ + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INTERNAL +} InvestigationGraphLoadTaskError; + +/** + * @brief Domaine d'erreur de la tâche. + */ +#define INVESTIGATION_GRAPH_LOAD_TASK_ERROR \ + investigation_graph_load_task_error_quark() + +/** + * @brief Callback final exécuté sur le contexte principal ayant démarré la + * tâche. + * + * En cas de succès, graph_model appartient au destinataire du callback. + * + * En cas d'échec, error est empruntée et valable uniquement pendant le + * callback. + * + * @param load_task Tâche ayant terminé. + * @param graph_model Graphe transféré, ou NULL. + * @param error Erreur empruntée, ou NULL. + * @param user_data Données fournies au démarrage. + */ +typedef void (*InvestigationGraphLoadTaskCallback)( + InvestigationGraphLoadTask *load_task, + InvestigationGraphModel *graph_model, + const GError *error, + gpointer user_data +); + +/** + * @brief Retourne le domaine d'erreur de la tâche. + */ +GQuark investigation_graph_load_task_error_quark(void); + +/** + * @brief Crée une tâche associée à un chemin SQLite. + * + * Le chemin est copié. La base n'est pas ouverte par le constructeur. + * + * @param database_path Chemin non vide de la base SQLite. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouvelle tâche, ou NULL en cas d'échec. + */ +InvestigationGraphLoadTask *investigation_graph_load_task_new( + const char *database_path, + GError **error +); + +/** + * @brief Libère la référence publique de la tâche. + * + * Si une exécution est active, elle est annulée et la destruction réelle est + * différée jusqu'à la fin du worker. Aucun callback utilisateur n'est alors + * invoqué. + * + * Cette fonction accepte NULL. + * + * @param load_task Tâche à libérer. + */ +void investigation_graph_load_task_free( + InvestigationGraphLoadTask *load_task +); + +/** + * @brief Démarre le chargement en arrière-plan. + * + * En cas de succès, la tâche prend possession de user_data et appellera + * user_data_destroy exactement une fois après le callback, ou lors de la + * destruction anticipée de la tâche. + * + * En cas d'échec immédiat, l'appelant conserve la propriété de user_data. + * + * @param load_task Tâche valide et inactive. + * @param callback Callback final obligatoire. + * @param user_data Données facultatives du callback. + * @param user_data_destroy Destructeur facultatif de user_data. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si l'exécution a été lancée. + */ +gboolean investigation_graph_load_task_start( + InvestigationGraphLoadTask *load_task, + InvestigationGraphLoadTaskCallback callback, + gpointer user_data, + GDestroyNotify user_data_destroy, + GError **error +); + +/** + * @brief Demande l'annulation coopérative de l'exécution active. + * + * Cette fonction accepte NULL et peut être appelée plusieurs fois. + * + * @param load_task Tâche concernée. + */ +void investigation_graph_load_task_cancel( + InvestigationGraphLoadTask *load_task +); + +/** + * @brief Indique si une exécution est active. + * + * @param load_task Tâche à consulter. + * + * @return TRUE uniquement entre le démarrage accepté et la finalisation. + */ +gboolean investigation_graph_load_task_is_running( + const InvestigationGraphLoadTask *load_task +); + +G_END_DECLS + +#endif diff --git a/include/core/investigation_graph_load_task_test.h b/include/core/investigation_graph_load_task_test.h new file mode 100644 index 0000000..4040ecd --- /dev/null +++ b/include/core/investigation_graph_load_task_test.h @@ -0,0 +1,57 @@ +/****************************************************************************** + * @file investigation_graph_load_task_test.h + * @brief Hooks déterministes réservés aux tests du chargement asynchrone. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LOAD_TASK_TEST_H +#define LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LOAD_TASK_TEST_H + +#include + +G_BEGIN_DECLS + +/** + * @brief Réinitialise tous les hooks. + * + * À appeler uniquement lorsqu'aucun worker n'est encore suspendu. + */ +void investigation_graph_load_task_test_reset(void); + +/** + * @brief Configure une suspension avant l'ouverture SQLite. + */ +void investigation_graph_load_task_test_pause_before_open(void); + +/** + * @brief Attend que le worker atteigne la suspension avant ouverture. + */ +void investigation_graph_load_task_test_wait_until_before_open(void); + +/** + * @brief Libère le worker suspendu avant ouverture. + */ +void investigation_graph_load_task_test_release_before_open(void); + +/** + * @brief Configure une suspension après l'ouverture SQLite. + */ +void investigation_graph_load_task_test_pause_after_open(void); + +/** + * @brief Attend que le worker atteigne la suspension après ouverture. + */ +void investigation_graph_load_task_test_wait_until_after_open(void); + +/** + * @brief Libère le worker suspendu après ouverture. + */ +void investigation_graph_load_task_test_release_after_open(void); + +/** + * @brief Retourne le nombre de finalisations observées depuis le dernier reset. + */ +guint investigation_graph_load_task_test_get_completion_count(void); + +G_END_DECLS + +#endif diff --git a/src/core/investigation_graph_load_task.c b/src/core/investigation_graph_load_task.c new file mode 100644 index 0000000..7981cf0 --- /dev/null +++ b/src/core/investigation_graph_load_task.c @@ -0,0 +1,1612 @@ +/****************************************************************************** + * @file investigation_graph_load_task.c + * @brief Chargement asynchrone du graphe métier d'une enquête. + ******************************************************************************/ + +#include "core/investigation_graph_load_task.h" + +#include "core/background_task.h" +#include "core/investigation_graph_loader.h" + +#include "database/database.h" + +#include +#include + +#ifdef INVESTIGATION_GRAPH_LOAD_TASK_ENABLE_TEST_HOOKS +#include "core/investigation_graph_load_task_test.h" +#endif + +/** + * @brief Données privées copiées pour une exécution du worker. + */ +typedef struct +{ + char *database_path; +} InvestigationGraphLoadWorkerData; + +/** + * @brief Résultat possédé par BackgroundTask jusqu'au callback final. + * + * Le callback retire graph_model de ce conteneur avant de le transmettre à + * l'appelant. Le destructeur du conteneur ne détruit donc que les graphes qui + * n'ont pas été remis. + */ +typedef struct +{ + InvestigationGraphModel *graph_model; +} InvestigationGraphLoadResult; + +/** + * @brief Contexte maintenant la tâche publique en vie jusqu'à la fin. + */ +typedef struct +{ + InvestigationGraphLoadTask *load_task; +} InvestigationGraphLoadCompletionContext; + +/** + * @brief État privé de la tâche de chargement. + */ +struct InvestigationGraphLoadTask +{ + gatomicrefcount reference_count; + GMutex mutex; + + char *database_path; + + BackgroundTask *background_task; + + InvestigationGraphLoadTaskCallback callback; + gpointer user_data; + GDestroyNotify user_data_destroy; + + gboolean running; + gboolean disposed; +}; + + +#ifdef INVESTIGATION_GRAPH_LOAD_TASK_ENABLE_TEST_HOOKS + +/** + * @brief État global des synchronisations déterministes réservées aux tests. + */ +typedef struct +{ + GMutex mutex; + GCond condition; + + gboolean pause_before_open; + gboolean reached_before_open; + gboolean release_before_open; + + gboolean pause_after_open; + gboolean reached_after_open; + gboolean release_after_open; + + guint completion_count; +} InvestigationGraphLoadTaskTestHooks; + +static InvestigationGraphLoadTaskTestHooks +investigation_graph_load_task_test_hooks; + +static gsize investigation_graph_load_task_test_hooks_initialized = + 0; + +/** + * @brief Initialise une seule fois les primitives des hooks de test. + */ +static void investigation_graph_load_task_test_ensure_initialized(void) +{ + if (g_once_init_enter( + &investigation_graph_load_task_test_hooks_initialized + )) + { + g_mutex_init( + &investigation_graph_load_task_test_hooks.mutex + ); + + g_cond_init( + &investigation_graph_load_task_test_hooks.condition + ); + + g_once_init_leave( + &investigation_graph_load_task_test_hooks_initialized, + 1 + ); + } +} + +/** + * @brief Suspend éventuellement le worker avant l'ouverture SQLite. + */ +static void investigation_graph_load_task_test_wait_before_open_hook(void) +{ + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + investigation_graph_load_task_test_hooks.reached_before_open = + TRUE; + + g_cond_broadcast( + &investigation_graph_load_task_test_hooks.condition + ); + + while (investigation_graph_load_task_test_hooks.pause_before_open && + !investigation_graph_load_task_test_hooks.release_before_open) + { + g_cond_wait( + &investigation_graph_load_task_test_hooks.condition, + &investigation_graph_load_task_test_hooks.mutex + ); + } + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); +} + +/** + * @brief Suspend éventuellement le worker après l'ouverture SQLite. + */ +static void investigation_graph_load_task_test_wait_after_open_hook(void) +{ + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + investigation_graph_load_task_test_hooks.reached_after_open = + TRUE; + + g_cond_broadcast( + &investigation_graph_load_task_test_hooks.condition + ); + + while (investigation_graph_load_task_test_hooks.pause_after_open && + !investigation_graph_load_task_test_hooks.release_after_open) + { + g_cond_wait( + &investigation_graph_load_task_test_hooks.condition, + &investigation_graph_load_task_test_hooks.mutex + ); + } + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); +} + +/** + * @brief Signale qu'une finalisation BackgroundTask a été exécutée. + */ +static void investigation_graph_load_task_test_mark_completion(void) +{ + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + investigation_graph_load_task_test_hooks.completion_count++; + + g_cond_broadcast( + &investigation_graph_load_task_test_hooks.condition + ); + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); +} + +void investigation_graph_load_task_test_reset(void) +{ + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + investigation_graph_load_task_test_hooks.pause_before_open = + FALSE; + + investigation_graph_load_task_test_hooks.reached_before_open = + FALSE; + + investigation_graph_load_task_test_hooks.release_before_open = + FALSE; + + investigation_graph_load_task_test_hooks.pause_after_open = + FALSE; + + investigation_graph_load_task_test_hooks.reached_after_open = + FALSE; + + investigation_graph_load_task_test_hooks.release_after_open = + FALSE; + + investigation_graph_load_task_test_hooks.completion_count = + 0; + + g_cond_broadcast( + &investigation_graph_load_task_test_hooks.condition + ); + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); +} + +void investigation_graph_load_task_test_pause_before_open(void) +{ + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + investigation_graph_load_task_test_hooks.pause_before_open = + TRUE; + + investigation_graph_load_task_test_hooks.reached_before_open = + FALSE; + + investigation_graph_load_task_test_hooks.release_before_open = + FALSE; + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); +} + +void investigation_graph_load_task_test_wait_until_before_open(void) +{ + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + while (!investigation_graph_load_task_test_hooks.reached_before_open) + { + g_cond_wait( + &investigation_graph_load_task_test_hooks.condition, + &investigation_graph_load_task_test_hooks.mutex + ); + } + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); +} + +void investigation_graph_load_task_test_release_before_open(void) +{ + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + investigation_graph_load_task_test_hooks.release_before_open = + TRUE; + + g_cond_broadcast( + &investigation_graph_load_task_test_hooks.condition + ); + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); +} + +void investigation_graph_load_task_test_pause_after_open(void) +{ + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + investigation_graph_load_task_test_hooks.pause_after_open = + TRUE; + + investigation_graph_load_task_test_hooks.reached_after_open = + FALSE; + + investigation_graph_load_task_test_hooks.release_after_open = + FALSE; + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); +} + +void investigation_graph_load_task_test_wait_until_after_open(void) +{ + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + while (!investigation_graph_load_task_test_hooks.reached_after_open) + { + g_cond_wait( + &investigation_graph_load_task_test_hooks.condition, + &investigation_graph_load_task_test_hooks.mutex + ); + } + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); +} + +void investigation_graph_load_task_test_release_after_open(void) +{ + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + investigation_graph_load_task_test_hooks.release_after_open = + TRUE; + + g_cond_broadcast( + &investigation_graph_load_task_test_hooks.condition + ); + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); +} + +guint investigation_graph_load_task_test_get_completion_count(void) +{ + guint completion_count = + 0; + + investigation_graph_load_task_test_ensure_initialized(); + + g_mutex_lock( + &investigation_graph_load_task_test_hooks.mutex + ); + + completion_count = + investigation_graph_load_task_test_hooks.completion_count; + + g_mutex_unlock( + &investigation_graph_load_task_test_hooks.mutex + ); + + return completion_count; +} + +#endif + +/** + * @brief Enregistre une erreur littérale. + */ +static void investigation_graph_load_task_set_error_literal( + GError **error, + InvestigationGraphLoadTaskError error_code, + const char *message +) +{ + if (error == NULL || + *error != NULL) + { + return; + } + + g_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR, + error_code, + message + ); +} + +/** + * @brief Enregistre une erreur en conservant le message d'origine. + */ +static void investigation_graph_load_task_set_nested_error( + GError **error, + InvestigationGraphLoadTaskError error_code, + const char *context, + const GError *nested_error +) +{ + if (error == NULL || + *error != NULL) + { + return; + } + + g_set_error( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR, + error_code, + "%s : %s", + context, + nested_error != NULL + ? nested_error->message + : "erreur inconnue" + ); +} + +/** + * @brief Libère les données privées d'un worker. + */ +static void investigation_graph_load_worker_data_free( + gpointer user_data +) +{ + InvestigationGraphLoadWorkerData *worker_data = + user_data; + + if (worker_data == NULL) + { + return; + } + + g_free( + worker_data->database_path + ); + + g_free( + worker_data + ); +} + +/** + * @brief Libère un résultat qui n'a pas été transféré au callback. + */ +static void investigation_graph_load_result_free( + gpointer user_data +) +{ + InvestigationGraphLoadResult *load_result = + user_data; + + if (load_result == NULL) + { + return; + } + + investigation_graph_model_free( + load_result->graph_model + ); + + load_result->graph_model = + NULL; + + g_free( + load_result + ); +} + +/** + * @brief Ajoute une référence interne à la tâche publique. + */ +static InvestigationGraphLoadTask * +investigation_graph_load_task_ref_internal( + InvestigationGraphLoadTask *load_task +) +{ + if (load_task == NULL) + { + return NULL; + } + + g_atomic_ref_count_inc( + &load_task->reference_count + ); + + return load_task; +} + +/** + * @brief Détruit définitivement une tâche dont la dernière référence disparaît. + */ +static void investigation_graph_load_task_destroy( + InvestigationGraphLoadTask *load_task +) +{ + BackgroundTask *background_task = + NULL; + + gpointer user_data = + NULL; + + GDestroyNotify user_data_destroy = + NULL; + + if (load_task == NULL) + { + return; + } + + background_task = + load_task->background_task; + + user_data = + load_task->user_data; + + user_data_destroy = + load_task->user_data_destroy; + + load_task->background_task = + NULL; + + load_task->callback = + NULL; + + load_task->user_data = + NULL; + + load_task->user_data_destroy = + NULL; + + if (background_task != NULL) + { + background_task_cancel( + background_task + ); + + background_task_unref( + background_task + ); + } + + if (user_data != NULL && + user_data_destroy != NULL) + { + user_data_destroy( + user_data + ); + } + + g_free( + load_task->database_path + ); + + load_task->database_path = + NULL; + + g_mutex_clear( + &load_task->mutex + ); + + g_free( + load_task + ); +} + +/** + * @brief Libère une référence interne ou publique. + */ +static void investigation_graph_load_task_unref_internal( + InvestigationGraphLoadTask *load_task +) +{ + if (load_task == NULL) + { + return; + } + + if (!g_atomic_ref_count_dec( + &load_task->reference_count + )) + { + return; + } + + investigation_graph_load_task_destroy( + load_task + ); +} + +/** + * @brief Libère le contexte de finalisation. + */ +static void investigation_graph_load_completion_context_free( + gpointer user_data +) +{ + InvestigationGraphLoadCompletionContext *completion_context = + user_data; + + if (completion_context == NULL) + { + return; + } + + investigation_graph_load_task_unref_internal( + completion_context->load_task + ); + + completion_context->load_task = + NULL; + + g_free( + completion_context + ); +} + +/** + * @brief Vérifie l'annulation du worker. + */ +static gboolean investigation_graph_load_task_check_cancelled( + GCancellable *cancellable, + GError **error +) +{ + if (cancellable == NULL) + { + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INTERNAL, + "L'objet d'annulation du chargement est absent." + ); + + return TRUE; + } + + return g_cancellable_set_error_if_cancelled( + cancellable, + error + ); +} + +/** + * @brief Exécute le chargement dans un thread secondaire. + */ +static gboolean investigation_graph_load_task_worker( + BackgroundTask *background_task, + GCancellable *cancellable, + gpointer worker_data_pointer, + gpointer *result, + GError **error +) +{ + InvestigationGraphLoadWorkerData *worker_data = + worker_data_pointer; + + Database *database = + NULL; + + InvestigationGraphLoader *graph_loader = + NULL; + + InvestigationGraphModel *graph_model = + NULL; + + InvestigationGraphLoadResult *load_result = + NULL; + + GError *component_error = + NULL; + + if (background_task == NULL || + cancellable == NULL || + worker_data == NULL || + worker_data->database_path == NULL || + result == NULL || + error == NULL) + { + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INTERNAL, + "Le contexte du worker de chargement est invalide." + ); + + return FALSE; + } + + *result = + NULL; + + if (investigation_graph_load_task_check_cancelled( + cancellable, + error + )) + { + return FALSE; + } + +#ifdef INVESTIGATION_GRAPH_LOAD_TASK_ENABLE_TEST_HOOKS + investigation_graph_load_task_test_wait_before_open_hook(); +#endif + + if (investigation_graph_load_task_check_cancelled( + cancellable, + error + )) + { + return FALSE; + } + + background_task_report_progress( + background_task, + 0.05, + "Ouverture de la base de données" + ); + + database = + database_open( + worker_data->database_path + ); + + if (database == NULL) + { + g_set_error( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_DATABASE_OPEN, + "Impossible d'ouvrir la base SQLite : %s", + worker_data->database_path + ); + + return FALSE; + } + +#ifdef INVESTIGATION_GRAPH_LOAD_TASK_ENABLE_TEST_HOOKS + investigation_graph_load_task_test_wait_after_open_hook(); +#endif + + if (investigation_graph_load_task_check_cancelled( + cancellable, + error + )) + { + goto failure; + } + + background_task_report_progress( + background_task, + 0.20, + "Préparation du chargeur de graphe" + ); + + graph_loader = + investigation_graph_loader_new( + database, + &component_error + ); + + if (graph_loader == NULL) + { + investigation_graph_load_task_set_nested_error( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_LOAD, + "Impossible de préparer le chargement du graphe", + component_error + ); + + goto failure; + } + + g_clear_error( + &component_error + ); + + background_task_report_progress( + background_task, + 0.35, + "Chargement des entités et des relations" + ); + + graph_model = + investigation_graph_loader_load( + graph_loader, + &component_error + ); + + if (graph_model == NULL) + { + investigation_graph_load_task_set_nested_error( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_LOAD, + "Impossible de charger le graphe d'enquête", + component_error + ); + + goto failure; + } + + g_clear_error( + &component_error + ); + + if (investigation_graph_load_task_check_cancelled( + cancellable, + error + )) + { + goto failure; + } + + investigation_graph_loader_free( + graph_loader + ); + + graph_loader = + NULL; + + database_close( + database + ); + + database = + NULL; + + if (investigation_graph_load_task_check_cancelled( + cancellable, + error + )) + { + goto failure; + } + + load_result = + g_try_new0( + InvestigationGraphLoadResult, + 1 + ); + + if (load_result == NULL) + { + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_MEMORY, + "Impossible d'allouer le résultat du chargement." + ); + + goto failure; + } + + load_result->graph_model = + graph_model; + + graph_model = + NULL; + + if (investigation_graph_load_task_check_cancelled( + cancellable, + error + )) + { + investigation_graph_load_result_free( + load_result + ); + + return FALSE; + } + + background_task_report_progress( + background_task, + 1.0, + "Graphe d'enquête chargé" + ); + + *result = + load_result; + + return TRUE; + +failure: + + g_clear_error( + &component_error + ); + + investigation_graph_model_free( + graph_model + ); + + investigation_graph_loader_free( + graph_loader + ); + + if (database != NULL) + { + database_close( + database + ); + } + + return FALSE; +} + +/** + * @brief Produit l'erreur publique correspondant à l'état final. + */ +static GError *investigation_graph_load_task_create_callback_error( + BackgroundTask *background_task +) +{ + BackgroundTaskState background_state; + + GError *background_error = + NULL; + + background_state = + background_task_get_state( + background_task + ); + + if (background_state == + BACKGROUND_TASK_STATE_CANCELLED) + { + return g_error_new_literal( + INVESTIGATION_GRAPH_LOAD_TASK_ERROR, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_CANCELLED, + "Le chargement du graphe d'enquête a été annulé." + ); + } + + background_error = + background_task_dup_error( + background_task + ); + + if (background_error == NULL) + { + return g_error_new_literal( + INVESTIGATION_GRAPH_LOAD_TASK_ERROR, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INTERNAL, + "La tâche a échoué sans fournir d'erreur." + ); + } + + if (background_error->domain == + INVESTIGATION_GRAPH_LOAD_TASK_ERROR) + { + return background_error; + } + + { + GError *callback_error = + g_error_new( + INVESTIGATION_GRAPH_LOAD_TASK_ERROR, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INTERNAL, + "Échec interne de la tâche de fond : %s", + background_error->message + ); + + g_error_free( + background_error + ); + + return callback_error; + } +} + +/** + * @brief Finalise une exécution sur le contexte ayant lancé BackgroundTask. + */ +static void investigation_graph_load_task_on_completed( + BackgroundTask *background_task, + gpointer user_data +) +{ + InvestigationGraphLoadCompletionContext *completion_context = + user_data; + + InvestigationGraphLoadTask *load_task = + NULL; + + InvestigationGraphLoadTaskCallback callback = + NULL; + + gpointer callback_data = + NULL; + + GDestroyNotify callback_data_destroy = + NULL; + + InvestigationGraphModel *graph_model = + NULL; + + GError *callback_error = + NULL; + + gboolean disposed = + FALSE; + + if (background_task == NULL || + completion_context == NULL || + completion_context->load_task == NULL) + { + return; + } + + load_task = + completion_context->load_task; + + g_mutex_lock( + &load_task->mutex + ); + + disposed = + load_task->disposed; + + if (load_task->background_task == + background_task) + { + load_task->background_task = + NULL; + + load_task->running = + FALSE; + } + + if (!disposed) + { + callback = + load_task->callback; + + callback_data = + load_task->user_data; + + callback_data_destroy = + load_task->user_data_destroy; + } + + load_task->callback = + NULL; + + load_task->user_data = + NULL; + + load_task->user_data_destroy = + NULL; + + g_mutex_unlock( + &load_task->mutex + ); + + if (callback != NULL) + { + if (background_task_get_state( + background_task + ) == BACKGROUND_TASK_STATE_COMPLETED) + { + InvestigationGraphLoadResult *load_result = + background_task_get_result( + background_task + ); + + if (load_result != NULL && + load_result->graph_model != NULL) + { + graph_model = + load_result->graph_model; + + load_result->graph_model = + NULL; + } + else + { + callback_error = + g_error_new_literal( + INVESTIGATION_GRAPH_LOAD_TASK_ERROR, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INTERNAL, + "La tâche terminée ne contient aucun graphe." + ); + } + } + else + { + callback_error = + investigation_graph_load_task_create_callback_error( + background_task + ); + } + + callback( + load_task, + graph_model, + callback_error, + callback_data + ); + } + + if (callback_data != NULL && + callback_data_destroy != NULL) + { + callback_data_destroy( + callback_data + ); + } + + g_clear_error( + &callback_error + ); + + /* + * Libère la référence à BackgroundTask possédée par le wrapper. + * La référence interne de BackgroundTask protège encore l'appel courant. + */ + background_task_unref( + background_task + ); + +#ifdef INVESTIGATION_GRAPH_LOAD_TASK_ENABLE_TEST_HOOKS + investigation_graph_load_task_test_mark_completion(); +#endif +} + +GQuark investigation_graph_load_task_error_quark(void) +{ + return g_quark_from_static_string( + "investigation-graph-load-task-error-quark" + ); +} + +InvestigationGraphLoadTask *investigation_graph_load_task_new( + const char *database_path, + GError **error +) +{ + InvestigationGraphLoadTask *load_task = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (database_path == NULL || + database_path[0] == '\0') + { + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INVALID_ARGUMENT, + "Le chemin de la base SQLite est obligatoire." + ); + + return NULL; + } + + load_task = + g_try_new0( + InvestigationGraphLoadTask, + 1 + ); + + if (load_task == NULL) + { + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_MEMORY, + "Impossible d'allouer la tâche de chargement." + ); + + return NULL; + } + + load_task->database_path = + g_strdup( + database_path + ); + + if (load_task->database_path == NULL) + { + g_free( + load_task + ); + + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_MEMORY, + "Impossible de copier le chemin de la base SQLite." + ); + + return NULL; + } + + g_atomic_ref_count_init( + &load_task->reference_count + ); + + g_mutex_init( + &load_task->mutex + ); + + load_task->running = + FALSE; + + load_task->disposed = + FALSE; + + return load_task; +} + +void investigation_graph_load_task_free( + InvestigationGraphLoadTask *load_task +) +{ + BackgroundTask *background_task = + NULL; + + gpointer user_data = + NULL; + + GDestroyNotify user_data_destroy = + NULL; + + if (load_task == NULL) + { + return; + } + + g_mutex_lock( + &load_task->mutex + ); + + load_task->disposed = + TRUE; + + user_data = + load_task->user_data; + + user_data_destroy = + load_task->user_data_destroy; + + load_task->callback = + NULL; + + load_task->user_data = + NULL; + + load_task->user_data_destroy = + NULL; + + if (load_task->running && + load_task->background_task != NULL) + { + background_task = + background_task_ref( + load_task->background_task + ); + } + + g_mutex_unlock( + &load_task->mutex + ); + + if (user_data != NULL && + user_data_destroy != NULL) + { + user_data_destroy( + user_data + ); + } + + if (background_task != NULL) + { + background_task_cancel( + background_task + ); + + background_task_unref( + background_task + ); + } + + investigation_graph_load_task_unref_internal( + load_task + ); +} + +gboolean investigation_graph_load_task_start( + InvestigationGraphLoadTask *load_task, + InvestigationGraphLoadTaskCallback callback, + gpointer user_data, + GDestroyNotify user_data_destroy, + GError **error +) +{ + InvestigationGraphLoadWorkerData *worker_data = + NULL; + + InvestigationGraphLoadCompletionContext *completion_context = + NULL; + + BackgroundTask *background_task = + NULL; + + GError *background_error = + NULL; + + gboolean started = + FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (load_task == NULL || + callback == NULL) + { + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INVALID_ARGUMENT, + "La tâche ou son callback final est invalide." + ); + + return FALSE; + } + + worker_data = + g_try_new0( + InvestigationGraphLoadWorkerData, + 1 + ); + + completion_context = + g_try_new0( + InvestigationGraphLoadCompletionContext, + 1 + ); + + background_task = + background_task_new( + "Charger le graphe d'enquête" + ); + + if (worker_data == NULL || + completion_context == NULL || + background_task == NULL) + { + investigation_graph_load_worker_data_free( + worker_data + ); + + g_free( + completion_context + ); + + background_task_unref( + background_task + ); + + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_MEMORY, + "Impossible de préparer l'exécution asynchrone." + ); + + return FALSE; + } + + worker_data->database_path = + g_strdup( + load_task->database_path + ); + + if (worker_data->database_path == NULL) + { + investigation_graph_load_worker_data_free( + worker_data + ); + + g_free( + completion_context + ); + + background_task_unref( + background_task + ); + + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_MEMORY, + "Impossible de copier le chemin SQLite pour le worker." + ); + + return FALSE; + } + + completion_context->load_task = + investigation_graph_load_task_ref_internal( + load_task + ); + + g_mutex_lock( + &load_task->mutex + ); + + if (load_task->disposed) + { + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INVALID_ARGUMENT, + "La tâche de chargement a déjà été libérée." + ); + + goto start_failure_locked; + } + + if (load_task->running) + { + investigation_graph_load_task_set_error_literal( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_ALREADY_RUNNING, + "Un chargement du graphe est déjà actif." + ); + + goto start_failure_locked; + } + + load_task->background_task = + background_task; + + load_task->callback = + callback; + + load_task->user_data = + user_data; + + load_task->user_data_destroy = + user_data_destroy; + + load_task->running = + TRUE; + + started = + background_task_start( + background_task, + investigation_graph_load_task_worker, + worker_data, + investigation_graph_load_worker_data_free, + investigation_graph_load_result_free, + investigation_graph_load_task_on_completed, + completion_context, + investigation_graph_load_completion_context_free, + &background_error + ); + + if (!started) + { + load_task->background_task = + NULL; + + load_task->callback = + NULL; + + load_task->user_data = + NULL; + + load_task->user_data_destroy = + NULL; + + load_task->running = + FALSE; + + investigation_graph_load_task_set_nested_error( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INTERNAL, + "Impossible de démarrer BackgroundTask", + background_error + ); + + goto start_failure_locked; + } + + g_mutex_unlock( + &load_task->mutex + ); + + g_clear_error( + &background_error + ); + + return TRUE; + +start_failure_locked: + + g_mutex_unlock( + &load_task->mutex + ); + + g_clear_error( + &background_error + ); + + investigation_graph_load_worker_data_free( + worker_data + ); + + investigation_graph_load_completion_context_free( + completion_context + ); + + background_task_unref( + background_task + ); + + return FALSE; +} + +void investigation_graph_load_task_cancel( + InvestigationGraphLoadTask *load_task +) +{ + BackgroundTask *background_task = + NULL; + + if (load_task == NULL) + { + return; + } + + g_mutex_lock( + &load_task->mutex + ); + + if (load_task->running && + load_task->background_task != NULL) + { + background_task = + background_task_ref( + load_task->background_task + ); + } + + g_mutex_unlock( + &load_task->mutex + ); + + if (background_task != NULL) + { + background_task_cancel( + background_task + ); + + background_task_unref( + background_task + ); + } +} + +gboolean investigation_graph_load_task_is_running( + const InvestigationGraphLoadTask *load_task +) +{ + InvestigationGraphLoadTask *mutable_load_task = + NULL; + + gboolean running = + FALSE; + + if (load_task == NULL) + { + return FALSE; + } + + mutable_load_task = + (InvestigationGraphLoadTask *) load_task; + + g_mutex_lock( + &mutable_load_task->mutex + ); + + running = + mutable_load_task->running; + + g_mutex_unlock( + &mutable_load_task->mutex + ); + + return running; +} diff --git a/tests/test_investigation_graph_load_task.c b/tests/test_investigation_graph_load_task.c new file mode 100644 index 0000000..080a587 --- /dev/null +++ b/tests/test_investigation_graph_load_task.c @@ -0,0 +1,1878 @@ +/****************************************************************************** + * @file test_investigation_graph_load_task.c + * @brief Tests du chargement asynchrone du graphe d'enquête. + ******************************************************************************/ + +#include "core/investigation_graph_load_task.h" +#include "core/investigation_graph_load_task_test.h" + +#include "dao/entity_dao.h" +#include "dao/relation_dao.h" + +#include "database/database.h" + +#include "models/entity_record.h" +#include "models/investigation_graph_model.h" +#include "models/relation_record.h" + +#include +#include +#include + +#include +#include + +#define TEST_ENTITY_A_IDENTIFIER \ + "10000000-0000-4000-8000-000000000001" + +#define TEST_ENTITY_B_IDENTIFIER \ + "20000000-0000-4000-8000-000000000001" + +#define TEST_ENTITY_C_IDENTIFIER \ + "30000000-0000-4000-8000-000000000001" + +#define TEST_RELATION_AB_IDENTIFIER \ + "40000000-0000-4000-8000-000000000001" + +#define TEST_RELATION_AC_IDENTIFIER \ + "40000000-0000-4000-8000-000000000002" + +#define TEST_RELATION_CA_IDENTIFIER \ + "40000000-0000-4000-8000-000000000003" + +/** + * @brief Base SQLite temporaire utilisée par les tests. + */ +typedef struct +{ + char *temporary_directory; + char *database_path; + + Database *database; + EntityDao *entity_dao; + RelationDao *relation_dao; +} TestGraphLoadTaskFixture; + +/** + * @brief Résultats observés depuis le callback asynchrone. + */ +typedef struct +{ + GMainLoop *main_loop; + GThread *main_thread; + + guint callback_count; + guint destroy_count; + + gboolean timed_out; + + InvestigationGraphModel *graph_model; + GError *error; +} TestGraphLoadTaskObservation; + +/** + * @brief Données possédées par une exécution asynchrone. + */ +typedef struct +{ + TestGraphLoadTaskObservation *observation; +} TestGraphLoadTaskCallbackData; + +/** + * @brief Vérifie le domaine et le code d'une erreur publique. + */ +static void test_graph_load_task_assert_error( + const GError *error, + InvestigationGraphLoadTaskError expected_code +) +{ + assert(error != NULL); + + assert( + error->domain == + INVESTIGATION_GRAPH_LOAD_TASK_ERROR + ); + + assert( + error->code == + (gint) expected_code + ); + + assert(error->message != NULL); + assert(error->message[0] != '\0'); +} + +/** + * @brief Supprime un fichier temporaire s'il existe. + */ +static void test_graph_load_task_remove_optional_file( + const char *path +) +{ + if (path == NULL) + { + return; + } + + if (g_file_test( + path, + G_FILE_TEST_EXISTS + )) + { + assert( + g_remove( + path + ) == 0 + ); + } +} + +/** + * @brief Crée une fixture SQLite réelle. + */ +static TestGraphLoadTaskFixture +test_graph_load_task_fixture_create(void) +{ + TestGraphLoadTaskFixture fixture = + {0}; + + GError *error = + NULL; + + fixture.temporary_directory = + g_dir_make_tmp( + "labfy-graph-load-task-test-XXXXXX", + &error + ); + + assert(fixture.temporary_directory != NULL); + assert(error == NULL); + + fixture.database_path = + g_build_filename( + fixture.temporary_directory, + "Enquete.sqlite", + NULL + ); + + assert(fixture.database_path != NULL); + + assert( + database_initialize( + fixture.database_path, + "Enquete_GraphLoadTask", + fixture.temporary_directory + ) + ); + + fixture.database = + database_open( + fixture.database_path + ); + + assert(fixture.database != NULL); + + fixture.entity_dao = + entity_dao_new( + fixture.database, + &error + ); + + assert(fixture.entity_dao != NULL); + assert(error == NULL); + + fixture.relation_dao = + relation_dao_new( + fixture.database, + &error + ); + + assert(fixture.relation_dao != NULL); + assert(error == NULL); + + return fixture; +} + +/** + * @brief Ferme les objets d'écriture avant le démarrage du worker. + */ +static void test_graph_load_task_fixture_close_database( + TestGraphLoadTaskFixture *fixture +) +{ + assert(fixture != NULL); + + if (fixture->relation_dao != NULL) + { + relation_dao_free( + fixture->relation_dao + ); + + fixture->relation_dao = + NULL; + } + + if (fixture->entity_dao != NULL) + { + entity_dao_free( + fixture->entity_dao + ); + + fixture->entity_dao = + NULL; + } + + if (fixture->database != NULL) + { + database_close( + fixture->database + ); + + fixture->database = + NULL; + } +} + +/** + * @brief Détruit la fixture et ses éventuels fichiers SQLite auxiliaires. + */ +static void test_graph_load_task_fixture_clear( + TestGraphLoadTaskFixture *fixture +) +{ + char *write_ahead_log_path = + NULL; + + char *shared_memory_path = + NULL; + + char *journal_path = + NULL; + + assert(fixture != NULL); + + test_graph_load_task_fixture_close_database( + fixture + ); + + write_ahead_log_path = + g_strconcat( + fixture->database_path, + "-wal", + NULL + ); + + shared_memory_path = + g_strconcat( + fixture->database_path, + "-shm", + NULL + ); + + journal_path = + g_strconcat( + fixture->database_path, + "-journal", + NULL + ); + + test_graph_load_task_remove_optional_file( + write_ahead_log_path + ); + + test_graph_load_task_remove_optional_file( + shared_memory_path + ); + + test_graph_load_task_remove_optional_file( + journal_path + ); + + test_graph_load_task_remove_optional_file( + fixture->database_path + ); + + assert( + g_rmdir( + fixture->temporary_directory + ) == 0 + ); + + g_free( + journal_path + ); + + g_free( + shared_memory_path + ); + + g_free( + write_ahead_log_path + ); + + g_free( + fixture->database_path + ); + + g_free( + fixture->temporary_directory + ); + + memset( + fixture, + 0, + sizeof(*fixture) + ); +} + +/** + * @brief Crée une entité valide. + */ +static EntityRecord *test_graph_load_task_create_entity( + const char *identifier, + const char *value +) +{ + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + entity_record = + entity_record_new( + identifier, + "person", + value, + NULL, + NULL, + 80, + "2026-07-21T22:00:00Z", + "2026-07-21T22:00:00Z", + ENTITY_STATUS_ACTIVE, + &error + ); + + assert(entity_record != NULL); + assert(error == NULL); + + return entity_record; +} + +/** + * @brief Insère une entité avec le DAO réel. + */ +static void test_graph_load_task_insert_entity( + TestGraphLoadTaskFixture *fixture, + const char *identifier, + const char *value +) +{ + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + assert(fixture != NULL); + assert(fixture->entity_dao != NULL); + + entity_record = + test_graph_load_task_create_entity( + identifier, + value + ); + + assert( + entity_dao_insert( + fixture->entity_dao, + entity_record, + &error + ) + ); + + assert(error == NULL); + + entity_record_free( + entity_record + ); +} + +/** + * @brief Crée une relation valide. + */ +static RelationRecord *test_graph_load_task_create_relation( + const char *identifier, + const char *source_identifier, + const char *target_identifier, + const char *relation_type +) +{ + RelationRecord *relation_record = + NULL; + + GError *error = + NULL; + + relation_record = + relation_record_new( + identifier, + source_identifier, + target_identifier, + relation_type, + NULL, + "Relation asynchrone de test.", + 75, + "2026-07-21T22:10:00Z", + "2026-07-21T22:10:00Z", + RELATION_STATUS_ACTIVE, + &error + ); + + assert(relation_record != NULL); + assert(error == NULL); + + return relation_record; +} + +/** + * @brief Insère une relation avec le DAO réel. + */ +static void test_graph_load_task_insert_relation( + TestGraphLoadTaskFixture *fixture, + const char *identifier, + const char *source_identifier, + const char *target_identifier, + const char *relation_type +) +{ + RelationRecord *relation_record = + NULL; + + GError *error = + NULL; + + assert(fixture != NULL); + assert(fixture->relation_dao != NULL); + + relation_record = + test_graph_load_task_create_relation( + identifier, + source_identifier, + target_identifier, + relation_type + ); + + assert( + relation_dao_insert( + fixture->relation_dao, + relation_record, + &error + ) + ); + + assert(error == NULL); + + relation_record_free( + relation_record + ); +} + +/** + * @brief Insère le graphe standard utilisé par plusieurs tests. + */ +static void test_graph_load_task_insert_standard_graph( + TestGraphLoadTaskFixture *fixture +) +{ + test_graph_load_task_insert_entity( + fixture, + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ); + + test_graph_load_task_insert_entity( + fixture, + TEST_ENTITY_B_IDENTIFIER, + "Personne B" + ); + + test_graph_load_task_insert_entity( + fixture, + TEST_ENTITY_C_IDENTIFIER, + "Personne C" + ); + + test_graph_load_task_insert_relation( + fixture, + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + test_graph_load_task_insert_relation( + fixture, + TEST_RELATION_AC_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "controls" + ); + + test_graph_load_task_insert_relation( + fixture, + TEST_RELATION_CA_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + "knows" + ); +} + +/** + * @brief Initialise une observation asynchrone. + */ +static TestGraphLoadTaskObservation +test_graph_load_task_observation_create(void) +{ + TestGraphLoadTaskObservation observation = + {0}; + + observation.main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + assert(observation.main_loop != NULL); + + observation.main_thread = + g_thread_self(); + + return observation; +} + +/** + * @brief Libère le résultat d'une observation. + */ +static void test_graph_load_task_observation_clear( + TestGraphLoadTaskObservation *observation +) +{ + assert(observation != NULL); + + investigation_graph_model_free( + observation->graph_model + ); + + g_clear_error( + &observation->error + ); + + g_main_loop_unref( + observation->main_loop + ); + + memset( + observation, + 0, + sizeof(*observation) + ); +} + +/** + * @brief Copie le résultat final reçu sur le thread principal. + */ +static void test_graph_load_task_callback( + InvestigationGraphLoadTask *load_task, + InvestigationGraphModel *graph_model, + const GError *error, + gpointer user_data +) +{ + TestGraphLoadTaskCallbackData *callback_data = + user_data; + + TestGraphLoadTaskObservation *observation = + NULL; + + assert(load_task != NULL); + assert(callback_data != NULL); + assert(callback_data->observation != NULL); + + observation = + callback_data->observation; + + assert( + g_thread_self() == + observation->main_thread + ); + + observation->callback_count++; + + assert( + observation->callback_count == + 1 + ); + + assert( + (graph_model != NULL && error == NULL) || + (graph_model == NULL && error != NULL) + ); + + observation->graph_model = + graph_model; + + if (error != NULL) + { + observation->error = + g_error_copy( + error + ); + } + + g_main_loop_quit( + observation->main_loop + ); +} + +/** + * @brief Vérifie la destruction unique des données utilisateur. + */ +static void test_graph_load_task_callback_data_free( + gpointer user_data +) +{ + TestGraphLoadTaskCallbackData *callback_data = + user_data; + + assert(callback_data != NULL); + assert(callback_data->observation != NULL); + + callback_data->observation->destroy_count++; + + g_free( + callback_data + ); +} + +/** + * @brief Démarre une tâche avec une observation possédée. + */ +static void test_graph_load_task_start_or_abort( + InvestigationGraphLoadTask *load_task, + TestGraphLoadTaskObservation *observation +) +{ + TestGraphLoadTaskCallbackData *callback_data = + NULL; + + GError *error = + NULL; + + callback_data = + g_new0( + TestGraphLoadTaskCallbackData, + 1 + ); + + callback_data->observation = + observation; + + assert( + investigation_graph_load_task_start( + load_task, + test_graph_load_task_callback, + callback_data, + test_graph_load_task_callback_data_free, + &error + ) + ); + + assert(error == NULL); +} + +/** + * @brief Source de sécurité arrêtant une attente anormalement longue. + */ +static gboolean test_graph_load_task_timeout( + gpointer user_data +) +{ + TestGraphLoadTaskObservation *observation = + user_data; + + assert(observation != NULL); + + observation->timed_out = + TRUE; + + g_main_loop_quit( + observation->main_loop + ); + + return G_SOURCE_REMOVE; +} + +/** + * @brief Attend le callback final avec un timeout uniquement défensif. + */ +static void test_graph_load_task_wait_for_callback( + TestGraphLoadTaskObservation *observation +) +{ + guint timeout_source = + 0; + + assert(observation != NULL); + + timeout_source = + g_timeout_add_seconds( + 5, + test_graph_load_task_timeout, + observation + ); + + assert(timeout_source != 0); + + if (observation->callback_count == 0) + { + g_main_loop_run( + observation->main_loop + ); + } + + if (!observation->timed_out) + { + assert( + g_source_remove( + timeout_source + ) + ); + } + + assert(!observation->timed_out); + assert(observation->callback_count == 1); + assert(observation->destroy_count == 1); +} + +/** + * @brief Exécute une source idle pour vérifier la réactivité du contexte. + */ +static gboolean test_graph_load_task_mark_idle( + gpointer user_data +) +{ + gboolean *idle_executed = + user_data; + + assert(idle_executed != NULL); + + *idle_executed = + TRUE; + + return G_SOURCE_REMOVE; +} + +/** + * @brief Attend la finalisation interne d'une tâche détruite. + */ +static void test_graph_load_task_wait_for_internal_completion(void) +{ + gint64 deadline = + g_get_monotonic_time() + + (5 * G_TIME_SPAN_SECOND); + + while (investigation_graph_load_task_test_get_completion_count() == 0 && + g_get_monotonic_time() < deadline) + { + g_main_context_iteration( + NULL, + TRUE + ); + } + + assert( + investigation_graph_load_task_test_get_completion_count() == + 1 + ); +} + +/** + * @brief Vérifie la construction, les arguments et l'état initial. + */ +static void test_graph_load_task_lifecycle(void) +{ + InvestigationGraphLoadTask *load_task = + NULL; + + GError *error = + NULL; + + load_task = + investigation_graph_load_task_new( + NULL, + &error + ); + + assert(load_task == NULL); + + test_graph_load_task_assert_error( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + load_task = + investigation_graph_load_task_new( + "", + NULL + ); + + assert(load_task == NULL); + + load_task = + investigation_graph_load_task_new( + "/tmp/labfy-unused.sqlite", + &error + ); + + assert(load_task != NULL); + assert(error == NULL); + + assert( + !investigation_graph_load_task_is_running( + load_task + ) + ); + + assert( + !investigation_graph_load_task_is_running( + NULL + ) + ); + + assert( + !investigation_graph_load_task_start( + load_task, + NULL, + NULL, + NULL, + &error + ) + ); + + test_graph_load_task_assert_error( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + investigation_graph_load_task_cancel( + NULL + ); + + investigation_graph_load_task_free( + load_task + ); + + investigation_graph_load_task_free( + NULL + ); +} + +/** + * @brief Vérifie une base vide, l'état actif et l'absence de blocage. + */ +static void test_graph_load_task_empty_database(void) +{ + TestGraphLoadTaskFixture fixture = + test_graph_load_task_fixture_create(); + + InvestigationGraphLoadTask *load_task = + NULL; + + TestGraphLoadTaskObservation observation = + test_graph_load_task_observation_create(); + + gboolean idle_executed = + FALSE; + + GError *error = + NULL; + + test_graph_load_task_fixture_close_database( + &fixture + ); + + investigation_graph_load_task_test_reset(); + investigation_graph_load_task_test_pause_before_open(); + + load_task = + investigation_graph_load_task_new( + fixture.database_path, + &error + ); + + assert(load_task != NULL); + assert(error == NULL); + + test_graph_load_task_start_or_abort( + load_task, + &observation + ); + + investigation_graph_load_task_test_wait_until_before_open(); + + assert( + investigation_graph_load_task_is_running( + load_task + ) + ); + + assert( + g_idle_add( + test_graph_load_task_mark_idle, + &idle_executed + ) != 0 + ); + + while (!idle_executed) + { + g_main_context_iteration( + NULL, + TRUE + ); + } + + assert(idle_executed); + + assert( + !investigation_graph_load_task_start( + load_task, + test_graph_load_task_callback, + NULL, + NULL, + &error + ) + ); + + test_graph_load_task_assert_error( + error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_ALREADY_RUNNING + ); + + g_clear_error( + &error + ); + + investigation_graph_load_task_test_release_before_open(); + + test_graph_load_task_wait_for_callback( + &observation + ); + + assert(observation.error == NULL); + assert(observation.graph_model != NULL); + + assert( + investigation_graph_model_get_entity_count( + observation.graph_model + ) == 0 + ); + + assert( + investigation_graph_model_get_relation_count( + observation.graph_model + ) == 0 + ); + + assert( + !investigation_graph_load_task_is_running( + load_task + ) + ); + + investigation_graph_load_task_free( + load_task + ); + + test_graph_load_task_observation_clear( + &observation + ); + + test_graph_load_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le chargement complet du graphe et de ses index. + */ +static void test_graph_load_task_complete_graph(void) +{ + TestGraphLoadTaskFixture fixture = + test_graph_load_task_fixture_create(); + + InvestigationGraphLoadTask *load_task = + NULL; + + TestGraphLoadTaskObservation observation = + test_graph_load_task_observation_create(); + + const RelationRecord *relation_ab = + NULL; + + GPtrArray *outgoing_relations = + NULL; + + GPtrArray *incoming_relations = + NULL; + + GError *error = + NULL; + + test_graph_load_task_insert_standard_graph( + &fixture + ); + + test_graph_load_task_fixture_close_database( + &fixture + ); + + investigation_graph_load_task_test_reset(); + + load_task = + investigation_graph_load_task_new( + fixture.database_path, + &error + ); + + assert(load_task != NULL); + assert(error == NULL); + + test_graph_load_task_start_or_abort( + load_task, + &observation + ); + + test_graph_load_task_wait_for_callback( + &observation + ); + + assert(observation.error == NULL); + assert(observation.graph_model != NULL); + + assert( + investigation_graph_model_get_entity_count( + observation.graph_model + ) == 3 + ); + + assert( + investigation_graph_model_get_relation_count( + observation.graph_model + ) == 3 + ); + + relation_ab = + investigation_graph_model_find_relation( + observation.graph_model, + TEST_RELATION_AB_IDENTIFIER + ); + + assert(relation_ab != NULL); + + assert( + strcmp( + relation_record_get_source_entity_identifier( + relation_ab + ), + TEST_ENTITY_A_IDENTIFIER + ) == 0 + ); + + assert( + strcmp( + relation_record_get_target_entity_identifier( + relation_ab + ), + TEST_ENTITY_B_IDENTIFIER + ) == 0 + ); + + outgoing_relations = + investigation_graph_model_list_outgoing_relations( + observation.graph_model, + TEST_ENTITY_A_IDENTIFIER, + &error + ); + + assert(outgoing_relations != NULL); + assert(error == NULL); + assert(outgoing_relations->len == 2); + + incoming_relations = + investigation_graph_model_list_incoming_relations( + observation.graph_model, + TEST_ENTITY_A_IDENTIFIER, + &error + ); + + assert(incoming_relations != NULL); + assert(error == NULL); + assert(incoming_relations->len == 1); + + g_ptr_array_unref( + incoming_relations + ); + + g_ptr_array_unref( + outgoing_relations + ); + + investigation_graph_load_task_free( + load_task + ); + + test_graph_load_task_observation_clear( + &observation + ); + + test_graph_load_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l'annulation avant l'ouverture SQLite. + */ +static void test_graph_load_task_cancel_before_open(void) +{ + TestGraphLoadTaskFixture fixture = + test_graph_load_task_fixture_create(); + + InvestigationGraphLoadTask *load_task = + NULL; + + TestGraphLoadTaskObservation observation = + test_graph_load_task_observation_create(); + + GError *error = + NULL; + + test_graph_load_task_fixture_close_database( + &fixture + ); + + investigation_graph_load_task_test_reset(); + investigation_graph_load_task_test_pause_before_open(); + + load_task = + investigation_graph_load_task_new( + fixture.database_path, + &error + ); + + assert(load_task != NULL); + assert(error == NULL); + + test_graph_load_task_start_or_abort( + load_task, + &observation + ); + + investigation_graph_load_task_test_wait_until_before_open(); + + investigation_graph_load_task_cancel( + load_task + ); + + investigation_graph_load_task_cancel( + load_task + ); + + investigation_graph_load_task_test_release_before_open(); + + test_graph_load_task_wait_for_callback( + &observation + ); + + assert(observation.graph_model == NULL); + + test_graph_load_task_assert_error( + observation.error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_CANCELLED + ); + + investigation_graph_load_task_free( + load_task + ); + + test_graph_load_task_observation_clear( + &observation + ); + + test_graph_load_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l'annulation après l'ouverture de la connexion du worker. + */ +static void test_graph_load_task_cancel_after_open(void) +{ + TestGraphLoadTaskFixture fixture = + test_graph_load_task_fixture_create(); + + InvestigationGraphLoadTask *load_task = + NULL; + + TestGraphLoadTaskObservation observation = + test_graph_load_task_observation_create(); + + GError *error = + NULL; + + test_graph_load_task_fixture_close_database( + &fixture + ); + + investigation_graph_load_task_test_reset(); + investigation_graph_load_task_test_pause_after_open(); + + load_task = + investigation_graph_load_task_new( + fixture.database_path, + &error + ); + + assert(load_task != NULL); + assert(error == NULL); + + test_graph_load_task_start_or_abort( + load_task, + &observation + ); + + investigation_graph_load_task_test_wait_until_after_open(); + + investigation_graph_load_task_cancel( + load_task + ); + + investigation_graph_load_task_test_release_after_open(); + + test_graph_load_task_wait_for_callback( + &observation + ); + + assert(observation.graph_model == NULL); + + test_graph_load_task_assert_error( + observation.error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_CANCELLED + ); + + investigation_graph_load_task_free( + load_task + ); + + test_graph_load_task_observation_clear( + &observation + ); + + test_graph_load_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'une destruction active supprime le callback utilisateur. + */ +static void test_graph_load_task_free_while_running(void) +{ + TestGraphLoadTaskFixture fixture = + test_graph_load_task_fixture_create(); + + InvestigationGraphLoadTask *load_task = + NULL; + + TestGraphLoadTaskObservation observation = + test_graph_load_task_observation_create(); + + GError *error = + NULL; + + test_graph_load_task_fixture_close_database( + &fixture + ); + + investigation_graph_load_task_test_reset(); + investigation_graph_load_task_test_pause_before_open(); + + load_task = + investigation_graph_load_task_new( + fixture.database_path, + &error + ); + + assert(load_task != NULL); + assert(error == NULL); + + test_graph_load_task_start_or_abort( + load_task, + &observation + ); + + investigation_graph_load_task_test_wait_until_before_open(); + + investigation_graph_load_task_free( + load_task + ); + + load_task = + NULL; + + assert(observation.callback_count == 0); + assert(observation.destroy_count == 1); + + investigation_graph_load_task_test_release_before_open(); + + test_graph_load_task_wait_for_internal_completion(); + + assert(observation.callback_count == 0); + assert(observation.destroy_count == 1); + assert(observation.graph_model == NULL); + assert(observation.error == NULL); + + test_graph_load_task_observation_clear( + &observation + ); + + test_graph_load_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la propagation d'un échec d'ouverture SQLite. + */ +static void test_graph_load_task_database_open_failure(void) +{ + char *temporary_directory = + NULL; + + char *missing_directory = + NULL; + + char *database_path = + NULL; + + InvestigationGraphLoadTask *load_task = + NULL; + + TestGraphLoadTaskObservation observation = + test_graph_load_task_observation_create(); + + GError *error = + NULL; + + temporary_directory = + g_dir_make_tmp( + "labfy-graph-open-failure-test-XXXXXX", + &error + ); + + assert(temporary_directory != NULL); + assert(error == NULL); + + missing_directory = + g_build_filename( + temporary_directory, + "repertoire-absent", + NULL + ); + + database_path = + g_build_filename( + missing_directory, + "Enquete.sqlite", + NULL + ); + + investigation_graph_load_task_test_reset(); + + load_task = + investigation_graph_load_task_new( + database_path, + &error + ); + + assert(load_task != NULL); + assert(error == NULL); + + test_graph_load_task_start_or_abort( + load_task, + &observation + ); + + test_graph_load_task_wait_for_callback( + &observation + ); + + assert(observation.graph_model == NULL); + + test_graph_load_task_assert_error( + observation.error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_DATABASE_OPEN + ); + + assert( + strstr( + observation.error->message, + database_path + ) != NULL + ); + + investigation_graph_load_task_free( + load_task + ); + + test_graph_load_task_observation_clear( + &observation + ); + + assert( + g_rmdir( + temporary_directory + ) == 0 + ); + + g_free( + database_path + ); + + g_free( + missing_directory + ); + + g_free( + temporary_directory + ); +} + +/** + * @brief Vérifie la propagation d'un échec provenant du chargeur synchrone. + */ +static void test_graph_load_task_load_failure(void) +{ + char *temporary_directory = + NULL; + + char *database_path = + NULL; + + InvestigationGraphLoadTask *load_task = + NULL; + + TestGraphLoadTaskObservation observation = + test_graph_load_task_observation_create(); + + GError *error = + NULL; + + temporary_directory = + g_dir_make_tmp( + "labfy-graph-load-failure-test-XXXXXX", + &error + ); + + assert(temporary_directory != NULL); + assert(error == NULL); + + database_path = + g_build_filename( + temporary_directory, + "SansSchema.sqlite", + NULL + ); + + assert( + g_file_set_contents( + database_path, + "", + 0, + &error + ) + ); + + assert(error == NULL); + + investigation_graph_load_task_test_reset(); + + load_task = + investigation_graph_load_task_new( + database_path, + &error + ); + + assert(load_task != NULL); + assert(error == NULL); + + test_graph_load_task_start_or_abort( + load_task, + &observation + ); + + test_graph_load_task_wait_for_callback( + &observation + ); + + assert(observation.graph_model == NULL); + + test_graph_load_task_assert_error( + observation.error, + INVESTIGATION_GRAPH_LOAD_TASK_ERROR_LOAD + ); + + assert( + strstr( + observation.error->message, + "graphe" + ) != NULL + ); + + investigation_graph_load_task_free( + load_task + ); + + test_graph_load_task_observation_clear( + &observation + ); + + test_graph_load_task_remove_optional_file( + database_path + ); + + assert( + g_rmdir( + temporary_directory + ) == 0 + ); + + g_free( + database_path + ); + + g_free( + temporary_directory + ); +} + +/** + * @brief Vérifie la réutilisation après une première finalisation. + */ +static void test_graph_load_task_reuse(void) +{ + TestGraphLoadTaskFixture fixture = + test_graph_load_task_fixture_create(); + + InvestigationGraphLoadTask *load_task = + NULL; + + TestGraphLoadTaskObservation first_observation = + test_graph_load_task_observation_create(); + + TestGraphLoadTaskObservation second_observation = + test_graph_load_task_observation_create(); + + GError *error = + NULL; + + test_graph_load_task_insert_entity( + &fixture, + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ); + + test_graph_load_task_fixture_close_database( + &fixture + ); + + investigation_graph_load_task_test_reset(); + + load_task = + investigation_graph_load_task_new( + fixture.database_path, + &error + ); + + assert(load_task != NULL); + assert(error == NULL); + + test_graph_load_task_start_or_abort( + load_task, + &first_observation + ); + + test_graph_load_task_wait_for_callback( + &first_observation + ); + + assert(first_observation.graph_model != NULL); + assert(first_observation.error == NULL); + + assert( + !investigation_graph_load_task_is_running( + load_task + ) + ); + + test_graph_load_task_start_or_abort( + load_task, + &second_observation + ); + + test_graph_load_task_wait_for_callback( + &second_observation + ); + + assert(second_observation.graph_model != NULL); + assert(second_observation.error == NULL); + + assert( + first_observation.graph_model != + second_observation.graph_model + ); + + assert( + investigation_graph_model_find_entity( + first_observation.graph_model, + TEST_ENTITY_A_IDENTIFIER + ) != + investigation_graph_model_find_entity( + second_observation.graph_model, + TEST_ENTITY_A_IDENTIFIER + ) + ); + + investigation_graph_load_task_free( + load_task + ); + + test_graph_load_task_observation_clear( + &second_observation + ); + + test_graph_load_task_observation_clear( + &first_observation + ); + + test_graph_load_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie deux tâches simultanées et deux connexions indépendantes. + */ +static void test_graph_load_task_two_simultaneous_tasks(void) +{ + TestGraphLoadTaskFixture fixture = + test_graph_load_task_fixture_create(); + + InvestigationGraphLoadTask *first_task = + NULL; + + InvestigationGraphLoadTask *second_task = + NULL; + + TestGraphLoadTaskObservation first_observation = + {0}; + + TestGraphLoadTaskObservation second_observation = + {0}; + + GMainLoop *shared_main_loop = + NULL; + + guint timeout_source = + 0; + + GError *error = + NULL; + + test_graph_load_task_insert_standard_graph( + &fixture + ); + + test_graph_load_task_fixture_close_database( + &fixture + ); + + shared_main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + assert(shared_main_loop != NULL); + + first_observation.main_loop = + shared_main_loop; + + first_observation.main_thread = + g_thread_self(); + + second_observation.main_loop = + shared_main_loop; + + second_observation.main_thread = + g_thread_self(); + + investigation_graph_load_task_test_reset(); + + first_task = + investigation_graph_load_task_new( + fixture.database_path, + &error + ); + + assert(first_task != NULL); + assert(error == NULL); + + second_task = + investigation_graph_load_task_new( + fixture.database_path, + &error + ); + + assert(second_task != NULL); + assert(error == NULL); + + test_graph_load_task_start_or_abort( + first_task, + &first_observation + ); + + test_graph_load_task_start_or_abort( + second_task, + &second_observation + ); + + timeout_source = + g_timeout_add_seconds( + 5, + test_graph_load_task_timeout, + &first_observation + ); + + assert(timeout_source != 0); + + while (first_observation.callback_count == 0 || + second_observation.callback_count == 0) + { + g_main_loop_run( + shared_main_loop + ); + + assert(!first_observation.timed_out); + } + + assert( + g_source_remove( + timeout_source + ) + ); + + assert(first_observation.destroy_count == 1); + assert(second_observation.destroy_count == 1); + + assert(first_observation.error == NULL); + assert(second_observation.error == NULL); + + assert(first_observation.graph_model != NULL); + assert(second_observation.graph_model != NULL); + + assert( + first_observation.graph_model != + second_observation.graph_model + ); + + investigation_graph_load_task_free( + second_task + ); + + investigation_graph_load_task_free( + first_task + ); + + investigation_graph_model_free( + second_observation.graph_model + ); + + investigation_graph_model_free( + first_observation.graph_model + ); + + g_clear_error( + &second_observation.error + ); + + g_clear_error( + &first_observation.error + ); + + g_main_loop_unref( + shared_main_loop + ); + + test_graph_load_task_fixture_clear( + &fixture + ); +} + +int main( + int argc, + char **argv +) +{ + g_test_init( + &argc, + &argv, + NULL + ); + + /* + * Certains scénarios provoquent volontairement des avertissements de la + * couche SQLite. Ils ne doivent pas masquer les erreurs structurées + * vérifiées par les tests. + */ + g_log_set_always_fatal( + G_LOG_FATAL_MASK + ); + + test_graph_load_task_lifecycle(); + test_graph_load_task_empty_database(); + test_graph_load_task_complete_graph(); + test_graph_load_task_cancel_before_open(); + test_graph_load_task_cancel_after_open(); + test_graph_load_task_free_while_running(); + test_graph_load_task_database_open_failure(); + test_graph_load_task_load_failure(); + test_graph_load_task_reuse(); + test_graph_load_task_two_simultaneous_tasks(); + + printf( + "InvestigationGraphLoadTask : tous les tests sont valides.\n" + ); + + return 0; +}