feat(core): add investigation node hierarchy

This commit is contained in:
grayTerminal-sh 2026-07-13 12:21:18 +02:00
parent 6c524f6825
commit dbd86cf5fc
7 changed files with 561 additions and 8 deletions

View file

@ -0,0 +1,268 @@
# Ticket #009
## Titre
Ajouter les relations parent/enfants à `InvestigationNode`.
---
## Objectif
Faire évoluer `InvestigationNode` afin qu'un nœud puisse appartenir à une arborescence.
Chaque nœud pourra :
- connaître son parent ;
- posséder plusieurs enfants ;
- exposer ses enfants en lecture seule via une API publique.
---
## Responsabilités
Le module `InvestigationNode` doit :
- mémoriser un pointeur vers son parent ;
- posséder un tableau dynamique d'enfants ;
- ajouter un enfant ;
- retourner un enfant par son index ;
- retourner le nombre d'enfants ;
- retourner son parent ;
- détruire récursivement les enfants qu'il possède.
---
## Hors périmètre
Ce ticket ne doit pas :
- parcourir le système de fichiers ;
- construire automatiquement une arborescence depuis un dossier ;
- afficher quoi que ce soit avec GTK ;
- communiquer avec SQLite ;
- gérer le tri des enfants ;
- gérer la suppression individuelle d'un enfant ;
- gérer le déplacement d'un nœud entre deux parents.
---
## Architecture
```text
InvestigationTreeModel
InvestigationNode
├── InvestigationNode
├── InvestigationNode
└── InvestigationNode
```
Le module appartient à la couche `core`.
Il peut dépendre de GLib, mais jamais de GTK.
---
## Gestion de la propriété
Un nœud devient propriétaire de chaque enfant ajouté avec :
```c
investigation_node_add_child(parent, child);
```
Après un ajout réussi :
- `parent` possède `child` ;
- le code appelant ne doit plus libérer `child` directement ;
- `child` conserve une référence non propriétaire vers `parent`.
Lors de la destruction du parent, tous ses enfants sont détruits récursivement.
---
## Structure interne attendue
```c
struct InvestigationNode
{
char *name;
InvestigationNodeType type;
InvestigationNode *parent;
GPtrArray *children;
};
```
La structure reste privée dans `investigation_node.c`.
---
## Interface publique attendue
Les fonctions existantes sont conservées :
```c
InvestigationNode *investigation_node_new(
const char *name,
InvestigationNodeType type
);
void investigation_node_free(
InvestigationNode *node
);
const char *investigation_node_get_name(
const InvestigationNode *node
);
InvestigationNodeType investigation_node_get_type(
const InvestigationNode *node
);
```
Les fonctions suivantes sont ajoutées :
```c
bool investigation_node_add_child(
InvestigationNode *parent,
InvestigationNode *child
);
const InvestigationNode *investigation_node_get_child(
const InvestigationNode *node,
size_t index
);
size_t investigation_node_get_children_count(
const InvestigationNode *node
);
const InvestigationNode *investigation_node_get_parent(
const InvestigationNode *node
);
```
---
## Comportement attendu
Exemple :
```c
InvestigationNode *root = NULL;
InvestigationNode *child = NULL;
root = investigation_node_new(
"Template",
INVESTIGATION_NODE_DIRECTORY
);
child = investigation_node_new(
"00_BaseDeDonnees",
INVESTIGATION_NODE_DIRECTORY
);
if (!investigation_node_add_child(root, child))
{
investigation_node_free(child);
investigation_node_free(root);
return;
}
```
Après l'ajout :
```text
Template
└── 00_BaseDeDonnees
```
Le nœud `root` devient propriétaire de `child`.
Le nettoyage correct est uniquement :
```c
investigation_node_free(root);
```
---
## Règles d'ajout
`investigation_node_add_child()` doit refuser :
- un parent `NULL` ;
- un enfant `NULL` ;
- un parent qui représente un fichier ;
- l'ajout d'un nœud comme enfant de lui-même ;
- un enfant possédant déjà un parent.
En cas d'échec, la propriété de l'enfant reste au code appelant.
---
## Dépendances
- C17 ;
- GLib.
Aucune dépendance GTK ou SQLite.
---
## Contraintes techniques
- structure opaque ;
- aucun état global ;
- utilisation de `GPtrArray` ;
- tableau créé avec une fonction de destruction ;
- documentation Doxygen ;
- compilation sans warning ;
- respect des conventions de nommage ;
- aucune modification directe du tableau en dehors du module.
---
## Critères d'acceptation
- [ ] Le projet compile sans warning.
- [ ] Un dossier peut posséder plusieurs enfants.
- [ ] Un fichier ne peut pas recevoir d'enfant.
- [ ] Un enfant connaît son parent.
- [ ] Le nombre d'enfants est correct.
- [ ] Un enfant peut être récupéré par son index.
- [ ] Un index invalide retourne `NULL`.
- [ ] Un enfant ne peut pas avoir deux parents.
- [ ] Un nœud ne peut pas être son propre enfant.
- [ ] La destruction d'un parent détruit récursivement ses enfants.
- [ ] Aucun code GTK.
- [ ] Aucun code SQLite.
---
## Tests
- créer un parent dossier ;
- ajouter un enfant dossier ;
- ajouter un enfant fichier ;
- vérifier le nombre d'enfants ;
- récupérer chaque enfant ;
- vérifier le parent d'un enfant ;
- tester un index invalide ;
- refuser l'ajout à un fichier ;
- refuser un parent `NULL` ;
- refuser un enfant `NULL` ;
- refuser l'auto-référence ;
- refuser un enfant possédant déjà un parent ;
- détruire un arbre complet ;
- appeler les getters avec `NULL`.
---
## Commit attendu
```text
feat(core): add investigation node hierarchy
```

