Proiectul ``Ballista'' pentru testarea programelor

Mihai Budiu -- mihaib+@cs.cmu.edu
http://www.cs.cmu.edu/~mihaib/

octombrie 2000

Subiect:
o metodologie pentru testarea automată a robusteții software-ului în prezența condițiilor excepționale
Cunoștințe necesare:
cunoștințe elementare de programare
Cuvinte cheie:
software engineering, excepții, robustețe, testare, Ballista, POSIX, Win32


Cuprins




Robustețea programelor

Anul 2000 a fost așteptat cu înfricoșare de o mulțime de oameni; de data asta însă spaimele milenariste au avut o aromă tehnologică: mai ales societatea industrializată era foarte temătoare în fața ravagiilor pe care bug-ul Y2K le va aduce. Miezul nopții a trecut însă fără mare tamtam, și toate temerile de dinainte ni se par acum ridicole.

Adevărul este însă că, din ce în ce mai mult, viața noastră de zi cu zi depinde de calculatoare. Chiar dacă nu vor fi toate vulnerabile dintr-o dată unui virus, malfuncția multora dintre ele poate afecta în mod dramatic existența multora dintre noi, în moduri nebănuite. Rețeaua telefonică digitală, rețelele de televiziune digitală, Internet-ul sunt rețele de calculatoare; rețeaua de distribuție a energiei electrice, controlul de trafic aerian, armata, și cine știe cîte alte părți ale infrastructurii unei țări depind din ce în ce mai mult de calculatoare interconectate.

Oricare dintre dumneavoastră care a scris un program de dimensiuni chiar modeste apreciază dificultatea de a face un program să funcționeze corect în orice circumstanțe. Nici experiența, nici sculele, nici managementul nu pot produce la ora actuală programe fără defecte; singurul lucru la care putem spera este să avem cît mai puține defecte, care să fie cît mai insignifiante și care să se manifeste cît mai rar. De fapt calitatea unui program este direct proporțională cu vechimea sa și cu frecvența de execuție: cu cît a fost executat de mai multe ori cu date de intrare diferite, cu atît șansele ca eventualele bug-uri să se fi manifestat sunt mai mari. Cele mai insidioase defecte rămîn deci în fragmentele de program rar executate, cum ar fi pe bucățile de cod care se execută numai în mod excepțional.

Acest articol va prezenta o metodologie de testare automată a unor programe, încercînd să le supună unor torturi care să le oblige să execute cazuri excepționale. Ideile folosite sunt foarte simple și naturale; aproape că fiecare din noi ar putea zice: ``și eu puteam face așa ceva''. Dar cu toată simplitatea metodei, rezultatele aplicării ei sunt deosebit de interesante și totodată îngrijorătoare: practic nici unul dintre sistemele testate, provenind de la mai mult de 10 fabricanți diferiți, nu a exhibat o robustețe foarte bună. Ca să vă țin treaz interesul, voi amîna discuția rezultatelor calitative pentru o secțiune ulterioară.

Grupul de cercetare care a dezvoltat această metodologie este condus de profesorul Philip Koopman în cadrul Grupului de sisteme software complexe din Departamentului de inginerie electrică și calculatoare al Universității Carnegie Mellon. Numele proiectului este ``Ballista'', care înseamnă în engleză același lucru ca și în română (mai puțin litera L dublată): catapultă. Numele vine din faptul că sistemul de testat este ``bombardat'' cu ``proiectile'' în intenția de a-i găsi defecțiuni. Acest gen de testare se mai numește ``injecție de defecte'' (fault injection). Proiectul Ballista (îl voi scrie în continuare cu numele englezesc) încearcă să cuantifice robustețea unui sistem într-un mod concret, pentru a face posibilă compararea unor sisteme diferite care oferă aceeași funcționalitate. ``Robustețea'' este definită ca fiind ``gradul în care un sistem (software) funcționează corect în prezența unor date excepționale sau a unui mediu stresant''.

Interfețe, modularitate și paranoia

Vom vedea spre final că proiectul Ballista are foarte multe limitări, dar cu toate acestea utilitatea lui este de necontestat. Ballista poate fi utilizat pentru a testa implementarea modulelor software. Modulele software sunt programe (sau biblioteci de funcții) care oferă servicii altor programe prin interfețe bine definite.

