Précédent : Présentation générale et
objectifs Remonter : Projet PAMPA, Modèles et outils
Suivant : Grands domaines
d'application
Résumé : le projet élabore de nouvelles technologies logicielles permettant d'aider le développement des logiciels répartis. Les problèmes centraux sont la modélisation des processus et comportements, et le développement d'algorithmes associés pour raffiner la conception, générer du code ou des tests. Ces questions sont examinées dans le cadre des architectures logicielles (à objets répartis). Les techniques de validation utilisées s'appuient sur des simulations complexes des modèles considérés.
Logiciel réparti: désigne un programme informatique dont
l'exécution met en jeu un ensemble de calculateurs travaillant en
réseau. Nous considérons généralement que l'interaction entre ces
calculateurs est asynchrone et s'effectue par échange de
messages. Asynchrone signifie qu'un message peut rester en
transit un temps non déterminé, découplant ainsi fortement
l'activité des processus s'exécutant sur ces calculateurs. En
général ce type de logiciel est aussi réactif dans le sens où
chacun des processus doit réagir aux sollicitations de son
environnement et émettre des réponses à ces
sollicitations.
Mots-clés : systèmes de transitions étiquetés,
ensembles partiellement ordonnés
Résumé : La structure mathématique qui caractérise le mieux les fondements des travaux de recherche en vérification et génération de programmes répartis sont les systèmes de transitions étiquetés (labelled transition systems en anglais, abréviation LTS) [Arn92]. Cette structure, développée il y a près de cinquante ans est l'un des fondements de l'informatique ; aussi il nous a paru utile de préciser de quelle façon nous utilisons cette structure, notamment sa construction au vol. L'autre aspect fondamental est la notion de causalité entre événements dans les exécutions réparties. C'est le concept central qui permet de parler de l'analyse des comportements des systèmes distribués [Jar94]. Il est aussi à la base des plus beaux résultats en algorithmique répartie.
Un LTS est un graphe orienté dont les arêtes, appelées
transitions, sont étiquetées par une lettre prise dans un
alphabet d'événements. Les sommets de ce graphe sont appelés
états.
Avec : ensemble des états,
l'état initial,
l'ensemble des événements,
la relation de transition.
Il est usuel de parler d'automate d'états finis pour désigner un système de transitions étiqueté dont l'ensemble des états et celui des événements sont finis. Il s'agit en fait du modèle de machine le plus simple que l'on puisse imaginer. Nous employons les LTS pour modéliser des systèmes réactifs le plus souvent répartis. Dans ce cadre les événements représentent les interactions (entrées ou sorties) du système avec son environnement. On parle alors de système de transitions entrées-sorties ou de IOLTS (input-output LTS).
Ces systèmes de transitions sont obtenus à partir de spécifications de systèmes réactifs répartis décrits dans des langages de haut niveau comme LDS ou LOTOS. L'association d'un LTS à un programme se fait par l'intermédiaire d'une définition opérationnelle de la sémantique du langage et est en général formalisée sous la forme d'un système de déductions. Pour un langage aussi simple qu'une algèbre de processus (CCS par exemple), la définition de sa sémantique opérationnelle tient en moins de dix axiomes et règles d'inférences ; alors que pour un langage aussi complexe que LDS, cela est plutôt l'affaire d'un document de plus de cent pages.
Pour des raisons de performance, ces sémantiques opérationnelles ne sont jamais mises en oeuvre directement ; mais font l'objet de transformations diverses. En particulier, la compacité du codage des états est un facteur déterminant de l'efficacité de la génération des LTS.
Les calculs et transformations opérés sur les LTS se résument à des parcours et calculs de points fixes sur les graphes. L'originalité réside dans la façon de les effectuer : par calcul explicite du LTS ou bien implicitement, sans calcul ou stockage exhaustif du LTS.
Les algorithmes classiques de théorie des langages construisent explicitement des automates d'états finis. Ils sont le plus souvent intégralement stockés en mémoire. Cependant, pour les problèmes qui nous intéressent, la construction (ou la mémorisation) exhaustive des LTS n'est pas toujours nécessaire. Une construction partielle suffit et des stratégies analogues aux évaluations paresseuses des programmes fonctionnels peuvent être employées : seule la partie nécessaire à l'algorithme est calculée.
Dans le même esprit il est possible d'oublier certaines parties précédemment calculées du LTS ; et par recyclage judicieux de la mémoire, il est possible d'économiser l'espace mémoire utilisé par nos algorithmes.
La combinaison de ces stratégies de calcul sur des LTS implicites permet de traiter des systèmes de taille réelle même en utilisant des moyens de calcul tout à fait ordinaires.
On considère qu'une exécution répartie sur un réseau de
processus est faite d'événements atomiques
,
certains étant observables, d'autre ne l'étant pas. Chaque
événement est l'occurrence d'une action ou opération (Notons
l'alphabet des actions) ; on considère
habituellement qu'une action a lieu sur un seul et même processus
du réseau. Nous avons donc :
La relation de causalité décrit le plus petit ordonnancement partiel sur les événements que l'on peut déduire du modèle de fonctionnement du réseau de processus que l'on s'est donné. Il a été présenté sous cette forme pour la première fois dans [Lam78] avec comme hypothèses sur le fonctionnement de l'architecture répartie :
Ces deux axiomes nous permettent de définir une relation
d'ordre sur
: c'est la relation de
causalité.
Tout ce que l'on peut dire est que cet ordonnancement aurait respecté l'ordonnancement causal ; autrement dit, le comportement réel du système est une extension linéaire de l'ordre de causalité.
Il n'est cependant ni réaliste ni même utile de chercher à savoir quelle extension linéaire s'est réellement produite. Cela n'est pas réaliste car les architectures réparties existantes n'offrent pas les moyens de synchroniser des horloges locales à chaque processus avec une précision suffisante. Cela n'est pas utile car cet ordonnancement dépend de conditions d'exécution qui ne sont pas contrôlables ou répétables. Il faut donc considérer que toute extension linéaire de la relation de causalité est un ordonnancement plausible.
La difficulté est dans la combinatoire en général exponentielle dans le nombre d'événements des extensions linéaires d'une relation d'ordre. Il existe cependant une structure intéressante pour représenter l'ensemble les états dans lequel le système a pu se trouver : c'est le treillis des antichaînes de la relation d'ordre [DP90]. En terme d'exécution répartie ceci correspond à désigner pour chaque processus quel a été le dernier événement qui s'est produit ; cela définit sans ambiguïté un état possible du système. Ce treillis (distributif) peut être représenté sous la forme d'un LTS, avec comme relation de transition, la relation de couverture.
Mots-clés : Test de conformité, spécification,
implantation sous test (IUT), cas de test, objectif de test,
point de contrôle et d'observation (PCO), système de transitions
à entrées sorties (IOLTS)
Spécification: Une description du comportement attendu
du système à tester. Dans le cadre de la génération automatique
de tests, la spécification est supposée donnée dans un langage
formel (LDS, LOTOS, Estelle ou autre). Son comportement donné par
la sémantique opérationnelle du langage est décrit par un système
de transitions.
Implantation sous test (IUT): L'implantation réelle du
système à tester. On fait l'hypothèse que le comportement de
l'IUT est modélisable par un système de transition.
Cas de test: Décrit un ensemble d'interactions entre le
testeur et l'implantation sous test, des opérations sur des
temporisateurs de test, et des verdicts.
Objectif de test: Description abstraite d'un aspect du
système à tester.
Point de contrôle et d'observation (PCO): Interface du
système par lequel le testeur peut interagir avec l'IUT.
Relation de conformité: Relation entre spécifications
et modèles d'implantations qui caractérise les implantations
correctes.
Résumé : Le test de conformité est un test de type boîte noire. On se donne une spécification d'un système ouvert qui sert de modèle de référence et une implémentation réelle de ce système dont on ne connait le comportement que par ses interactions avec l'environnement. Un test consiste à stimuler l'IUT par des événements émis depuis l'environnement par un testeur, à observer les réactions de l'IUT, et à en déduire la correction ou non de l'IUT par rapport à sa spécification en fonction d'une relation de conformité. En pratique le test de conformité ne pouvant être exhaustif, les tests permettent de montrer la présence d'erreurs (non-conformité) mais ne permettent jamais de montrer leur absence (conformité). La génération automatique de test consiste à produire automatiquement des cas de tests en fonction de la spécification et de la relation de conformité choisie. La sélection d'un ensemble significatif de cas de tests peut se faire en utilisant des objectifs de test. C'est l'approche suivie actuellement lors de l'écriture manuelle des tests.
Le modèle de base permettant la modélisation des objets relatifs au test de conformité est un modèle dérivé des systèmes de transition, appelé système de transitions à entrée sortie (IOLTS). Il distingue explicitement les entrées des sorties, permettant ainsi de modéliser le contrôle et l'observation.
Un IOLTS est un système de transitions où l'alphabet est
décomposé en ,
l'alphabet
d'entrées,
l'alphabet de sorties, et
est une action interne.
Le comportement de la spécification est décrit par un IOLTS
.Le test de conformité permettant de considérer
uniquement des traces d'exécutions visibles aux PCOs, le
comportement observable de la spécification est obtenu par
abstraction des actions internes et déterminisation,
éventuellement minimisation. C'est aussi un IOLTS
.
L'implantation n'est connue que par ses interactions avec l'environnement. Mais on suppose que le comportement de l'IUT peut être vu comme un IOLTS I qui ne peut refuser aucune entrée. Cette hypothèse de base est nécessaire pour permettre de raisonner sur la conformité des implantations.
Un objectif de test sert à sélectionner des cas de test par
rapport aux comportements décrits dans la spécification. Dans les
tests manuels, il décrit informellement une propriété attendue de
l'implantation. Dans le cadre formel, il est décrit par un IOLTS
muni d'un ensemble d'états accepteurs, lui donnant
ainsi la structure d'automate.
Un cas de test décrit des comportements du testeur. Il sera
modélisé par une extension d'IOLTS dont les
entrées/sorties sont images miroir d'actions de la spécification.
Certaines actions peuvent porter sur des temporisateurs
(armement, désarmement, échéance) et/ou contenir des verdicts
PASS, (PASS), FAIL et INCONCLUSIVE.
Plusieurs relations de conformité peuvent être choisies. Ce
choix dépend du niveau de contrôle et d'observation que l'on peut
espérer avoir sur l'IUT ainsi que des différences de comportement
que l'on tolère entre la spécification et l'IUT. Il paraît
raisonnable de penser que l'on peut observer des traces et des
blocages et de tolérer que l'IUT permette des comportements non
prévus dans la spécification à condition qu'ils diffèrent à
partir d'une entrée de l'IUT (la spécification est partielle). La
relation de conformité ioconf choisie dit donc que l'IUT
est conforme à la spécification
si après toute
trace de la spécification, les sorties possibles de l'IUT sont
incluses dans celles de la spécification et l'IUT se bloque si et
seulement si la spécification le peut également.
L'algorithme de génération de test [FJJV96] est adapté
d'algorithmes connus issus du domaine de la vérification, en
particulier les algorithmes de vérification à la volée qui
permettent de vérifier des propriétés pendant la construction du
système de transitions de la spécification. L'algorithme parcourt
un produit synchrone entre l'objectif et l'IOLTS
représentant le comportement visible de la
spécification. Le cas de test
muni de ses verdicts est
synthétisé pendant le parcours. Il correspond à l'image miroir
d'un sous-graphe de
composé de séquences acceptées par
. Il est contrôlable au sens où il ne permet aucun choix
entre une sortie et une entrée ou une sortie. Les opérations sur
les timers sont ajoutées par un parcours ultérieur de
.
Afin d'éviter le problème d'explosion combinatoire lors du
calcul de , l'algorithme précédent peut être utilisé à la
volée, cf. module
.
L'algorithme de génération de test prend en entrée un objectif
et une spécification dont le comportement est décrit
explicitement ou implicitement (génération à la volée) par
, et produit en sortie un cas de test
.On
peut montrer que les cas de tests produits sont valides (sans
biais) i.e. si l'application de
sur une implantation
produit un verdict FAIL alors l'implantation
est
non conforme à
pour la relation ioconf. Du fait du
non-déterminisme et de la sélection des tests, il est impossible
d'avoir la propriété inverse qui garantirait l'exhaustivité. Par
contre, on peut montrer que si une IUT
est non conforme, il
existe un objectif
tel que l'algorithme de génération
peut produire un cas de test
qui peut produire un
verdict FAIL. En d'autres termes, moyennant une hypothèse
d'équité sur les comportements de l'IUT, l'ensemble (infini) des
tests que peut produire l'algorithme est exhaustif pour la
relation ioconf.
Mots-clés :
objets, composants logiciels, motifs de conception,
frameworks
L'approche objet est aujourd'hui devenue incontournable pour l'analyse, la conception et la réalisation des grands systèmes d'information devant évoluer sur de longues périodes de temps, et pour lesquels l'effort principal en termes de logiciel (parfois jusqu'à 80% ou plus) est consacré à la maintenance [Mey88]. Fondée sur la notion d'objets, c'est à dire d'instances de classes modélisant les entités stables d'un système d'information comme des modules autonomes organisés selon des relations d'héritage (classification) et d'utilisation, cette approche permet en effet de gérer la nature fondamentalement incrémentale, itérative et évolutive du développement de tels logiciels [Jac85,Boo94]. Les diverses phases d'analyse, de conception et de réalisation utilisent le même cadre conceptuel (fondé sur cette notion d'objet) et n'ont pas de frontières rigides entre elles, ce qui fait que le processus de développement objet est parfois qualifié de continu [Jéz96].
Cette notion d'objet fourni le substrat nécessaire au développement du concept de composant logiciel: l'encapsulation et le masquage d'information permettent d'établir une analogie avec les composants matériels, avec en plus la notion d'adaptabilité apportée par l'héritage qui permet de les spécialiser souplement. L'idée maîtresse est ici de réaliser des économies d'échelle dans le développement de logiciels en réutilisant des composants plutôt qu'en redéveloppant les applications à partir de zéro. Ceci a un impact majeur sur le cycle de vie du logiciel, qui doit maintenant intégrer des activités de :
Mais pour espérer faire de réelles économies d'échelle dans le développement de logiciels, il faut aller au delà de la réutilisation de composants dits techniques (e.g. structures de données générales, objets d'interfaces graphiques, etc.) et réutiliser aussi des composants «métiers», ce qui pose des problèmes difficiles d'intégration et de validation, comme en témoigne le crash du vol Ariane 501 [18].
Si la construction de systèmes complexes à l'aide des technologies objets commence à être assez bien maîtrisée, en revanche le bât blesse encore pour tout ce qui touche au parallélisme et à la répartition. Il paraît en particulier difficile d'apporter une solution universelle prenant en compte tous les aspects possibles de leur programmation. Ne serait-ce qu'en termes de sémantique de la communication entre processus (rendez-vous, files, RPC, hypothèses de fiabilité, d'ordonnancement, d'isochronisme, etc.) quel que soit le choix effectué, il serait forcément fermé, donc trop limitatif pour certaines applications (comment exprimer que je n'ai que faire de perdre des messages pour certains flots multi-média?), et par essence inapproprié pour des systèmes ouverts et évolutifs.
Mais un peu comme on a sorti les structures de données des languages de programmation modernes pour les ranger dans des bibliothèques ouvertes aux modifications et améliorations, il paraît prometteur de définir des modèles spécifiques à des domaines d'applications particuliers, comme par exemple le modèle d'exécution SPMD associé à la distribution de données pour le calcul scientifique intensif, ou le modèle en couches de protocoles pour les systèmes de télécommunications; et de fournir des cadres de conception, de réalisation et de validation adaptés à ces modèles : c'est la notion de «framework».
Un framework fournit un ensemble intégré de fonctionnalités spécifiques à un domaine, implanté par une collection de classes liées entre elles par de multiples schémas (patterns) de collaboration statiques et dynamiques. Il fournit un modèle d'interaction entre les différents objets instances des classes définies (ou seulement spécifiées pour les classes abstraites) dans le framework. Celui-ci présente en général une inversion du contrôle à l'exécution : alors qu'une application utilisant une bibliothèque s'appuie sur celle-ci, dans le cas d'une application utilisant un framework, c'est le framework qui effectue l'essentiel du travail et appelle «de temps en temps» un composant spécifique réalisé par l'implanteur de l'application. Un framework peut donc être vu comme une application semi-complète. Des applications complètes sont développées en héritant et en instantiant des composants paramétrés de frameworks. Il suffit donc en quelque sorte d'enficher dans un framework les composants spécifiques de son application pour obtenir une application complète.
Dans un contexte de programmation par objets, on s'appuie sur le mécanisme de la liaison dynamique pour dissocier la spécification d'une opération (donnée dans une classe du framework) de son implantation dans une sous-classe, qui fait partie du code applicatif fourni par l'utilisateur du framework.