Proiecte: testare, analiză și depanare de software
Caut să lucrez la aceste proiecte pe termen lung cu studenți
începând din anii mici, sau eventual diplomanzi. Pe subiecte mai
complexe pot lucra și cu masteranzi începând din primul an.
Principalele condiții necesare sunt interesul, buna înțelegere
a problemei abordate și disponibilitatea de timp.
Pentru practica de vară, teme posibile sunt studii și implementări
preliminare pentru proiectele de mai jos, sau proiecte de dimensiuni mai
mici legate de programe și infrastructuri existente de testarea, analiză
și depanare de software, pentru a învăța folosirea lor, a le evalua pe
programe tipice, sau a scrie module de extensie.
Generare automată de teste
O metodă de a evalua cât de bine e testat un program e de a măsura
acoperirea codului prin teste. Criteriile cele mai simple
(acoperirea fiecărei linii și a fiecărei ramificații)
sunt insuficient de puternice pentru detectarea multor tipuri de erori.
E necesară deci:
- folosirea unor criterii de acoperire mai precise, care reflectă mai
bine funcționalitatea codului analizat
- generarea automată a unor teste cât mai relevante, care să
maximizeze acoperirea după criteriile selectate
-
Modul Visual Studio pentru analiza acoperirii după fluxul de date
(dataflow coverage) în programe C#.
Acest criteriu de acoperire corelează utilizările unei
variabile cu locurile anterioare de definiție (atribuire)
care dau posibilele ei valori (vezi un
articol
care studiază problema).
Scopul e de a măsura calitatea acoperirii acestui criteriu, și, în pasul
următor, a genera automat teste care acoperă căile încă netestate. Un
proiect de licență anterior a tratat această problemă pentru Java
folosind sistemul de verificare
Java PathFinder.
Alt proiect a măsurat acoperirea liniilor de cod și ramificațiilor
folosind infrastructura de analiză
Phoenix de
la Microsoft.
-
Testare cu acoperire bazată pe predicate (limbaj la alegere: Java, C#, C, ...)
O bună suită de teste parcurge programul testat pe toate ramurile.
Însă ramificațiile unui program nu sunt independente: comportamentul
pe o ramură poate depinde de calea aleasă într-o porțiune anterioară
a codului (care a setat un fanion sau a făcut alte atribuiri
relevante). Noțiunea de acoperire bazată pe predicate
(predicate coverage)
exprimă acest aspect, evaluând la fiecare punct în program corelarea
dintre condiții (predicate) considerate relevante. Proiectul va evalua
acoperirea unei suite de teste, în raport cu predicate definite de
utilizator sau selectate prin analiza codului, și va genera teste
suplimentare pentru cazurile neacoperite.
- Testarea bazată pe metrici de corelare obiectuală
Pentru programe și biblioteci orientate pe obiecte, testele conțin
secvențe de apeluri de metode. Cum numărul de secvențe posibile crește
exponențial cu numărul de apeluri, e important ca ele să fie alese
judicios. Similar cu acoperirea bazată pe flux de date, proiectul va
defini criterii de generare a testelor bazate pe corelarea între metode
(de ex. combinarea apelurilor la metode tip getter cu cele de tip setter
pentru aceiași membri de date; gruparea în teste a metodelor care
interacționează, etc.
-
Testarea ierarhiilor de clase
Programele orientate pe obiecte pot
avea tipuri de erori specifice, datorate polimorfismului (apelul unei
metode din altă clasă decât se intenționează), sau cuplajului între
metode care accesează aceiași membri de date. Pornind de la
modele
de eroare cunoscute, proiectul va continua analiza lor și va genera
suite de teste adaptate la detectarea acestor tipuri de erori.
Execuție simbolică
Când raționăm despre diversele cazuri în execuția
unui program, adesea nu ne interesează valorile concrete ale unor
variabile, ci doar relațiile în care se află (avem x - y
> 0 ? sau a[i] <= a[j] ?). Astfel, nu executăm
programul pe valori numerice concrete, ci substituim simboluri și
expresii, ca în matematică, pâstrând pe parcurs
relațiile relevante între acestea. Execuția
Execuție simbolică cu Java PathFinder pentru tipuri de date complexe
Pentru tipuri de date numerice, execuția simbolică e relativ
simplă conceptual, și necesită prelucrarea de relații
(ecuații, inecuații) matematice. Mai dificilă e implementarea
unor abstracții potrivite pentru tipuri de date complexe: șiruri
de caractere, tablouri, obiecte care conțin la rândul lor obiecte.
Se va extinde la tipuri de date mai complexe execuția simbolică
din Java PathFinder,
unul din cele mai cunoscute sisteme de verificare pentru programe Java,
care implementează o mașină virtuală proprie pentru Java.
Execuție simbolică înapoi cu Java PathFinder
Sistemul Java Pathfinder implementează execuția simbolică
înainte, pornind de la un apel de metodă specificat de utilizator.
De interes e însă și execuția simbolică înapoi, de la
un punct relevant (de exemplu, de eroare) în care a ajuns programul,
pentru a obține condițiile care au determinat atingerea acelui
punct. Execuția simbolică înapoi poate fi folosită apoi la
depanarea programelor.
Evaluarea sistemului Klee de detectie de erori prin execuție simbolică
Klee e un sistem recent care
detectează erori în programe prin execuție simbolică
direcționată fie aleator, fie bazat pe diverse criterii (vezi și acest
articol).
Scopul proiectului e de a evalua performanța detecției erorilor pe programe
de dimensiuni realiste, și de a propune extensii și optimizări.
Depanare automată
Odată detectată o execuție eronată în program, e
importantă localizarea cu un grad cât mai mare de automatizare a
cauzei erorii, și corectarea ei. Diverse abordări de succes se
bazează fie pe comparația directă între o execuție corectă și una eronată (delta debugging), sau analiză statistică a unui număr mare de execuții
- Depanare ierarhică și bazată pe componente
O abordare naturală pentru localizarea erorilor este cea ierarhică,
privind execuția programului la diverse nivele de detalii (v. un
articol)
relevant. Asemănător, putem aborda depanarea într-un sistem format
din componente, efectuând întâi întâi interacțiunile
la interfața dintre componente, pentru a detecta eventualele erori de
interacțiune -- sau, în absența acestora, a localiza eroarea în
interiorul uneia din componente.
- Depanare prin analiză statistică a execuțiilor
Analiza statistică a unui număr mare de execuții poate da
indicii asupra locurilor din cod suspecte de eroare (v. următorul
articol). Proiectul va analiza o bază de programe benchmark cu erori
cunoscute, urmând ca pe baza rezultatelor să continue dezvoltarea
unei metode de localizare a erorilor, posibil bazat pe învățarea
unui model de funcționare corectă/eronată a programului.
- Depanare pornind de la structura fișierelor prelucrate
O categorie de programe sunt cele care prelucrează fișiere, de exemplu documente în diverse formate, pentru vizualizare. Proiectul își propune să ușureze depanarea erorilor din acest tip de programe abordând problema în pornind de la structura (cunoscută sau dedusă prin analiză) a fișierelor prelucrate, și corelând execuția programului cu prelucrarea realizată în fișier.
- Depanarea interacțiunilor între programe complexe
Ați întâlnit probabil situații în care un program (de exemplu un editor de texte, sau un browser) se termină printr-o eroare, provocând în lanț o eroare fatală în mediul grafic de rulare (server X/window manager). Proiectul va aborda localizarea erorii, prin monitorizarea interacțiunii între programe.
Analiză statică
Proiectele din această categorie urmăresc detectarea unor erori sau extragerea unor informații despre funcționarea programului prin analiza codului sursă, fără rularea efectivă a programului.
- Detectarea pierderilor de memorie
Un proiect anterior detectează pierderi de memorie în programe C/C++
folosind infrastructura LLVM. Analiza
este însă aproximativă, găsind potențiale căi de execuție eronate (pe care
nu se face o dealocare sau se face o dublă dealocare), dar nu verifică
dacă programul poate într-adevăr urma calea găsită. Pentru
aceasta, trebuie reținute condițiile la fiecare ramificare de pe cale,
și verificat dacă ele pot fi adevărate simultan. Se va folosi
un modul deja existent de reprezentare a condițiilor, și biblioteci
care verifică dacă ele pot fi adevărate. Analiza trebuie să fie
interprocedurală, adică să analizeze programul în
totalitate, ținând cont de efectele care apar la apelurile de
proceduri prin transmiterea de parametri și returnarea de rezultate.
- Determinarea sumarelor și contractelor la nivel de procedură
Cum programele sunt modulare, analiza corectitudinii lor trebuie să țină cont de aceasta. Informații despre cum se leagă rezultatele funcțiilor de parametri acestora sunt necesare, fie că e vorba de detectarea de pierderi de memorie sau de accese invalide, etc. Proiectul va implementa și eventual extinde algoritmi care calculează astfel de relații care rezumă efectul unei funcții (procedure summaries), în scopul folosirii în analize interprocedurale.
- Analiza acceselor la tablouri pentru paralelizare
Pentru execuția performantă a codului, parcurgerile tablouri de
dimensiuni mari se pot paraleliza, alocând un fragment de tablou
fiecărei unități de procesare. Pentru ca transformarea să fie
corectă, la prelucrare nu trebuie să existe interferențe între
fragmente: iterația trebuie să avanseze uniform de la un element
a altul, iar elementele să nu aibă zone de memorie comune.
Folosind o infrastructură de analiză pentru Java (de exemplu
Wala de la IBM, se va realiza
o analiză care identifică astfel de prelucrări "bine structurate"
într-un program.
- Determinarea de invarianți pentru cicluri
Scrierea corectă a programelor este dificilă datorită
ciclurilor, unde pot apărea ușor erori. Invarianții,
proprietăți care se păstrează la fiecare iterație au un
rol cheie în verificarea programelor. Pentru diverse tipuri de
cicluri simple se pot stabili matematic invarianți, și se pot
determina formule pentru calculul valorilor la sfârșitul ciclului.
Infrastructura LLVM implementează
astfel de algoritmi în modulul ScalarEvolution. Proiectul va
extinde acest modul cu alte tipuri de invarianți polinomiali
tratați în literatură, pentru a permite verificarea unor
programe mai complexe.
- Simplificarea programelor prin slicing
La analiza unui program -- pentru depanare, înțelegere sau
testare, dorim să urmărim doar porțiunile relevante. Aceasta se
realizează prin slicing, determinarea acelor
instrucțiuni din program care vor fi afectate de valoarea din punctul
curent (forward slicing) sau reciproc, a celor care ar fi putut cauza
valoarea curentă (backward slicing). În ciuda utilității,
există puține infrastructuri public disponibile pentru slicing.
Scopul proiectului e de a realiza un modul de slicing pentru o
infrastructură existentă de analiză de cod (C sau Java), pentru
a putea fi apoi folosită la depanare.
Infrastructuri de analiză
O listă de infrastructuri reprezentative pentru analiză, detecție de erori și testare, care pot fi folosite în aceste proiecte sau altele similare:
- CIL: infrastructură de analiză pentru C, scrisă în ML (dialectul OCaml, funcțional/orientat pe obiecte), cu multe facilități pentru scrierea de analize la nivel înalt
- LLVM, infrastructură de compilare și analiză pentru C/C++ bazată pe o reprezentare de nivel scăzut, cu maturitate și performanțe la nivel industrial.
- Java PathFinder. Sistem de verificare bazat pe o mașină virtuală Java proprie.
- Soot o infrastructură de analiză pentru bytecode Java
- Wala o infrastructură de analiză pentru bytecode Java, dezvoltată la IBM
- Key, un sistem pentru demonstrarea corectitudinii programelor Java
- Why, un alt sistem de verificare deductivă de programe C sau Java
- Phoenix, o infrastructură de analiză și compilare pentru platforma .NET (Microsoft)
- PEX, o infrastructură de generare automată de teste pentru platforma .NET (Microsoft)
- CHESS, un sistem de detectare a erorilor in programe concurente (Microsoft)
Last modified: Thu Jun 18 17:00:00 EEST 2009