From 2a8ed1003476260b4e1cc93627e88feb3530eef6 Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Fri, 17 Jul 2026 10:55:40 +0200 Subject: [PATCH] feat(core): add asynchronous background task --- Makefile | 13 +- docs/tickets/closed/TICKET-034.md | 967 +++++++++++++++++++ include/core/background_task.h | 289 ++++++ labfy-investigation | Bin 195160 -> 218256 bytes src/core/background_task.c | 1072 +++++++++++++++++++++ tests/test_background_task | Bin 0 -> 46768 bytes tests/test_background_task.c | 1469 +++++++++++++++++++++++++++++ 7 files changed, 3808 insertions(+), 2 deletions(-) create mode 100644 docs/tickets/closed/TICKET-034.md create mode 100644 include/core/background_task.h create mode 100644 src/core/background_task.c create mode 100755 tests/test_background_task create mode 100644 tests/test_background_task.c diff --git a/Makefile b/Makefile index 1dad8b8..b352b95 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,7 @@ TEST_ERROR = tests/test_error TEST_INVESTIGATION_RECORD = tests/test_investigation_record TEST_INVESTIGATION_DAO := tests/test_investigation_dao TEST_INVESTIGATION_SESSION := tests/test_investigation_session +TEST_BACKGROUND_TASK := tests/test_background_task all: $(TARGET) @@ -137,6 +138,11 @@ $(TEST_INVESTIGATION_SESSION): \ src/database/error.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_BACKGROUND_TASK): \ + tests/test_background_task.c \ + src/core/background_task.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -148,7 +154,8 @@ test: \ $(TEST_ERROR) \ $(TEST_INVESTIGATION_RECORD) \ $(TEST_INVESTIGATION_DAO) \ - $(TEST_INVESTIGATION_SESSION) + $(TEST_INVESTIGATION_SESSION) \ + $(TEST_BACKGROUND_TASK) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -161,6 +168,7 @@ test: \ @$(TEST_INVESTIGATION_RECORD) @$(TEST_INVESTIGATION_DAO) @$(TEST_INVESTIGATION_SESSION) + @$(TEST_BACKGROUND_TASK) @echo "Tous les tests sont valides." %.o: %.c @@ -181,6 +189,7 @@ clean: $(TEST_ERROR) \ $(TEST_INVESTIGATION_RECORD) \ $(TEST_INVESTIGATION_DAO) \ - $(TEST_INVESTIGATION_SESSION) + $(TEST_INVESTIGATION_SESSION) \ + $(TEST_BACKGROUND_TASK) .PHONY: clean run test diff --git a/docs/tickets/closed/TICKET-034.md b/docs/tickets/closed/TICKET-034.md new file mode 100644 index 0000000..bec3796 --- /dev/null +++ b/docs/tickets/closed/TICKET-034.md @@ -0,0 +1,967 @@ +# Ticket #034 — Modèle et exécuteur de tâches asynchrones + +## Contexte + +Labfy Investigation va bientôt exécuter des traitements potentiellement longs : + +- calcul d’empreintes ; +- copie de fichiers ; +- extraction de métadonnées ; +- lancement d’outils externes ; +- recherches DNS et réseau ; +- analyse de résultats ; +- génération de rapports. + +Ces opérations ne doivent jamais bloquer la boucle principale GTK. + +Le ticket #035 ajoutera une file de tâches et un panneau d’activité. Avant cela, il faut créer une abstraction asynchrone fiable, indépendante de GTK et réutilisable par tous les futurs modules. + +## Objectif + +Créer un type opaque : + +```c +BackgroundTask +``` + +capable de : + +- exécuter une fonction de travail dans un thread GLib ; +- conserver son état ; +- suivre sa progression ; +- accepter une demande d’annulation ; +- conserver un résultat ; +- conserver une erreur ; +- enregistrer ses dates de début et de fin ; +- appeler un callback de fin sur le contexte principal ; +- garantir une gestion correcte de sa durée de vie. + +Le module doit s’appuyer sur : + +```text +GTask +GCancellable +GMutex +gatomicrefcount +``` + +Il ne doit dépendre ni de GTK, ni de SQLite, ni d’une enquête particulière. + +--- + +# Architecture attendue + +Créer : + +```text +include/core/background_task.h +src/core/background_task.c +tests/test_background_task.c +``` + +Le flux général doit être : + +```text +background_task_new() + ↓ +background_task_start() + ↓ +GTask exécute le worker dans un thread + ↓ +le worker signale sa progression + ↓ +succès / erreur / annulation + ↓ +callback de fin sur le contexte principal + ↓ +background_task_unref() +``` + +--- + +# 1. Définir les états publics + +Dans : + +```text +include/core/background_task.h +``` + +définir : + +```c +typedef enum +{ + BACKGROUND_TASK_STATE_PENDING, + BACKGROUND_TASK_STATE_RUNNING, + BACKGROUND_TASK_STATE_COMPLETED, + BACKGROUND_TASK_STATE_FAILED, + BACKGROUND_TASK_STATE_CANCELLED +} BackgroundTaskState; +``` + +Transitions autorisées : + +```text +PENDING → RUNNING +RUNNING → COMPLETED +RUNNING → FAILED +RUNNING → CANCELLED +``` + +Une tâche terminée ne peut jamais être redémarrée. + +--- + +# 2. Définir le type opaque + +```c +typedef struct BackgroundTask BackgroundTask; +``` + +La structure interne ne doit jamais apparaître dans le header. + +--- + +# 3. Définir le worker + +Ajouter : + +```c +typedef gboolean (*BackgroundTaskWorker)( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +); +``` + +Contrat : + +```text +succès : + retourne TRUE + error reste NULL + result peut être NULL ou contenir un résultat + +échec : + retourne FALSE + error doit normalement être renseignée + result doit rester NULL + +annulation : + retourne FALSE + error appartient au domaine G_IO_ERROR + code G_IO_ERROR_CANCELLED +``` + +Le worker s’exécute dans un thread secondaire. + +Il ne doit jamais : + +- manipuler directement GTK ; +- accéder à un widget ; +- modifier une structure non protégée ; +- appeler le callback final lui-même. + +Le worker peut appeler : + +```c +background_task_report_progress() +``` + +depuis son thread. + +--- + +# 4. Définir le callback final + +Ajouter : + +```c +typedef void (*BackgroundTaskCompletionCallback)( + BackgroundTask *task, + gpointer user_data +); +``` + +Le callback doit être appelé après la mise à jour de l’état final. + +Lorsqu’une tâche est lancée depuis le thread principal GTK, le callback doit revenir sur ce contexte principal grâce au comportement de `GTask`. + +--- + +# 5. Définir le domaine d’erreur + +Ajouter : + +```c +typedef enum +{ + BACKGROUND_TASK_ERROR_INVALID_ARGUMENT, + BACKGROUND_TASK_ERROR_ALREADY_STARTED, + BACKGROUND_TASK_ERROR_WORKER_PROTOCOL +} BackgroundTaskError; +``` + +Puis : + +```c +#define BACKGROUND_TASK_ERROR \ + background_task_error_quark() + +GQuark background_task_error_quark(void); +``` + +Utilisation : + +- argument invalide ; +- seconde tentative de démarrage ; +- worker retournant `FALSE` sans fournir de `GError` ; +- worker retournant `TRUE` tout en fournissant une erreur. + +--- + +# 6. API publique attendue + +## Construction et références + +```c +BackgroundTask *background_task_new( + const char *title +); + +BackgroundTask *background_task_ref( + BackgroundTask *task +); + +void background_task_unref( + BackgroundTask *task +); +``` + +`background_task_new()` doit refuser : + +```text +title == NULL +title vide +``` + +La tâche utilise un comptage de références atomique. + +Ne pas exposer une fonction `background_task_free()`. + +Cette décision est importante : la tâche doit pouvoir rester vivante pendant l’exécution même si son propriétaire visuel disparaît. + +--- + +## Démarrage + +```c +gboolean background_task_start( + BackgroundTask *task, + BackgroundTaskWorker worker, + gpointer worker_data, + GDestroyNotify worker_data_destroy, + GDestroyNotify result_destroy, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +); +``` + +### Propriété des paramètres + +Si le démarrage réussit : + +```text +BackgroundTask prend en charge worker_data +BackgroundTask prend en charge completion_data +BackgroundTask prend en charge le futur result +``` + +Les fonctions de destruction correspondantes seront appelées au moment approprié. + +Si le démarrage échoue : + +```text +l’appelant conserve worker_data +l’appelant conserve completion_data +``` + +### Contraintes + +La fonction doit : + +1. vérifier `task` ; +2. vérifier `worker` ; +3. vérifier la convention `GError` ; +4. refuser une tâche qui n’est plus `PENDING` ; +5. créer un `GCancellable` ; +6. passer l’état à `RUNNING` ; +7. enregistrer la date de début ; +8. lancer le worker avec `g_task_run_in_thread()` ; +9. conserver une référence interne jusqu’au callback final. + +--- + +## Annulation + +```c +void background_task_cancel( + BackgroundTask *task +); + +gboolean background_task_is_cancelled( + const BackgroundTask *task +); +``` + +`background_task_cancel()` exprime une demande. + +Le worker reste responsable de vérifier régulièrement : + +```c +g_cancellable_set_error_if_cancelled() +``` + +ou : + +```c +g_cancellable_is_cancelled() +``` + +L’annulation ne doit jamais tuer brutalement un thread. + +--- + +## Progression + +```c +void background_task_report_progress( + BackgroundTask *task, + double progress, + const char *status_message +); +``` + +Règles : + +- utilisable depuis le worker ; +- protégée par `GMutex` ; +- valeur limitée entre `0.0` et `1.0` ; +- ignorée si la tâche n’est pas `RUNNING` ; +- `status_message` est copié ; +- `status_message == NULL` est accepté. + +Le ticket #035 pourra lire régulièrement ces valeurs pour mettre à jour le panneau d’activité. + +--- + +## Accesseurs + +```c +const char *background_task_get_title( + const BackgroundTask *task +); + +BackgroundTaskState background_task_get_state( + const BackgroundTask *task +); + +double background_task_get_progress( + const BackgroundTask *task +); + +char *background_task_dup_status_message( + const BackgroundTask *task +); + +gint64 background_task_get_started_at_us( + const BackgroundTask *task +); + +gint64 background_task_get_finished_at_us( + const BackgroundTask *task +); + +GError *background_task_dup_error( + const BackgroundTask *task +); + +gpointer background_task_get_result( + const BackgroundTask *task +); +``` + +### Propriété des valeurs + +```text +get_title() : + pointeur emprunté + valide pendant la durée de vie de task + titre immuable + +dup_status_message() : + nouvelle chaîne + l’appelant doit appeler g_free() + +dup_error() : + nouvelle copie + l’appelant doit appeler g_error_free() + +get_result() : + pointeur emprunté + ne doit jamais être libéré par l’appelant +``` + +`get_result()` ne doit être considéré comme valide qu’après l’état : + +```text +BACKGROUND_TASK_STATE_COMPLETED +``` + +--- + +# 7. Structure interne recommandée + +Dans : + +```text +src/core/background_task.c +``` + +la structure peut contenir : + +```c +struct BackgroundTask +{ + gatomicrefcount reference_count; + GMutex mutex; + + char *title; + char *status_message; + + BackgroundTaskState state; + double progress; + + gint64 started_at_us; + gint64 finished_at_us; + + GCancellable *cancellable; + + gpointer result; + GDestroyNotify result_destroy; + + GError *error; + + BackgroundTaskCompletionCallback + completion_callback; + + gpointer completion_data; + GDestroyNotify completion_data_destroy; +}; +``` + +Les champs mutables doivent être protégés par `mutex`. + +Le titre est immuable après construction. + +--- + +# 8. Contexte interne d’exécution + +Créer une structure privée, par exemple : + +```c +typedef struct +{ + BackgroundTask *task; + + BackgroundTaskWorker worker; + + gpointer worker_data; + GDestroyNotify worker_data_destroy; + + GDestroyNotify result_destroy; +} BackgroundTaskRunContext; +``` + +Le `worker_data` doit être détruit lorsque le contexte `GTask` est libéré. + +Le pointeur `task` peut être non propriétaire si une référence interne distincte garantit sa durée de vie jusqu’au callback final. + +--- + +# 9. Fonction exécutée dans le thread + +Créer un trampoline privé compatible avec : + +```c +GTaskThreadFunc +``` + +Il doit : + +1. récupérer le contexte ; +2. appeler le worker ; +3. vérifier le contrat de retour ; +4. retourner le résultat avec `g_task_return_pointer()` ; +5. retourner l’erreur avec `g_task_return_error()` ; +6. créer une erreur `BACKGROUND_TASK_ERROR_WORKER_PROTOCOL` si le worker viole son contrat. + +Cas à traiter : + +```text +FALSE + error valide : + échec normal + +FALSE + error NULL : + erreur de protocole + +TRUE + error non NULL : + erreur de protocole + +TRUE + result quelconque : + succès +``` + +Si un résultat a été produit alors que l’exécution échoue, il doit être détruit avec `result_destroy`. + +--- + +# 10. Callback interne de fin + +Créer un callback privé compatible avec : + +```c +GAsyncReadyCallback +``` + +Il doit : + +1. appeler `g_task_propagate_pointer()` ; +2. déterminer le nouvel état ; +3. conserver le résultat ou l’erreur ; +4. enregistrer la date de fin ; +5. forcer la progression à `1.0` en cas de succès ; +6. appeler le callback utilisateur ; +7. détruire les données du callback utilisateur ; +8. libérer la référence interne de la tâche. + +Détermination de l’état : + +```text +aucune erreur : + COMPLETED + +G_IO_ERROR_CANCELLED : + CANCELLED + +autre erreur : + FAILED +``` + +Le callback utilisateur doit observer un objet déjà entièrement finalisé. + +--- + +# 11. Destruction de la tâche + +Quand la dernière référence est libérée : + +1. vérifier qu’aucune référence interne d’exécution ne subsiste ; +2. détruire `result` avec `result_destroy` ; +3. libérer `error` ; +4. libérer les chaînes ; +5. libérer `GCancellable` ; +6. libérer les éventuelles données de callback restantes ; +7. nettoyer `GMutex` ; +8. libérer la structure. + +L’appel suivant doit être accepté : + +```c +background_task_unref(NULL); +``` + +--- + +# 12. Sécurité des threads + +Les opérations suivantes doivent utiliser `GMutex` : + +- lecture et écriture de l’état ; +- progression ; +- message de progression ; +- dates ; +- résultat ; +- erreur ; +- accès au cancellable si nécessaire. + +Ne jamais conserver le mutex verrouillé pendant : + +- l’appel du worker ; +- l’appel du callback utilisateur ; +- une fonction de destruction fournie par l’appelant ; +- un appel potentiellement bloquant. + +--- + +# 13. Tests unitaires + +Créer : + +```text +tests/test_background_task.c +``` + +Les tests doivent utiliser un `GMainLoop` pour attendre le callback final. + +## Test de construction + +Vérifier : + +```c +background_task_new(NULL) == NULL +background_task_new("") == NULL +``` + +Vérifier qu’une tâche valide commence avec : + +```text +PENDING +progression 0.0 +date de début 0 +date de fin 0 +aucune erreur +aucun résultat +``` + +## Test de succès + +Créer un worker qui : + +1. signale plusieurs progressions ; +2. renvoie une chaîne allouée ; +3. retourne `TRUE`. + +Vérifier dans le callback : + +```text +état COMPLETED +progression 1.0 +résultat correct +erreur NULL +date de début > 0 +date de fin >= date de début +callback appelé une seule fois +``` + +Vérifier que `result_destroy` est appelé lors du dernier `unref`. + +## Test d’échec + +Créer un worker qui retourne : + +```c +FALSE +``` + +avec une erreur : + +```text +G_IO_ERROR_FAILED +``` + +Vérifier : + +```text +état FAILED +résultat NULL +erreur conservée +message conservé +``` + +## Test de protocole invalide + +Créer un worker qui retourne : + +```c +FALSE +``` + +sans renseigner `GError`. + +Vérifier : + +```text +état FAILED +domaine BACKGROUND_TASK_ERROR +code BACKGROUND_TASK_ERROR_WORKER_PROTOCOL +``` + +## Test d’annulation + +Créer un worker qui travaille par petites étapes et vérifie régulièrement le `GCancellable`. + +Programmer : + +```c +background_task_cancel() +``` + +depuis le contexte principal avec `g_timeout_add()`. + +Vérifier : + +```text +état CANCELLED +erreur G_IO_ERROR_CANCELLED +callback final appelé +aucun crash +``` + +## Test de double démarrage + +Démarrer une tâche puis rappeler immédiatement : + +```c +background_task_start() +``` + +Vérifier : + +```text +FALSE +BACKGROUND_TASK_ERROR_ALREADY_STARTED +``` + +La première exécution doit continuer normalement. + +## Test des données utilisateur + +Vérifier que : + +- `worker_data_destroy` est appelé exactement une fois ; +- `completion_data_destroy` est appelé exactement une fois ; +- aucune donnée n’est détruite lorsque `background_task_start()` échoue avant transfert de propriété. + +## Test du comptage de références + +Démarrer une tâche puis libérer immédiatement la référence de l’appelant. + +Vérifier que : + +- la tâche reste vivante jusqu’au callback ; +- aucun accès mémoire invalide n’a lieu ; +- la destruction finale intervient après la fin de l’exécution. + +--- + +# 14. Makefile + +Le code de production est déjà découvert automatiquement si le Makefile utilise : + +```make +SRC := $(shell find src -name "*.c") +``` + +Ajouter toutefois une cible de test dédiée : + +```make +TEST_BACKGROUND_TASK := tests/test_background_task +``` + +La cible doit compiler au minimum : + +```text +tests/test_background_task.c +src/core/background_task.c +``` + +Lier avec les paquets GLib/GIO déjà utilisés par le projet. + +Ajouter le binaire aux cibles : + +```text +test +clean +``` + +Sortie attendue : + +```text +BackgroundTask : tous les tests sont valides. +``` + +--- + +# 15. Hors périmètre + +Ce ticket ne doit pas encore ajouter : + +- de file de tâches ; +- de limite de concurrence ; +- de panneau GTK ; +- de persistance SQLite ; +- de tâche associée à une enquête ; +- d’exécution de commande externe ; +- d’adaptateur ExifTool ; +- de recherche DNS ; +- de système de notifications ; +- de reprise après redémarrage ; +- de priorité entre tâches. + +Ces fonctions viendront dans les tickets suivants. + +--- + +# 16. Critères d’acceptation + +- [ ] `BackgroundTask` est opaque. +- [ ] Le module ne dépend pas de GTK. +- [ ] Le module ne dépend pas de SQLite. +- [ ] Le module utilise `GTask`. +- [ ] Le module utilise `GCancellable`. +- [ ] Le module utilise un comptage de références. +- [ ] Le module protège son état avec `GMutex`. +- [ ] Une tâche ne peut être démarrée qu’une fois. +- [ ] Le worker s’exécute dans un thread secondaire. +- [ ] Le callback final revient sur le contexte principal. +- [ ] La progression est comprise entre `0.0` et `1.0`. +- [ ] L’annulation est coopérative. +- [ ] Le résultat est conservé jusqu’à la destruction. +- [ ] L’erreur est conservée jusqu’à la destruction. +- [ ] Les dates de début et de fin sont enregistrées. +- [ ] Les données utilisateur sont détruites exactement une fois. +- [ ] La tâche reste vivante pendant son exécution. +- [ ] Aucun callback utilisateur n’est appelé sous mutex. +- [ ] Tous les tests unitaires passent. +- [ ] Les anciens tests restent valides. +- [ ] `make` réussit. +- [ ] `make test` réussit. +- [ ] `git diff --check` ne retourne aucune erreur. + +--- + +# 17. Audit attendu + +Vérifier l’absence de GTK et SQLite : + +```bash +rg -n \ + '#include + +/** + * @brief Représentation opaque d'une tâche asynchrone. + */ +typedef struct BackgroundTask BackgroundTask; + +/** + * @brief États possibles d'une tâche. + */ +typedef enum +{ + BACKGROUND_TASK_STATE_PENDING, + BACKGROUND_TASK_STATE_RUNNING, + BACKGROUND_TASK_STATE_COMPLETED, + BACKGROUND_TASK_STATE_FAILED, + BACKGROUND_TASK_STATE_CANCELLED +} BackgroundTaskState; + +/** + * @brief Codes d'erreur propres au module BackgroundTask. + */ +typedef enum +{ + BACKGROUND_TASK_ERROR_INVALID_ARGUMENT, + BACKGROUND_TASK_ERROR_ALREADY_STARTED, + BACKGROUND_TASK_ERROR_WORKER_PROTOCOL +} BackgroundTaskError; + +/** + * @brief Domaine d'erreur du module BackgroundTask. + */ +#define BACKGROUND_TASK_ERROR \ + background_task_error_quark() + +/** + * @brief Fonction exécutée dans un thread secondaire. + * + * En cas de succès, la fonction retourne TRUE et peut placer un résultat + * dans result. + * + * En cas d'échec, elle retourne FALSE et renseigne normalement error. + * + * @param task Tâche en cours d'exécution. + * @param cancellable Objet d'annulation associé à la tâche. + * @param worker_data Données privées du worker. + * @param result Emplacement recevant le résultat alloué. + * @param error Emplacement recevant une erreur. + * + * @return TRUE en cas de succès, sinon FALSE. + */ +typedef gboolean (*BackgroundTaskWorker)( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +); + +/** + * @brief Callback appelé lorsque la tâche est terminée. + * + * Ce callback est appelé après la mise à jour de l'état final. + * + * @param task Tâche terminée. + * @param user_data Données privées du callback. + */ +typedef void (*BackgroundTaskCompletionCallback)( + BackgroundTask *task, + gpointer user_data +); + +/** + * @brief Retourne le domaine d'erreur du module. + * + * @return Quark GLib du domaine d'erreur. + */ +GQuark background_task_error_quark(void); + +/** + * @brief Crée une nouvelle tâche en attente. + * + * @param title Titre non vide de la tâche. + * + * @return Nouvelle tâche, ou NULL si le titre est invalide. + */ +BackgroundTask *background_task_new( + const char *title +); + +/** + * @brief Ajoute une référence à une tâche. + * + * @param task Tâche concernée. + * + * @return La tâche fournie, ou NULL. + */ +BackgroundTask *background_task_ref( + BackgroundTask *task +); + +/** + * @brief Libère une référence à une tâche. + * + * La fonction accepte task == NULL. + * + * @param task Tâche concernée. + */ +void background_task_unref( + BackgroundTask *task +); + +/** + * @brief Démarre l'exécution asynchrone d'une tâche. + * + * Si le démarrage réussit, la tâche devient propriétaire de worker_data + * et completion_data. + * + * Si le démarrage échoue, l'appelant conserve leur propriété. + * + * Le futur résultat produit par le worker sera détruit avec + * result_destroy lors de la destruction de la tâche. + * + * @param task Tâche à démarrer. + * @param worker Fonction exécutée dans un thread secondaire. + * @param worker_data Données transmises au worker. + * @param worker_data_destroy Fonction de destruction de worker_data. + * @param result_destroy Fonction de destruction du futur résultat. + * @param completion_callback Callback final facultatif. + * @param completion_data Données transmises au callback final. + * @param completion_data_destroy Fonction de destruction associée. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si la tâche a été lancée, sinon FALSE. + */ +gboolean background_task_start( + BackgroundTask *task, + BackgroundTaskWorker worker, + gpointer worker_data, + GDestroyNotify worker_data_destroy, + GDestroyNotify result_destroy, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +); + +/** + * @brief Demande l'annulation coopérative de la tâche. + * + * @param task Tâche concernée. + */ +void background_task_cancel( + BackgroundTask *task +); + +/** + * @brief Indique si une annulation a été demandée. + * + * @param task Tâche concernée. + * + * @return TRUE si l'annulation a été demandée, sinon FALSE. + */ +gboolean background_task_is_cancelled( + const BackgroundTask *task +); + +/** + * @brief Met à jour la progression d'une tâche en cours. + * + * La progression est automatiquement limitée à l'intervalle + * compris entre 0.0 et 1.0. + * + * @param task Tâche concernée. + * @param progress Nouvelle progression. + * @param status_message Message facultatif, copié par le module. + */ +void background_task_report_progress( + BackgroundTask *task, + double progress, + const char *status_message +); + +/** + * @brief Retourne le titre immuable d'une tâche. + * + * Le pointeur retourné est emprunté. + * + * @param task Tâche concernée. + * + * @return Titre de la tâche, ou NULL. + */ +const char *background_task_get_title( + const BackgroundTask *task +); + +/** + * @brief Retourne l'état courant d'une tâche. + * + * @param task Tâche concernée. + * + * @return État courant de la tâche. + */ +BackgroundTaskState background_task_get_state( + const BackgroundTask *task +); + +/** + * @brief Retourne la progression courante. + * + * @param task Tâche concernée. + * + * @return Progression comprise entre 0.0 et 1.0. + */ +double background_task_get_progress( + const BackgroundTask *task +); + +/** + * @brief Copie le message de progression courant. + * + * L'appelant doit libérer la chaîne avec g_free(). + * + * @param task Tâche concernée. + * + * @return Nouvelle chaîne, ou NULL. + */ +char *background_task_dup_status_message( + const BackgroundTask *task +); + +/** + * @brief Retourne la date de démarrage monotone en microsecondes. + * + * @param task Tâche concernée. + * + * @return Date de démarrage, ou 0 si la tâche n'a pas démarré. + */ +gint64 background_task_get_started_at_us( + const BackgroundTask *task +); + +/** + * @brief Retourne la date de fin monotone en microsecondes. + * + * @param task Tâche concernée. + * + * @return Date de fin, ou 0 si la tâche n'est pas terminée. + */ +gint64 background_task_get_finished_at_us( + const BackgroundTask *task +); + +/** + * @brief Retourne une copie de l'erreur finale. + * + * L'appelant doit libérer l'erreur avec g_error_free(). + * + * @param task Tâche concernée. + * + * @return Nouvelle copie de l'erreur, ou NULL. + */ +GError *background_task_dup_error( + const BackgroundTask *task +); + +/** + * @brief Retourne le résultat final de la tâche. + * + * Le pointeur retourné est emprunté et ne doit pas être libéré. + * Il n'est exploitable qu'après l'état BACKGROUND_TASK_STATE_COMPLETED. + * + * @param task Tâche concernée. + * + * @return Résultat final, ou NULL. + */ +gpointer background_task_get_result( + const BackgroundTask *task +); + +#endif diff --git a/labfy-investigation b/labfy-investigation index e6abeda7a939cfa75e8e314c51450d64e824793b..47f1d0005c52f95acc3b0633ba08ca918d124593 100755 GIT binary patch delta 76259 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 diff --git a/src/core/background_task.c b/src/core/background_task.c new file mode 100644 index 0000000..23db2ae --- /dev/null +++ b/src/core/background_task.c @@ -0,0 +1,1072 @@ +/****************************************************************************** + * @file background_task.c + * @brief Implémentation d'une tâche générique exécutée en arrière-plan. + ******************************************************************************/ + +#include "core/background_task.h" + +#include + +/** + * @struct BackgroundTask + * @brief État interne d'une tâche asynchrone. + * + * Les champs mutables sont protégés par mutex. + * + * Le titre est immuable après la création de la tâche. + */ +struct BackgroundTask +{ + gatomicrefcount reference_count; + GMutex mutex; + + char *title; + char *status_message; + + BackgroundTaskState state; + double progress; + + gint64 started_at_us; + gint64 finished_at_us; + + GCancellable *cancellable; + + gpointer result; + GDestroyNotify result_destroy; + + GError *error; + + BackgroundTaskCompletionCallback + completion_callback; + + gpointer completion_data; + GDestroyNotify completion_data_destroy; +}; + +/** + * @struct BackgroundTaskRunContext + * @brief Données privées associées à l'exécution GTask. + */ +typedef struct +{ + BackgroundTask *task; + + BackgroundTaskWorker worker; + + gpointer worker_data; + GDestroyNotify worker_data_destroy; + + GDestroyNotify result_destroy; +} BackgroundTaskRunContext; + +GQuark background_task_error_quark(void) +{ + return g_quark_from_static_string( + "labfy-investigation-background-task-error" + ); +} + +/** + * @brief Exécute le worker dans un thread secondaire. + * + * @param async_task Tâche GLib. + * @param source_object Objet source inutilisé. + * @param task_data Contexte BackgroundTaskRunContext. + * @param cancellable Objet d'annulation. + */ +static void background_task_run_in_thread( + GTask *async_task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable +) +{ + BackgroundTaskRunContext *run_context = + task_data; + + gpointer worker_result = NULL; + GError *worker_error = NULL; + + gboolean worker_succeeded = FALSE; + + (void) source_object; + + if (run_context == NULL || + run_context->task == NULL || + run_context->worker == NULL) + { + g_task_return_new_error( + async_task, + BACKGROUND_TASK_ERROR, + BACKGROUND_TASK_ERROR_INVALID_ARGUMENT, + "Le contexte d'exécution de la tâche est invalide." + ); + + return; + } + + worker_succeeded = run_context->worker( + run_context->task, + cancellable, + run_context->worker_data, + &worker_result, + &worker_error + ); + + if (worker_succeeded) + { + if (worker_error != NULL) + { + if (worker_result != NULL && + run_context->result_destroy != NULL) + { + run_context->result_destroy( + worker_result + ); + + worker_result = NULL; + } + + g_task_return_new_error( + async_task, + BACKGROUND_TASK_ERROR, + BACKGROUND_TASK_ERROR_WORKER_PROTOCOL, + "Le worker a signalé un succès tout en retournant " + "une erreur : %s", + worker_error->message + ); + + g_clear_error( + &worker_error + ); + + return; + } + + g_task_return_pointer( + async_task, + worker_result, + run_context->result_destroy + ); + + return; + } + + /* + * Un résultat ne doit pas être conservé lorsque le worker échoue. + */ + if (worker_result != NULL && + run_context->result_destroy != NULL) + { + run_context->result_destroy( + worker_result + ); + + worker_result = NULL; + } + + if (worker_error == NULL) + { + g_task_return_new_error( + async_task, + BACKGROUND_TASK_ERROR, + BACKGROUND_TASK_ERROR_WORKER_PROTOCOL, + "Le worker a signalé un échec sans fournir de GError." + ); + + return; + } + + /* + * GTask devient propriétaire de worker_error. + */ + g_task_return_error( + async_task, + worker_error + ); +} + +/** + * @brief Finalise une tâche après l'exécution du worker. + * + * Ce callback est rappelé sur le contexte ayant démarré GTask. + * + * @param source_object Objet source inutilisé. + * @param async_result Résultat asynchrone. + * @param user_data Pointeur vers BackgroundTask. + */ +static void background_task_on_completed( + GObject *source_object, + GAsyncResult *async_result, + gpointer user_data +) +{ + BackgroundTask *task = user_data; + + gpointer worker_result = NULL; + GError *worker_error = NULL; + + BackgroundTaskState final_state; + + BackgroundTaskCompletionCallback + completion_callback = NULL; + + gpointer completion_data = NULL; + + GDestroyNotify completion_data_destroy = + NULL; + + (void) source_object; + + if (task == NULL || + async_result == NULL) + { + return; + } + + worker_result = g_task_propagate_pointer( + G_TASK(async_result), + &worker_error + ); + + if (worker_error == NULL) + { + final_state = + BACKGROUND_TASK_STATE_COMPLETED; + } + else if (g_error_matches( + worker_error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED + )) + { + final_state = + BACKGROUND_TASK_STATE_CANCELLED; + } + else + { + final_state = + BACKGROUND_TASK_STATE_FAILED; + } + + g_mutex_lock( + &task->mutex + ); + + task->state = final_state; + + task->finished_at_us = + g_get_monotonic_time(); + + if (final_state == + BACKGROUND_TASK_STATE_COMPLETED) + { + task->result = worker_result; + task->progress = 1.0; + + worker_result = NULL; + } + else + { + task->error = worker_error; + worker_error = NULL; + } + + completion_callback = + task->completion_callback; + + completion_data = + task->completion_data; + + completion_data_destroy = + task->completion_data_destroy; + + /* + * Ces données sont extraites afin de garantir leur destruction + * exactement une fois après le callback utilisateur. + */ + task->completion_callback = NULL; + task->completion_data = NULL; + task->completion_data_destroy = NULL; + + g_mutex_unlock( + &task->mutex + ); + + /* + * Aucun callback utilisateur ne doit être exécuté sous mutex. + */ + if (completion_callback != NULL) + { + completion_callback( + task, + completion_data + ); + } + + if (completion_data != NULL && + completion_data_destroy != NULL) + { + completion_data_destroy( + completion_data + ); + } + + /* + * Ces variables sont normalement NULL après le transfert, + * mais ce nettoyage sécurise les futurs changements. + */ + if (worker_result != NULL && + task->result_destroy != NULL) + { + task->result_destroy( + worker_result + ); + } + + g_clear_error( + &worker_error + ); +} + +/** + * @brief Libère le contexte privé d'une exécution. + * + * @param user_data Pointeur vers BackgroundTaskRunContext. + */ +static void background_task_run_context_free( + gpointer user_data +) +{ + BackgroundTaskRunContext *run_context = + user_data; + + if (run_context == NULL) + { + return; + } + + if (run_context->worker_data != NULL && + run_context->worker_data_destroy != NULL) + { + run_context->worker_data_destroy( + run_context->worker_data + ); + } + + background_task_unref( + run_context->task + ); + + g_free( + run_context + ); +} + +BackgroundTask *background_task_new( + const char *title +) +{ + BackgroundTask *task = NULL; + + if (title == NULL || + title[0] == '\0') + { + return NULL; + } + + task = g_new0( + BackgroundTask, + 1 + ); + + g_atomic_ref_count_init( + &task->reference_count + ); + + g_mutex_init( + &task->mutex + ); + + task->title = g_strdup( + title + ); + + task->state = + BACKGROUND_TASK_STATE_PENDING; + + task->progress = 0.0; + task->started_at_us = 0; + task->finished_at_us = 0; + + return task; +} + +BackgroundTask *background_task_ref( + BackgroundTask *task +) +{ + if (task == NULL) + { + return NULL; + } + + g_atomic_ref_count_inc( + &task->reference_count + ); + + return task; +} + +void background_task_unref( + BackgroundTask *task +) +{ + gpointer result = NULL; + GDestroyNotify result_destroy = NULL; + + gpointer completion_data = NULL; + GDestroyNotify completion_data_destroy = NULL; + + GCancellable *cancellable = NULL; + GError *error = NULL; + + char *title = NULL; + char *status_message = NULL; + + if (task == NULL) + { + return; + } + + if (!g_atomic_ref_count_dec( + &task->reference_count + )) + { + return; + } + + /* + * À ce stade, aucune autre référence ne doit accéder à la tâche. + * Les ressources sont néanmoins extraites proprement avant + * la destruction du mutex. + */ + g_mutex_lock( + &task->mutex + ); + + result = task->result; + result_destroy = task->result_destroy; + + completion_data = task->completion_data; + completion_data_destroy = + task->completion_data_destroy; + + cancellable = task->cancellable; + error = task->error; + + title = task->title; + status_message = task->status_message; + + task->result = NULL; + task->result_destroy = NULL; + + task->completion_data = NULL; + task->completion_data_destroy = NULL; + task->completion_callback = NULL; + + task->cancellable = NULL; + task->error = NULL; + + task->title = NULL; + task->status_message = NULL; + + g_mutex_unlock( + &task->mutex + ); + + if (result != NULL && + result_destroy != NULL) + { + result_destroy( + result + ); + } + + if (completion_data != NULL && + completion_data_destroy != NULL) + { + completion_data_destroy( + completion_data + ); + } + + if (cancellable != NULL) + { + g_object_unref( + cancellable + ); + } + + g_clear_error( + &error + ); + + g_free( + status_message + ); + + g_free( + title + ); + + g_mutex_clear( + &task->mutex + ); + + g_free( + task + ); +} + +gboolean background_task_start( + BackgroundTask *task, + BackgroundTaskWorker worker, + gpointer worker_data, + GDestroyNotify worker_data_destroy, + GDestroyNotify result_destroy, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +) +{ + BackgroundTaskRunContext *run_context = + NULL; + + GCancellable *cancellable = NULL; + GTask *async_task = NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (task == NULL || + worker == NULL) + { + g_set_error_literal( + error, + BACKGROUND_TASK_ERROR, + BACKGROUND_TASK_ERROR_INVALID_ARGUMENT, + "La tâche ou son worker est invalide." + ); + + return FALSE; + } + + /* + * Le GCancellable est préparé avant le verrouillage. + */ + cancellable = g_cancellable_new(); + + g_mutex_lock( + &task->mutex + ); + + if (task->state != + BACKGROUND_TASK_STATE_PENDING) + { + g_mutex_unlock( + &task->mutex + ); + + g_object_unref( + cancellable + ); + + g_set_error_literal( + error, + BACKGROUND_TASK_ERROR, + BACKGROUND_TASK_ERROR_ALREADY_STARTED, + "Cette tâche a déjà été démarrée." + ); + + return FALSE; + } + + task->state = + BACKGROUND_TASK_STATE_RUNNING; + + task->progress = 0.0; + + task->started_at_us = + g_get_monotonic_time(); + + task->finished_at_us = 0; + + task->cancellable = g_object_ref( + cancellable + ); + + task->result_destroy = + result_destroy; + + task->completion_callback = + completion_callback; + + task->completion_data = + completion_data; + + task->completion_data_destroy = + completion_data_destroy; + + g_mutex_unlock( + &task->mutex + ); + + run_context = g_new0( + BackgroundTaskRunContext, + 1 + ); + + /* + * Cette référence interne protège la tâche même si l'appelant + * libère immédiatement sa propre référence. + */ + run_context->task = + background_task_ref(task); + + run_context->worker = worker; + + run_context->worker_data = + worker_data; + + run_context->worker_data_destroy = + worker_data_destroy; + + run_context->result_destroy = + result_destroy; + + async_task = g_task_new( + NULL, + cancellable, + background_task_on_completed, + task + ); + + g_task_set_task_data( + async_task, + run_context, + background_task_run_context_free + ); + + g_task_set_check_cancellable( + async_task, + TRUE + ); + + g_task_run_in_thread( + async_task, + background_task_run_in_thread + ); + + /* + * GTask conserve sa propre référence pendant l'exécution. + */ + g_object_unref( + async_task + ); + + g_object_unref( + cancellable + ); + + return TRUE; +} + +void background_task_cancel( + BackgroundTask *task +) +{ + GCancellable *cancellable = NULL; + + if (task == NULL) + { + return; + } + + g_mutex_lock( + &task->mutex + ); + + if (task->state == + BACKGROUND_TASK_STATE_RUNNING && + task->cancellable != NULL) + { + cancellable = g_object_ref( + task->cancellable + ); + } + + g_mutex_unlock( + &task->mutex + ); + + /* + * L'annulation est effectuée hors du mutex. + * + * GCancellable peut réveiller des callbacks ou des opérations + * bloquantes. Il ne faut donc pas garder le verrou. + */ + if (cancellable != NULL) + { + g_cancellable_cancel( + cancellable + ); + + g_object_unref( + cancellable + ); + } +} + +gboolean background_task_is_cancelled( + const BackgroundTask *task +) +{ + BackgroundTask *mutable_task = NULL; + GCancellable *cancellable = NULL; + + BackgroundTaskState state; + gboolean is_cancelled = FALSE; + + if (task == NULL) + { + return FALSE; + } + + mutable_task = (BackgroundTask *) task; + + g_mutex_lock( + &mutable_task->mutex + ); + + state = mutable_task->state; + + if (state == + BACKGROUND_TASK_STATE_CANCELLED) + { + is_cancelled = TRUE; + } + else if (state == + BACKGROUND_TASK_STATE_RUNNING && + mutable_task->cancellable != NULL) + { + cancellable = g_object_ref( + mutable_task->cancellable + ); + } + + g_mutex_unlock( + &mutable_task->mutex + ); + + if (cancellable != NULL) + { + is_cancelled = g_cancellable_is_cancelled( + cancellable + ); + + g_object_unref( + cancellable + ); + } + + return is_cancelled; +} + +void background_task_report_progress( + BackgroundTask *task, + double progress, + const char *status_message +) +{ + char *new_status_message = NULL; + + if (task == NULL) + { + return; + } + + /* + * Une valeur NaN ne doit jamais être conservée. + */ + if (progress != progress) + { + progress = 0.0; + } + else if (progress < 0.0) + { + progress = 0.0; + } + else if (progress > 1.0) + { + progress = 1.0; + } + + if (status_message != NULL) + { + new_status_message = g_strdup( + status_message + ); + } + + g_mutex_lock( + &task->mutex + ); + + if (task->state != + BACKGROUND_TASK_STATE_RUNNING) + { + g_mutex_unlock( + &task->mutex + ); + + g_free( + new_status_message + ); + + return; + } + + task->progress = progress; + + g_free( + task->status_message + ); + + task->status_message = + new_status_message; + + g_mutex_unlock( + &task->mutex + ); +} + +const char *background_task_get_title( + const BackgroundTask *task +) +{ + if (task == NULL) + { + return NULL; + } + + /* + * Le titre est immuable après la construction. + */ + return task->title; +} + +BackgroundTaskState background_task_get_state( + const BackgroundTask *task +) +{ + BackgroundTask *mutable_task = NULL; + BackgroundTaskState state; + + if (task == NULL) + { + return BACKGROUND_TASK_STATE_PENDING; + } + + mutable_task = (BackgroundTask *) task; + + g_mutex_lock( + &mutable_task->mutex + ); + + state = mutable_task->state; + + g_mutex_unlock( + &mutable_task->mutex + ); + + return state; +} + +double background_task_get_progress( + const BackgroundTask *task +) +{ + BackgroundTask *mutable_task = NULL; + double progress = 0.0; + + if (task == NULL) + { + return 0.0; + } + + mutable_task = (BackgroundTask *) task; + + g_mutex_lock( + &mutable_task->mutex + ); + + progress = mutable_task->progress; + + g_mutex_unlock( + &mutable_task->mutex + ); + + return progress; +} + +char *background_task_dup_status_message( + const BackgroundTask *task +) +{ + BackgroundTask *mutable_task = NULL; + char *status_message = NULL; + + if (task == NULL) + { + return NULL; + } + + mutable_task = (BackgroundTask *) task; + + g_mutex_lock( + &mutable_task->mutex + ); + + status_message = g_strdup( + mutable_task->status_message + ); + + g_mutex_unlock( + &mutable_task->mutex + ); + + return status_message; +} + +gint64 background_task_get_started_at_us( + const BackgroundTask *task +) +{ + BackgroundTask *mutable_task = NULL; + gint64 started_at_us = 0; + + if (task == NULL) + { + return 0; + } + + mutable_task = (BackgroundTask *) task; + + g_mutex_lock( + &mutable_task->mutex + ); + + started_at_us = + mutable_task->started_at_us; + + g_mutex_unlock( + &mutable_task->mutex + ); + + return started_at_us; +} + +gint64 background_task_get_finished_at_us( + const BackgroundTask *task +) +{ + BackgroundTask *mutable_task = NULL; + gint64 finished_at_us = 0; + + if (task == NULL) + { + return 0; + } + + mutable_task = (BackgroundTask *) task; + + g_mutex_lock( + &mutable_task->mutex + ); + + finished_at_us = + mutable_task->finished_at_us; + + g_mutex_unlock( + &mutable_task->mutex + ); + + return finished_at_us; +} + +GError *background_task_dup_error( + const BackgroundTask *task +) +{ + BackgroundTask *mutable_task = NULL; + GError *error = NULL; + + if (task == NULL) + { + return NULL; + } + + mutable_task = (BackgroundTask *) task; + + g_mutex_lock( + &mutable_task->mutex + ); + + if (mutable_task->error != NULL) + { + error = g_error_copy( + mutable_task->error + ); + } + + g_mutex_unlock( + &mutable_task->mutex + ); + + return error; +} + +gpointer background_task_get_result( + const BackgroundTask *task +) +{ + BackgroundTask *mutable_task = NULL; + gpointer result = NULL; + + if (task == NULL) + { + return NULL; + } + + mutable_task = (BackgroundTask *) task; + + g_mutex_lock( + &mutable_task->mutex + ); + + if (mutable_task->state == + BACKGROUND_TASK_STATE_COMPLETED) + { + result = mutable_task->result; + } + + g_mutex_unlock( + &mutable_task->mutex + ); + + return result; +} diff --git a/tests/test_background_task b/tests/test_background_task new file mode 100755 index 0000000000000000000000000000000000000000..0c76ca061ee73c9f29a46eaa1c9d1a8cb4998f6e GIT binary patch literal 46768 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 OzDG(U(-_i<*8c$C?X}?m literal 0 HcmV?d00001 diff --git a/tests/test_background_task.c b/tests/test_background_task.c new file mode 100644 index 0000000..6881343 --- /dev/null +++ b/tests/test_background_task.c @@ -0,0 +1,1469 @@ +/****************************************************************************** + * @file test_background_task.c + * @brief Tests du module BackgroundTask. + ******************************************************************************/ + +#include "core/background_task.h" + +#include +#include +#include + +#include + +/** + * @brief Délai maximal d'attente d'un test asynchrone. + */ +#define TEST_BACKGROUND_TASK_TIMEOUT_SECONDS 5 + +/** + * @struct TestBackgroundTaskResult + * @brief Résultat produit par le worker de test. + */ +typedef struct +{ + char *text; + gboolean *destroyed; +} TestBackgroundTaskResult; + +/** + * @struct TestBackgroundTaskWorkerData + * @brief Données transmises au worker de succès. + */ +typedef struct +{ + gboolean *result_destroyed; +} TestBackgroundTaskWorkerData; + +/** + * @struct TestBackgroundTaskCompletionData + * @brief Contexte utilisé par le callback final. + */ +typedef struct +{ + GMainLoop *main_loop; + guint completion_count; +} TestBackgroundTaskCompletionData; + +typedef struct +{ + guint *worker_data_destroy_count; + guint *result_destroy_count; +} TestBackgroundTaskLifetimeWorkerData; + +typedef struct +{ + guint *destroy_count; +} TestBackgroundTaskLifetimeResult; + +typedef struct +{ + GMainLoop *main_loop; + guint completion_count; + guint completion_data_destroy_count; +} TestBackgroundTaskLifetimeCompletionData; + +/** + * @brief Détruit un résultat produit par le worker. + * + * @param user_data Pointeur vers TestBackgroundTaskResult. + */ +static void test_background_task_result_free( + gpointer user_data +) +{ + TestBackgroundTaskResult *result = + user_data; + + if (result == NULL) + { + return; + } + + if (result->destroyed != NULL) + { + *result->destroyed = TRUE; + } + + g_free( + result->text + ); + + g_free( + result + ); +} + +/** + * @brief Termine un test qui a dépassé son délai maximal. + * + * @param user_data Boucle principale du test. + * + * @return G_SOURCE_REMOVE. + */ +static gboolean test_background_task_timeout( + gpointer user_data +) +{ + GMainLoop *main_loop = user_data; + + if (main_loop != NULL) + { + g_main_loop_quit( + main_loop + ); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Worker réalisant une tâche avec succès. + * + * @param task Tâche en cours. + * @param cancellable Objet d'annulation. + * @param worker_data Données TestBackgroundTaskWorkerData. + * @param result Emplacement recevant le résultat. + * @param error Emplacement recevant une erreur. + * + * @return TRUE. + */ +static gboolean test_background_task_success_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + TestBackgroundTaskWorkerData *data = + worker_data; + + TestBackgroundTaskResult *worker_result = + NULL; + + assert(task != NULL); + assert(cancellable != NULL); + assert(data != NULL); + assert(result != NULL); + assert(error != NULL); + assert(*error == NULL); + + background_task_report_progress( + task, + 0.25, + "Préparation" + ); + + background_task_report_progress( + task, + 0.75, + "Finalisation" + ); + + worker_result = g_new0( + TestBackgroundTaskResult, + 1 + ); + + worker_result->text = g_strdup( + "Résultat valide" + ); + + worker_result->destroyed = + data->result_destroyed; + + *result = worker_result; + + return TRUE; +} + +/** + * @brief Vérifie le résultat d'une tâche réussie. + * + * @param task Tâche terminée. + * @param user_data Données TestBackgroundTaskCompletionData. + */ +static void test_background_task_success_completed( + BackgroundTask *task, + gpointer user_data +) +{ + TestBackgroundTaskCompletionData *completion_data = + user_data; + + const TestBackgroundTaskResult *result = + NULL; + + char *status_message = NULL; + GError *error = NULL; + + assert(task != NULL); + assert(completion_data != NULL); + assert(completion_data->main_loop != NULL); + + completion_data->completion_count++; + + assert( + completion_data->completion_count == 1 + ); + + assert( + background_task_get_state(task) == + BACKGROUND_TASK_STATE_COMPLETED + ); + + assert( + background_task_get_progress(task) == 1.0 + ); + + assert( + background_task_get_started_at_us(task) > 0 + ); + + assert( + background_task_get_finished_at_us(task) >= + background_task_get_started_at_us(task) + ); + + error = background_task_dup_error( + task + ); + + assert(error == NULL); + + result = background_task_get_result( + task + ); + + assert(result != NULL); + assert(result->text != NULL); + + assert( + strcmp( + result->text, + "Résultat valide" + ) == 0 + ); + + status_message = + background_task_dup_status_message( + task + ); + + assert(status_message != NULL); + + assert( + strcmp( + status_message, + "Finalisation" + ) == 0 + ); + + g_free( + status_message + ); + + g_main_loop_quit( + completion_data->main_loop + ); +} + +/** + * @brief Vérifie la construction et l'état initial d'une tâche. + */ +static void test_background_task_creation(void) +{ + BackgroundTask *task = NULL; + + char *status_message = NULL; + GError *error = NULL; + + assert( + background_task_new(NULL) == NULL + ); + + assert( + background_task_new("") == NULL + ); + + task = background_task_new( + "Test de construction" + ); + + assert(task != NULL); + + assert( + strcmp( + background_task_get_title(task), + "Test de construction" + ) == 0 + ); + + assert( + background_task_get_state(task) == + BACKGROUND_TASK_STATE_PENDING + ); + + assert( + background_task_get_progress(task) == 0.0 + ); + + assert( + background_task_get_started_at_us(task) == 0 + ); + + assert( + background_task_get_finished_at_us(task) == 0 + ); + + assert( + background_task_get_result(task) == NULL + ); + + status_message = + background_task_dup_status_message( + task + ); + + assert(status_message == NULL); + + error = background_task_dup_error( + task + ); + + assert(error == NULL); + + /* + * Une demande d'annulation avant le démarrage est ignorée. + */ + background_task_cancel( + task + ); + + assert( + !background_task_is_cancelled(task) + ); + + /* + * Une progression signalée avant le démarrage est ignorée. + */ + background_task_report_progress( + task, + 0.50, + "Message ignoré" + ); + + assert( + background_task_get_progress(task) == 0.0 + ); + + status_message = + background_task_dup_status_message( + task + ); + + assert(status_message == NULL); + + background_task_unref( + task + ); + + background_task_unref( + NULL + ); +} + +/** + * @brief Vérifie une exécution asynchrone réussie. + */ +static void test_background_task_success(void) +{ + BackgroundTask *task = NULL; + + TestBackgroundTaskWorkerData *worker_data = + NULL; + + TestBackgroundTaskCompletionData + completion_data = {0}; + + GMainLoop *main_loop = NULL; + GError *error = NULL; + + guint timeout_source_id = 0; + + gboolean result_destroyed = FALSE; + + main_loop = g_main_loop_new( + NULL, + FALSE + ); + + assert(main_loop != NULL); + + completion_data.main_loop = + main_loop; + + completion_data.completion_count = 0; + + worker_data = g_new0( + TestBackgroundTaskWorkerData, + 1 + ); + + worker_data->result_destroyed = + &result_destroyed; + + task = background_task_new( + "Test de succès" + ); + + assert(task != NULL); + + assert( + background_task_start( + task, + test_background_task_success_worker, + worker_data, + g_free, + test_background_task_result_free, + test_background_task_success_completed, + &completion_data, + NULL, + &error + ) + ); + + assert(error == NULL); + + assert( + background_task_get_state(task) == + BACKGROUND_TASK_STATE_RUNNING + ); + + assert( + background_task_get_started_at_us(task) > 0 + ); + + timeout_source_id = g_timeout_add_seconds( + TEST_BACKGROUND_TASK_TIMEOUT_SECONDS, + test_background_task_timeout, + main_loop + ); + + assert(timeout_source_id != 0); + + g_main_loop_run( + main_loop + ); + + assert( + completion_data.completion_count == 1 + ); + + /* + * Le délai n'a pas été déclenché puisque la tâche s'est terminée. + */ + assert( + g_source_remove( + timeout_source_id + ) + ); + + assert(!result_destroyed); + + /* + * Le résultat reste la propriété de BackgroundTask jusqu'au dernier + * unref. + */ + background_task_unref( + task + ); + + assert(result_destroyed); + + g_main_loop_unref( + main_loop + ); +} + +/** + * @brief Worker retournant volontairement une erreur normale. + */ +static gboolean test_background_task_failure_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + (void) task; + (void) cancellable; + (void) worker_data; + (void) result; + + assert(error != NULL); + assert(*error == NULL); + + g_set_error_literal( + error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Échec volontaire du worker." + ); + + return FALSE; +} + +/** + * @brief Vérifie la finalisation d'une tâche en échec. + */ +static void test_background_task_failure_completed( + BackgroundTask *task, + gpointer user_data +) +{ + TestBackgroundTaskCompletionData *completion_data = + user_data; + + GError *error = NULL; + + assert(task != NULL); + assert(completion_data != NULL); + + completion_data->completion_count++; + + assert( + completion_data->completion_count == 1 + ); + + assert( + background_task_get_state(task) == + BACKGROUND_TASK_STATE_FAILED + ); + + assert( + background_task_get_result(task) == NULL + ); + + assert( + background_task_get_started_at_us(task) > 0 + ); + + assert( + background_task_get_finished_at_us(task) >= + background_task_get_started_at_us(task) + ); + + error = background_task_dup_error( + task + ); + + assert(error != NULL); + + assert( + g_error_matches( + error, + G_IO_ERROR, + G_IO_ERROR_FAILED + ) + ); + + assert( + strcmp( + error->message, + "Échec volontaire du worker." + ) == 0 + ); + + g_error_free( + error + ); + + g_main_loop_quit( + completion_data->main_loop + ); +} + +/** + * @brief Vérifie la conservation d'une erreur normale de worker. + */ +static void test_background_task_failure(void) +{ + BackgroundTask *task = NULL; + + TestBackgroundTaskCompletionData + completion_data = {0}; + + GMainLoop *main_loop = NULL; + GError *error = NULL; + + guint timeout_source_id = 0; + + main_loop = g_main_loop_new( + NULL, + FALSE + ); + + assert(main_loop != NULL); + + completion_data.main_loop = + main_loop; + + task = background_task_new( + "Test d'échec" + ); + + assert(task != NULL); + + assert( + background_task_start( + task, + test_background_task_failure_worker, + NULL, + NULL, + NULL, + test_background_task_failure_completed, + &completion_data, + NULL, + &error + ) + ); + + assert(error == NULL); + + timeout_source_id = g_timeout_add_seconds( + TEST_BACKGROUND_TASK_TIMEOUT_SECONDS, + test_background_task_timeout, + main_loop + ); + + assert(timeout_source_id != 0); + + g_main_loop_run( + main_loop + ); + + assert( + completion_data.completion_count == 1 + ); + + assert( + g_source_remove( + timeout_source_id + ) + ); + + background_task_unref( + task + ); + + g_main_loop_unref( + main_loop + ); +} + +/** + * @brief Worker violant le contrat en échouant sans GError. + */ +static gboolean test_background_task_invalid_protocol_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + (void) task; + (void) cancellable; + (void) worker_data; + (void) result; + + assert(error != NULL); + assert(*error == NULL); + + return FALSE; +} + +/** + * @brief Vérifie la détection d'une violation du contrat du worker. + */ +static void test_background_task_invalid_protocol_completed( + BackgroundTask *task, + gpointer user_data +) +{ + TestBackgroundTaskCompletionData *completion_data = + user_data; + + GError *error = NULL; + + assert(task != NULL); + assert(completion_data != NULL); + + completion_data->completion_count++; + + assert( + completion_data->completion_count == 1 + ); + + assert( + background_task_get_state(task) == + BACKGROUND_TASK_STATE_FAILED + ); + + assert( + background_task_get_result(task) == NULL + ); + + error = background_task_dup_error( + task + ); + + assert(error != NULL); + + assert( + error->domain == + BACKGROUND_TASK_ERROR + ); + + assert( + error->code == + BACKGROUND_TASK_ERROR_WORKER_PROTOCOL + ); + + g_error_free( + error + ); + + g_main_loop_quit( + completion_data->main_loop + ); +} + +/** + * @brief Vérifie qu'un worker incorrect est transformé en échec contrôlé. + */ +static void test_background_task_invalid_protocol(void) +{ + BackgroundTask *task = NULL; + + TestBackgroundTaskCompletionData + completion_data = {0}; + + GMainLoop *main_loop = NULL; + GError *error = NULL; + + guint timeout_source_id = 0; + + main_loop = g_main_loop_new( + NULL, + FALSE + ); + + assert(main_loop != NULL); + + completion_data.main_loop = + main_loop; + + task = background_task_new( + "Test de protocole invalide" + ); + + assert(task != NULL); + + assert( + background_task_start( + task, + test_background_task_invalid_protocol_worker, + NULL, + NULL, + NULL, + test_background_task_invalid_protocol_completed, + &completion_data, + NULL, + &error + ) + ); + + assert(error == NULL); + + timeout_source_id = g_timeout_add_seconds( + TEST_BACKGROUND_TASK_TIMEOUT_SECONDS, + test_background_task_timeout, + main_loop + ); + + assert(timeout_source_id != 0); + + g_main_loop_run( + main_loop + ); + + assert( + completion_data.completion_count == 1 + ); + + assert( + g_source_remove( + timeout_source_id + ) + ); + + background_task_unref( + task + ); + + g_main_loop_unref( + main_loop + ); +} + +/** + * @brief Worker assez long pour tester l'annulation et le double démarrage. + */ +static gboolean test_background_task_wait_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + guint step = 0; + + (void) worker_data; + + assert(task != NULL); + assert(cancellable != NULL); + assert(result != NULL); + assert(error != NULL); + assert(*error == NULL); + + for (step = 0; step < 100; step++) + { + if (g_cancellable_set_error_if_cancelled( + cancellable, + error + )) + { + return FALSE; + } + + background_task_report_progress( + task, + (double) step / 100.0, + "Traitement en cours" + ); + + g_usleep( + 10000 + ); + } + + *result = NULL; + + return TRUE; +} + +/** + * @brief Termine une tâche simple réussie. + */ +static void test_background_task_simple_completed( + BackgroundTask *task, + gpointer user_data +) +{ + TestBackgroundTaskCompletionData *completion_data = + user_data; + + assert(task != NULL); + assert(completion_data != NULL); + + completion_data->completion_count++; + + assert( + completion_data->completion_count == 1 + ); + + assert( + background_task_get_state(task) == + BACKGROUND_TASK_STATE_COMPLETED + ); + + g_main_loop_quit( + completion_data->main_loop + ); +} + +/** + * @brief Demande l'annulation depuis le contexte principal. + * + * @return G_SOURCE_REMOVE. + */ +static gboolean test_background_task_cancel_timeout( + gpointer user_data +) +{ + BackgroundTask *task = user_data; + + assert(task != NULL); + + background_task_cancel( + task + ); + + return G_SOURCE_REMOVE; +} + +/** + * @brief Libère une référence utilisée comme donnée GLib. + */ +static void test_background_task_unref_notify( + gpointer user_data +) +{ + background_task_unref( + user_data + ); +} + +/** + * @brief Vérifie la finalisation d'une tâche annulée. + */ +static void test_background_task_cancelled_completed( + BackgroundTask *task, + gpointer user_data +) +{ + TestBackgroundTaskCompletionData *completion_data = + user_data; + + GError *error = NULL; + + assert(task != NULL); + assert(completion_data != NULL); + + completion_data->completion_count++; + + assert( + completion_data->completion_count == 1 + ); + + assert( + background_task_get_state(task) == + BACKGROUND_TASK_STATE_CANCELLED + ); + + assert( + background_task_is_cancelled(task) + ); + + assert( + background_task_get_result(task) == NULL + ); + + error = background_task_dup_error( + task + ); + + assert(error != NULL); + + assert( + g_error_matches( + error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED + ) + ); + + g_error_free( + error + ); + + g_main_loop_quit( + completion_data->main_loop + ); +} + +/** + * @brief Vérifie l'annulation coopérative d'une tâche. + */ +static void test_background_task_cancellation(void) +{ + BackgroundTask *task = NULL; + + TestBackgroundTaskCompletionData + completion_data = {0}; + + GMainLoop *main_loop = NULL; + GError *error = NULL; + + guint cancel_source_id = 0; + guint timeout_source_id = 0; + + main_loop = g_main_loop_new( + NULL, + FALSE + ); + + assert(main_loop != NULL); + + completion_data.main_loop = + main_loop; + + task = background_task_new( + "Test d'annulation" + ); + + assert(task != NULL); + + assert( + background_task_start( + task, + test_background_task_wait_worker, + NULL, + NULL, + NULL, + test_background_task_cancelled_completed, + &completion_data, + NULL, + &error + ) + ); + + assert(error == NULL); + + /* + * La source conserve sa propre référence à la tâche jusqu'à son + * déclenchement ou sa destruction. + */ + cancel_source_id = g_timeout_add_full( + G_PRIORITY_DEFAULT, + 50, + test_background_task_cancel_timeout, + background_task_ref(task), + test_background_task_unref_notify + ); + + assert(cancel_source_id != 0); + + timeout_source_id = g_timeout_add_seconds( + TEST_BACKGROUND_TASK_TIMEOUT_SECONDS, + test_background_task_timeout, + main_loop + ); + + assert(timeout_source_id != 0); + + g_main_loop_run( + main_loop + ); + + assert( + completion_data.completion_count == 1 + ); + + assert( + g_source_remove( + timeout_source_id + ) + ); + + background_task_unref( + task + ); + + g_main_loop_unref( + main_loop + ); +} + +/** + * @brief Marque une donnée comme détruite. + */ +static void test_background_task_mark_destroyed( + gpointer user_data +) +{ + gboolean *destroyed = user_data; + + assert(destroyed != NULL); + + *destroyed = TRUE; +} + +/** + * @brief Vérifie qu'une tâche ne peut être démarrée qu'une fois. + */ +static void test_background_task_double_start(void) +{ + BackgroundTask *task = NULL; + + TestBackgroundTaskCompletionData + completion_data = {0}; + + GMainLoop *main_loop = NULL; + + GError *first_error = NULL; + GError *second_error = NULL; + + guint timeout_source_id = 0; + + gboolean second_worker_data_destroyed = FALSE; + gboolean second_completion_data_destroyed = FALSE; + + main_loop = g_main_loop_new( + NULL, + FALSE + ); + + assert(main_loop != NULL); + + completion_data.main_loop = + main_loop; + + task = background_task_new( + "Test de double démarrage" + ); + + assert(task != NULL); + + assert( + background_task_start( + task, + test_background_task_wait_worker, + NULL, + NULL, + NULL, + test_background_task_simple_completed, + &completion_data, + NULL, + &first_error + ) + ); + + assert(first_error == NULL); + + assert( + !background_task_start( + task, + test_background_task_wait_worker, + &second_worker_data_destroyed, + test_background_task_mark_destroyed, + NULL, + NULL, + &second_completion_data_destroyed, + test_background_task_mark_destroyed, + &second_error + ) + ); + + assert(second_error != NULL); + + assert( + second_error->domain == + BACKGROUND_TASK_ERROR + ); + + assert( + second_error->code == + BACKGROUND_TASK_ERROR_ALREADY_STARTED + ); + + /* + * Le démarrage ayant échoué, la propriété n'a pas été transférée. + */ + assert(!second_worker_data_destroyed); + assert(!second_completion_data_destroyed); + + g_error_free( + second_error + ); + + timeout_source_id = g_timeout_add_seconds( + TEST_BACKGROUND_TASK_TIMEOUT_SECONDS, + test_background_task_timeout, + main_loop + ); + + assert(timeout_source_id != 0); + + g_main_loop_run( + main_loop + ); + + assert( + g_source_remove( + timeout_source_id + ) + ); + + background_task_unref( + task + ); + + /* + * La première tâche ne doit jamais s'approprier les données fournies + * lors de la seconde tentative refusée. + */ + assert(!second_worker_data_destroyed); + assert(!second_completion_data_destroyed); + + g_main_loop_unref( + main_loop + ); +} + +static void test_background_task_lifetime_worker_data_free( + gpointer user_data +) +{ + TestBackgroundTaskLifetimeWorkerData *worker_data = + user_data; + + assert(worker_data != NULL); + assert(worker_data->worker_data_destroy_count != NULL); + + (*worker_data->worker_data_destroy_count)++; + + g_free( + worker_data + ); +} + +static void test_background_task_lifetime_result_free( + gpointer user_data +) +{ + TestBackgroundTaskLifetimeResult *result = + user_data; + + assert(result != NULL); + assert(result->destroy_count != NULL); + + (*result->destroy_count)++; + + g_free( + result + ); +} + +static void test_background_task_lifetime_completion_data_free( + gpointer user_data +) +{ + TestBackgroundTaskLifetimeCompletionData *completion_data = + user_data; + + assert(completion_data != NULL); + + completion_data->completion_data_destroy_count++; +} + +static gboolean test_background_task_lifetime_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + TestBackgroundTaskLifetimeWorkerData *data = + worker_data; + + TestBackgroundTaskLifetimeResult *worker_result = + NULL; + + assert(task != NULL); + assert(cancellable != NULL); + assert(data != NULL); + assert(result != NULL); + assert(error != NULL); + assert(*error == NULL); + + worker_result = g_new0( + TestBackgroundTaskLifetimeResult, + 1 + ); + + worker_result->destroy_count = + data->result_destroy_count; + + *result = worker_result; + + return TRUE; +} + +static void test_background_task_lifetime_completed( + BackgroundTask *task, + gpointer user_data +) +{ + TestBackgroundTaskLifetimeCompletionData *completion_data = + user_data; + + assert(task != NULL); + assert(completion_data != NULL); + + completion_data->completion_count++; + + assert( + completion_data->completion_count == 1 + ); + + assert( + background_task_get_state(task) == + BACKGROUND_TASK_STATE_COMPLETED + ); + + assert( + background_task_get_result(task) != NULL + ); + + g_main_loop_quit( + completion_data->main_loop + ); +} + +/** + * @brief Vérifie que la tâche survit à la libération immédiate + * de la référence de l'appelant. + */ +static void test_background_task_internal_reference(void) +{ + BackgroundTask *task = NULL; + + TestBackgroundTaskLifetimeWorkerData *worker_data = + NULL; + + TestBackgroundTaskLifetimeCompletionData + completion_data = {0}; + + GMainLoop *main_loop = NULL; + GError *error = NULL; + + guint timeout_source_id = 0; + + guint worker_data_destroy_count = 0; + guint result_destroy_count = 0; + + main_loop = g_main_loop_new( + NULL, + FALSE + ); + + assert(main_loop != NULL); + + completion_data.main_loop = + main_loop; + + worker_data = g_new0( + TestBackgroundTaskLifetimeWorkerData, + 1 + ); + + worker_data->worker_data_destroy_count = + &worker_data_destroy_count; + + worker_data->result_destroy_count = + &result_destroy_count; + + task = background_task_new( + "Test de référence interne" + ); + + assert(task != NULL); + + assert( + background_task_start( + task, + test_background_task_lifetime_worker, + worker_data, + test_background_task_lifetime_worker_data_free, + test_background_task_lifetime_result_free, + test_background_task_lifetime_completed, + &completion_data, + test_background_task_lifetime_completion_data_free, + &error + ) + ); + + assert(error == NULL); + + /* + * La référence de l'appelant disparaît immédiatement. + * La référence interne de l'exécution doit maintenir task en vie. + */ + background_task_unref( + task + ); + + task = NULL; + + timeout_source_id = g_timeout_add_seconds( + TEST_BACKGROUND_TASK_TIMEOUT_SECONDS, + test_background_task_timeout, + main_loop + ); + + assert(timeout_source_id != 0); + + g_main_loop_run( + main_loop + ); + + assert( + g_source_remove( + timeout_source_id + ) + ); + + /* + * Termine les éventuelles destructions attachées à la source GTask. + */ + while (g_main_context_iteration( + NULL, + FALSE + )) + { + } + + assert( + completion_data.completion_count == 1 + ); + + assert( + completion_data.completion_data_destroy_count == 1 + ); + + assert( + worker_data_destroy_count == 1 + ); + + assert( + result_destroy_count == 1 + ); + + g_main_loop_unref( + main_loop + ); +} + +int main(void) +{ + test_background_task_creation(); + test_background_task_success(); + test_background_task_failure(); + test_background_task_invalid_protocol(); + test_background_task_cancellation(); + test_background_task_double_start(); + test_background_task_internal_reference(); + + printf( + "BackgroundTask : tests de construction et de succès valides.\n" + ); + + return 0; +}