trace - Segue le istruzioni Python mentre sono eseguite

Scopo Monitora quali istruzioni e funzioni sono eseguite mentre un programma è in esecuzione per produrre informazioni di code coverage e grafici di chiamata
Versione Python 2.3 e superiore

Il modulo trace aiuta nel comprendere in che modo viene eseguito un proprio programma. Si possono tracciare le istruzioni eseguite, produrre rapporti di coverage, ed investigare sulle relazioni tra le funzioni che si chiamano vicendevolmente.

Code coverage è una misura usata nel test dei software. Descrive il grado di test a cui è stato sottoposto il codice sorgente di un programma. E' una forma di test che ispeziona il codice direttamente, quindi si tratta di una forma di test strutturale. In breve riguarda in quale misura i propri test utilizzano la propria base di codice. Lo scopo dei test, naturalmente, è di veriicare che il proprio codice faccia quello che ci si aspetti che faccia, ma anche per documentare quello che ci si aspetta dal codice. Più estensivamente il code coverage può essere considerata una indiretta misura della qualità, indiretta in quanto si tratta di quanto i test coprano il proprio codice, o semplicemente, la qualità dei test. In altre parole il code coverage non si usa per verificare la qualità del prodotto finale.

Interfaccia da Riga di Comando

E' facile usare trace direttamente dalla riga di comando. Dati i seguenti script Python come input:

from recurse import recurse

def main():
    print 'Questo è il programma principale.'
    recurse(2)
    return

if __name__ == '__main__':
    main()
def recurse(level):
    print 'ricorsione(%s)' % level
    if level:
        recurse(level-1)
    return

def not_called():
    print 'Questa funzione non è mai chiamata.'

Esecuzione della tracciatura

Si può vedere quali istruzioni sono esegute mentre il programma è in esecuzione usando l'opzione --trace.

$ python -m trace --trace  trace_example/main.py
 --- modulename: threading, funcname: settrace
threading.py(90):     _trace_hook = func
 --- modulename: main, funcname: 
main.py(4): from recurse import recurse
 --- modulename: recurse, funcname: 
recurse.py(4): def recurse(level):
recurse.py(10): def not_called():
main.py(6): def main():
main.py(11): if __name__ == '__main__':
main.py(12):     main()
 --- modulename: main, funcname: main
main.py(7):     print 'Questo è il programma principale.'
Questo è il programma principale.
main.py(8):     recurse(2)
 --- modulename: recurse, funcname: recurse
recurse.py(5):     print 'ricorsione(%s)' % level
ricorsione(2)
recurse.py(6):     if level:
recurse.py(7):         recurse(level-1)
 --- modulename: recurse, funcname: recurse
recurse.py(5):     print 'ricorsione(%s)' % level
ricorsione(1)
recurse.py(6):     if level:
recurse.py(7):         recurse(level-1)
 --- modulename: recurse, funcname: recurse
recurse.py(5):     print 'ricorsione(%s)' % level
ricorsione(0)
recurse.py(6):     if level:
recurse.py(8):     return
recurse.py(8):     return
recurse.py(8):     return
main.py(9):     return

La prima parte del risultato mostra alcune operazioni di impostazione eseguite da trace. Il resto del risultato mostra l'entrata in ogni funzione, compreso il modulo dove la funzione si trova, quindi le righe del file sorgente, mentre vengono eseguite. Si può notare che in recurse() si entra per tre volte, proprio come ci si aspetta, visto come è stata chiamata in main().

Code Coverage

L'esecuzione di trace da riga di comando con l'opzione --count produrrà un rapporto informativo di code coverage , in modo che si possa vedere quali righe sono state eseguite e quali sono state saltate. Visto che un programma in genere è composto da file multipli l'informativa viene prodotta per ognuno. In modalità predefinita i file contenenti l'informativa sono scritti nella stessa directory del modulo, e si chiamano con il nome del modulo, ma con una estensione .cover in luogo di .py

$ python -m trace --count trace_example/main.py
Questo è il programma principale.
ricorsione(2)
ricorsione(1)
ricorsione(0)

Ecco i due file .cover, trace_example/main.cove:

    1: from recurse import recurse

    1: def main():
    1:     print 'Questo è il programma principale.'
    1:     recurse(2)
    1:     return

    1: if __name__ == '__main__':
    1:     main()

e trace_example/recurse.cove:

    1: def recurse(level):
    3:     print 'ricorsione(%s)' % level
    3:     if level:
    2:         recurse(level-1)
    3:     return

    1: def not_called():
           print 'Questa funzione non è mai chiamata.'
