Pages

Sunday, January 8, 2012

Test Driven Development, C++ e Qt: ottimizzazioni layout progetti

Nella puntata precedente abbiamo creato un primo "vero" test di dominio perfettamente funzionante; il ciclo del TDD prevede a questo punto di effettuare un po' di refactoring, se necessario, prima di iniziare la nuova funzionalità.
In questo capitolo ci occuperemo non tanto di refactoring relativi al design (qualcosa si può fare, ma avendo scritto pochissimo codice è davvero poca roba), quanto piuttosto di trasformare quello che adesso è codice "sperimentale" in qualcosa di più professionale, usando layout di progetto, ottimizzazioni e convenzioni che sono da favorire in progetti pronti per la produzione.
Ma che problemi ha il nostro progettino?
In realtà un bel po', cominciamo dalle cose più evidenti: la prima è che i progetti sono collegati tra loro piuttosto malamente.
Sappiamo che il progetto DownloadManager deve usare DownloadManagerCore, ma al momento non c'è nessun riferimento ad esso. Sappiamo anche che il progetto UnitTests deve fare, e in realtà fa già, la stessa cosa. Ma per farlo abbiamo dovuto aggiungere la riga:

    LIBS += -L../DownloadManagerCore-build-desktop/ -lDownloadManagerCore

e di certo non vorremo tenerla così: quando infatti dovremo compilare in modalità release sarebbe necessario modificare manualmente il file di progetto (tanto nel progetto UnitTests quanto in DownloadManager), e non è sicuramente il massimo!
Ma anche nel codice sorgente c'è qualcosina che non va: in downloadmanagercoretest.cpp vediamo la riga

    #include "../DownloadManagerCore/downloadmanagercore.h"

che dovremmo ripetere per ogni futuro test case. Non va bene!

Per fortuna qmake ci viene in aiuto con alcune funzioni interessanti.
Aggiungendo al file di progetto la riga

    INCLUDEPATH += ../DownloadManagerCore

rendiamo disponibli tutti gli header di DownloadManagerCore al progetto UnitTests, trasformando quindi la riga precedente in

    #include "downloadmanagercore.h"

Applichiamo questa modifica anche al progetto DownloadManager.

Per distinguere invece tra debug e release, usando anche un path migliore per la direttiva LIBS dobbiamo invece usare una funzionalità un po' più avanzata di qmake.
L'idea alla base è di compilare DownloadManagerCore, installarlo in una directory comune (con suffisso -debug o -release a seconda di come l'abbiamo compilata) e far puntare gli altri progetti a quella directory.
Per far questo dobbiamo aggiungere questa sezione a tutti i file di progetto (quindi DownloadManagerCore, DownloadManager e UnitTests):


win32 {
    build_pass:CONFIG(debug, debug|release)   { COMPILE_MODE='debug'   }
    else:build_pass                           { COMPILE_MODE='release' }
} else {
    CONFIG(debug, debug|release)   { COMPILE_MODE='debug' }
    else                           { COMPILE_MODE='release' }
}


Brevemente, viene assegnata una variabile COMPILE_MODE con un valore dipendente da come si compila il progetto corrente.
win32 viene gestito a parte perchè ho notato che COMPILE_MODE rimane vuota con la sintassi specificata nel ramo else.
Probabilmente dipende dal tipo di compilatore in uso, la sezione nel ramo win32 è stata testata su visual c++ 2010, probabilmente usando invece mingw non è ncessaria.
In ogni caso, provate a vedere se COMPILE_MODE viene popolata o meno usando l'istruzione


message("Using compile mode: $$COMPILE_MODE")



Adesso che abbiamo definito la variabile, proviamo ad usarla.
Nel progetto DownloadManagerCore aggiungiamo il path per l'installazione.



target.path='../static-libraries.bin/$$COMPILE_MODE'
INSTALLS += target


