Outil de test structurel pour JAVA/JML - Université de Franche

publicité
FIEUX Corentin
GÉRARDIN Charles
Master 2 Informatique
finalité Professionnelle
option S2L
Outil de test structurel pour JAVA/JML
Projet annuel de seconde année de Master Informatique
Tuteurs : Frédéric DADEAU, Fabien PEUREUX
Année 2010 – 2011
Table des matières
1)Introduction.......................................................................................................................................3
2)Présentation du sujet.........................................................................................................................5
2.1)Description du sujet...................................................................................................................5
2.2)Cahier des charges.....................................................................................................................6
2.2.1)Contraintes fonctionnelles.................................................................................................7
2.2.2)Contraintes Techniques......................................................................................................8
2.2.3)Autres contraintes..............................................................................................................8
3)Réalisation.........................................................................................................................................9
3.1)Analyse......................................................................................................................................9
3.1.1)Répartition des tâches........................................................................................................9
3.1.1.1)Fragmentation du sujet...............................................................................................9
3.1.1.2)Planning...................................................................................................................10
3.1.2)Choix des outils...............................................................................................................10
3.1.2.1)Sorcerer....................................................................................................................10
3.1.2.2)JavaDoc....................................................................................................................11
3.1.3)Modélisation....................................................................................................................12
3.1.3.1)Méta-modèle JAVA..................................................................................................12
3.1.3.2)Modélisation du graphe de flot de contrôle.............................................................15
3.2)Développement........................................................................................................................18
3.2.1)Implémentation de la structure de données.....................................................................18
3.2.2)Développement du Parser................................................................................................18
3.2.2.1)Variables de stockage...............................................................................................18
3.2.2.2)Méthodes de visite...................................................................................................19
3.2.2.2.1)Le traitement commun.....................................................................................20
3.2.2.2.2)Les méthodes représentant une condition........................................................20
3.2.2.2.3)Les méthodes représentant une instruction......................................................21
3.2.2.2.4)Les autres méthodes.........................................................................................21
3.2.3)Implémentation de la structure des graphes.....................................................................23
3.2.4)Parcours de graphe...........................................................................................................23
3.2.4.1)La fonction Parcours().............................................................................................24
3.2.4.2)La fonction ParcoursBoucle()..................................................................................25
3.2.5)Test...................................................................................................................................26
3.2.6)Documentation.................................................................................................................26
4)Résultat............................................................................................................................................27
4.1)Fichier JAVA analysé...............................................................................................................27
4.2)Structure Correspondante........................................................................................................28
4.3)Graphe résultant.......................................................................................................................28
4.4)Couverture du graphe..............................................................................................................29
5)Bilans...............................................................................................................................................30
5.1)Bilan pédagogique...................................................................................................................30
5.2)Bilan humain...........................................................................................................................30
6)Conclusion.......................................................................................................................................31
1) Introduction
L'informatique est aujourd'hui omniprésente dans notre vie quotidienne, de façon plus ou
moins perceptible, et à tel point qu'il serait difficile, voire impossible de s'en passer.
L'automatisation des procédures est de plus en plus recherchée afin de gagner en temps et en
efficacité.
Cependant et malgré l'influence des programmes informatiques au quotidien, tous n'ont pas
bénéficié du même soin accordé lors de leur développement. En effet, si l'on conçoit qu'une
anomalie dans un programme gérant une machine à café aie des conséquences limitées, on imagine
mal un programme de pilotage d'avion qui risque à tout moment de planter et qui ne soit donc pas
sûr à 100 % (même si cette sûreté totale reste utopique). Ces systèmes, dont les actions sont
essentielles et sensibles, sont dit « critiques » ; parmi eux, on peut citer les systèmes mettant en jeu
d'importantes sommes d'argent (transactions bancaires, projet coûteux tel celui d’une navette
spatiale) ou même des vies humaines (avions, centrales).
Si lors des débuts de l'informatique le fait de tester une application afin d'en limiter les
risques de plantage était complètement occulté, c'est aujourd'hui un aspect tout aussi important que
la conception du programme lui-même et ce pour plusieurs raisons. La première étant évidement la
sécurité ; comme nous l'avons évoqué précédemment, la sûreté d'un programme est essentielle dans
les systèmes critiques. La seconde est économique ; en effet, si le fait de tester un programme
s'avère une phase coûteuse, elle est cependant amortie par la suite lors de la phase de maintenance
qui s'en trouve grandement allégée. Et enfin, la dernière raison est la satisfaction du client qui aura
évidement une confiance accrue en un programme testé et « sûr » plutôt qu'en un programme qui ne
l'est pas. La concurrence au sein du monde de l'informatique étant importante, l'aspect du test est
donc naturellement passé sur le devant de la scène lors du développement d'une application.
Il existe de nombreuses manières de tester un logiciel et ce à différents niveaux. Parmi ces
techniques, le test structurel ou « boîte blanche» consiste à analyser le comportement d'un
programme dont on connaît la structure interne ; cela a l'avantage de générer une liste exhaustive
des comportements du programme suivant les données qu'il traite, et permet ainsi de détecter des
erreurs subtiles là où un test fonctionnel ou « boîte noire» (par opposition à boîte blanche) serait
passé à coté.
Un aspect important de l'activité de test est qu'elle s'adapte à tous les langages de
programmation, de plus en plus nombreux, avec une complexité variable lors de sa mise en place.
La facilité et l'efficacité à tester un programme n'est donc pas une caractéristique négligeable au
moment du choix du langage de programmation d'une application. Et dans la jungle actuelle des
langages informatiques, certains parviennent à tirer leur épingle du jeu ; c'est le cas du langage
JAVA, énormément utilisé car il offre un excellent compromis entre accessibilité aux développeurs
et performances ; il est donc important que ce langage compte des outils de test. Qui plus est, au
langage JAVA se rajoute un module d'annotation : le JML1. Il permet d'annoter des éléments du code
suivant le paradigme de programmation par contrat, et permet ensuite, à l'aide d'un vérificateur
d'assertion, de prouver le programme.
1 JML (JAVA Modeling Language) : langage de spécification pour le JAVA basé sur la programmation par contrat.
3
C'est dans cet aspect de test structurel d'un programme JAVA annoté en JML que s'inscrit
notre projet annuel de seconde année de Master Informatique option S2L (Sécurité et Sûreté du
Logiciel) à l'Université de Franche-Comté. Ce projet spécifique à l'option S2L vise à mettre en
application les connaissances acquises au cours de l'année en ce qui concerne le test et la sûreté d'un
programme.
4
2) Présentation du sujet
2.1) Description du sujet
L'Université de Franche-Comté compte de nombreux laboratoires comptant eux-mêmes
plusieurs équipes. L'équipe VESONTIO2 est une des équipes du laboratoire Informatique de
l'Université : le LIFC3 ; l'un des travaux qu'elle réalise porte sur la validation du code JAVA annoté
en JM.
Durant les cinq mois de deuxième année de Master Informatique que les étudiants passent à
l'Université de Franche-Comté, un projet annuel est attribué à chaque binôme d'étudiants suivant
leur option. L'équipe VESONTIO a donc profité de l'occasion pour soumettre un projet dont la
réalisation apportera une aide à son travail.
Le projet consiste, à partir d'un programme JAVA annoté en JML, à extraire les éléments du
programme (classes, méthodes, attributs, … ) ainsi que les éléments JML (invariant, post-condition,
… ), à les stocker dans une structure qui comportera également des opérations permettant de
générer des éléments nécessaires à un test structurel comme le graphe de flot de contrôle4 (cf.
Figure 1) ainsi que les chemins de couverture de graphe suivant les critères classiques (tous les
nœuds, tous les arcs, tous les n-chemins) ; ces opérations étant également à implémenter lors du
projet.
2 VESONTIO : VErification, SpécificatiON, Test et Ingénierie des mOdèles,
3 LIFC : Laboratoire Informatique de Franche-Comté
4 Graphe de flot de contrôle : mise en forme d'une programme informatique suivant un graphe dont les nœuds sont des
instructions ou des conditions.
5
Figure 1: Exemple de génération d'un graphe de flot de contrôle
Ce projet s'inscrit dans le cursus de Master Informatique option S2L car il permet de mettre
en application les connaissances acquises lors des deux années du cursus. La partie JML ayant été
abordée en première année lors du cours de PEP (Preuve et Évaluation de Programmes) et le test
structurel se référant au cours de deuxième année SVT (Spécification Vérification et Test) dans
lequel on créait à la main un graphe de flot de contrôle à partir d'un programme C/C++ en suivant
des algorithmes établis, puis on recherchait les chemins permettant la couverture. En résumé, le
projet consiste à automatiser la génération de graphe de flot de contrôle et la création de chemin
permettant de satisfaire les critères de couverture.
2.2) Cahier des charges
Notre première tâche a été d'établir un cahier des charges en accord avec les attentes de nos
tuteurs. Lors de la première réunion avec eux, nous avons pu établir une liste des fonctionnalités et
contraintes afin de définir clairement les attentes et exigences autour de ce projet. Nous avons
ensuite affiné ce cahier des charges au cours des réunions suivantes et au fur et à mesure de
l'avancement du projet.
Bien que nos responsables aient eu une idée assez précise de ce qu'ils attendaient, le projet,
par conséquent le cahier des charges, a quelque peu évolué au cours de la réalisation afin de
simplifier certains aspects ou de répondre plus précisément au besoin.
6
2.2.1) Contraintes fonctionnelles
En premier lieu et suivant les instructions de nos responsables, nous avons commencé par
établir les contraintes fonctionnelles qui constituent la liste des fonctionnalités attendues au terme
de ce projet.
Comme énoncé dans la description du projet, celui-ci consiste à extraire les données d'un
programme JAVA et à les formater de façon à ce qu'un utilisateur puisse les réutiliser par la suite
dans d'autres programmes JAVA. La première fonctionnalité attendue est donc de modéliser cette
structure de données permettant d'accéder aux informations suivantes du programme :
• Classes
• Relations d'héritage entre les classes
• Méthodes
• Argument des méthodes
• Variables locales des méthodes
• Type de retour des méthodes
• Exceptions pouvant être générées par les méthodes
• Attributs
• Type des attributs
• Valeur des attributs
• Packages
L'accès à ces données se fera sous la forme d'une API5.
Dans un second temps, et toujours en relation avec la structure de données, il nous a été
demandé d'enregistrer également les annotations JML qui peuvent figurer dans le programme.
Cependant, cet aspect a été remplacé au cours du développement suite aux difficultés rencontrées
pour récupérer, stocker et accéder aux informations : les annotations JML sont ignorées et les
informations qui s'y trouvaient sont stockées, au moment du développement du programme JAVA,
sous la forme d'une fonction statique dans un fichier à part.
Troisièmement, une fois la structure disponible pour l'utilisateur, l'API offre également à ce
dernier des opérations permettant d'obtenir des informations sur les graphes de flot de contrôle. En
effet, chaque méthode du programme JAVA analysé possède son propre graphe de flot de contrôle
accessible à l'utilisateur. Il nous a également été demandé de développer des fonctionnalités
permettant à ce dernier de récupérer le ou les chemins, sous forme d'une suite de nœuds du graphe,
permettant de couvrir le graphe suivant les critères de couverture suivants :
• Tous les nœuds
• Tous les arcs
• Tous les n-chemins
5 API (Application Programming Interface) : fonctions d'un programme informatique disponible via une interface .
7
Enfin, quelques fonctionnalités venant compléter la structure de données et facilitant l'accès
aux dites données ont été demandées :
• Fonction de recherche d'une méthode suivant son nom
• Fonction de recherche d'une classe suivant son nom
• Fonction permettant de récupérer la signature d'une méthode
• Fonction de négation d'une condition sur un nœud condition
• Fonction d'export du graphe au format DOT6
2.2.2) Contraintes Techniques
Une fois les fonctionnalités définies, le cahier des charges a été complété par quelques
contraintes d'ordre technique.
La première de ces contraintes concerne le langage ; on nous a demandé d'utiliser JAVA afin
d'implémenter l'API. Ce choix se justifie par le fait que l'on peut utiliser l'introspection offerte par
JAVA ce qui facilite certains aspects du développement.
Seconde contrainte toujours en relation avec le langage de programmation : il s'agit de ne
pas utiliser les classes du package JAVA java.lang.reflect mais de créer nos propres classes afin de
modéliser les méthodes, classes et autres éléments du programme. Cette demande a été faite pour
avoir une API spécifique à la demande de nos tuteurs pour son utilisation future ; cela évite en effet
d'avoir des méthodes inutiles dans l'API.
Enfin, et même s’il avait pu être remplacé par un éventuel autre outil, on nous a encouragé à
utiliser l'outil Sorcerer afin de parser le programme JAVA en entrée.
En ce qui concerne les performances, aucune demande n'a été formulée quant à la
complexité des algorithmes ou au temps de réponse des fonctions de l'API.
2.2.3) Autres contraintes
Parallèlement aux contraintes fonctionnelles et techniques, il nous est demandé d'assurer la
« réutilisabilité » de notre travail par des documents les plus détaillés et les plus clairs possible,
ainsi qu'un code commenté de sorte à être plus facilement compris lors d'une réutilisation ou
modification future.
6 Format DOT : graphe modélisé sous format texte pouvant être converti en image par la suite.
8
3) Réalisation
3.1) Analyse
La réalisation se passe en deux étapes, la première de ces étapes est l'analyse pour la
compréhension du sujet et la validation de nos choix par les responsables avant d’amorcer le
développement à proprement parler.
3.1.1) Répartition des tâches
La phase d'analyse commence avec l'identification des différentes tâches du sujet.
3.1.1.1) Fragmentation du sujet
Une fois notre sujet clairement défini, nous avons cherché à le découper en tâches afin de les
répartir de la manière la plus pertinente possible entre les deux étudiants du binôme. Ces tâches
peuvent être classées en deux phases comme il suit :
• Analyse
• Analyse globale du projet : établir le planning, comprendre le sujet.
• Analyse des données à modéliser sous forme UML7
• Méta-modèle de JAVA : définir le diagramme UML des classes pour stocker les
éléments du programme JAVA (classes, attributs, méthodes, … ).
• Modélisation du graphe : définir le diagramme UML des classes permettant de créer
et de parcourir le graphe de flot de contrôle.
• Étude de l'outil Sorcerer : apprendre à utiliser Sorcerer dans le cadre du projet.
• Développement
• Création des classes du modèle UML
• Création des classes du méta-modèle : créer les classes JAVA stockant les métainformations du programme.
• Création des classes du graphe : créer les classes JAVA modélisant un graphe de flot
de contrôle.
• Développement du parser : adapter le code de Sorcerer afin de récupérer les données du
programme et d'instancier les classes afin d'avoir accès au éléments de ce programme.
• Implémentation des opérations
• Implémentations des opérations basiques : codage des getters/setters et autres
méthodes simple permettant l'accès aux données.
• Implémentation du parcours de graphe : codage des algorithmes de parcours de
graphe afin de récupérer le chemin permettant sa couverture suivant un critère.
• Tests divers : tester les éléments que nous avons réalisés.
Les tâches en italique sont atomiques, ce sont celles que nous avons concrètement réalisées.
7 UML (Unified Modeling Language) : Langage de modélisation graphique.
9
3.1.1.2) Planning
Les tâches identifiées, nous avons également établi les relations de précédence des tâches
pour aboutir au planning ci-dessous ; étant donné que le projet se confond avec d'autres projets
durant l'année, nous ne faisons pas figurer les durées suivant les semaines.
Monôme
Tâches
Charles
Création
classes
métamodèle
Étude de Sorcerer
Analyse
Globale
Corentin
Méta-modèle de JAVA
Modélisation du Graphe
Développement du parser
Implémentation des opérations
basiques
Création
classes
graphe
Test
Implémentation du
parcours
• En bleu : phase d'analyse
• En vert : phase de développement
A ce planning viennent s'ajouter régulièrement des réunions avec nos tuteurs afin de rendre
compte de l'avancée du projet et faire valider certaines éléments avant de continuer.
3.1.2) Choix des outils
Il existe plusieurs outils disponibles - et gratuits - qui peuvent nous permettre de gagner du
temps sur le développement ; nous avons donc préalablement recherché ce genre d'outil.
3.1.2.1) Sorcerer
Au cours de ce projet nous avons décidé, en concertation avec nos tuteurs, d'exploiter un
outil de « cross referencing8 » du code source, à savoir Sorcerer.
Créé par Kohsuke Kawaguchi, cet outil a pour vocation d'analyser un ensemble de fichiers
sources Java pour produire des pages HTML référençant l'ensemble des éléments de ces fichiers.
Pour ce faire, il exploite les AST Java pour parcourir chaque élément de chaque fichier et créer les
pages HTML correspondantes.
De plus, Sorcerer a l'avantage de comprendre ces AST Java de la même manière que le font
les IDE, ce qui par conséquent lui permet d'offrir une navigation plus riche en ce qui concerne les
pages HTML créées (cf. Figure 2) par rapport à d'autres outils semblables tels que JXR ou encore
OpenGrok.
8 Cross referencing : C'est le fait d'extraire des informations d'un document tout en gardant les références point par
point du document d'origine,
10
Figure 2: Exemple de page HTML générée par Sorcerer
L'un des buts de notre projet étant de créer une structure de données à partir de l'analyse d'un
ensemble de classes Java, c'est donc tout naturellement que nous nous sommes tournés vers cet outil
Open Source.
Le fait que ce dernier soit Open Source a grandement favorisé notre choix puisqu'au final,
cela nous a permis d'intervenir directement dans le code source afin de remplacer la création des
fichiers HTML réalisée originellement par Sorcerer par notre propre Visiteur qui, par
l'intermédiaire d'un parcours de tous les nœuds de l'arbre, nous permet de créer notre structure de
données.
3.1.2.2) JavaDoc
Le sujet demandant de fournir, en plus d'un livrable, une documentation aussi complète que
possible, nous nous sommes tournés vers la JavaDoc qui permet, à partir d'un code JAVA et
d'annotations spécifiques, de générer une documentation simple et claire. La JavaDoc était donc
parfaitement adaptée pour répondre à cette contrainte.
11
3.1.3) Modélisation
La modélisation a précédé toutes les phases de développement. Cette étape vise à modéliser
les données à gérer ainsi que les opérations sur celles-ci sans les implémenter. Une fois cette
modélisation réalisée, nous avons sollicité nos responsables afin qu'ils vérifient que cela
correspondait bien à leurs attentes, et ce jusqu'à la validation complète de l’étape.
Étant donné la quantité de données à gérer et l'aspect général du projet, nous avons séparé la
modélisation du projet en deux parties : la méta-modélisation des éléments JAVA et la modélisation
des graphes de flot de contrôle.
Pour réaliser la modélisation, nous avons utilisé le diagramme des classes selon la norme
UML 2.
3.1.3.1) Méta-modèle JAVA
En premier lieu, nous avons travaillé sur la méta-modélisation des éléments JAVA (cf.
Figure 3) afin de pouvoir, après validation, développer le parser.
Figure 3: Diagramme des classes UML de la méta-modélisation des éléments JAVA
12
Voici la liste des classes modélisées pour cette partie :
•
Classe : modélise une classe JAVA.
• Attributs :
• nom : le nom donné à la classe.
• Méthodes :
• getNomQualifie() : retourne le nom complet de la classe incluant son package.
• getMethode(nom) : retourne la méthode de la classe dont le nom correspond à
l'argument « nom ».
•
Package : modélise les packages des classes JAVA.
• Attributs :
• nom : le nom donné au package.
•
Methode : modélise une classe JAVA.
• Attributs :
• nom : le nom donné à la méthode.
• Méthodes :
• getSignature() : retourne la signature de la méthode comprenant ses arguments, son
type de retour et les exceptions générées.
•
Constructeur : modélise les méthodes particulières que sont les constructeurs des classes
JAVA.
•
Donnee : modélise une donnée JAVA ; aussi bien une variable qu'un attribut, la
différenciation entre les deux se fait par les relations auxquelles la donnée est liée.
• Attributs :
• nom : l'identifiant de la donnée.
• valeur : la valeur de la donnée ; on utilise une liste pour prendre en compte les
tableaux ; la valeur de la case i est donc la valeur stockée à l'indice i dans la liste.
•
Type : modélise le type d'une donnée ; la classe est abstraite afin de modéliser plus finement
les différents types de type de donnée en JAVA.
• Attributs :
• nom : le nom donné au type.
• dimension : la dimension dans le cas d'un tableau à plusieurs dimensions ; ainsi un
tableau de type t[][] porte la valeur 2.
•
TypePrimitif : modélise les types primitifs en JAVA tels que « int » ou « char ».
•
TypeObjet : modélise les types correspondant à des objets JAVA tels que « String » ou un
objet créé par un utilisateur « MonObjet ».
•
TypeException : modélise les types d'objet particulier que sont les exceptions en JAVA tels
que « RuntimeException ».
13
Ces classes sont complétées par les relations entre elles :
•
retourne : permet de modéliser le type de retour d'une méthode ; on peut avoir 0 ou 1 type
associé pour couvrir le cas où une méthode n'a pas de type de retour.
•
typée : permet de modéliser le type d'une donnée.
•
génère : permet de modéliser les Exceptions pouvant être levées par une méthode ; on peut
avoir plusieurs ou aucune Exception susceptible d'être levée par une méthode.
•
est variable : permet de modéliser les variables locales d'une méthode.
•
est argument : permet de modéliser les arguments d'une méthode.
•
Composition [Methode → Classe] : modélise l'appartenance d'une méthode à une classe ;
cette appartenance est obligatoire d'où l'utilisation d'une composition.
•
Agrégation [Donnee → Classe] : permet de modéliser les attributs d'une classe ; une
donnée n'étant pas forcement un attribut, on utilise une agrégation plutôt qu'une
composition.
•
Agrégation [Classe → Package] : modélise les classes composant un package ; une classe
n'ayant pas forcement de package, on utilise une agrégation plutôt qu'une composition.
Une fois cette partie de la modélisation faite, il restait alors à modéliser les graphes de flot
de contrôle.
14
3.1.3.2) Modélisation du graphe de flot de contrôle
En second lieu nous avons modélisé les graphes de flot de contrôle (cf. Figure 4).
Figure 4: Diagramme des classes UML de la modélisation d'un graphe de flot de contrôle
Voici la liste des classes modélisant cette autre partie.
•
Graphe : modélise un graphe de flot de contrôle.
• Attributs :
• nom : le nom donné au graphe,
• Méthodes :
• afficherCode() : retourne le code ayant servi à construire le graphe.
• afficherGraphe() : retourne le graphe sous forme de texte.
• exporterDOT(fichier) : exporte le graphe au format DOT dans le fichier spécifié par
le chemin passé par l'argument « fichier ».
• parcourir(critere, donnee) : retourne la liste des chemins à emprunter pour assurer la
couverture suivant le critère indiqué par l'argument « critere » et éventuellement
complété par l'objet passé via l'argument « donnee » (dans le cas du critère
TypeChemin.TOUS_LES_N_CHEMINS où l'objet est le nombre d'itérations
maximal d'une boucle).
15
•
Noeud : modélise un nœud du graphe ; la classe est abstraite pour permettre le
polymorphisme et la modélisation des deux types de nœud du graphe.
• Attributs :
• numero : le numéro du nœud ; utile pour l'affichage.
• marque : une marque apposée sur le nœud ; utile pour les parcours, on utilise un
entier car dans le cas du parcours itératif, on a besoin de savoir combien de fois on
est passé sur le nœud.
• Méthodes :
• voisins() : retourne les nœuds voisins du nœud courant d'après le graphe ; les voisins
ne sont que les nœuds auxquels on peut arriver depuis le nœud courant et non tous
les nœuds liés.
• estMarque() : indique si un nœud est marqué ou non ; un nœud marqué a son attribut
marque strictement supérieur à 0.
• marquer() : marque le nœud courant ; l'opération incrémente le compteur de l'attribut
marque d'une unité.
•
NoeudInstruction : modélise un nœud de type instruction du graphe.
• Méthodes :
• suivant() : retourne le nœud suivant le nœud courant selon le graphe.
•
NoeudCondition : modélise un nœud de type condition du graphe.
• Méthodes :
• suivantVrai() : retourne le nœud suivant le nœud courant selon le graphe si la
condition est vérifiée.
• suivantFaux() : retourne le nœud suivant le nœud courant selon le graphe si la
condition n'est pas vérifiée.
• negation() : retourne la négation de l'expression utilisée pour le test du nœud.
•
Expression : modélise une expression JAVA ; exemple : « i >= 4 ».
• Attributs :
• expression : la valeur de l'expression.
•
Instruction : modélise une instruction JAVA ; exemple : « i++ ».
• Attributs :
• instruction : la valeur de l'instruction.
16
•
Chemin : modélise un chemin composé d'une suite de nœuds.
• Méthodes :
• add(noeud) : ajoute le nœud passé par l'argument « noeud » à la fin du chemin.
• addFirst(noeud) : ajoute le nœud passé par l'argument « noeud » au début du chemin.
• addAll(chemin) : ajoute tous les nœuds du chemin passés par l'argument « chemin »
au début du chemin courant.
• clone() : retourne une nouvelle instance identique du chemin courant.
• contains(noeud) : indique si oui ou non le chemin contient le nœud passé par
l'argument « noeud ».
• get(i) : retourne le i-ème nœud du chemin.
• size() : retourne le nombre de nœuds du chemin.
• doublonNoeud(chemin1, chemin2) : teste si tous les nœuds du chemin le plus long
des deux passés en paramètre sont contenus dans l'autre ; si tel est le cas, on retourne
le chemin en trop, « null » sinon.
• doublonArc(chemin1, chemin2) : teste si tous les arcs du chemin le plus long des
deux passés en paramètre sont contenus dans l'autre ; si tel est le cas, on retourne le
chemin en trop, « null » sinon.
•
TypeChemin : une classe d'énumération correspondant aux différents critères de couverture
gérés par les parcours.
• Valeurs :
• TOUS_LES_NOEUDS : critère de couverture « tous-les-nœuds ».
• TOUS_LES_ARCS : critère de couverture « tous-les-arcs »
• TOUS_LES_N_CHEMINS : critère de couverture « tous-les-n-chemins »
Les relations suivantes indiquent les interactions entre ces classes :
•
racine : permet d'obtenir la référence sur le premier nœud du graphe. Celui-ci est forcément
unique et on utilise ensuite les méthodes de la classe Noeud pour naviguer dans le graphe.
•
noeud suivant instruction : permet d'indiquer quel nœud suit un nœud instruction ; il peut
n'y avoir aucun nœud suivant dans le cas d'un nœud terminal ; en revanche, un nœud peut
être précédé d'un ou plusieurs nœuds instruction.
•
noeud suivant vrai : permet d'indiquer quel nœud suit un nœud condition dans le cas où la
condition est vérifiée ; il y a forcement un suivant si la condition est vraie et un nœud peut
être précédé d'un ou plusieurs nœuds condition.
•
noeud suivant faux : permet d'indiquer quel nœud suit un nœud condition dans le cas où la
condition n'est pas vérifiée ; il y a forcément un suivant si la condition est fausse et un nœud
peut être précédé d'un ou plusieurs nœuds condition.
•
contient : indique les instructions d'un nœud instruction ; il peut y en avoir plusieurs ou
aucune dans le cas d'un nœud terminal utilisé pour lier les autres.
•
subordonné à : indique l'expression à évaluer pour tester la condition du nœud instruction.
17
•
Agrégation [Noeud → Chemin] : modélise la liste ordonnée des nœuds à emprunter pour
parcourir le chemin.
En plus de ces classes, une classe Structure rassemblant les autres a été implémentée afin de
constituer la racine de l'application. C'est depuis cette classe que l'on peut accéder aux informations
comme les classes du programme, méthodes, attributs, graphes, … .
Cette modélisation a été validée par les responsables et nous avons pu clôturer la phase
d'analyse pour passer au développement.
3.2) Développement
Une fois l'analyse effectuée et validée, nous nous sommes lancés dans le développement à
proprement parler où nous avons codé les classes et méthodes du projet.
3.2.1) Implémentation de la structure de données
L'une des premières étapes du développement a été l'implémentation de la structure de
données préalablement définie lors de la phase d'analyse. Pour ce faire, nous avons procédé comme
suit :
• Chaque classe représentée dans le modèle UML correspond à une classe dans le package
« structure » de l'application.
• Chaque lien, qu'il soit récursif ou entre deux classes, est représenté par une variable dans la
classe correspondante. Par exemple, pour représenter le fait qu'un graphe représente une
méthode, il faut ajouter une variable de type « Graphe » dans la classe « Methode ».
• Les opérations et les attributs représentés dans le modèle UML sont implémentés en tant que
tel dans le code.
3.2.2) Développement du Parser
La plus grosse étape du projet a été le développement du Visiteur, afin de pouvoir récupérer
tous les éléments nécessaires au remplissage de la structure de données.
3.2.2.1) Variables de stockage
Pour effectuer ce remplissage, il est nécessaire d'utiliser les quelques variables suivantes
pour stocker des données :
• structure : Objet de type Structure destiné à collecter les informations des différentes classes
analysées.
• packageCourant : Objet de type « Paquetage » représentant le package auquel sont soumises
les classes du fichier parcouru.
• classeCourante : Objet de type « Classe » représentant la classe en cours d'analyse.
18
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
methodeCourante : Objet de type « Methode » représentant la méthode en cours d'analyse.
grapheCourant : Objet de type « Graphe » représentant le graphe de la méthode en cours
d'analyse.
noeudCourant : Objet de type « Nœud » représentant le nœud du graphe en cours
d'utilisation.
numeroNoeud : Entier représentant le numéro du nœud courant.
indiceCondition : Entier représentant le nombre de conditions imbriquées.
conditions : Tableau de « Noeud » représentant les nœuds condition.
inSuivantVrai : Tableau de booléens permettant de savoir si on définit le suivant « vrai »
d'une condition.
InSuivantFaux : Tableau de booléens permettant de savoir si on définit le suivant « faux »
d'une condition.
apresCondition : Tableau de booléens permettant de savoir si on vient de quitter un nœud
condition.
dernierSuivantVrai : Tableau de « Noeud » correspondant au dernier nœud suivant « vrai »
rencontré.
dernierSuivantFaux : Tableau de « Noeud » correspondant au dernier nœud suivant « faux »
rencontré.
inDoWhile : Booléen permettant de savoir si on vient de rentrer dans un « Do / While ».
premierDoWhile : Objet de type « Noeud » représentant le premier nœud rencontré suite à
l'entrée dans la boucle « Do / While ».
donneeCourante : Objet de type « Donnee » représentant la variable ou l'argument en cours
de parcours.
breakManquant : Booléen permettant de savoir si le dernier « case » du « switch » en cours
de parcours ne se finit pas par un « break ».
derniereInstructionBreakManquant : Objet de type « Noeud » représentant la dernière
instruction d'un « case » sans « break ».
dernierNoeudsCases : Tableau de « Noeud » contenant tous les derniers nœuds de chaque
« case » se finissant par un « break ».
inApresSwitch : Booléen permettant de savoir si on vient de quitter un « switch ».
3.2.2.2) Méthodes de visite
Concernant notre visiteur « MyTreePrinterVisitor », il hérite d'un « SimpleTreeVisitor
<Object, String> » dont il surcharge toutes les méthodes, afin de permettre l'exploitation de toutes
les instructions possibles lors de l'analyse. Ces méthodes se divisent en trois groupes :
• Celles représentant une condition, qui bénéficient quasiment toutes d'un traitement
semblable.
• Celles représentant une instruction, qui elles aussi sont quasiment toutes identiques.
• Les autres.
Ce que l'on peut également dire concernant les méthodes des deux premiers groupes, c'est
qu'elles ont une grosse partie commune afin de mettre à jour les différentes variables utilisées pour
le remplissage de la structure, que je qualifierai par la suite de « traitement commun ».
19
3.2.2.2.1) Le traitement commun
Dans ce traitement commun, on effectue les tests suivants :
• Si l'instruction courante est la première d'un nouveau nœud.
• On crée le nœud correspondant.
• On teste si c'est la première instruction d'un « Do / While », et si c'est le cas, alors
premierDoWhile prend comme valeur celle du nœud courant.
• On teste si c'est l'instruction qui suit directement un « case » qui ne contenait pas de
« break », et dans ce cas, si derniereInstructionBreakManquant contient un nœud
instruction, alors son suivant devient le nœud courant.
• On teste si c'est l'instruction qui suit directement la sortie d'un « switch », et si c'est le
cas, le nœud courant devient le suivant de tous les nœuds contenus dans
dernierNoeudsCases.
• On teste si c'est la toute première instruction parcourue de la méthode, et si c'est le cas,
le nœud courant devient le nœud racine du graphe.
• On fait plusieurs tests consécutifs sur les tableaux dernierSuivantVrai,
dernierSuivantFaux, apresCondition, inSuivantVrai, inSuivanFaux pour mettre à jour les
suivants des derniers nœuds d'une condition lorsqu'on vient juste de sortir de cette
dernière.
• On effectue enfin un dernier test pour savoir si on est dans une condition et on met à jour
les suivant « vrai » et « faux » si nécessaire.
• Sinon, on se contente d'ajouter l'instruction à la liste des instructions du nœud courant.
3.2.2.2.2) Les méthodes représentant une condition
•
public Object visitCase()
« switch » est atteinte.
•
public Object visitConditionalExpression() : Parcourue si une expression
conditionnelle ((x > 10) ? true : false par exemple) est rencontrée.
•
public Object visitDoWhileLoop() Parcourue si une boucle « Do / While »
est rencontrée.
•
public Object visitEnhancedForLoop() : Parcourue si une boucle for
« améliorée »
((for(int
i
:
collectionEntier)
par
exemple,
où
collectionEntier est une collection itérable d'entiers) est rencontrée.
•
public Object
rencontrée.
•
public Object
rencontrée.
•
public Object visitWhileLoop() : Parcourue si une boucle « while » est
rencontrée.
:
visitForLoop()
visitIf()
:
Parcourue
:
si
Parcourue
Parcourue
si
la
si
clause
une
une
« case »
d'un
boucle
« for »
est
condition
« if »
est
Après avoir réalisé le traitement commun dans ces méthodes, on ajoute le nœud représentant
la condition courante au tableau conditions, tout en ajoutant une entrée dans les tableaux
inSuivantVrai, inSuivantFaux, dernierSuivantVrai, dernierSuivantFaux et apresCondition et en
initialisant les valeurs à « false » ou « null ».
20
On parcourt ensuite respectivement les nœuds suivant « vrai » et suivant « faux » de la
condition en prenant soin :
• de mettre noeudCourant a null avant chacun des parcours.
• de préciser respectivement que inSuivantVrai puis inSuivantFaux sont à « true » avant les
parcours.
• d'enregistrer les dernierSuivantVrai et dernierSuivantFaux après chacun des parcours.
• de mettre noeudCourant à « null » et apresCondition à « true » une fois les deux parcours
effectués.
3.2.2.2.3) Les méthodes représentant une instruction
•
public Object visitAssert() : Parcourue si une assertion (assert(i == 3) par exemple) est
rencontrée.
•
public Object visitBreak() : Parcourue si l'instruction « break; » est rencontrée.
•
public Object visitContinue() : Parcourue si l'instruction « continue; » est rencontrée.
•
public Object visitExpressionStatement() : Parcourue si une expression (i = 3 par
exemple) est rencontrée.
•
public Object
rencontrée.
•
public Object visitReturn() : Parcourue si une instruction de retour ( return i par
exemple) est rencontrée.
•
public Object visitThrow() : Parcourue si une instruction « throw » est rencontrée.
•
public Object visitVariable() : Parcourue si une déclaration de variable est rencontrée.
visitLabeledStatement()
:
Parcourue
si
une
déclaration
nommée
est
Pour toutes ces méthodes, on se contente d'effectuer le traitement commun.
3.2.2.2.4) Les autres méthodes
Parmi ces méthodes, la plupart font appel à d'autres méthodes pour visiter plus en
profondeur l'instruction courante. Néanmoins, certaines d'entre elles effectuent quand même
quelques modifications sur les variables permettant de remplir la structure de données. Si tel est le
cas, les actions réalisées par ces méthodes seront spécifiées juste en-dessous de leur nom.
• public Object defaultAction() : Parcourue par défaut si la méthode correspondant à
l'instruction rencontrée n'est pas implémentée.
• public Object visitAnnotation() : Parcourue si une annotation (@Override par exemple) est
rencontrée.
• public Object visitArrayAccess() : Parcourue si un accès à un tableau (tab[i] par
exemple) est rencontré.
21
• public Object visitArrayType() : Parcourue si une déclaration de tableau (String[] tab
par exemple) est rencontrée.
• public Object visitAssignment() : Parcourue si une assignation (i = 5 par exemple) est
rencontrée.
• public Object visitBinary() : Parcourue si une expression binaire (i == 3 && test == true
par exemple) est rencontrée.
• public Object visitBlock() : Parcourue si un bloc d'instructions est rencontré.
◦ Si c'est le premier bloc d'instructions parcouru d'une méthode, alors il permet
d'instancier la variable « code » du graphe correspondant à la méthode courante.
• public Object visitCatch() : Parcourue si la clause « catch » d'un « try » est atteinte.
• public Object visitClass() : Parcourue si une classe est rencontrée.
◦ Permet de définir le nom de la classe courante, l'éventuel package auquel elle est
associée, et l'éventuel super-classe dont elle dépend.
• public Object visitCompilationUnit() : Parcourue si l'analyse d'un nouveau fichier
débute.
◦ Permet de récupérer le package dont dépendront toutes les classes de ce fichier.
• public Object visitCompoundAssignment() : Parcourue si une assignation composée (i += 3
ou i /= 2 par exemple) est rencontrée.
• public Object visitEmptyStatement() : Parcourue si seul un point virgule est rencontré.
• public Object ErroneousTree() : Parcourue si une expression malformée est rencontrée.
• public Object visitIdentifier() : Parcourue si un nom de variable (i par exemple) est
rencontré.
• public Object visitImport() : Parcourue si un import (import java.lang.* par exemple) est
rencontré.
• public Object visitInstanceOf() : Parcourue si une instruction de type « instanceof » (i
instanceof Integer par exemple) est rencontrée.
• public Object visitLiteral() : Parcourue si la valeur littérale d'une variable (5 pour un
entier par exemple) est rencontrée.
• public Object visitMemberSelect() : Parcourue si un accès à un membre d'une classe
(Personne.nom serait l'accès à la variable nom de la classe « Personne » par exemple) est
rencontré.
• public Object visitMethodInvocation() : Parcourue si un appel à une méthode est
rencontré.
• public Object visitMethod() : Parcourue si une méthode est rencontrée.
◦ Permet de distinguer les méthodes et les constructeurs, de définir leurs nom, leur
type de retour, leurs paramètres, les éventuelles exceptions qu'elles sont
susceptibles de lancer, et d'initialiser la majeure partie des tableaux .
• public Object visitModifiers() : Parcourue lorsqu'une déclaration d'une nouvelle variable
ou d'une nouvelle méthode est rencontrée, pour récupérer sa visibilité et son type s'ils
existent.
22
• public Object visitNewArray() : Parcourue si l'instanciation d'un tableau est rencontrée.
◦ Permet de récupérer la dimension des tableaux qui sont déclarés.
• public Object visitNewClass() : Parcourue si l'instanciation d'une classe est rencontrée.
• public
Object
visitParameterizedType()
:
Parcourue
(SimpleTree<Object, String> par exemple) est rencontré.
si
un
type
paramétré
• public Object visitParenthesized() : Parcourue si une expression entre parenthèses est
rencontrée.
• public Object visitPrimitiveType() : Parcourue si un type primitif est rencontré.
• public Object visitSwitch() : Parcourue si une condition « switch » est rencontrée.
◦ Permet d'initialiser le tableau dernierNoeudsCases et de mettre inApresSwitch a
« true » une fois toutes les « cases » parcourues.
• public Object visitSynchronized() : Parcourue si un bloc synchronisé est rencontré.
• public Object visitTry() : Parcourue si un bloc « try » est rencontré.
• public Object visitTypeCast() : Parcourue si une coercition ((short) i par exemple) est
rencontrée.
• public Object visitTypeParameter() : Parcourue si un héritage est rencontré.
• public Object visitUnary() : Parcourue si une expression unaire ( i++ par exemple) est
rencontrée.
3.2.3) Implémentation de la structure des graphes
La structure modélisant les graphes a été développée parallèlement au parser étant donné
qu'ils sont mutuellement dépendants.
Il s'agit principalement de modéliser les classes, les méthodes s'apparentant à des
getters/setters avec quelques conditions et/ou formatage.
3.2.4) Parcours de graphe
Cette partie du développement vient en quelque sorte concrétiser le travail fait en aval
puisqu'il s'agit de la fonctionnalité finale du projet, à savoir automatiser la recherche des chemins
permettant la couverture des graphes. Cette partie dépend entièrement de toutes les autres et ne
peut donc pas être réalisée sans leur achèvement préalable.
Le parcours consiste à implémenter une fonction qui, à partir d'un graphe, renvoie une liste
des chemins nécessaires pour assurer la couverture de celui-ci suivant un critère passé en entrée.
La fonction est une méthode de la classe Graphe car de cette façon, on peut savoir
directement sur quel graphe travailler ; voici son détail :
• Prototypage : public ArrayList<Chemin> parcourir(TypeChemin critere, Object donnee)
throws IllegalArgumentException
23
•
•
•
•
Retour : une liste contenant les chemins nécessaires pour assurer la couverture.
Paramètres :
• critere : le critère de la couverture
• donnee : une donnée venant compléter le critère ; en l'état actuel de la fonction, ce
paramètre est utilisé avec le critère « TOUS_LES_N_CHEMINS » pour indiquer la
valeur de « N » correspondant au nombre d'itérations dans les boucles.
Exceptions :
• IllegalArgumentException : cette exception est levée lors du passage d'un paramètre
incorrect ; actuellement, cette exception est lancée lors de l'utilisation du critère
« TOUS_LES_N_CHEMINS » si la donnée complémentaire passée est incorrecte (le
type n'est pas un entier ou la valeur n'est pas positive).
Algorithme : la fonction délègue le parcours de graphe, suivant le critère passé en
paramètre, à des fonctions privées de la classe Graphe. Il existe deux sous-fonctions :
• parcours(...) : effectue un parcours en profondeur du graphe, sans doublon de nœuds (les
boucles sont parcourues une seule et unique fois). On l'utilise pour les critères suivants :
• tous-les-arcs : la fonction parcours tous les arcs du graphe, on peut donc directement
retourner les chemins trouvés après un post-traitement pour éliminer les souschemins contenant les mêmes arcs ; exemple [1, 2, 3, 2, 4] et [1, 2, 4] : on conserve
uniquement [1, 2, 3, 2, 4].
• tous-les-noeuds : les chemins trouvés subissent un post-traitement dans lequel on
élimine les chemins contenant des nœuds redondants d'un chemin à l'autre ; exemple
[1, 3, 4, 5] et [1, 2, 3, 4, 5, 6,] : on conserve uniquement [1, 2, 3, 4, 5, 6,].
• parcoursBoucle(...) : effectue un parcours en profondeur du graphe au cours duquel les
boucles sont parcourues un nombre fini de fois (ce nombre étant indiqué lors de l'appel à
la fonction). On l'utilise pour les critères suivants :
• tous-les-n-chemins : la fonction parcourt tous les arcs et effectue le nombre maximal
d'itérations dans les boucles. On retourne alors directement les chemins trouvés. La
fonction effectuant un marquage des nœuds, on annule également ce marquage une
fois les chemins trouvés.
Voici les algorithmes utilisés pour les parcours :
3.2.4.1) La fonction Parcours()
•
•
•
Description : effectue un parcours en profondeur du graphe en évitant les boucles.
Idée : On explore récursivement chaque nœud de proche en proche en se déplaçant sur les
successeurs (voisins) suivant le graphe. Avant de parcourir un successeur, on vérifie qu'il n'a
pas déjà été parcouru dans le cas où il y a plusieurs possibilités de successeur ; dans le cas
contraire, on se déplace vers l'unique successeur. Chaque nœud parcouru est donc stocké
dans une liste que l'on passe à chaque appel de la fonction.
Paramètres :
• depart : Nœud courant à partir duquel on déroule le graphe
• c : Chemin contenant les nœuds déjà parcourus
24
•
Algorithme :
c ← ajouterFin(depart)
SI depart a des successeurs ALORS
POUR tous les successeurs s de depart FAIRE
SI s est un noeud condition ALORS
SI s n'appartient pas à c ALORS
r ← parcours(s, c)
POUR tous les chemins ch de r FAIRE
ch ← ajouterDebut(depart)
FinPOUR
FinSI
SINON
r ← parcours(s, c)
POUR tous les chemins ch de r FAIRE
ch ← ajouterDebut(depart)
FinPOUR
FinSI
FinPOUR
retour r
SINON
ch ← cheminVide()
ch ← ajouterFin(depart)
lstCh ← listeCheminsVide()
lstCh ← ajouter(ch)
retour lstCh
FinSi
3.2.4.2) La fonction ParcoursBoucle()
•
•
•
Description : effectue un parcours en profondeur du graphe en itérant un nombre fini de fois
les boucles.
Idée : On explore récursivement chaque nœud de proche en proche en se déplaçant sur les
successeurs (voisins) suivant le graphe. Avant de parcourir un successeur, on marque le
nœud courant en incrémentant son compteur. Si le successeur a déjà été parcouru, on vérifie
qu'on puisse rentrer dans la boucle, sinon on continue sans y passer.
Paramètres :
• depart : Nœud courant à partir duquel on déroule le graphe
• c : Chemin contenant les nœuds déjà parcourus
• it : Entier indiquant le nombre d'itérations maximal pour chaque boucle.
25
•
Algorithme :
c ← ajouterFin(depart)
incrementerMarque(depart)
SI depart a des successeurs ALORS
POUR tous les successeurs s de depart FAIRE
SI s est un noeud condition ALORS
SI s n'appartient pas à c ALORS
r ← parcoursBoucle(s, c, it)
POUR tous les chemins ch de r FAIRE
ch ← ajouterDebut(depart)
FinPOUR
SINON SI valeurMarque(depart) <= it ET s est le successeur si VRAI ALORS
r ← parcoursBoucle(s, c, it)
POUR tous les chemins ch de r FAIRE
ch ← ajouterDebut(depart)
FinPOUR
SINON SI s est le successeur si FAUX ALORS
r ← parcoursBoucle(s, c, it)
POUR tous les chemins ch de r FAIRE
ch ← ajouterDebut(depart)
FinPOUR
FinSI
SINON
r ← parcoursBoucle(s, c, it)
POUR tous les chemins ch de r FAIRE
ch ← ajouterDebut(depart)
FinPOUR
FinSI
FinPOUR
retour r
SINON
ch ← cheminVide()
ch ← ajouterFin(depart)
lstCh ← listeCheminsVide()
lstCh ← ajouter(ch)
retour lstCh
FinSi
3.2.5) Test
Une fois les éléments implémentés, nous avons pu tester le programme sur les exemples
fournis par nos tuteurs et ainsi corriger les derniers problèmes.
3.2.6) Documentation
En complément du code, nous avons également commenté certains éléments afin de pouvoir
générer la JavaDoc qui aidera à la compréhension du code, cette documentation venant compléter
les commentaires à l'intérieur du code.
26
4) Résultat
Dans cette partie, nous allons vous présenter un exemple qui montre le cheminement réalisé
par l'application qui, à partir d'un fichier Java à analyser, crée une structure de données avant de,
éventuellement, créer le graphe de flot de contrôle d'une des méthodes présente dans la structure si
l'utilisateur le souhaite, par l'intermédiaire du logiciel dot.
4.1) Fichier JAVA analysé
Le fichier analysé pour cet exemple, qui n'est autre qu'une partie de celui fourni par nos
tuteurs de projet, est celui présenté ci-dessous :
Figure 5: Fichier exemple JAVA à traiter
27
4.2) Structure Correspondante
L'analyse du fichier JAVA commence par générer une structure correspondant à la métamodélisation du fichier. Le diagramme d'objets UML ci-dessous décrit cette structure :
Figure 6: Diagramme d'objets UML représentant l'instanciation des éléments du fichier JAVA
4.3) Graphe résultant
Une fois le fichier Java analysé et la structure de données créée, l'utilisateur peut alors
décider de créer le graphe de flot de contrôle d'une des méthodes du fichier.
Dans notre cas, il s'agit de la méthode « main » de la classe « eurochk ». Pour ce faire, il
suffit à l'utilisateur d'ajouter, dans le fichier « Lanceur.java », les deux lignes suivantes :
Graphe g = s.getGraphe("eurochk", "void main(String[])");
g.dessinerGraphe(g.getRacine());
28
La première ligne permet de récupérer dans la structure un graphe en spécifiant le nom de la
classe dans laquelle la méthode dont il dépend est contenue, et la signature de cette même méthode.
La seconde ligne permet quant à elle de créer le fichier « main.png » qui correspond au graphe de
flot de contrôle représentant notre méthode.
On obtient donc finalement le graphe de flot de contrôle suivant :
Figure 7: Graphe résultant de l'analyse du fichier JAVA
4.4) Couverture du graphe
Une fois le graphe généré, on peut appeler la méthode à parcourir afin de trouver les
chemins permettant la couverture du graphe suivant les différents critères ; on obtient ainsi :
•
•
•
Tous les nœuds : [1 2 3 2 4]
Tous les arcs : [1 2 3 2 4]
Tous les n chemins (n = 2) : [1 2 4] [1 2 3 2 4] [1 2 3 2 3 2 4]
29
5) Bilans
Arrivant au terme de ce rapport et de ce projet, nous pouvons maintenant tirer un bilan de
cette expérience.
5.1) Bilan pédagogique
D'un point de vue pédagogique, ce projet nous a permis d'approfondir nos connaissances en
ce qui concerne la structure des fichiers Java grâce à toutes les méthodes implémentées dans notre
visiteur, à savoir « MyTreePrinterVisitor ».
Il nous a également permis de découvrir comment fonctionne le compilateur « javac », grâce
au travail réalisé sur l'API du « Compiler Tree » par l'intermédiaire du visiteur
« MyTreePrinterVisitor ».
Enfin, le fait de pouvoir réaliser à la fois une phase d'analyse, avec le diagramme UML de la
structure de données, et une phase de développement, avec l'implémentation de cette même
structure et le développement du visiteur, durant le même projet est une façon de procéder
intéressante et trop peu répandue à notre sens puisqu'elle présente l’avantage d’une certaine rigueur
de travail.
5.2) Bilan humain
Le projet se déroulant en binôme, il a nécessité une communication importante. Si les tâches
que nous avions identifiées étaient relativement indépendantes entre elles, il fallait néanmoins se
tenir au courant de l'avancement de son monôme afin d'éviter un effet tunnel qui aurait abouti à de
sérieux problèmes au moment de la mise en commun.
Ce projet en binôme n'étant pas le premier, il nous a donné l'occasion d'améliorer les points
qui nous avaient posé problème lors des précédent projets. Au final, nos expériences respectives
précédentes nous ont été profitables et ont permis un travail commun plus efficace.
30
6) Conclusion
Arrivés au terme de ce projet nous pouvons en dire qu’il fût très enrichissant au niveau
pédagogique. Nous avons en effet eu l'occasion de mettre en application nos connaissances acquises
pendant le cursus voire même de les approfondir. C’est une application concrète de ce qui nous a été
enseigné en cours. Nous avons pu, une nouvelle fois, expérimenter le travail en binôme et tester
certaines méthodes de travail pour réaliser ce projet, ce qui nous a offert une occasion de remédier
aux problèmes que nous avions rencontrés lors de nos précédents projets en binôme.
Bien évidemment, nous n’aurons pas été les seuls bénéficiaires de ce projet ; l'équipe
VESONTIO du laboratoire Informatique de l'Université de Franche-Comté possède désormais un
outil qui lui sera utile dans ses futurs travaux.
Bien que nous ayons satisfait au cahier des charges, il reste de nombreuses évolutions
possibles. En effet, il existe d'autres critères de couverture que les trois que nous avons traitées et il
peut être intéressant de les implémenter. De même, nous avons dû occulter certains éléments de la
grammaire JAVA comme la gestion des « iterator » dans les boucles. Et à un niveau plus élevé,
l'outil ne permet d'avoir des applications que pour les tests structurels mais il peut être réutilisé (au
moins en partie) pour d'autres types de tests. L'outil est donc fonctionnel, répondant au cahier des
charges, mais perfectible.
Au final, et à quelques jours du stage en entreprise de Master Informatique, nous avons pu
nous exercer à un travail similaire à celui que nous aurons à effectuer (programmation JAVA, test,
…) et surmonter les problèmes pour achever le projet qui aura été un véritable plus dans notre
cursus. Ce dernier projet nous aura confortés sur nos capacités à nous adapter et à répondre aux
besoins et nous permet d’envisager sereinement notre avenir de développeurs informatiques.
31
Références :
– Documentation JAVA officielle : http://download.oracle.com/javase/6/docs/
– Site officiel Sorcerer : https://sorcerer.dev.java.net/
– Portail Wikipédia parcours de graphe : http://fr.wikipedia.org/wiki/Parcours_de_graphe
32
Outils utilisés :
−
−
−
−
−
Modélisation UML : IBM Rational Software
Édition / génération de document texte : OpenOffice.org / JavaDoc
Développement : Eclipse
Génération de graphe sous forme d'image : DOT
Génération de la documentation JAVA : JavaDoc
33
Résumé : Le Master informatique à l'Université de FrancheComté se termine sur un projet tutoré destiné à appliquer les
connaissances acquises durant le cursus. Notre projet concerne
l'automatisation de tests d'un programme JAVA annoté en JML
et plus précisément de tests structurels basés sur l'utilisation d'un
graphe de flot de contrôle. Il s'agit ici de parser un fichier JAVA
et d'en générer les graphes de flot de contrôle des méthodes
correspondants.
Mots clef : JAVA, JML, Graphe de flot de contrôle, Test
Abstract : The Master degree in the Université de FrancheComté ends with a project which aim is for us to apply our
knowledge gathered during our studies. Our project is about the
JAVA/JML program testing automation, and more precisely of
structural tests based on control flow graphs. Concretely, we
have to parse a JAVA file and generate the corresponding control
flow graphs of its methods.
Keywords : JAVA, JML, Control flow graph, Testing
34
Téléchargement