Outil de test structurel pour JAVA/JML - FIEUX Corentin GÉRARDIN Charles

La page est créée Guy Chauvet
 
CONTINUER À LIRE
Outil de test structurel pour JAVA/JML - FIEUX Corentin GÉRARDIN Charles
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éta-
               informations 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
                                                               Création        Développement du parser
                                                               classes
Charles                           Étude de Sorcerer
                                                                méta-        Implémentation des opérations
              Analyse                                          modèle                 basiques
                                                                                                                 Test
              Globale
                                                                           Création
                                                                                        Implémentation du
Corentin                  Méta-modèle de JAVA     Modélisation du Graphe   classes
                                                                                            parcours
                                                                            graphe
                                                                              • 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
 » 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()            :    Parcourue      si     la   clause   « case »     d'un
        « 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     visitForLoop()         :   Parcourue     si    une   boucle    « for »   est
        rencontrée.
    •   public Object      visitIf()     :       Parcourue    si      une    condition   « if »    est
        rencontrée.
    •   public Object visitWhileLoop() : Parcourue si une boucle « while » est
        rencontrée.

         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   visitLabeledStatement()   :   Parcourue   si   une   déclaration   nommée   est
        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.

        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       si    un   type    paramétré
      (SimpleTree par exemple) est rencontré.
    • 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 parcourir(TypeChemin critere, Object donnee)
       throws IllegalArgumentException

                                                                                                  23
Vous pouvez aussi lire