Salta al contenuto
Note per Studenti Debugging e testing

Debugging e testing

In questa pagina 5

Tipi di errore

Tipo Quando emerge Esempio
sintattico prima dell'esecuzione (Python) / in compilazione (C) : mancante, ; mancante
di tipo a run-time in Python, in compilazione in C (in parte) "a" + 1
a run-time durante l'esecuzione divisione per zero, indice fuori intervallo, NULL dereferenziato
logico mai segnalato: risultato sbagliato < invece di <=, accumulatore inizializzato male

In C molti errori a run-time non vengono segnalati affatto (comportamento indefinito): il programma può funzionare per caso e fallire altrove.

Metodo

  1. Riprodurre il problema con l'input più piccolo possibile.
  2. Leggere il messaggio: in Python il traceback dal fondo (tipo, riga, catena di chiamate, vedi Eccezioni in PythonEccezioni e traceback, eccezioni predefinite più comuni, try/except/else/finally, raise per segnalare errori, propagazione lungo le chiamate.Eccezioni in Python →); in C il primo errore del compilatore (i successivi sono spesso conseguenze).
  3. Formulare un'ipotesi e verificarla osservando lo stato (valori delle variabili) nel punto sospetto.
  4. Restringere: dimezzare il codice o l'input finché non si isola la causa.
  5. Correggere la causa, non il sintomo, e aggiungere un test che avrebbe trovato il bug.

Strumenti

Stampe di controllo

python
print(f"{i = }, {somma = }")          # stampa nome e valore
c
fprintf(stderr, "i=%d somma=%d\n", i, somma);   /* stderr non è bufferizzato */

Da togliere a lavoro finito.

Debugger

Permette di fermare il programma in un punto (breakpoint), eseguire una riga alla volta e ispezionare le variabili.

Azione VS Code pdb (Python) gdb (C, compilare con -g)
avvio F5 python -m pdb prog.py o breakpoint() nel codice gdb ./prog, poi run
breakpoint clic a sinistra del numero di riga b 12 break 12
riga successiva F10 n next
entra nella funzione F11 s step
continua F5 c continue
valore pannello Variabili p x print x
stack delle chiamate pannello Call Stack w backtrace

gdb su un programma che va in segmentation fault mostra, con backtrace, la riga esatta del crash.

assert

assert condizione, "messaggio" interrompe il programma se la condizione è falsa. Serve a verificare ipotesi interne (precondizioni, invarianti di ciclo), non a validare l'input dell'utente.

python
def media(v):
    assert len(v) > 0, "lista vuota"
    return sum(v) / len(v)
c
#include <assert.h>
assert(n > 0);    /* disattivato compilando con -DNDEBUG */

Errori di memoria in C

  • gcc -Wall -Wextra -g -fsanitize=address,undefined prog.c: a run-time segnala accessi fuori dai limiti, uso dopo free, overflow di interi con segno.
  • valgrind --leak-check=full ./prog: memoria non liberata e letture di memoria non inizializzata.

Vedi Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →.

Testing

Un test esegue il codice su un input con risultato noto e confronta. I test mostrano la presenza di errori, non ne dimostrano l'assenza: vanno scelti bene.

Scegliere i casi

  • Casi normali: qualche input tipico.
  • Casi limite: sequenza vuota, un solo elemento, primo/ultimo elemento, valori uguali, zero, negativi, massimo intero in C.
  • Partizioni: dividere gli input in classi che il codice tratta allo stesso modo (es. per la ricerca: elemento presente all'inizio, in mezzo, alla fine, assente) e testare almeno un caso per classe e i confini tra le classi.
  • Input non validi: verificare che venga sollevata l'eccezione prevista o restituito il codice d'errore.

Test in Python

Con semplici assert in una funzione di test:

python
def test_ricerca_binaria():
    v = [1, 3, 5, 7]
    assert ricerca_binaria(v, 1) == 0       # primo
    assert ricerca_binaria(v, 7) == 3       # ultimo
    assert ricerca_binaria(v, 4) == -1      # assente, in mezzo
    assert ricerca_binaria([], 1) == -1     # vuota
    assert ricerca_binaria([5], 5) == 0     # un elemento

if __name__ == "__main__":
    test_ricerca_binaria()
    print("tutti i test superati")

Con il modulo unittest della libreria standard:

python
import unittest
from algoritmi import ricerca_binaria, media    # modulo con il codice da testare

class TestRicerca(unittest.TestCase):
    def test_presente(self):
        self.assertEqual(ricerca_binaria([1, 3, 5], 3), 1)

    def test_assente(self):
        self.assertEqual(ricerca_binaria([1, 3, 5], 2), -1)

    def test_media_vuota(self):
        with self.assertRaises(ValueError):
            media([])

if __name__ == "__main__":
    unittest.main()

Con doctest gli esempi nella docstring diventano test (python -m doctest modulo.py -v):

python
def quadrato(x):
    """
    >>> quadrato(3)
    9
    >>> quadrato(-2)
    4
    """
    return x * x

Test in C

Senza librerie esterne, una funzione di test con assert chiamata da main:

c
#include <assert.h>

void test_lunghezza(void) {
    assert(lunghezza("") == 0);
    assert(lunghezza("a") == 1);
    assert(lunghezza("ciao") == 4);
}

int main(void) {
    test_lunghezza();
    printf("ok\n");
    return 0;
}

Quando testare

Scrivere i test insieme al codice (o prima), rieseguirli dopo ogni modifica (test di regressione). Funzioni piccole che restituiscono valori invece di stamparli (Funzioni in Pythondef e return, parametri posizionali, con nome e con valore predefinito, *args e **kwargs; passaggio per riferimento a oggetto; ambito delle variabili LEGB; docstring, type hint, lambda.Funzioni in Python →) sono molto più facili da testare.

Errori tipici

  • Correggere a tentativi senza un'ipotesi.
  • Testare solo il caso "medio" e mai sequenze vuote o estremi.
  • Usare assert per controllare l'input dell'utente (in C sparisce con -DNDEBUG, in Python con python -O).
  • Lasciare stampe di debug nel codice consegnato.

Teoria collegata