Dopo aver introdotto a livello teorico la pratica della Continuous Integration, è giunto il momento di mettere in pratica quanto abbiamo visto. In questo articolo parleremo dell'automazione lato client, nello specifico introdurremo phing, uno strumento di build basato su Ant, attraverso il quale definire una serie di operazioni automatizzate, tramite l'uso di un file XML.
I nostri esempi saranno contestualizzati in ambiente LAMP (nel nostro caso Ubuntu), ma attraverso opportuni adattamenti saranno trasportabili anche su altri ambienti.
Standardizziamo i nostri progetti
Per prima cosa definiamo una struttura cartelle coerente per i nostri progetti:
.
├── application/
│ ├── model/
│ └── bootstrap.php
│
├── build/
│
├── _data /
│ │
│ ├── build.properties
│ ├── citest.create.sql
│ ├── citest.data.sql
│ ├── citest.fixtures.sql
│ ├── citest.structure.sql
│ ├── dbdump.sh
│ ├── dbload.sh
│ └── install.sh
│
├── _tests/
│ │
│ └── SomeThingTest.php
│
├── utils/
│ │
│ └── yuicompressor-2.4.7.jar
│
├── web
│ │
│ ├── css/
│ ├── js/
│ └── index.php
│
└── build.xml
All'interno di _data avremo una serie di files “di servizio” utili all'automatizzazione di nostri compiti. All'interno di web e application troveremo, rispettivamente, la parte pubblica della nostra webapp e la relativa business logic. In _test troveremo i test automatici, mentre in utilis tutte le librerie esterne che possono essere a supporto del nostro sistema di build.
Il file build.xml è infine l'oggetto principale di questo articolo, ed andremo a dettagliarlo bene nelle prossime righe.
Installiamo phing
Procediamo con l'installazione di phing, completo di tutte le dipendenze, attraverso PEAR.
Con permessi di root:
# pear channel-discover pear.phing.info
# pear config-set auto_discover 1
# pear config-set preferred_state alpha
# pear install --alldeps phing/phing
# pear config-set preferred_state stable
Attraverso la prima istruzione diremo a PEAR di eseguire la discover del canale per l'installazione di phing, mentre nelle successive 2, lo istruiremo ad eseguire la discover automatica per tutte le dipendenze e di utilizzare anche pacchetti in versione alpha (questo è necessario per ottenerne alcuni che possono risultare utili durante la scrittura delle nostre procedure automatiche).
Installiamo infine lo strumento e rimettiamo a posto PEAR, ripristinando la preferenza di scaricare elementi marcati come stable.
Se tutto è andato a buon fine, avremo a disposizione il comando phing, insieme ad altri tool come phpunit e phpcs.
Il file build.xml
Ora che phing è installato non ci resta che lanciarlo la prima volta, ottenendo il messaggio:
$ phing
Buildfile: build.xml does not exist!
Questo errore è normale: phing fa uso di un file xml per la definizione dei task da automatizzare, andiamo a crearlo:
<?xml version="1.0" encoding="UTF-8"?>
<project name="citest" basedir="." default="hello">
<property file="_data/build.properties"/>
<!-- HELLO TARGET-->
<target name="hello">
<echo msg="Hello World !!!" />
</target>
</project>
Il file è composto da un tag root <project> all'interno del quale troveremo uno o più <target>. Sono questi ultimi che definiranno le nostre operazioni, attraverso l'uso di task. Nel nostro primo esempio abbiamo definito un target “hello”, che stampa da console la celebre frase tramite il task <echo>.
Attraverso il tag <property> possiamo definire una serie di proprietà, in questo caso salvate su file, da poter utilizzare come variabili all'interno del nostro script (es: parametri di connessione a db, path dove risiede la cartella pubblica del progetto, ecc...).
E' consigliabile utilizzare due versioni del file build: build.properties_template da porre sotto controllo di versione ed uno specifico dell'utente, build.properties, che verrà personalizzato alla prima checkout e posto in ignore per evitare il dilagare di configurazioni personali nel repository. Anche in questo caso vale la regola di non stravolgere il template e seguire il più possibile le linee guida di default, definite in fase di avvio del progetto.
Creiamo un po' di target
Iniziamo a creare dei target utili ad impostare il nostro ambiente di Continuous Integration.
Setup iniziale del client
Definiamo un target per l'automatizzazione del setup iniziale del client di sviluppo:
<!-- INIT DEVELOPER ENVIRONMENT -->
<target name="init-dev-env"
description="### necessita permessi di root ### prepara l'ambiente di
sviluppo locale configurando apache e file di host">
<echo msg="Installazione in locale del progetto" />
<exec command="sh install.sh ${public-dir} ${vhosts-dir} ${apache-exe}"
dir="_data" checkreturn="true" passthru="true" />
</target>
Ottenendo quindi il comando (da lanciare con permessi di root):
# phing init-env-dev
Attraverso questo primo target, abbiamo automatizzato il setup del client, impostando il file degli hosts e creando un virtual host dedicato alla nostra web application. Phing si occuperà di passare i parametri necessari allo script di installazione (vi rimando al download dei sorgenti per i dettagli). In questo caso lo script è per Ubuntu, ma attraverso il task <if>, la proprietà os.name ed altri possibili parametri di configurazione in build.properties sarà possibile supportare qualsiasi sistema operativo e configurazione (es: lanciare dei .bat su windows, piuttosto che dei .sh).
Operiamo con il database
Uno dei compiti che risulta vantaggioso automatizzare è la gestione dei dati del database. La pratica della Continuous Integration vuole che il DB sia posto sotto controllo di versione, per cui possiamo definire due target: uno per popolare il DB locale tramite dati provenienti da file .sql (db-load), l'altro per l'operazione opposta (db-dump).
<!-- LOAD DB DATA FROM SQL FILE -->
<target name="db-load"
description="Popola il database partendo da _data/data.sql se non diversamente specificato tramite -Ddb.data-source=datasource">
<property name="db.data-source" value="data" override="false" />
<echo msg="Loading ${db.data-source} on ${db.name} ..." />
<exec command="sh dbload.sh '${db.params}' ${db.name} ${db.data-source}" dir="_data" checkreturn="true"/>
<echo msg="Done!" />
</target>
<!-- DUMP DB DATA TO SQL FILE -->
<target name="db-dump"
description="Esegue il dump del database su _data/data.sql se non diversamente specificato tramite -Ddb.data-dest=datadest">
<property name="db.data-dest" value="data" override="false" />
<echo msg="Dumping ${db.name} on ${db.data-dest} ..." />
<exec command="sh dbdump.sh '${db.params}' ${db.name} ${db.data-dest}" dir="_data" checkreturn="true"/>
<echo msg="Done!" />
</target>
Entrambi i target accettano un parametro per specificare quale sia la fonte (o la destinazione) dei dati. Nel nostro esempio siamo interessati a data.sql contenente generici dati simili a quelli di produzione, e fixtures.sql per dati utili nei test.
Lanciamo i test automatici
Per l'esecuzione dei test automatici, definiamo due target: do-tests che lancia i test, e test che usa il primo e si occupa inoltre di sistemare il database.
<!-- TEST WITH PHPUNIT -->
<target name="do-tests"
description="Lancia i test automatici sulla code base">
<phpcodesniffer standard="Zend" format="summary" file="${path}"
allowedFileExtensions="php php5 inc"
haltonwarning="true" haltonerror="true" />
<phplint>
<fileset dir="web">
<include name="**/*.php"/>
</fileset>
<fileset dir="_tests">
<include name="**/*.php"/>
</fileset>
</phplint>
<phpunit bootstrap="./application/bootstrap.php"
haltonfailure="true" haltonerror="true">
<formatter type="plain" usefile="false"/>
<batchtest>
<fileset dir="_tests">
<include name="**/*Test*.php"/>
</fileset>
</batchtest>
</phpunit>
</target>
Con il primo target abbiamo introdotto i task <phpcodesniffer> per il rispetto del code style, <phplint> per individuare subito eventuali parse error nel codice ed infine <phpunit> per il lancio dei test automatici veri e propri.
Introduciamo adesso il target test: come possiamo vedere esso salva il database corrente all'interno di un file temporaneo, lo popola con dati di fixtures e successivamente lancia do-tests. Infine ripristina il database allo stato precedente al lancio dei test.
<!-- TEST WITH DB -->
<target name="test"
description="Carica le fixtures nel database, lancia i test automatici e riporta il database allo stato precedente">
<phingcall target="db-dump">
<property name="db.data-dest" value="tempdata" />
</phingcall>
<phingcall target="db-load">
<property name="db.data-source" value="fixtures" />
</phingcall>
<trycatch property="test-error">
<try>
<phingcall target="do-tests" />
</try>
<catch>
<!-- just catch the exception to keep clean the output -->
</catch>
<finally>
<phingcall target="db-load">
<property name="db.data-source" value="tempdata" />
</phingcall>
</finally>
</trycatch>
<fail if="test-error" msg="Something gone wrong with your tests!" />
</target>
In questa fase è interessante notare l'utilizzo del task <trycatch>: qualora i test fallissero (lanciati all'interno della clausola <try>) è possibile definire un'operazione di recovery all'interno di <catch> (niente nel nostro caso).
Abbiamo messo infine il ripristino del database nella clausola <finally>: questa viene sempre e comunque eseguita, così da non finire in uno stato incoerente qualora i nostri test fallissero. Attraverso la proprietà test-error, riportata da <trycatch> possiamo far fallire la build in caso di errore nei test, con il task <fail>.
Esecuzione della build
Abbiamo quasi tutto il necessario per lanciare la nostra build, attraverso l'uso di un solo comando. Introduciamone il relativo target ed andiamo a spiegarlo:
<target name="build" depends="clean,test"
description="Testa il software, copia tutti i file necessari al deploy nella cartella build ed esegue il minify dei file .js e .css" >
<mkdir dir="build" />
<copy todir="build" >
<fileset dir=".">
<include name="application/**" />
<include name="web/**" />
</fileset>
</copy>
<echo msg="Minifying JS files ..." />
<jsMin targetDir="build/web/js" failOnError="false" suffix="">
<fileset dir="web/js">
<include name="**/*.js"/>
</fileset>
</jsMin>
<echo msg="Done!" />
<echo msg="Minifying CSS files ..." />
<foreach param="filename" absparam="absfilename" target="minify-css-file">
<fileset dir="build/web/css">
<include name="*.css"/>
</fileset>
</foreach>
<echo msg="Done!" />
</target>
Come possiamo vedere il nostro target, prima di essere eseguito, ne lancia altri due: il primo, clean, per eliminare residui delle build precedenti, ed il secondo, test, per testare il software.
Fatto ciò verranno eseguite una serie di operazioni per ottenere tutti i nostri files necessari al deploy: dalla copia dei sorgenti nella cartella build, al lancio del minify dei file .js tramite il task <jsMin> e quelli .css tramite un target da noi creato, il minify-css-file, che fa uso del tool yui-compressor.
A build completata, troveremo nella cartella build il nostro software pronto al deploy.
Per completezza, introduciamo i due target mancanti:
<!-- CLEAN -->
<target name="clean"
description="Elimina la build corrente">
<delete dir="build" includeemptydirs="true"
verbose="true" failonerror="true" />
</target>
<!-- UTILITY PER IL MINIFY DEI CSS CON YUI COMPRESSOR -->
<target name="minify-css-file">
<exec command="java -jar ${yui-jarfile} -o build/web/css/${filename} build/web/css/${filename}" checkreturn="true" />
</target>
Conclusioni
Attraversi l'utilizzo di phing, abbiamo automatizzato la gran parte dei compiti lato client. Come possiamo vedere abbiamo tutto quanto richiesto dalla Continuous Integration: il database viene trattato come codice sorgente ed i test sono automatizzati. Come ultima cosa, ma non meno importante, la possibilità di lanciare una “one-commend build”, tramite la quale ottenere il nostro software testato e funzionante, pronto per il deploy.
A build funzionante, non ci resta altro che eseguire la commit verso il repository e passare la palla al Continuous Integration server.. ma questa è un'altra storia :)
Scarica i sorgenti proposti in questo articolo e crea il tuo ambiente.