Vous êtes sur la page 1sur 256

Automatisation du Test Logiciel

Sébastien Bardin

CEA-LIST, Laboratoire de Sûreté Logicielle

sebastien.bardin@cea.fr
http://sebastien.bardin.free.fr

S.Bardin Test Logiciel 1/ 198


Objectifs du cours

Compétences à aquérir

connaı̂tre les principes généraux du test logiciel


comprendre l’état de la technique en automatisation du test logiciel
avoir un aperçu des tendances et de l’état de l’art académique

Pour quelles industries ?


bagage général de l’honnête informaticien [Xtrem Programming, etc.]
systèmes critiques
systèmes “de qualité” (sécurité, etc.)

S.Bardin Test Logiciel 2/ 198


Plan

Introduction
Automatisation de la génération de tests
Critères de test avancés

S.Bardin Test Logiciel 3/ 198


Plan

Introduction
◮ Contexte
◮ Définition du test
◮ Point de vue industriel
◮ Aspects pratiques
◮ Discussion
Automatisation de la génération de tests
Critères de test avancés

S.Bardin Test Logiciel 4/ 198


Motivations

Coût des bugs


Coûts économique : 64 milliards $/an rien qu’aux US (2002)
Coûts humains, environnementaux, etc.

Nécessité d’assurer la qualité des logiciels

Domains critiques
atteindre le (très haut) niveau de qualité imposée par les
lois/normes/assurances/... (ex : DO-178B pour aviation)

Autres domaines
atteindre le rapport qualité/prix jugé optimal (c.f. attentes du client)

S.Bardin Test Logiciel 5/ 198


Motivations (2)
Validation et Vérification (V & V)
Vérification : est-ce que le logiciel fonctionne correctement ?
◮ “are we building the product right ?”
Validation : est-ce que le logiciel fait ce que le client veut ?
◮ “are we building the right product ?”

Quelles méthodes ?
revues
simulation/ animation
tests méthode de loin la plus utilisée
méthodes formelles encore très confidentielles, même en syst. critiques

Coût de la V & V
10 milliards $/an en tests rien qu’aux US
plus de 50% du développement d’un logiciel critique
(parfois > 90%)
en moyenne 30% du développement d’un logiciel standard

S.Bardin Test Logiciel 6/ 198


Motivations (3)

La vérification est une part cruciale du développement


Le test est de loin la méthode la plus utilisée
Les méthodes manuelles de test passent très mal à l’échelle en terme de
taille de code / niveau d’exigence
fort besoin d’automatisation

S.Bardin Test Logiciel 7/ 198


Plan

Introduction
◮ Contexte
◮ Définition du test
◮ Aspects pratiques
◮ Discussion
Automatisation de la génération de tests
Critères de test avancés

S.Bardin Test Logiciel 8/ 198


Définition du test

Le test est une méthode dynamique visant à trouver des bugs

Tester, c’est exécuter le programme dans l’intention d’y trouver des anomalies
ou des défauts
- G. J. Myers (The Art of Software Testing, 1979)

S.Bardin Test Logiciel 9/ 198


Process du test

1 choisir un cas de test (CT) [≈ scénario] à exécuter


2 estimer le résultat attendu [oracle]
3 déterminer une donnée de test (DT) exerçant le CT, et son oracle concret
[concrétisation]
4 exécuter le programme sur la DT [script de test]
5 comparer le résultat obtenu à l’oracle [verdict : pass/fail]
6 a-t-on assez de tests ? si oui stop, sinon goto 1
[notion de critère de couverture]

Suite / Jeu de tests : ensemble de cas de tests

S.Bardin Test Logiciel 10/ 198


Process du test

1 choisir un cas de test (CT) [≈ scénario] à exécuter


2 estimer le résultat attendu [oracle]
3 déterminer une donnée de test (DT) exerçant le CT, et son oracle concret
[concrétisation]
4 exécuter le programme sur la DT [script de test]
5 comparer le résultat obtenu à l’oracle [verdict : pass/fail]
6 a-t-on assez de tests ? si oui stop, sinon goto 1
[notion de critère de couverture]

Suite / Jeu de tests : ensemble de cas de tests

S.Bardin Test Logiciel 10/ 198


Process du test

1 choisir un cas de test (CT) [≈ scénario] à exécuter


2 estimer le résultat attendu [oracle]
3 déterminer une donnée de test (DT) exerçant le CT, et son oracle concret
[concrétisation]
4 exécuter le programme sur la DT [script de test]
5 comparer le résultat obtenu à l’oracle [verdict : pass/fail]
6 a-t-on assez de tests ? si oui stop, sinon goto 1
[notion de critère de couverture]

Suite / Jeu de tests : ensemble de cas de tests

S.Bardin Test Logiciel 10/ 198


Process du test

1 choisir un cas de test (CT) [≈ scénario] à exécuter


2 estimer le résultat attendu [oracle]
3 déterminer une donnée de test (DT) exerçant le CT, et son oracle concret
[concrétisation]
4 exécuter le programme sur la DT [script de test]
5 comparer le résultat obtenu à l’oracle [verdict : pass/fail]
6 a-t-on assez de tests ? si oui stop, sinon goto 1
[notion de critère de couverture]

Suite / Jeu de tests : ensemble de cas de tests

S.Bardin Test Logiciel 10/ 198


Process du test

1 choisir un cas de test (CT) [≈ scénario] à exécuter


2 estimer le résultat attendu [oracle]
3 déterminer une donnée de test (DT) exerçant le CT, et son oracle concret
[concrétisation]
4 exécuter le programme sur la DT [script de test]
5 comparer le résultat obtenu à l’oracle [verdict : pass/fail]
6 a-t-on assez de tests ? si oui stop, sinon goto 1
[notion de critère de couverture]

Suite / Jeu de tests : ensemble de cas de tests

S.Bardin Test Logiciel 10/ 198


Process du test

1 choisir un cas de test (CT) [≈ scénario] à exécuter


2 estimer le résultat attendu [oracle]
3 déterminer une donnée de test (DT) exerçant le CT, et son oracle concret
[concrétisation]
4 exécuter le programme sur la DT [script de test]
5 comparer le résultat obtenu à l’oracle [verdict : pass/fail]
6 a-t-on assez de tests ? si oui stop, sinon goto 1
[notion de critère de couverture]

Suite / Jeu de tests : ensemble de cas de tests

S.Bardin Test Logiciel 10/ 198


Process du test

1 choisir un cas de test (CT) [≈ scénario] à exécuter


2 estimer le résultat attendu [oracle]
3 déterminer une donnée de test (DT) exerçant le CT, et son oracle concret
[concrétisation]
4 exécuter le programme sur la DT [script de test]
5 comparer le résultat obtenu à l’oracle [verdict : pass/fail]
6 a-t-on assez de tests ? si oui stop, sinon goto 1
[notion de critère de couverture]

Suite / Jeu de tests : ensemble de cas de tests

S.Bardin Test Logiciel 10/ 198


Exemple

int[] my-sort (int[] vec)


//@ Ensures : tri du tableau d’entrée + élimination de la redondance

Quelques cas de tests (CT) et leurs oracles :

CT1 tableau d’entiers non redondants le tableau trié


CT2 tableau vide le tableau vide
CT3 tableau avec 2 entiers redondants trié sans redondance

Concrétisation : DT et résultat attendu

DT1 vec = [5,3,15] res = [3,5,15]


DT2 vec = [] res = []
DT3 vec = [10,20,30,5,30,0] res = [0,5,10,20,30]

S.Bardin Test Logiciel 11/ 198


Exemple (2)
Script de test
1 void t e s t S u i t e () {
2
3 i n t [ ] td1 = [ 5 , 3 , 1 5 ] ; /∗ p r e p a r e d a t a ∗/
4 int [ ] oracle1 = [3 ,5 ,15] ; /∗ p r e p a r e o r a c l e ∗/
5 i n t [ ] r e s 1 = my−s o r t ( td1 ) ; /∗ r u n CT and ∗/
6 /∗ o b s e r v e r e s u l t ∗/
7 i f ( a r r a y −compare ( r e s 1 , o r a c l e 1 ) ) /∗ a s s e s s v a l i d i t y ∗/
8 t h e n p r i n t ( ‘ ‘ t e s t 1 ok ’ ’ )
9 else { print ( ‘ ‘ test1 erreur ’ ’ )};
10
11
12 i n t [ ] td2 = [ ] ; /∗ p r e p a r e d a t a ∗/
13 int [ ] oracle2 = [ ] ; /∗ p r e p a r e o r a c l e ∗/
14 i n t [ ] r e s 2 = my−s o r t ( td2 ) ;
15 i f ( a r r a y −compare ( r e s 2 , o r a c l e 2 ) ) /∗ a s s e s s v a l i d i t y ∗/
16 t h e n p r i n t ( ‘ ‘ t e s t 2 ok ’ ’ )
17 else { print ( ‘ ‘ test2 erreur ’ ’ )};
18
19
20 ... /∗ same f o r TD3 ∗/
21
22
23 }
S.Bardin Test Logiciel 12/ 198
Difficultés du test

× choisir les cas de test / données de test à exécuter


× estimer le résultat attendu [oracle]
X exécuter le programme sur les données de test
. attention : systèmes embarqués / cyber-physiques
. attention : niveau unitaire, code incomplet ! [stubs, mocks]
X comparer le résultat obtenu à l’oracle
. attention : systèmes embarqués / cyber-physiques
X a-t-on assez de tests ? si oui stop, sinon goto 1
. attention : calcul couverture X, choix du critère de couverture •
X rejouer les tests à chaque changement [test de régression]
. attention : rejeu X, maintenance / optimisation •

pour les X, des solutions standard existent, doivent être appliquées ! !

S.Bardin Test Logiciel 13/ 198


Deux aspects différents du test
1- Construire la qualité du produit

lors de la phase de conception / codage


en partie par les développeurs (tests unitaires)
but = trouver rapidement le plus de bugs possibles (avant la
commercialisation)
◮ test réussi = un test qui trouve un bug

2- Démontrer la qualité du produit à un tiers


une fois le produit terminé
idéalement : par une équipe dédiée
but = convaincre (organismes de certification, hiérarchie, client)
◮ test réussi = un test qui passe sans problème
◮ + tests jugés représentatifs
(systèmes critiques : audit du jeu de tests)

S.Bardin Test Logiciel 14/ 198


Qu’apporte le test ?
Le test ne peut pas prouver au sens formel la validité d’un programme
Testing can only reveal the presence of errors but never their absence.
- E. W. Dijkstra (Notes on Structured Programming, 1972)

Par contre, le test peut “augmenter notre confiance” dans le bon


fonctionnement d’un programme
correspond au niveau de validation des systèmes non informatiques

Un bon jeu de tests doit donc :


exercer un maximum de “comportements différents” du programme
[notion de critères de test]
notamment
◮ tests nominaux : cas de fonctionnement les plus fréquents
◮ tests de robustesse : cas limites / délicats

S.Bardin Test Logiciel 15/ 198


En contre-point à Dijkstra

Beware of bugs in the above code ; I have only proved it correct, not tried it.
- Donald Knuth (1977)

It has been an exciting twenty years, which has seen the research focus evolve
[. . .] from a dream of automatic program verification to a reality of
computer-aided [design] debugging.
- Thomas A. Henzinger (2001)

S.Bardin Test Logiciel 16/ 198


Vocabulaire : éléments de classification

Distinguer selon la phase du cycle de développement


test unitaire, d’intégration, système, acceptation, regression

Distinguer selon la source de sélection des cas de test


boite blanche / boite noire / probabiliste

S.Bardin Test Logiciel 17/ 198


Selon le cycle de développement (1)

S.Bardin Test Logiciel 18/ 198


Selon le cycle de développement (2)
Tests unitaire : tester les différents modules en isolation
définition non stricte de “module unitaire” (procédures, classes, packages,
composants, etc.)
uniquement test de correction fonctionnelle

Tests d’intégration : tester le bon comportement lors de la composition des


modules
uniquement test de correction fonctionnelle

Tests système / de conformité : valider l’adéquation du code aux spécifications


on teste aussi toutes les caractéristiques émergentes
sécurité, performances, etc.

Tests de validation / acceptance : valider l’adéquation aux besoins du client


souvent similaire au test système, mais réaliser / vérifier par le client

Tests de régression : vérifier que les corrections / évolutions du code n’ont pas
introduits de bugs

S.Bardin Test Logiciel 19/ 198


Selon la source de cas de tests

Boı̂te Noire : à partir de spécifications


dossier de conception
interfaces des fonctions / modules
modèles formels ou semi-formels

Boı̂te Blanche : à partir du code


critères basés sur le graphe de flôt de contrôle
critères basés sur le graphe de flôt de données
mutations syntaxiques

Probabiliste : domaines des entrées + arguments statistiques


random : distribution uniforme [ou pas ...]
statistique : distribution mimant le profil opérationnel attendu

S.Bardin Test Logiciel 20/ 198


Selon la source de cas de tests : test “boı̂te noire”

Ne nécesite pas de connaı̂tre la structure interne du système

Basé sur la spécification de l’interface du système et de ses


fonctionnalités : taille raisonnable

Permet d’assurer la conformance spéc - code, mais aveugle aux défauts


fins de programmation

Pas trop de problème d’oracle pour le CT, mais problème de la


concrétisation

Approprié pour le test du système mais également pour le test unitaire

S.Bardin Test Logiciel 21/ 198


Selon la source de cas de tests : test “boı̂te blanche”

La structure interne du système doı̂t être accessible

Se base sur le code : très précis, mais plus “gros” que les spécifications

Conséquences : DT potentiellement plus fines, mais très nombreuses

Pas de problème de concrétisation, mais problème de l’oracle

Sensible aux défauts fins de programmation, mais aveugle aux


fonctionnalités absentes

S.Bardin Test Logiciel 22/ 198