Sebbene la riga def recurse(level): abbia il conteggio di 1, non vuole dire che la funzione è stata eseguita una sola volta. Significa che la definizione della funzione è stata eseguita una sola volta.

E' anche possibile eseguire il programma diverse volte, magari con opzioni differenti, per salvare i dati di coverage e produrre un rapporto combinato.

$ python -m trace --coverdir coverdir1 --count --file coverdir1/coverage_report.dat trace_example/main.py
Questo è il programma principale.
ricorsione(2)
ricorsione(1)
ricorsione(0)
Skipping counts file 'coverdir1/coverage_report.dat': [Errno 2] No such file or directory: 'coverdir1/coverage_report.dat'

$ python -m trace --coverdir coverdir1 --count --file coverdir1/coverage\
> _report.dat trace_example/main.py
Questo è il programma principale.
ricorsione(2)
ricorsione(1)
ricorsione(0)

$ python -m trace --coverdir coverdir1 --count --file coverdir1/coverage\
> _report.dat trace_example/main.py
Questo è il programma principale.
ricorsione(2)
ricorsione(1)
ricorsione(0)

Una volta che l'informativa di coverage è stata registrata nei file .cover, si possono produrre dei rapporti con l'opzione --report

$ python -m trace --coverdir coverdir1 --report --summary --missing --file coverdir1/coverage_report.dat trace_example/main.py
lines   cov%   module   (path)
  607     0%   threading   (/usr/lib/python2.6/threading.py)
    7   100%   trace_example.main   (trace_example/main.py)
    7    85%   trace_example.recurse   (trace_example/recurse.py)

Visto che il programma viene eseguito tre volte, il rapporto di coverage mostra valori tre volte più alti rispetto al primo rapporto. L'opzione --summary aggiunge la percentuale di informazioni di coverage (il codice coperto) al risultato di cui sopra. Il modulo recurse ha una percentuale di copertura (coverage) del solo 87%. Una rapida occhiata al file .cover per recurse mostra che il corpo di not_called() non viene in effetti mai eseguito, indicato dal prefisso >>>>>>.

Relazioni di Chiamata

Oltre alle informazioni di coverage, trace raccoglie e riporta le relazioni tra le funzioni che si chiamano l'una con l'altra.

Per una semplice lista delle funzioni chiamate si usa --listfuncs:

£ python -m trace --listfuncs trace_example/main.py

Questo è il programma principale.
ricorsione(2)
ricorsione(1)
ricorsione(0)

functions called:
filename: /usr/lib/python2.6/threading.py, modulename: threading, funcname: settrace
filename: trace_example/main.py, modulename: main, funcname: 
filename: trace_example/main.py, modulename: main, funcname: main
filename: trace_example/recurse.py, modulename: recurse, funcname: 
filename: trace_example/recurse.py, modulename: recurse, funcname: recurse

Per maggiori dettagli su chi sta eseguendo la chiamata si usa --trackcalls.

$ python -m trace --listfuncs --trackcalls trace_example/main.py

Questo è il programma principale.
ricorsione(2)
ricorsione(1)
ricorsione(0)

calling relationships:

*** /usr/lib/python2.6/trace.py ***
  --> /usr/lib/python2.6/threading.py
    trace.Trace.runctx -> threading.settrace
  --> trace_example/main.py
    trace.Trace.runctx -> main.

*** trace_example/main.py ***
    main. -> main.main
  --> trace_example/recurse.py
    main. -> recurse.
    main.main -> recurse.recurse

*** trace_example/recurse.py ***
    recurse.recurse -> recurse.recurse

Programmare l'Interfaccia

Per un maggiore controllo sull'interfaccia di trace, si può chiamare all'interno del proprio programma usando un oggetto Trace, che consente di impostare degli impianti ed altre dipendenze prima di eseguire una singola funzione od un comando Python da tracciare.

import trace
from trace_example.recurse import recurse

tracer = trace.Trace(count=False, trace=True)
tracer.run('recurse(2)')

Visto che l'esempio traccia solo all'interno della funzione recurse(), nel risultato non sono incluse le informazioni circa main.py

$ python trace_run.py
 --- modulename: recurse, funcname: recurse
recurse.py(5):     print 'ricorsione(%s)' % level
ricorsione(2)
recurse.py(6):     if level:
recurse.py(7):         recurse(level-1)
 --- modulename: recurse, funcname: recurse
recurse.py(5):     print 'ricorsione(%s)' % level
ricorsione(1)
recurse.py(6):     if level:
recurse.py(7):         recurse(level-1)
 --- modulename: recurse, funcname: recurse