View file

@ -6,6 +6,10 @@
#ifndef LABFY_INVESTIGATION_INVESTIGATION_NODE_H #ifndef LABFY_INVESTIGATION_INVESTIGATION_NODE_H
#define LABFY_INVESTIGATION_INVESTIGATION_NODE_H #define LABFY_INVESTIGATION_INVESTIGATION_NODE_H
#include <stdbool.h>
#include <stddef.h>
/** /**
* @brief Représentation opaque d'un nœud d'enquête. * @brief Représentation opaque d'un nœud d'enquête.
* *
@ -77,4 +81,40 @@ InvestigationNodeType investigation_node_get_type(
const InvestigationNode *node const InvestigationNode *node
); );
/**
* @brief Ajoute un enfant à un dossier.
*
* En cas de succès, le parent devient propriétaire de l'enfant.
*
* @return true si l'ajout a réussi.
*/
bool investigation_node_add_child(
InvestigationNode *parent,
InvestigationNode *child
);
/**
* @brief Retourne un enfant.
*
* @return L'enfant ou NULL.
*/
const InvestigationNode *investigation_node_get_child(
const InvestigationNode *node,
size_t index
);
/**
* @brief Retourne le nombre d'enfants.
*/
size_t investigation_node_get_children_count(
const InvestigationNode *node
);
/**
* @brief Retourne le parent d'un nœud.
*/
const InvestigationNode *investigation_node_get_parent(
const InvestigationNode *node
);
#endif #endif

View file

@ -10,11 +10,16 @@
/** /**
* @struct InvestigationNode * @struct InvestigationNode
* @brief Représentation interne d'un nœud. * @brief Représentation interne d'un nœud.
*
* Le nœud possède son nom et ses enfants.
* Le pointeur parent est une simple référence non propriétaire.
*/ */
struct InvestigationNode struct InvestigationNode
{ {
char *name; char *name;
InvestigationNodeType type; InvestigationNodeType type;
InvestigationNode *parent;
GPtrArray *children;
}; };
InvestigationNode *investigation_node_new( InvestigationNode *investigation_node_new(
@ -29,10 +34,7 @@ InvestigationNode *investigation_node_new(
return NULL; return NULL;
} }
node = g_new0( node = g_new0(InvestigationNode, 1);
InvestigationNode,
1
);
if (node == NULL) if (node == NULL)
{ {
@ -47,6 +49,16 @@ InvestigationNode *investigation_node_new(
return NULL; return NULL;
} }
node->children = g_ptr_array_new_with_free_func(
(GDestroyNotify) investigation_node_free
);
if (node->children == NULL)
{
investigation_node_free(node);
return NULL;
}
node->type = type; node->type = type;
return node; return node;
@ -61,8 +73,16 @@ void investigation_node_free(
return; return;
} }
g_free(node->name); /*
* Le tableau possède les enfants.
* TRUE demande à GLib de libérer aussi le contenu du tableau.
*/
if (node->children != NULL)
{
g_ptr_array_free(node->children, TRUE);
}
g_free(node->name);
g_free(node); g_free(node);
} }
@ -89,3 +109,83 @@ InvestigationNodeType investigation_node_get_type(
return node->type; return node->type;
} }
bool investigation_node_add_child(
InvestigationNode *parent,
InvestigationNode *child
)
{
if (parent == NULL || child == NULL)
{
return false;
}
if (parent->type != INVESTIGATION_NODE_DIRECTORY)
{
return false;
}
if (parent == child)
{
return false;
}
if (child->parent != NULL)
{
return false;
}
child->parent = parent;
g_ptr_array_add(
parent->children,
child
);
return true;
}
const InvestigationNode *investigation_node_get_child(
const InvestigationNode *node,
size_t index
)
{
if (node == NULL || node->children == NULL)
{
return NULL;
}
if (index >= node->children->len)
{
return NULL;
}
return g_ptr_array_index(
node->children,
index
);
}
size_t investigation_node_get_children_count(
const InvestigationNode *node
)
{
if (node == NULL || node->children == NULL)
{
return 0;
}
return node->children->len;
}
const InvestigationNode *investigation_node_get_parent(
const InvestigationNode *node
)
{
if (node == NULL)
{
return NULL;
}
return node->parent;
}