Boite Noire vs Boite Blanche
Les deux familles de tests BB et BN sont complémentaires
Les approches structurelles trouvent plus facilement les
défauts de programmation
Les approches fonctionnelles trouvent plus facilement les
erreurs d’omission et de spécification

Spec : retourne la somme de 2 entiers modulo 20 000

fun (x:int, y:int) : int =


if (x=500 and y=600) then x-y (bug 1)
else x+y (bug 2)

fonctionnel : bug 1 difficile, bug 2 facile


structurel : bug 1 facile, bug 2 difficile

S.Bardin Test Logiciel 23/ 198


Boite Noire vs Boite Blanche
Les deux familles de tests BB et BN sont complémentaires
Les approches structurelles trouvent plus facilement les
défauts de programmation
Les approches fonctionnelles trouvent plus facilement les
erreurs d’omission et de spécification

Spec : retourne la somme de 2 entiers modulo 20 000

fun (x:int, y:int) : int =

fonctionnel : bug 1 difficile, bug 2 facile


structurel : bug 1 facile, bug 2 difficile

S.Bardin Test Logiciel 23/ 198


Boite Noire vs Boite Blanche
Les deux familles de tests BB et BN sont complémentaires
Les approches structurelles trouvent plus facilement les
défauts de programmation
Les approches fonctionnelles trouvent plus facilement les
erreurs d’omission et de spécification

fun (x:int, y:int) : int =


if (x=500 and y=600) then x-y (bug 1)
else x+y (bug 2)

fonctionnel : bug 1 difficile, bug 2 facile


structurel : bug 1 facile, bug 2 difficile

S.Bardin Test Logiciel 23/ 198


Boite Noire vs Boite Blanche
Les deux familles de tests BB et BN sont complémentaires
Les approches structurelles trouvent plus facilement les
défauts de programmation
Les approches fonctionnelles trouvent plus facilement les
erreurs d’omission et de spécification

Spec : retourne la somme de 2 entiers modulo 20 000

fun (x:int, y:int) : int =


if (x=500 and y=600) then x-y (bug 1)
else x+y (bug 2)

fonctionnel : bug 1 difficile, bug 2 facile


structurel : bug 1 facile, bug 2 difficile

S.Bardin Test Logiciel 23/ 198


Boite Noire vs Boite Blanche (2)

Exemples de complémentarités des critères

Bug du Pentium : cause = erreurs sur quelques valeurs (parmis des millions)
dans une table de calculs
impossible à trouver en test boite noire
aurait pu être trouvé facilement en test unitaire du module concerné

Bug du “Mars Climate Orbiter” : cause = problème de métriques (mètres vs


pouces)
chaque module fonctionne correctement, test unitaire inutile
aurait pu être détecté en phase de tests d’intégration, en se basant sur
l’interface de communication (boite noire)

S.Bardin Test Logiciel 24/ 198


Selon la source de cas de tests : test aléatoire / statistique
Les données sont choisies dans leur domaine selon une loi statistique
loi uniforme (test aléatoire )
loi statistique du profil opérationnel (test statistique)

Pros/Cons du test aléatoire


sélection aisée des DT en général
test massif si oracle (partiel) automatisé
“objectivité” des DT (pas de biais)
PB : peine à produire des comportements très particuliers (ex : x=y sur
32 bits)

Pros/Cons du test statistique


permet de déduire une garantie statistique sur le programme
trouve les défauts les plus probables : défauts mineurs ?
PB : difficile d’avoir la loi statistique

S.Bardin Test Logiciel 25/ 198


Plan

Introduction
◮ Contexte
◮ Définition du test
◮ Point de vue industriel
◮ Aspects pratiques
◮ Discussion
Automatisation de la génération de tests
Critères de test avancés

S.Bardin Test Logiciel 26/ 198


Test et cycle de développement
Systèmes critiques
domaines : aéronautique [DO-178B], énergie, ferroviaire, etc.
démontrer la qualité : tests exigés pour la certification
niveau unitaire : critères structurels (boite blanche) +/- exigeants [jusque
MCDC]
couverture calculée sur le code, mais tests justifiés vis à vis des
spécifications
Méthodes agiles et test-driven development
domaines : très large !
construire la qualité : au niveau unitaire, test-first & automatisation des
tests de régression (xUnit)
démontrer la qualité : scénarios de test définis par le client

Sécurité
domaines : OS, navigateurs, hyperviseurs / VM / sandbox, briques de
chiffrement, etc.
utilisation massive du fuzzing en test de vulnérabilité
test boite noire, essentiellement random, oracle pass/crash

S.Bardin Test Logiciel 27/ 198


Quelques success stories de l’automatisation du test (1)

Utilisation courante
xUnit et assimilés [régression]
outils autour de xUnit [calcul de couverture, stubs]
analyse dynamique (ex : Valgrind) [oracle automatisé un peu meilleur que
pass/crash]

S.Bardin Test Logiciel 28/ 198


Quelques success stories de l’automatisation du test (2)

Outils avancés de génération de test [génération de tests]

fuzzing (ex : Radamsa de Google)


(oracle = pass / crash)
Pex : but = aide au développeur (Parametrized unit testing), bientôt
intégré dans Visual Studio
(oracle = assert, CodeContract)
SAGE : but = trouver des bugs de sécurité, vise les lecteurs media, en
production en interne
(oracle = pass / crash)
CSmith : but = test de compilateurs
(oracle = back2back testing)

S.Bardin Test Logiciel 29/ 198


Quelques success stories de l’automatisation du test (3)

Plus confidentiel
outils pour calcul de couverture avancée (ex : Mu-Java, Frama-C/LTest)
[calcul de couverture]
spécifications exécutables (jml, CodeContract, e-ACSL)
[oracle automatisé + puissant]
détection d’objectifs de test infaisables (FramaC/LTest)
[aide génération, meilleur calcul de couverture]

S.Bardin Test Logiciel 30/ 198


Plan

Introduction
◮ Contexte
◮ Définition du test
◮ Point de vue industriel
◮ Aspects pratiques
◮ Discussion
Automatisation de la génération de tests
Critères de test avancés

S.Bardin Test Logiciel 31/ 198


Difficultés du test

× choisir les cas de test / données de test à exécuter


× estimer le résultat attendu [oracle]
X exécuter le programme sur les données de test
. attention : systèmes embarqués / cyber-physiques
. attention : niveau unitaire, code incomplet ! [stubs, mocks]
X comparer le résultat obtenu à l’oracle
. attention : systèmes embarqués / cyber-physiques
X a-t-on assez de tests ? si oui stop, sinon goto 1
. attention : calcul couverture X, choix du critère de couverture •
X rejouer les tests à chaque changement [test de régression]
. attention : rejeu X, maintenance / optimisation •

pour les X, des solutions standard existent, doivent être appliquées ! !

S.Bardin Test Logiciel 32/ 198


Quelques repères

oracle
script
tests de régression et JUnit
critères de couverture

S.Bardin Test Logiciel 33/ 198


Oracle et verdict

La définition de l’oracle est un problème très difficile

limite fortement certaines méthodes de test (ex : test random)


impose un trade-off avec la sélection de tests
point le plus mal maitrisé pour l’automatisation

S.Bardin Test Logiciel 34/ 198


Oracle et verdict (2)

Quelques cas pratiques d’oracles parfaits automatisables [à reconnaı̂tre !]


résultat simple à vérifier (ex : solution d’une équation)
comparer à une référence : logiciel existant, tables de résultats
disponibilité d’un logiciel similaire : test dos à dos

Des oracles partiels mais automatisés peuvent être utiles


oracle basique : pass/crash
instrumentation du code (assert)
plus évolué 1 : instrumentation dynamique (Valgrind)
plus évolué 2 : programmation avec contrats (Eiffel, Jml, etc.)

S.Bardin Test Logiciel 35/ 198


Quelques répères

oracle
script
tests de régression et JUnit
critères de couverture

S.Bardin Test Logiciel 36/ 198


Exécution du test : pas toujours simple

. Code manquant (test incrémental)


. Présence d’un environnement (réseau, Base de Données, etc.)
. Exécution d’un test très coûteuse en temps
. Hardware réel non disponible, ou peu disponible
. Réinitialisation possible du système
si non, l’ordre des tests est très important

. Moyens d’observation et d’action sur le système


sources dispo, compilables et instrumentables : cas facile, script = code
si non : difficile, “script de test” = succession d’opérations (manuelles ?)
sur l’interface disponible (informatique ? électronique ? mécanique ?)

S.Bardin Test Logiciel 37/ 198


Quelques répères

oracle
script
tests de régression et JUnit
critères de couverture

S.Bardin Test Logiciel 38/ 198


Tests de (non) régression
Tests de régression : à chaque fois que le logiciel est modifié, s’assurer que “ce
qui fonctionnait avant fonctionne toujours”

Pourquoi modifier le code déjà testé ?


correction de défaut
ajout de fonctionnalités

Quand ?
en phase de maintenance / évolution
ou durant le développement

Quels types de tests ?


tous : unitaires, intégration, système, etc.

Objectif : avoir une méthode automatique pour


rejouer automatiquement les tests [perfs !]
détecter les tests dont les scripts ne sont plus corrects

S.Bardin Test Logiciel 39/ 198


Solution “à la JUnit”
Junit pour Java : idée principale = tests écrits en Java
simplifie l’exécution et le rejeu des tests (juste tout relancer)
simplifie la détection d’une partie des tests non à jour : tests recompilés
en même temps que le programme
simplifie le stockage et la réutilisation des tests ( tests de MyClass dans
MyClassTest)

JUnit offre :
des primitives pour créer un test (assertions)
des primitives pour gérer des suites de tests
des facilités pour l’exécution des tests
statistiques sur l’exécution des tests
interface graphique pour la couverture des tests
points d’extensions pour des situations spécifiques

Solution très simple et extrêmement efficace

S.Bardin Test Logiciel 40/ 198


Limites

. création manuelle des tests


. pas de détection des scripts de test devenus sémantiquement incorrects
. rejeu total, pas de minimisation des tests à rejouer

S.Bardin Test Logiciel 41/ 198


Principe du test avec JUnit

1. Pour chaque fichier Foo.java créer un fichier FooTest.java (dans le


même repertoire) qui inclut (au moins) le paquetage junit.framework.*
2. Dans FooTest.java, pour chaque classe Foo de Foo.java écrire une
classe FooTest qui hérite de TestCase
3. Dans FooTest définir les méthodes suivantes :
◮ le constructeur qui initialise le nom de la suite de tests
◮ setUp appelée avant chaque test
◮ tearDown appelée après chaque test
◮ une ou plusieurs méthodes dont le nom est prefixé par test
qui implementent les tests unitaires
◮ suite qui appelle les tests unitaires
◮ main qui appelle l’exécution de la suite

S.Bardin Test Logiciel 42/ 198


Écrire les tests unitaires avec Junit

Dans les méthodes de test unitaire, les méthodes testées sont appelées et leur
résultat est testé à l’aide d’assertions :
assertEquals(a,b)
teste si a est éqal à b (a et b sont soit des valeurs primitives, soit des
objets possédant une méthode equals)
assertTrue(a) et assertFalse(a)
testent si a est vrai resp. faux, avec a une expression booléenne
assertSame(a,b) et assertNotSame(a,b)
testent si a et b référent au même objet ou non.
assertNull(a) et assertNotNull(a)
testent si a est null ou non, avec a un objet
fail(message)
si le test doit echouer (levée d’exception)

S.Bardin Test Logiciel 43/ 198


Exemple : Conversion binaire/entier
// File Binaire.java
public class Binaire {
private String tab;
public Binaire() {tab = new String(); }
public Binaire(String b, boolean be) {
tab = new String(b); if (be) revert(); }

private void revert() {


byte[] btab = tab.getBytes();
for (int i = 0; i < (btab.length >> 1); i++) {
byte tmp = btab[i]; btab[i] = btab[btab.length - i];
btab[btab.length - i] = tmp; }
tab = new String(btab); }

public int getInt() {


int nombre = 0;
/* little endian */
for (int i = tab.length()-1; i >= 0; i--) {
nombre = (nombre << 1) + (tab.charAt(i) - ’0’); }
return nombre; }
}
S.Bardin Test Logiciel 44/ 198
Exemple : Test Conversion binaire/entier (2)
// Fichier BinaireTest.java
import junit.framework.*;