Ballista a fost aplicat cu succes pentru a testa următoarele componente software: sisteme de operare din familia Unix, sisteme de operare din familia Windows, componente ORB din arhitecturi CORBA, și biblioteca C standard. În total au fost examinate 15 versiuni majore de Unix, 6 versiuni de Windows și 9 implementări ale componentei ORB (Object Request Broker) din CORBA.

Toate aceste componente au cîteva trăsături comune: toate oferă funcționalitate altor programe. Toate vor fi folosite de programe dezvoltate de alte persoane decît cele care au scris modulele. Toate aceste module trebuie să ofere servicii mai multor programe diferite simultan (mai puțin biblioteca C standard, care de obicei este copiată într-un exemplar diferit pentru fiecare program care-i folosește funcțiile). Aceste module nu au ``încredere'' în programul care le cere serviciile; mai ales sistemele de operare și ORB-ul, pentru că oferă servicii mai multor programe-clienți simultan, nu trebuie să permită nici unuia dintre ei să fure resurse sau să compromită funcționarea întregului sistem în detrimentul altor clienți.

Neîncrederea nucleului sistemului de operare (SO) în cererile programelor care se execută (procesele) trebuie să fie apropiată de paranoia: SO este un program care se execută cu foarte multe privilegii, și care poate scrie oriunde pe disc, trimite orice pachete în rețea, și poate face alte astfel acțiuni cu potențial distructiv. Dacă un proces încearcă să păcălească SO pentru a face acțiuni la care nu are dreptul, SO trebuie să refuze execuția cererii. SO este singurul care gestionează resursele partajate între toate procesele în execuție; acestea trebuie să ceară orice acces la resurse prin intermediul unor apeluri de sistem1.

De exemplu, pentru a deschide un fișier, un proces în Unix trebuie să cheme următorul apel de sistem: open(const char* nume_fisier, int mod). open are două argumente: numele unui fișier și modul în care fișierul va fi accesat (citire, scriere, etc.). Dacă procesul trimite în loc de nume o adresă oarecare din memorie (de exemplu un număr negativ), și dacă nucleul nu verifică faptul că această adresă este ilegală, nucleul ar putea citi zone de memorie care nu există, ceea ce ar putea duce la căderea nucleului și deci a întregii mașini.

Codul fiecărui apel de sistem începe deci prin a face teste amănunțite asupra argumentelor, verificînd dacă valorile lor sunt corecte și au sens. După aceea, SO copiază unele din argumente în interiorul nucleului pentru procesare, după care verifică privilegiile procesului (adică dacă procesul are dreptul de a face operația cerută), și abia la sfîrșit, dacă toate preliminariile au mers bine, execută operația cerută și returnează rezultatele. Dacă argumentele sunt ilegale, SO trebuie să semnaleze cumva acest lucru procesului. În sistemele de operare gen Unix de obicei apelul de sistem returnează un rezultat special (un număr negativ), un cod de eroare. În sistemele de tip Windows, nucleul generează o excepție.

Metodologia de testare Ballista

Ballista folosește modulele pe care le testează ca pe niște ``cutii negre'' (black-box testing), adică nu este interesată de cum sunt construite, ci doar de reacția vizibilă dinafară cînd primesc felurite intrări. Ballista face un lucru foarte simplu: cere serviciile modulului care este testat folosind la intrare valori care nu au sens. Ballista monitorizează apoi comportarea modulului și răspunsurile pe care acesta le dă.

Modulele bine scrise vor trebui sa raporteze erori (fie prin coduri de eroare ca rezultat, fie prin excepții). Ballista verifică codul de eroare și captează toate excepțiile. Problemele mari însă apar cînd modulele nu fac ceea ce ar trebui.

Pentru fiecare serviciu care este testat, Ballista generează cîteva sute sau mii de combinații de parametri ilegali. Balllista apoi crează un proces separat, care execută comanda pentru a cere serviciul, folosind parametrii ilegali. Dacă procesul care execută testul se termină, returnează codul primit; dacă nu, după o vreme procesul master trage concluzia că cel de testare a murit. Rezultatele fiecărui test sunt înregistrate pe disc, pentru a preveni pierderea lor în cazul unei catastrofe.