recurse.py(5):     print 'ricorsione(%s)' % level
ricorsione(0)
recurse.py(6):     if level:
recurse.py(8):     return
recurse.py(8):     return
recurse.py(8):     return

Si sarebbe potuto produrre lo stesso risultato anche con il metodo runfunc(). Questo metodo accetta parametri posizionali e con parola chiave, i quali sono passati alla funzione quanto viene chiamata dal tracciatore.

import trace
from trace_example.recurse import recurse

tracer = trace.Trace(count=False, trace=True)
tracer.runfunc(recurse, 2)
$ python trace_runfunc.py
 --- modulename: recurse, funcname: recurse
recurse.py(5):     print 'ricorsione(%s)' % level
ricorsione(2)
recurse.py(6):     if level:
recurse.py(7):         recurse(level-1)
 --- modulename: recurse, funcname: recurse
recurse.py(5):     print 'ricorsione(%s)' % level
ricorsione(1)
recurse.py(6):     if level:
recurse.py(7):         recurse(level-1)
 --- modulename: recurse, funcname: recurse
recurse.py(5):     print 'ricorsione(%s)' % level
ricorsione(0)
recurse.py(6):     if level:
recurse.py(8):     return
recurse.py(8):     return
recurse.py(8):     return

Salvare i Dati Ottenuti

Anche le informazioni su conteggi e coverage possono essere registrate, prooprio come con l'interfaccia da riga di comando. I dati devono essere esplicitamente salvati, usando una istanza di CoveragaResults dall'oggetto Trace-

mport trace
from trace_example.recurse import recurse

tracer = trace.Trace(count=True, trace=False)
tracer.runfunc(recurse, 2)

results = tracer.results()
results.write_results(coverdir='coverdir2')
$ python trace_CoverageResults.py
ricorsione(2)
ricorsione(1)
ricorsione(0)

$ find coverdir2
coverdir2
coverdir2/trace_example.recurse.cover

Ecco il contenuto di coverdir2/trace_example.recurse.cover:

       #!/usr/bin/env python
       # -*- coding: UTF-8 -*-

>>>>>> def recurse(level):
    3:     print 'ricorsione(%s)' % level
    3:     if level:
    2:         recurse(level-1)
    3:     return

>>>>>> def not_called():
>>>>>>     print 'Questa funzione non è mai chiamata.'

Per salvare i dati di conteggio per la generazione di rapporti, si usano i parametri di Trace infile ed outfile

import trace
from trace_example.recurse import recurse

tracer = trace.Trace(count=True, trace=False, outfile='trace_report.dat')
tracer.runfunc(recurse, 2)

report_tracer = trace.Trace(count=False, trace=False, infile='trace_report.dat')
results = tracer.results()
results.write_results(summary=True, coverdir='/tmp')

Si passa un nome di file a infile per leggere dati precedentemente salvati, ed un nome di file a outfile per scrivere i nuovi risultati dopo la tracciatura. Se infile ed outfile sono uguali, si ottiene l'effetto di aggiornamento del file con dati comulativi.

$ python trace_report..py
ricorsione(2)
ricorsione(1)
ricorsione(0)
lines   cov%   module   (path)
    7    57%   trace_example.recurse   (/home/robby/pydev/pymotw-it/dumpscripts/trace_example/recurse.py)

Opzioni di Trace

Il cotruttore di Trace riceve parecchi diversi parametri opzionali per controllare il comportamento in fase di esecuzione.

count
Booleano. Attiva il conteggio del numero della riga, Predefinito: True.
countfuncs
Booleano. Attiva una lista di funzioni chiamate durante l'esecuzione. Predefinito: False
countcallers
Booleano. Attiva la tracciatura di chiamanti e chiamati. Predefinito: False
ignoremods
Sequenza. Lista di moduli o pacchetti da ignorare nella raccolta di coverage durante la tracciatura. Predefinito: una tupla vuota
ignoredirs
Sequenza. Lista di directory che contengono moduli o package da ignorare. Predefinito: Tupla vuota.
infile
Nome del file che contiene i valori di conteggio recuperati. Predefinito: None
outfile
Nome del file da usare per conservare i conteggi recuperati. Predefinito: None, ed i dati non sono salvati.

Vedere anche:

trace
La documentazione della libreria standard per questo modulo
Tracing a Program As It Runs
Il modulo sys comprende delle strutture per aggiungere all'interprete le proprio funzioni di tracciatura all'interprete in fase di esecuzione.
coverage.py
Il modulo coverage di Ned Batchelder
flgleaf
L'applicazione di coverage di Titus Brown