diff --git a/Makefile b/Makefile index 6c48d4e..88c5ded 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ TEST_BACKGROUND_TASK := tests/test_background_task TEST_TASK_MANAGER := tests/test_task_manager TEST_TOOL_REGISTRY := tests/test_tool_registry TEST_TOOL_PROCESS := tests/test_tool_process +TEST_TOOL_TASK := tests/test_tool_task all: $(TARGET) @@ -162,6 +163,13 @@ $(TEST_TOOL_PROCESS): \ src/core/tool_process.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +$(TEST_TOOL_TASK): \ + tests/test_tool_task.c \ + src/core/tool_task.c \ + src/core/tool_registry.c \ + src/core/tool_process.c \ + src/core/background_task.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) test: \ $(TEST_NODE) \ @@ -178,7 +186,8 @@ test: \ $(TEST_BACKGROUND_TASK) \ $(TEST_TASK_MANAGER) \ $(TEST_TOOL_REGISTRY) \ - $(TEST_TOOL_PROCESS) + $(TEST_TOOL_PROCESS) \ + $(TEST_TOOL_TASK) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -195,6 +204,7 @@ test: \ @$(TEST_TASK_MANAGER) @$(TEST_TOOL_REGISTRY) @$(TEST_TOOL_PROCESS) + @$(TEST_TOOL_TASK) @echo "Tous les tests sont valides." %.o: %.c @@ -219,6 +229,7 @@ clean: $(TEST_BACKGROUND_TASK) \ $(TEST_TASK_MANAGER) \ $(TEST_TOOL_REGISTRY) \ - $(TEST_TOOL_PROCESS) + $(TEST_TOOL_PROCESS) \ + $(TEST_TOOL_TASK) .PHONY: clean run test diff --git a/docs/tickets/open/TICKET-038.md b/docs/tickets/open/TICKET-038.md new file mode 100644 index 0000000..ef5c1a0 --- /dev/null +++ b/docs/tickets/open/TICKET-038.md @@ -0,0 +1,1083 @@ +# TICKET-038 — Exécution d’un outil externe dans une BackgroundTask + +## Statut + +À faire + +## Priorité + +Haute + +## Objectif + +Créer une couche d’intégration permettant d’exécuter un outil externe dans une `BackgroundTask`, puis de suivre cette exécution avec le `TaskManager` et le panneau d’activité. + +Ce ticket doit relier proprement les modules déjà validés : + +```text +ToolRegistry +ToolProcess +BackgroundTask +TaskManager +TaskPanel +``` + +Le résultat attendu est une abstraction réutilisable capable de : + +- sélectionner un outil enregistré ; +- vérifier sa disponibilité ; +- préparer une exécution structurée ; +- lancer cette exécution dans un thread secondaire ; +- transmettre l’annulation ; +- publier un statut compréhensible ; +- conserver le résultat brut de l’outil ; +- signaler proprement les erreurs ; +- rester indépendante de GTK. + +--- + +## Contexte + +Les tickets précédents ont fourni : + +### Ticket #034 + +```text +BackgroundTask +``` + +pour exécuter un travail asynchrone, publier une progression et gérer l’annulation. + +### Ticket #035 + +```text +TaskManager +TaskPanel +``` + +pour conserver, afficher et annuler les tâches. + +### Ticket #036 + +```text +ToolRegistry +ToolInfo +``` + +pour détecter les exécutables disponibles sur la machine. + +### Ticket #037 + +```text +ToolProcess +ToolProcessResult +``` + +pour lancer un exécutable sans shell, capturer ses sorties et gérer son annulation. + +Il manque maintenant une couche métier qui assemble ces briques sans obliger `Application`, les futurs adaptateurs OSINT ou les widgets GTK à connaître tous les détails de leur fonctionnement interne. + +--- + +## Nom proposé du module + +```text +ToolTask +``` + +Fichiers attendus : + +```text +include/core/tool_task.h +src/core/tool_task.c +tests/test_tool_task.c +``` + +Le nom peut être ajusté avant implémentation s’il existe une meilleure proposition cohérente avec le projet. + +--- + +## Responsabilité du module + +`ToolTask` doit représenter une exécution externe préparée pour être lancée dans une `BackgroundTask`. + +Le module devra : + +1. recevoir un `ToolRegistry` ; +2. rechercher un outil par identifiant ; +3. vérifier que l’outil est disponible ; +4. copier les arguments reçus ; +5. copier le dossier de travail facultatif ; +6. créer une `BackgroundTask` ; +7. utiliser `ToolProcess` dans son worker ; +8. transmettre le `GCancellable` fourni par `BackgroundTask` ; +9. publier un statut avant le lancement ; +10. publier un statut après la fin ; +11. transférer un résultat structuré au propriétaire de la tâche ; +12. convertir les erreurs de préparation en erreurs `ToolTask` ; +13. laisser les erreurs d’exécution de l’outil disponibles dans le résultat ou dans la tâche selon leur nature. + +--- + +## Hors périmètre + +Ce ticket ne doit pas encore : + +- ajouter un outil OSINT réel à l’interface ; +- créer un adaptateur DNS ; +- analyser la sortie d’un outil ; +- enregistrer une preuve ; +- écrire les sorties dans SQLite ; +- sauvegarder les résultats bruts sur disque ; +- afficher directement les sorties dans GTK ; +- gérer une limite de temps ; +- gérer stdin ; +- relancer automatiquement une commande ; +- gérer un pipeline entre plusieurs outils ; +- déterminer automatiquement les arguments d’un outil ; +- modifier le `ToolRegistry`. + +Le premier adaptateur OSINT concret viendra après ce ticket. + +--- + +## Contraintes générales + +- C17. +- GLib et GIO. +- Aucune dépendance GTK. +- Compilation stricte : + +```text +-Wall -Wextra -Wpedantic -Werror +``` + +- Aucun shell. +- Aucune concaténation de commande. +- Les arguments doivent rester séparés. +- Les structures publiques doivent être opaques. +- Les fonctions doivent être préfixées par : + +```text +tool_task_ +tool_task_result_ +``` + +- Aucun état global mutable. +- Les règles de propriété doivent être documentées. +- Le module doit être testable sans dépendre d’un outil réellement installé. + +--- + +## Modèle public + +### ToolTaskResult + +Créer une structure opaque : + +```c +typedef struct ToolTaskResult ToolTaskResult; +``` + +Cette structure doit conserver au minimum : + +```text +tool_identifier +executable_path +arguments +working_directory +process_result +``` + +Le champ `process_result` doit contenir le `ToolProcessResult` produit par `ToolProcess`. + +Le résultat doit être indépendant du `ToolRegistry` après sa création. + +### ToolTask + +Deux approches sont possibles. + +#### Approche recommandée + +Ne pas créer une structure persistante `ToolTask`. + +Créer directement une `BackgroundTask` configurée à partir d’une requête. + +Exemple : + +```c +BackgroundTask *tool_task_create( + const ToolRegistry *tool_registry, + const char *tool_identifier, + const char *task_title, + const char *const arguments[], + const char *working_directory, + GError **error +); +``` + +Le `BackgroundTask` retourné n’est pas encore démarré. + +Une seconde fonction démarre la tâche : + +```c +gboolean tool_task_start( + BackgroundTask *background_task, + BackgroundTaskCompletedCallback completed_callback, + gpointer completed_user_data, + GDestroyNotify completed_user_data_destroy, + GError **error +); +``` + +#### Approche alternative + +Créer une structure opaque `ToolTask` possédant sa `BackgroundTask`. + +Cette approche n’est acceptable que si elle simplifie clairement la propriété et les tests. + +--- + +## Domaine d’erreur + +Créer : + +```c +#define TOOL_TASK_ERROR \ + tool_task_error_quark() +``` + +Énumération minimale : + +```c +typedef enum +{ + TOOL_TASK_ERROR_INVALID_ARGUMENT, + TOOL_TASK_ERROR_TOOL_NOT_FOUND, + TOOL_TASK_ERROR_TOOL_NOT_CHECKED, + TOOL_TASK_ERROR_TOOL_MISSING, + TOOL_TASK_ERROR_TASK_CREATION, + TOOL_TASK_ERROR_TASK_START, + TOOL_TASK_ERROR_PROCESS +} ToolTaskError; +``` + +Fonction : + +```c +GQuark tool_task_error_quark(void); +``` + +--- + +## Règles de préparation + +### Outil inconnu + +Si l’identifiant n’existe pas dans le registre : + +```text +retour : NULL +erreur : TOOL_TASK_ERROR_TOOL_NOT_FOUND +``` + +### Outil non vérifié + +Si l’état vaut : + +```text +TOOL_AVAILABILITY_UNKNOWN +``` + +la création doit échouer avec : + +```text +TOOL_TASK_ERROR_TOOL_NOT_CHECKED +``` + +Le module ne doit pas appeler automatiquement `tool_registry_refresh()`. + +Cette décision garde les responsabilités séparées. + +### Outil absent + +Si l’état vaut : + +```text +TOOL_AVAILABILITY_MISSING +``` + +la création doit échouer avec : + +```text +TOOL_TASK_ERROR_TOOL_MISSING +``` + +### Outil disponible sans chemin + +Un outil marqué disponible mais sans chemin résolu représente un état incohérent. + +La création doit échouer. + +### Arguments + +Le tableau d’arguments : + +```c +const char *const arguments[] +``` + +peut être `NULL`. + +Lorsqu’il est fourni : + +- chaque chaîne doit être non `NULL` jusqu’au terminateur ; +- les chaînes doivent être dupliquées ; +- leur ordre doit être conservé ; +- aucune interprétation ne doit être faite ; +- le tableau interne doit se terminer par `NULL`. + +### Titre + +Le titre de la tâche doit être non vide. + +Exemple : + +```text +Interrogation DNS de example.org +``` + +Le titre ne doit pas être fabriqué automatiquement par le module. + +--- + +## Cycle de vie recommandé + +### Création + +```c +BackgroundTask *tool_task_create( + const ToolRegistry *tool_registry, + const char *tool_identifier, + const char *task_title, + const char *const arguments[], + const char *working_directory, + GError **error +); +``` + +Comportement : + +1. valider les arguments ; +2. rechercher `ToolInfo` ; +3. vérifier l’état ; +4. récupérer le chemin résolu ; +5. dupliquer : + - identifiant ; + - chemin ; + - arguments ; + - dossier de travail ; +6. créer les données du worker ; +7. créer une `BackgroundTask` ; +8. attacher les données au futur lancement. + +### Démarrage + +```c +gboolean tool_task_start( + BackgroundTask *background_task, + BackgroundTaskCompletedCallback completed_callback, + gpointer completed_user_data, + GDestroyNotify completed_user_data_destroy, + GError **error +); +``` + +Cette fonction doit : + +1. vérifier que la tâche provient bien de `ToolTask` ; +2. appeler `background_task_start()` ; +3. transmettre : + - le worker ; + - les données du worker ; + - leur destructeur ; + - le destructeur du résultat ; + - le callback de fin ; + - ses données utilisateur. + +La fonction ne doit pas ajouter elle-même la tâche dans un `TaskManager`. + +Cette responsabilité reste à l’appelant. + +--- + +## Worker + +Le worker doit respecter la signature actuelle de `BackgroundTask`. + +Comportement attendu : + +1. publier : + +```text +Préparation de l’exécution +``` + +2. publier : + +```text +Exécution de +``` + +3. appeler `tool_process_run()` avec : + - le chemin détecté ; + - les arguments copiés ; + - le dossier de travail ; + - le `GCancellable` reçu ; +4. si l’appel échoue : + - propager une erreur exploitable ; + - ne produire aucun résultat ; +5. si l’appel réussit : + - créer `ToolTaskResult` ; + - y transférer `ToolProcessResult` ; + - publier un statut final ; +6. placer le résultat dans : + +```c +*result +``` + +### Progression + +Une commande externe ne fournit pas toujours une progression mesurable. + +Pour ce ticket, utiliser une progression qualitative : + +```text +0.0 Préparation +0.1 Lancement +0.9 Exécution terminée, traitement du résultat +1.0 Terminé +``` + +Il ne faut pas simuler une progression régulière avec un minuteur. + +--- + +## Code de sortie non nul + +Un programme lancé correctement mais terminant avec un code non nul ne doit pas faire échouer la `BackgroundTask`. + +Dans ce cas : + +```text +BackgroundTask : COMPLETED +ToolTaskResult : disponible +ToolProcessResult : is_success == FALSE +``` + +Le résultat doit conserver : + +- le code de sortie ; +- stdout ; +- stderr. + +Cette distinction est essentielle pour les futurs adaptateurs. + +Exemple : + +```text +dig retourne 9 +``` + +La tâche technique est terminée, mais le résultat fonctionnel signale un échec. + +--- + +## Annulation + +Le `GCancellable` reçu par le worker doit être transmis directement à : + +```c +tool_process_run() +``` + +Si `ToolProcess` retourne : + +```text +TOOL_PROCESS_ERROR_CANCELLED +``` + +le worker doit permettre à `BackgroundTask` d’aboutir à l’état : + +```text +BACKGROUND_TASK_STATE_CANCELLED +``` + +Il ne doit pas transformer l’annulation en erreur métier générique. + +Aucun processus enfant ne doit rester actif. + +--- + +## Résultat + +### Cycle de vie + +```c +void tool_task_result_free( + ToolTaskResult *result +); +``` + +La fonction doit accepter `NULL`. + +### Identifiant + +```c +const char *tool_task_result_get_tool_identifier( + const ToolTaskResult *result +); +``` + +Chaîne empruntée. + +### Chemin de l’exécutable + +```c +const char *tool_task_result_get_executable_path( + const ToolTaskResult *result +); +``` + +Chaîne empruntée. + +### Arguments + +```c +gsize tool_task_result_get_argument_count( + const ToolTaskResult *result +); +``` + +```c +const char *tool_task_result_get_argument( + const ToolTaskResult *result, + gsize index +); +``` + +Chaînes empruntées. + +### Dossier de travail + +```c +const char *tool_task_result_get_working_directory( + const ToolTaskResult *result +); +``` + +Retourne `NULL` si aucun dossier n’a été défini. + +### Résultat du processus + +```c +const ToolProcessResult *tool_task_result_get_process_result( + const ToolTaskResult *result +); +``` + +Pointeur emprunté. + +Une fonction de transfert ou de référence n’est pas nécessaire pour ce ticket. + +--- + +## Propriété des données + +### Données de préparation + +Le module doit copier toutes les données nécessaires avant le démarrage. + +Il ne doit pas dépendre de la durée de vie : + +- du `ToolRegistry` ; +- du `ToolInfo` ; +- du tableau d’arguments d’origine ; +- du titre d’origine ; +- du dossier de travail d’origine. + +### Résultat + +`ToolTaskResult` devient propriétaire de : + +- l’identifiant copié ; +- le chemin copié ; +- la copie des arguments ; +- la copie du dossier de travail ; +- `ToolProcessResult`. + +Le destructeur doit tout libérer. + +### BackgroundTask + +La propriété de la `BackgroundTask` reste conforme au ticket #034. + +--- + +## API publique suggérée + +```c +typedef struct ToolTaskResult ToolTaskResult; + +typedef enum +{ + TOOL_TASK_ERROR_INVALID_ARGUMENT, + TOOL_TASK_ERROR_TOOL_NOT_FOUND, + TOOL_TASK_ERROR_TOOL_NOT_CHECKED, + TOOL_TASK_ERROR_TOOL_MISSING, + TOOL_TASK_ERROR_TASK_CREATION, + TOOL_TASK_ERROR_TASK_START, + TOOL_TASK_ERROR_PROCESS +} ToolTaskError; + +#define TOOL_TASK_ERROR \ + tool_task_error_quark() + +GQuark tool_task_error_quark(void); + +BackgroundTask *tool_task_create( + const ToolRegistry *tool_registry, + const char *tool_identifier, + const char *task_title, + const char *const arguments[], + const char *working_directory, + GError **error +); + +gboolean tool_task_start( + BackgroundTask *background_task, + BackgroundTaskCompletedCallback completed_callback, + gpointer completed_user_data, + GDestroyNotify completed_user_data_destroy, + GError **error +); + +void tool_task_result_free( + ToolTaskResult *result +); + +const char *tool_task_result_get_tool_identifier( + const ToolTaskResult *result +); + +const char *tool_task_result_get_executable_path( + const ToolTaskResult *result +); + +gsize tool_task_result_get_argument_count( + const ToolTaskResult *result +); + +const char *tool_task_result_get_argument( + const ToolTaskResult *result, + gsize index +); + +const char *tool_task_result_get_working_directory( + const ToolTaskResult *result +); + +const ToolProcessResult *tool_task_result_get_process_result( + const ToolTaskResult *result +); +``` + +L’API peut évoluer si l’implémentation de `BackgroundTask` rend une autre forme plus sûre. + +--- + +## Marquage d’une BackgroundTask + +`tool_task_start()` doit pouvoir vérifier que la tâche reçue a été créée par `tool_task_create()`. + +Approches possibles : + +### Structure privée associée + +Conserver les données du worker dans une structure privée connue uniquement de `ToolTask`. + +### Extension de BackgroundTask + +Ajouter un pointeur de contexte privé à `BackgroundTask` uniquement si cela reste générique et justifié. + +### Wrapper opaque + +Créer un `ToolTask` opaque contenant la `BackgroundTask`. + +Cette solution peut être préférable si la vérification devient fragile. + +Le choix final doit privilégier : + +- sécurité de propriété ; +- lisibilité ; +- testabilité ; +- absence de cast dangereux. + +--- + +## Tests unitaires obligatoires + +Les tests doivent créer un faux outil temporaire. + +Ils ne doivent pas dépendre d’un outil réel. + +--- + +### 1. Arguments invalides + +Tester au minimum : + +- registre `NULL` ; +- identifiant `NULL` ; +- identifiant vide ; +- titre `NULL` ; +- titre vide ; +- dossier de travail vide ; +- `GError` déjà initialisé si la convention est vérifiée. + +Résultat attendu : + +```text +TOOL_TASK_ERROR_INVALID_ARGUMENT +``` + +--- + +### 2. Outil inconnu + +Registre valide mais identifiant absent. + +Résultat : + +```text +TOOL_TASK_ERROR_TOOL_NOT_FOUND +``` + +--- + +### 3. Outil non vérifié + +Enregistrer un outil sans appeler `tool_registry_refresh()`. + +Résultat : + +```text +TOOL_TASK_ERROR_TOOL_NOT_CHECKED +``` + +--- + +### 4. Outil absent + +Enregistrer un faux outil absent, puis rafraîchir. + +Résultat : + +```text +TOOL_TASK_ERROR_TOOL_MISSING +``` + +--- + +### 5. Création valide + +Créer un faux outil dans un `PATH` temporaire. + +Vérifier : + +- registre rafraîchi ; +- création de la `BackgroundTask` ; +- titre correct ; +- état initial `PENDING`. + +--- + +### 6. Copie des arguments + +Créer les arguments avec des chaînes dynamiques. + +Créer la tâche, puis libérer les chaînes originales. + +Démarrer la tâche. + +Le résultat doit toujours contenir les bonnes valeurs. + +--- + +### 7. Exécution réussie + +Le faux outil écrit dans stdout et retourne zéro. + +Vérifier : + +- tâche terminée ; +- état `COMPLETED` ; +- résultat non `NULL` ; +- identifiant correct ; +- chemin correct ; +- arguments corrects ; +- `ToolProcessResult` disponible ; +- `is_success == TRUE`. + +--- + +### 8. Code de sortie non nul + +Le faux outil retourne : + +```text +exit 7 +``` + +Vérifier : + +- tâche `COMPLETED` ; +- résultat disponible ; +- code `7` ; +- succès fonctionnel `FALSE`. + +--- + +### 9. Erreur de lancement + +Créer la tâche avec un exécutable détecté, puis supprimer le fichier avant le démarrage. + +Vérifier : + +- tâche `FAILED` ; +- erreur exploitable ; +- aucun résultat. + +--- + +### 10. Annulation + +Créer un faux outil long. + +Démarrer la tâche puis l’annuler. + +Vérifier : + +- état final `CANCELLED` ; +- aucun résultat ; +- aucun processus enfant actif. + +--- + +### 11. Dossier de travail + +Le faux outil affiche son dossier courant. + +Vérifier que le résultat correspond au dossier demandé. + +--- + +### 12. Plusieurs tâches simultanées + +Créer deux tâches à partir du même outil avec des arguments différents. + +Les lancer simultanément. + +Vérifier : + +- deux résultats indépendants ; +- aucun mélange des arguments ; +- aucun partage incorrect des sorties ; +- aucun état global mutable. + +--- + +### 13. Destruction avant démarrage + +Créer une tâche puis la libérer sans la lancer. + +Vérifier : + +- aucune fuite ; +- destruction des copies d’arguments ; +- destruction des données privées. + +--- + +### 14. Destruction après exécution + +Exécuter une tâche, lire le résultat, puis libérer la tâche. + +Vérifier la libération complète. + +--- + +## Noms de tests suggérés + +```text +/tool_task/invalid_arguments +/tool_task/tool_not_found +/tool_task/tool_not_checked +/tool_task/tool_missing +/tool_task/create +/tool_task/argument_ownership +/tool_task/success +/tool_task/nonzero_exit +/tool_task/spawn_failure +/tool_task/cancellation +/tool_task/working_directory +/tool_task/concurrent_tasks +/tool_task/free_before_start +/tool_task/free_after_completion +``` + +--- + +## Synchronisation des tests + +Les tests doivent utiliser une `GMainLoop` pour attendre la fin des `BackgroundTask`. + +Ils ne doivent pas utiliser une attente active infinie. + +Prévoir un timeout de sécurité afin qu’un test défectueux ne bloque pas toute la suite. + +Exemple : + +```text +5 secondes maximum pour un test normal +``` + +Le test d’annulation doit se terminer rapidement. + +--- + +## Makefile + +Ajouter : + +```make +TEST_TOOL_TASK := tests/test_tool_task +``` + +Règle attendue : + +```make +$(TEST_TOOL_TASK): \ + tests/test_tool_task.c \ + src/core/tool_task.c \ + src/core/tool_registry.c \ + src/core/tool_process.c \ + src/core/background_task.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +``` + +Ajouter la cible à : + +```text +make test +make clean +``` + +--- + +## Vérifications manuelles + +```bash +make clean +make +make tests/test_tool_task +./tests/test_tool_task +make test +``` + +--- + +## Vérification mémoire + +```bash +G_DEBUG=gc-friendly \ +G_SLICE=always-malloc \ +valgrind \ + --leak-check=full \ + --show-leak-kinds=all \ + ./tests/test_tool_task +``` + +Surveiller particulièrement : + +- création sans démarrage ; +- échec de démarrage ; +- annulation ; +- résultat avec code non nul ; +- tâches simultanées. + +--- + +## Critères d’acceptation + +Le ticket est validé lorsque : + +- le module compile en C17 strict ; +- aucune dépendance GTK n’est ajoutée ; +- un outil est recherché par identifiant dans `ToolRegistry` ; +- les états `UNKNOWN` et `MISSING` sont correctement refusés ; +- le chemin résolu est copié ; +- les arguments sont copiés ; +- le dossier de travail est copié ; +- la tâche utilise `ToolProcess` dans un thread secondaire ; +- le `GCancellable` est transmis ; +- l’annulation aboutit à `CANCELLED` ; +- un code de sortie non nul produit une tâche `COMPLETED` ; +- le résultat brut est accessible ; +- les résultats sont indépendants du registre après création ; +- plusieurs tâches peuvent fonctionner simultanément ; +- aucun shell n’est utilisé ; +- aucun processus enfant n’est abandonné ; +- aucune fuite mémoire n’est détectée ; +- tous les tests passent ; +- le test est intégré au `Makefile`. + +--- + +## Démonstration finale prévue + +Après validation du module, remplacer temporairement le bouton actuel : + +```text +Tâche de test +``` + +par une vraie démonstration basée sur un faux outil ou un outil stable détecté. + +Cette démonstration devra passer par : + +```text +ToolRegistry +→ ToolTask +→ BackgroundTask +→ TaskManager +→ TaskPanel +``` + +Le bouton temporaire sera supprimé dès que le premier adaptateur OSINT réel sera disponible. + +--- + +## Suite prévue + +Après ce ticket : + +1. ticket #039 — catalogue initial des outils externes ; +2. interrogation de leurs versions ; +3. ticket d’adaptateur DNS ; +4. conservation structurée des exécutions ; +5. stockage des sorties brutes ; +6. création d’observations et de preuves ; +7. affichage dans le workspace ; +8. packaging Ubuntu avec dépendances optionnelles. diff --git a/include/core/tool_task.h b/include/core/tool_task.h new file mode 100644 index 0000000..cdc3e7b --- /dev/null +++ b/include/core/tool_task.h @@ -0,0 +1,299 @@ +/****************************************************************************** + * @file tool_task.h + * @brief Exécution d'un outil externe dans une tâche d'arrière-plan. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_TOOL_TASK_H +#define LABFY_INVESTIGATION_TOOL_TASK_H + +#include "core/background_task.h" +#include "core/tool_process.h" +#include "core/tool_registry.h" + +#include +#include + +G_BEGIN_DECLS + +/** + * @brief Erreurs produites par ToolTask. + */ +typedef enum +{ + /** + * Un argument transmis au module est invalide. + */ + TOOL_TASK_ERROR_INVALID_ARGUMENT, + + /** + * L'identifiant demandé n'existe pas dans le registre. + */ + TOOL_TASK_ERROR_TOOL_NOT_FOUND, + + /** + * La disponibilité de l'outil n'a pas encore été vérifiée. + */ + TOOL_TASK_ERROR_TOOL_NOT_CHECKED, + + /** + * L'outil a été vérifié mais son exécutable est absent. + */ + TOOL_TASK_ERROR_TOOL_MISSING, + + /** + * L'outil est disponible, mais son chemin résolu est invalide. + */ + TOOL_TASK_ERROR_INVALID_TOOL_STATE, + + /** + * La BackgroundTask n'a pas pu être créée. + */ + TOOL_TASK_ERROR_TASK_CREATION, + + /** + * La BackgroundTask n'a pas pu être démarrée. + */ + TOOL_TASK_ERROR_TASK_START, + + /** + * L'exécution externe a échoué. + */ + TOOL_TASK_ERROR_PROCESS +} ToolTaskError; + +/** + * @brief Domaine d'erreur de ToolTask. + */ +#define TOOL_TASK_ERROR \ + tool_task_error_quark() + +/** + * @brief Exécution préparée d'un outil externe. + * + * Cette structure possède : + * + * - la BackgroundTask avant son transfert éventuel à d'autres modules ; + * - les données du worker avant le démarrage ; + * - les copies de l'identifiant, du chemin et des arguments. + */ +typedef struct ToolTask ToolTask; + +/** + * @brief Résultat opaque produit par une ToolTask. + */ +typedef struct ToolTaskResult ToolTaskResult; + +/** + * @brief Retourne le domaine d'erreur de ToolTask. + * + * @return Quark GLib du domaine d'erreur. + */ +GQuark tool_task_error_quark(void); + +/** + * @brief Prépare une tâche exécutant un outil enregistré. + * + * Cette fonction ne démarre pas la tâche. + * + * Toutes les informations nécessaires à l'exécution sont copiées. Le + * ToolTask créé ne dépend donc plus de la durée de vie du registre ni des + * chaînes fournies. + * + * L'outil doit avoir été vérifié avec tool_registry_refresh() et être dans + * l'état TOOL_AVAILABILITY_AVAILABLE. + * + * @param tool_registry Registre contenant l'outil. + * @param tool_identifier Identifiant interne de l'outil. + * @param task_title Titre affiché pour la BackgroundTask. + * @param arguments Tableau d'arguments terminé par NULL, ou NULL. + * @param working_directory Dossier de travail facultatif, ou NULL. + * @param error Emplacement facultatif pour l'erreur. + * + * @return Nouveau ToolTask, ou NULL en cas d'échec. + */ +ToolTask *tool_task_new( + const ToolRegistry *tool_registry, + const char *tool_identifier, + const char *task_title, + const char *const arguments[], + const char *working_directory, + GError **error +); + +/** + * @brief Libère une ToolTask. + * + * Avant le démarrage, cette fonction libère également les données préparées + * pour le worker. + * + * Après un démarrage réussi, les données du worker appartiennent à la + * BackgroundTask. + * + * La fonction accepte NULL. + * + * @param tool_task Tâche à libérer. + */ +void tool_task_free( + ToolTask *tool_task +); + +/** + * @brief Retourne la BackgroundTask associée. + * + * Le pointeur retourné est emprunté. Il reste valide tant que ToolTask + * conserve sa propre référence ou qu'un autre propriétaire, par exemple + * TaskManager, conserve une référence. + * + * Cette fonction permet notamment d'ajouter la tâche au TaskManager avant + * son démarrage. + * + * @param tool_task ToolTask consultée. + * + * @return BackgroundTask associée, ou NULL. + */ +BackgroundTask *tool_task_get_background_task( + const ToolTask *tool_task +); + +/** + * @brief Démarre l'exécution asynchrone de l'outil. + * + * Si le démarrage réussit : + * + * - la BackgroundTask devient propriétaire des données du worker ; + * - ToolTask ne doit plus les libérer ; + * - ToolTask peut être libérée dès qu'un autre propriétaire conserve une + * référence sur la BackgroundTask. + * + * Si le démarrage échoue, ToolTask conserve la propriété de ses données. + * + * @param tool_task ToolTask à démarrer. + * @param completion_callback Callback final facultatif. + * @param completion_data Données facultatives du callback. + * @param completion_data_destroy Destructeur facultatif des données. + * @param error Emplacement facultatif pour l'erreur. + * + * @return TRUE si la tâche a été démarrée, sinon FALSE. + */ +gboolean tool_task_start( + ToolTask *tool_task, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +); + +/** + * @brief Retourne le résultat d'une BackgroundTask créée par ToolTask. + * + * Cette fonction doit uniquement être appelée avec une BackgroundTask + * obtenue par tool_task_get_background_task(). + * + * Le pointeur retourné est emprunté et appartient à la BackgroundTask. + * Il n'est disponible qu'après l'état BACKGROUND_TASK_STATE_COMPLETED. + * + * @param background_task Tâche créée par ToolTask. + * + * @return Résultat ToolTask emprunté, ou NULL. + */ +const ToolTaskResult *tool_task_result_from_background_task( + const BackgroundTask *background_task +); + +/** + * @brief Libère un résultat ToolTask. + * + * Cette fonction est normalement transmise comme destructeur du résultat à + * background_task_start(). + * + * La fonction accepte NULL. + * + * @param result Résultat à libérer. + */ +void tool_task_result_free( + ToolTaskResult *result +); + +/** + * @brief Retourne l'identifiant de l'outil exécuté. + * + * La chaîne retournée est empruntée. + * + * @param result Résultat consulté. + * + * @return Identifiant de l'outil, ou NULL. + */ +const char *tool_task_result_get_tool_identifier( + const ToolTaskResult *result +); + +/** + * @brief Retourne le chemin de l'exécutable utilisé. + * + * La chaîne retournée est empruntée. + * + * @param result Résultat consulté. + * + * @return Chemin de l'exécutable, ou NULL. + */ +const char *tool_task_result_get_executable_path( + const ToolTaskResult *result +); + +/** + * @brief Retourne le nombre d'arguments transmis à l'outil. + * + * Le chemin de l'exécutable, utilisé comme argv[0], n'est pas compté. + * + * @param result Résultat consulté. + * + * @return Nombre d'arguments. + */ +gsize tool_task_result_get_argument_count( + const ToolTaskResult *result +); + +/** + * @brief Retourne un argument transmis à l'outil. + * + * La chaîne retournée est empruntée. + * + * @param result Résultat consulté. + * @param index Index de l'argument. + * + * @return Argument correspondant, ou NULL si l'index est invalide. + */ +const char *tool_task_result_get_argument( + const ToolTaskResult *result, + gsize index +); + +/** + * @brief Retourne le dossier de travail utilisé. + * + * La chaîne retournée est empruntée. + * + * @param result Résultat consulté. + * + * @return Dossier de travail, ou NULL. + */ +const char *tool_task_result_get_working_directory( + const ToolTaskResult *result +); + +/** + * @brief Retourne le résultat brut du processus externe. + * + * Le pointeur retourné est emprunté et ne doit pas être libéré. + * + * @param result Résultat consulté. + * + * @return Résultat ToolProcess, ou NULL. + */ +const ToolProcessResult *tool_task_result_get_process_result( + const ToolTaskResult *result +); + +G_END_DECLS + +#endif diff --git a/labfy-investigation b/labfy-investigation index a960d38..65db09d 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/core/tool_task.c b/src/core/tool_task.c new file mode 100644 index 0000000..3780107 --- /dev/null +++ b/src/core/tool_task.c @@ -0,0 +1,981 @@ +/****************************************************************************** + * @file tool_task.c + * @brief Exécution d'un outil externe dans une tâche d'arrière-plan. + ******************************************************************************/ + +#include "core/tool_task.h" + +/** + * @struct ToolTaskWorkerData + * @brief Données privées transmises au worker. + */ +typedef struct +{ + char *tool_identifier; + char *tool_display_name; + char *executable_path; + + char **arguments; + gsize argument_count; + + char *working_directory; +} ToolTaskWorkerData; + +/** + * @struct ToolTask + * @brief Exécution externe préparée. + */ +struct ToolTask +{ + BackgroundTask *background_task; + ToolTaskWorkerData *worker_data; + + gboolean started; +}; + +/** + * @struct ToolTaskResult + * @brief Résultat d'une exécution externe en arrière-plan. + */ +struct ToolTaskResult +{ + char *tool_identifier; + char *executable_path; + + char **arguments; + gsize argument_count; + + char *working_directory; + + ToolProcessResult *process_result; +}; + +/** + * @brief Vérifie qu'une chaîne est définie et non vide. + * + * @param text Chaîne à vérifier. + * + * @return TRUE si la chaîne est valide. + */ +static gboolean tool_task_string_is_valid( + const char *text +) +{ + return text != NULL && + text[0] != '\0'; +} + +/** + * @brief Duplique un tableau d'arguments terminé par NULL. + * + * Un tableau vide terminé par NULL est créé lorsque arguments vaut NULL. + * + * @param arguments Arguments à copier, ou NULL. + * @param out_argument_count Emplacement recevant le nombre d'arguments. + * + * @return Nouveau tableau terminé par NULL. + */ +static char **tool_task_duplicate_arguments( + const char *const arguments[], + gsize *out_argument_count +) +{ + char **arguments_copy = NULL; + + gsize argument_count = 0; + gsize argument_index = 0; + + if (out_argument_count != NULL) + { + *out_argument_count = 0; + } + + if (arguments != NULL) + { + while (arguments[argument_count] != NULL) + { + argument_count++; + } + } + + arguments_copy = g_new0( + char *, + argument_count + 1 + ); + + for (argument_index = 0; + argument_index < argument_count; + argument_index++) + { + arguments_copy[argument_index] = + g_strdup( + arguments[argument_index] + ); + } + + arguments_copy[argument_count] = NULL; + + if (out_argument_count != NULL) + { + *out_argument_count = + argument_count; + } + + return arguments_copy; +} + +/** + * @brief Libère les données privées du worker. + * + * @param user_data Pointeur vers ToolTaskWorkerData. + */ +static void tool_task_worker_data_free( + gpointer user_data +) +{ + ToolTaskWorkerData *worker_data = + user_data; + + if (worker_data == NULL) + { + return; + } + + g_clear_pointer( + &worker_data->tool_identifier, + g_free + ); + + g_clear_pointer( + &worker_data->tool_display_name, + g_free + ); + + g_clear_pointer( + &worker_data->executable_path, + g_free + ); + + g_clear_pointer( + &worker_data->arguments, + g_strfreev + ); + + g_clear_pointer( + &worker_data->working_directory, + g_free + ); + + g_free( + worker_data + ); +} + +/** + * @brief Adapte tool_task_result_free() à GDestroyNotify. + * + * @param user_data Pointeur vers ToolTaskResult. + */ +static void tool_task_result_destroy( + gpointer user_data +) +{ + tool_task_result_free( + user_data + ); +} + +/** + * @brief Produit une erreur ToolTask avec une cause facultative. + * + * @param error Emplacement facultatif pour l'erreur. + * @param error_code Code ToolTask. + * @param context_message Description de l'échec. + * @param cause Erreur d'origine facultative. + */ +static void tool_task_set_wrapped_error( + GError **error, + ToolTaskError error_code, + const char *context_message, + const GError *cause +) +{ + const char *cause_message = NULL; + + if (cause == NULL || + cause->message == NULL) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + error_code, + context_message + ); + + return; + } + + cause_message = cause->message; + + g_set_error( + error, + TOOL_TASK_ERROR, + error_code, + "%s : %s", + context_message, + cause_message + ); +} + +/** + * @brief Crée le résultat ToolTask à partir des données du worker. + * + * La fonction devient propriétaire de process_result. + * + * @param worker_data Données de l'exécution. + * @param process_result Résultat brut du processus. + * + * @return Nouveau résultat. + */ +static ToolTaskResult *tool_task_result_new( + const ToolTaskWorkerData *worker_data, + ToolProcessResult *process_result +) +{ + ToolTaskResult *task_result = NULL; + + if (worker_data == NULL || + process_result == NULL) + { + return NULL; + } + + task_result = g_new0( + ToolTaskResult, + 1 + ); + + task_result->tool_identifier = + g_strdup( + worker_data->tool_identifier + ); + + task_result->executable_path = + g_strdup( + worker_data->executable_path + ); + + task_result->arguments = + tool_task_duplicate_arguments( + (const char *const *) + worker_data->arguments, + &task_result->argument_count + ); + + task_result->working_directory = + g_strdup( + worker_data->working_directory + ); + + task_result->process_result = + process_result; + + return task_result; +} + +/** + * @brief Exécute l'outil externe dans le thread secondaire. + * + * @param background_task Tâche en cours. + * @param cancellable Objet d'annulation. + * @param worker_data_pointer Données privées du worker. + * @param result Emplacement recevant ToolTaskResult. + * @param error Emplacement recevant une erreur. + * + * @return TRUE lorsqu'un résultat exploitable a été produit. + */ +static gboolean tool_task_worker( + BackgroundTask *background_task, + GCancellable *cancellable, + gpointer worker_data_pointer, + gpointer *result, + GError **error +) +{ + ToolTaskWorkerData *worker_data = + worker_data_pointer; + + ToolProcessResult *process_result = NULL; + ToolTaskResult *task_result = NULL; + + GError *process_error = NULL; + + char *execution_status = NULL; + char *final_status = NULL; + + gboolean process_success = FALSE; + + if (background_task == NULL || + cancellable == NULL || + worker_data == NULL || + result == NULL || + error == NULL) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT, + "Les arguments du worker ToolTask sont invalides." + ); + + return FALSE; + } + + *result = NULL; + + background_task_report_progress( + background_task, + 0.0, + "Préparation de l'exécution" + ); + + /* + * Une annulation demandée avant le lancement doit être transmise + * sous la forme attendue par BackgroundTask. + */ + if (g_cancellable_set_error_if_cancelled( + cancellable, + error + )) + { + return FALSE; + } + + execution_status = g_strdup_printf( + "Exécution de %s", + tool_task_string_is_valid( + worker_data->tool_display_name + ) + ? worker_data->tool_display_name + : worker_data->tool_identifier + ); + + background_task_report_progress( + background_task, + 0.1, + execution_status + ); + + g_free( + execution_status + ); + + execution_status = NULL; + + process_success = tool_process_run( + worker_data->executable_path, + (const char *const *) + worker_data->arguments, + worker_data->working_directory, + cancellable, + &process_result, + &process_error + ); + + if (!process_success) + { + /* + * BackgroundTask reconnaît une annulation lorsque le domaine + * d'erreur est G_IO_ERROR et le code G_IO_ERROR_CANCELLED. + */ + if (process_error != NULL && + g_error_matches( + process_error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_CANCELLED + )) + { + g_set_error( + error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED, + "%s", + process_error->message + ); + } + else + { + tool_task_set_wrapped_error( + error, + TOOL_TASK_ERROR_PROCESS, + "L'exécution de l'outil externe a échoué", + process_error + ); + } + + g_clear_error( + &process_error + ); + + return FALSE; + } + + if (process_result == NULL) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_PROCESS, + "ToolProcess n'a produit aucun résultat exploitable." + ); + + return FALSE; + } + + background_task_report_progress( + background_task, + 0.9, + "Traitement du résultat" + ); + + task_result = tool_task_result_new( + worker_data, + process_result + ); + + if (task_result == NULL) + { + tool_process_result_free( + process_result + ); + + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_PROCESS, + "Le résultat de l'outil n'a pas pu être construit." + ); + + return FALSE; + } + + /* + * ToolTaskResult possède maintenant ToolProcessResult. + */ + process_result = NULL; + + if (tool_process_result_is_success( + task_result->process_result + )) + { + final_status = g_strdup( + "Exécution terminée avec succès" + ); + } + else if (tool_process_result_exited_normally( + task_result->process_result + )) + { + final_status = g_strdup_printf( + "Exécution terminée avec le code %d", + tool_process_result_get_exit_status( + task_result->process_result + ) + ); + } + else if (tool_process_result_was_signaled( + task_result->process_result + )) + { + final_status = g_strdup_printf( + "Exécution terminée par le signal %d", + tool_process_result_get_termination_signal( + task_result->process_result + ) + ); + } + else + { + final_status = g_strdup( + "Exécution terminée" + ); + } + + background_task_report_progress( + background_task, + 1.0, + final_status + ); + + g_free( + final_status + ); + + *result = task_result; + + return TRUE; +} + +GQuark tool_task_error_quark(void) +{ + return g_quark_from_static_string( + "labfy-investigation-tool-task-error" + ); +} + +ToolTask *tool_task_new( + const ToolRegistry *tool_registry, + const char *tool_identifier, + const char *task_title, + const char *const arguments[], + const char *working_directory, + GError **error +) +{ + ToolTask *tool_task = NULL; + ToolTaskWorkerData *worker_data = NULL; + + const ToolInfo *tool_info = NULL; + + const char *tool_display_name = NULL; + const char *executable_path = NULL; + + ToolAvailability availability; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (tool_registry == NULL || + !tool_task_string_is_valid( + tool_identifier + ) || + !tool_task_string_is_valid( + task_title + ) || + (working_directory != NULL && + working_directory[0] == '\0')) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT, + "Les arguments fournis à ToolTask sont invalides." + ); + + return NULL; + } + + tool_info = tool_registry_find( + tool_registry, + tool_identifier + ); + + if (tool_info == NULL) + { + g_set_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_NOT_FOUND, + "Aucun outil portant l'identifiant '%s' n'est enregistré.", + tool_identifier + ); + + return NULL; + } + + availability = tool_info_get_availability( + tool_info + ); + + if (availability == + TOOL_AVAILABILITY_UNKNOWN) + { + g_set_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_NOT_CHECKED, + "La disponibilité de l'outil '%s' n'a pas été vérifiée.", + tool_identifier + ); + + return NULL; + } + + if (availability == + TOOL_AVAILABILITY_MISSING) + { + g_set_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_MISSING, + "L'outil '%s' est absent de la machine.", + tool_identifier + ); + + return NULL; + } + + executable_path = + tool_info_get_resolved_path( + tool_info + ); + + if (!tool_task_string_is_valid( + executable_path + )) + { + g_set_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_TOOL_STATE, + "L'outil '%s' est disponible mais ne possède aucun chemin.", + tool_identifier + ); + + return NULL; + } + + tool_display_name = + tool_info_get_display_name( + tool_info + ); + + worker_data = g_new0( + ToolTaskWorkerData, + 1 + ); + + worker_data->tool_identifier = + g_strdup( + tool_identifier + ); + + worker_data->tool_display_name = + g_strdup( + tool_display_name + ); + + worker_data->executable_path = + g_strdup( + executable_path + ); + + worker_data->arguments = + tool_task_duplicate_arguments( + arguments, + &worker_data->argument_count + ); + + worker_data->working_directory = + g_strdup( + working_directory + ); + + tool_task = g_new0( + ToolTask, + 1 + ); + + tool_task->background_task = + background_task_new( + task_title + ); + + if (tool_task->background_task == NULL) + { + tool_task_worker_data_free( + worker_data + ); + + g_free( + tool_task + ); + + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TASK_CREATION, + "La tâche d'arrière-plan n'a pas pu être créée." + ); + + return NULL; + } + + tool_task->worker_data = + worker_data; + + tool_task->started = + FALSE; + + return tool_task; +} + +void tool_task_free( + ToolTask *tool_task +) +{ + if (tool_task == NULL) + { + return; + } + + /* + * Avant le démarrage, ToolTask possède les données du worker. + * + * Après un démarrage réussi, le pointeur vaut NULL car la propriété + * a été transférée à BackgroundTask. + */ + if (tool_task->worker_data != NULL) + { + tool_task_worker_data_free( + tool_task->worker_data + ); + + tool_task->worker_data = NULL; + } + + background_task_unref( + tool_task->background_task + ); + + tool_task->background_task = NULL; + + g_free( + tool_task + ); +} + +BackgroundTask *tool_task_get_background_task( + const ToolTask *tool_task +) +{ + if (tool_task == NULL) + { + return NULL; + } + + return tool_task->background_task; +} + +gboolean tool_task_start( + ToolTask *tool_task, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +) +{ + GError *start_error = NULL; + gboolean start_success = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (tool_task == NULL || + tool_task->background_task == NULL || + tool_task->worker_data == NULL) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT, + "La ToolTask fournie est invalide." + ); + + return FALSE; + } + + if (tool_task->started || + background_task_get_state( + tool_task->background_task + ) != BACKGROUND_TASK_STATE_PENDING) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TASK_START, + "La ToolTask a déjà été démarrée." + ); + + return FALSE; + } + + start_success = background_task_start( + tool_task->background_task, + tool_task_worker, + tool_task->worker_data, + tool_task_worker_data_free, + tool_task_result_destroy, + completion_callback, + completion_data, + completion_data_destroy, + &start_error + ); + + if (!start_success) + { + tool_task_set_wrapped_error( + error, + TOOL_TASK_ERROR_TASK_START, + "La tâche d'arrière-plan n'a pas pu être démarrée", + start_error + ); + + g_clear_error( + &start_error + ); + + /* + * BackgroundTask n'a pas pris possession des données. + */ + return FALSE; + } + + /* + * La propriété des données du worker appartient désormais à + * BackgroundTask. + */ + tool_task->worker_data = NULL; + tool_task->started = TRUE; + + return TRUE; +} + +const ToolTaskResult *tool_task_result_from_background_task( + const BackgroundTask *background_task +) +{ + if (background_task == NULL || + background_task_get_state( + background_task + ) != BACKGROUND_TASK_STATE_COMPLETED) + { + return NULL; + } + + /* + * Précondition publique : + * background_task doit provenir de ToolTask. + */ + return background_task_get_result( + background_task + ); +} + +void tool_task_result_free( + ToolTaskResult *result +) +{ + if (result == NULL) + { + return; + } + + g_clear_pointer( + &result->tool_identifier, + g_free + ); + + g_clear_pointer( + &result->executable_path, + g_free + ); + + g_clear_pointer( + &result->arguments, + g_strfreev + ); + + g_clear_pointer( + &result->working_directory, + g_free + ); + + g_clear_pointer( + &result->process_result, + tool_process_result_free + ); + + g_free( + result + ); +} + +const char *tool_task_result_get_tool_identifier( + const ToolTaskResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->tool_identifier; +} + +const char *tool_task_result_get_executable_path( + const ToolTaskResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->executable_path; +} + +gsize tool_task_result_get_argument_count( + const ToolTaskResult *result +) +{ + if (result == NULL) + { + return 0; + } + + return result->argument_count; +} + +const char *tool_task_result_get_argument( + const ToolTaskResult *result, + gsize index +) +{ + if (result == NULL || + result->arguments == NULL || + index >= result->argument_count) + { + return NULL; + } + + return result->arguments[index]; +} + +const char *tool_task_result_get_working_directory( + const ToolTaskResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->working_directory; +} + +const ToolProcessResult *tool_task_result_get_process_result( + const ToolTaskResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->process_result; +} diff --git a/tests/test_tool_task b/tests/test_tool_task new file mode 100755 index 0000000..ad9a91a Binary files /dev/null and b/tests/test_tool_task differ diff --git a/tests/test_tool_task.c b/tests/test_tool_task.c new file mode 100644 index 0000000..6ede14f --- /dev/null +++ b/tests/test_tool_task.c @@ -0,0 +1,2461 @@ +/****************************************************************************** + * @file test_tool_task.c + * @brief Tests unitaires de l'exécution d'outils dans BackgroundTask. + ******************************************************************************/ + +#include "core/tool_task.h" + +#include +#include + +#include +#include + +/** + * @struct ToolTaskFixture + * @brief Environnement temporaire contenant un faux outil. + */ +typedef struct +{ + char *temporary_directory; + char *fake_tool_path; + char *original_path; +} ToolTaskFixture; + +/** + * @struct ToolTaskWaitContext + * @brief Synchronisation entre une BackgroundTask et le test principal. + */ +typedef struct +{ + GMainLoop *main_loop; + + BackgroundTask *completed_task; + + guint timeout_source_id; + + gboolean completion_called; + gboolean timed_out; +} ToolTaskWaitContext; + +/** + * @struct ToolTaskCancellationContext + * @brief Contexte utilisé pendant un test d'annulation. + */ +typedef struct +{ + GMainLoop *main_loop; + BackgroundTask *background_task; + + guint cancel_source_id; + guint timeout_source_id; + + gboolean completion_called; + gboolean timed_out; +} ToolTaskCancellationContext; + +/** + * @struct ToolTaskConcurrentContext + * @brief Contexte d'attente de deux tâches simultanées. + */ +typedef struct +{ + GMainLoop *main_loop; + + BackgroundTask *first_task; + BackgroundTask *second_task; + + guint completed_count; + guint timeout_source_id; + + gboolean timed_out; +} ToolTaskConcurrentContext; + +static char *test_tool_task_shared_directory = NULL; + +static void test_tool_task_write_script( + ToolTaskFixture *fixture, + const char *script_content +); + +static void test_tool_task_fixture_setup( + ToolTaskFixture *fixture, + gconstpointer user_data +); + +static ToolRegistry *test_tool_task_create_registry( + ToolTaskFixture *fixture, + gboolean create_executable, + gboolean refresh_registry +); + +static gboolean test_tool_task_on_timeout( + gpointer user_data +); + +static void test_tool_task_on_completed( + BackgroundTask *background_task, + gpointer user_data +); + +static gboolean test_tool_task_start_and_wait( + ToolTask *tool_task, + ToolTaskWaitContext *wait_context, + GError **error +); + +static void test_tool_task_assert_bytes_equal_text( + GBytes *bytes, + const char *expected_text +); + +static ToolRegistry *test_tool_task_create_registry( + ToolTaskFixture *fixture, + gboolean create_executable, + gboolean refresh_registry +); + +static gboolean test_tool_task_cancel_task( + gpointer user_data +); + +static gboolean test_tool_task_cancellation_timeout( + gpointer user_data +); + +static void test_tool_task_on_cancellation_completed( + BackgroundTask *background_task, + gpointer user_data +); + +static gboolean test_tool_task_concurrent_timeout( + gpointer user_data +); + +static void test_tool_task_on_concurrent_completed( + BackgroundTask *background_task, + gpointer user_data +); + +static void test_tool_task_cancellation( + ToolTaskFixture *fixture, + gconstpointer user_data +); + +static void test_tool_task_concurrent_tasks( + ToolTaskFixture *fixture, + gconstpointer user_data +); + +/** + * @brief Demande l'annulation de la tâche testée. + */ +static gboolean test_tool_task_cancel_task( + gpointer user_data +) +{ + ToolTaskCancellationContext *cancellation_context = + user_data; + + if (cancellation_context == NULL) + { + return G_SOURCE_REMOVE; + } + + cancellation_context->cancel_source_id = 0; + + background_task_cancel( + cancellation_context->background_task + ); + + return G_SOURCE_REMOVE; +} + +/** + * @brief Interrompt le test d'annulation en cas de blocage. + */ +static gboolean test_tool_task_cancellation_timeout( + gpointer user_data +) +{ + ToolTaskCancellationContext *cancellation_context = + user_data; + + if (cancellation_context == NULL) + { + return G_SOURCE_REMOVE; + } + + cancellation_context->timeout_source_id = 0; + cancellation_context->timed_out = TRUE; + + if (cancellation_context->main_loop != NULL) + { + g_main_loop_quit( + cancellation_context->main_loop + ); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Reçoit la fin de la tâche annulée. + */ +static void test_tool_task_on_cancellation_completed( + BackgroundTask *background_task, + gpointer user_data +) +{ + ToolTaskCancellationContext *cancellation_context = + user_data; + + if (cancellation_context == NULL) + { + return; + } + + g_assert_true( + background_task == + cancellation_context->background_task + ); + + cancellation_context->completion_called = TRUE; + + if (cancellation_context->cancel_source_id != 0) + { + g_source_remove( + cancellation_context->cancel_source_id + ); + + cancellation_context->cancel_source_id = 0; + } + + if (cancellation_context->timeout_source_id != 0) + { + g_source_remove( + cancellation_context->timeout_source_id + ); + + cancellation_context->timeout_source_id = 0; + } + + if (cancellation_context->main_loop != NULL) + { + g_main_loop_quit( + cancellation_context->main_loop + ); + } +} + +/** + * @brief Interrompt un test resté bloqué trop longtemps. + * + * @param user_data Pointeur vers ToolTaskWaitContext. + * + * @return G_SOURCE_REMOVE. + */ +static gboolean test_tool_task_on_timeout( + gpointer user_data +) +{ + ToolTaskWaitContext *wait_context = + user_data; + + if (wait_context == NULL) + { + return G_SOURCE_REMOVE; + } + + wait_context->timeout_source_id = 0; + wait_context->timed_out = TRUE; + + if (wait_context->main_loop != NULL) + { + g_main_loop_quit( + wait_context->main_loop + ); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Reçoit la notification finale d'une BackgroundTask. + * + * @param background_task Tâche terminée. + * @param user_data Pointeur vers ToolTaskWaitContext. + */ +static void test_tool_task_on_completed( + BackgroundTask *background_task, + gpointer user_data +) +{ + ToolTaskWaitContext *wait_context = + user_data; + + if (wait_context == NULL) + { + return; + } + + wait_context->completion_called = TRUE; + wait_context->completed_task = + background_task; + + if (wait_context->timeout_source_id != 0) + { + g_source_remove( + wait_context->timeout_source_id + ); + + wait_context->timeout_source_id = 0; + } + + if (wait_context->main_loop != NULL) + { + g_main_loop_quit( + wait_context->main_loop + ); + } +} + +/** + * @brief Démarre une ToolTask et attend sa terminaison. + * + * @param tool_task Tâche à démarrer. + * @param wait_context Contexte d'attente à initialiser. + * @param error Emplacement facultatif pour l'erreur de démarrage. + * + * @return TRUE si la tâche a été démarrée et terminée avant le timeout. + */ +static gboolean test_tool_task_start_and_wait( + ToolTask *tool_task, + ToolTaskWaitContext *wait_context, + GError **error +) +{ + gboolean start_success = FALSE; + + if (tool_task == NULL || + wait_context == NULL) + { + return FALSE; + } + + wait_context->main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + wait_context->completed_task = NULL; + wait_context->completion_called = FALSE; + wait_context->timed_out = FALSE; + + wait_context->timeout_source_id = + g_timeout_add_seconds( + 5, + test_tool_task_on_timeout, + wait_context + ); + + start_success = tool_task_start( + tool_task, + test_tool_task_on_completed, + wait_context, + NULL, + error + ); + + if (!start_success) + { + if (wait_context->timeout_source_id != 0) + { + g_source_remove( + wait_context->timeout_source_id + ); + + wait_context->timeout_source_id = 0; + } + + g_main_loop_unref( + wait_context->main_loop + ); + + wait_context->main_loop = NULL; + + return FALSE; + } + + g_main_loop_run( + wait_context->main_loop + ); + + if (wait_context->timeout_source_id != 0) + { + g_source_remove( + wait_context->timeout_source_id + ); + + wait_context->timeout_source_id = 0; + } + + g_main_loop_unref( + wait_context->main_loop + ); + + wait_context->main_loop = NULL; + + return wait_context->completion_called && + !wait_context->timed_out; +} + +/** + * @brief Vérifie qu'un GBytes contient exactement le texte attendu. + * + * @param bytes Données à vérifier. + * @param expected_text Texte attendu. + */ +static void test_tool_task_assert_bytes_equal_text( + GBytes *bytes, + const char *expected_text +) +{ + gconstpointer bytes_data = NULL; + + gsize bytes_size = 0; + gsize expected_size = 0; + + g_assert_nonnull( + bytes + ); + + g_assert_nonnull( + expected_text + ); + + bytes_data = g_bytes_get_data( + bytes, + &bytes_size + ); + + expected_size = strlen( + expected_text + ); + + g_assert_cmpuint( + bytes_size, + ==, + expected_size + ); + + g_assert_cmpmem( + bytes_data, + bytes_size, + expected_text, + expected_size + ); +} + +/** + * @brief Vérifie que ToolTask copie toutes les données nécessaires. + */ +static void test_tool_task_argument_ownership( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + + BackgroundTask *background_task = NULL; + + const ToolTaskResult *task_result = NULL; + const ToolProcessResult *process_result = NULL; + + ToolTaskWaitContext wait_context = {0}; + + GBytes *stdout_bytes = NULL; + + char *first_argument = NULL; + char *second_argument = NULL; + char *working_directory = NULL; + + const char *arguments[3]; + + GError *error = NULL; + + (void) user_data; + + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "for argument in \"$@\"\n" + "do\n" + " printf '%s\\n' \"$argument\"\n" + "done\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + first_argument = g_strdup( + "premier argument" + ); + + second_argument = g_strdup( + "second;non-interprete" + ); + + working_directory = g_strdup( + fixture->temporary_directory + ); + + arguments[0] = first_argument; + arguments[1] = second_argument; + arguments[2] = NULL; + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Test de propriété", + arguments, + working_directory, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + tool_task + ); + + /* + * Toutes ces données doivent maintenant être indépendantes. + */ + g_free( + first_argument + ); + + g_free( + second_argument + ); + + g_free( + working_directory + ); + + tool_registry_free( + tool_registry + ); + + tool_registry = NULL; + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_true( + test_tool_task_start_and_wait( + tool_task, + &wait_context, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_COMPLETED + ); + + task_result = + tool_task_result_from_background_task( + background_task + ); + + g_assert_nonnull( + task_result + ); + + g_assert_cmpuint( + tool_task_result_get_argument_count( + task_result + ), + ==, + 2 + ); + + g_assert_cmpstr( + tool_task_result_get_argument( + task_result, + 0 + ), + ==, + "premier argument" + ); + + g_assert_cmpstr( + tool_task_result_get_argument( + task_result, + 1 + ), + ==, + "second;non-interprete" + ); + + g_assert_cmpstr( + tool_task_result_get_working_directory( + task_result + ), + ==, + fixture->temporary_directory + ); + + process_result = + tool_task_result_get_process_result( + task_result + ); + + g_assert_nonnull( + process_result + ); + + stdout_bytes = + tool_process_result_ref_stdout( + process_result + ); + + test_tool_task_assert_bytes_equal_text( + stdout_bytes, + "premier argument\n" + "second;non-interprete\n" + ); + + g_bytes_unref( + stdout_bytes + ); + + tool_task_free( + tool_task + ); +} + +/** + * @brief Vérifie une exécution complète réussie. + */ +static void test_tool_task_success( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + + BackgroundTask *background_task = NULL; + + const ToolTaskResult *task_result = NULL; + const ToolProcessResult *process_result = NULL; + + ToolTaskWaitContext wait_context = {0}; + + GBytes *stdout_bytes = NULL; + GBytes *stderr_bytes = NULL; + + const char *arguments[] = + { + "example.org", + "nom avec espaces", + NULL + }; + + GError *error = NULL; + + (void) user_data; + + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "printf 'stdout:%s|%s' \"$1\" \"$2\"\n" + "printf 'stderr-test' >&2\n" + "exit 0\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Exécution réussie", + arguments, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + tool_task + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_true( + test_tool_task_start_and_wait( + tool_task, + &wait_context, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_true( + wait_context.completion_called + ); + + g_assert_true( + wait_context.completed_task == + background_task + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_COMPLETED + ); + + g_assert_cmpfloat( + background_task_get_progress( + background_task + ), + ==, + 1.0 + ); + + task_result = + tool_task_result_from_background_task( + background_task + ); + + g_assert_nonnull( + task_result + ); + + g_assert_cmpstr( + tool_task_result_get_tool_identifier( + task_result + ), + ==, + "test.fake" + ); + + g_assert_cmpstr( + tool_task_result_get_executable_path( + task_result + ), + ==, + fixture->fake_tool_path + ); + + g_assert_cmpuint( + tool_task_result_get_argument_count( + task_result + ), + ==, + 2 + ); + + process_result = + tool_task_result_get_process_result( + task_result + ); + + g_assert_nonnull( + process_result + ); + + g_assert_true( + tool_process_result_is_success( + process_result + ) + ); + + g_assert_cmpint( + tool_process_result_get_exit_status( + process_result + ), + ==, + 0 + ); + + stdout_bytes = + tool_process_result_ref_stdout( + process_result + ); + + stderr_bytes = + tool_process_result_ref_stderr( + process_result + ); + + test_tool_task_assert_bytes_equal_text( + stdout_bytes, + "stdout:example.org|nom avec espaces" + ); + + test_tool_task_assert_bytes_equal_text( + stderr_bytes, + "stderr-test" + ); + + g_bytes_unref( + stdout_bytes + ); + + g_bytes_unref( + stderr_bytes + ); + + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie qu'un code non nul produit une tâche terminée. + */ +static void test_tool_task_nonzero_exit( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + + BackgroundTask *background_task = NULL; + + const ToolTaskResult *task_result = NULL; + const ToolProcessResult *process_result = NULL; + + ToolTaskWaitContext wait_context = {0}; + + GError *error = NULL; + + (void) user_data; + + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "printf 'erreur fonctionnelle' >&2\n" + "exit 7\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Code non nul", + NULL, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_true( + test_tool_task_start_and_wait( + tool_task, + &wait_context, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_COMPLETED + ); + + task_result = + tool_task_result_from_background_task( + background_task + ); + + g_assert_nonnull( + task_result + ); + + process_result = + tool_task_result_get_process_result( + task_result + ); + + g_assert_nonnull( + process_result + ); + + g_assert_false( + tool_process_result_is_success( + process_result + ) + ); + + g_assert_cmpint( + tool_process_result_get_exit_status( + process_result + ), + ==, + 7 + ); + + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie l'échec lorsque l'exécutable disparaît avant le lancement. + */ +static void test_tool_task_spawn_failure( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + + BackgroundTask *background_task = NULL; + + ToolTaskWaitContext wait_context = {0}; + + GError *task_error = NULL; + GError *stored_error = NULL; + + (void) user_data; + + tool_registry = test_tool_task_create_registry( + fixture, + TRUE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Échec de lancement", + NULL, + NULL, + &task_error + ); + + g_assert_no_error( + task_error + ); + + g_assert_nonnull( + tool_task + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_cmpint( + g_remove( + fixture->fake_tool_path + ), + ==, + 0 + ); + + /* + * Le démarrage asynchrone réussit. C'est le worker qui découvrira + * ensuite que l'exécutable a disparu. + */ + g_assert_true( + test_tool_task_start_and_wait( + tool_task, + &wait_context, + &task_error + ) + ); + + g_assert_no_error( + task_error + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_FAILED + ); + + g_assert_null( + tool_task_result_from_background_task( + background_task + ) + ); + + stored_error = + background_task_dup_error( + background_task + ); + + g_assert_nonnull( + stored_error + ); + + g_assert_error( + stored_error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_PROCESS + ); + + g_clear_error( + &stored_error + ); + + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie la transmission du dossier de travail. + */ +static void test_tool_task_working_directory( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + + BackgroundTask *background_task = NULL; + + const ToolTaskResult *task_result = NULL; + const ToolProcessResult *process_result = NULL; + + ToolTaskWaitContext wait_context = {0}; + + GBytes *stdout_bytes = NULL; + + char *canonical_directory = NULL; + + GError *error = NULL; + + (void) user_data; + + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "printf '%s' \"$PWD\"\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Dossier de travail", + NULL, + fixture->temporary_directory, + &error + ); + + g_assert_no_error( + error + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_true( + test_tool_task_start_and_wait( + tool_task, + &wait_context, + &error + ) + ); + + g_assert_no_error( + error + ); + + task_result = + tool_task_result_from_background_task( + background_task + ); + + g_assert_nonnull( + task_result + ); + + process_result = + tool_task_result_get_process_result( + task_result + ); + + stdout_bytes = + tool_process_result_ref_stdout( + process_result + ); + + canonical_directory = + g_canonicalize_filename( + fixture->temporary_directory, + NULL + ); + + test_tool_task_assert_bytes_equal_text( + stdout_bytes, + canonical_directory + ); + + g_assert_cmpstr( + tool_task_result_get_working_directory( + task_result + ), + ==, + fixture->temporary_directory + ); + + g_bytes_unref( + stdout_bytes + ); + + g_free( + canonical_directory + ); + + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Crée un faux exécutable dans le dossier temporaire. + * + * @param fixture Fixture du test. + * @param script_content Contenu complet du script. + */ +static void test_tool_task_write_script( + ToolTaskFixture *fixture, + const char *script_content +) +{ + GError *error = NULL; + gboolean write_success = FALSE; + int chmod_result = 0; + + g_assert_nonnull( + fixture + ); + + g_assert_nonnull( + fixture->fake_tool_path + ); + + g_assert_nonnull( + script_content + ); + + write_success = g_file_set_contents( + fixture->fake_tool_path, + script_content, + -1, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_true( + write_success + ); + + chmod_result = g_chmod( + fixture->fake_tool_path, + S_IRUSR | + S_IWUSR | + S_IXUSR + ); + + g_assert_cmpint( + chmod_result, + ==, + 0 + ); +} + +static void test_tool_task_fixture_setup( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + (void) user_data; + + g_assert_nonnull( + fixture + ); + + g_assert_nonnull( + test_tool_task_shared_directory + ); + + fixture->temporary_directory = + g_strdup( + test_tool_task_shared_directory + ); + + fixture->fake_tool_path = + g_build_filename( + fixture->temporary_directory, + "fake_tool", + NULL + ); + + fixture->original_path = NULL; +} + +/** + * @brief Supprime les fichiers propres au test. + * + * Le dossier temporaire et le PATH sont partagés par toute la suite de + * tests. Ils sont donc nettoyés uniquement à la fin de main(). + */ +static void test_tool_task_fixture_teardown( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + (void) user_data; + + if (fixture->fake_tool_path != NULL) + { + g_remove( + fixture->fake_tool_path + ); + } + + g_clear_pointer( + &fixture->fake_tool_path, + g_free + ); + + g_clear_pointer( + &fixture->temporary_directory, + g_free + ); + + g_clear_pointer( + &fixture->original_path, + g_free + ); +} + +/** + * @brief Crée un registre contenant le faux outil. + * + * @param fixture Fixture du test. + * @param create_executable TRUE pour créer le faux exécutable. + * @param refresh_registry TRUE pour détecter sa disponibilité. + * + * @return Nouveau registre. + */ +static ToolRegistry *test_tool_task_create_registry( + ToolTaskFixture *fixture, + gboolean create_executable, + gboolean refresh_registry +) +{ + ToolRegistry *tool_registry = NULL; + GError *error = NULL; + + if (create_executable) + { + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "exit 0\n" + ); + } + + tool_registry = tool_registry_new(); + + g_assert_nonnull( + tool_registry + ); + + g_assert_true( + tool_registry_register( + tool_registry, + "test.fake", + "Fake Tool", + "fake_tool", + TOOL_REQUIREMENT_OPTIONAL, + &error + ) + ); + + g_assert_no_error( + error + ); + + if (refresh_registry) + { + g_assert_true( + tool_registry_refresh( + tool_registry, + &error + ) + ); + + g_assert_no_error( + error + ); + } + + return tool_registry; +} + +/** + * @brief Vérifie le refus des arguments invalides. + */ +static void test_tool_task_invalid_arguments( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + GError *error = NULL; + + (void) user_data; + + tool_registry = test_tool_task_create_registry( + fixture, + TRUE, + TRUE + ); + + tool_task = tool_task_new( + NULL, + "test.fake", + "Tâche de test", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_task = tool_task_new( + tool_registry, + NULL, + "Tâche de test", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_task = tool_task_new( + tool_registry, + "", + "Tâche de test", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + NULL, + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Tâche de test", + NULL, + "", + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie le refus d'un identifiant inconnu. + */ +static void test_tool_task_tool_not_found( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + GError *error = NULL; + + (void) fixture; + (void) user_data; + + tool_registry = tool_registry_new(); + + g_assert_nonnull( + tool_registry + ); + + tool_task = tool_task_new( + tool_registry, + "unknown.tool", + "Tâche inconnue", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_NOT_FOUND + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie le refus d'un outil non encore contrôlé. + */ +static void test_tool_task_tool_not_checked( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + GError *error = NULL; + + (void) user_data; + + tool_registry = test_tool_task_create_registry( + fixture, + TRUE, + FALSE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Tâche non vérifiée", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_NOT_CHECKED + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie le refus d'un outil explicitement absent. + */ +static void test_tool_task_tool_missing( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + GError *error = NULL; + + (void) user_data; + + /* + * Aucun fichier fake_tool n'est créé avant le rafraîchissement. + */ + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Tâche impossible", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_MISSING + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie la création d'une ToolTask valide. + */ +static void test_tool_task_create( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + BackgroundTask *background_task = NULL; + + const char *arguments[] = + { + "premier argument", + "second", + NULL + }; + + GError *error = NULL; + + (void) user_data; + + tool_registry = test_tool_task_create_registry( + fixture, + TRUE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Exécution du faux outil", + arguments, + fixture->temporary_directory, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + tool_task + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_nonnull( + background_task + ); + + g_assert_cmpstr( + background_task_get_title( + background_task + ), + ==, + "Exécution du faux outil" + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_PENDING + ); + + g_assert_cmpfloat( + background_task_get_progress( + background_task + ), + ==, + 0.0 + ); + + /* + * Vérifie aussi la destruction d'une tâche jamais démarrée. + */ + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie l'annulation d'un outil externe en cours. + */ +static void test_tool_task_cancellation( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + BackgroundTask *background_task = NULL; + + ToolTaskCancellationContext cancellation_context = {0}; + + GError *error = NULL; + + gboolean start_success = FALSE; + + (void) user_data; + + /* + * Boucle entièrement interne au shell. + * Aucun programme externe comme sleep n'est lancé. + */ + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "while :\n" + "do\n" + " :\n" + "done\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Annulation du faux outil", + NULL, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + tool_task + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + cancellation_context.main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + cancellation_context.background_task = + background_task; + + cancellation_context.cancel_source_id = + g_timeout_add( + 200, + test_tool_task_cancel_task, + &cancellation_context + ); + + cancellation_context.timeout_source_id = + g_timeout_add_seconds( + 5, + test_tool_task_cancellation_timeout, + &cancellation_context + ); + + start_success = tool_task_start( + tool_task, + test_tool_task_on_cancellation_completed, + &cancellation_context, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_true( + start_success + ); + + g_main_loop_run( + cancellation_context.main_loop + ); + + if (cancellation_context.cancel_source_id != 0) + { + g_source_remove( + cancellation_context.cancel_source_id + ); + } + + if (cancellation_context.timeout_source_id != 0) + { + g_source_remove( + cancellation_context.timeout_source_id + ); + } + + g_main_loop_unref( + cancellation_context.main_loop + ); + + cancellation_context.main_loop = NULL; + + g_assert_false( + cancellation_context.timed_out + ); + + g_assert_true( + cancellation_context.completion_called + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_CANCELLED + ); + + g_assert_null( + tool_task_result_from_background_task( + background_task + ) + ); + + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Interrompt le test concurrent en cas de blocage. + */ +static gboolean test_tool_task_concurrent_timeout( + gpointer user_data +) +{ + ToolTaskConcurrentContext *concurrent_context = + user_data; + + if (concurrent_context == NULL) + { + return G_SOURCE_REMOVE; + } + + concurrent_context->timeout_source_id = 0; + concurrent_context->timed_out = TRUE; + + if (concurrent_context->main_loop != NULL) + { + g_main_loop_quit( + concurrent_context->main_loop + ); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Compte les tâches concurrentes terminées. + */ +static void test_tool_task_on_concurrent_completed( + BackgroundTask *background_task, + gpointer user_data +) +{ + ToolTaskConcurrentContext *concurrent_context = + user_data; + + if (concurrent_context == NULL) + { + return; + } + + g_assert_true( + background_task == + concurrent_context->first_task || + background_task == + concurrent_context->second_task + ); + + concurrent_context->completed_count++; + + if (concurrent_context->completed_count < 2) + { + return; + } + + if (concurrent_context->timeout_source_id != 0) + { + g_source_remove( + concurrent_context->timeout_source_id + ); + + concurrent_context->timeout_source_id = 0; + } + + if (concurrent_context->main_loop != NULL) + { + g_main_loop_quit( + concurrent_context->main_loop + ); + } +} + +/** + * @brief Vérifie l'indépendance de deux ToolTask simultanées. + */ +static void test_tool_task_concurrent_tasks( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + + ToolTask *first_tool_task = NULL; + ToolTask *second_tool_task = NULL; + + BackgroundTask *first_background_task = NULL; + BackgroundTask *second_background_task = NULL; + + const ToolTaskResult *first_task_result = NULL; + const ToolTaskResult *second_task_result = NULL; + + const ToolProcessResult *first_process_result = NULL; + const ToolProcessResult *second_process_result = NULL; + + ToolTaskConcurrentContext concurrent_context = {0}; + + GBytes *first_stdout = NULL; + GBytes *second_stdout = NULL; + + const char *first_arguments[] = + { + "premiere-tache", + NULL + }; + + const char *second_arguments[] = + { + "seconde-tache", + NULL + }; + + GError *error = NULL; + + (void) user_data; + + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "counter=0\n" + "while [ \"$counter\" -lt 10000 ]\n" + "do\n" + " counter=$((counter + 1))\n" + "done\n" + "printf '%s' \"$1\"\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + first_tool_task = tool_task_new( + tool_registry, + "test.fake", + "Première tâche", + first_arguments, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + first_tool_task + ); + + second_tool_task = tool_task_new( + tool_registry, + "test.fake", + "Seconde tâche", + second_arguments, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + second_tool_task + ); + + first_background_task = + tool_task_get_background_task( + first_tool_task + ); + + second_background_task = + tool_task_get_background_task( + second_tool_task + ); + + concurrent_context.main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + concurrent_context.first_task = + first_background_task; + + concurrent_context.second_task = + second_background_task; + + concurrent_context.timeout_source_id = + g_timeout_add_seconds( + 5, + test_tool_task_concurrent_timeout, + &concurrent_context + ); + + g_assert_true( + tool_task_start( + first_tool_task, + test_tool_task_on_concurrent_completed, + &concurrent_context, + NULL, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_true( + tool_task_start( + second_tool_task, + test_tool_task_on_concurrent_completed, + &concurrent_context, + NULL, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_main_loop_run( + concurrent_context.main_loop + ); + + if (concurrent_context.timeout_source_id != 0) + { + g_source_remove( + concurrent_context.timeout_source_id + ); + } + + g_main_loop_unref( + concurrent_context.main_loop + ); + + concurrent_context.main_loop = NULL; + + g_assert_false( + concurrent_context.timed_out + ); + + g_assert_cmpuint( + concurrent_context.completed_count, + ==, + 2 + ); + + g_assert_cmpint( + background_task_get_state( + first_background_task + ), + ==, + BACKGROUND_TASK_STATE_COMPLETED + ); + + g_assert_cmpint( + background_task_get_state( + second_background_task + ), + ==, + BACKGROUND_TASK_STATE_COMPLETED + ); + + first_task_result = + tool_task_result_from_background_task( + first_background_task + ); + + second_task_result = + tool_task_result_from_background_task( + second_background_task + ); + + g_assert_nonnull( + first_task_result + ); + + g_assert_nonnull( + second_task_result + ); + + g_assert_cmpstr( + tool_task_result_get_argument( + first_task_result, + 0 + ), + ==, + "premiere-tache" + ); + + g_assert_cmpstr( + tool_task_result_get_argument( + second_task_result, + 0 + ), + ==, + "seconde-tache" + ); + + first_process_result = + tool_task_result_get_process_result( + first_task_result + ); + + second_process_result = + tool_task_result_get_process_result( + second_task_result + ); + + first_stdout = + tool_process_result_ref_stdout( + first_process_result + ); + + second_stdout = + tool_process_result_ref_stdout( + second_process_result + ); + + test_tool_task_assert_bytes_equal_text( + first_stdout, + "premiere-tache" + ); + + test_tool_task_assert_bytes_equal_text( + second_stdout, + "seconde-tache" + ); + + g_bytes_unref( + first_stdout + ); + + g_bytes_unref( + second_stdout + ); + + tool_task_free( + first_tool_task + ); + + tool_task_free( + second_tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +int main( + int argc, + char **argv +) +{ + GError *error = NULL; + int test_result = 0; + + g_test_init( + &argc, + &argv, + NULL + ); + test_tool_task_shared_directory = + g_dir_make_tmp( + "labfy-tool-task-XXXXXX", + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + test_tool_task_shared_directory + ); + + g_assert_true( + g_setenv( + "PATH", + test_tool_task_shared_directory, + TRUE + ) + ); + + g_test_add( + "/tool_task/invalid_arguments", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_invalid_arguments, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/tool_not_found", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_tool_not_found, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/tool_not_checked", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_tool_not_checked, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/tool_missing", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_tool_missing, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/create", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_create, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/argument_ownership", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_argument_ownership, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/success", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_success, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/nonzero_exit", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_nonzero_exit, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/spawn_failure", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_spawn_failure, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/working_directory", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_working_directory, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/cancellation", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_cancellation, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/concurrent_tasks", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_concurrent_tasks, + test_tool_task_fixture_teardown + ); + + test_result = g_test_run(); + + g_rmdir( + test_tool_task_shared_directory + ); + + g_clear_pointer( + &test_tool_task_shared_directory, + g_free + ); + + return test_result; +}