Debugging e Testing del Software - unisi.itDebugging.pdf · 2018-06-06 · Debugging e Testing del...

Post on 30-Jun-2020

4 views 0 download

Transcript of Debugging e Testing del Software - unisi.itDebugging.pdf · 2018-06-06 · Debugging e Testing del...

Debugging e Testing del Software

Michelangelo DiligentiIngegneria Informatica e

dell'Informazione

diligmic@diism.unisi.it

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 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

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 qualche decina di volte più lento che il

binario originale Uso della memoria aumenta di molto Difficile fare debugging di errori che avvengono dopo

un lungo tempo di esecuzione

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

da w, ma l'esecuzione è sbagliata e spesso non deterministica!

Memorianon allocata

scorro vettore Non vadoout-of-boundary

Solo quiout-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 non 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 ottenete un warning per ogni accesso

non coretto alla memoria

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 esecuzioneCPUPROFILE=/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 esecuzioneHEAPPROFILE=/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

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 codiceint 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ù test 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

Test di correttezza: diversi livelli● 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

Altri 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

Alla base resta un test di correttezza: basato 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 DesignTest

ImplementationTest

ExecutionResults

Verification

Test ManagementLibrary

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 DesignTest

ImplementationTest

ExecutionResults

Verification

AutomatizzazioneTest ManagementLibrary

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 case è 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 Con terminazione del programma

ASSERT_TRUE(bool); ASSERT_FALSE(bool);

Senza terminazione 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: esempioclass 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 TestTEST_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: 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