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