Unit Testing dinamico per Ada con Python

Quella che segue è una bozza, l’elaborato consegnato al professore è qua:

Presto spero :-) anche i sorgenti.

### Unit Testing dinamico per Ada con Python ###

#### Abstract ####

Da tempo è noto che fare Test fin dalle prime fasi della stesura di un programma è una procedura che porta presto dei vantaggi.

Test ben fatti ed il più possibile esaustivi possono risparmiare tediosi problemi dopo il rilascio di un prodotto o nelle successive fasi di aggiornamento.

Ada offre alcuni strumenti per facilitare la stesura ed il controllo dei Test, ma spesso il voler generare Test nello stesso linguaggio dell’applicazione può risultare un limite.

Oggi possiamo sfruttare una costellazione di linguaggi dinamici ed interfacciarli in modo molto semplice ad un programma scritto in Ada.

Ad esempio Python permette di accedere alle funzioni esportate in C con una minima sforzo ed offre una enorme quantità di librerie, da un’ottima suite di UnitTesting, a librerie per i più svariati compiti (SQL, internet, interfacce grafiche, calcolo matriciale, plot…).

#### Caso di studio ####

Supponiamo di essere nelle prime fasi della progettazione di un sistema di irrigazione per serre.

Il sistema è composto da dei componenti forniti di sensori (**ingressi**):

* 3 sensori di umidità
* Uscita normalizzata tra 0 e 100

* 3 sensori di temperatura
* Temperatura in gradi Celsius

* 3 sensori di luminosità
* Intensità liminosa normalizzata tra 0 e 100

Il sistema di controllo ha le seguenti **uscite**:

* Pompa di irrigazione/nebulizzazione

* Illuminazione

* Riscaldamento

Le specifiche richiedono:

* Funzionamento anche in carenza di misurazioni secondo un programma standard.

* Identificazione e segnalazione di malfunzionamenti nei vari componenti.

#### Progetto dei test ####

I Test di questo progetto sono abbastanza complessi, una volta scelta una logica di comportamento ed implementato il software, per avere dei Test affidabili sarà necessario implementare anche un simulatore di Pompa ed dei simulatori di Sensori.

Se scegliamo di implementare i Test in Ada avremo una crescente quantità di software da gestire e, nel caso di logica complessa, da correggere.

Scegliendo invece di fare i Test in un linguaggio di scripting come Python, abbiamo la possibilità di scrivere i Test in modo più compatto ed inoltre possiamo aggiungere nuovi casi senza ricompilare l’applicazione, tutto questo con ovvi vantaggi nei tempi di sviluppo.

Il problema di questo approccio era, fino a pochi anni fa, l’obbligo di sfruttare un linguaggio tipo C per collegare Ada a Python. Oggi esistono almeno due alternative, Pyrex e ctypes.

#### Tecnologie ####

Prendiamo ad esempio solo l’interfacciamento con Python, alternativamente potremmo sfruttare uno dei tantissimi linguaggi di scripting esistenti (Perl, Ruby, Lua…) perché ad oggi tutti offrono potenzialità analoghe.

##### Python

Dalla documentazione ufficiale:

> Python è un linguaggio di programmazione potente e di facile apprendimento. Utilizza efficienti strutture dati di alto livello e un semplice ma efficace approccio alla programmazione orientata agli oggetti. L’elegante sintassi di Python e la tipizzazione dinamica, unite alla sua natura di linguaggio interpretato, lo rendono ideale per lo scripting e lo sviluppo rapido di applicazioni in molte aree diverse e sulla maggior parte delle piattaforme.

> L’interprete Python e l’ampia libreria standard sono liberamente disponibili, in file sorgenti o binari, per tutte le principali piattaforme sul sito web di Python, http://www.python.org/, e possono essere liberamente distribuiti. Lo stesso sito contiene anche, oltre alle distribuzioni, puntatori a molti moduli Python liberi e gratuiti di terzi, interi programmi, strumenti di sviluppo e documentazione addizionale.

> L’interprete Python è facilmente estendibile con nuove funzioni o tipi di dato implementati in C o C++ (o altri linguaggi richiamabili dal C). Python è anche adatto come linguaggio di estensione per applicazioni personalizzabili.

