Outil de test structurel pour JAVA/JML - FIEUX Corentin GÉRARDIN Charles
←
→
Transcription du contenu de la page
Si votre navigateur ne rend pas la page correctement, lisez s'il vous plaît le contenu de la page ci-dessous
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