Ballista este folosită pentru a compara între ele sisteme care implementează aceleași interfețe. De exemplu, toate sistemele de tip Unix trebuie să implementeze funcțiile descrise de standardul POSIX2. Toate sistemele din familia Windows implementează interfața Win32. Astfel putem compara erorile diferitelor sisteme cînd sunt folosite pentru a executa aceleași funcții.

Tipuri de eșecuri

Ballista folosește o clasificare a defecțiunilor într-una din cinci categorii diferite; inițialele acestor categorii în limba engleză formează acronimul CRASH.

Catastrophic
(catastrofe): sunt erorile care corup modulul testat sau cauzează întregul sistem să se blocheze complet sau să se reseteze. Astfel de erori sunt foarte greu de detectat de un program, mai ales dacă se execută pe chiar sistemul afectat. Prezența unei astfel de erori este o eroare severă de proiectare a sistemului.

Restart
(repornire): aceste erori se petrec cînd serviciul chemat nu returnează vreodată o valoare. Calculatorul continuă să funcționeze însă; doar serviciul cerut nu se termină. Astfel de erori sunt detectate folosind un ceas de alarmă care după o vreme de la cererea serviciului oprește Ballista din așteptare.

Abort
(moarte): o astfel de eroare cauzează moartea procesului care cere serviciul, în sisteme Unix de obicei cu un semnal SIGSEGV sau SIGBUS iar în Windows cu o excepție de gen Application Error.

Silent
(silențioase): astfel de erori constau în lipsa raportării unei erori. Deși sistemul ar trebui să raporteze o eroare, el spune de fapt că cererea a fost executată cu succes.

Hindering
(iritante): sunt cele în care sistemul raportează o eroare, dar nu este cea ``reală''. Aceste probleme sunt neplăcute, pentru că dacă cel care a cerut serviciul încearcă să remedieze eroarea, informația primită de la modul nu-i va fi utilă pentru a ghici cauza reală a problemei. Proiectul Ballista nu încearcă să măsoare astfel de eșecuri.

Corect
adaug la scara anterioară (CRASH) și comportarea corectă: sistemul ar trebui să returneze un cod de eroare sau o excepție care să indice exact de ce serviciul cerut nu se poate executa.

Cazuri de test derivate din tipuri de date

Dar cum știm care valori ale argumentelor unei funcții sunt legale și care ilegale? Fiecare funcție are tot felul de parametri diferiți, care codifică obiecte felurite. De exemplu, unele funcții primesc pointeri spre buffere unde datele trebuie depozitate, numere întregi care codifică dimensiunile datelor, numere întregi care codifică descriptori de fișiere deschise, numere ale proceselor pentru a le livra semnale, și cîte și mai cîte.

Cea mai elegantă idee din Ballista este folosită pentru a rezolva această problemă: toate tipurile posibile sunt catalogate în cîteva clase (20 de tipuri de date sunt suficiente pentru a testa toate serviciile UNIX pe toate platformele folosite, și doar unul în plus pentru a testa sistemele din familia Windows).

De exemplu, pentru a testa apelul de sistem Unix, care scrie într-un fișier, trebuie să oferim trei argumente: write(int fisier, const void* date, size_t marime_date).

Deși primul argument este un număr întreg, de fapt acest număr codifică un descriptor de fișier. Ballista are o listă cu valori excepționale pentru un descriptor de fișier; iată un fragment tipic:

Argument Semnificație
FD_CLOSED Fișier care a fost închis
FD_OPEN_READ Fișier deschis doar pentru citire
FD_DELETED Fișier care a fost șters între timp
FD_NOEXIST Un fișier inexistent
FD_EMPTY Fișier gol
FD_PAST_END Fișier la care cursorul poziției curente este după sfîrșit
FD_TERM Fișier care este un terminal
FD_MAXINT Cel mai mare număr întreg
FD_NEG_ONE Minus unu
etc.  

Pentru a testa funcția write, Ballista generează cele trei argumente luînd o valoare pentru fiecare din tabela corespunzătoare. Dacă numărul total de combinații este relativ mic, toate cele posibile sunt încercate. Altfel, 5000 de combinații posibile sunt generate aleator.

În Windows cele mai multe apeluri de sistem au mai mult de patru argumente, deci numărul de combinații este aproape întotdeauna prea mare.

