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:
  1. 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.
  2. 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.
  3. 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.
  4. 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
    1. 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.
    2. 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.
    3. 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.
    4. 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.
    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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: Last modified: Thu Jun 18 17:00:00 EEST 2009