Relazione - Toni Mancini

Transcript

Relazione - Toni Mancini
A.A. 2006-2007
Seminario per il corso di Metodi Formali per
l'Ingegneria del Software
Docente: prof. Toni Mancini
Traduzione degli Interaction Diagram in
Spin
Francesco Fia
Marco Augusto Lauretti
Introduzione………………………………………………………………………..…3
Parte I: Interaction Diagram …………………………………………………..……..4
Sezione 1.1 – Sequence Diagram
Sezione 1.1.1 – Descrizione dei messaggi nei Sequence Diagram
Sezione 1.1.1.1 – Messaggio asincrono
Sezione 1.1.1.2 – Messaggio sincrono
Sezione 1.1.2 – Combined Fragment
Sezione 1.1.3 – Esempio
Sezione 1.2 – Collaboration Diagram
Sezione 1.2.1 – Esempio
Parte II: Spin ………………………………………………………………...………12
Sezione 2.1 – Traduzione dei Sequence Diagram
Sezione 2.1.1 – Traduzione dei messaggi
Sezione 2.1.1.1 – Messaggio Asincrono
Sezione 2.1.1.2 – Messaggio Sincrono
Sezione 2.1.2 – Traduzione dei Combined Fragment
Sezione 2.1.2.1 - Traduzione Alternative Fragment (alt)
Sezione 2.1.2.2 - Traduzione Option Fragment (opt)
Sezione 2.1.2.3 - Traduzione Parallel Fragment (par)
Sezione 2.1.2.4 - Traduzione Critical Fragment (critical)
Sezione 2.1.2.5 - Traduzione Weak Sequencing Fragment (seq)
Sezione 2.1.2.6 - Traduzione Negative Fragment (neg)
Sezione 2.1.2.7 - Traduzione Ignore Fragment (ignore)
Sezione 2.1.2.8 - Traduzione Consider Fragment (consider)
Sezione 2.1.2.9 - Traduzione Assertion Fragment (assert)
Sezione 2.1.2.10 - Traduzione Loop Fragment (loop)
Sezione 2.1.2.11 - Traduzione Break Fragment (break)
Sezione 2.1.2.12 - Traduzione Strict Sequencing Fragment (strict)
Sezione 2.1.3 – Esempi
Sezione 2.2 – Traduzione dei Collaboration Diagram
Parte III: Verifica di proprietà ……………………………………………………....57
Sezione 3.1 – Tecniche di verifica
Sezione 3.2 – Proprietà da verificare
Sezione 3.3 – Esempi
Bibliografia ………………………………………………………………………….76
2
Introduzione
Il progetto prevede l’analisi degli Interaction Diagram e la loro traduzione in un
linguaggio di modellazione. Ci si è soffermati sui messaggi e sui Combined Fragment
presenti nei Sequence Diagram a partire dalla versione 2.0 di UML. Inoltre, vengono
analizzate proprietà ritenute interessanti.
Il linguaggio di modellazione scelto è PROMELA e si è utilizzato il suo interprete
SPIN. Si è scelto PROMELA in quanto consente la comunicazione tra processi
diversi mediante lo scambio di messaggi attraverso l’uso di canali di comunicazione.
3
Parte I: Interaction Diagram
Gli Interaction Diagram mostrano un’interazione tra un insieme di oggetti includendo
i messaggi che vengono scambiati tra di loro.
Vengono utilizzati per esprimere l’aspetto dinamico di un sistema.
Esistono due tipi principali di Interaction Diagram:
• Sequence Diagram
• Collaboration(Communication) Diagram
I Sequence diagrams e Collaboration diagrams sono sematicamente equivalenti fino
alla versione precedente al UML 2.0, infatti da questa versione sono stati introdotti i
Combined Fragment nei Sequence Diagram , i quali non sono rappresentati nei
Collaboration Diagram
Fino ad allora era possibile passare da un diagramma ad un altro.
Pongono enfasi su viste differenti
• sequence: scambio messaggi e tempo
• collaboration: scambio messaggi e struttura/relazioni
Sezione 1.1 – Sequence Diagram
I Sequence Diagram mostrano lo scambio di messaggi tra oggetti in un sistema
esprimendo un possibile comportamento di interazione di più’ oggetti ponendo
l’enfasi sull’ordine temporale (parziale) dei messaggi.
I Sequence Diagram mostrano in più rispetto ai Collaboration Diagram:
- le linee di vita degli oggetti
- il focus di controllo
I simboli usati nella progettazione dei sequence diagram sono:
- Simboli d’oggetto
- La linea di vita dell’oggetto (Life Line)
- Frecce di interazione tra oggetti
4
Il simbolo d’oggetto viene disegnato sopra una Life Line. La Life Line rappresenta
l’esistenza di un’istanza di un oggetto in un particolare momento.
Una Life Line può essere separata per rappresentare condizioni differenti di
esecuzione.
Lungo la lifeline si trova un piccolo rettangolo chiamato "attivazione" (activation).
L'activation rappresenta l'esecuzione di un'operazione di cui l'oggetto si fa carico. La
lunghezza del rettangolo, invece, rappresenta la durata dell'activation.
Le frecce di interazione rappresentano i messaggi scambiati tra gli oggetti e possono
essere di tre tipi:
Chiamata Sincrona
Chiamata Asincrona
Return
ll sequence diagram rappresenta il trascorrere del tempo se visto in direzione
verticale.
5
Il tempo, graficamente, viene fatto partire alla base di ogni oggetto e prosegue fino in
basso.
Ad esempio, un messaggio che si trovi più vicino all'oggetto rispetto ad un altro
(rispetto alla direzione verticale), si verificherà prima nel tempo.
Un oggetto può eseguire un'operazione che invochi lo stesso oggetto che l'ha
provocata. In tal caso si parla di ricorsione.
Ad esempio, si supponga che uno degli oggetti del sistema sia un calcolatore e che
una delle sue operazioni sia il calcolo degli interessi. Al fine di permettere il calcolo
per un lasso di tempo che racchiuda parecchi periodi, l'operazione per calcolare gli
interessi deve invocare se stessa diverse volte. Il sequence diagram nella figura
sottostante, mostra tale caso:
6
Si può dire che i Sequence Diagram in particolare possono essere usati per:
- Specificare come le istanze delle classi interagiscono tra loro
- Identificare le interfacce delle classi
- Ripartire tra le classi l’adempimento dei requisiti desiderati
Sezione 1.1.1 – Descrizione dei messaggi nei Sequence Diagram
Un’interazione tra oggetti è eseguita tramite l’invio di un messaggio da un oggetto
all’altro. L’invio di un messaggio da un oggetto A ad un oggetto B può essere inteso
per esempio come:
- A invoca un metodo dell’oggetto B
- B esegue l’operazione associata al metodo
- il controllo ritorna ad A con un eventuale valore di ritorno
Per ciò che riguarda la spedizione dei messaggi tra un oggetto e l’altro, si fa
l’assunzione che la durata dell’invio dei messaggi viene considerata atomica (nulla
accade nel frattempo).
I messaggi posso essere di due tipi:
- asincroni
- sincroni
Il messaggio è sincrono, se il sender rimane in attesa di una risposta, o asincrono, nel
caso in cui il sender non aspetti la risposta e questa può arrivare in un secondo
momento.
Il messaggio che viene generato in risposta ad un precedente messaggio, al quale si
riferisce anche come contenuto informativo, è detto messaggio di risposta.
Un messaggio, in cui il receiver è nello stesso tempo il sender, è detto ricorsivo.
7
Un messaggio asincrona indica una comunicazione asincrona, dove non c’è flusso di
controllo annidato
Il Sender invia lo stimolo e continua immediatamente con il prossimo passo
nell’esecuzione.
Un messaggio sincrono indica, invece, una chiamata di un’operazione o di un altro
flusso di controllo annidato.
Se c’è un flusso di controllo annidato, il controllo passa al chiamante dopo che è
terminata l’esecuzione del flusso annidato.
Sezione 1.1.2 – Combined Fragment
Un Sequence Diagram può essere suddiviso in piu’ frammenti ed è possibile
combinare tali frammenti, seguendo diverse regole attraverso i Combined Fragment .
Ogni Combined Fragmnet prende il nome dall’Interaction Operator utilizzato.
Gli InteractionOperator definiscono come tali frammenti possono essere
combinati insieme, cioè definiscono un insieme di regole .
Gli Interaction Operator sono:
• Alternative Fragment (alt)
L’operatore alt modella il construtto if…then…else, cioè rappresenta la
possibilità di scegliere solo una alternativa attraverso l’uso di guardie.
• Option Fragment (opt)
L’operatore opt indica che il frammento è opzionale, cioè verrà eseguito
tutto o non verrà eseguito.
• Parallel Fragment (par)
L’operatore par modella processi concorrenti, il cui ordine di
esecuzione è indifferente ma il contenuto al loro interno è prestabilito.
• Critical Fragment (critical)
L’operatore critical viene usato per definire tracce all’interno della
regione che non possono essere interlevead. Esiste un'unica seqenza di
azione valide. La regione verrà trattata atomicamente.
• Weak Sequencing Fragment (seq)
L’operatore seq rappresenta un variazione al “par” dove è permesso il
parallelismo solo tra regioni critiche, mentre i messaggi all’interno della
regione non possono essere scambiati.
8
• Negative Fragment (neg)
L’operatore neg rappresenta una serie di tracce da considerare invalide.
• Ignore Fragment (ignore)
L’operatore ignore rappresenta una lista di messaggi di non interesse,
insignificanti per il frammento e ignorati nell’esecuzione oppure che
possono apparire in qualsiasi punto dell’applicazione.
• Consider Fragment (consider)
L’operatore “consider” rappresenta invece la lista dei messaggi
interessanti per il frammento.
• Assertion Fragment (assert)
L’operatore assert serve per asserire qualcosa che deve necessariamente
avvenire, l’unico stato valido è quello previsto dal contenuto dell’assert.
• Loop Fragment (loop)
L’operatore loop viene utilizzato per ripetere più volte il frammento, una
guardia o una espressione booleana esprimono il numero di iterazioni.
• Break Fragment (break)
L’operatore “break” modella una sequenza alternativa di evento da
processare al posto del resto del diagramma.
• Strict Sequencing Fragment (strict)
L’operatore “strict” racchiude una serie di messaggi da processare
nell’ordine stabilito.
Sezione 1.1.3 – Esempio
Supponiamo di dover modellare la sequenza di azioni compiute su una Macchina
Self-Service per poter acquistare un prodotto, per esempio un distributore automatico
di bibite.
Definiamo i seguenti tre oggetti della Macchina Self-Service con cui descriveremo il
nostro diagramma:
• La Parte Frontale
• La Cassetta delle monete
• Il Contenitore dei Prodotti
La Parte Frontale è praticamente l'interfaccia che la macchina presenta all'utente. La
cassetta è la parte in cui vengono accumulate le monete. Questa parte si occupa anche
di gestire i vari controlli della macchina.
Il Contenitore dei prodotti è la sezione del distributore che contiene gli alimenti che
vengono acquistati dal cliente.
Il Sequence Diagram che modelleremo farà uso della seguente sequenza di azioni:
9
• Il cliente inserisce le monete nella macchina
• Il cliente esegue la selezione del prodotto desiderato
• Le monete arrivano nella Cassetta delle Monete
• Il dispositivo di controllo della Cassetta delle monete verifica se il prodotto
desiderato è presente nel Contenitore dei Prodotti
• La Cassetta delle Monete aggiorna la sua riserva di monete
• La parte frontale seleziona il prodotto dal Contenitore Dei Prodotti
• Il Contenitore Dei Prodotti espellere il prodotto desiderato dalla Parte Frontale
della macchina
Sezione 1.2 – Collaboration Diagram
I Collaboration Diagram rappresentano un tipo di diagramma molto simile a quello
visto nei Sequence Diagram.
Essi sono semanticamente equivalenti ma diversi per quanto riguarda il target di
interesse. In particolare i Collaboration Diagram oltre che mostrare le associazioni tra
gli oggetti, danno visibilità ai messaggi che gli oggetti si inviano per interagire tra
loro.
I messaggi vengono rappresentati con delle frecce che puntano all’oggetto che riceve
il messaggio stesso.
Tali frecce sono posizionate vicino la linea di associazione tra gli oggetti. Una label
vicino la freccia indica la specifica di un messaggio.
10
Un messaggio, tipicamente, richiede all'oggetto ricevente di eseguire una delle sue
operazioni. Una coppia di parentesi terminano il messaggio; al loro interno si trovano
gli eventuali parametri con cui le operazioni lavorano.
I collaboration diagrams possono essere convertiti in sequence diagrams e viceversa.
Per far ciò, però, si deve tenere presente che è necessario aggiungere un numero alla
label di un messaggio che corrisponda all'ordine in cui il messaggio appare nella
sequenza.
Sezione 1.2.1 – Esempio
Riprendiamo l’esempio del distributore Self-Service e traduciamo il Sequence
Diagram prodotto nel relativo Collaboration Diagram.
Lo scenario del caso migliore consiste di diversi passi:
•
•
•
Il cliente inserisce le monete nella macchina ed esegue le selezione di uno o
più prodotti presenti sulla macchina.
Quando il dispositivo di introduzione delle monete ottiene la somma dovuta (in
questo caso si suppone che il cliente inserisca il corretto numero di monete
necessarie) e vi è la disponibilità dei prodotti scelti, allora viene espulso dalla
macchina il prodotto scelto.
Il prodotto (o i prodotti) viene espulso dalla parte frontale della macchina in
modo che il cliente possa prelevarli.
Questo scenario si traduce nel seguente Collaboration Diagram:
11
Parte II: Spin
Spin è un verification tool usato per analizzare la consistenza logica di sistemi
concorrenti e, in particolare, dei protocolli di comunicazione in essi utilizzati.
Una volta definito formalmente un sistema attraverso il linguaggio di
modellazione Promela (PROcess MEta LAnguage), è possibile eseguire
simulazioni su di esso oppure analizzarne le proprietà di correttezza. Spin offre
inoltre un buon numero di ottimizzazioni nella creazione delle strutture dati per
l'analisi, che consentono di ridurre significativamente la memoria utilizzata e il
tempo di esecuzione.
Introduciamo alcuni concetti interessanti di PROMELA
Un canale è l'entità attraverso la quale dei processi possono scambiarsi dei
Messaggi.
chan nomecanale = [buffer] of { tipo1, tipo2 ...};
dove buffer indica la dimensione massima della coda, mentre tipo1,tipo2 indicano
quanti argomenti ha il messaggio (in base al loro numero) e quali tipi di dato
trasmettere; va mantenuto l’ordine dei tipo di dato definito nella dichiarazione del
canale.
Un tipo di dato interessante è mtype; mtype consente di definire etichette utili ai
processi per filtrare i messaggi.
mtype { SINC, ASINC};
Per scrivere su un canale bisogna rispettare la seguente sintassi:
nomecanale ! <expr1>, <expr2>, … <exprn>;
dove expr1,expr2 sono i dati da inviare del tipo definito nella dichiarazione del
canale, mentre per leggere da un canale:
nomecanale ? <var1>, <var2>, … <varn>;
dove var1,var2 indicano le variabili in cui andranno salvati i valori letti.
Per dichiarare un processo si utilizza l’istruzione proctype:
proctype nomeprocesso(parametri) {
/* corpo del processo */
}
12
Tra i parametri si possono anche includere i canali, solamente nei canali passati come
parametro il processo può interloquire.
Un processo può essere attivato anteponendo l’istruzione active nella sua definizione
oppure tramite l’istruzione
run nomeprocesso(parametri)
interna al “corpo del processo” di un altro processo.
Un particolare processo è init ; questo processo viene automaticamente inizializzato ,
non ha argomenti, non si possono creare più copie e viene utilizzato per inizializzare
altri processi tramite l’istruzione run
Alcune istruzioni interessanti per definire lo stato del canale sono:
• empty(nomecanale), la quale ritorna true se il canale è vuoto, false altrimenti.
• nempty(nomecanale), la quale ritorna true se il canale non è vuoto, false
altrimenti
• len(nomecanale), la quale ritorna il numero di messaggi presenti sul canale.
• full(nomecanale), la quale ritorna true se il canale è pieno
Consideriando il canale
chan canale = [1] of {mtype,mtype, byte};
bisogna fare alcune osservazioni:
• Se vengono inviati più dati di quelli previsti questi vengono persi,
o Ad esempio canale ! ASINC, SetValue, valore1,valore2
dove valore1 e valore2 sono byte, solamente il dato extra viene perso.
• Se invio meno dati di quelli previsti il valore mancante viene considerato come
indefinito
o Si tiene conto sia dell’ordine con cui sono definiti i parametri sia il tipo
di parametro. Ad esempio utilizzando canale ! valore si considerano
indefiniti i primi due valori mtype, se valore è un byte.
o Il sistema accetta meno parametri di quelli definiti ma avverte attraverso
un warning della mancanza di parametri
warning: missing params in send
o La ricezione del messaggio canale ? valore è possibile , se si tenta di
leggere un dato non inviato sarà considerato indefinito
o Il sistema nel caso canale ? valore avverte attraverso un warning
della mancanza di parametri
warning: missing params in next recv
13
• Se si deve usare un canale e tra i dati da inviare ci sono bool e byte il canale
deve avere la seguente forma:
chan canale = [1] of { byte,bool };
e mai
chan canale = [1] of { bool,byte };
in quanto SPIN tratta il bool come n bit, 1 per true e 0 per false e se si desidera
inviare solo il byte , questo nel secondo tipo di canale verrà confuso con il bool
e può portare ad errori; se invece si desidera inviare sono un bool il primo caso
non crea problemi in quanto un bool può essere trattato come un byte ma non il
contrario..
Esistono notazioni alternative e equivalenti al semplice elenco dei parametri.
Ad esmpio nel caso di
canale ! SetValue, ASINC, , valore
una notazione equivalente , simile alle funzioni nei linguaggi di programmazione, è
canale ! SetValue(ASINC, valore)
Sezione 2.1 – Traduzione dei Sequence Diagram
In questa sezione vengono tradotti i Sequence Diagram a partire dai messaggi,
passando per i Combined Fragment e infine mostrando qualche esempio.
Sezione 2.1.1 – Traduzione dei messaggi
In questa sezione ci occuperemo di tradurre l’interazione tra gli oggetti attraverso
l’invio di un messaggio asincrono e di un messaggio sincrono
In Promela possiamo definire i due tipi di messaggi tramite un’etichetta che permette
di identificare la tipologia di messaggio e quindi la tipologia di chiamata:
mtype { SINC, ASINC};
14
Sezione 2.1.1.1 – Messaggio Asincrono
Indica una comunicazione asincrona, dove non c’è flusso di controllo annidato
Il Sender invia lo stimolo e continua immediatamente con il prossimo passo
nell’esecuzione.
Supponiamo di avere due oggetti: un Sender che effettua le chiamate e un oggetto
Receiver sul quale è possibile chiamare 2 metodi: setValue e getValue.
Supponiamo quindi di voler modellare due oggetti Receiver e Sender.
L’oggetto Receiver espone il metodo setValue che non restituisce alcun valore e al
quale è possibile passare un parametro. L’oggetto Sender si occupa di chiamare il
metodo setValue passando un opportuno valore.
In pseudocodice:
Object Receiver {
var value;
function setValue(valore){
value = valore;
print(“Ricevuta chiamata a setValue dal Sender”);
print(“Letto parametro “ + valore);
}
}
Mentre il Sender:
Object Sender {
}
function main(){
Receiver r;
valore = 21;
r.setValue(valore);
}
Ovviamente la chiamata che Sender fa verso Receiver è di tipo asincrono.
15
Modelliamo la chiamata asincrona setValue nella modalità presentata di seguito.
Aggiungiamo alle tipologie di messaggio anche i due metodi che Receiver espone.
mtype { SINC, ASINC, setValue, getValue};
Definiamo un canale, di dimensione 1, che si occuperà di trasportare il messaggio dal
Sender al Receiver.
chan c = [1] of {mtype,mtype ,byte};
Il terzo parametro definito sul canale indica che stiamo trasferendo byte da un oggetto
all’altro.
Traduciamo i due oggetti come due procedure che hanno come canale per lo scambio
di messaggi, il canale definito in precedenza.
proctype Receiver(chan in) {
byte s;
do
:: in ? ASINC,setValue,s ->
printf("Ricevuta chiamata a setValue dal
Sender\n");
printf("Letto parametro %u\n",s);
od
16
}
proctype Sender(chan out) {
byte s;
s = 21;
out! ASINC,setValue,s;
}
Receiver invia quindi sul canale (detto OUT) il messaggio contenente il tipo di
chiamata, il nome del metodo che stiamo chiamando e il valore passato al metodo:
out! ASINC,setValue,s;
La procedura di init si occupa di inizializzare le procedure Sender e Receiver
passando il canale.
init {
run Sender(c);
run Receiver(c);
}
Per semplificare la lettura e renderla molto più simile alla chiamata di un metodo è
possibile definire il messaggio indicante la chiamata nel seguente modo:
out! ASINC,setValue(s);
Supponendo che il file contenente il codice mostrato in precedenza si chiami
asynch_message.pr, è possibile eseguire il codice tramite Spin semplicemente
eseguendo il comando:
> spin asynch_message.pr
Di seguito l’output dell’esecuzione del codice:
> Ricevuta chiamata a setValue dal Sender
> Letto parametro: 21
> timeout
> #processes: 3
> queue 1 (c):
> 10: proc 2 (Receiver) line 30 "asynch_message.pr" (state 4)
> 10: proc 1 (Sender) line 23 "asynch_message.pr" (state 5) <valid end state>
17
> 10:
proc 0 (:init:) line 42 "asynch_message.pr" (state 3) <valid end state>
Sezione 2.1.1.2 – Messaggio Sincrono
Indica una chiamata di un’operazione o di un altro flusso di controllo annidato.
Se c’è un flusso di controllo annidato, il controllo passa al chiamante dopo che è
terminata l’esecuzione del flusso annidato.
Riprendiamo l’esempio precedente, aggiungendo all’oggetto Receiver un metodo per
il recupero del valore settato tramite il metodo setValue. Il metodo si chiamerà
getValue. Tale metodo non prende alcun parametro in input, ma restiuisce un valore.
getValue indica quindi una chiamata sincrona, in cui il Sender aspetta di ricevere un
messaggio di risposta dopo aver inviato lo stimolo al Receiver.
In pseudocodice:
Object Receiver {
var value;
function getValue(){
print(“ Ricevuta chiamata a getValue dal Sender”);
print(“ Restituisco il valore “ + value”);
return value;
}
}
Mentre il Sender:
Object Sender {
function main(){
Receiver r;
print(“Valore di ritorno della chiamata: “ +
r.getValue());
}
}
Modelliamo quindi la chiamata sincrona getValue nel seguente modo.
18
Definiamo un singolo canale per la comunicazione tra gli oggetti. Tale canale sarà
half-duplex ma permetterà la comunicazione in entrambe le direzioni. Quindi sullo
stesso canale sarà possibile inviare i messaggi e ricevere le risposte.
mtype {SINC, ASINC,setValue,getValue};
chan c = [1] of {mtype,mtype, byte};
In Promela gli oggetti Sender e Receiver possono essere modellati come segue:
proctype Sender(chan canale) {
byte valore;
canale ! SINC,getValue;
do
:: canale ? valore ->
printf("Valore di ritorno della chiamata:
%u",valore);
od
}
proctype Receiver(chan canale) {
byte valore;
valore = 12;
do
19
:: canale ? SINC,getValue ->
printf("Ricevuta chiamata a getValue dal Sender\n");
printf("Restituisco il valore %u",valore);
canale ! valore;
od
}
Il Sender effettua la chiamata sincrona verso Receiver scrivendo il messaggio sul
canale:
canale ! SINC,getValue;
e si mette in attesa della risposta da Receiver:
do
:: canale ? valore ->
printf("Valore di ritorno della chiamata:
%u",valore);
od
Receiver invece attende le chiamate del proprio metodo, e quando le riceve, scrive sul
canale il valore di ritorno del metodo:
do
od
:: canale ? SINC,getValue ->
printf("Ricevuta chiamata a getValue dal Sender\n");
printf("Restituisco il valore %u",valore);
canale ! valore;
La procedura di init si occupa di inizializzare le procedure Sender e Receiver
passando il canale.
init {
run Sender(c);
run Receiver(c);
}
20
Nell’ottica di migliorare la leggibilità del codice Promela, è possibile anche eliminare
la tipologia di messaggio (SINC, ASINC), lasciando passare sul canale di
comunicazione solamente le chiamate ai metodi degli oggetti e i messaggi di risposta.
Quindi nel caso della chiamata setValue passando il parametro valore sarà possibile
eseguire la chiamata nel Sender semplicemente inviando sul canale il seguente
messaggio:
out! setValue(s);
Di seguito il risultato dell’esecuzione del codice prodotto per modellare la chiamata
sincrona con Spin:
> Ricevuta chiamata a getValue dal Sender
> Restituisco il valore 12
> Valore di ritorno della chiamata: 12
> timeout
> #processes: 3
> queue 1 (c):
> 14: proc 2 (Receiver) line 30 "synch_message_1_canale.pr" (state 6)
> 14: proc 1 (Sender) line 18 "synch_message_1_canale.pr" (state 4)
> 14: proc 0 (:init:) line 42 "synch_message_1_canale.pr" (state 3) <valid end
state>
Sezione 2.1.2 – Traduzione dei Combined Fragment
In questa sezione affronteremo la traduzione di ogni operatore in Spin e per ogni
operatore verrà mostrato un esempio di un sequence che effettui una chiamata, in
modo tale da determinare come far interagire l’uso dell’operatore e della chiamata.
Sezione 2.1.2.1 - Traduzione Alternative Fragment (alt)
L’operatore alt modella il construtto if…then…else, cioè rappresenta la possibilità
di scegliere solo una alternativa attraverso l’uso di guardie.
Spin prevede tra i suoi statements lo statement IF che modella esattamente
l’operatore ALT.
Un semplice esempio :
/* Combined Fragment ALT */
21
bit flag=0;
proctype SD() {
if
:: (flag) -> printf("Flag true,choice uno\n");
:: else ->printf("Flag false,choice else\n");
fi
}
init {
run SD(); }
Il frammento ALT non richiede l’uso di scambio di messaggi per la sua traduzione.
Combinare ALT con una chiamata sincrona/asincrona significa effettuare un if su una
guardia, e in base all’alternativa selezionata effettuare una diversa chiamata
attraverso lo scambio di messaggi.
Nell’esempio vedremo un caso di chiamata asincrona per settare un valore se l’if è
ritenuto valido e una chiamata asincrona per leggere un valore in caso contrario.
22
/* Combined Fragment ALT */
mtype {SINC, ASINC,setValue,getValue};
/*canale c1 per le chiamate */
chan c1 = [1] of {mtype,mtype,byte};
/*canale c2 per ottenere valore di ritorno*/
chan c2 = [1] of {byte};
/* guardia*/
bit flag=0;
proctype Sender(chan in,out) {
byte s;
s = 21;
byte s1;
/* se if valido effettuo chiamata sincrona */
/* altrimenti effettuo chiamata asincrona */
if
:: (flag) -> printf("Flag true,choice uno\n");
out ! ASINC,setValue,s;
:: else ->printf("Flag false,choice else\n");
out ! SINC,getValue;
do
:: in ? s1 ->
printf("Valore di ritorno della chiamata: %u",s1);
od
fi
}
proctype Receiver(chan in,out) {
byte s;
byte s1;
s1 = 12;
do
/* se chiamata sincrona eseguo blocco */
:: in ? ASINC,setValue,s ->
printf("Ricevuta chiamata a setValue dal Sender\n");
printf("Letti parametro %u\n",s);
/* se chiamata sincrona eseguo blocco */
:: in ? SINC,getValue ->
printf("Ricevuta chiamata a getValue dal Sender\n");
printf("Restituisco il valore %u",s1);
out ! s1;
od
23
}
init {
/* run equivale a create nei SequenceDiagram */
run Sender(c2,c1);
run Receiver(c1,c2);
}
Sezione 2.1.2.2 - Traduzione Option Fragment (opt)
L’operatore opt indica che il frammento è opzionale, cioè verrà eseguito tutto o non
verrà eseguito.
Per la traduzine l’idea è quella di utilizzare un IF che se verificato permette di
eseguire un codice e se non è verificato non prevedere alternative.
/* Combined Fragment OPT */
bit flag=1;
proctype SD() {
if
:: (flag) -> printf("Flag true,choice uno\n");
fi
}
init {
run SD(); }
Il frammento opt non richiede l’uso di scambio di messaggi per la sua traduzione.
Opt è molto simile al Par, con l’unica differenza che nel Par viene eseguito l’else nel
caso in cu la guardia non venga rispettata, mentre nel Opt nel caso in cui la guardia
non venga rispettata non viene eseguita la guardia else.
Combinare opt con una chiamata sincrona/asincrona significa effettuare un if su una
guardia, e se risulti valida effettuare una chiamata attraverso lo scambio di messaggi.
Nell’esempio vedremo un caso di chiamata sincrona per settare un valore se l’if è
ritenuto valido senza prevedere alcuna operazione nel caso contrario.
24
/* Combined Fragment OPT*/
mtype {SINC, ASINC,setValue,getValue};
/*canale c per le chiamate asincrone*/
chan c = [1] of {mtype,mtype ,byte};
/* guardia*/
bit flag=0;
proctype Sender(chan out) {
byte s;
s = 21;
/* se if valido effettuo chiamata sincrona */
/* altrimenti si esce dall'if senza */
/* eseguire niente*/
if
:: (flag) -> printf("Flag true,choice uno\n");
out ! ASINC,setValue,s;
fi
}
proctype Receiver(chan in) {
byte s;
do
/* se chiamata sincrona eseguo blocco */
:: in ? ASINC,setValue,s ->
25
printf("Ricevuta chiamata a
setValue dal
Sender\n");
printf("Letti parametro %u\n",s);
od
}
init {
/* run equivale a create nei SequenceDiagram */
run Sender(c);
run Receiver(c);
}
Sezione 2.1.2.3 - Traduzione Parallel Fragment (par)
L’operatore par modella processi concorrenti, il cui ordine di esecuzione è
indifferente ma il contenuto al loro interno è prestabilito.
L’idea è quella di utilizzare un processo per ogni frammento che può essere eseguito
in parallelo, ogni processo inizierà con una guardia che leggerà il valore da un canale
opportunamente riempito dal processo chiamante, cioè il sequence generale. Il
processo che identifica il sequence generale invierà i messaggi sul canale in modo
non deterministico senza attendere una risposta sincrona. Solamente dopo averli
spediti controllerà, se aspetta valori di ritorno in un canale.
Il non determinismo può essere facilmente introdotto attraverso un if in cui le guardie
siano sempre valide.
if
:: 1 -> printf("choice uno\n");
:: 1 -> printf("choice due\n");
fi
In questo modo si effettuerà una sola scelta tra quelle disponibili e sarà SPIN a
scegliere quale eseguire.
Utilizzando un ciclo do al posto dell’if si possono eseguire non deterministicamente
più guardie.
do
:: flagA -> printf("Flag true,choice uno\n"); flagA=0;
:: flagB -> printf("Flag false,choice else\n");flagB=0 ;
:: else -> goto PAR
od
26
PAR: printf("codice da eseguire successivamente\n");
Una volta eseguita la guardia viene resa false per non farla ripetere, quando tutte le
guardie sono false attraverso l’else si potrà eseguire il resto del codice.
Ecco un caso completo di traduzione.
/* Combined Fragment PAR */
mtype {MSG1, MSG2};
chan chan1 = [1] of {mtype};
proctype SD(chan chan2) {
bit flagA=1;
bit flagB=1;
do
:: flagA -> printf("choice uno\n");
chan2 ! MSG1 ->
run PAR1(chan1);
flagA=0;
:: flagB -> printf("choice due\n");
chan2 ! MSG2 ->
run PAR2(chan1);
flagB=0;
:: else ->goto PAR;
od;
PAR: printf("codice da eseguire successivamente\n");
}
proctype PAR1(chan chan2) {
do
:: chan2 ? MSG1-> printf("Sono in PAR1\n");
od
}
proctype PAR2(chan chan2) {
do
:: chan2 ? MSG2-> printf("Sono in PAR2\n");
od
}
init {
run SD(chan1); }
27
Il frammento PAR richiede l’uso di scambio di messaggi per la sua traduzione. I
processi da eseguire in parallelo corrisponderanno a diversi oggetti creati
direttamente . Bisogna combinare i processi da eseguire in parallelo con gli oggetti
invocati tramite chiamata sincrona/asincrona
Nell’esempio si mostra un caso di chiamata sincrona verso due oggetti differenti da
eseguire in parallelo e solamente dopo l’invio di entrambi i messaggi si attendono i
valori di ritorno di entrambe le chiamate.
Viene aggiunto per sicurezza un tipo di messaggio che specifica il destinatario del
messaggio.
/* Combined Fragment PAR */
mtype {SINC, ASINC,RCV1,RCV2,setValue,getValue};
chan c1 = [3] of {mtype,mtype,mtype};
chan c2 = [3] of {mtype,byte};
proctype Sender(chan in,out) {
byte s1;
28
byte s2;
bit flagA=1;
bit flagB=1;
do
:: flagA -> printf("choice uno\n");
out ! SINC,RCV1,getValue;
flagA=0;
:: flagB -> printf("choice due\n");
out ! SINC,RCV2,getValue;
flagB=0;
:: else ->goto PAR;
od;
PAR: printf("codice da eseguire successivamente\n");
do
:: in ? RCV1,s1 ->
printf("RCV1 Valore di ritorno della chiamata: %u",s1);
:: in ? RCV2,s2 ->
printf("RCV2 Valore di ritorno della chiamata: %u",s2);
od
}
proctype Receiver1(chan in,out) {
byte s1;
s1 = 44;
do
:: in ? SINC,RCV1,getValue ->
printf("Ricevuta chiamata a getValue dal Sender\n");
printf("Restituisco il valore %u",s1);
out !RCV1, s1;
od
}
proctype Receiver2(chan in,out) {
byte s2;
s2 = 33;
do
:: in ? SINC,RCV2,getValue ->
printf("Ricevuta chiamata a getValue dal Sender\n");
printf("Restituisco il valore %u",s2);
out ! RCV2,s2;
od
29
}
init {
run Sender(c2,c1);
run Receiver1(c1,c2);
run Receiver2(c1,c2);
}
Sezione 2.1.2.4 - Traduzione Critical Fragment (critical)
L’operatore critical viene usato per definire tracce all’interno della regione che non
possono essere interlevead. Esiste un'unica seqenza di azione valide. La regione verrà
trattata atomicamente.
L’idea è di utilizzare lo statement “atomic” di spin.
/* Combined Fragment CRITICAL */
proctype SD() {
atomic{printf("Messaggio1\n");printf("Messaggio2\n");}
}
init {
run SD(); }
Il frammento critical non richiede l’uso di scambio di messaggi per la sua traduzione.
Nell’esempio viene trattata come CRITICAL la ricezione del valore di ritorno di una
chiamata asincrona.
30
/* Combined Fragment CRITICAL */
mtype {SINC, ASINC,setValue,getValue};
chan c1 = [1] of {mtype,mtype};
chan c2 = [1] of {byte};
proctype Sender(chan in, out) {
byte s;
out ! SINC,getValue;
atomic{
do
:: in ? s ->
printf("Ricevuto valore di ritorno\n");
printf("Valore : %u",s);
od
}
}
proctype Receiver1(chan in, out) {
byte s1;
31
s1 = 12;
do
:: in ? SINC,getValue ->
printf("Ricevuta chiamata a getValue dal
Sender\n");
printf("Restituisco il valore %u",s1);
out ! s1;
od
}
init {
run Sender(c2,c1);
run Receiver1(c1,c2);
}
Sezione 2.1.2.5 - Traduzione Weak Sequencing Fragment (seq)
L’operatore seq rappresenta un variazione al “par” dove è permesso il parallelismo
solo tra regioni critiche, mentre i messaggi all’interno della regione non possono
essere scambiati.
Per la traduzione l’idea è di utilizzare un processo per ogni regione e rendere atomic
il loro contenuto.
/* Combined Fragment SEQ */
mtype {MSG1, MSG2};
chan chan1 = [1] of {mtype};
proctype SD(chan chan2) {
bit flagA=1;
bit flagB=1;
do
:: flagA -> printf("choice uno\n");
chan1 ! MSG1 ->
run SEQ1(chan1);
flagA=0;
:: flagB -> printf("choice due\n");
chan1 ! MSG2 ->
run SEQ2(chan1);
flagB=0;
32
:: else ->goto SEQ;
od;
SEQ: printf("codice da eseguire successivamente\n");
}
proctype SEQ1(chan chan2) {
atomic{
do
:: chan2 ? MSG1-> {printf("Sono in SEQ1\n");}
od
}
}
proctype SEQ2(chan chan2) {
atomic{
do
:: chan2 ? MSG2-> {printf("Sono in SEQ2\n"); }
od
}
}
init { run SD(chan1);
}
Il caso concreto è analogo al par tenendo conto della differenza negli operatori.
33
/* Combined Fragment SEQ */
mtype {SINC, ASINC,RCV1,RCV2,setValue,getValue};
chan c1 = [3] of {mtype,mtype,mtype};
chan c2 = [3] of {mtype,byte};
proctype Sender(chan in,out) {
byte s1;
byte s2;
bit flagA=1;
bit flagB=1;
do
:: flagA -> printf("choice uno\n");
out ! SINC,RCV1,getValue;
flagA=0;
:: flagB -> printf("choice due\n");
out ! SINC,RCV2,getValue;
flagB=0;
:: else ->goto SEQ;
od;
SEQ: printf("codice da eseguire successivamente\n");
do
:: in ? RCV1,s1 ->
printf("RCV1 Valore di ritorno della chiamata: %u",s1);
:: in ? RCV2,s2 ->
printf("RCV2 Valore di ritorno della chiamata: %u",s2);
od
}
proctype Receiver1(chan in,out) {
atomic{
byte s1;
s1 = 44;
do
:: in ? SINC,RCV1,getValue ->
printf("Ricevuta chiamata a getValue dal Sender\n");
printf("Restituisco il valore %u",s1);
out !RCV1, s1;
od }
}
proctype Receiver2(chan in,out) {
34
atomic{
byte s2;
s2 = 33;
do
:: in ? SINC,RCV2,getValue ->
printf("Ricevuta chiamata a getValue dal Sender\n");
printf("Restituisco il valore %u",s2);
out ! RCV2,s2;
od
}
}
init {
run Sender(c2,c1);
run Receiver1(c1,c2);
run Receiver2(c1,c2);
}
Sezione 2.1.2.6 - Traduzione Negative Fragment (neg)
L’operatore neg rappresenta una serie di tracce da considerare invalide.
Nella traduzione l’idea è di utilizzare assert(false) nel caso in cui la traccia sia
considerata neg.
/* Combined Fragment NEG */
mtype {OK, NEG1, NEG2};
chan chan1 = [1] of {mtype};
proctype SD(chan channel1) {
if
:: channel1 ? NEG1 -> printf("messaggio invalido NEG1\n");
assert(false);
:: channel1 ? NEG2 -> printf("messaggio invalido NEG2\n");
assert(false);
:: channel1 ? OK -> printf("messaggio OK\n");
fi
}
init {
chan1 ! NEG1;run SD(chan1);
}
35
Neq, ad esempio, può essere utilizzato per controllare se la funzione invocata svolga
il proprio lavoro; in questo caso ritornerà True . Nel caso contrario ritornerà False e
si considererà la traccia invalida.
/* Combined Fragment NEG*/
mtype {SINC, ASINC,getValue,setValue};
chan c1 = [1] of {mtype,mtype};
chan c2 = [1] of {bool};
proctype Sender(chan in, out) {
bool b;
out ! SINC,getValue;
do
:: in ? b ->
if
::b==false->printf("Chiamata non valida ");
assert(false);
::else -> printf("Chiamata valida ");
fi
}
od
36
proctype Receiver(chan in, out) {
bool b1;
b1 = false;
do
:: in ? SINC,getValue ->
printf("Ricevuta chiamata a getValue dal
Sender\n");
printf("Restituisco il valore %u",b1);
out ! b1;
od
}
init {
run Sender(c2,c1);
run Receiver(c1,c2);
}
Sezione 2.1.2.7 - Traduzione Ignore Fragment (ignore)
L’operatore ignore rappresenta una lista di messaggi di non interesse, insignificanti
per il frammento e ignorati nell’esecuzione oppure che possono apparire in qualsiasi
punto dell’applicazione.
Per la traduzione l’idea è di non far eseguire niente al processo che legge un
messaggio ignore, il messaggio sarà composto da due campi: un filtro che indichi il
messaggio come IGNORE e il messaggio stesso .
/* Combined Fragment IGNORE */
mtype { IGN, MSG1,MSG2};
chan chan1 = [1] of {mtype,mtype};
proctype SD(chan channel1) {
if
:: channel1 ? IGN,MSG1
:: channel1 ? IGN,MSG2
:: channel1 ? MSG1 -> printf("messaggio OK\n");
fi
}
37
init {
chan1 ! MSG1;run SD(chan1);
}
Non ci sono particolari accorgimenti da prendere nel caso in cui si voglia utilizzare
contemporaneamente IGNORE e una chiamata.
/* Combined Fragment IGNORE*/
mtype { IGN,MSG1,MSG2};
chan c = [2] of {mtype,mtype};
proctype Sender(chan out) {
out! IGN,MSG1;
out! IGN,MSG2;
}
proctype Receiver(chan in) {
do
::in ?
/* non
::in ?
/* non
IGN,MSG1 ;
viene eseguito niente*/
IGN,MSG2 ;
viene eseguito niente*/
od
}
init {
run Sender(c);
38
run Receiver(c);
}
Sezione 2.1.2.8 - Traduzione Consider Fragment (consider)
L’operatore “consider” rappresenta invece la lista dei messaggi interessanti per il
frammento.
Per la traduzione l’idea è di inviare un messaggio composto da due campi: un filtro
che indichi il messaggio come CONSIDER e il messaggio stesso .
/* Combined Fragment CONSIDER */
mtype {CONS, MSG1, MSG2};
chan chan1 = [1] of {mtype,mtype};
proctype SD(chan channel1) {
if
:: channel1 ? CONS,MSG1-> printf("messaggio OK\n");
fi
}
init {
chan1 ! CONS,MSG1; run SD(chan1); }
Non ci sono particolari accorgimenti da prendere nel caso in cui si voglia utilizzare
contemporaneamente CONSIDER e una chiamata.
39
/* Combined Fragment CONSIDER*/
mtype { CONS,MSG1,MSG2};
chan c = [2] of {mtype,mtype};
proctype Sender(chan out) {
out! CONS,MSG1;
out! CONS,MSG2;
}
proctype Receiver(chan in) {
do
::in ? CONS,MSG1 -> printf("Messaggio1\n");
::in ? CONS,MSG2 -> printf("Messaggio2\n");
od
}
init {
run Sender(c);
run Receiver(c);
}
Sezione 2.1.2.9 - Traduzione Assertion Fragment (assert)
L’operatore assert serve per asserire qualcosa che deve necessariamente avvenire,
l’unico stato valido è quello previsto dal contenuto dell’assert.
Spin prevede tra i suoi statements lo statement assert che modella esattamente
l’operatore Assert.
/* Combined Fragment ASSERT */
bit flag=0;
proctype SD() {
assert(flag);
}
init {run SD();
}
40
Nell’esempio seguente viene utilizzato l’ASSERT per controllare il valore di ritorno
di una chiamata sincrona, se restituisce true significa che il ricevente ha portato ha
termine il suo compito senza incorrere in errori.
/* Combined Fragment ASSERT*/
mtype {SINC, ASINC,setValue,getValue};
chan c1 = [1] of {mtype,mtype};
chan c2 = [1] of {bit};
proctype Sender(chan in, out) {
bit flag=0;
out ! SINC,getValue;
do
od
:: in ? flag ->
assert(flag);
printf("Valore di ritorno della chiamata: %u",flag);
}
proctype Receiver(chan in, out) {
bit flag=0;
do
41
:: in ? SINC,getValue ->
printf("Ricevuta chiamata a getValue dal
Sender\n");
printf("Restituisco il valore %u",flag);
out ! flag;
od
}
init {
run Sender(c2,c1);
run Receiver(c1,c2);
}
Sezione 2.1.2.10 - Traduzione Loop Fragment (loop)
L’operatore loop viene utilizzato per ripetere più volte il frammento, una guardia o
una espressione booleana esprimono il numero di iterazioni.
L’idea è di utilizzare un ciclo do, controllare la validità di una condizione e nel caso
contrario effettuare una break per terminare il ciclo.
/* Combined Fragment LOOP */
proctype SD() {
byte cont=3;
do
:: cont>0 -> printf("Valore cont %u\n",cont);
cont=cont-1;
od
}
:: else -> printf("Ciclo terminato\n");
break;
init {
run SD(); }
Il frammento LOOP non richiede l’uso di scambio di messaggi per la sua traduzione.
Combinare LOOP con una chiamata sincrona/asincrona significa effettuare un ciclo
Do
:: condizione -> invio messaggio
:: else -> break
OD
Dove solo se la condizione è verificata viene effettuato l’invio del messaggio.
42
/* Combined Fragment LOOP */
mtype { SINC, ASINC,setValue,getValue};
chan c = [1] of {mtype,mtype ,byte};
proctype Sender(chan out) {
byte cont;
cont =
3;
do
:: cont>0 -> out! ASINC,setValue,cont->
cont=cont-1;
:: else -> printf("Ciclo terminato\n");
break;
od
}
proctype Receiver(chan in) {
byte s;
do
:: in ? ASINC,setValue,s ->
printf("Ricevuta chiamata a setValue dal
Sender\n");
printf("Letti parametro %u\n",s);
od
}
43
init {
run Sender(c);
run Receiver(c);
}
Sezione 2.1.2.11 - Traduzione Break Fragment (break)
L’operatore “break” modella una sequenza alternativa di evento da processare al
posto del resto del diagramma.
Per la traduzione l’idea è quella di creare il label BREAK e di chiamarlo nel caso si
necessiti la sequenza alternativa attraverso il GOTO.
/* Combined Fragment BREAK */
bit flag=1;
proctype SD() {
do
:: (flag) -> printf("Flag true\n"); flag=0;
:: (!flag) ->goto BREAK;
od;
BREAK:
}
init {
printf("ciao\n");
run SD(); }
BREAK e chiamata coesistono facilmente senza interferirsi. Ad esempio si può
modificare il caso LOOP aggiungendo il Label BREAK e modificando l’else in
modo tale da richiamarlo attraverso il goto.
In questo caso il Break farà apparire a video solamente una scritta di ciclo terminato.
44
/* Combined Fragment BREAK */
mtype { SINC, ASINC,setValue,getValue};
chan c = [1] of {mtype,mtype ,byte};
proctype Sender(chan out) {
byte cont;
cont =
3;
do
:: cont>0 -> out! ASINC,setValue,cont;
cont=cont-1;
:: else -> goto BREAK;
od;
BREAK: printf("Ciclo terminato\n");
}
proctype Receiver(chan in) {
byte s;
do
:: in ? ASINC,setValue,s ->
printf("Ricevuta chiamata a setValue dal
Sender\n");
printf("Letti parametro %u\n",s);
45
od
}
init {
run Sender(c);
run Receiver(c);
}
Sezione 2.1.2.12 - Traduzione Strict Sequencing Fragment (strict)
L’operatore “strict” racchiude una serie di messaggi da processare nell’ordine
stabilito.
La lettura dei messaggi nell’ordine prestabilito viene fatto attraverso lo statement
atomic che ad ogni passo cerca di leggere il valore del messaggio che si aspetta di
ricevere.
/* Combined Fragment STRICT */
mtype {MSG1, MSG2};
chan chan1 = [2] of {mtype};
proctype SD(chan channel1) {
atomic{
channel1 ? MSG1 -> printf("Messaggio1\n");
channel1 ? MSG2 -> printf("Messaggio2\n");
}
}
init {
}
chan1 ! MSG1;
chan1 ! MSG2;
run SD(chan1);
Per combinare STRICT e chiamata asincrona/sincrona basta effettuare il controllo nel
processo ricevente racchiudendo la ricezione dei messaggi nel costrutto ATOMIC.
46
/* Combined Fragment STRICT*/
mtype { SINC, ASINC,MSG1,MSG2};
chan c = [2] of {mtype,mtype};
proctype Sender(chan out) {
out! ASINC,MSG1;
out! ASINC,MSG2;
}
proctype Receiver(chan in) {
do
::atomic{
}
od
in ? ASINC,MSG1 -> printf("Messaggio1\n");
in ? ASINC,MSG2 -> printf("Messaggio2\n");
}
init {
run Sender(c);
run Receiver(c);
}
47
Sezione 2.1.3 – Esempi
CASO MACCHINETTA SELF SERVICE
Riprendendo l’esempio di Sequence Diagram presentato nelle sezioni
precedenti, mostriamo come sia possibile tradurre l’interazione tra gli oggetti in
PROMELA e verificare alcune proprietà sul diagramma.
Modelliamo innanzitutto il tipo di messaggi che il canale può trasportare. I
messaggi sono:
- inserisceMonete
- effettuaSelezione
- inviaMonete
- selezione
- restituisciProdotto
- returnProdotto
Quindi, come già visto, definiamo il tipo di messaggi nel seguente modo:
mtype {inserisceMonete, selezioneProdotto, inviaMonete,
selezione, restituisciProdotto, return};
Il messaggio return viene utilizzato per i metodi che restituiscono un valore.
E’ possibile definire le strutture che conterranno i parametri da passare nei
messaggi, tramite l’istruzione typedef:
typedef Importo {
short valore;
}
typedef Prodotto {
short ID;
short prezzo;
}
Abbiamo quindi una struttura Importo (possiamo vederla come fosse un DTO,
un oggetto di trasporto dati con attributi pubblici senza metodi setter o getter)
formata da un attributo di tipo short, indicante il valore dell’importo inserito e una
struttura Prodotto, formata da id del prodotto e dal prezzo del prodotto.
Viene utilizzato un singolo canale per l’invio di messaggi tra gli oggetto:
chan canale = [1] of {mtype, Importo, Prodotto, short};
48
Ogni oggetto del diagramma viene modellato come una procedura in
PROMELA. Ogni stato degli oggetti è stato definito da un’etichetta significativa.
Consideriamo l’oggetto Utente, esso effettua le seguenti operazioni:
• definisce il prodotto da acquistare.
• inserisce le monete nella parte frontale della macchina.
• seleziona il prodotto
Possiamo quindi modellarlo secondo la seguente procedura:
proctype Utente(chan in, out) {
Importo imp;
/*Importo delle monete inserite*/
imp.valore = 130;
/*ID del prodotto scelto*/
short ID = 20;
inserimentoMonete:
out ! inserisceMonete(imp) ->
goto sceltaProdotto;
ID);
}
sceltaProdotto:
out ! selezioneProdotto(ID);
printf("Utente: Prodotto Selezionato %u\n",
L’oggetto parte frontale riceve le monete, riceve la selezione del prodotto,
invia la selezione del prodotto al contenitore e riceve il prodotto restituito da
questo:
proctype ParteFrontale(chan in, out) {
Importo imp;
Prodotto product;
short ID;
inserimentoImporto:
in ? inserisceMonete(imp) ->
printf("Parte Frontale: Ricevuto Importo
dall'utente %u\n",imp.valore);
goto sceltaProdotto;
sceltaProdotto:
in ? selezioneProdotto(ID) ->
49
printf("Parte Frontale: Ricevuta selezione
prodotto dall'utente %u\n", ID);
goto invioMonete;
invioMonete:
out ! inviaMonete(imp) ->
goto selezionaIlProdotto;
selezionaIlProdotto:
out ! selezione(ID) ->
goto restituzioneProdotto;
restituzioneProdotto:
in ? return(product) ->
printf("Parte Frontale: ricevuto prodotto dal
contenitore %u\n",product.ID);
goto end;
end:
printf("Parte Frontale: Sequenza terminata, prodotto
ricevuto\n");
}
La cassetta delle monete riceve l’importo dalla parte frontale. La parte frontale
invia la richiesta di selezione del prodotto al contenitore dei prodotti, il quale
restituisce il prodotto selezionato.
proctype CassettaMonete(chan in, out){
Importo imp;
invioMonete:
in ? inviaMonete(imp) ->
printf("Cassetta Monete: ricevuto importo
%u\n",imp.valore);
}
proctype ContenitoreProdotti(chan in, out) {
Prodotto product;
short ID;
selezionaIlProdotto:
in ? selezione(ID) ->
printf("Contenitore Prodotti: Ricevuta
richiesta di prodotto: %u\n" , ID);
product.ID = ID;
product.prezzo = 130;
50
out ! return(product);
}
Infine è possibile creare il metodo init che inizializza le procedure e fa partire i
processi:
init {
run
run
run
run
}
Utente(canale, canale);
ParteFrontale(canale, canale);
CassettaMonete(canale, canale);
ContenitoreProdotti(canale, canale);
Di seguito il risultato dell’esecuzione del codice in SPIN:
> Parte Frontale: Ricevuto Importo dall'utente 130
> Utente: Prodotto Selezionato 20
> Parte Frontale: Ricevuta selezione prodotto dall'utente 20
> Cassetta Monete: ricevuto importo 130
> Contenitore Prodotti: Ricevuta richiesta di prodotto: 20
> Parte Frontale: ricevuto prodotto dal contenitore 20
> Parte Frontale: Sequenza terminata, prodotto ricevuto
In questo esempio, per non rendere troppa complicata la sua lettura, non è stato
implementato il codice per controllare se l’importo inserito sia sufficiente.
Nel prossimo esempio verrà esaminato un aspetto analogo, se l’ordine può essere
soddisfatto in termini di quantità richiesta.
CASO ORDINALIBRI
Introduciamo un secondo esempio che riguarda l’ordinazione di un libro
Il Sequence Diagram che modelleremo farà uso della seguente sequenza di azioni:
L’user cerca nel catalogo se è presente il libro desiderato.
Il catalogo controlla se è presente il libro
Se il libro è presente l’user inizializza il processo ordine
Se il libro è presente l’user ordina il libro indicando anche il numero di copie
che necessita.
• Se esistono abbastanza copie del libro, il processo ordine aggiorna il numero di
copie disponibili e avverte l’user dell’accettazione dell’ordine
•
•
•
•
51
Non esiste la versione Collaboration del caso in quanto si fa uso del
OptionalFragmnent non presente nei suddetti diagrammi.
Di seguito viene riportato il codice della traduzione del diagramma.
/* esempio OrdinaLibri */
mtype { ordina,cerca,ris};
chan user_cat = [1] of {mtype,byte,bool };
chan user_ord = [1] of {mtype,byte,byte,bool};
bool risCatalogo;
bool risOrdine;
proctype user(chan cat,ord) {
byte codice=34;
byte quantita=44;
bool mom;
cat ! cerca,codice;
do
:: cat ? ris,risCatalogo ->
52
if
:: risCatalogo==true ->printf("libro trovato, inoltrato ordine");
run ordine(user_ord);
ord ! ordina,codice,quantita;
do
:: ord ? ris,risOrdine ->
if
::risOrdine==true->printf("ordinato effettuato");
::else -> printf("quantita non disponibile");
fi
od
:: else ->
printf("libro non trovato");
break;
fi
od
}
proctype ordine(chan ut) {
byte libri[3];
libri[0]=12;
libri[1]=27;
libri[2]=34;
byte numero[3];
numero[0]=43;
numero[1]=5;
numero[2]=56;
byte libro;
byte quantita;
bool ordineEffettuato;
bool mom;
do
:: ut? ordina,libro,quantita ->
if
:: (libri[0]==libro) & (numero[0]>=quantita)->
ordineEffettuato=true;
numero[0]=numero[0]-quantita;
:: (libri[1]==libro) & (numero[1]>=quantita)->
ordineEffettuato=true;
numero[1]=numero[1]-quantita;
:: (libri[2]==libro) & (numero[2]>=quantita)->
ordineEffettuato=true;
numero[2]=numero[2]-quantita;
:: else ->ordineEffettuato=false;
fi;
53
ut ! ris,ordineEffettuato;
od
}
proctype catalogo(chan ut) {
bool verificaPresenza;
byte libri[3];
libri[0]=12;
libri[1]=27;
libri[2]=34;
byte libro;
bool mom;
do
:: ut? cerca,libro ->
if
:: libri[0]==libro ->verificaPresenza=true;
:: libri[1]==libro ->verificaPresenza=true;
:: libri[2]==libro ->verificaPresenza=true;
:: else ->verificaPresenza=false;
fi;
ut ! ris,verificaPresenza;
od
}
init {
run user(user_cat,user_ord);
run catalogo(user_cat);
}
Sezione 2.2 – Traduzione dei Collaboration Diagram
La traduzione dei Collaboration Diagram è essenzialmente uguale alla traduzione dei
Sequence Diagram. Esiste un unica differenza, nei Collaboration Diagram i messaggi
vengono etichettati con un numero sequenziale; in questo modo si indica il momento
esatto in cui il messaggio deve accadere.
Per tradurli in SPIN , basandoci su quanto visto nei Sequence Diagram, basta definire
una variabile globale che indica il numero sequenziale e prevedere un controllo prima
54
di ogni invio di messaggio in base al valore di sequenza previsto. Una volta inviato il
messaggio, il numero sequenziale viene aggiornato.
Introduciamo il numero sequenziale esternamente ai processi, lo inizializziamo ad 1
per adattarci alla numerazione dei messaggi nei Collaboration.
byte sequenza=1;
Per la chiamata, verrà inizialmente controllato se il numero di sequenza è quello
previsto , e dopo l’invio del meassaggio verrà aggiornato il valore di sequenza.
if
::sequenza==0 -> out ! SINC,getValue -> sequenza=sequenza+1;
fi;
Nel caso di una chiamata sincrona il codice è il seguente:
/*Collaboration*/
mtype {SINC, ASINC,setValue,getValue};
chan c1 = [1] of {mtype,mtype};
chan c2 = [1] of {byte};
/* sequenza indica il numero di messaggio da eseguire*/
byte sequenza=1;
proctype Sender(chan in, out) {
byte s;
if
::sequenza==1 ->
out ! SINC,getValue ->
sequenza=sequenza+1;
55
fi;
do
:: in ? s ->
printf("Valore di ritorno della chiamata: %u",s);
od
}
proctype Receiver(chan in, out) {
byte s1;
s1 = 12;
do
:: in ? SINC,getValue ->
printf("Ricevuta chiamata a getValue dal Sender\n");
printf("Restituisco il valore %u",s1);
if
::sequenza==2 ->
out ! s1;
sequenza=sequenza+1;
fi
od
}
init {
run Sender(c2,c1);
run Receiver(c1,c2);
}
56
Parte III: Verifica di proprietà
Alcune proprietà interessanti da verificare per i sequence diagram sono legate
all’utilizzo di chiamate e all’uso di canali per la loro traduzione in SPIN.
Alcune proprietà interessanti da verificare per i sequence diagram sono legate
all’utilizzo di chiamate e all’uso di canali per la loro traduzione in SPIN.
Prima di verificare in dettaglio le varie proprietà , vengono introdotte le tecniche di
verifica utilizzate
Sezione 3.1 – Tecniche di verifica
Sono state utilizzate due tecniche:
• Utilizzo del processo Monitor
• Utilizzo deil Never_Claim
Monitor è il nome standard utilizzato in SPIN per indicare un processo nel quale
vengono verificate proprietà.
Nelle verifica di proprietà con Monitor se si usa il modello
active proctype monitor() {
atomic { !invariant ->
assert(false) }
}
L’invariant è un tipo particolare di proprietà valida durante l’intera esecuzione,
altrimenti si attiva assert(false) e l’esecuzione termina.
Se c'è bisogno di verificare una proprietà che sia valida solamente in un futuro il
monitor va modificato in
active proctype monitor() {
atomic { proprietà ->
assert(true) }
}
se viene eseguito assert(true) si è sicuri che la proprietà è verificata.
57
Racchiudere la proprietà da verificare all’interno dello statement atomic viene
consigliato dai curatori di SPIN in quanto consente di evitare, come nel caso in cui
non sia presente, di forzare il sistema ad eseguire l’asserzione prima che la variabile
TIMEOUT sia true.
L’utilizzo di un never_claim ci fornisce la possibilità di fare uso della sintassi LTL
per la verifica delle proprietà.
A differenza dell’altro metodo non si può controllare lo stato della canale
direttamente; per ovviare il problema si cattura lo stato del canale attraverso
l’aggiunta di variabili.
Se si vuole indicare un invariant si usa l’operatore temporale Globally (simbolo [])
attraverso l’istruzione []invariant.
Le proprietà possono essere scritte direttamente in linguaggio LTL, sarà direttamente
SPIN attraverso un traduttore integrato a tradurre la formula in linguaggio
PROMELA.
Se le proprietà introdotte devono rispettare un ordine in cui diventano valide, cioè
solo se quando la prima proprietà è valida allora controllo se dal prossimo stato vale
la seconda proprietà, ecc.. ; la formula deve avere la sequente forma:
<>(proprietà1 -> X <>(proprietà2 -> X<>(proprietà3)))
Sezione 3.2 – Proprietà da verificare
Le proprietà che verificheremo sono:
1) Nel caso di una chiamata, controllare se una volta inviato il messaggio da parte
del Sender questo viene ricevuto dal Receiver.
2) Nel caso di una chiamata sincrona controllare se il Sender riceve una risposta
dal Receiver.
3) Nel caso di una chiamata sincrona controllare se il Sender riceve una risposta
dal Receiver e controllare se il valore di ritorno ottenuto, (nel caso in cui sia
una variabile booleana che indichi se il Receiver ha effettuato il suo lavoro con
o senza errori) sia true .
4) Nel caso del LoopFragment controllare se ci sono ancora messaggi in coda
dopo che il loop è terminato.
Valgono le stesse proprietà sia per i Sequence sia per i Collaboration, per quest’ultimi
effettuare verifiche sulla variabile di sequenza risuterebbe ridondante.
Di seguito si analizzeranno in dettaglio le varie proprietà.
58
1) Nel caso di una chiamata asincrona, per controllare se una volta inviato il
messaggio da parte del sender questo viene ricevuto dal Reicever, utilizzando un
processo monitor, si può controllare che lo stato della coda del canale sia non vuota (
nempty(nome_canale) ) e successivamente vuota ( empty(nome_canale) ). L’idea è
quella di controllare che qualcuno abbia inserito un messaggio nella coda e che
qualcuno lo abbia estratto.
Si può modificare il codice della chiamata asincrona, visto precedentemente,
aggiungendo il processo monitor.
Active proctype monitor() {
atomic {
nempty(c) ->
empty(c) ->
assert(true)
}
}
Con il Never_Claim una volta inviato sul canale un messaggio si imposta una
variabile boolena che indichi l’invio.
out! ASINC,setValue,s -> invio=true ;
Quando si riceve un messaggio si imposta una variabile boolena che indichi la
ricezione.
in ? ASINC,setValue,s ->ricevo=true;
La clausola Never_claim da utilizzare è:
<>(invio -> X<>ricevo)
Spin traduce tale clausola in:
never { /* !(<>(invio -> X<>ricevo)) */
accept_init:
T0_init:
if
:: ((invio)) -> goto accept_S4
fi;
accept_S4:
59
T0_S4:
if
:: (! ((ricevo)) && (invio)) -> goto accept_S4
fi;
}
Ecco il codice completo con le modifiche effettuate:
/* Verifica Proprietà su chiamata */
/* asincrona con l'utilizzo di un */
/* never_claim */
mtype { SINC, ASINC,setValue,getValue};
chan c = [1] of {mtype,mtype ,byte};
bool invio;
bool ricevo;
proctype Sender(chan out) {
byte s;
s = 21;
out! ASINC,setValue,s -> invio=true ;
}
proctype Receiver(chan in) {
byte s;
do
:: in ? ASINC,setValue,s ->
ricevo=true;
printf("Ricevuta chiamata a setValue dal Sender\n");
printf("Letti parametro %u\n",s);
od
}
init {
run Sender(c);
run Receiver(c);
}
60
2) Nel caso di una chiamata sincrona per controllare se il Sender riceve una risposta
dal Receiver bisogna tener conto dell’utilizzo di due canali, uno per la chiamata e uno
per la ricezione.
Utilizzando un processo monitor, come prima cosa bisogna controllare che il Sender
abbia inserito la richiesta, poi che il Receiver abbia inserito la risposta e infine
controllare che il Sender abbia letto la risposta.
Si può modificare il codice della chiamata sincrona visto precedentemente
aggiungendo il processo monitor.
Active proctype Monitor() {
atomic {
nempty(c1) ->
nempty(c2) ->
empty(c2)->
assert(true)
}
}
Con il Never_Claim una volta inviato sul canale un messaggio si imposta una
variabile boolena che indichi l’invio.
out! SINC,setValue,s -> invio=true ;
Quando si riceve un messaggio si imposta una variabile boolena che indichi la
ricezione, per il receiver e per il sender.
in ? SINC,getValue -> leggoReceiver=true;
in ? s -> leggoSender=true;
La clausola Never_claim da utilizzare è:
<>(invio->X<>(leggoReceiver->X<>leggoSender))
Spin traduce tale clausola in:
never { /* !(<>(invio->X<>(leggoReceiver->X<>leggoSender))) */
accept_init:
T0_init:
if
:: ((invio)) -> goto accept_S1
fi;
accept_S1:
61
T0_S1:
if
:: ((invio) && (leggoReceiver)) -> goto accept_S15
fi;
accept_S15:
T0_S15:
if
:: (! ((leggoSender)) && (invio) && (leggoReceiver)) -> goto accept_S15
fi;
}
Ecco il codice completo con le modifiche effettuate:
/* Verifica Proprietà su chiamata */
/* sincrona con l'utilizzo di */
/* never_claim */
mtype {SINC, ASINC,setValue,getValue};
chan c1 = [1] of {mtype,mtype};
chan c2 = [1] of {byte};
bool invio;
bool leggoReceiver;
bool leggoSender;
proctype Sender(chan in, out) {
byte s;
out ! SINC,getValue->
invio=true;
do
:: in ? s ->
leggoSender=true;
printf("Valore di ritorno della chiamata: %u",s);
od
}
proctype Receiver(chan in, out) {
byte s1;
s1 = 12;
do
:: in ? SINC,getValue ->
leggoReceiver=true;
62
printf("Ricevuta chiamata a getValue dal Sender\n");
printf("Restituisco il valore %u",s1);
out ! s1;
od
}
init {
run Sender(c2,c1);
run Receiver(c1,c2);
}
3) Nel caso di una chiamata sincrona controllare se il Sender riceve una risposta dal
Receiver e controllare se il valore di ritorno ottenuto, (nel caso in cui sia una variabile
booleana che indichi se il Receiver ha effettuato il suo lavoro con o senza errori) sia
true .
Con un processo monitor si controlla se il sender abbia inviato la richiesta, dopo si
controlla se il Receiver abbia inviato la risposta e infine si controlla se la risposta è
stata letta dal Sender e se il valore letto è true.
active proctype Monitor() {
atomic {
nempty(c1) ->
nempty(c2) ->
empty(c2) && flag==true->
assert(true)
}
}
Il Never_Claim , in questo caso, è simile al caso precedente con l’unica differenza
che si controlla anche il valore di ritorno del Reicever
La clausola Never_claim da utilizzare è:
<>(invio->X<>(leggoReceiver->X<>(leggoSender && flag)))
Spin traduce tale clausola in:
never { /* !(<>(invio->X<>(leggoReceiver->X<>(leggoSender && flag)))) */
accept_init:
T0_init:
if
63
:: ((invio)) -> goto accept_S17
fi;
accept_S14:
T0_S14:
if
:: (((! ((flag)) && (invio) && (leggoReceiver)) || (! ((leggoSender)) && (invio) &&
(leggoReceiver)))) -> goto accept_S14
fi;
accept_S17:
T0_S17:
if
:: ((invio) && (leggoReceiver)) -> goto accept_S14
fi;
}
Ecco il codice completo con le modifiche effettuate:
/* Verifica Proprietà su chiamata */
/* sincrona con controllo stato Receiver */
/* con l'utilizzo di never_claim */
mtype {SINC, ASINC,setValue,getValue};
chan c1 = [1] of {mtype,mtype};
chan c2 = [1] of {byte};
bool invio;
bool leggoReceiver;
bool leggoSender;
proctype Sender(chan in, out) {
bool flag;
out ! SINC,getValue->
invio=true;
do
:: in ? flag ->
leggoSender=true;
printf("Valore di ritorno della chiamata: %u",flag);
od
}
proctype Receiver(chan in, out) {
bool s1;
s1 = 1;
do
64
:: in ? SINC,getValue ->
leggoReceiver=true;
printf("Ricevuta chiamata a getValue dal Sender\n");
printf("Restituisco il valore %u",s1);
out ! s1;
od
}
init {
run Sender(c2,c1);
run Receiver(c1,c2);
}
4) Nel caso del LoopFragment controllare se ci sono ancora messaggi in coda dopo
che il loop è terminato.
In questo caso, il processo monitor ha bisogno di qualche di una variabile di supporto
condivisa tra i processi. Questa variabile si chiama statoCoda e viene incrementata di
un’unità se si scrive nella coda e diminuita di un’unità se si legge. Si è resa necessaria
l’introduzione di questa variabile per controllare che tutti i messaggi vengano letti.
Infatti il processo monitor come prima cosa verifica che qualcuno ha scritto nella
coda, poi basta verificare se contemporaneamente la coda è vuota, il loop è terminato
(cont==0) e non ci sono messaggi da leggere in coda (statoCoda==0)
active proctype monitor() {
atomic {
nempty(c) ->
(empty(c) && cont==0 && statoCoda==0) ->
assert(true) }
}
Di seguito viene riportato , visto le modifiche al suo interno, il codice per intero:
/* Verifica Proprietà messaggi letti*/
/* dopo che loop terminato */
/* con l'utilizzo di un processo Monitor */
mtype { SINC, ASINC,setValue,getValue};
chan c = [3] of {mtype,mtype ,byte};
bool fine;
byte cont=3;
65
byte statoCoda;
proctype Sender(chan out) {
do
:: cont>0 -> out! ASINC,setValue,cont ->
cont=cont-1;
statoCoda=statoCoda+1;
:: else -> printf("Ciclo terminato\n");
fine=1;
break;
od
}
proctype Receiver(chan in) {
byte s;
do
:: in ? ASINC,setValue,s ->
printf("Ricevuta chiamata a setValue dal Sender\n");
printf("Letti parametro %u\n",s);
statoCoda=statoCoda-1;
od
}
active proctype monitor() {
atomic {
nempty(c) ->
(empty(c) && cont==0 && statoCoda==0) ->
assert(true) }
}
init {
run Sender(c);
run Receiver(c);
}
Con il Never_Claim una volta inviato sul canale un messaggio si imposta una
variabile boolena che indichi l’invio.
out! ASINC,setValue,cont -> invio=true;
Il Receiver setta la variabile ricevi=true quando non ha più messaggi in coda e il
Sender setta la variabile fine=true quando non ha più messaggi da inviare.
66
La clausola Never_claim da utilizzare è:
<>(invio->X<>(
fine && ricevi))
Spin traduce tale clausola in:
never { /* !(<>(invio->X<>( fine && ricevi))) */
accept_init:
T0_init:
if
:: ((invio)) -> goto accept_S9
fi;
accept_S9:
T0_S9:
if
:: (((! ((fine)) && (invio)) || (! ((ricevi)) && (invio)))) -> goto accept_S9
fi;
}
Ecco il codice completo con le modifiche effettuate:
/* Verifica Proprietà messaggi letti*/
/* dopo che loop terminato */
/* con l'utilizzo di never_claim */
mtype { SINC, ASINC,setValue,getValue};
chan c = [3] of {mtype,mtype ,byte};
bool fine;
byte cont=3;
byte statoCoda;
bool invio;
bool ricevi
proctype Sender(chan out) {
do
:: cont>0 -> out! ASINC,setValue,cont->
cont=cont-1; statoCoda=statoCoda+1;invio=true;
:: else -> printf("Ciclo terminato\n");
fine=1;
break;
od
}
67
proctype Receiver(chan in) {
byte s;
do
:: in ? ASINC,setValue,s ->
printf("Ricevuta chiamata a setValue dal Sender\n");
printf("Letti parametro %u\n",s);
statoCoda=statoCoda-1;
if
:: statoCoda==0->ricevi=true;
fi
od
}
init {
run Sender(c);
run Receiver(c);
}
Sezione 3.3 – Esempi
Caso Macchinetta Self Service
La prima proprietà da verificare è la seguente:
Per una chiamata asincrona, inviato il messaggio da parte del sender questo viene
ricevuto dal Reicever.
Prendiamo come Sender l’Utente e come Receiver la Parte Frontale. E’ necessario
verificare che prima il canale sia non vuoto, ovvero che l’utente abbia inserito le
monete, poi che il canale sia vuoto, ovvero che la parte frontale ha ricevuto le monete
consumando il messaggio sul canale:
proctype monitor() {
atomic {
nempty(canale) ->
empty(canale) ->
assert(true);
}
}
68
Naturalmente in questo modo è necessario isolare l’interazione tra i due oggetti dalle
altre interazioni in quanto altrimenti non è possibile riconoscere l’oggetto che sta
producendo il messaggio da quello che lo sta consumando.
E’ possibile procedere alla verifica della stessa proprietà tramite la definizione di un
never_claim su variabili booleane appositamente definite.
Quando l’utente va ad inserire le monete pone un flag di invio a true:
out ! inserisceMonete(imp) ->
moneteInviate = true;
Quando la parte frontale riceve le monete, imposta un flag a true per indicare
l’avvenuta ricezione:
in ? inserisceMonete(imp) -> moneteRicevute = true;
La proprietà da verificare è la seguente:
<> (moneteInviate -> X<> moneteRicevute)
La traduzione in SPIN della proprietà precedente è:
never {
/* !(<> (moneteInviate -> X<> moneteRicevute)) */
accept_init:
T0_init:
if
:: ((moneteInviate)) -> goto accept_S4
fi;
accept_S4:
T0_S4:
if
:: (! ((moneteRicevute)) && (moneteInviate)) -> goto accept_S4
fi;
}
La seconda proprietà da verificare, riguarda le chiamate sincrone ed è la seguente:
Per una chiamata sincrona, ricevuto il messaggio dal Receiver questo risponde al
Sender che riceve la risposta.
Nel nostro esempio, la chiamata sincrona che possiamo verificare è la
selezioneProdotto che la Parte Frontale esegue verso il Contenitore dei prodotti.
Utilizziamo entrambi i metodi presentati precedentemente.
Il monitor che si occupa di verificare la proprietà può essere il seguente:
69
proctype Monitor() {
atomic {
nempty(canale) ->
empty(canale) ->
nempty(canale) ->
empty(canale)->
assert(true);
}
}
In questo caso, utilizzando un singolo canale è necessario verificare che il Sender
produca il messaggio di richiesta e il Sender lo consumi e in seguito che il Sender
produca il messaggio di risposta e il Receiver lo consumi.
Possiamo verificare la stessa proprietà utilizzando il never_claim e definendo dei flag
booleani ad hoc per la verifica.
Quindi, la Parte Frontale quando produce il messaggio di selezione imposta il flag
selezionoProdotto a true:
out ! selezione(ID) -> selezionoProdotto = true;
Il Contenitore dei Prodotti, consuma il messaggio, indicandolo nel flag:
in ? selezione(ID) -> ricevoSelezione = true;
Infine, la Parte Frontale, ricevendo il prodotto, imposta il relativo flag a true per
completare la verifica:
in ? return(product) -> ricevoProdotto = true;
La formula da verificare è:
<>( selezionoProdotto ->
ricevoProdotto))
X<> (ricevoSelezione -> X<>
Tradotta da SPIN come:
never {
/* !(<>( selezionoProdotto -> X<> (ricevoSelezione ->
X<> ricevoProdotto))) */
accept_init:
T0_init:
if
70
:: ((selezionoProdotto)) -> goto accept_S1
fi;
accept_S1:
T0_S1:
if
:: ((ricevoSelezione) && (selezionoProdotto)) -> goto
accept_S8
fi;
accept_S8:
T0_S8:
if
:: (! ((ricevoProdotto)) && (ricevoSelezione) &&
(selezionoProdotto)) -> goto accept_S8
fi;
}
Caso Ordina Libri
Iniziamo con il verificare le proprietà tramite il processo monitor
Per la proprietà 1 si può definire un monitor che controlli se il catalogo e l’ordine
abbiamo ricevuto il messaggio.
/* proprietà 1*/
active proctype Monitor() {
atomic {
nempty(user_cat) ->
empty(user_cat) ->
assert(true)
}
atomic {
nempty(user_ord) ->
empty(user_ord) ->
assert(true)
}
}
Anche per la proprietà 2 si può definire un monitor che controlli se l’user abbia
ricevuto risposta dal catalogo e dall’ordine.
/* proprietà 2 */
active proctype Monitor() {
atomic {
nempty(user_cat) ->
empty(user_cat) ->
nempty(user_cat) ->
empty(user_cat) ->
assert(true)
}
71
atomic {
nempty(user_ord) ->
empty(user_ord) ->
nempty(user_ord) ->
empty(user_ord) ->
assert(true)
}
}
Per la proprietà 3 si è deciso di considerare l’intera esecuzione e controllare se
l’ordine è avvenuto con successo
/* proprietà 3 */
active proctype Monitor() {
atomic {
nempty(user_cat) ->
empty(user_cat) ->
nempty(user_cat) ->
empty(user_cat) && risCatalogo==true->
nempty(user_ord) ->
empty(user_ord) ->
nempty(user_ord) ->
empty(user_ord) && risOrdine==true->
assert(true)
}
}
Verifica delle proprietà utilizzando Never_Claim
Viene riportato il codice di OrdinaLibri nel caso dell’utilizzo del never_claim in
quanto fa uso di variabili aggiuntive per indicare lo stato della coda.
/* esempio OrdinaLibri Never */
mtype { ordina,cerca,ris};
chan user_cat = [1] of {mtype,byte };
chan user_ord = [1] of {mtype,byte,byte};
chan cat_user = [1] of {mtype,bool };
chan ord_user = [1] of {mtype,bool};
/* variabili che indicano lo stato della coda*/
bool invioCat;
bool invioOrd;
bool leggoCat;
bool leggoOrd;
bool leggoUserCat;
bool leggoUserOrd;
72
bool risCatalogo;
bool risOrdine;
proctype user(chan in1,in2,out1,out2) {
byte codice=34;
byte quantita=44;
out1 ! cerca,codice -> invioCat=true;
do
:: in1 ? ris,risCatalogo -> leggoUserCat=true;
if
:: risCatalogo==true ->printf("libro trovato, inoltrato ordine");
run ordine(user_ord,ord_user);
out2 ! ordina,codice,quantita -> invioOrd=true;
do
:: in2 ? ris,risOrdine -> leggoUserOrd=true;
if
::risOrdine==true->printf("ordinato effettuato");
::else -> printf("quantita non disponibile");
fi
od
:: else ->
printf("libro non trovato");
break;
fi
od
}
proctype ordine(chan in,out) {
byte libri[3];
libri[0]=12;
libri[1]=27;
libri[2]=34;
byte numero[3];
numero[0]=43;
numero[1]=5;
numero[2]=56;
byte libro;
byte quantita;
bool ordineEffettuato;
do
:: in? ordina,libro,quantita -> leggoOrd=true;
if
:: (libri[0]==libro) & (numero[0]>=quantita)->
73
ordineEffettuato=true;
numero[0]=numero[0]-quantita;
:: (libri[1]==libro) & (numero[1]>=quantita)->
ordineEffettuato=true;
numero[1]=numero[1]-quantita;
:: (libri[2]==libro) & (numero[2]>=quantita)->
ordineEffettuato=true;
numero[2]=numero[2]-quantita;
:: else ->ordineEffettuato=false;
fi;
out ! ris,ordineEffettuato;
od
}
proctype catalogo(chan in,out) {
bool verificaPresenza;
byte libri[3];
libri[0]=12;
libri[1]=27;
libri[2]=34;
byte libro;
do
:: in? cerca,libro -> leggoCat=true;
if
:: libri[0]==libro ->verificaPresenza=true;
:: libri[1]==libro ->verificaPresenza=true;
:: libri[2]==libro ->verificaPresenza=true;
:: else ->verificaPresenza=false;
fi;
out ! ris,verificaPresenza;
od
}
init {
run user(cat_user,ord_user,user_cat,user_ord);
run catalogo(user_cat,cat_user);
}
74
Proprietà 1 , vengono verificate separatamente la lettura del messaggio da parte del
catalogo e da parte dell’ordine
<>(invioCat -> X<>leggoCat)
<>(invioOrd -> X<>leggoOrd)
Proprietà 2, vengono verificate separatamente la lettura da parte dell’user delle
risposte inviate dal catalogo e dall’ordine
<>(invioCat -> X<>(leggoCat -> X<>leggoUserCat))
<>(invioOrd -> X<>(leggoOrd -> X<>leggoUserOrd))
Proprietà 3, si è deciso di considerare l’intera esecuzione e controllare se l’ordine è
avvenuto con successo
<>(invioCat-> X<>(leggoCat -> X<>((leggoUserCat && risCatalogo) -> X<>(invioOrd ->
X<>(leggoOrd -> X<>(leggoUserOrd && risOrdine))))))
75
Bibliografia
• Promela and SPIN man pages and guidelines for verification
(http://spinroot.com)
• Tesina del corso di MFIS “Verifica formale del
software:SPIN”, D.Nifosi
• OMG Unified Modeling Language (OMG UML),Superstructure, V2.1.2
(http://www.omg.org/spec/UML/2.1.2/Superstructure/PDF)
• Sparx Systems - UML 2 Tutorial - Sequence Diagram
• Materiale didattico per il corso Modeling Web Application with the UML
tenuto da Henry Muccini (http://www.di.univaq.it/muccini/MWT07)
• Materiale didattico per il corso Formal Methods for Distributed Systems
tenuto da Andrew Ireland (http://www.macs.hw.ac.uk/~air/spin/)
76