Pentru testarea CORBA, tipurile de argumente sunt organizate într-o ierarhie de tipuri. De exemplu, tipul ``număr întreg'' este un subtip al lui ``caracter''. Astfel tipul ``număr întreg'' moștenește automat toate testele aplicate tipul caracter.

Aceste tabele sunt construite manual de către cei care au implementat sistemul, folosind valori-limită și experiența personală.

Dar cum generăm o valoare ca FD_CLOSED, care codifică un fișier închis? Fiecare valoare are asociate două proceduri: o procedură care construiește valoarea și una care o distruge. Structura unui test Ballista este deci următoarea:

test()
{
        initializari();
        valori = alege_valori_parametri();
        parametri = genereaza_parametri(valori);
        eroare = creaza_subproces(executa_test, parametri);
        exceptie = capteaza_exceptii();
        analizeaza_eroare(eroare, exceptie);
        curata(valori);
}

De exemplu, pentru a genera un parametru cu valoarea FD_CLOSED, care reprezintă un fișier închis, se va invoca o procedură care va crea un fișier, îl va deschide, îl va închide și va da ca rezultat identificatorul obținut la deschiderea fișierului.

După ce testul este terminat trebuie făcută ``curățenie'': de exemplu trebuie să ștergem fișierul care a fost creat pentru a genera FD_CLOSED.

Detecția erorilor prin votare

Dacă luăm toate combinațiile de valori posibile pentru argumente, atunci unele dintre ele vor fi plauzibile (adică nu vor genera o eroare); combinațiile astea nu ne interesează, pentru că nu stresează de fapt sistemul testat. Dar de unde știm care combinații sunt legale și care ilegale? E imposibil să anotăm de mînă fiecare combinație, pentru a le identifica pe cele legale.

Pentru a rezolva această dilemă ne folosim de faptul că supunem testului mai multe sisteme ``echivalente'', care oferă aceeași funcționalitate. Dacă nici unul dintre diferitele sisteme pe care le testăm nu generează vreo eroare pentru o anumită combinație, declarăm acea combinație ca fiind corectă.

O altă problemă este: cum putem detecta erorile ``silențioase'', care raportează OK cînd de fapt a apărut o problemă? O altă euristică este folosită aici: presupunem că dacă cel puțin unul dintre sisteme raportează o eroare, atunci combinația de argumente este ilegală, și celelalte sisteme eșuează în mod silențios.

Din păcate această din urmă metodă s-a dovedit incorectă atunci cînd unele din aceste cazuri au fost examinate manual. Cam 20% din erorile clasificate ca ``silențioase'' erau cauzate de malfuncția unuia dintre sisteme la o combinație corectă! De exemplu, sistemul de operare QNX nu permite fișiere al căror nume conține spații, deci toate funcțiile care încercau să opereze cu astfel de fișiere eșuau, deși standardul POSIX afirmă că acestea sunt perfect legale. O asfel de situație duce la clasificarea (în mod eronat) a acestei combinații ca fiind o eroare silențioasă la toate celelalte sisteme, care nu vor raporta o eroare pentru că funcționează corect.

Erorile ``iritante'' (hindering) sunt foarte greu de clasificat, mai ales pentru că standardele nu specifică clar ce eroare trebuie returnată în fiecare caz. De exemplu, dacă două argumente sunt eronate, care din erori trebuie raportată? Un alt exemplu este cînd încercăm să scriem 0 octeți într-un fișier inexistent: dacă testul de lungime este făcut întîi, funcția va returna succes, pentru că a scris într-adevăr 0 octeți. Dar dacă întîi se descoperă că fișierul este inexistent, funcția ar putea returna o eroare. Standardul nu specifică comportarea corectă pentru astfel de cazuri.

Rezultate

În fine, ajungem la secțiunea cea mai suculentă: cît de bune sunt feluritele sisteme de operare? Care e mai rezilient? Sunt sistemele folosite în aplicații importante mai robuste?

Răspunsurile sunt extrem de surprinzătoare: deși robustețea variază de la sistem la sistem, nu există nici unul foarte bun, și cam toate sunt la fel de vulnerabile. Iată rezultatele concrete pentru fiecare familie de sisteme testate.

POSIX

