|
// TTCN-3
// TUTORIAL
// USAGE
// INTERFACES
// DOWNLOAD
// ABOUT
TTCN-3 TUTORIALTWO COFFEES, PLEASETTCN-3 is a language for testing reactive systems. A reactive system accepts stimuli from the environment and issues responses. To test a reactive system, you provide stimuli and analyze the responses.As a simple case study of a reactive system we will manufacture and test a coffee machine. OverviewThe coffee machine accepts coins as stimuli and responds with coffee. For fifty cents it will emit a coffee. If you supply only thirty cents it will wait for another twenty cents until it emits the coffee. If you then supply fifty cents it will respond with a coffee and will take the ramaining twenty cents as a prepayment for the next coffee.We procede in three steps: At first, we will model the system in TTCN-3, i.e. we will write a TTCN-3 component that behaves like a coffee machine. This may act as a specification and it will allow us to run our tests before the real coffee machine is available as a product. (It also allows us to introduce/recall important TTCN-3 language feature before we discuss the interaction of TTCN-3 and external devices.) We will also provide a simple test case that orders two coffees. We will then build the coffee machine. For reasons of economy we don't build a hardware device but deliver a coffee machine written in C#. To understand the interface of the machine before we will connect it with TTCN-3, we will write a C# program that interacts with the machine. Finally we will connect the TTCN-3 test suite and the external device. This allows us to study the standardized API that defines how we adapt an abstract test suite to a specific system under test. TTCN-3We now describe the coffee machine in TTCN-3. We first define the abstract interface.
// Coffee Machine
type port IntegerInputPortType message { in integer }
type port CharstringOutputPortType message { out charstring }
type component CoffeeMachineComponentType {
port IntegerInputPortType InputPort;
port CharstringOutputPortType OutputPort;
}
We introduce two port types:
IntegerInputPortType is the type of a port that acts as input
for integers (we will represent coins as integers).
CharstringOutputPortType is the type of a port that serves to output
charstrings (we will represent a cup of coffee by the string "coffee".
CoffeeMachineComponentType is th type of the coffee machine. A coffee machine has two ports: one to accept coins (integers) and one to emit coffee (charstrings).
We now define the behaviour of the machine as a function
CoffeeMachineFunction. It runs on a component of type
CoffeeMachineComponentType and therefore has access to the ports
of a coffee machine.
The behaviour is described as a function:
In the first case the component increments the coffee counter and waits again. After 5.0 seconds the component checks the counter. If its value is 2, everything is ok and the component sets the test verdict to pass.
In the test case definition we create, connect and start the two components:
Because the coffee machine is in an infinite loop,
we wait 6.0 seconds and shut it down:
+-------------------------------------------+ | | | +----------+ +---------+ | | | | | | | | | OutputPort --> InputPort | | | |Coffee | | Coffee| | | |Drinker | | Machine| | | | InputPort <-- OutputPort | | | | | | | | | +----------+ +---------+ | | | +-------------------------------------------+ The External Coffee MachineWe now build the "real" coffee machine. It is implemented by the C# class CoffeeMachine
using System.Collections.Generic;
using System.Threading;
using Etsi.Ttcn3;
public class CoffeeMachine {
public static Queue<byte[]> Input;
public static Queue<byte[]> Output;
static Thread Task;
public static void SwitchOn()
{
Input = new Queue<byte[]>();
Output = new Queue<byte[]>();
Task = new Thread( new ThreadStart(Behaviour) );
Task.Start();
}
public static void SwitchOff()
{
Task.Abort();
}
static void Behaviour()
{
const int price = 50;
int amount = 0;
while(true) {
while(Input.Count == 0) Thread.Sleep(100);
byte[] bytes = Input.Dequeue();
int i = Convert.ByteArrayToInt(bytes);
amount = amount+i;
while (amount >= price) {
Output.Enqueue(Convert.StringToByteArray("coffee"));
amount = amount-price;
}
}
}
}
The class provides two public functions:
SwitchOn and SwitchOff.
SwitchOn starts a thread that executes the method Behaviour. SwitchOff terminates this thread. The method Behaviour is very similar to the TTCN-3 function that described the behaviour of the coffee machine. Messages (coins and coffee) are arrays of bytes. There is an input and output queue of such messages. The receive statements is replaced by polling the input message queue and dequeuing the first element if a message arrives. The send statement is replaced by enqueuing a message into the output queue.
Before we are confronted with the complexity of connecting
this device with our TTCN-3 test,
we first take a look at C# class that interacts with the coffee machine.
The behaviour of the threads is described by the methods SenderBehaviour and ReceiverBehaviour. The Sender sends three messages to the coffee machine by enqueuing them into its input queue. The Sendersimply waits for messages in the machine's output queue and displays them. The configuration can be depicted as follows: +--------+ +---------+ | | | | |Sender | --> InputQueue | +--------+ | Coffee| +--------+ | Machine| |Receiver| <-- OutputQueue | | | | | +--------+ +---------+
Converting integers and string into arrays of bytes and vice versa
is delegated to a class Convert that we do not further discuss here.
Testing the External Coffee Machine with TTCN-3We now use TTCN-3 to test the "real" coffee machine. To do this we have to rewrite our test case from above as follows:
testcase TwoCoffeesPlease ()
runs on EmptyComponentType
system CoffeeDrinkerComponentType
{
var CoffeeDrinkerComponentType CoffeeDrinker;
CoffeeDrinker := CoffeeDrinkerComponentType.create;
map(CoffeeDrinker:OutputPort, system:OutputPort);
map(CoffeeDrinker:InputPort, system:InputPort);
CoffeeDrinker.start( CoffeeDrinkerFunction() );
CoffeeDrinker.done;
unmap(CoffeeDrinker:OutputPort, system:OutputPort);
unmap(CoffeeDrinker:InputPort, system:InputPort);
}
First, we add a system clause to the test case heading.
The system clause describes how the test system
(the program written in TTCN-3) appears to its environment. This is done
by specifying a component type. The test system appears as a component of this
type. In our case the test system appears to its environment as component
of type CoffeeDrinkerComponentType.
(If there is no system clause the same type as specified in the mandatory runs on clause is implicitely assumed. There is also an implicite mapping of ports that we avoid in this example. In the above test cases the runs on clause specified an empty component and therefore the test system also appeared as component without ports and hence no connection to its environment.)
The new test case header is
Instead of connecting the output port of CoffeeDrinkerComponent with the input port of CoffeeMachineComponent and the input port of CoffeeDrinkerComponent with the output port of CoffeeMachineComponent we use map statements to to define the external appearance of the test system:
We map the output port of the CoffeeDrinkerComponent
to the output port of the test system and map the
input port of the CoffeeDrinkerComponent to the
input port of the test system.
Here is the complete module
+-------------------------------+ | Test System| | +----------+ | | | | | | | OutputPort --> OutputPort | |Coffee | | | |Drinker | | | | InputPort <---- InputPort | | | | | +----------+ | | | +-------------------------------+ The AdapterThe coffee test suite is abstract, it does not make any assumptions about the concrete coffee machine. It only tests the abstract protocol without depending on a concrete data encoding and a concrete message passing mechanism.This has a price: we have to adapt the abstract test suite to the concrete system under test. The test system emits stimuli and receives responses. So an adapter can be decomposed into a stimulus adapter and a response adapter. The stimulus adapter obtains stimuli from the test system and passes them to the system under test. The response adapter waits for responses of the system under test and passes them to the test system. In our example: +-------------------------------+ | Test System| | +----------+ | +--------+ +---------+ | | | | |Stimulus| | | | | OutputPort --> OutputPort --->|Adapter | --> InputQueue | | |Coffee | | +--------+ | Coffee| | |Drinker | | +--------+ | Machine| | | InputPort <---- InputPort <-- |Response| <-- OutputQueue | | | | | |Adapter | | | | +----------+ | +--------+ +---------+ | | +-------------------------------+The stimulus adapter is invoked by the test system each time it has to submit a message to the system under test. It can run on the same thread as the CoffeeDrinker component. The response adapter, however, has to wait for messages from the system under test. Hence it must be started as an independent thread. It cannot run on the thread of the coffee machine since we are not allowed to modify the system under test. The stimulus adapter is invoked from time to time by the test system. Hence it has to offer methods known to the test system. These methods are standardized in the TRI (TTCN-3 Runtime Interface) specification. They are collected in the interface TriCommunicationSA. In order to implement a specific version, the stimulus adapter is derived from class Adapter (which implements TriCommunicationSA) and overrides the required methods. The response adapter passes messages to the test system, i.e. has to call specific methods implemented by the test system. These methods are also defined in the TRI specification in the interface TriCommunicationTE. An object of a class implementing this interface can be obtained via a call Etsi.Ttcn3.Framework.GetTriCommunicationTE(). Note that the response adapter does not have to implement the methods, it just calls them. This situation is summarized in the following figure. +----------------+ +--------------------+ | Test System | | Stimulus Adapter | | | | | | | | implements | | | | +--------------+ | | calls------->|FromTestSystem| | | | | |methods | | | | | +--------------+ | | | +--------------------+ | | +--------------------+ | implements | | Response Adapter | | +------------+ | | | | |ToTestSystem|<-------calls | | |methods | | | | | +------------+ | | | +----------------+ +--------------------+
The Stimulus AdapterWe now implement the stimulus adapter for the coffee machine. It has to provide methods that are called by the test system at certain events, e.g. when a test case excutes a send statement. In this case the test system invokes a method of an adapter object. The class of this object must have been registered by the user as his or her adapter implementation.The adapter class is derived from the class FromTestSystem. In our case it overrides the following methods:
When the test case executes a statement
compPortId is a description of Component:ComponentPort, tsiPortId is a description of system:SystemPort. We can use this information to establish a connection to the system under test. In our case it suffices to switch on the coffee machine. We also take the opportunity to switch on the response adapter that we will discuss in the next section. Because the response adapter has to pass messages to the InputPort of the test system with the target component CoffeeDrinkerComponent we supply the corresponding information: tsiPortId and the identifier of the CoffeeDrinkerComponent which is obtained by compPortId.getComponentId(). This information is available at the second call of triMap which is identified by the fact that compId.getPortName() yields the string "InputPort". We signal successful execution of triMap by returning the the value TriStatus.TRI_OK.
Here is our definition of triMap:
sendMessage.getEncodedMessage() returns the encoded
form of Value (which is of type bytes[]).
We simply pass this to the coffee machine:
The Response AdapterWe also have to implement the response adapter for the coffee machine. It waits for messages from the system under test and passes them to the test system. Because this can happen at any time and because we cannot implement the response adapter as part of the system under test, we have to invoke it on its own thread.In our implementation of the C# coffee machine user the Receiver took a similar role. We can take it as our starting point. We introduce a class ResponseAdapter with a private method ReceiverBehaviour. This method defines the behaviour of a thread that is started when the public method SwitchOn is invoked. It is terminate when the the public method SwitchOff is called. The method ReceiverBehaviour polls the output queue of the coffee machine for outgoing messages. If there is a new one it must be removed from the queue and passed to the test system. For this purpose the test system provides a class that implements the interface TriCommunicationTE with methods that the response adapter can call.
For example, if a test case executes a statement
If bytes is the encoded value obtained from the coffee machine,
Here is the complete response adapter (file ResponseAdapter.cs):
The CodecThe test system abstracts from the concrete message passing mechanism of the system under test as well as from the concrete data encoding. The adapter discussed above bridges the first gap. However, we still have to deal with the second.In our abstract test suite data are described as values of TTCN-3 types. When they are passed to the outer world (e.g. as argument of triSend) they are encoded as byte arrays. Vice versa, when we pass data to the test system (e.g. as argument of triEnqueueMsg), we have to provide them as byte arrays, in the test case they are then received as values of TTCN-3 types.
This encoding and decoding depends on the concrete system under test
and must be specified by the user.
The user has to provide a class that implements
the TCI interface TciCDProvided
(for example by deriving from class Codec)
and defines the methods
encode and decode:
value.getType() returns the type of the value (of type Type). Then type.getTypeClass() yields the type class (of type int). This can be used to select a type-specific encoding action. In our case we expect only values of TTCN-3 type integer with a type class of TypeClass.Integer. If the type class is TypeClass.Integer we safely cast the Value to a IntegerValue. From such an object we can obtain the int it represents using the method getInt().
After converting this to an array of bytes we construct a new
TriMessage and set its message field to the array of bytes.
If the type class of decodingHypothesis is
TciTypeClass.CHARSTRING,
we convert the byte array of the message into a string.
We construct a new
CharstringValue (subtype of Value),
which contains this string.
The new value is returned by decode.
Compiling and Running the ExampleTo compile the TTCN-3 module (CoffeeSuite.ttcn3) use the command:C:\ESPRESSO\ttcn-3\ttxp /compile CoffeeSuite(assuming your TTCN-3 system has been installed in the directory C:\ESPRESSO).
Use the C# compiler csc as follows to compile adapter, codec,
and other C# files:
To run the example then type:
|