feat(core): add investigation node hierarchy
This commit is contained in:
parent
6c524f6825
commit
dbd86cf5fc
7 changed files with 561 additions and 8 deletions
268
labfy-investigation/docs/tickets/closed/TICKET-009.md
Normal file
268
labfy-investigation/docs/tickets/closed/TICKET-009.md
Normal 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
|
||||
```
|
||||
|
|
@ -6,6 +6,10 @@
|
|||
#ifndef 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.
|
||||
*
|
||||
|
|
@ -77,4 +81,40 @@ InvestigationNodeType investigation_node_get_type(
|
|||
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
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -10,11 +10,16 @@
|
|||
/**
|
||||
* @struct InvestigationNode
|
||||
* @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
|
||||
{
|
||||
char *name;
|
||||
InvestigationNodeType type;
|
||||
InvestigationNode *parent;
|
||||
GPtrArray *children;
|
||||
};
|
||||
|
||||
InvestigationNode *investigation_node_new(
|
||||
|
|
@ -29,10 +34,7 @@ InvestigationNode *investigation_node_new(
|
|||
return NULL;
|
||||
}
|
||||
|
||||
node = g_new0(
|
||||
InvestigationNode,
|
||||
1
|
||||
);
|
||||
node = g_new0(InvestigationNode, 1);
|
||||
|
||||
if (node == NULL)
|
||||
{
|
||||
|
|
@ -47,6 +49,16 @@ InvestigationNode *investigation_node_new(
|
|||
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;
|
||||
|
||||
return node;
|
||||
|
|
@ -61,8 +73,16 @@ void investigation_node_free(
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
@ -89,3 +109,83 @@ InvestigationNodeType investigation_node_get_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;
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -27,6 +27,8 @@ static void test_directory_node(void)
|
|||
investigation_node_get_type(node) ==
|
||||
INVESTIGATION_NODE_DIRECTORY
|
||||
);
|
||||
assert(investigation_node_get_parent(node) == NULL);
|
||||
assert(investigation_node_get_children_count(node) == 0);
|
||||
|
||||
investigation_node_free(node);
|
||||
}
|
||||
|
|
@ -53,6 +55,140 @@ static void test_file_node(void)
|
|||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -79,10 +219,15 @@ int main(void)
|
|||
{
|
||||
test_directory_node();
|
||||
test_file_node();
|
||||
test_add_children();
|
||||
test_invalid_additions();
|
||||
test_invalid_index();
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue