09-Testing+Debugging

Transcript

09-Testing+Debugging
Testing e Debugging
del Software
Michelangelo Diligenti
Ingegneria Informatica e
dell'Informazione
[email protected]
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 è 2 n
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 Design
Test
Implementation
Test
Execution
Results
Verification
Test Management
Library
●
●
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 ed automazione
Test Design
Test
Implementation
Test Management
Library
●
Test
Execution
Results
Verification
Automatizzazione
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
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



Non fatali



ASSERT_TRUE(bool);
ASSERT_FALSE(bool);
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
Chiamare TEST_F() per fare test usando Fixture
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



MEMFLT: il codice genera un fault di memoria



Analisi del flusso del codice con print di debug
Analisi del flusso del codice con debugger
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

Spesso lo si fa in Makefiles con la variabile CFLAGS
Debugging con gdb
●
●
GDB: GNU debugger, debugger open-source per
sistemi UNIX
Metodo più evoluto per il debugging di bug INCRT

●
Permette di eseguire un programma passo-passo

●
ATTENZIONE: compilare con l'opzione -g del g++
perché il debugging sia leggibile
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];
Memoria non allocata
scorro vettore

Out-of-boundary, Segmentation Fault!
Il fault avviene appena si esce dal boundary

Sia GDB che Valgrind lo tracciano
Debugging e Valgrind
●
Caso 2
int* v = new int[7]; int* w = new int[7];
for (i=0; i<15;++i) v[i] = 1;
Memoria
non allocata
Non vado
out-of-boundary
scorro vettore

Solo qui
out-of-boundary
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
da w, ma l'esecuzione è sbagliata e spesso non
deterministica!
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):
pprof --gv binario /tmp/mybin.prof
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