Testing & Debugging
Laboratorio di Informatica
GHOST, una vulnerabilit in Linux recentemente scoperta
Prof. Filippo Lanubile
i bug
Bug = errore sintattico o semantico presente
in un programma
Debug = processo di riconoscimento e
rimozione dei bug
Attenzione:
I bug sono molto frequenti, anche in programmi
semplici
Il debug un'attivit difficile, che richiede un tempo
imprevedibile
Occorre adottare tutte le tecniche che riducano la
presenza di bug e il tempo del debug
4
origine della parola bug
Il 9 settembre 1947 il tenente Grace
Hopper ed il suo gruppo stavano
cercando la causa del
malfunzionamento di un computer
Mark II quando, con stupore, si
accorsero che una falena si era
incastrata tra i circuiti. Dopo aver
rimosso l'insetto (alle ore 15.45), il
tenente incoll la falena rimossa sul
registro del computer e annot:
1545. Relay #70 Panel F (moth) in
relay. First actual case of bug being
found. Questo registro
conservato presso lo Smithsonian
National Museum of American
History.
fonte: Wikipedia
testing vs. debugging
Il testing una fase di verifica sistematica
della correttezza di un software.
Stima della correttezza
Parte integrante dei processi di sviluppo del sw
Il debugging un processo atto a scovare la
causa di un errore.
E. Dijkstra: Il testing pu solo dimostrare la
presenza di bug, ma non la loro assenza
Il testing parte prevalente nelle
metodologie agili
6
...They keep their design
simple and clean.
They get feedback by
testing their software
starting on day one.
They deliver the system to
the customers as early as
possible and implement
changes as suggested
eXtreme Programming una metdologia Agile
dove la fase di test predominante7
Verifica delle condizioni
limite
La maggior parte dei bug si verificano in
corrispondenza dei limiti
Cicli: cosa succede se il numero di cicli 0?
Array: cosa succede se si tenta di colmare un array?
Input: cosa succede se l'input acquisito nullo?
Stream: cosa succede se si accede a un file
inesistente, un disco pieno, una connessione
interrotta, etc.?
Approccio: ogni volta che si scrive un blocco
di codice significativo (ciclo, condizione,
input), testarne le condizioni limite.
Occorre immaginare tutte le possibili condizioni
Esempio
Questo programma tenta di leggere una
sequenza di caratteri da un file e li memorizza
in un array fino a quando viene letta una
newline o si raggiunge la dimensione massima
MAX>0
int i = 0;
char s[MAX];
while ((s[i] = fgetc(file)) != '\n' && i < MAX-1)
i++;
s[--i] = '\0';
9
Esempio
Quali sono le condizioni limite?
1. L'input vuoto (\n)
2. MAX == 1
3. L'input ha una lunghezza pari a MAX
4. L'input ha una lunghezza maggiore di MAX
5. L'input non contiene una newline (se
possibile)
10
Esempio
Riscrivendo il codice usando uno stile pi
leggibile, si eviterebbe l'errore del codice
precedente
int i = 0;
char s[MAX];
int stop = 0;
while (!stop && i < MAX) {
s[i] = fgetc(file);
stop = (s[i] == '\n');
i++;
}
s[--i] = '\0';
11
Esercizio
/**
* Restituisce la media aritmetica di un array.
*
* @param a double di len_a elementi; len_a>0
* @return media aritmetica degli elementi dell'array
*/
double avg(double a[], int len_a) {
int i;
double sum = 0.0;
for (i=0; i < len_a; i++) {
sum += a[i];
}
return sum / len_a;
}
12
Programmazione difensiva
Aggiunta di codice per casi impossibili
if (age < 0 || age > MAX_AGE) {
range = "???";
} else if (age <= 18) {
range = "Teenager"
} ...
13
Controllo dei valori di errore
restituiti
Se una funzione restituisce dei valori di errore
questi vanno verificati dal chiamante
range = num_to_range(age);
if (strcmp(range,"???")==0) {
/* errore */
...
} else {
...
14
Testing sistematico
Verifica incrementale
Verifica bottom-up
Verifica dei risultati attesi
Verifica della copertura
15
Verifica incrementale
Test di pari passo con l'implementazione
Test di unit elementari
Una procedura o funzione
Un blocco significativo di procedura
16
Verifica bottom-up
Testare prima:
Le parti (componenti/casi) pi semplici
Le parti pi frequentemente utilizzate
Esempio: Ricerca binaria
Ricerca in un array vuoto
Ricerca in un array con un solo elemento
Ricerca di un elemento minore di quello presente
Ricerca di un elemento uguale a quello presente
Ricerca di un elemento maggiore di quello presente
Ricerca in un array con due elementi
5 combinazioni possibili
Ricerca in presenza di elementi ripetuti
Ricerca in presenza di elementi contigui e non
17
Verifica dei risultati attesi
Per ogni test, occorre conoscere il risultato
atteso
Questo ovvio per molti casi, ma non per
tutti
Testare un compilatore
Testare un programma di calcolo numerico
Compilare uno o pi programmi formalmente corretti e testarli
Verificare i limiti dell'algoritmo
Verificare propriet note
Testare problemi con risultati gi noti
Analisi statistiche
Testare un programma grafico/multimediale
Uso di strumenti di image editing
Analisi statistiche
18
Verificare la copertura
dei test
I test applicati devono garantire che ogni
istruzione del programma sia eseguita
almeno una volta
Rami then-else
Tutti i case di una switch
Esecuzione dei cicli
0 volte, 1 volta, il numero max. di volte, il numero max. di volte
1
Classi di equivalenza
Es. n ]min, max[, n < min, n > max, n = min, n =
max
L'analisi del codice pu aiutare a individuare
gli input che consentono di coprire tutto il
19
CUnit
introduzione
Unit test
Cos?
Tecnica di progetto e sviluppo del software
A cosa serve?
A ottenere evidenza che le singole unit software
sviluppate siano corrette e pronte alluso
Unit software?
In un linguaggio procedurale come il C una unit
pu essere un programma, una funzione, una
procedura
Come si fa?
Si scrivono degli unit test (o casi di test)
rappresentanti una sorta di contratto scritto che la
porzione di codice testata deve assolutamente
21
xUnit test framework
In principio fu JUnit per Java
Creato da Kent Beck e Erich Gamma
Never in the field of software development have so
many owed so much to so few lines of code (M.
Fowler a proposito di JUnit)
E stato portato verso innumerevoli altri
linguaggi (C/C++, C#, PHP, Python,
JavaScript, Ruby, ) dando vita
allecosistema dei framework di tipo xUnit
Ha dato vita al Test-Driven Development
(TDD sviluppo guidato dal test)
22
CUnit
Framework di unit test per il linguaggio C
Home page: [Link]
Libreria che va inclusa in ogni progetto
Eclipse CDT che intende avvalersene
Guida di installazione disponibile qui:
[Link]
23
Struttura del framework
CUnit
Il framework esegue automaticamente tutte
le test suite inserite nel test registry
Ogni test suite composta da uno o pi
test method logicamente correlati
Es. suite per testare tutti i metodi di un particolare modulo
24
Method under test
Programma da testare
Costituito da diversi file .c (detti moduli) contenenti
diverse funzioni e/o procedure
funz_1(),, funz_n(), proc_1(),, proc_m()
Queste funzioni e procedure sono detti methods under test
Per ciascun metodo da testare occorre
scrivere almeno un test method:
Ciascun metodo di test va chiamato test_xyz()
test_funz_1(), , test_funz_n(), test_proc_1(),, test_proc_m()
Un metodo di test verifica la presenza di
errori nel corrispettivo metodo sotto test
Errore comportamento diverso da quello atteso
25
Attenzione: Lordine di inserimento ha importanza!
Le test suite sono eseguite nello stesso ordine di inserimento nel registry
I test method sono eseguiti nello stesso ordine di inserimento nella suite
26
Ciclo di unit test
Sequenza tipica di uso di un framework di unit
test, incluso CUnit:
1. Scrivi tutti i test method necessari
2. Crea il test registry
3. Crea la test suite e aggiungila al test registry
4. Aggiungi i test method alle test suite definite
5. Se necessario, ripeti i passi 3-4 per unaltra
suite
6. Esegui il test registry
7. Pulisci il test registry
27
Procedure e Funzioni in
C
un breve ripasso
28
Procedure e funzioni
Sono istruzioni non primitive per risolvere
parti specifiche di un problema: i
sottoprogrammi (o metodi)
Sono realizzate mediante la definizione di
unit di programma (sottoprogrammi)
distinte dal programma principale (main)
Rappresentano nuove istruzioni/operatori
che agiscono sui dati utilizzati dal
programma
Sono definite a partire da una sequenza di
istruzioni primitive e altre procedure/funzioni
29
Sottoprogrammi: funzioni e
procedure
Tutti i linguaggi di alto livello offrono la
possibilit di utilizzare funzioni e procedure
mediante:
costrutti per la definizione di sottoprogrammi
meccanismi per la chiamata a sottoprogrammi
Nei linguaggi ad alto livello funzioni e
procedure sono molto utili per raggiungere:
Astrazione
Riusabilit
Modularit (strutturazione)
Leggibilit
30
Funzioni e procedure:
definizione
Nella fase di definizione di una funzione o
procedura si stabilisce:
Un identificatore del sottoprogramma
(cio il nome da usare per chiamare/invocare lo
stesso)
Un corpo del sottoprogramma
(cio, l'insieme di istruzioni che sar eseguito ogni
volta che il sottoprogramma sar chiamato)
Una lista di parametri formali
(cio come avviene la comunicazione tra l'unit di
programma che usa il sottoprogramma ed il
sottoprogramma stesso)
31
Esempio di funzione e
procedura in C
Funzione
Procedura
// prototipo
int sum(int a, int b);
// prototipo
void print(int a);
// dichiarazione
int sum(int a, int b) {
int c = a + b;
return c;
}
// dichiarazione
void print(int a) {
printf("%d", a);
}
// chiamata
int main(void) {
int op1, op2, result;
result = sum(op1, op2);
return 0;
}
// chiamata
int main(void) {
int value;
print(value);
}
32
Funzioni e procedure:
differenze
Funzione
Operatore non
primitivo
Permette di definire
nuovi operatori
complessi da
affiancare a quelli
primitivi
Restituisce un valore
di ritorno (mediante
return)
Procedura
Istruzione non
primitiva
E attivabile in un
qualunque punto del
programma in cui
pu comparire
unistruzione
Non restituisce un
valore di ritorno
(mediante return)
33
Funzioni e procedure:
parametri
I parametri costituiscono il mezzo di
comunicazione tra unit chiamante e unit
chiamata
Supportano lo scambio di informazioni tra chiamante
e sottoprogramma
Si differenziano in
Parametri formali (specificati nella definizione)
Parametri attuali (specificati nella chiamata)
Parametri attuali e formali devono
corrispondersi in numero, posizione e tipo
34
Parametri formali e
parametri attuali
Parametri formali
Sono quelli specificati
nella definizione del
sottoprogramma
Sono in numero
prefissato e a ognuno
di essi viene
associato un tipo
Le istruzioni del
corpo del
sottoprogramma
utilizzano i parametri
formali
Parametri attuali
Sono i valori
effettivamente forniti
dallunit chiamante
al sottoprogramma
allatto della
invocazione
Associazione tra parametri
attuali e parametri formali
Il passaggio di parametri pu avvenire in due
modi:
Per valore
Per indirizzo (o riferimento) lo vedremo pi avanti
Il C di default adotta il passaggio per valore
Il valore dei parametri copiato nello stack
Il passaggio per riferimento si ottiene memorizzando
nello stack lindirizzo (puntatore) in cui allocata
una variabile
Il passaggio per valore anche pi sicuro
. Di ritorno a CUnit .
Aggiungere le librerie CUnit
a un progetto
Guida di installazione e configurazione
disponibile alla sezione dispense sul sito del
corso
[Link]
38
Includere le librerie di CUnit
nei file di test di un progetto
Quando preparate il modulo con i metodi di test
ricordate di includere i file header in questo
modo:
#include <stdio.h>
#include <stdlib.h>
#include CUnit/Basic.h
39 39
Ciclo di Unit Test
[Link] tutti i test method necessari
[Link] il test registry
[Link] la test suite e aggiungila al test registry
[Link] i test method alle test suite definite
[Link] necessario, ripeti i passi 3-4 per unaltra suite
[Link] il test registry
[Link] il test registry
Scrivere un test method
Un metodo di test in CUnit si presenta sempre nella
forma di procedura senza parametri
void test_xyz(void)
Un metodo di test un contratto che stabilisce i vincoli
che devono essere soddisfatti dal software
I vincoli sono stabiliti attraverso delle asserzioni
Unasserzione in un linguaggio di programmazione
una funzione che verifica una condizione logica e
restituisce:
Vero, se lasserzione rispettata
Falso, altrimenti
Asserzioni di base (CUnit)
Asserzione
Significato
CU_ASSERT(int espressione)
CU_TEST(int espressione)
Asserisce che espressione TRUE (diverso da 0)
CU_ASSERT_TRUE(valore)
Asserisce che valore TRUE (diverso da 0)
CU_ASSERT_FALSE(valore)
Asserisce che valore FALSE (uguale a 0)
CU_ASSERT_EQUAL(reale, atteso)
Asserisce che reale == atteso
CU_ASSERT_NOT_EQUAL(reale, atteso)
Asserisce che reale != atteso
CU_ASSERT_STRING_EQUAL(reale, atteso)
Asserisce che le stringhe reale e atteso coincidono
CU_ASSERT_STRING_NOT_EQUAL(reale,
atteso)
Asserisce che le stringhe reale e atteso
differiscono
Esempio di test method per
la funzione max(a,b)
void test_max(void) {
CU_ASSERT_EQUAL(max(0,2), 2);
CU_ASSERT_TRUE(max(0,-2) == 0);
CU_TEST(max(2,2) == 2);
// questa asserzione sbagliata e fallisce
CU_ASSERT_TRUE(max(5,6) == 2);
}
Esempio di metodo di test
per la funzione factorial(x)
1
se n = 0
n! =
void test_factorial(void) {
n(n-1)!
se n > 1
// fallisce
CU_ASSERT_EQUAL(factorial(4), 12 );
CU_ASSERT(factorial(3) == 6);
CU_TEST(factorial(6) == 720);
}
Ciclo di Unit Test
[Link] tutti i test method necessari
[Link] il test registry
[Link] la test suite e aggiungila al test registry
[Link] i test method alle test suite definite
[Link] necessario, ripeti i passi 3-4 per unaltra suite
[Link] il test registry
[Link] il test registry
Il Test Registry
Raccoglie tutte le test suite
Quando si esegue un Test Registry si
eseguono tutte le suite al suo interno e, di
conseguenza, tutti i test method allinterno
delle suite
Inizializzazione del
Test Registry
Linizializzazione del test registry la prima
operazione da effettuare
47
Ciclo di Unit Test
[Link] tutti i test method necessari
[Link] il test registry
[Link] la test suite e aggiungila al test registry
[Link] i test method alle test suite definite
[Link] necessario, ripeti i passi 3-4 per unaltra suite
[Link] il test registry
[Link] il test registry
48
Test Suite
Una test suite definita da:
Una descrizione testuale
Una procedura di inizializzazione (init)
Una procedura di pulitura (clean)
Le test suite definite vengono aggiunte al
test registry (lordine rilevante!)
Di default inizializzazione e pulitura sono procedure vuote.
49
Inizializzazione e pulizia
delle suite
Le test suite devono essere inizializzate e
ripulite prima e dopo luso
I metodi non sono forniti da CUnit ma devono essere
scritti dal programmatore
Perch?
Perch devono liberare le risorse allocate
specificatamente per eseguire il caso di test
Es. file, connessioni, etc.
50
Inizializzazione e pulizia
// Alloca tutte le risorse necessarie allesecuzione
// dei test
int init_suite_default(void) {
return 0; // tutto ok
}
// dealloca tutte le risorse allocate allinizializzazione
int clean_suite_default(void) {
return 0; // tutto ok
}
51
Ciclo di Unit Test
[Link] tutti i test method necessari
[Link] il test registry
[Link] la test suite e aggiungila al test registry
[Link] i test method alle test suite definite
[Link] necessario, ripeti i passi 3-4 per unaltra suite
[Link] il test registry
[Link] il test registry
52
Test method e test suite
Un test method viene aggiunto ad una
test suite specificando:
il puntatore alla suite
una descrizione testuale del test
il puntatore al test method
Lordine dei test nelle suite rilevante!
53
Ciclo di Unit Test
[Link] tutti i test method necessari
[Link] il test registry
[Link] la test suite e aggiungila al test registry
[Link] i test method alle test suite definite
[Link] necessario, ripeti i passi 3-4 per unaltra suite
[Link] il test registry
[Link] il test registry
54
Registrazione ed
esecuzione dei test
La procedura CU_basic_run_tests esegue tutte
le suite del registry e mostra i risultati
possibile impostare il livello di verbosit
delloutput
55
Ciclo di Unit Test
[Link] tutti i test method necessari
[Link] il test registry
[Link] la test suite e aggiungila al test registry
[Link] i test method alle test suite definite
[Link] necessario, ripeti i passi 3-4 per unaltra suite
[Link] il test registry
[Link] il test registry
56
Pulire il registry
Pulizia dopo aver eseguito tutti i test nel
registro
Il
Procedura void CU_cleanup_registry(void)
main() termina con la return delleventuale codice di errore di CUnit
57
Esercitazione 1
CUnit
Link allesercitazione
[Link]
Implementazione di una serie di funzioni e
testing di queste con lutilizzo di CUnit
template di CUnit disponibile a
[Link]
58
Debugging
59
Ariane 5 Flight 501
60
testing vs. debugging
Il testing una fase di verifica sistematica
della correttezza di un software.
Il debugging un processo atto a
scovare la causa di un errore.
un processo costoso, dai tempi non prevedibili
lesperienza importante
gli strumenti possono velocizzare il debugging
61
Supporto del compilatore
Molti compilatori emettono dei warning,
cio dei messaggi di avvertimento
if (a=0)
x=x
nessun return
codice orfano
condizioni tautologiche
...
62
Backward reasoning
Quando si scopre un bug, occorre pensare
al contrario
Partendo dal risultato, occorre risalire alla catena
delle cause che lo hanno portato.
Una delle cause della catena sar errata
Scrivere codice leggibile aiuta al backward
reasoning e, quindi, a localizzare i bug
63
Pattern familiari
Riconoscere variazioni rispetto a modelli
(pattern) di codice familiari
int n;
scanf("%d", n);
int n;
scanf("%d", &n);
Luso di un corretto stile di programmazione
aiuta a ridurre la presenza di bug
64
Sviluppo incrementale
Testare le procedure man mano che
vengono sviluppate
Se i test allistante t hanno successo ma falliscono
allistante t+1, allora molto probabilmente i big si
annidano nel codice sviluppato tra t e t+1
La progettazione modulare del codice aiuta a
individuare meglio la posizione dei bug
65
Esaminare codice simile
Se un bug presente in una porzione di
codice, allora probabile che se ne annidi
un altro in un codice simile
problema del copy-and-paste
Una buona progettazione del codice riduce
la ridondanza e, quindi, la possibilit di bug
duplicati
66
Non rimandare il debugging
Se un bug individuato, va eliminato subito
Il trasferimento di un bug nei passi successivi del
ciclo di sviluppo di un software fa crescere il costo
del debugging in termini esponenziali.
The Mars Pathfinder mission was widely
proclaimed as "flawless" in the early days after its
July 4th, 1997 landing on the Martian surface. [...]
But a few days into the mission, not long after
Pathfinder started gathering meteorological data,
the spacecraft began experiencing total system
resets, each resulting in losses of data.
(D. Wilner, 1997 IEEE Real-Time Systems Symposium)
67
Leggere e spiegare il codice
Leggere il codice e comprenderne il
significato
Il codice un pezzo di conoscenza che deve essere
compreso dalla macchina e da chi la programma
La leggibilit del codice fondamentale
Spiegare ad altri il codice aiuta a ridurre
bias cognitivi
68
Rendere riproducibile un
bug
Individuare tutte le condizioni che portano
alla manifestazione di un bug
Input e altri parametri
Condizioni della macchina
Seed di numeri casuali
...
69
Divide et impera
Individuare le condizioni minimali che
rendono manifesto un bug
es. il pi piccolo array, la stringa pi breve
Test dei casi limite fondamentale
Le condizioni minimali possono facilitare la
localizzazione di un bug
Se il bug non si manifesta in un caso limite, provare
mediante dimezzamenti successivi dellinput
Ricerca binaria sulla lunghezza dellinput
70
Ricerca di regolarit
Alcuni bug si presentano con regolarit, ma
non sempre
In questo caso, occorre capire il modello
(pattern) che genera la regolarit
Es. Un editor di testi salta la visualizzazione di alcuni
caratteri
Lanalisi del testo mostra che i caratteri saltati sono
sempre intervallati da 1023 caratteri stampati
Regolarit: 1 carattere saltato ogni 1023
Lanalisi del codice rivela che gli array che
memorizzano le stringhe sono da 1024 byte
1023 caratteri + \0 BUG
71
Stampe ausiliarie
Per seguire lesecuzione di un programma
pu essere utile introdurre stampe ausiliarie
Valido soprattutto per situazioni che non possono
essere tracciate da un debugger
es. sistemi distribuiti, programmi paralleli, etc.
Le stampe ausiliarie devono
necessariamente essere eliminate dopo aver
scovato il bug
Rischio di violazione delle specifiche
Possono essere commentate anzich eliminate
Per situazioni complesse, si possono usare
strumenti di logging
72
Altre tecniche
Visualizzazioni grafiche
Test statistici
Strumenti di analisi di testo
grep
diff
...
73
Debugger
Un debugger guarda dentro il programma
durante lesecuzione
Tracing del programma
Visualizzazione del contenuto delle variabili
Valutazione dinamica di espressioni
Breakpoint, anche condizionali
Stack trace
Sono strumenti molto sofisticati, abituarsi al
loro uso pu migliorare significativamente la
produttivit nella programmazione.
74
Compilazione per il debug
Un debugger ha bisogno di informazioni
aggiuntive nel codice compilato
link tra il codice compilato e il codice sorgente
Per stabilire la corrispondenza tra codice
compilato e codice sorgente, la compilazione
per il debug non deve essere ottimizzata
Due modalit di compilazione
Debug
Meno efficiente, per il debug
Release
Ottimizzata
75
la debug perspective in Eclipse
76
Esecuzione passo-passo
Il debugger consente di eseguire il
programma una istruzione alla volta
Al termine dellesecuzione di una istruzione, il
controllo passa al debugger, che pu visualizzare lo
stato della macchina (variabili, stack, etc.)
Per velocizzare il processo di debugging, si
pu optare per eseguire il programma fino a
unistruzione specifica, segnalata da un
breakpoint.
77
Esecuzione passo-passo
Step into
Esegue listruzione
corrente, e procede
allistruzione
successiva che sar
effettivamente eseguita
Step over
Esegue listruzione
corrente, trattando le
routine come istruzioni
primitive.
continua lesecuzione
Step return fino al termine della
procedura.
78
Step Into
79
Step over
80
Step return
81
Informazioni di debug:
variabili
Le variabili visibili
nellambito
dellistruzione
corrente sono
visualizzate
Nome, tipo, valore
Le variabili che
cambiano valore
sono evidenziate
82
Informazioni di debug:
espressioni
Unespressione un
pezzo ben formato
di codice (snippet)
che pu essere
valutato per
produrre un risultato
83
Informazioni di debug:
stack trace
Visualizza la pila
delle chiamate
Si pu selezionare
un elemento della
pila per conoscerne
lo stato corrente
84
Breakpoint
Un breakpoint interrompe il flusso di
esecuzione su una linea selezionata
I breakpoint possono essere inseriti o rimossi
I breakpoint inseriti possono essere attivati o
disattivati
85
Resume & Terminate
Resume
Esegue le istruzioni fino al prossimo
breakpoint oppure al termine del
programma
Terminate
Interrompe lesecuzione del programma
Utile quando il programma va in loop infinito
Utile quando si scova un programma
Attenzione: i programmi non terminati
rimangono in esecuzione per il sistema
operativo
Occupazione inutile di memoria
Problemi per la ricompilazione
86
Breakpoint condizionali
I breakpoint possono interrompere
lesecuzione solo quando una condizione
diventa vera
Condizione: espressione booleana
87