Testing e Debugging del Software
Michelangelo Diligenti Ingegneria Informatica e
dell'Informazione
diligmic@dii.unisi.it
Sommario
●
Testing
La correttezza del software
Tipi di test
Esercizi con libreria gtest
●
Debugging
Tecniche
Strumenti
gdb
valgrind
Esercizi
Testing di correttezza
●
Analisi di correttezza di un modulo o intero sistema
Esecuzione di singoli casi (test case)
Confronto risultato con valore atteso
●
Esecuzione non corretta risulta in failure
Permette di scoprire gli errori (bugs/defect/fault)
Porta il software ad un livello di qualità accettabile
Non vuol dire perfezione
La correttezza del software
●
Assiomi del testing
Non possibile testare un programma in modo completo
Dimostrare correttezza vuol dire testare tutte le coppie input/output
Input space troppo ampio
Stato interno dei programmi ampio
Numero di possibili esecuzioni enorme
Specifiche sono spesso interpretabili in modo soggettivo
La correttezza del software
●
Esempio, dimostrare correttezza di
int func(int x, int y) { return x + y; }
Necessario testare tutte le combinazioni (x,y)
Se un int è 4 byte
232 * 232 = 264 casi da testare!
La correttezza del software
●
Se si testano solo alcuni input, spesso non possibile testare tutti i cammini (path) che segue il codice
int func(int x, int y) {
for (int i = 0; i < n; ++i) if (a[i] == b[i/2])
a[i] += 100;
else
b[i] /= 2;
}
a e b vettori di dimensione 100
2 path ad ogni loop, ma il path scelto influenza i path successivi
Numero path possibili sull'intera esecuzione è 2n
La correttezza del software
●
Software testing è un processo risk-based
Più tests ci sono più probabile che il software sia corretto
Non possibile provare correttezza
Possibile ci siano altri bug
Più bug sono stati trovati, più probabile ce ne siano altri
In generale, necessario trovare un compromesso tra costo del testing e benefici attesi
La correttezza del software
●
Software testers
Spesso non amati come componenti dei teams
Chi programma pensa che blocchino sviluppo e creatività
Non vero!
Testing richiede creatività e professionalità, oltre capacità ben specifiche
Senza competenze specifiche impossibile creare test efficaci nel scovare bugs
Tipi di test
●
Unit testing: test di singoli moduli software (spesso una singola classe)
Black box testing: basati sulle specifiche. In caso di classi testando l'interfaccia pubblica della classe
White box testing: basati sulla logica interna. Il test è friend del test
Gray box testing: una mistura dei precedenti
●
Integration testing: test dell'integrazione tra moduli
●
System testing: test a livello di sistema
Tipi di test
●
Performance testing: test del tempo e risorse necessarie per svolgere un certo task
●
Stress testing: test nel caso di chiamate ripetute o concorrenti ad un certo task
Fondamentale per codice che gira su servers
●
Regression testing: controlla che un cambiamento nel codice non introduce nuovi bug o problemi
Spesso si basa su unittesting
Ma può essere necessario aggiungere integration e performance testing
Unittesting
●
Meccanismo di basso livello ma fondamentale per il successo del testing
Importante che ogni modulo e classe abbia test associato
Basato su un insieme di test cases
Primo passo per realizzare Regression Testing
Disponibili librerie per supportare l'implementazione ed esecuzione degli unittest
Dette Test Management Libraries
Le studieremo
Integration e System testing
●
Realizzabile bottom-up
Si testano i singoli moduli con unittesting e poi gruppi di moduli sempre più grandi fino all'intero sistema
●
Top-down
Si testa l'intero sistema e poi gruppi di moduli fino ai singoli moduli con unittesting
Test nello sviluppo del software
●
Test processo complesso che va dal design, all'implementazione ed all'esecuzione
●
Test sono eseguiti durante l'implementazione di una classe/modulo per controllare stato
Sempre prima di effettuare svn commit!
Test Design Test
Implementation Test
Execution Results Verification
Test Management Library
Test ed automazione
●
Consigliabile automatizzare l'esecuzione dei test
Eseguiti in modo regolare automaticamente per controllare stato del codice nel repository
Possibile anche automatizzare eseguzione test in svn, evita commit di codice che rompe i test
Test Design Test
Implementation Test
Execution Results Verification
Automatizzazione
Test Management Library
gtest
●
Libreria open-source in C++ inizialmente realizzata da Google
Detta googletest o gtest
Scaricabile da
http://code.google.com/p/googletest/
Utilizzabile liberamente in ogni contesto (anche industriale)
Rende facile la creazione di test ed il loro monitoraggio
gtest
●
Google Test si basa sul concetto di assertion, controllo che una condizione sia verificata
Assertion può essere fatale (fatal), se blocca
esecuzione del test o non fatale se il test continua
Se possibile, usare le non fatali, il programma continua e si ottiene sommario finale dell'andamento del test
●
Test è un insieme di assertions
●
Test case è un gruppo di test che condividono strutture dati e concetti
●
Programma di test contiene più test cases
gtest e test case
●
Test è una funzione
TEST(NomeTestCase, NomeTest) { … }
●
Più test associati a stesso test case, formano un test case
TEST(NomeTestCase, NomeTest1) { … } TEST(NomeTestCase, NomeTest2) { … }
…
TEST(NomeTestCase, NomeTestN) { … }
●
Intero programma è test program
gtest e assertions
●
Vi sono tanti modi di scrivere assertions, prendono due argomenti che devono rispettare la condizione
Fatali
ASSERT_TRUE(bool);
ASSERT_FALSE(bool);
Non fatali
EXPECT_TRUE(bool);
EXPECT_FALSE(bool);
Esempio
ASSERT_TRUE(ptr != NULL);
gtest e assertions
●
Vi sono tanti modi di scrivere assertions, prendono due argomenti che devono rispettare la condizione
Fatali
ASSERT_EQ(arg1, arg2)
ASSERT_GT(arg1, arg2)
ASSERT_GE(arg1, arg2)
ASSERT_NE(arg1, arg2)
Non fatali
EXPECT_EQ(arg1, arg2)
EXPECT_GT(arg1, arg2)
EXPECT_GE(arg1, arg2)
EXPECT_NE(arg1, arg2)
gtest e main
●
Il main del test deve chiamare le funzioni
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
●
La seconda funzione esegue tutti i test
gtest: esempio Test
TEST(IntegerTest, OperatorIncrement) { Integer i(5);
++i;
EXPECT_EQ(6, i.Get());
}
●
Vediamo ora l'implementazione completa di un test:
operator_test.cc
gtest: white box testing
●
White box testing richiede che test sia friend
●
gtest permette questo nel seguente modo
nella definizione della classe testata
#include "gtest/gtest_prod.h" // necessario includere gtest!
class ClasseDaTestare {
FRIEND_TEST(NomeTestCase, NomeTest);
… /* implementazione classe */
};
nel test
TEST(NomeTestCase, NomeTest) { ClasseDaTestare c;
EXPECT_EQ(0, c.a); // a dato privato di c
gtest e classi
●
Talvolta ci sono operazioni da fare all'inizio e fine di ogni test
Ripeterle ogni volta poco elegante
Se non fatte si rischia che il test non sia valido
gtest permette di definire queste operazioni in modo consistente
Test Fixture: classe figlia di ::testing::Test;
Contiene oggetti che si vuole usare per composizione
Implementare metodo SetUp per definire cosa fare ad inizio test
Implementare metodo TearDown per definire cosa fare a fine test
Chiamare TEST_F() per fare test usando Fixture
gtest e Test Fixture: esempio
class IntegerTestWithFixture : public ::testing::Test { protected:
Integer i;
virtual void SetUp() { // chiamato prima di ogni test i = Integer(5);
}
// virtual void TearDown() {} // chiamato dopo ogni test };// Lista di singoli Test
TEST_F(IntegerTestWithFixture, Constructors) { EXPECT_EQ(5, i.Get());
}TEST_F(IntegerTestWithFixture, OperatorEqual) { Integer j(0);
j = i;
EXPECT_EQ(5, j.Get());
gtest e Test Fixture
●
Anche con Fixture possibile dichiarare i singoli test friend nella classe da testare
●
Oppure dichiarare l'intera classe come friend
class Integer {
friend class IntegerTestWithFixture;
// Implementazione della classe }
gtest e classi
●
Talvolta ci sono operazioni da fare all'inizio e fine di ogni test
Ripeterle ogni volta poco elegante
Se non fatte si rischia che il test non sia valido
gtest permette di definire queste operazioni in modo consistente
Test Fixture: classe figlia di ::testing::Test;
Contiene oggetti che si vuole usare per composizione
Implementare metodo SetUp per definire cosa fare ad inizio test
Implementare metodo TearDown per definire cosa fare a fine test
gtest e classi: esercizi
●
Esercizio 1: testare la classe Integer attraverso gtest (lo facciamo insieme)
●
Esercizio 2: testare la classe Integer attraverso gtest con Fixture (lo facciamo insieme)
●
Esercizio 3: testare una classe Lista
●
Esercizio 4: testa la classe Matrix
Debugging
●
OK, avete trovato un bug, ed adesso?
Se i test sono ben congeniati, spesso accade attraverso un test
●
Talvolta la sorgente del bug è evidente
●
Talvolta è necessario debugging del codice
Analisi dettagliata del funzionamento del codice
●
Trovare la sorgente degli errori in generale (in C++)
in particolare, può essere difficile
Debugging
●
I bug sono di vario tipo
INCRT: il codice non genera uscita desiderata per alcuni input
Analisi del flusso del codice con print di debug
Analisi del flusso del codice con debugger
MEMFLT: il codice genera un fault di memoria
Necessario trovare l'errore con debugger
Utile usare metodi di analisi degli accessi alla memoria
NONDET: il codice non ha uscita deterministica
Spesso causati da utiizzo di memoria non inizializzata (simili al tipo 2)
Utile usare metodi di analisi degli accessi alla memoria
Debugging e print dello stato
●
Il primo e sempre valido metodo per il debugging di bug INCRT
Spesso metodo semplice è il più efficace
Consiglio di usare il preprocessore, esempio:
#if DEBUG > 1
cerr << “Variabile pippo:” << pippo;
#endif
●
Settare la variabile con opzione -DDEBUG NUM
Debugging con gdb
●
GDB: GNU debugger, debugger open-source per sistemi UNIX
●
Metodo più evoluto per il debugging di bug INCRT
ATTENZIONE: compilare con l'opzione -g del g++
perché il debugging sia leggibile
●
Permette di eseguire un programma passo-passo
Mentre si monitora lo stato di qualsiasi variabile
●
Possibile settare breakpoints
breakpoint: punto del codice in cui l'esecuzione deve fermarsi per poi reiniziarla
Debugging con gdb
●
Eseguire un programma con gdb
gdb nome_binario
Esce prompt dei comandi (gdb)
●
Settaggio della linea di comando
(gdb) set args opzioni_da_linea_di_comando
●
Settaggio breakpoint
(gdb) break nome_file.cc:numero_linea
Esempio
(gdb) break operator_streams.cc:10
Debugging con gdb
●
Esecuzione (si ferma a breakpoint)
(gdb) run
Se ridigitato si reinizia l'esecuzione dall'inizio
●
Esecuzione fino a punto specificato
(gdb) until nome_file.cc:numero_linea
Esempio
(gdb) until operator_streams.cc:20
Debugging con gdb
●
Esecuzione passo-passo (esegue 1 riga di codice)
Esegue prossima riga nel flusso del programma
Non necessariamente la riga successiva sul file.cc
Se chiamata funzione, entra in funzione (gdb) step
●
Stampa dello stack (per vedere catena chiamata funzioni nel punto attuale)
(gdb) backtrace
Possibile avanzare in alto o basso sullo stack (gdb) up (gdb) down
Debugging con gdb
●
Print di una variabile
(gdb) print nome_variabile
Esempio print el
●
Display di una variabile
Display è un print permanente
La variabile è stampata sullo schermo ogni volta che l'esecuzione si arresta
(gdb) display nome_variabile
●
Per uscire digitare quit o cntr-D
Debugging di errori di memoria
●
Errore provoca un segmentation fault, MEMFLT
In genere provocato da una errata gestione della memoria
SOLUZIONE 1
Compilare il programma con opzione -g
Rieseguire il programma per generare il core file (settando ulimit -c unlimited)
Analizzare il core file con un debugger come gdb
Debugging e core files
●
Core file è un dump dello stato della memoria usata da un binario
Nome da quando memoria era un core magnetico
Generabile in qualsiasi momento con una chiamata di sistema nei sistemi UNIX
Tipicamente viene generato in caso di un fault (di memoria o altro tipo)
Per abilitare la generazione ulimit -c unlimited
Se il binario usa molta memoria file può essere grande
Dal core file possibile a posteriori analizzare il fault
Core files e gdb
●
gdb permette di analizzare il core file
gdb nome_binario core_file
Una volta aperto il gdb permette di analizzare la memoria. Esempio per vedere il punto in cui è avvenuto l'errore
(gdb) backtrace
O andare up nello stack e verificare valore di variabili (gdb) up
(gdb) print el
Debugging e Valgrind
●
Valgrind è un tool open-source per l'analisi del software
Scaricabile liberamente da http://valgrind.org
O tramite pacchetto della distribuzione Linux usata
Nato come tool per fare check della memoria
Debugging e Valgrind
●
Valgrind è nato come tool per fare check della memoria
Oggi fa molto di più (vedremo cosa sono queste cose)
CPU e mem profiling, Cache profiler
Race condition in codice Multi-threaded
●
Valgrind usa una macchina virtuale con compilazione al volo (just-in-time)
Codice da eseguire viene tradotto in un linguaggio intermedio, poi riconvertito in codice da eseguire
Codice tradotto è tracciabile e monitorabile
Debugging e Valgrind
●
Valgrind è supportato da Linux e Mac OS X
●
Prezzo da pagare per la traduzione:
Codice gira da 5 a 20 volte più lento che il binario originale
Uso della memoria aumenta di molto
Debugging e Valgrind
●
Valgrind è il miglior metodo per analizzare i bug NONDET
Trova errori dovuti ad uso di memoria non inizializzata
Mappa tutte le celle di memoria usate come inizializzate o no
Genera errore se si accede a memoria non inizializzata
Usa tanta memoria e CPU aggiuntiva per fare questo
●
Trova inoltre bug dovuti a
deallocazione di memoria non allocata
Accesso out-of-boundary in vettori od a memoria non allocata in generale
Debugging e Valgrind
●
Vantaggio fondamentale nell'uso dei memcheck
Debuggers trovano errore quando avviene il fault
Valgrind trova l'errore appena avviene, esempio1:
int* v = new int[7];
for (i=0; i<15;++i) v[i] = 1; // ma v=new int[7];
Il fault avviene appena si esce dal boundary
Sia GDB che Valgrind lo tracciano
Memoria non allocata
scorro vettore Out-of-boundary, Segmentation Fault!
Debugging e Valgrind
●
Caso 2
int* v = new int[7]; int* w = new int[7];
for (i=0; i<15;++i) v[i] = 1;
Compilatore ha messo il vettore v e w accanto!
Non garantito ma probabile, succeda. Per ottimizzare
caching, SO alloca vicino aree di memoria del processo!
Segmantation Fault non avviene finché non si esce anche
Memoria non allocata
scorro vettore Non vado
out-of-boundary
Solo qui out-of-boundary
Debugging e Valgrind
●
Il fault non è nemmeno detto avvenga (ad esempio se il loop si ferma a 10)
Ma il programma era sbagliato!
In generale tracciano il Fault con GDB si trova il primo fault ,ma la sorgente iniziale dell'errore!
Valgrind traccia esattamente le allocazioni:
v → [v, v+7*sizeof(int)]
Ad ogni accesso in memoria v[i], controlla se si cade nel range [v, v+7*sizeof(int)]
Se non succede da un warning
Valgrind: memcheck, uso
●
Valgrind usa convertitore, pertanto non serve modificare la compilazione
Tranne usare l'ozione -g del g++ perché Valgrind dia messaggi più informativi
Uso
valgrind –tool=memcheck nome_binario argomenti
Valgrind: memory leaks
●
Valgrind può tenere traccia della memoria allocata e non più raggiungibile e mai deallocata
Traccia tutte le allocazioni fatte e verifica se la
memoria allocata è accessibile tramite un puntatore
Da messaggio di errore se non succede e conta la quantità di memoria persa
Uso
valgrind –tool=memcheck –leak-check=yes nome_binario args
Valgrind: controllo uso cache
●
Programmi che usano le cache di basso livello sono più veloci
Usare una struttura dati od un'altra possono cambiare il tasso di cache hit
Ma come analizzare tutto questo? In generale è nascosto al programmatore
Valgrind fornisce uno strumento per controllare il numero di accessi alle cache di diverso livello
Uso
valgrind –tool=cachegrind nome_binario args
CPU e mem profilers
●
Talvolta il software non presenta bags ma è troppo lento od usa troppa memoria
Come ottimizzare il consumo di memoria e CPU
●
Intanto, REGOLA 1 dell'ottimizzazione del codice
Non ottimizzare il codice presto
Inizialmente cura il design e la flessibilità
Ad esempio, non rinunciare mai a fare un metodo virtual
Ultimo passo: ottimizza DOVE SERVE
Il CPU o MEM profiler ti dicono dove val la pena di farlo
CPU profiler: funzionamento
●
Funzionamento basato su sampling
Ogni x millisecondi, si chiede al binario di fornire il suo stack
Stack fornisce la funzione in cui ci si trova, da chi si è chiamati, ecc.
La percentuale di volte in cui il sampling ha trovato che ci si trova in una funzione approssima la CPU usata dalla stessa
Possibile anche contare il numero di volte che si segue un path rispetto ad un altro
MEM profiler: funzionamento
●
Funzionamento basato su ridefinizione della libreria che gestisce le allocazioni di memoria
Non si chiama new, delete di sistema ma quelle definite in libreria aggiunta in linking
Le librerie aggiunte per il profiling in genere
Tracciano la funzione che chiama l'allocatore o deallocatore
Passano la chiamata all'allocatore o deallocatore di sistema
Si paga una penalità in performance, i binari su cui si fa il profiling sono più lenti
Aggiungere le librerie solo quando si ottimizza il codice
gprof
●
Strumento per effettuare cpu profiling integrato con g++, con utilizzo molto semplice
In compilazione e linking usare le opzioni -g e -pg
Eseguire il programma → genera file gmon.out
Per analizzare il file di output
gprof nome_programma gmon.out
Stampa profilo testuale
gprof
●
Profile testuale ottenuto ha formato
% cumulative self self total
time seconds seconds calls ms/call ms/call name 33.34 0.02 0.02 7208 0.00 0.00 open
16.67 0.03 0.01 244 0.04 0.12 offtime 16.67 0.04 0.01 8 1.25 1.25 memccpy 16.67 0.05 0.01 7 1.43 1.43 write
16.67 0.06 0.01 236 0.00 0.00 tzset 0.00 0.06 0.00 192 0.00 0.00 tolower 0.00 0.06 0.00 47 0.00 0.00 strlen 0.00 0.06 0.00 45 0.00 0.00 strchr 0.00 0.06 0.00 1 0.00 50.00 main 0.00 0.06 0.00 1 0.00 0.00 memcpy 0.00 0.06 0.00 1 0.00 10.11 print
0.00 0.06 0.00 1 0.00 0.00 profil 0.00 0.06 0.00 1 0.00 50.00 report
google-perftools: introduzione
●
Strumento per effettuare cpu e mem profiling
Liberamente scaricabile (utilizzabile senza restrizioni) da
http://code.google.com/p/google-perftools/
Istallazione ./configure make
make install
google-perftools: utilizzo CPU
●
In Linking
aggiungi -lprofiler
●
In esecuzione
CPUPROFILE=/tmp/mybin.prof binario_con_cprofiler_linkato
●
Per analizzare l'output
pprof --text binario /tmp/mybin.prof
o in modalità grafica (richiede dot e ghostview):
pprof --gv binario /tmp/mybin.prof
google-perftools: utilizzo MEM
●
In Linking
aggiungi -ltcmalloc
●
In esecuzione
HEAPPROFILE=/tmp/mybin.prof binario_con_mprofiler_linkato
●
Per analizzare l'output
pprof --text binario /tmp/mybin.prof
o in modalità grafica (richiede dot e ghostview):
google-perftools: output
●
Modalità testuale, collezione di linee
14 2.1% 17.2% 58 8.7% std::_Rb_tree::find ...
numero samples la funzione era ultima sullo stack
% samples in cui si era nella funzione (% volte la funzione era ultima sullo stack)
% of profiling samples in the functions printed so far
numero samples la funzione era sullo stack
% di profiling samples nella funzione e nelle funzioni chiamate (% volte che la funzione era sullo stack)
Nome della funzione
google-perftools: output
●
Modalità grafica
Valgrind: mem profiling
●
Anche Valgrind ha strumento per l'analisi dell'uso della memoria
Controlla periodicamente l'uso della memoria
Possibile tracciare come aumenta nel tempo e chi la richiede
Uso
valgrind –tool=massif nome_binario args