public class BinaireTest extends TestCase {


private Binaire bin; // variable pour les tests

public BinaireTest(String name) {super(name); }

protected void setUp() throws Exception {


bin = new Binaire(); }

protected void tearDown() throws Exception {


bin = null; }

public void testBinaire0() {


assertEquals(bin.getInt(),0); }

public void testBinaire1() {


bin = new Binaire("01",false);
assertEquals(bin.getInt(),2); }

S.Bardin Test Logiciel 45/ 198


Quelques règles de bonne conduite avec JUnit

Ecrire les test en même temps que le code.


Exécuter ses tests aussi souvent que possible, idéalement après chaque
changement de code.
Ecrire un test pour tout bogue signalé (même s’il est corrigé).
Ne pas mettre plusieurs assert dans un même test : JUnit s’arrete à la
première erreur.
Attention, les méthodes privées ne peuvent pas être testées !

S.Bardin Test Logiciel 46/ 198


Quelques répères

oracle
script
tests de régression et JUnit
critères de couverture

S.Bardin Test Logiciel 47/ 198


Sélection des tests

Problèmes de la sélection de tests :


efficacité du test dépend crucialement de la qualité des CT/DT
ne pas “râter” un comportement fautif
MAIS les CT/DT sont coûteux (design, exécution, stockage, etc.)

Deux enjeux :
DT suffisamment variées pour espérer trouver des erreurs
maı̂triser la taille : éviter les DT redondantes ou non pertinentes

S.Bardin Test Logiciel 48/ 198


Critères de test

Sujet central du test


Tente de répondre à la question : “qu’est-ce qu’un bon jeu de test ?”
Plusieurs utilisations des critères :
guide pour choisir les CT/DT les plus pertinents
évaluer la qualité d’un jeu de test
donner un critère objectif pour arrêter la phase de test

Quelques qualités atttendues d’un critère de test :


bonne corrélation au pouvoir de détection des fautes
concis
automatisable

S.Bardin Test Logiciel 49/ 198


Quelques répères

oracle
script
tests de régression et JUnit
critères de couverture
◮ boite noire
◮ boite blanche

S.Bardin Test Logiciel 50/ 198


Critères boite noire

. Couverture des partitions des domaines d’entrée


. Test aux limites
. Approche pair-wise pour limiter la combinatoire

Remarque : si on dispose d’un modèle formel, on peut appliquer les critères de


couverture boite blanche au niveau du modèle

S.Bardin Test Logiciel 51/ 198


Partition des entrées : principe

Principe :
diviser le domaine des entrées en un nombre fini de classes tel que le
programme réagisse pareil (en principe) pour toutes valeurs d’une classe
conséquence : il ne faut tester qu’une valeur par classe !
⇒ permet de se ramener à un petit nomlbre de CTs

Exemple : valeur absolue : abs : int 7→ int


232 entrées
MAIS seulement 3 classes naturelles : < 0, = 0, > 0
on teste avec un DT par classe, exemple : -5, 0, 19

S.Bardin Test Logiciel 52/ 198


Partition des entrées : principe (2)

Procédure :
1. Identifier les classes d’équivalence des entrées
◮ Sur la base des conditions sur les entrées/sorties
◮ En prenant des classes d’entrées valides et invalides
2. Définir des CT couvrant chaque classe

S.Bardin Test Logiciel 53/ 198


Comment faire les partitions ?
Définir un certain nombre de caractéristiques Ci représentatives des entrées du
programme

Pour chaque caractéristique Ci , définir des blocs bi ,j ⊆ Ci


(couverture) ∪j bi ,j = Ci
(séparation) idéalement bi ,j ′ ∩ bi ,j = ∅

Pourquoi plusieurs caractéristiques ?


plusieurs variables : foo(int a, bool b) :
C1 = {< 0, = 0, > 0} et C2 = {⊤, ⊥}
caractéristiques orthogonales : foo(list<int> l) :
C1 = {sorted(l), ¬sorted(l)} et C2 = {size(l) > 10, size(l) ≤ 10, }

Les partitions obtenues sont le produit cartésien des bi ,j


attention à l’explosion !
on verra une méthode moins coûteuse plus tard

S.Bardin Test Logiciel 54/ 198


Deux grands types de partition

interface-based
basée uniquement sur les types des données d’entrée
facile à automatiser ! (cf. exos)

functionality-based
prend en compte les relations entre variables d’entrées
plus pertinent
peu automatisable

Exemple : searchList : list<int> × int 7→ bool


interface-based : {empty (l), ¬empty (l)} × {< 0, = 0, > 0}
functionality-based : {empty (l), e ∈ l, ¬empty (l) ∧ e 6∈ l}

S.Bardin Test Logiciel 55/ 198


À propos des entrées invalides du programme

Conseil 1 : attention à en faire !


Conseil 2 : attention à ne pas trop en faire ! !

Pour une fonction de calcul de valeur absolue :


si le programme a une interface textuelle : légitime de tester les cas où
l’entrée n’est pas un entier, il n’y a pas d’entrée, il y a plusieurs entiers,
etc.
si on a à faire à un module de calcul avec une interface “propre” (un
unique argument entier) : on ne teste pas les valeurs invalides sur le
moteur de calcul (la phase de compilation nous assure de la correction),
mais sur le front-end (GUI, texte)

Conseil 3 : Ne pas mélanger les valeurs invalides !

S.Bardin Test Logiciel 56/ 198


Analyse des valeurs limites

Le test des valeurs limites est une tactique pour améliorer l’efficacité des DT
produites par d’autres familles.

s’intègre très naturellement au test partitionnel

Idée : les erreurs se nichent dans les cas limites, donc tester aussi les valeurs
aux limites des domaines ou des classes d’équivalence.
test partitionnel en plus agressif
plus de blocs, donc plus de DT donc plus cher

Stratégie de test :
Tester les bornes des classes d’équivalence, et juste à côté des bornes
Tester les bornes des entrées et des sorties

S.Bardin Test Logiciel 57/ 198


Analyse des valeurs limites (2)

Exemples :
soit N le plus petit/grand entier admissible : tester N − 1, N, N + 1
ensemble vide, ensemble à un élément
fichier vide, fichier de taille maximale, fichier juste trop gros
string avec une requête sql intégrée
...

S.Bardin Test Logiciel 58/ 198


Exemple : Valeur absolue, bis

Exemple : valeur absolue : abs : int 7→ int


232 entrées
seulement 3 classes naturelles : < 0, = 0, > 0
on ajoute la limite suivante : −231 [question : pourquoi ?]

S.Bardin Test Logiciel 59/ 198


Caractéristiques multiples

Si on a plusieurs entrées :
dans le cas où il y a trop de bi ,j , le nombre de partitions Πbi ,j explose et la
technique devient inutilisable

Comment faire : l’approche combinatoire peut être appliquée aux bi ,j


on ne cherche plus à couvrir tout Πbi ,j
mais par exemple toutes les paires (bi ,j , bi ′ ,j ′ )
on retrouve l’approche pair-wise [cf juste après]

S.Bardin Test Logiciel 60/ 198


Test des domaines d’entrées

outType function-under-test(inType x, inType y);

Constat : test exhaustif souvent impratiquable


espace des entrées non borné / paramétré / infini (BD,
pointeurs, espace physique, etc.)
simplement deux entiers 32 bits : 264 possibilités

S.Bardin Test Logiciel 61/ 198


Test Combinatoire

Test combinatoire = test exhaustif sur une sous-partie (bien


identifiée) des combinaisons possibles des valeurs d’entrée

Approche pairwise : sélectionner les DT pour couvrir toutes les


paires de valeurs
observation 1 : # paires bcp plus petit que # combinaisons
observation 2 : un seul test couvre plusieurs paires
# DT diminue fortement par rapport à test exhaustif

Remarque : on peut étendre à t-uplet, t fixé


plus de tests, meilleur qualité
ne semble guère intéressant en pratique

S.Bardin Test Logiciel 62/ 198


Test Combinatoire (2)

Hypothèse sous-jacente :
majorité des fautes détectées par des combinaisons de 2 valeurs de
variables
◮ semble ok en pratique

Utile quand : beaucoup d’entrées, chacune ayant un domaine restreint


typiquement : GUI (menus déroulants), interface “ligne de commande”
avec de nombreux paramètres, tests de configuration (cf exos)
très utile aussi en addition au test partitionnel (cf. ci-après)

S.Bardin Test Logiciel 63/ 198


Test Combinatoire (3)
Exemple : 3 variables booléennes A, B ,C

Nombre de combinaisons de valeurs / tests : 23 = 8


Nombre de paires de valeurs (= nb paires de variables × 4) : 12
(A=1,B=1), (A=1,B=0), (A=1,C=1), (A=1,C=0)
(A=0,B=1), (A=0,B=0), (A=0,C=1), (A=0,C=0)
(B=1,C=1), (B=1,C=0)
(B=0,C=1), (B=0,C=0)

IMPORTANT : le DT (A=1,B=1,C=1) couvre 3 paires, mais


1 seule combinaison
(A=1,B=1), (A=1,C=1), (B=1,C=1)

Ici 6 tests pour tout couvrir :


(0,0,1), (0,1,0), (1,0,1), (1,1,0) couvrent presque tout,
sauf (*,0,0) et (*,1,1)
on ajoute (1,0,0) et (1,1,1)

S.Bardin Test Logiciel 64/ 198


Test Combinatoire (4)

Sur de plus gros exemples avec N variables à M valeurs :


nb combinaisons : M N
nb paires de valeurs : ≈ M 2 × N(N − 1)/2
un test couvre au plus N(N − 1)/2 paires de valeurs

On peut espérer tout couvrir en M 2 tests plutôt que M N


indépendant de N
plus sensible à la taille des domaines qu’au nombre de
variables

Attention : trouver un ensemble de tests de cardinal minimal pour


couvrir t-wise est NP-complet
se contenter de méthodes approchées

S.Bardin Test Logiciel 65/ 198


Test Combinatoire (5)
Pour aller plus loin 1 : algorithmes usuels [Aditya Mathur, chap. 4]
covering arrays
pour M = 2 : procédure dédiée efficace (polynomiale)

Pour aller plus loin 2 : les DT générées par l’algorithme précédent


ne sont pas équilibrées : certaines valeurs sont exercées bien plus
que d’autres
algorithmes à base de carrés latins orthogonaux pour assurer
aussi l’équilibrage

Pour aller plus loin 3 : on peut vouloir intégrer certaines


contraintes sur les entrées, typiquement exprimer que certaines
paires de valeurs sont impossibles
S.Bardin Test Logiciel 66/ 198
Quelques répères

oracle
script
tests de régression et JUnit
critères de couverture
◮ boite noire
◮ boite blanche

S.Bardin Test Logiciel 67/ 198


Critères boite blanche (1)

Le graphe de flot de contrôle d’un programme est défini par :


un noeud pour chaque instruction, plus un noeud final de sortie
pour chaque instruction du programme, le CFG comporte un arc reliant le
noeud de l’instruction au noeud de l’instruction suivante (ou au noeud
final si pas de suivant), l’arc pouvant être étiquetté par l’instruction en
question

Quelques définitions sur les instructions conditionnelles :


if (a<3 && b<4) then ... else ...

un if est une instruction conditionnelle / branchante


(a<3 && b<4) est la condition
les deux décisions possibles sont (condition, true) et (condition, false)
(chaque transition)
les conditions simples sont a<3 et b<4

S.Bardin Test Logiciel 68/ 198


Critères boite blanche (2)

1 START
2 input ( i )
3 sum := 0
4 l o o p : i f ( i > 5 ) goto end
5 input ( j )
6 i f ( j < 0 ) goto end
7 sum := sum + j
8 i f ( sum > 1 0 0 ) goto end
9 i := i +1
10 goto l o o p
11 end : HALT

S.Bardin Test Logiciel 69/ 198


Critères boite blanche (3)

Quelques critères de couverture sur flot de contrôle

Tous les noeuds (I) : le plus faible.


Tous les arcs / décisions (D) : test de chaque décision
Toutes les conditions (C) : peut ne pas couvrir toutes les décisions
Toutes les conditions/décisions (DC)
Toutes les combinaisons de conditions (MC) : explosion combinatoire !
Tous les chemins : le plus fort, impossible à réaliser s’il y a des boucles

Remarque : il existe d’autres critères boite blanche


basés sur la couverture du flot de données
basés sur les mutations syntaxiques du code

S.Bardin Test Logiciel 70/ 198


Critères boite blanche (4) - MCDC

Utilisé en avionique (DO-178B). But :


puissance entre DC et MC
ET garde un nombre raisonnable de tests

Définition
critère DC
ET les tests doivent montrer que chaque condition atomique peut
influencer la décision :
par exemple, pour une condition C = a ∧ b, les deux DT a = 1, b = 1 et
a = 1, b = 0 prouvent que b seul peut influencer la décision globale C

S.Bardin Test Logiciel 71/ 198


Critères boite blanche (5) - Hiérarchie des critères

Notion de hiérarchie entre ces différents critères de couverture


Le critère CT1 est plus fort que le critère CT2 (CT1 subsumes CT2, noté CT1
 CT2) si pour tout programme P et toute suite de tests TS pour P, si TS
couvre CT1 (sur P) alors TS couvre CT2 (sur P).

Exercice : supposons que TS2 couvre CT2 et trouve un bug sur P, et TS1
couvre un critère CT1 tq CT1  CT2.
Question : TS1 trouve-t-il forcément le même bug que TS2 ?

S.Bardin Test Logiciel 72/ 198


Critères boite blanche (6) - Hiérarchie des critères

S.Bardin Test Logiciel 73/ 198


Limite des critères de tests existants

Ne sont pas reliés à la qualité finale du logiciel (MTBF, PDF, ...)


sauf test statistique

Ne sont pas non plus vraiment reliés au # bugs /kloc


exception : mcdc et contrôle-commande
exception : mutations

Mais toujours mieux que rien ...

S.Bardin Test Logiciel 74/ 198


Récapitulatif des problèmes

Sélection des CT/DT pertinents : très difficile


expériences industrielles de synthèse automatique

Script de test : de facile à difficile, mais toujours très ad hoc


Verdict et oracle : très difficile
certains cas particuliers s’y prêtent bien
des oracles partiels automatisés peuvent être utiles

Régression : bien automatisé (ex : JUnit pour Java)

S.Bardin Test Logiciel 75/ 198


Bibliographie

Livres

Introduction to software testing [Ammann-Offutt 08]

Foundations of Software Testing [Mathur 08]

Art of Software Testing (2nd édition) [Myers 04]

Software Engineering [Sommerville 01]

S.Bardin Test Logiciel 76/ 198


Plan

Introduction
◮ Contexte
◮ Définition du test
◮ Aspects pratiques
◮ Complément test de régression
◮ Discussion
Automatisation de la génération de tests
Critères de test avancés

S.Bardin Test Logiciel 77/ 198


Tests de régression : problème SMP

Compromis entre tout rejouer (sûr mais trop cher) et ne pas rejouer assez.
certains tests ne passent pas par les modifications : les ignorer

Problème additionnel : temps total pour le rejeu limité


on arrête après N tests
avec cette limite, le rejeu total est risqué
faire tests pertinents d’abord

Trois phases distinctes dans la sélection :


Sélectionner les tests pertinents (aucune perte)
Minimiser les tests pertinents (perte possible)
Prioritiser les tests restants (aucune perte)

S.Bardin Test Logiciel 78/ 198


Tests de régression : problème SMP (2)

S.Bardin Test Logiciel 79/ 198


Plan

Introduction
◮ Contexte
◮ Définition du test
◮ Point de vue industriel
◮ Aspects pratiques
◮ Discussion
Automatisation de la génération de tests
Critères de test avancés

S.Bardin Test Logiciel 80/ 198


Automatisation du test

Test = activité difficile et coûteuses

Difficile
trouver les défauts = pas naturel (surtout pour le programmeur)
qualité du test dépend de la pertinence des cas de tests

Coûteux : entre 30 % et 50 % du développement

Besoin de l’automatiser/assister au maximum

Gains attendus d’une meilleur architecture de tests


amélioration de la qualité du logiciel
et/ou réduction des coûts (développement - maintenance) et du
time-to-market

S.Bardin Test Logiciel 81/ 198


Dilemmes du test

Si la campagne de tests trouve peu d’erreurs


choix 1 (optimiste) : le programme est très bon
choix 2 (pessimiste) : les tests sont mauvais

Si la campagne de tests trouve beaucoup d’erreurs


choix 1 (optimiste) : la majeure partie des erreurs est découverte
choix 2 (pessimiste) : le programme est de très mauvaise qualité, encore
plus de bugs sont à trouver

S.Bardin Test Logiciel 82/ 198


Adoption industrielle

Aller doucement, du plus simple au plus compliqué

1. test-driven development : test-first, xUnit, intégration continue, objectifs


de couverture
2. automatisation simple de la génération de tests (random, fuzzing, etc.)
3. langage d’annotation
4. automatisation plus poussée
◮ smart fuzzing, parametrized unit testing
◮ model-based testing

S.Bardin Test Logiciel 83/ 198


Philosophie du test

Testing can only reveal the presence of errors but never their absence
- E. W. Dijkstra (Notes on Structured Programming, 1972)

Oui, mais ...


Correspond au niveau de fiabilité exigé du reste du système
Correspond aux besoins réels de beaucoup d’industriels
Peut attaquer des programmes + complexes

S.Bardin Test Logiciel 84/ 198


Philosophie du test

Testing can only reveal the presence of errors but never their absence
- E. W. Dijkstra (Notes on Structured Programming, 1972)

Oui, mais ...


Correspond au niveau de fiabilité exigé du reste du système
Correspond aux besoins réels de beaucoup d’industriels
Peut attaquer des programmes + complexes

S.Bardin Test Logiciel 84/ 198


Philosophie du test

Testing can only reveal the presence of errors but never their absence
- E. W. Dijkstra (Notes on Structured Programming, 1972)

Oui, mais ...


Correspond au niveau de fiabilité exigé du reste du système
Correspond aux besoins réels de beaucoup d’industriels
Peut attaquer des programmes + complexes

S.Bardin Test Logiciel 84/ 198


Philosophie du test

Testing can only reveal the presence of errors but never their absence
- E. W. Dijkstra (Notes on Structured Programming, 1972)

Oui, mais ...


Correspond au niveau de fiabilité exigé du reste du système
Correspond aux besoins réels de beaucoup d’industriels
Peut attaquer des programmes + complexes

déjà utilisé : ne modifie ni les process ni les équipes


retour sur investissement proportionnel à l’effort
simple : pas d’annotations complexes, de faux négatifs, ...
robuste aux bugs de l’analyseur / hypotheses d’utilisation
trouve des erreurs non spécifiées

S.Bardin Test Logiciel 84/ 198


Philosophie du test

Testing can only reveal the presence of errors but never their absence
- E. W. Dijkstra (Notes on Structured Programming, 1972)

Oui, mais ...


Correspond au niveau de fiabilité exigé du reste du système
Correspond aux besoins réels de beaucoup d’industriels
Peut attaquer des programmes + complexes

S.Bardin Test Logiciel 84/ 198


Philosophie du test

Testing can only reveal the presence of errors but never their absence
- E. W. Dijkstra (Notes on Structured Programming, 1972)

Oui, mais ...


Correspond au niveau de fiabilité exigé du reste du système
Correspond aux besoins réels de beaucoup d’industriels
Peut attaquer des programmes + complexes

Offre des solutions (partielles) pour


librairies sous forme binaire (COTS)
codes mélangeant différents langages (assembleur, SQL, ...)
code incomplet

S.Bardin Test Logiciel 84/ 198


Citations

Beware of bugs in the above code ; I have only proved it correct, not tried it.
- Donald Knuth (1977)

It has been an exciting twenty years, which has seen the research focus evolve
[. . .] from a dream of automatic program verification to a reality of
computer-aided [design] debugging.
- Thomas A. Henzinger (2001)

S.Bardin Test Logiciel 85/ 198


Test et vérification (avis personnel)
Opposition historique forte

Complémentaire au niveau fonctionnel


propriété prouvée = pas besoin de tests
propriété non prouvée = peut être fausse ? (test !)
certaines classes de propriétés sont pour le moment non modélisables
[perfs, ergonomie, etc.]
facilité de mise en oeuvre : AS unsound ≤ test ≤ AS sound

De plus en plus similaire en terme de technologie


preuve d’invariance vs preuve d’accessibilité
mêmes outils : logique, sémantique, analyse de programme, etc.
mais approximations différentes (sur- vs sous-), importance de la synthèse
de plus en plus de techniques “hybrides” (bounded model checking,
context-bounded analysis, etc.)
remarque : langage de spécification utile pour vérification et test
[next big step in industry ?]

S.Bardin Test Logiciel 86/ 198


Plan

Introduction
Automatisation de la génération de tests
Critères de test avancés

S.Bardin Test Logiciel 87/ 198


Génération de tests : Approches structurelles

On se concentre dans cette partie sur la génération de données de test à partir


du code

L’oracle est vu comme un problème orthogonal


On suppose qu’on dispose d’un oracle automatisé
oracle exact dans certains cas (test dos à dos)
oracle partiel sinon : assertions, contrats (JML, Spec#)

S.Bardin Test Logiciel 88/ 198


Génération de tests structurels par contraintes
Principe : transformer tout ou partie du programme en une formule logique ϕ
telle que solution de ϕ = DT cherchée

Approche globale : tout le programme est transformé en une formule logique


théories complexes : quantificateurs ou points fixes pour les boucles
comment transformer le programme (boucles) ?

Approche locale / orientée chemin : un seul chemin est considéré à la fois


théories plus simples : sans quantificateur, juste conjonction
mais énumération de chemins
nous verrons deux techniques
◮ exécution symbolique
◮ exécution symbolique dynamique (ou exécution concolique)

exemples d’outils : SAGE & Pex (Microsoft), PathCrawler (CEA), Klee


(Imp. College), etc.

S.Bardin Test Logiciel 89/ 198


Plan

Introduction
Automatisation de la génération de tests
◮ Prédicat de chemins
◮ Exécution symbolique
◮ Exécution concolique
◮ En pratique
◮ Discussion
◮ Complément : Aspects logiques
◮ Complément : Optimisations
Critères de test avancés

S.Bardin Test Logiciel 90/ 198


Rappel : Control-Flow Graph (CFG)
Le graphe de flot de contrôle d’un programme est défini par :
un noeud pour chaque instruction, plus un noeud final de sortie
pour chaque instruction du programme, le CFG comporte un arc reliant le
noeud de l’instruction au noeud de l’instruction suivante (ou au noeud
final si pas de suivant)

S.Bardin Test Logiciel 91/ 198


Notion centrale : le prédicat de chemin

• π un chemin (fini) du programme P

c-à-d π ∈ L(P), si P vu comme automate

• D l’espace des entrées du programme (arguments, variables volatiles, etc.)


ex : D = N × N pour void foo(int a, int b)

• V ∈ D une entrée du programme

• On note P(V ) la trace d’exécution de P lancé sur la donnée d’entrée V

S.Bardin Test Logiciel 92/ 198


Notion centrale : le prédicat de chemin (2)

• On se donne une théorie logique T


théorie : ensemble d’opérateurs / prédicats permis, et leurs axiomes
ex : arithmétique linéaire

• On note par  la relation de préfixe entre les chemins


(≈ préfixes de mots, ab  abc )

Prédicat de chemin
Un prédicat de chemin pour π (dans T ) est une formule logique ϕπ ∈ T
interprétée sur D telle que si V |= ϕπ alors l’exécution du programme sur V
suit le chemin π, c’est à dire que π  P(V ).

S.Bardin Test Logiciel 93/ 198


Construction du prédicat de chemin (1)

Un prédicat de chemin pour π peut se calculer en exécutant symboliquement le


chemin
exécution concrète : màj des valeurs des variables
exécution symbolique : màj des relations logiques entre variables (calcul
avec post)

Mémoire concrète / symbolique


concret : (variable, point de contrôle) → valeur concrète
symbolique : (variable, point de contrôle) → formule logique

S.Bardin Test Logiciel 94/ 198


Construction du prédicat de chemin (2)

Loc Instruction
0 input(y,z)
1 w := y+1
2 x := w + 3
3 if (x < 2 * z) (branche True)
4 if (x < z) (branche False)

Prédicat de chemin (entrées Y0 et Z0 )


S.Bardin Test Logiciel 95/ 198


Construction du prédicat de chemin (2)

Loc Instruction
0 input(y,z)
1 w := y+1
2 x := w + 3
3 if (x < 2 * z) (branche True)
4 if (x < z) (branche False)

Prédicat de chemin (entrées Y0 et Z0 )


⊤ ∧ W1 = Y0 + 1

S.Bardin Test Logiciel 95/ 198


Construction du prédicat de chemin (2)

Loc Instruction
0 input(y,z)
1 w := y+1
2 x := w + 3
3 if (x < 2 * z) (branche True)
4 if (x < z) (branche False)

Prédicat de chemin (entrées Y0 et Z0 )


⊤ ∧ W1 = Y0 + 1 ∧ X2 = W1 + 3

S.Bardin Test Logiciel 95/ 198


Construction du prédicat de chemin (2)

Loc Instruction
0 input(y,z)
1 w := y+1
2 x := w + 3
3 if (x < 2 * z) (branche True)
4 if (x < z) (branche False)

Prédicat de chemin (entrées Y0 et Z0 )


⊤ ∧ W1 = Y0 + 1 ∧ X2 = W1 + 3 ∧ X2 < 2 × Z0

S.Bardin Test Logiciel 95/ 198


Construction du prédicat de chemin (2)

Loc Instruction
0 input(y,z)
1 w := y+1
2 x := w + 3
3 if (x < 2 * z) (branche True)
4 if (x < z) (branche False)

Prédicat de chemin (entrées Y0 et Z0 )


⊤ ∧ W1 = Y0 + 1 ∧ X2 = W1 + 3 ∧ X2 < 2 × Z0 ∧ X2 ≥ Z0

S.Bardin Test Logiciel 95/ 198


Construction du prédicat de chemin (2)

Loc Instruction
0 input(y,z)
1 w := y+1
2 x := w + 3
3 if (x < 2 * z) (branche True)
4 if (x < z) (branche False)

Prédicat de chemin (entrées Y0 et Z0 )


⊤ ∧ W1 = Y0 + 1 ∧ X2 = W1 + 3 ∧ X2 < 2 × Z0 ∧ X2 ≥ Z0

Projection sur les entrées Y0 + 4 < 2 × Z0 ∧ Y0 + 4 ≥ Z0

S.Bardin Test Logiciel 95/ 198


Construction du prédicat de chemin (3)

attention : introduire une nouvelle variable logique à chaque nouvelle utilisation


d’une variable du programme
les “variables” du programme C peuvent être modifiées à chaque étape de
l’exécution
les “variables” de la théorie T sont des inconnues, de valeur constante
le renommage est nécessaire pour prendre en compte la dynamique de
l’exécution
◮ prédicat de chemin pour x := x+1 ?
◮ Xn+1 = Xn + 1, plutôt que X = X + 1

S.Bardin Test Logiciel 96/ 198


Compléments sur le prédicat de chemin
Soit des variables sur un domaine D quelconque, ϕ une formule dans une
logique interprétée sur D, et t une transition d’un programme.
t
On note x − → y pour indiquer que la valuation y ∈ D est obtenue en appliquant
la transition t à la valuation x ∈ D.

JϕK est l’ensemble des d ∈ D tq d |= ϕ.

t
wpre(t, X ′ ) : ensemble X ⊆ D tq ∀x ∈ X , ∀y tq x −
→ y alors y ∈ X ′
ensemble des éléments dont tous les successeurs par t sont dans X’
wpre(t, ϕ′ ) : formule ϕ tq JϕK = wpre(t, Jϕ′ K)

t
post(X , t) : ensemble X ′ ⊆ D tq ∀y ∈ X ′ , ∃x ∈ X telque x −→y
ensemble des éléments ayant au moins un prédécesseur par t dans X
post(ϕ, t) : formule ϕ′ tq Jϕ′ K = post(JϕK , t)

S.Bardin Test Logiciel 97/ 198


Compléments sur le prédicat de chemin (2)

1 2 n t t t
Soit un chemin du programme P : π =−
→ −
→ ... −

Alors
le prédicat de chemin le plus faible de π est défini par :
ϕ¯π = wpre(t1 , wpre(t2 , . . . wpre(tn , ⊤)))
conséquence : un prédicat de chemin quelconque ϕπ pour π vérifie :
ϕπ ⇒ ϕ¯π

On utilise souvent le calcul en avant (post) pour calculer un prédicat de chemin

ϕ¯π ′ = post(post(post(⊤, t1 ), t2 ) . . . , tn )
relation : ϕ¯π ′ ⇒ ϕ¯π et (ϕ¯π ′ , ϕ¯π ) équisatisfiable

S.Bardin Test Logiciel 98/ 198


Plan

Introduction
Automatisation de la génération de tests
◮ Prédicat de chemins
◮ Exécution symbolique
◮ Exécution concolique
◮ En pratique
◮ Discussion
◮ Complément : Aspects logiques
◮ Complément : Optimisations
Critères de test avancés

S.Bardin Test Logiciel 99/ 198


Exécution symbolique

Génération de tests basée sur les chemins


1 choisir un chemin π du CFG
2 calculer un de ses prédicats de chemin ϕπ
3 résoudre ϕπ : une solution = un DT exerçant le chemin π
4 si couverture incomplète, goto 1

Idée ancienne, mais automatisation complète récente


PathCrawler, Dart, Cute, Exe

concept introduit par King dans les années 1970


au début : tout à la main, utilisateur se débrouille
ensuite : on crée ϕπ , puis utilisateur se débrouille
automatisation complète sur des programmes : 2004-2006 (Berkeley,
CEA, CMU, Microsoft, Stanford)

S.Bardin Test Logiciel 100/ 198


Exécution symbolique
input : a program P
output : a test suite TS covering all feasible paths of Paths ≤k (P)
pick a path σ ∈ Paths ≤k (P)
compute a path predicate ϕσ of σ [wpre, spost]
solve ϕσ for satisfiability [smt solver]
SAT(s) ? get a new pair < s, σ >
loop until no more path to cover

S.Bardin Test Logiciel 101/ 198


Exécution symbolique
input : a program P
output : a test suite TS covering all feasible paths of Paths ≤k (P)
pick a path σ ∈ Paths ≤k (P)
compute a path predicate ϕσ of σ [wpre, spost]
solve ϕσ for satisfiability [smt solver]
SAT(s) ? get a new pair < s, σ >
loop until no more path to cover

S.Bardin Test Logiciel 101/ 198


Exécution symbolique
input : a program P
output : a test suite TS covering all feasible paths of Paths ≤k (P)
pick a path σ ∈ Paths ≤k (P)
compute a path predicate ϕσ of σ [wpre, spost]
solve ϕσ for satisfiability [smt solver]
SAT(s) ? get a new pair < s, σ >
loop until no more path to cover

S.Bardin Test Logiciel 101/ 198


Exécution symbolique
input : a program P
output : a test suite TS covering all feasible paths of Paths ≤k (P)
pick a path σ ∈ Paths ≤k (P)
compute a path predicate ϕσ of σ [wpre, spost]
solve ϕσ for satisfiability [smt solver]
SAT(s) ? get a new pair < s, σ >
loop until no more path to cover

S.Bardin Test Logiciel 101/ 198


Exécution symbolique
input : a program P
output : a test suite TS covering all feasible paths of Paths ≤k (P)
pick a path σ ∈ Paths ≤k (P)
compute a path predicate ϕσ of σ [wpre, spost]
solve ϕσ for satisfiability [smt solver]
SAT(s) ? get a new pair < s, σ >
loop until no more path to cover

S.Bardin Test Logiciel 101/ 198


Exécution symbolique
input : a program P
output : a test suite TS covering all feasible paths of Paths ≤k (P)
pick a path σ ∈ Paths ≤k (P)
compute a path predicate ϕσ of σ [wpre, spost]
solve ϕσ for satisfiability [smt solver]
SAT(s) ? get a new pair < s, σ >
loop until no more path to cover

S.Bardin Test Logiciel 101/ 198


Exécution symbolique
input : a program P
output : a test suite TS covering all feasible paths of Paths ≤k (P)
pick a path σ ∈ Paths ≤k (P)
compute a path predicate ϕσ of σ [wpre, spost]
solve ϕσ for satisfiability [smt solver]
SAT(s) ? get a new pair < s, σ >
loop until no more path to cover

S.Bardin Test Logiciel 101/ 198


Procédure symbolique de base (parcours DFS)

Variable globale Tests initialisée à ∅


Procédure principale : Search(node init, ε, ⊤)
/* màj Tests, ensemble de paires (TD,π) */

procedure Search(node, π, Φ)
input : CFG node, path prefix π, path predicate Φ for π
output : no result, update Tests
1: Case node of
2: | halt → /* end node */
3: try Sp := solve(Φ) ; Tests := Tests + {(Sp , π)} /* new TD */
4: with unsat → () ;
5: end try
6: | block i → Search(node.next, π · node, Φ ∧ symb(i))
7: | goto tnode → Search(tnode, π · node, Φ)
8: | ite(cond,inode,tnode) → /*branching*/
9: Search(inode, π · node,Φ ∧ symb(cond)) ;
10: Search(tnode, π · node,Φ ∧ ¬symb(cond))
11: end case

S.Bardin Test Logiciel 102/ 198


Procédure symbolique de base (2)

Procédure SYMB : Instr 7→ T


transforme une instruction de base en formule de la théorie T
exemple : x:=x+1 → X1 = X0 + 1
attention : introduire une nouvelle variable logique à chaque nouvelle
utilisation d’une variable du programme

Procédure SOLVE : T 7→ {SAT (Sol), UNSAT }


procédure de décision pour la théorie T
retourne SAT (+ une solution) ou UNSAT

S.Bardin Test Logiciel 103/ 198


Procédure SYMB, exemple

expr ::= | VC | k ∈ N | expr (+,-,*) expr

Expressions de la théorie logique T définies par

termF : := k ∈ N | VF
| termF +F termF | termF −F termF | termF ×F termF

let SYMB e = match e with


| VC → α(Vc ) // fonction de renommage
| k →k
| e1 (+,-,*) e2 → SYMB(e1 ) (+F ,−F ,×F ) SYMB(e2 )

SYMB définit de manière similaire sur les conditions

S.Bardin Test Logiciel 104/ 198


Procédure SYMB, exemple (2)

Pourquoi α(Vc ) :
les “variables” du programme C peuvent être modifiées à chaque étape de
l’exécution
les “variables” de la théorie T sont des inconnues, de valeur constante
le renommage est nécessaire pour prendre en compte la dynamique de
l’exécution
◮ prédicat de chemin pour x := x+1 ?
◮ Xn+1 = Xn + 1, plutôt que X = X + 1

S.Bardin Test Logiciel 105/ 198


Propriétés : correction et complétude
Le calcul de prédicat de chemin est :
correct s’il produit un prédicat de chemin plus fort que le prédicat de
chemin le plus lâche
complet s’il produit un prédicat de chemin équisatisfiable au prédicat de
chemin le plus lâche

Le calcul symbolique est correct (resp. complet) si :


le calcul de prédicat de chemin est correct (resp. complet)
le solveur est correct et complet pour la théorie considérée

Propriétés
Correction si le calcul symbolique est correct, alors la procédure est correcte : chaque
DT généré suit le chemin prévu
Complétude si le calcul symbolique est complet, alors la procédure est complète :
quand la procédure termine, chaque chemin faisable est couvert
Terminaison la procédure termine ssi le nombre de chemins est fini

S.Bardin Test Logiciel 106/ 198


Intérêt pour la vérification
La procédure produit des témoins d’accessibilité :
on peut vérifier le résultat fournit par des outils externes simples (calcul
de couverture)
un couple (DT, π) est plus facile à comprendre humainement que des
invariants
les DT peuvent être exportées vers des outils classiques de gestion de
tests (couverture, tests de régression)

Correction : chaque DT généré suit le chemin prévu


pas de faux positifs ! !
un bug reporté est un bug trouvé
la couverture du jeu de tests fourni est effectivement atteinte
les instructions couvertes lors de l’exécution symboliques sont vraiment
atteignables

MAIS : La complétude n’est que rarement obtenue, car le nombre de chemins


doit être limité a priori

S.Bardin Test Logiciel 107/ 198


Ajouts classiques à la procédure de base

Ajouts classiques à la procédure


1. borne sur la longueur des chemins
2. time out sur le solveur
3. gestion de couverture (instructions, branches)

Les points 1. et 2. cassent la propriétés de complétude pour assurer terminaison


et temps de calcul raisonnable

En pratique, l’hypothèse de calcul symbolique parfait (correct + complet) est


difficile à obtenir.
pour du test, il vaut mieux garder la correction et sacrifier la complétude
(cohérent avec la restriction arbitraire du nombre de chemins)
remarque : dans le cas concolique (cf + tard), on peut imaginer se passer
dans une certaine mesure de la correction du solveur

S.Bardin Test Logiciel 108/ 198


Ajouts classiques à la procédure de base (2)

Critère de test naturellement associé : couverture de chemins

Peut être modifié pour d’autres critères (instructions ou branches)


arrêt lorsque le taux de couverture est suffisant
guide le choix des chemins

Paramètres principaux de la méthode : théorie logique, énumération de


chemins, critère d’arrêt

S.Bardin Test Logiciel 109/ 198


Problèmes de l’exécution symbolique

Passage à l’échelle / Performances (cf plus tard dans le cours)


nombre de chemins
coût d’un appel au solveur

Exploration (inutile) de chemins infaisables (PB1)


pas de détection : coûteux en # chemins inutiles explorés
détection au plus tôt : coûteux en # appels solveurs

Constructions du langage hors de portée de la théorie choisie (PB2)


opérations non linéaire
assembleur incorporé, bibliothèques en code natif

L’exécution concolique apporte des solutions aux 2 derniers problèmes (cf après)

S.Bardin Test Logiciel 110/ 198


Problème PB1 : chemins infaisables

Supposons un chemin infaisable dans l’arbre des exécutions possibles

S.Bardin Test Logiciel 111/ 198


Problème PB1 : chemins infaisables

Supposons un chemin infaisable dans l’arbre des exécutions possibles

S.Bardin Test Logiciel 111/ 198


Problème PB1 : chemins infaisables

Méthode usuelle : résoudre le prédicat à la fin du chemin


+ : un appel au solveur par chemin (sur un arbre : 2N )
- : on peut continuer la recherche à partir de préfixes UNSAT
KO sur programmes avec beaucoup de chemins infaisables

S.Bardin Test Logiciel 111/ 198


Problème PB1 : chemins infaisables

Alternative : résoudre le prédicat à chaque branche


+ : détecte UNSAT au plus tôt
- : un appel au solveur par préfixe de chemin faisable,
et un appel au solveur par préfixe minimal infaisable
(sur un arbre : 2 ∗ 2N − 1)
KO sur programmes avec peu de chemins infaisables
S.Bardin Test Logiciel 111/ 198
Problème PB2
Un problème classique : constructions du langage hors de portée de la théorie
choisie

Générer un test pour f atteignant ERROR ci-dessous


(hyp : théorie = arithmétique linéaire)

g(int x) {return x*x; }


f(int x, int y) {z=g(x); if (y == z) {ERROR; }else OK }

Problème
Une exécution symbolique génère une expression symbolique de type
z =x ∗x
Cette expression n’est pas solvable en arithmétique linéaire

Solutions classiques tirées de l’analyse statique / preuve de programme


surapproximation, ici par exemple z = ⊤
PROBLEME : on perd la correction, DT ne suit pas le chemin prévu

S.Bardin Test Logiciel 112/ 198


Teaser

L’exécution concolique offre :

une solution très élégante à PB1


◮ détecte UNSAT au plus tôt
◮ un appel au solveur par chemin (maximal) faisable + un appel
par prefixe minimal infaisable
une solution pragmatique à PB2

S.Bardin Test Logiciel 113/ 198


Plan

Introduction
Automatisation de la génération de tests
◮ Prédicat de chemins
◮ Exécution symbolique
◮ Exécution concolique
◮ En pratique
◮ Discussion
◮ Complément : Aspects logiques
◮ Complément : Optimisations
Critères de test avancés

S.Bardin Test Logiciel 114/ 198


Exécution concolique

Combinaison d’exécutions symboliques et concrètes

[GKS-05] [SMA-05] [WMM-04]

Exécution concrète : collecte des infos pour aider le raisonnement symbolique


concrétisation : force une variable symbolique à prendre sa valeur
concrète courante

Deux utilisations typiques


suivre uniquement des chemins faisables à moindre coût
. toujours suivre une exécution concrète + résoudre au plus tôt
approximation de constructions du langage “difficiles”
. concrétisation d’une partie des entrées/sorties
. approximations correctes

S.Bardin Test Logiciel 115/ 198


(Rappel) Problème 2

Générer un test pour f atteignant ERROR ci-dessous


(hyp : théorie = arithmétique linéaire)

g(int x) {return x*x; }


f(int x, int y) {z=g(x); if (y == z) {ERROR; }else OK }

une exécution symbolique génère une expression symbolique de type


z =x ∗x
non solvable en arithmétique linéaire

S.Bardin Test Logiciel 116/ 198


Exploiter l’information dynamique

g(int x) {return x*x+(x%2); }


f(int x, int y) {z=g(x); if (y == z) {ERROR; }else OK }

Exploitation d’une exécution concrète


première exécution avec comme entrées de f : x = 3, y = 4
lors du calcul de prédicat, x*x reconnu non traitable
l’expression est “concrétisée” à 9, ET ses opérandes (ici x) sont aussi
concrétisés.
l’exécution aboutit au prédicat de chemin (y ! = 9) (branche else du test,
hyp x = 3)
un nouveau chemin est obtenu par négation du prédicat, soit (y = 9)
(branche then du test, hyp x = 3)
on résoud, on trouve x = 3, y = 9, atteint bien ERROR

S.Bardin Test Logiciel 117/ 198


Exploiter l’information dynamique

g(int x) {return x*x+(x%2); }


f(int x, int y) {z=g(x); if (y == z) {ERROR; }else OK }

Exploitation d’une exécution concrète


première exécution avec comme entrées de f : x = 3, y = 4
lors du calcul de prédicat, x*x reconnu non traitable
l’expression est “concrétisée” à 9, ET ses opérandes (ici x) sont aussi
concrétisés.
l’exécution aboutit au prédicat de chemin (y ! = 9) (branche else du test,
hyp x = 3)
un nouveau chemin est obtenu par négation du prédicat, soit (y = 9)
(branche then du test, hyp x = 3)
on résoud, on trouve x = 3, y = 9, atteint bien ERROR

Technique correcte et robuste, mais perte de complétude

S.Bardin Test Logiciel 117/ 198


Rappel Problème 1 : chemins infaisables

S.Bardin Test Logiciel 118/ 198


Suivre seulement des chemins faisables

X<0 X >= 0

X <> 12 X = 12

X >= -3 X < -3

concret : X=12
backtrack + résolution, solution X = 5
concret : X=5
backtrack + résolution, unsat
S.Bardin Test Logiciel 119/ 198
Suivre seulement des chemins faisables

X<0 X >= 0

X <> 12 X = 12

X >= -3 X < -3

concret : X=12
backtrack + résolution, solution X = 5
concret : X=5
backtrack + résolution, unsat
S.Bardin Test Logiciel 119/ 198
Suivre seulement des chemins faisables

X<0 X >= 0

X <> 12 X = 12

X >= -3 X < -3

concret : X=12
backtrack + résolution, solution X = 5
concret : X=5
backtrack + résolution, unsat
S.Bardin Test Logiciel 119/ 198
Suivre seulement des chemins faisables

X<0 X >= 0

X <> 12 X = 12

X >= -3 X < -3

concret : X=12
backtrack + résolution, solution X = 5
concret : X=5
backtrack + résolution, unsat
S.Bardin Test Logiciel 119/ 198
Suivre seulement des chemins faisables

X<0 X >= 0

X <> 12 X = 12

X >= -3 X < -3

concret : X=12
backtrack + résolution, solution X = 5
concret : X=5
backtrack + résolution, unsat
S.Bardin Test Logiciel 119/ 198
Suivre seulement des chemins faisables

X<0 X >= 0

X <> 12 X = 12

X >= -3 X < -3

concret : X=12
backtrack + résolution, solution X = 5
concret : X=5
backtrack + résolution, unsat
S.Bardin Test Logiciel 119/ 198
Suivre seulement des chemins faisables (2)

S.Bardin Test Logiciel 120/ 198


Suivre seulement des chemins faisables (2)

S.Bardin Test Logiciel 120/ 198


Suivre seulement des chemins faisables (2)

Méthode usuelle : résoudre le prédicat à la fin du chemin


+ : un appel au solveur par chemin (sur un arbre : 2N )
- : on peut continuer la recherche à partir de préfixes UNSAT
KO sur programmes avec beaucoup de chemins infaisables

S.Bardin Test Logiciel 120/ 198


Suivre seulement des chemins faisables (2)

Alternative : résoudre le prédicat à chaque branche


+ : détecte UNSAT au plus tôt
- : un appel au solveur par préfixe de chemin faisable
et un appel au solveur par préfixe minimal infaisable
(sur un arbre : 2 ∗ 2N − 1)
KO sur programmes avec peu de chemins infaisables

S.Bardin Test Logiciel 120/ 198


Suivre seulement des chemins faisables (2)

Un exemple possible d’exécution concolique


(hyp : exéc. concrète suit le fils gauche)
(noeud violet : couvert par exec concrète)

S.Bardin Test Logiciel 120/ 198


Suivre seulement des chemins faisables (2)

Un exemple possible d’exécution concolique


+ : détecte UNSAT au plus tôt
+ : un appel au solveur par chemin (maximal) faisable + un appel par
prefixe minimal infaisable
+ : toujours moins d’appels que les deux autres méthodes

S.Bardin Test Logiciel 120/ 198


Intérêts de la concrétisation

Le mécanisme général de concrétisation peut être utiliser de multiples manières,


pour donner des sous-approximations pertinentes

instructions du programme avec une sémantique hors de T


instructions du programme hors scope de l’analyseur (ex : asm, sql,
librairies en binaire, etc.)
programmes avec alias et structures complexes : imposer un ensemble fini
mais réaliste de relations d’alias entre variables, ou de “formes mémoires”
multi-thread : imposer un (des) entrelacement(s) réaliste(s) des processus
contraintes générées trop complexes dû au nombre de variables trop
élevé : réduction a priori du nombre de variables via concrétisation

S.Bardin Test Logiciel 121/ 198


Intérêts de la concrétisation (2)

Le mécanisme de concrétisation est un levier très utile pour adapter la méthode


sur des cas difficiles
compromis d’utilisation
robustesse à la classe de programmes supportés
conserve la correction
ex : pas besoin de gérer parfaitement toutes les constructions d’un
langage pour développer une analyse concolique correcte pour ce langage

S.Bardin Test Logiciel 122/ 198


Procédure concolique basique
Nouvel argument : état mémoire concret C
lancement : Search(node.init, ε ,⊤, 0)

procedure Search(n, π, Φ, C)
1: Case n of
2: | halt → () /* end node */
3: | block i → Search(n.next, π · n, Φ ∧ symb(i), update(C,i))
4: | goto n’ → Search(n’, π · n,Φ, C)
5: | ite(cond,in,tn) →
6: Case eval(cond,C) of /* follow concrete branch */
7: | true →
8: Search(in, π · n, Φ ∧ SYMB(cond), C) ;
9: try /* solve new branch first */
10: Sp := solve(Φ ∧ ¬cond) ; Tests := Tests + {(Sp , π.tn)}
11: C ′ := update C for branching(Sp )
12: Search(tn, π · n,Φ ∧ ¬SYMB(cond),C’) /* branching */
13: with unsat → ()
14: end try
15: | false → ..... /* symmetric case */
16: end case
17: end case
S.Bardin Test Logiciel 123/ 198
Procédure concolique basique (2)

update(C : mem-conc,i : instr) → mem-conc : màj de l’état mémoire actuel

eval(cond : predicat,C : mem-conc) → bool : évalue la condition cond vis à vis


de l’état mémoire actuel

update C for branching(Sp : DT)→ mem-conc : créer un nouvel état mémoire


cohérent avec le préfixe de chemin suivi par DT

( Explication du dernier point : à chaque moment, invariant : Φ et C cohérents


avec chemin suivi jusque là. Cet invariant est cassé quand on impose un
branchement car l’exécution concrète ne “partait pas dans ce sens là” ).

S.Bardin Test Logiciel 124/ 198


Réflexions sur concolique

Plutôt récent
déjà dans PathCrawler (CEA, 2004) pour chemins infaisables
popularisé par DART et CUTE (2005) pour robustesse

Symbolic execution : ≈ analyses statiques sur un chemin


plus facile car juste un chemin, mais incomplet
garde les défauts des analyses statique pure (enfermé dans une théorie)

Concolic execution : statique + dynamique


paradigme vraiment différent de statique pure
grande robustesse
compromis de mise en œuvre

S.Bardin Test Logiciel 125/ 198


Plan

Introduction
Automatisation de la génération de tests
◮ Prédicat de chemins
◮ Exécution symbolique
◮ Exécution concolique
◮ En pratique
◮ Discussion
◮ Complément : Aspects logiques
◮ Complément : Optimisations
Critères de test avancés

S.Bardin Test Logiciel 126/ 198


Quelques prototypes existants

PathCrawler (CEA) 2004


Dart (Bell Labs), Cute (Berkeley) 2005
Exe (Stanford) 2006
Java PathFinder (NASA) 2007
Osmose (CEA), Sage (Microsoft), Pex (Microsoft) 2008

S.Bardin Test Logiciel 127/ 198


Quelques résultats pratiques

Pex bientôt livré dans Visual C#


cible = aide au programmeur

Sage en production interne chez Microsoft (sécurité)


service interne de “smart fuzzing”
le logiciel tourne en boucle sur de gros serveurs
nombreux bugs trouvés

Études de cas académiques sur des codes type drivers / kernel (Linux, BSD,
Microsoft .NET)
codes souvent déjà bien testés, nombreux bugs trouvés
ex : Klee : > 95 % de couverture obtenue automatiquement sur les Unix
coreutils (make, grep, wc, etc.)

S.Bardin Test Logiciel 128/ 198


Démo

PathCrawler online

http ://pathcrawler-online.com/

S.Bardin Test Logiciel 129/ 198


Préconditions complexes

Prise en compte des préconditions de la fonction à tester

Pourquoi : éviter des tests non pertinents


ex : algorithme de recherche dichotomique
le tableau d’entrée généré doit être trié, sinon le test n’est pas
représentatif

SOLUTION 1 : filtrer a posteriori


générer les DT comme avant
puis éliminer toutes les DT ne respectant pas la précondition
PB : nécessite une précondition exécutable
PB : ne fonctionne pas si précondition trop contrainte

S.Bardin Test Logiciel 130/ 198


Préconditions complexes (2)

SOLUTION 2 : générer à coup sûr des DT satisfaisant Pre

Approche 1 : gestion de la précondition au niveau logique

on résoud des formules de la forme π ∧ PRECOND


PB : les préconditions élaborées (ex : tableau trié) demandent des
quantificateurs ∀

Approche 2 : gestion de la précondition au niveau du code


on ajoute au début du programme une fonction check precond(args)
PB1 : les préconditions élaborées (ex : tableau trié) demandent des
boucles (#chemins ++)
PB2 : nécessite des préconditions exécutables

S.Bardin Test Logiciel 131/ 198


Préconditions complexes (3)

S.Bardin Test Logiciel 132/ 198


Préconditions complexes (3)

Remarque 1 : certaines préconditions simples sont aisées à intégrer, dans un cas


comme dans l’autre
exemple : X >= 0, ou X != NULL

Remarque 2 : les préconditions peuvent aussi être un moyen donné à


l’utilisateur pour diminuer l’espace de recherche de la DSE

exemple : restreindre arbitrairement les valeurs possibles de l’input X

S.Bardin Test Logiciel 133/ 198


Oracle

Prise en compte de l’oracle de la fonction à tester


rapport de tests plus informatifs (dt,chemin,verdict)
idéal : générer directement des tests fautifs

On retrouve les mêmes problèmes que pour la précondition


a posteriori : tests générés sans oracle, puis verdict ensuite
a priori : tests générés contre l’oracle

Quel format pour l’oracle ?


code ou formule : même pb que pour la précondition
des oracles partiels simples (runtime errors) peuvent être légers et
intéressants du point de vue guidage de la génération

S.Bardin Test Logiciel 134/ 198


Oracle (2)

En pratique :
la prise en compte de la précondition à la génération est essentielle
(correction des tests)
filtrage a posteriori pas satisfaisant
pour l’oracle, la prise en compte à la génération est lourde et pas
forcément couronnée de succès
◮ filtrage a posteriori si le but est de considérer un oracle complet
◮ intégration a priori envisageable avec un oracle partiel et léger

S.Bardin Test Logiciel 135/ 198


Autres

Sortir les tests dans un format réutilisable


exporter vers autres outils
◮ JUnit, calcul de couverture, sélection / minimisation, etc.
utilisation conjointe de DT issues d’autres méthodes / outils
◮ tests issus de méthodes manuelles ou orientées modèles
◮ méthodes automatiques simples (random testing, interface-based
testing)

Génération en complément de tests existants


éviter redondance avec tests existants
génération incrémentale

S.Bardin Test Logiciel 136/ 198


Plan

Introduction
Automatisation de la génération de tests
◮ Prédicat de chemins
◮ Exécution symbolique
◮ Exécution concolique
◮ En pratique
◮ Discussion
◮ Complément : Aspects logiques
◮ Complément : Optimisations
Critères de test avancés

S.Bardin Test Logiciel 137/ 198


Bilan (subjectif) sur l’approche concolique
Points forts pour une utilisation industrielle
totalement automatisée si oracle automatique
robuste aux “vrais” programmes
correcte, résultats facilement vérifiables
s’insère dans pratiques de test existantes (process, outils, etc.)
utilisation (naturellement) incrémentale
gain incrémental
surpasse assez facilement les pratiques usuelles (ex : fuzzbox testing)

Puissance maximale si couplée avec un langage de contrat

Quels domaines d’utilisation ?


pb pour la certification : traçabilite DT - exigences
pb si l’on veut absolument 100% de couverture, même sur des codes de
taille petite / moyenne
mais ok pour le débogage intensif

S.Bardin Test Logiciel 138/ 198


Quelques challenges

Passage à l’échelle (# chemins)

quelle notion de résumé de fonction / boucle ?


comment “bouchonner” facilement un morceau de code ?

Prise en compte de préconditions complexes en cas de structures dynamiques


quantificateurs, axiomes

Au niveau solveurs :
chaines de caractères, flottants

S.Bardin Test Logiciel 139/ 198


Plan

Introduction
Automatisation de la génération de tests
◮ Prédicat de chemins
◮ Exécution symbolique
◮ Exécution concolique
◮ En pratique
◮ Discussion
◮ Complément : Aspects logiques
◮ Complément : Optimisations
Critères de test avancés

S.Bardin Test Logiciel 140/ 198


À propos des théories utilisées

Chaque instruction doit être traduite vers une formule


le langage des formules peut être très riche

Les contraintes doivent être résolues automatiquement


satisfiabilité décidable + résolution efficace en pratique
beaucoup moins de liberté ! !

Remarques
compromis expressivité VS décidabilité / complexité
si théorie pas assez expressive : approximations [concrétisation]

S.Bardin Test Logiciel 141/ 198


Quelques définitions

Une théorie logique T

un ensemble prédéfini de symboles de fonctions et prédicats [en général


au moins =]
une sémantique implicite [domaine des variables, “sens” des fonctions et
prédicats]
des axiomes imposant la sémantique implicite
on note T |= ϕ pour dire que la formule ϕ est valide dans la théorie T

Un fragment logique
on limite les connecteurs logiques
typiquement : pas de quantificateur, pas de ∨

S.Bardin Test Logiciel 142/ 198


À propos des théories utilisées (2)

Avantage d’être sur un chemin


fragment simple : pas de quantificateur, seulement des conjonctions
beaucoup de classes décidables, voir solubles efficacement

Théories pour les types de base


(N, x − y #k) (logique de différence, P )
(R, +, ×k) (arithmétique linéaire, P [Simplex (non polynomial)])
(N, +, ×k) (arithmétique linéaire, NP-complet [Omega test])
B (booléens, NP-complet [DPLL ou BDDs])
(N≤ , +, ×) (arithmétique bornée non linéaire, NP-complet [CP(FD)])
BV (bitvecteurs, NP-complet [bitblasting])
FLOAT (arithmétique flottante, NP-complet)
types algébriques (algèbre libre, P [unification])

S.Bardin Test Logiciel 143/ 198


Exemple de théorie : EUF
Théorie des fonctions non interprétées (EUF)

signature : h=, 6=, x1 , . . . , xn , f1 (. . .), . . . , fm (. . .)i


axiomatique : (FC ) x = y ⇒ f (x) = f (y )

Utilité : pratique pour relier des éléments entre eux de manière implicite
&x en C devient addr (X )
x une structure avec deux champs num et flag : num(X ) et flag (X )
Résolution très efficace
algorithme de congruence closure (cf Wikipedia)
polynomial

Variantes
(AC) axiomes d’associativité / commutativité sur certains symboles de
fonction
(free-algebra) algèbre de types libres

S.Bardin Test Logiciel 144/ 198


Exemple de théorie : Arrays

Théorie des tableaux

signature : hARRAY , I , E , =I , 6=I , =E , 6=E , load, storei


sémantique :
load : ARRAY × I 7→ E
store : ARRAY × I × E 7→ ARRAY
axiomes : FC pour load/store, plus
(RoW 1) i = j ⇒ load(store(A, i, v ), j) = v
(RoW 2) i 6= j ⇒ load(store(A, i, v ), j) = load(A, j)

Utilité : tableaux bien sûrs, mais aussi map, vectors, etc.


Résolution : EUF + case-split, problème NP-complet

S.Bardin Test Logiciel 145/ 198


Quelles théories en pratique ?
expressivité ր : moins d’échecs mais résolution sur un chemin + chère
ex : BV + Array (NP-complet)

expressivité ց : risque plus d’échecs (concrétisation), mais résolution sur un


chemin - chère
ex : Difference + EUF (Polynomial)

Compromis idéal ? ?
Observation 2004-2011 : théories de + en + puissantes
Deux technologies de solveurs
SMT : schéma très intéressant de combinaison de solveurs
(Nelson-Oppen) intégré à une gestion efficace des booléens
(plus confidentiel) Constraint Programming :
◮ pour les variables à domaines finis
◮ des approches intéressantes pour FLOAT, BV, (N≤ , +, ×)

S.Bardin Test Logiciel 146/ 198


Modélisation sous-jacente des programmes C

Considérons l’instruction : x := a + b

Traduction 1 :

Xn+1 = An + Bn

S.Bardin Test Logiciel 147/ 198


Modélisation sous-jacente des programmes C

Considérons l’instruction : x := a + b

Traduction 1 :

Xn+1 = An + Bn

Modèle mémoire sous-jacent : ensemble de variables {A, B, X , . . .}

S.Bardin Test Logiciel 147/ 198


Modélisation sous-jacente des programmes C

Considérons l’instruction : x := a + b

Traduction 1 :

Xn+1 = An + Bn

Modèle mémoire sous-jacent : ensemble de variables {A, B, X , . . .}

Bien mais ne pourra prendre en compte les pointeurs

S.Bardin Test Logiciel 147/ 198


Modélisation sous-jacente des programmes C

Considérons l’instruction : x := a + b

Traduction 2 : ajout d’un état mémoire M

store(M, addr (X ), load(M, addr (A)) + load(M, addr (B)))

S.Bardin Test Logiciel 147/ 198


Modélisation sous-jacente des programmes C

Considérons l’instruction : x := a + b

Traduction 2 : ajout d’un état mémoire M

store(M, addr (X ), load(M, addr (A)) + load(M, addr (B)))

Modèle mémoire sous-jacent : map {Addr1 7→ A, Addr2 7→ B, . . .}

S.Bardin Test Logiciel 147/ 198


Modélisation sous-jacente des programmes C

Considérons l’instruction : x := a + b

Traduction 2 : ajout d’un état mémoire M

store(M, addr (X ), load(M, addr (A)) + load(M, addr (B)))

Modèle mémoire sous-jacent : map {Addr1 7→ A, Addr2 7→ B, . . .}

ok pour les pointeurs, mais on ne peut écrire au milieu de x (respect du typage,


modèle mémoire à la Java)

S.Bardin Test Logiciel 147/ 198


Modélisation sous-jacente des programmes C

Considérons l’instruction : x := a + b

S.Bardin Test Logiciel 147/ 198


Modélisation sous-jacente des programmes C

Considérons l’instruction : x := a + b

Traduction 3 : encodage de M au niveau octet (ici : 3 octets)

let tmpA = load(M,addr(A)) @ load(M,addr(A)+1) @ load(M,addr(A)+2)


and tmpB = load(M,addr(B)) @ load(M,addr(B)+1) @ load(M,addr(B)+2)
in
let nX = tmpA+tmpB
in
store(
store(
store(M, addr (X ), nX [0]),
addr (X ) + 1, nX [1]),
addr (X ) + 2, nX [2])

S.Bardin Test Logiciel 147/ 198


Modélisation sous-jacente des programmes C

Considérons l’instruction : x := a + b

Traduction 3 : encodage de M au niveau octet (ici : 3 octets)

let tmpA = load(M,addr(A)) @ load(M,addr(A)+1) @ load(M,addr(A)+2)


and tmpB = load(M,addr(B)) @ load(M,addr(B)+1) @ load(M,addr(B)+2)
in
let nX = tmpA+tmpB
in
store(
store(
store(M, addr (X ), nX [0]),
addr (X ) + 1, nX [1]),
addr (X ) + 2, nX [2])

ok pour du C ... mais la formule est complexe

S.Bardin Test Logiciel 147/ 198


Modélisation sous-jacente des programmes C

Considérons l’instruction : x := a + b

PB ouvert : affiner automatiquement le niveau d’abstraction de la modélisation

S.Bardin Test Logiciel 147/ 198


Plan

Introduction
Automatisation de la génération de tests
◮ Prédicat de chemins
◮ Exécution symbolique
◮ Exécution concolique
◮ En pratique
◮ Discussion
◮ Complément : Aspects logiques
◮ Complément : Optimisations
Critères de test avancés

S.Bardin Test Logiciel 148/ 198


Optimisations de la DSE

Diminuer le nombre d’appels au solveur


méthodes correctes d’élagage de chemins
heuristiques : parcours de chemin plus malins que la DFS

Diminuer le coût moyen d’un appel au solveur


on se place dans le cas où le solveur est utilisé en boite noire
on peut quand même améliorer ses performances

S.Bardin Test Logiciel 149/ 198


Optimisation de chemins : élagage

Élimination de chemins redondants : certains chemins sont redondants pour le


critère de couverture choisi, on peut les éviter
techniques complètes

Gestion des appels de fonction : cause principale de l’explosion du nombre de


chemins
pas encore satisfaisant

S.Bardin Test Logiciel 150/ 198


Chemins redondants

Couper les chemins qui ne peuvent atteindre de nouvelles instructions


pour chaque chemin actif, calcul des instructions accessibles à partir de
l’état actif
on stop le chemin si le préfixe ne peut atteindre de cible non couverte
calcul des accessibles peut être fait très efficacement
technique complète vis à vis de la couverture d’instructions

Technique complète vis à vis de la couverture d’instructions


on ne perd rien

Technique puissante : (DFS + optim) meilleure que bcp de parcours avancés

S.Bardin Test Logiciel 151/ 198


Chemins redondants (2)

Couper les chemins amenant à un état symbolique déjà couvert : si on a deux


préfixes π et σ tq φπ ⇒ φσ , alors on garde seulement le préfixe σ
technique complète vis à vis de la couverture d’instructions
potentiellement très coûteuse, demande de vérifer ⇒
on peut utiliser un calcul approché de ⇒ via 

Technique complète vis à vis de la couverture d’instructions

S.Bardin Test Logiciel 152/ 198


Gestion des appels de fonction

Reste un problème ouvert

Quelques solutions partielles


couper l’exploration à une certaine profondeur [incomplet !]
(Osmose, Java PathFinder)
gestion paresseuse des fonctions (Sage)
construction itérative de résumés de fonctions (Dart)
spécifications de fonctions (PathCrawler)

S.Bardin Test Logiciel 153/ 198


Gestion des appels de fonction (2)

Couper l’exploration à une certaine profondeur

simple à mettre en oeuvre ! !


pas complet

Variantes

concrétiser : prédicat de chemin correct et simple, mais très (trop ?)


contraint
remplacer l’appel de fonction par ⊤ : formule simple mais incorrecte ( !)
rentrer dans la fonction mais empêcher l’énumération de chemin : correct,
mais formule compliquée

S.Bardin Test Logiciel 154/ 198


Gestion des appels de fonction (3)

~ ∧ Post(In,
Résumés de fonction de type Pre(In) ~ Out)
~

C’est le cas idéal

pas besoin d’entrer dans la fonction appelée pour énumérer les chemins
pas besoin d’entrer dans la fonction appelée pour générer des contraintes

Attention

le solveur doit pouvoir gérer la spéc


la spéc doit être fonctionnelle, et suffisamment précise pour permettre de
déduire des valeurs (pas de surapproximation)
qui donne la spéc ?

S.Bardin Test Logiciel 155/ 198


Gestion des appels de fonction (4)

Alternatives pour les résumés de fonction

résumés en sous-approximation
utilisation incrémentale de résumés en surapproximation

W
Résumés en sous-approximation de type ~ ⇒ ψ(out)
φ(in) ~

correct, et peut être construit pendant l’exploration


rappelle la logique des “stubs” en test usuel
ajoute des ∨, formules plus lourdes
peut être construit incrémentallement

S.Bardin Test Logiciel 156/ 198


Rappel : Optimisations de la DSE

Diminuer le nombre d’appels au solveur


méthodes correctes d’élagage de chemins
heuristiques : parcours de chemin plus malins que la DFS

Diminuer le coût moyen d’un appel au solveur

S.Bardin Test Logiciel 157/ 198


Heuristique : Couvrir plus vite

Technique usuelle : parcours en profondeur (DFS)

Avantage classique de DFS


un seul contexte ouvert à la fois (mémoire)
simple à implanter en récursif

Problème de la DFS pour la génération de tests


si #DT limité, la DFS se concentre sur une portion très restreinte du code

S.Bardin Test Logiciel 158/ 198


Couvrir plus vite (2)

(a) DFS (b) BFS

Couverture pour 4 tests générés

S.Bardin Test Logiciel 159/ 198


Couvrir plus vite (3)

Couverture des chemins par DSE : tous les parcours de chemins se valent

Couverture des branches (budget limité ou non) : tous les parcours de chemins
ne se valent pas
DFS est souvent très mauvais

Quelques solutions
parcours hybride (Cute) : DFS + aléatoire
[simple, meilleur que dfs]
fitness guided [Exe, Sage, Pex] : les préfixes actifs sont évaluées, celui
de plus haut score est étendu
[mécanisme très versatile]

S.Bardin Test Logiciel 160/ 198


Parcours fitnex-guided

Ingrédients de l’exécution symbolique “Fitness-guided”

chemin actif : chemin non couvert, dont le plus long (strict) préfix est
couvert [on dit aussi préfix actif]
notion de score d’un chemin actif
à chaque étape : sélection + extension +
◮ choisir le chemin actif ayant le meilleur score
◮ “étendre” ce chemin : résolution + exécution
◮ ajouter les nouveaux chemins actifs créés par cette exécution

S.Bardin Test Logiciel 161/ 198


Parcours fitnex-guided (2)

S.Bardin Test Logiciel 162/ 198


Parcours fitnex-guided (2)

S.Bardin Test Logiciel 162/ 198


Parcours fitnex-guided (2)

S.Bardin Test Logiciel 162/ 198


Parcours fitnex-guided (2)

S.Bardin Test Logiciel 162/ 198


Parcours fitnex-guided (2)

S.Bardin Test Logiciel 162/ 198


Parcours fitnex-guided (2)

S.Bardin Test Logiciel 162/ 198


Parcours fitnex-guided (2)

S.Bardin Test Logiciel 162/ 198


Parcours fitnex-guided (3)

Nous supposons disponible les deux fonctions de base suivantes :


get initial value : ε 7→ inputData : fournit une donnée d’entrée
concrète initiale (constante ou aléatoire).
get new paths : inputData 7→ set<path> : lance une exécution
concrète à partir de valeurs d’entrées, observe le chemin π suivi à
l’exécution et retourne les préfixes actifs de π qui n’ont pas encore été
collectés. Une implémentation réelle demande un type plus complexe,
prenant en compte l’historique des préfixes actifs collectés jusque là.

L’algorithme générique nécessite un type abstrait VAL de score, et les deux


fonctions abstraites suivantes :
score : path 7→ VAL : évaluation d’un préfixe
compare : VAL × VAL 7→ {<, =, >} : comparaison à partir du score

La fonction suivante est déduite facilement :


get best : set<path> 7→ path : utilise compare

S.Bardin Test Logiciel 163/ 198


Parcours fitnex-guided (4)

input : un programme P
output : RES : ensemble de couples (π - dt), tq pour chaque couple, P(dt)
couvre π et l’ensemble des dt couvre toutes les branches faisables de P

1: RES := ∅
2: v0 := get initial value /* arbitrary initial concrete value */
3: H := get new paths(v0 ) /* get all active prefixes from an execution */
4: While still uncovered branches or paths do
5: π := get best(H)
6: H := H\{π}
7: case Solve(Symb(π)) of
8: | UNSAT → nop
9: | SAT(v ) →
10: RES := RES ∪ {(π, v )} ;
11: H := H ∪ get new paths(v )
12: end case
13: end while
14: return RES

S.Bardin Test Logiciel 164/ 198


Quelle fonction de Score ?

Par exemple, on peut baser le score sur :


longueur du chemin, profondeur d’appel de la dernière instruction
nb de fois où la dernière instruction a été couverte
...

Intérêts : permet d’intégrer facilement de nombreuses heuristiques de parcours


de chemin
chemin choisi aléatoirement
dfs, bfs, dfs avec seuil
dfs modulée par la profondeur d’appel et priorité aux branches non
couvertes
...

S.Bardin Test Logiciel 165/ 198


Quelles fonctions de Score (2) ?
Quelques exemples d’heuristiques

minCallDepth-dfs
les chemins dont le noeud final a la plus petite profondeur d’appel sont
prioritaires
puis dfs sur ces préfixes

hybrid dfs
alterner k1 test aléatoire puis k2 étapes de dfs à partir du dernier chemin
aléatoire

Best first
le prochain chemin est celui ayant le gain le plus élevé
gain, ex1 : nb de nouvelles instructions couvertes à coup sûr
gain, ex2 : nb d’instructions successeurs non encore couvertes

S.Bardin Test Logiciel 166/ 198


Rappel : Optimisations de la DSE

Diminuer le nombre d’appels au solveur


méthodes correctes d’élagage de chemins
heuristiques : parcours de chemin plus malins que la DFS

Diminuer le coût moyen d’un appel au solveur

S.Bardin Test Logiciel 167/ 198


Optimisations avant l’appel au solveur

simplification de formule (dont slicing)


séparation des sous-formules indépendantes
solveur “léger”
système de cache
réutilisation des solutions précédentes

S.Bardin Test Logiciel 168/ 198


Préprocessing : slicing

Enlever toutes les contraintes qui n’affectent pas le contrôle du chemin courant
ex : expressions de calcul du résultat final
à faire sur la formule, ou sur l’expression de chemin (plus simple)

Exemple de chemin :
y := y+1; x := a+b ; assume(y < 10) ; return x
prédicat de chemin : Y1 = Y0 + 1 ∧X1 = A0 + B0 ∧Y1 < 10
avec slicing : Y1 = Y0 + 1 ∧ Y1 < 10

S.Bardin Test Logiciel 169/ 198


Préprocessing : slicing (2)

Remarque : si le langage de formules permet la définition de termes, alors le


slicing se fait aussi bien niveau chemin que niveau formule

chemin y := y+1; x := a+b ; assume(y < 10) ; return x


prédicat en avant :
let Y1 :=Y0 + 1 in let X1 :=A0 + B0 in Y1 < 10
le terme X1 n’est jamais utilisé, on l’enlève
formule simplifiée : let Y1 :=Y0 + 1 in Y1 < 10

S.Bardin Test Logiciel 170/ 198


Préprocessing : simplification de formule
propagation de constantes : règles de la forme
(élim de var) X = 5 : remplacer X par 5 dans toute la formule, se
souvenir de X = 5
(élim de déf) let X :=5 : remplacer X par 5 dans toute la formule,
éliminer X
(calcul de terme) 5 + 3 : faire le calcul, remplacer par 8
(calcul partiel de terme)
X + 0 : remplacer par X
X × 0 : remplacer par 0
X × 1 : remplacer par X
...

propagation d’égalités

si X = Y , garder seulement X ou seulement Y dans la formule


on peut étendre aux opérateurs : X = A + B ∧ Y = A + B alors X = Y

S.Bardin Test Logiciel 171/ 198


Préprocessing : simplification de formule (2)

unification des sous-termes identiques (introduction de défs)

transformer X = A + B + 1 ∧ Y = A + B + 2 ∧ B ≤ Z en
let T :=A + B in X = T + 1 ∧ Y = T + 2 ∧ B ≤ Z

On peut normaliser les opérateurs AC pour trouver plus d’égalités


AC : associatif - commutatif
fonctionne pour propagation des égalités et unification de sous-termes
identiques
ex : réécrire les additions en ordonnant les opérandes selon ordre
lexicographique
ex : X = A + B ∧ Y = B + A, on normalise en X = A + B ∧ Y = A + B,
on déduit que X = Y

S.Bardin Test Logiciel 172/ 198


Préprocessing : simplification de formule (3)

reconnaissance et utilisation des variables “proxy”


sur une formule de type X = A + B ∧ X + 3 ≤ 100
X est un proxy pour A et B : A et B ne sont pas utilisés ailleurs, et
X = A + B est satisfaisable pour toute valeur de X
dans ce cas : on enlève A et B de la formule (on résoud avec X ), et on
résoud à part X = A + B
ici on peut même imposer directement par exemple A = X et B = 0

Remarques

ne marche pas si A ou B ont d’autres contraintes


ce n’est pas un cas particulier de formules indépendantes

S.Bardin Test Logiciel 173/ 198


Séparation des sous-formules indépendantes

~ ,V
Séparation de formules : Soit V ~1, V
~ 2 des ensembles de variables. Si ϕ(V
~)
~ ~ ~ ~
peut se décomposer en ϕ1 (V1 ) ∧ ϕ2 (V2 ) et V1 ∩ V2 = ∅, alors :
si Solve(ϕ1 ) retourne UNSAT alors UNSAT
sinon si Solve(ϕ2 ) retourne UNSAT alors UNSAT
sinon SAT, et une solution = solution(V~1 ) ∪ solution(V~2 )

Rmq 1 : dès que Solve a une complexité supérieure à linéaire, on gagne à


séparer la formule
Rmq 2 : (pratique, cas UNSAT) dans quel ordre résoudre les ϕi ?
Rmq 3 : plus les formules sont petites, mieux le cache fonctionne

S.Bardin Test Logiciel 174/ 198


Utilisation d’un solveur léger, résolution en 2 étapes

On utilise déjà un solveur peu coûteux mais incomplet

répond UNSAT ou MAYBE

Le solveur léger est lancé avant le solveur complet


on gagne du temps si la formule est UNSAT

Comment obtenir le solveur léger ?


le faire soi-même
utiliser un solveur existant avec juste des théories simples
un preprocessing élaboré peut servir de solveur léger

S.Bardin Test Logiciel 175/ 198


Cache sur les formules

Cache de formules : pour certaines ϕ′ déjà résolues, on garde dans un cache


C : ϕ′ 7→ SAT (et une solution) ou ϕ′ →
7 UNSAT.

Soit ϕ à résoudre, à chaque appel du solveur on fait :


si ∃ϕ′ ∈ C tq ϕ ⇒ ϕ′ et ϕ′ UNSAT, alors ϕ UNSAT
sinon si ∃ϕ′ ∈ C tq ϕ′ ⇒ ϕ et ϕ′ SAT, alors ϕ SAT (et même solution)
sinon Solve(ϕ)

Remarque : calculer ⇒ est coûteux, on approxime A ⇒ B par B  A


(sous-terme syntaxique)

S.Bardin Test Logiciel 176/ 198


Réutilisation de solutions précédentes

on résoud les préfixes de chemin de manière incrémentale


donc on résoud une formule du type φ(...) ∧ pred(X ~ ), en connaissant déjà
une solution de φ(...)
on peut réutiliser l’ancienne solution comme suit : toute la sous-formule
de φ n’affectant pas X ~ est enlevée, les variables concernées prennent leurs
valeurs anciennement trouvées, et on résoud ce qui reste

S.Bardin Test Logiciel 177/ 198


Réutilisation de solutions précédentes (2)

* Supposons que l’on a déjà résolu


X = Y + 3 ∧ X ≤ 5 ∧B ≥ 0
solution trouvée : X = 6, Y = 8, B = 0

* pour résoudre
X = Y + 3 ∧ X ≤ 5 ∧B ≥ 0 ∧B + 12 ≤ Z
on réutilise les anciennes valeurs de X , Y (6 et 8), et on résoud seulement
B ≥ 0 ∧B + 12 ≤ Z

S.Bardin Test Logiciel 178/ 198


Réutilisation de solutions précédentes (3)

Remarque : peut être simulé en utilisant séparation des sous-formules


indépendantes (cf après) et utilisation du cache

Solution tout de même intéressante


plus facile à mettre en oeuvre qu’un cache de calcul (mais moins général)
même si le cache est dispo, économise la recherche dans le cache

S.Bardin Test Logiciel 179/ 198


Remarque

L’efficacité de ces optims dépend du type de logiciel

pour le code de type “parseur simple”, les systèmes de cache et de


séparation de sous-formules fonctionnent très bien

Les optimisations se combinent bien

plus on simplifie, plus on peut séparer les formules


plus les formules sont simples / petites, plus le cache fonctionne
...

S.Bardin Test Logiciel 180/ 198


Plan

Introduction
Automatisation de la génération de tests
Critères de test avancés

S.Bardin Test Logiciel 181/ 198


Problème général

Critère mutationnel :
critère de test puissant en terme de détection de fautes
difficile à automatiser

Critères de couverture usuels (instructions, branches) :


critères moins puissants
permettent de manière efficace :
◮ le calcul de couverture
◮ la génération de tests (exécution concolique)

S.Bardin Test Logiciel 182/ 198


Mutants et labels

Idée :
◮ Transformer les mutants (faibles) en prédicats/labels dans le
programme

Pourquoi :
◮ Bénéficier des avantages des deux familles de critères
◮ Automatiser efficacement la couverture de mutants
◮ Étendre l’exécution concolique à des critères de test avancés

S.Bardin Test Logiciel 183/ 198


Mutations fortes

S.Bardin Test Logiciel 184/ 198


Mutations fortes

S.Bardin Test Logiciel 184/ 198


Mutations fortes

S.Bardin Test Logiciel 184/ 198


Rappel sur les mutants

Intérêt
critère de couverture le plus puissant du point de vue
théorique (peut émuler la plupart des autres)
bien corrélé en pratique à la découverte de bugs

Difficile à automatiser
calcul de couverture : M compilations, T × M exécutions
(rmq : M souvent très grand)
génération de TD : inexistant

S.Bardin Test Logiciel 185/ 198


Mutations faibles

S.Bardin Test Logiciel 186/ 198


Mutations faibles

S.Bardin Test Logiciel 186/ 198


Mutations faibles

Mutation faible : presqu’aussi puissant (en pratique) que mutation


forte
S.Bardin Test Logiciel 186/ 198
Des mutants faibles aux labels
Mutant M1 Programme P Mutant M2

statement i-1; statement i-1; statement i-1;


x := f(d); x := d; x := d;
y := e; y := e; y := g(e);
statement i+2; statement i+2; statement i+2;

labels :
- prédicats
- non exécutés
Programme avec label - no side-effects

statement i-1;
x := d; //d != f(d)
y := e; //e != g(e)
statement i+2;

S.Bardin Test Logiciel 187/ 198


Analogie entre mutants faibles et labels

Correspondance forte des critères :

Mutation faible ←→ Label


Mutant tué de manière faible ←→ Label couvert
Score de mutation faible ←→ Taux de couverture des labels
Mutants équivalents ←→ Labels non couvrables

MAIS : labels plus facilement automatisables :


réutilisation d’outils de vérification
automatisation efficace

S.Bardin Test Logiciel 188/ 198


Mise en œuvre

Applications visées :
couverture
génération de tests

Méthode
utilisation d’outils en boı̂te noire :
◮ Emma : couverture
◮ PathCrawler : génération de tests
instrumentation du code pour “simuler” les labels

S.Bardin Test Logiciel 189/ 198


Instrumentation naı̈ve

couverture du label p ≡ couverture de la branche True

S.Bardin Test Logiciel 190/ 198


Calcul de couverture

temps de calcul temps de calcul


de couverture des labels VS de couverture des mutants
(avec Emma) (avec MuJava)

↓ ↓

M tests / 1 programme M tests / N programmes

S.Bardin Test Logiciel 191/ 198


Résultats

Pour un jeu de 100 tests

Emma MuJava
programme programme
mutants
initial avec labels
124 LOC
TCas 0,03 s 0,03 s 5s
111 labels
436 LOC
Replace 0,05 s 0,10 s 40 s
607 labels
5400 LOC
Jtopas 3,22 s 11,72 s 1 400 s
610 labels

Gain important (facteur 100) et surcoût raisonnable (facteur 4)

S.Bardin Test Logiciel 192/ 198


Génération de tests

L’instrumentation naı̈ve ne fonctionne pas pour la génération de


tests :
nombre exponentiel de chemins (cf ci après)
les chemins ajoutés sont complexes

S.Bardin Test Logiciel 193/ 198


Idée I : instrumentation plus fine

Chaque chemin d’exécution contient au plus un label

S.Bardin Test Logiciel 194/ 198


Comparaison des 2 instrumentations

S.Bardin Test Logiciel 195/ 198


Idée II : Utilisation incrémentale de PathCrawler

Partitionner l’ensemble des labels du programme P

Lancer PathCrawler (PC) successivement sur P muni d’une


des partitions

Entre chaque exécution de PC, élaguer les labels couverts lors


des générations lancées sur les partitions précédentes

S.Bardin Test Logiciel 196/ 198


Résultats
PC PC i PC i++
91% 100% 100%
50 LOC
Trityp 0s 466 s 1s
141 labels
14 TC 63 TC 84 TC
98% 98% 100%
100 LOC
Replace 2s 1 745 s 50 s
79 labels
121 TC 275 TC 393 TC
94% 96% 100%
124 LOC
TCas 4s 228 767 s 72 s
111 labels
164 TC 249 TC 1 049 TC

PC i (naif) : temps d’exécution trop long ⇒ couverture


non-maximale
PC i++ : couverture maximale + temps raisonnable
PC (no instrumentation) : très rapide + bonne couverture :
méthode hybride ?

S.Bardin Test Logiciel 197/ 198


Conclusion

L’utilisation des labels permet une automatisation efficace du test


de mutations via des techniques concoliques :
critère de couverture fort (mutants faibles)
temps raisonnable
réutilisation de techniques classiques

Une gestion native des labels dans l’algorithme d’exécution


concolique devrait permettre d’améliorer encore les performances
objectif : surcoût de 3x-4x par rapport à la couverture
d’instructions

S.Bardin Test Logiciel 198/ 198

Vous aimerez peut-être aussi