Testul sistemelor Unix verifică 233 de funcții specificate de standardul POSIX (Portable Operating System unIX). Au fost testate 15 sisteme de operare din familia Unix.

Figura 1: Robustețea sistemelor POSIX: rata de eroare normalizată pentru 233 de apeluri de sistem. Numai erorile catastrofale, abort și restart sunt contabilizate.
\begin{figure}\centerline{\epsfxsize=12cm\epsffile{posix.eps}}\end{figure}

Figura 1 ilustrează rata de eroare normalizată pentru 15 sisteme de operare comerciale din familia Unix, unele fiind versiuni diferite ale aceluiași sistem. Fiecare valoare este calculată în felul următor: se numără toate apelurile executate cu cel puțin un parametru ilegal. Apoi se numără cîte din acestea au dus la: distrugerea sistemului (catastrofă), blocarea procesului (restart) și la moartea procesului (abort). Se face apoi raportul dintre al doilea și primul număr. Rezultatul îl vedeți în figura 1. Cu cît barele sunt mai scurte, cu atît sistemul e mai bun, adică depistează mai multe erori în mod civilizat.

Tabela următoare indică fabricanții fiecăruia dintre sisteme; după cum vedeți o grămadă de lume bună: cele mai respectabile companii de software din lumea Unix sunt toate reprezentate.

Nume Fabricant
AIX IBM
FreeBSD free (bazat pe Berkeley Software Distribution)
HP-UX Hewlet Packard
Irix Silicon Graphics
Linux Linus Torvalds (free)
Lynx Lynux Works
NetBSD free (bazat pe Berkeley Software Distribution)
OSF/1 Digital (acum numit Compaq Tru64)
QNX QNX Software Systems
SunOs Sun
Solaris Sun

Putem face imediat o mulțime de observații interesante:

Figura 2: Un apel de sistem trece întîi prin biblioteca standard, care prelucrează argumentele și abia apoi invocă apelul de sistem propriu-zis al sistemului de operare.
\begin{figure}\centerline{\epsfxsize=7cm\epsffile{apel.eps}}\end{figure}

Dacă sunteți familiar cu modul în care sunt implementate apelurile de sistem, știți că programele scrise de dumneavoastră nu fac direct astfel de apeluri (vedeți și figura 2). Ele cheamă niște funcții dintr-o bibliotecă ``standard'', care fac ele însele tot felul de verificări, împachetează argumentele apelului de sistem și apoi execută apelul real.

Figura 3 arată care din erorile din figura 1 pot fi atribuite bibliotecii standard și care sunt adevărate slăbiciuni ale sistemului de operare. Observați că la majoritatea sistemelor cele mai multe slăbiciuni sunt de fapt în bibliotecă (deci sistemul de operare este ceva mai robust decît părea); excepția notabilă este din nou QNX, la care cele două bare sunt aproximativ egale.

Figura 3: Erori atribuibile bibliotecii C și apelurilor de sistem (rată de eroare normalizată).
\begin{figure}\centerline{\epsfxsize=12cm\epsffile{biblioteca.eps}}\end{figure}

În fine, figura 4 încearcă să califice erorile după tipul de operație executată. Rata de eșec este calculată separat pentru fiecare categorie de funcții.

Figura 4: Erori după fiecare tip de apel de sistem pentru sistemele POSIX. (Barele care nu se văd la ``ceasuri'' sunt foarte mici.)
\begin{figure}\centerline{\epsfxsize=14cm\epsffile{posix-functii.eps}}\end{figure}

Și această figură ne prilejuiește niște observații interesante:

Figura 5 este construită folosind metodele de ``votare'' descrise mai sus pentru:

Figura: Folosind tehnica votării au fost eliminate combinațiile de parametri care nu constituie erori. Apoi au fost detectat erorile silențioase, după cum explicăm în secțiunea despre votare. Pentru că sunt mai puține cazuri de bază la care ne raportăm, procentele de eroare sunt mai mari ca în figura 1. În plus aici arătăm erorile silențioase, care se adaugă la cele din acea figură.
\begin{figure}\centerline{\epsfxsize=12cm\epsffile{silent.eps}}\end{figure}