Questo farà in modo che eseguendo make install la libreria verrà copiata nella directory static-libraries.bin/debug (o release).
È consigliabile anche modificare i profili del progetto in qt creator: in "projects->build settings", tra i build steps, aggiungiamo al passo make l'argomento install. Questo per tutti i profili di DownloadManagerCore.
In questo modo, ad ogni build, la libreria statica verrà installata automaticamente.
Ora nei progetti DownloadManager e UnitTests andiamo ad usare questa nuova funzionalità. Molto semplicemente, la riga "LIBS+= ....." vista in precedenza, diventerà:


LIBS += -L../static-libraries.bin/$$COMPILE_MODE -lDownloadManagerCore

aggiungendola a DownloadManager dove non esisteva ancora.
Il modo migliore per verificare il risultato di quello che abbiamo fatto è cancellare tutte le directory di build, ricompilare da capo il progetto UnitTests (e naturalmente la sua dipendenza, DownloadManagerCore) e verificare che i test passino ancora.

Un' ultima ottimizzazione: risulta spesso utile in molti progetti avere una directory dove inserire script per automatizzare i task più frequenti, che spesso richiederebbero una serie di operazioni manuali non proprio comode. Creiamo quindi una directory "scripts". Che script possiamo scrivere per inaugurarla?
Beh, un primo task noioso e facilmente automatizzabile è la compilazione ed esecuzione automatica dei test: vogliamo uno script che sia in grado di compilare anzitutto il progetto DownloadManagerCore, installarlo nella directory static-libraries.bin, quindi compilare ed eseguire automaticamente i tests.

#!/bin/bash
set -e

PROJECTS="UnitTests"
COLORIZE_CMD="ccze -A"

MAIN_PROJECT="DownloadManagerCore"

BASE_DIRECTORY="$PWD/$( dirname $0)/.."
COMPILE_BUILD_PREFIX="$BASE_DIRECTORY/.build-"


if test "x$TERM" != "xxterm" || test "x$( which $( echo $COLORIZE_CMD | cut -d " " -f 1 ) )" == "x"; then #TODO add other terminals supported by ccze here
        COLORIZE_CMD="cat"
fi

if test "x$1" == "x--help"; then
        echo "Usage: $0 [Test-Project-Name [test-method-name]]"
        echo "It runs every project configured in the PROJECTS variable, or can select from parameters"
        echo "The variable MODE can be used to choose between debug and release compilation"
        echo "Currently supported test projects: $PROJECTS"
        exit 0
fi

if test "x$1" != "x"; then
        PROJECTS="$1"
        [ -f $PROJECTS ] && PROJECTS="$( basename "$( dirname "$PROJECTS")" )"
        echo "**** Project: $PROJECTS"
        shift
fi

BUILD_MODE="${MODE-debug}"

function buildProject() {
        project="$1"
        shift
        make_args="$1"
        shift
        #rm -rf "$project.build"
        mkdir -p "${COMPILE_BUILD_PREFIX}$project"
        cd "${COMPILE_BUILD_PREFIX}$project"
        qmake "CONFIG+=$BUILD_MODE silent" "DEFINES += QT_NO_DEBUG_OUTPUT" "$@" "../$project"
        make $make_args > /dev/null
        cd ..
}


function runTests() {
        suitename="$1"
        shift
        echo -e "Running suite $suitename\n"
        cd "${COMPILE_BUILD_PREFIX}$suitename"
        binary="./$suitename" 
        $binary $@ | while read line; do echo "$line" | $COLORIZE_CMD ; done
        ERRORS=${PIPESTATUS[0]}
        cd ..
        if test "x$ERRORS" == "x0"; then
                echo -e "\nSuite $suitename completed without errors\n"
        else
                echo -e "Suite $suitename finished with errors!\n"
                exit $ERRORS
        fi
}


buildProject $MAIN_PROJECT "all install"
for project in $PROJECTS; do
        buildProject $project "clean all" "LIBS+=-L../${COMPILE_BUILD_PREFIX}$MAIN_PROJECT" 
        runTests $project $@
done


Come al solito, i sorgenti per questa puntata sono disponibili sul mio account github.



Nella prossima puntata ci occuperemo invece di refactoring e ottimizzazioni del codice vero e proprio.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.