Questo è vero, ma negli anni sono nati alcuni progetti che semplificano ancora l’interazione tra Python e C.

##### Pyrex

Dal sito ufficiale:

> Pyrex è un linguaggio specificamente disegnato per scrivere moduli di estensione per Python. E’ progettato per fare da ponte tra il piacevole, di alto livello, semplice da usare, mondo del Python, al confusionario, di basso livello, mondo del C.

Pyrex è un metalinguaggio fortemente ispirato al Python ma con alcune aggiunte che permettono ad un compilatore di generare codice C.

Pyrex permette di accedere con estrema semplicità a tutte le librerie che esportino funzioni nello standard del C, e quindi anche alle funzioni Ada.

##### Ctypes

Dal sito ufficiale:

> Ctypes è un modulo Python per creare e manipolare tipi di dato C in Python e per chiamare funzioni condivise in dll. Ctypes permette di interfacciare queste librerie interamente in Python.

Con questo modulo possiamo accedere ad ogni libreria condivisa in modo dinamico, senza necessità di ricompilazioni, inoltre è possibile creare delle funzioni Python in modo che possano essere chiamate da C.

##### Esempio

Abbiamo un minimale package Ada con due funzioni:

**adahalf.adb**
[ada] with Ada.Text_IO;
package body AdaHalf is
function getHalf_from_Ada(Number : in Integer) return Integer is
Not_Even_Error : exception;
begin
if (Number rem 2 = 0) then
return Number / 2;
else
raise Not_Even_Error;
end if;
end getHalf_from_Ada;
function getHalf_from_Py(Number : in Integer) return Integer is
Not_Even_Error : exception;
begin
return pyhalf(Number);
end getHalf_from_Py;
end AdaHalf;[/ada]

Decidiamo di esportare entrambe le funzioni e di importare la funzione *pyhalf()*.

**adahalf.ads**
[ada] package AdaHalf is
function getHalf_from_Ada(Number : in Integer) return Integer;
pragma export(C, getHalf_from_Ada);
function getHalf_from_Py(Number : in Integer) return Integer;
pragma export(C, getHalf_from_Py);

function pyhalf(Number: in Integer) return Integer;
private
pragma import(C, pyhalf);
end AdaHalf;[/ada]

Vediamo adesso il semplice codice in Pyrex che si aggancia a questo package Ada:

**half.pyx**
[pyrex] cdef extern void adainit()
cdef extern void adafinal()
cdef extern int gethalf_from_ada(int number)

def adahalf(int number):
“”"Given an even number, returns it’s half”"”
adainit()
val = gethalf_from_ada(number)
adafinal()
return val

cdef public int pyhalf(int number):
“”"Given an int gets half from python”"”
import halfpy
return halfpy.half(number)

cdef extern int gethalf_from_py(int number)

def adapyhalf(int number):
“”"Python visible for public c-pyhalf passing from ada”"”
return gethalf_from_py(number)[/pyrex]

Le funzioni *adainit()* ed *adafinal()* sono necessarie per gestire l’ambiente Ada, le funzioni definite con def sono accessibili da Python, le funzioni definite con con cdef sono esportare in C.

Infine il modulo halfpy, modulo scritto in Python e quindi modificabile ma reso accessibile tramite Pyrex al package Ada.

**halfpy.py**
[python] def half(number):
“”"Given an even number, returns it’s half”"”
if number % 2 == 0:
return number / 2
else:
raise ValueError(“Number submited is Odd!”)[/python]

Sono evidenti le potenzialità di questo approccio, ogni funzione non ancora implementata in Ada può essere temporaneamente sostituita con una funzione Python dal comportamento analogo. Inoltre anche l’ambiente esterno può essere simulato semplicemente ad i cambiamenti nel comportamento di una risorsa esterna possono essere guidati anche direttamente dalla shell interattiva di Python.

#### Struttura dei test ####

Python offre due metodi per fare Test, il primo è il modulo **unittest**, modellato sulle linee guida di **JUnit**, il secondo è **doctest**.
Soffermiamoci sul secondo modulo per spiegarne le caratteristiche.
Doctest è nato per limitare il problema della manutenzione della documentazione, è un modulo che permette di estrarre in modo automatico i commenti dal codice sergente e, se riconosce il commento come codice eseguito alla shell interattiva, confronta il risultato presente nella documentazioni con quello di una nuova esecuzione.
Prendiamo ad esempio il modulo half.py e completiamolo con un semplice esempio d’uso:

**halfpy.py**
[python]def half(number):
“”"
Given an even number, returns it’s half.

>>> half(2)
1
>>> half(23)
Traceback (most recent call last):

ValueError: Number submited is Odd!
“”"
if number % 2 == 0:
return number / 2
else:
raise ValueError(“Number submited is Odd!”)

#Questo e’ il modo standard per rendere il modulo doctestabile
def _test():
import doctest, halfpy
return doctest.testmod(halfpy)

if __name__ == “__main__”:
_test()[/python]

Adesso eseguendo il test possiamo verificare che la documentazione è aggiornata e che il modulo opera come previsto:

[python]#python halfpy.py -v
Running halfpy.__doc__
0 of 0 examples failed in halfpy.__doc__
Running halfpy._test.__doc__
0 of 0 examples failed in halfpy._test.__doc__
Running halfpy.half.__doc__
Trying: half(2)
Expecting: 1
ok
Trying: half(23)
Expecting:
Traceback (most recent call last):

ValueError: Number submited is Odd!
ok
0 of 2 examples failed in halfpy.half.__doc__
2 items had no tests:
halfpy
halfpy._test
1 items passed all tests:
2 tests in halfpy.half
2 tests in 3 items.
2 passed and 0 failed.
Test passed.[/python]

Con questo semplice esempio possiamo capire quanto possa essere utile nel caso di progetti con logica complessa avere un metodo comodo per spiegare cosa sta accandendo.
Prendendo spunto dal *Literate Programming* cercheremo quindi di fare *Literate Testing*.

#### Logica del sistema di controllo ####

##### Sensori ####

I sensori operano tutti secondo la stessa logica:

* La misura restituita è la media delle misure fatte (una, due o tre).

* Il modulo ha uno stato che conserva l’indice dei sensori che hanno fornito l’ultima misura valida.

* In assenza totale di misure il modulo di gestione dei sensori genera un’eccezione.

##### Parametri operativi #####

Il sistema presenta una logica stabile con dei parametri impostabili dall’utente:

* L’**umidità** deve essere compresa nell’intervallo (*UmidMin*, *UmidMax*). Il sistema controlla l’umidità ogni *DtUmid*:=*DtUmidStandard*:
* Se *UmidNow* < *UmidMin* => Attiva l’irrigazione per *DtIrriga* => Imposta *DtUmid*:=*DtUmidIrriga*
* Se *UmidNow* > *UmidMax* => disattiva l’irrigazione => Imposta *DtUmid*:=*DtUmidStandard*
* Se *UmidNow* < *UmidMin* per *nUmid*>1 volte *DtUmidStandard* => Irrigazione Insufficiente.

* La **luminosità** deve essere compresa nell’intervallo (*LumMin*, *LumMax*) nell’intervallo (*tAlba*, *tTramonto*), altrimenti le luci devono essere spente. *LumMax* rappresenta l’intensità massima fornita dall’illuminazione artificiale.
* Se *tNow* in (*tAlba*, *tTramonto*):
* Se *LumNow* < *LumMin* => Accende le luci
* Se *LumNow* > *LumMax* => Spenge le luci (Si suppene che la luminosità della serra superi la luminosità artificiale solo in caso di illuminazione naturale)
* Se le luci sono accese ma *LumNow* < *LumMin* => segnala Illuminazione Insufficiente
* Se *tNow* not in (*tAlba*, *tTramonto*) => Luci spente

* La **temperatura** deve essere compresa nell’intervallo (*TempMin*, *TempMax*). Stessa logica del caso dell’umidita. Il sistema controlla la temperatura ogni *DtTempStandard*:
* Se *TempNow* < *TempMin* => Attiva o riscaldamento per *DtScalda* => Imposta *DtTemp*:=*DtTempScalda*
* Se *TempNow* > *TempMax* => disattiva il riscaldamento => Imposta *DtTemp*:=*DtTempStandard*
* Se *TempNow* < *TempMin* per *nTemp*>1 volte *DtTempStandard* => Riscaldamento Insufficiente.

Comments are closed.