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 e6abeda..47f1d00 100755 Binary files a/labfy-investigation and b/labfy-investigation differ 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 0000000..0c76ca0 Binary files /dev/null and b/tests/test_background_task differ 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; +}