View file

@ -27,6 +27,8 @@ static void test_directory_node(void)
investigation_node_get_type(node) == investigation_node_get_type(node) ==
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY
); );
assert(investigation_node_get_parent(node) == NULL);
assert(investigation_node_get_children_count(node) == 0);
investigation_node_free(node); investigation_node_free(node);
} }
@ -53,6 +55,140 @@ static void test_file_node(void)
investigation_node_free(node); investigation_node_free(node);
} }
static void test_add_children(void)
{
InvestigationNode *root = NULL;
InvestigationNode *database_directory = NULL;
InvestigationNode *database_file = NULL;
const InvestigationNode *returned_child = NULL;
root = investigation_node_new(
"Template",
INVESTIGATION_NODE_DIRECTORY
);
database_directory = investigation_node_new(
"00_BaseDeDonnees",
INVESTIGATION_NODE_DIRECTORY
);
database_file = investigation_node_new(
"Enquete.sqlite",
INVESTIGATION_NODE_FILE
);
assert(root != NULL);
assert(database_directory != NULL);
assert(database_file != NULL);
assert(
investigation_node_add_child(
root,
database_directory
)
);
assert(
investigation_node_add_child(
database_directory,
database_file
)
);
assert(investigation_node_get_children_count(root) == 1);
assert(
investigation_node_get_children_count(
database_directory
) == 1
);
returned_child = investigation_node_get_child(root, 0);
assert(returned_child == database_directory);
assert(
investigation_node_get_parent(database_directory) ==
root
);
assert(
investigation_node_get_parent(database_file) ==
database_directory
);
/*
* root possède database_directory,
* qui possède database_file.
* La destruction est donc récursive.
*/
investigation_node_free(root);
}
static void test_invalid_additions(void)
{
InvestigationNode *directory = NULL;
InvestigationNode *file = NULL;
InvestigationNode *child = NULL;
InvestigationNode *second_parent = NULL;
directory = investigation_node_new(
"Directory",
INVESTIGATION_NODE_DIRECTORY
);
file = investigation_node_new(
"File.txt",
INVESTIGATION_NODE_FILE
);
child = investigation_node_new(
"Child",
INVESTIGATION_NODE_DIRECTORY
);
second_parent = investigation_node_new(
"SecondParent",
INVESTIGATION_NODE_DIRECTORY
);
assert(directory != NULL);
assert(file != NULL);
assert(child != NULL);
assert(second_parent != NULL);
assert(!investigation_node_add_child(NULL, child));
assert(!investigation_node_add_child(directory, NULL));
assert(!investigation_node_add_child(file, child));
assert(!investigation_node_add_child(directory, directory));
assert(investigation_node_add_child(directory, child));
assert(!investigation_node_add_child(second_parent, child));
/*
* directory possède désormais child.
*/
investigation_node_free(directory);
investigation_node_free(file);
investigation_node_free(second_parent);
}
static void test_invalid_index(void)
{
InvestigationNode *node = NULL;
node = investigation_node_new(
"Template",
INVESTIGATION_NODE_DIRECTORY
);
assert(node != NULL);
assert(investigation_node_get_child(node, 0) == NULL);
assert(investigation_node_get_child(node, 42) == NULL);
assert(investigation_node_get_child(NULL, 0) == NULL);
investigation_node_free(node);
}
static void test_invalid_names(void) static void test_invalid_names(void)
{ {
assert( assert(
@ -70,8 +206,12 @@ static void test_invalid_names(void)
); );
} }
static void test_null_free(void) static void test_null_behaviour(void)
{ {
assert(investigation_node_get_name(NULL) == NULL);
assert(investigation_node_get_children_count(NULL) == 0);
assert(investigation_node_get_parent(NULL) == NULL);
investigation_node_free(NULL); investigation_node_free(NULL);
} }
@ -79,10 +219,15 @@ int main(void)
{ {
test_directory_node(); test_directory_node();
test_file_node(); test_file_node();
test_add_children();
test_invalid_additions();
test_invalid_index();
test_invalid_names(); test_invalid_names();
test_null_free(); test_null_behaviour();
printf("InvestigationNode : tous les tests sont valides.\n"); printf(
"InvestigationNode : tous les tests de hiérarchie sont valides.\n"
);
return 0; return 0;
} }