Lesson 6: Armouring the Valve Controller¶
Let’s consider the port ValveDriver
, which defines the interface contract between two Coco components, namely
ValveControllerImpl
and ValveDriverImpl
. Since both of these components are written in Coco, the verification
checks whether ValveDriverImpl
correctly implements ValveDriver
as its provided port, and whether
ValveControllerImpl
uses ValveDriver
correctly as its required port. If ValveControllerImpl
does use ValveDriver
correctly, then we say that ValveControllerImpl
is a well-behaved client of that port (and by definition, of any
implementation thereof), since it only makes calls on ValveDriver
that are allowed, can handle any return values
received, and can handle every signal sent by ValveDriver
.
When a Coco component is used by a non-Coco client, the verification assumes that the non-Coco client is a well-behaved client of the Coco component’s provided port. However, this does not always reflect reality when interfacing with native code that has not been developed in Coco and verified. Although the native code can be tested with the code generated from Coco, this does not guarantee that the client will always behave correctly with the Coco component it uses.
To address this problem, we are going to use a technique called armouring, where we insert a new Coco component
between the non-Coco client, and the Coco component that it is using. The purpose of this new component, called
ArmourImpl
, is to implement a robust version of ValveController
, which will not allow any client misbehaviour to reach
ValveControllerImpl
. Since ArmourImpl
is written in Coco, the verification will check that there is no execution
scenario that will violate ValveController
.
We will proceed as follows:
- Declare a port called
Armour
where every function is legal in every state, and returns a boolean value indicating whether the function can be called onValveControllerImpl
(i.e. it is legal according to its provided portValveController
), or is rejected because it is illegal to call it onValveControllerImpl
. - Implement a new component called
ArmourImpl
, which implementsArmour
and will sit in between theApplication
external component and theValveGroup
. Its purpose is to intercept all calls made byApplication
onValveGroup
, and only forward those that are allowed accordingValveController
(which is the provided port ofValveControllerImpl
). - Implement a new encapsulating component called
ArmouredValveGroup
, which will declare an instance ofArmourImpl
andValveGroup
, and connect them together.
Step 1: Specifying the Armour
port¶
To specify the Armour
port, we need to examine ValveController
to identify those functions that are always permitted versus
those that are sometimes not allowed. Examining ValveController
, we can see that:
isOpen()
andisClosed()
are allowed in every state, and can always be passed through.open()
andclose()
are only allowed inReady
, and must be rejected in all other states.setup(…)
is only allowed inInitial
, and must be rejected in all other states.
We need to define new signatures for those functions that can be rejected, namely open()
and
close()
to return a boolean as follows:
The other signatures are unchanged.
The state machine builds on your existing state machine for ValveController
, and extends it to allow all legal calls, and
intercept all illegal calls as follows:
machine {
var position : ValveDriver.Status = .Unknown
isOpen() = position == .Opened
isClosed() = position == .Closed
open() = false
close() = false
setup(_) = false
state Initial {
setup(_) = nondet {
{
setNextState(Ready);
return true;
},
{
return false;
},
}
}
state Ready {
open() = {
setNextState(Moving(.Opened));
return true;
}
close() = {
setNextState(Moving(.Closed));
return true;
}
}
state Moving(target : ValveDriver.Status) {
entry() = {
position = .Unknown;
}
spontaneous = setNextState(Ready)
exit() = {
position = target;
}
}
}
The complete Coco for this port is in Example_06B/src/Armour.coco
.
Step 2: Building the ArmourImpl
Component¶
We now need to build the ArmourImpl
component, which implements the Armour
port, and requires the ValveController
port. This
component should return true
on every allowed function call, and false
otherwise.
As is common with armour components, the component state machine is very similar to the state machine in its
provided port. This means that we can take the following steps as a shortcut to implement the ArmourImpl
component:
- Create a new module called ArmourImpl.coco, and declare a component called
ArmourImpl
. - Copy the state machine from the
Amour
port, and paste it into theArmourImpl
component declaration. - Modify the
ArmourImpl
component state machine to convert it into a proper implementation of its provided port, and add the necessary calls to its required port. - Verify your work after each step, and fix any verification errors that arise.
Declaring a skeleton component¶
Create a new module called ArmourImpl.coco. Details on how to create Coco modules can be found in Lesson 1.
The next step is to add the following lines to create a skeleton ArmourImpl
component without a state machine:
Building the ArmourImpl
State Machine¶
To get started, reuse the state machine from Armour
by copying and pasting it into the ArmourImpl
component.
This will give a number static errors as expected. To convert the copied port state machine into a proper component
state machine, follow these steps:
Delete the declaration of the variable
position
. This is not needed in the component state machine, because the valve state will be obtained directly from theValveControllerImpl
component (viaValveController
).Add the provided port name,
client
, as a prefix to all of the incoming function events. For example,isOpen()
becomesclient.isOpen()
.Modify the transitions for the
client.isOpen()
,client.isClosed()
,client.open()
,client.close()
, andsetup(…)
functions as follows:client.isOpen() = valve.isOpen() client.isClosed() = valve.isClosed() client.open() = false client.close() = false client.setup(_) = false
Modify the
client.setup(…)
transition in stateInitial
as follows:client.setup(settings : ValveParameters) = { val result : Bool = valve.setup(settings); if (result) setNextState(Ready); return result; }
The variable
result
is declared as a constant, because it is not modified once it has been initialised.Modify the transitions in the
Ready
state as follows:state Ready { client.open() = { valve.open(); setNextState(Moving); return true; } client.close() = { valve.close(); setNextState(Moving); return true; } }
Delete the parameters from the
Moving
state declaration.Delete the entry and exit functions from the
Moving
state.Replace the
spontaneous
transition in theMoving
state with a transition for the incoming signal eventvalve.moveEnded(…)
as follows:valve.moveEnded(_) = setNextState(Ready)
The complete Coco for this component is in Example_06B/src/ArmourImpl.coco
.
Verifying your Changes (Example_06B)¶
The verification finds the ArmourImpl
component is not well-formed, because its queue capacity is exceeded:
This counterexample illustrates how the ArmourImpl
component can receive a signal from its required port valve
. This
immediately exceeds its queue capacity, because we didn’t specify a queue for ArmourImpl
that can hold any signals.
Specifying a Queue¶
To fix the verification error above, we need to add the following line immediately in front of the ArmourImpl
component’s declaration:
@queue(1)
The complete Coco for this component is in Example_06C/src/ArmourImpl.coco
.
Verifying your Changes (Example_06C)¶
All of the ports and components verify successfully.
Step 3: Building the ArmouredValveGroup
Encapsulating Component¶
To make it more convenient for a client application using the armoured version of the ValveControllerImpl
, we will
make a new encapsulating component called ArmouredValveGroup
. Clients that want to use the armoured version can then
instantiate ArmouredValveGroup
while clients that want the unarmoured version can instantiate ValveGroup
. The
armoured version would typically be used by clients not written in Coco, because their correct use of the
ValveControllerImpl
cannot be verified by the Coco Platform. In contrast, clients written in Coco would typically use
the unarmoured version, because verifying these clients guarantees that they use the ValveControllerImpl
correctly.
The new encapsulating component is declared as follows:
import unqualified Armour
import unqualified ArmourImpl
import unqualified TestValveGroup
@runtime(.MultiThreaded)
component ArmouredValveGroup {
val client : Provided<Armour>
val armour : ArmourImpl
val testValve : TestValveGroup
init() = {
connect(client, armour.client);
connect(armour.valve, testValve.client);
}
}
The following architecture diagram is a graphical representation of ArmouredValveGroup
:
This binds the TestValveGroup
encapsulating component, which consists of instances of ValveControllerImpl
,
ValveDriverImpl
and SimulatorImpl
, to the ArmourImpl
component, thereby forming a testable stack of software
components. In turn, this means that a C++ program can then instantiate and access these components in a straightforward
way.
Verifying Your Project (Example_07A)¶
The project verifies without errors.