Acum avem o mșură mai exactă a robusteții fiecărui sistem; vedem că AIX era într-adevăr avantajat de faptul că transforma unele erori de tip abort în erori silențioase. Cele mai bune sistem după acest grafic sunt Solaris și Irix 6.2.

Win32

Să trecem acum la datele privitoare la sistemele firmei Microsoft. Se spune că Windows este un sistem foarte puțin fiabil, care ``crapă'' regulat, și care trebuie rebootat zilnic pentru a rămîne ``sănătos''. Se mai spune apoi că Windows NT și succesorul lui, Windows 2000 sunt mult mai robuste. De asemenea, lumea afirmă că Linux este mai fiabil. În ce măsură vor confirma datele aceste presupuneri?

În primul rînd, pentru a putea face o comparație relativ echitabilă între Windows și Linux, au fost testate din interfața Win32 numai o parte din funcții, care au echivalente aproximative în sistemele gen POSIX. De pildă, nici una dintre funcțiile care operează cu grafică nu face parte din acest test. Au fost alese 237 de funcții Win32, care sunt comparate cu 183 de apeluri de sistem Linux. Din cauză că erorile sunt raportate ca procente, diferența de număr nu e o problemă.

Iată sistemele comparate:

Figura 6: Funcții cu erori catastrofice în Windows și Linux.
\begin{figure}\centerline{\epsfxsize=10cm\epsffile{win-crash.eps}}\end{figure}

În figura 6 vedem numărul de funcții care apelate cu argumente incorecte pot face sistemul să eșueze în mod catastrofic. Într-adevăr, Linux, NT și Windows 2000 nu pot fi paradite chiar așa de ușor, dar celelalte sisteme sunt mult mai vulnerabile. Windows CE în particular este foarte vulnerabil.

Figura 7 arată doar apelurile de sistem (excluzînd biblioteca standard), pentru sistemele Windows și referința Linux. Observați că aproape întotdeauna Linux este mai robust decît toate celelalte sisteme, cu excepția managementului proceselor. Surprinzător în această figură este că NT și Windows 2000 au printre cele mai proaste rezultate (cele mai proaste la 4 categorii).

Figura 7: Procente de eșecuri în apeluri de sistem pentru sisteme Windows + Linux.
\begin{figure}\centerline{\epsfxsize=12cm\epsffile{win-rata.eps}}\end{figure}

Dacă ne uităm însă în figura 8 la erorile datorate doar bibliotecii standard, atunci Linux trece cam pe ultimul loc. Nu toate funcțiile de bibliotecă sunt implementate în Windows CE, așa că X-uri în figură indică absența unei categorii, și nu 0 erori. Nici de data aceasta NT sau Windows 2000 nu se detașează clar.

Figura 8: Procente de eșecuri generate de biblioteca C standard. Un ``x'' înseamnă că acel sistem nu avea astfel de funcții, deci nu a fost testat.
\begin{figure}\centerline{\epsfxsize=12cm\epsffile{win-biblioteca.eps}}\end{figure}

Ce înseamnă aceste numere? Vom discuta despre limitările măsurătorilor Ballista un pic mai jos, dar se cuvine menționat că toate aceste măsurători fac teste foarte simple. Faptul că Windows NT nu are nici o eroare catastrofală nu înseamnă că nu se pot întîlni astfel de erori, ci că programe de cîteva linii nu au găsit niciuna.

CORBA:

Din cauză că articolul de față este deja cam dens, voi sări peste măsurătorile sistemelor CORBA. Demn de spus este că sunt comparabile în calitate cu sistemele de operare prezentate mai sus.

Creșterea robusteții prin împachetare

Defecțiunile găsite sugerează și o soluție practică: implementarea unor funcții care împachetează pe cele defecte (wrappers), care sunt scrise cu grijă și testează toate argumentele înainte de a chema funcția reală.

Pentru sistemele CORBA a fost chiar elaborată o metodologie semi-automată de protecție împotriva erorilor: înainte de a trimite o cerere la distanță, crează un nou fir de execuție (thread) care trimite cererea. Dacă firul de execuție moare sau se blochează, programul principal folosește o alarmă pentru a continua execuția și pentru a raporta o eroare. În felul acesta, erori de implementare în biblioteci sau în modulul chemat nu afectează clientul, care-și poate continua execuția.

Limitări ale metodologiei Ballista

Am văzut că, deși bazată pe o idee foarte simplă, Ballista este surprinzător de eficace în a dezgropa o sumedenie de erori în sisteme care sunt extrem de folosite în viața de zi cu zi. Dacă reparăm defecțiunile descoperite de Ballista am rezolvat problema corectitudinii programelor?

Nu, deloc. Ballista descoperă numai cele mai simple dintre erori, care sunt cauzate de programe foarte mici. Ballista este neputincioasă în a găsi de pildă erori cauzate de încărcarea mare a sistemului (cînd multă memorie este ocupată sau cînd o mulțime de programe așteaptă o felie de timp a procesorului).

Ballista nu găsește erori care depind de starea internă a sistemului. De pildă, dacă un program nu dealocă memoria pe care nu o mai folosește, spunem că are scurgeri de memorie (memory leaks). Scurgerile de memorie nu influențează corectitudinea comportării programului, dar dacă programul se execută pentru mult timp, acaparează prea multă memorie pentru sine, ceea ce împiedică execuția celorlalte programe. În momentul în care memoria disponibilă este epuizată se pot produce accidente. Astfel de erori ar fi captate prin repetarea comenzii care cauzează scurgeri, dar Ballista execută fiecare comandă doar o dată cu un set de parametri.

De asemenea, Ballista nu găsește erori care survin din interacțiunea a multiple componente: fiecare test evaluează o singură funcție din interfață. Dacă apelul a mai multe funcții într-o anumită ordine ar putea duce la blocare (deadlock), Ballista nu va găsi acest gen de defecțiune.

În fine, cea mai mare limitare a lui Ballista provine din faptul că testează sistemele ca pe niște cutii negre: nu știe de fapt care ar trebui să fie răspunsul corect. Ballista se uită doar la condițiile care trebuie să genereze erori, dar este complet neinteresată de modul în care programul funcționează cînd poate să-și facă treaba.

Concluzii

Chiar dacă am încheiat cu o listă a limitărilor acestui proiect, nu trebuie să-i neglijăm meritele: am văzut în acest articol o metodologie foarte simplă de a testa robustețea sistemelor în fața condițiilor excepționale. Atunci cînd testează sisteme care implementează funcționalități asemănătoare, Ballista poate fi folosită pentru a compara cantitativ mai multe sisteme distincte, folosind rata normalizată de eșec.

Pentru că orice sistem de operare trebuie să ofere anumite funcții elementare, Ballista poate fi folosită chiar pentru a pune față-n față sisteme atît de diferite ca Windows și Linux.

Poate cel mai important dintre mesajele acestui text este că software-ul este mereu plin de găuri, și că aparent este foarte greu să produci soft de o calitate foarte ridicată: toate sistemele testate, de la mai mult de 10 fabricanți diferiți, sunt aproximativ la fel de puțin robuste.

Alte surse de informație

Pagina de web a proiectului Ballista este foarte bine asortată: http://www.ece.cmu.edu/ballista. Graficele și informațiile au fost extrase din articole și prezentări din această pagină.

În jurul lunii decembrie proiectul Ballista va face disponibil pe web un sistem de test automat, prin care puteți genera cod pentru testare de la distanță; adresa este http://ballista.ece.cmu.edu/test/.

Proiectul ``Fuzz'' de la universitatea Wisconsin a aplicat teste asemănătoare unor utilitare Unix. Două rapoarte ale lor sunt disponibile de la ftp://grilled.cs.wisc.edu/technical_papers/fuzz.ps.Z și ftp://grilled.cs.wisc.edu/technical_papers/fuzz-revisited.ps.Z. Acestea arată că programele dezvoltate open-source sunt în general mai robuste decît cele comerciale.

Proiectul ``CRASHME'' încearcă să ``bușească'' un sistem de operare generînd cod la întîmplare (folosind numere aleatoare) și punînd sistemul să-l execute: http://people.delphi.com/gjc/crashme.html



Note

... sistem1
Despre rolul nucleului unui sistem de operare, modul în care funcționează, și ce sunt apelurile de sistem și vedeți și articolul meu din PC Report din decembrie 1996, disponibil din pagina mea de web.
... POSIX2
Vedeți și articolul meu despre istoria sistemului de operare UNIX din BYTE din august 1996.