Welcome to TTCN-3 Express
Hello World
Here is a simple TTCN-3 module named HelloWorld,
which is stored in a file named HelloWorld.ttcn3.
module HelloWorld {
control {
log("Hello World");
}
}
The module merely consists of a control part
which uses a log statement to emit the text
Hello World.
The command
ttxp /run HelloWorld
compiles the module HelloWorld
and executes its control part. Hence,
Hello World is displayed.
If you type the command a second time,
the control part is executed once more,
but the module is not compiled again.
However, if you modify the module,
typing the command causes recompilation and execution of the
modified module.
If you just want to compile the module without execution,
type
ttxp /compile HelloWorld
Instead of the module name you may specify the file name
as in
ttxp /compile HelloWorld.ttcn3
and
ttxp /run HelloWorld.ttcn3
A module M must be contained in a file M.ttcn3.
A file can contain only one module.
A command
ttxp /run M
or
ttxp /run M.ttcn3
compiles module M in file M.ttcn3 (if neccessary)
and executes its control part.
A command
ttxp /compile M
or
ttxp /compile M.ttcn3
only compiles the module.
You may write library modules and compile them separately.
Here is a file LibraryModule.ttcn3. It contains a module
LibraryModule that provides a function HelloWorld
for reuse in other modules.
module LibraryModule
{
function HelloWorld()
{
log("Hello World");
}
}
This module is compiled with the command
ttxp /compile LibraryModule
Here is a module MainModule stored in MainModule.ttcn3.
It imports the content of LibraryModule and
invokes the function HelloWorld in its control part.
module MainModule
{
import from LibraryModule all;
control
{
HelloWorld();
}
}
MainModule is compiled and its control part is executed by
ttxp /run MainModule
Test Suites
A TTCN-3 test suite is decomposed into test cases.
Here is a test case that displays Hello.
testcase Hello() runs on EmptyComponentType
{
log("Hello");
}
A test case runs as a process on a component.
This can contain ports to communicate with other processes
and variables that are visible to the functions invoked by the process.
You have to specify the type of this component, even it the component
has no content. The type of an empty component can be declared
by
type component EmptyComponentType {}
Here is a complete test suite with two test cases.
module Suite {
type component EmptyComponentType {}
testcase Hello() runs on EmptyComponentType
{
log("Hello");
}
testcase GoodBye() runs on EmptyComponentType
{
log("Good Bye");
}
control {
execute( Hello() );
execute( GoodBye() );
}
}
As usual you run the control part by
ttxp /run Suite
The control part executes the two test cases and hence
Hello and Good Bye is displayed.
You may also execute a single test case.
Use the command
ttxp /run Suite Hello
to run the Hello test case.
Only Hello is displayed.
A command
ttxp /run M T
or
ttxp /run M.ttcn3 T
executes the test case T of module M.
Two Coffees, Please
TTCN-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 analize the responses.
As a simple case study of a reactive system
we will manufacture and test a coffee machine.
The 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 APIs that define how we adapt
an abstract test suite to a specific system under test.
TTCN-3
We 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.
function CoffeeMachineFunction() runs on CoffeeMachineComponentType
{
const integer Price := 50;
var integer Amount, Cents;
Amount := 0;
while (true) {
InputPort.receive(integer:?) -> value Cents;
Amount := Amount+Cents;
while (Amount >= Price) {
OutputPort.send(charstring:"coffee");
Amount := Amount-Price;
}
}
}
In an infinite loop the machine performs the following steps:
InputPort.receive(integer:?) -> value Cents;
receives an arbitrary integer from the port InputPort
its value is redirected to the variable Cents.
The value is added to the amount of money that the machine already has
gathered. For each fifty cents the machine then emits a coffee via its
output port:
OutputPort.send(charstring:"coffee");
Now let us introduce a component that uses the coffee machine.
It can be used to check whether our machine functions correctly.
Again we start with type definitions:
// Coffee Drinker
type port IntegerOutputPortType message { out integer }
type port CharstringInputPortType message { in charstring }
type component CoffeeDrinkerComponentType
{
port CharstringInputPortType InputPort;
port IntegerOutputPortType OutputPort;
}
The component has an output port to emit integer values (coins)
and an input port to accept charstring values (coffee).
The behaviour is described as a function:
function CoffeeDrinkerFunction() runs on CoffeeDrinkerComponentType
{
var integer Count;
OutputPort.send(100);
Count := 0;
timer t;
t.start(1.0);
alt {
[] InputPort.receive(charstring:"coffee") {
Count := Count+1;
repeat;
}
[] t.timeout {
}
}
if (Count == 2) { setverdict(pass); }
}
The component emits 100 cents (the price of two coffees) by
OutputPort.send(100);
It then waits 1.0 seconds for coffee.
This is done by declaring and starting a timer that runs for 1.0 seconds:
timer t;
t.start(1.0);
Then the component waits for two alternative events to happen:
InputPort.receive(charstring:"coffee")
coffee has arrived via the input port, or
t.timeout
time is over.
In the first case the component increments the coffee counter
and waits again.
After 1.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:
testcase TwoCoffeesPlease () runs on EmptyComponentType
{
var CoffeeMachineComponentType CoffeeMachine;
var CoffeeDrinkerComponentType CoffeeDrinker;
CoffeeMachine := CoffeeMachineComponentType.create;
CoffeeDrinker := CoffeeDrinkerComponentType.create;
connect(CoffeeDrinker:OutputPort, CoffeeMachine:InputPort);
connect(CoffeeDrinker:InputPort, CoffeeMachine:OutputPort);
CoffeeMachine.start( CoffeeMachineFunction() );
CoffeeDrinker.start( CoffeeDrinkerFunction() );
timer t; t.start(2.0); t.timeout;
CoffeeMachine.stop;
}
For example,
CoffeeMachine := CoffeeMachineComponentType.create;
creates a component of type CoffeeMachineComponentType.
connect(CoffeeDrinker:OutputPort, CoffeeMachine:InputPort);
connects the output port of the coffee drinker with the input port
of the coffee machine.
CoffeeMachine.start( CoffeeMachineFunction() );
starts the coffee machine component and defines
CoffeeMachineFunction() as its behaviour.
Because the coffee machine is in an infinite loop,
we wait 2.0 seconds and shut it down:
timer t; t.start(2.0); t.timeout;
CoffeeMachine.stop;
Here is the complete module.
module TestCoffeeMachine
{
// Coffee Machine
type port IntegerInputPortType message { in integer }
type port CharstringOutputPortType message { out charstring }
type component CoffeeMachineComponentType {
port IntegerInputPortType InputPort;
port CharstringOutputPortType OutputPort;
}
function CoffeeMachineFunction() runs on CoffeeMachineComponentType
{
const integer Price := 50;
var integer Amount, Cents;
Amount := 0;
while (true) {
InputPort.receive(integer:?) -> value Cents;
Amount := Amount+Cents;
while (Amount >= Price) {
OutputPort.send(charstring:"coffee");
Amount := Amount-Price;
}
}
}
// Coffee Drinker
type port IntegerOutputPortType message { out integer }
type port CharstringInputPortType message { in charstring }
type component CoffeeDrinkerComponentType
{
port CharstringInputPortType InputPort;
port IntegerOutputPortType OutputPort;
}
function CoffeeDrinkerFunction() runs on CoffeeDrinkerComponentType
{
var integer Count;
OutputPort.send(100);
Count := 0;
timer t;
t.start(1.0);
alt {
[] InputPort.receive(charstring:"coffee") {
Count := Count+1;
repeat;
}
[] t.timeout {
}
}
if (Count == 2) { setverdict(pass); }
}
type component EmptyComponentType {}
testcase TwoCoffeesPlease () runs on EmptyComponentType
{
var CoffeeMachineComponentType CoffeeMachine;
var CoffeeDrinkerComponentType CoffeeDrinker;
CoffeeMachine := CoffeeMachineComponentType.create;
CoffeeDrinker := CoffeeDrinkerComponentType.create;
connect(CoffeeDrinker:OutputPort, CoffeeMachine:InputPort);
connect(CoffeeDrinker:InputPort, CoffeeMachine:OutputPort);
CoffeeMachine.start( CoffeeMachineFunction() );
CoffeeDrinker.start( CoffeeDrinkerFunction() );
timer t; t.start(2.0); t.timeout;
CoffeeMachine.stop;
}
}
The connection of ports can be depicted as follows:
+-------------------------------------------+
| |
| +----------+ +---------+ |
| | | | | |
| | OutputPort --> InputPort | |
| |Coffee | | Coffee| |
| |Drinker | | Machine| |
| | InputPort <-- OutputPort | |
| | | | | |
| +----------+ +---------+ |
| |
+-------------------------------------------+
To run this example we use the command
ttxp /run TestCoffeeMachine TwoCoffeesPlease
The External Coffee Machine
We now build the "real" coffee machine.
It is implemented by the C# class CoffeeMachine
using System.Collections.Generic;
using System.Threading;
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.
using System.Collections.Generic;
using System.Threading;
public class UseCoffeeMachine {
public static void Main()
{
CoffeeMachine.SwitchOn();
Thread Sender = new Thread(new ThreadStart(SenderBehaviour));
Thread Receiver = new Thread(new ThreadStart(ReceiverBehaviour));
Sender.Start();
Receiver.Start();
Thread.Sleep(1000);
Sender.Abort();
Receiver.Abort();
CoffeeMachine.SwitchOff();
}
static void SenderBehaviour() {
CoffeeMachine.Input.Enqueue(Convert.IntToByteArray(101));
CoffeeMachine.Input.Enqueue(Convert.IntToByteArray(102));
CoffeeMachine.Input.Enqueue(Convert.IntToByteArray(103));
}
private static void ReceiverBehaviour() {
while(true) {
while(CoffeeMachine.Output.Count == 0) Thread.Sleep(100);
byte[] bytes = CoffeeMachine.Output.Dequeue();
string str = Convert.ByteArrayToString(bytes);
System.Console.WriteLine("Received '{0}'", str);
}
}
}
The Main method of class
UseCoffeeMachine
first switches the coffe machine on.
It then starts two threads, Sender and Receiver.
After some time it terminates these threads
and switches the coffee machine off.
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.
public class Convert {
public static byte[] StringToByteArray(string str)
{
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(str);
return bytes;
}
public static byte[] IntToByteArray(int i)
{
string str = System.Convert.ToString(i);
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(str);
return bytes;
}
public static string ByteArrayToString(byte[] bytes)
{
string str = System.Text.Encoding.UTF8.GetString(bytes);
return str;
}
public static int ByteArrayToInt(byte[] bytes)
{
string str = System.Text.Encoding.UTF8.GetString(bytes);
int i = int.Parse(str);
return i;
}
}
The commands
csc UseCoffeeMachine.cs CoffeeMachine.cs Convert.cs
UseCoffeeMachine
compile and then run our C# coffee program.
Testing the External Coffee Machine with TTCN-3
We 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
testcase TwoCoffeesPlease ()
runs on EmptyComponentType
system CoffeeDrinkerComponentType
We have removed the declaration, creation and start of the coffee machine
component which now is an external device.
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.
map(CoffeeDrinker:OutputPort, system:OutputPort);
map(CoffeeDrinker:InputPort, system:InputPort);
There are also corresponding unmap statements
(as we will see, we can use them to release resources).
Before unmapping, the testcase waits until CoffeeDrinkerComponent
has terminated.
Here is the complete module
module TestCoffeeMachine
{
// Coffee Drinker
type port IntegerOutputPortType message { out integer }
type port CharstringInputPortType message { in charstring }
type component CoffeeDrinkerComponentType
{
port CharstringInputPortType InputPort;
port IntegerOutputPortType OutputPort;
}
function CoffeeDrinkerFunction() runs on CoffeeDrinkerComponentType
{
var integer Count;
OutputPort.send(100);
Count := 0;
timer t;
t.start(1.0);
alt {
[] InputPort.receive(charstring:"coffee") {
Count := Count+1;
repeat;
}
[] t.timeout {
}
}
if (Count == 2) { setverdict(pass); }
}
type component EmptyComponentType {}
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);
}
}
The mapping of ports can be depicted as follows:
+-------------------------------+
| Test System|
| +----------+ |
| | | |
| | OutputPort --> OutputPort
| |Coffee | |
| |Drinker | |
| | InputPort <---- InputPort
| | | |
| +----------+ |
| |
+-------------------------------+
The Adapter
The 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 class FromTestSystem.
In order to implement a specific version, the stimulus adapter
is derived from this class 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.
They are implemented as static methods of the class
ToTestSystem.
Note that the response adapter does not have to implement these 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 Adapter
We 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:
- triMap (called when the test case
executes a map statement)
- triUnmap (called when the test case
executes a unmap statement)
- triSend (called when the test case
executes a send statement)
When the test case executes a statement
map(Component:ComponentPort, system:SystemPort)
this causes a method invocation
triMap(compPortId, tsiPortId)
where compPortId and tsiPortId
are parameters of type TTCN3.TriPortId.
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
new TTCN3.TriStatus().
Here is our definition of triMap:
public override TTCN3.TriStatus triMap (
TTCN3.TriPortId compPortId,
TTCN3.TriPortId tsiPortId
)
{
if (compPortId.getPortName() == "InputPort") {
CoffeeMachine.SwitchOn();
ResponseAdapter.SwitchOn(tsiPortId, compPortId.getComponentId());
}
return new TTCN3.TriStatus();
}
When the test case executes a statement
unmap(Component:ComponentPort, system:SystemPort)
this results in a method call
triUnmap(compPortId, tsiPortId)
Our implementaion of triUnmap
just switches off the coffee machine and the response adapter:
public override TTCN3.TriStatus triUnmap (
TTCN3.TriPortId compPortId,
TTCN3.TriPortId tsiPortId
)
{
if (compPortId.getPortName() == "InputPort") {
CoffeeMachine.SwitchOff();
ResponseAdapter.SwitchOff();
}
return new TTCN3.TriStatus();
}
The execution of a statement
Port.send(Value)
triggers a call
triSend(componentId, tsiPortId, address, sendMessage)
componentId (of type TTCN3.TriComponentId)
identifies the sending component;
tsiPortId (of type TTCN3.TriPortId) identifies
the test system port to which Port has been mapped;
address (of type TTCN3.TriAddress) is null
(it would have a value different from null if a to
clause would have been used in the send statement);
sendMessage (of type TTCN3.TriMessage)
is the message to be sent.
sendMessage.getEncodedMessage() returns the encoded
form of Value (which is of type bytes[]).
We simply pass this to the coffee machine:
public override TTCN3.TriStatus triSend (
TTCN3.TriComponentId componentId, // sending test component
TTCN3.TriPortId tsiPortId, // port via which the msg is sent
TTCN3.TriAddress address, // optional destination address
TTCN3.TriMessage sendMessage // encoded msg to be sent
)
{
byte[] bytes = sendMessage.getEncodedMessage();
CoffeeMachine.Input.Enqueue(bytes);
return new TTCN3.TriStatus();
}
Here is the complete stimulus adapter (file StimulusAdapter.cs):
public class StimulusAdapter : TTCN3.FromTestSystem {
public override TTCN3.TriStatus triMap (
TTCN3.TriPortId compPortId,
TTCN3.TriPortId tsiPortId
)
{
if (compPortId.getPortName() == "InputPort") {
CoffeeMachine.SwitchOn();
ResponseAdapter.SwitchOn(tsiPortId, compPortId.getComponentId());
}
return new TTCN3.TriStatus();
}
public override TTCN3.TriStatus triUnmap (
TTCN3.TriPortId compPortId,
TTCN3.TriPortId tsiPortId
)
{
if (compPortId.getPortName() == "InputPort") {
CoffeeMachine.SwitchOff();
ResponseAdapter.SwitchOff();
}
return new TTCN3.TriStatus();
}
public override TTCN3.TriStatus triSend (
TTCN3.TriComponentId componentId, // sending test component
TTCN3.TriPortId tsiPortId, // port via which the msg is sent
TTCN3.TriAddress address, // optional destination address
TTCN3.TriMessage sendMessage // encoded msg to be sent
)
{
byte[] bytes = sendMessage.getEncodedMessage();
CoffeeMachine.Input.Enqueue(bytes);
return new TTCN3.TriStatus();
}
}
The Response Adapter
We 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 purpos the test system provides a class ToTestSystem
with methods that the response adapter can call.
For example, if a test case executes a statement
Port.receive(Template)
it can accept a message that has been passed to it by a call
ToTestSystem.triEnqueueMsg
(tsiPortId, SUTaddress, componentId, receivedMessage)
Here tsiPortId (of type TTCN3.TriPortId)
identifies the test system port to that Port has been mapped.
SUTaddress (of type TTCN3.TriAddress) may indicate
an internal address of the system under test that can be accessed
with a from clause in the receive statement, it may also
be null.
componentId (of type TTCN3.TriComponentId)
identifies the target component.
receivedMessage (of type TTCN3.TriMessage)
contains the encoded value that is processed by the receive
statement.
If bytes is the encoded value obtained from the coffee machine,
byte[] bytes = CoffeeMachine.Output.Dequeue();
it can be passed to the test system as follows
TTCN3.TriMessage msg = new TTCN3.TriMessage();
msg.setEncodedMessage(bytes);
TTCN3.ToTestSystem.triEnqueueMsg (PortId, null, ComponentId, msg);
We have to specify the test system port and the target component.
We require that these values are passed to the response adapter when
it is started by its SwitchOn method.
This method is invoked by triMap where the information is available.
Here is the complete response adapter (file ResponseAdapter.cs):
using System.Threading;
public class ResponseAdapter {
static Thread Receiver;
static TTCN3.TriPortId PortId;
static TTCN3.TriComponentId ComponentId;
public static void SwitchOn(TTCN3.TriPortId pid, TTCN3.TriComponentId cid)
{
PortId = pid;
ComponentId = cid;
Receiver = new Thread( new ThreadStart(ReceiverBehaviour) );
Receiver.Start();
}
public static void SwitchOff() {
Receiver.Abort();
}
private static void ReceiverBehaviour() {
while(true) {
while(CoffeeMachine.Output.Count == 0) Thread.Sleep(100);
byte[] bytes = CoffeeMachine.Output.Dequeue();
TTCN3.TriMessage msg = new TTCN3.TriMessage();
msg.setEncodedMessage(bytes);
TTCN3.ToTestSystem.triEnqueueMsg (PortId, null, ComponentId, msg);
}
}
}
The Codec
The 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 is derived from TTCN3.Codec.
This defines two methods that must be overridden.
encode and decode:
public virtual TTCN3.TriMessage encode (
TTCN3.Value value
)
and
public virtual TTCN3.Value decode (
TTCN3.TriMessage message,
TTCN3.Type decodingHypothesis
)
The method encode is invoked
(e.g. when a send statement is executed)
with a parameter value
of type TTCN3.Value.
This must be converted into a value of type
TTCN3.TriMessage. This value is then passed to
a method such as triSend.
value.getType() returns the type of the value
(of type TTCN3.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 TTCN3.TypeClass.Integer.
If the type class is TTCN3.TypeClass.Integer we safely
cast the TTCN3.Value to a TTCN3.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
TTCN3.TriMessage and set its message field to the array of bytes.
public override TTCN3.TriMessage encode(TTCN3.Value value)
{
int i;
TTCN3.Type type = value.getType();
int typeclass = type.getTypeClass();
if (typeclass == TTCN3.TciTypeClass.INTEGER) {
i = ((TTCN3.IntegerValue)value).getInt();
byte[] bytes = Convert.IntToByteArray(i);
TTCN3.TriMessage msg = new TTCN3.TriMessage();
msg.setEncodedMessage(bytes);
return msg;
}
else {
// should not be reached, signal error
TTCN3.CodecSupport.tciErrorReq("unexpected typeclass");
return null;
}
}
The method decode is called when incoming data
(passed to the test system e.g. by TTCN3.ToTestSystem.EnqueueMsg)
must be matched against a template.
In this case, e.g. when executing a statement
Port.receive(Template), the type of the templates
determines the expected type when decoding the data.
Besides the message to be decoded (of type TTCN3.TriMessage,
this type is passed as a parameter decodingHypothesis
(of type TTCN3.Type) to decode.
It can be used to select a specific decoding schema (hence different
values of different types could be encoded by the same byte sequence).
If the type class of decodingHypothesis is
TTCN3.TciTypeClass.CHARSTRING,
we convert the byte array of the message into a string.
We construct a new
TTCN3.CharstringValue (subtype of TTCN3.Value),
which contains this string.
The new value is returned by decode.
public override TTCN3.Value decode
(TTCN3.TriMessage message, TTCN3.Type decodingHypothesis)
{
int typeclass = decodingHypothesis.getTypeClass();
if (typeclass == TTCN3.TciTypeClass.CHARSTRING) {
byte[] bytes = message.getEncodedMessage();
string str = Convert.ByteArrayToString(bytes);
TTCN3.CharstringValue val = new TTCN3.CharstringValue();
val.setString(str);
return val;
}
else {
// should not be reached, signal error
TTCN3.CodecSupport.tciErrorReq("unexpected typeclass");
return null;
}
}
Here is the complete codec (file CoffeeCodec.cs):
public class CoffeeCodec : TTCN3.Codec {
public override TTCN3.TriMessage encode(TTCN3.Value value)
{
int i;
TTCN3.Type type = value.getType();
int typeclass = type.getTypeClass();
if (typeclass == TTCN3.TciTypeClass.INTEGER) {
i = ((TTCN3.IntegerValue)value).getInt();
byte[] bytes = Convert.IntToByteArray(i);
TTCN3.TriMessage msg = new TTCN3.TriMessage();
msg.setEncodedMessage(bytes);
return msg;
}
else {
// should not be reached, signal error
TTCN3.CodecSupport.tciErrorReq("unexpected typeclass");
return null;
}
}
public override TTCN3.Value decode
(TTCN3.TriMessage message, TTCN3.Type decodingHypothesis)
{
int typeclass = decodingHypothesis.getTypeClass();
if (typeclass == TTCN3.TciTypeClass.CHARSTRING) {
byte[] bytes = message.getEncodedMessage();
string str = Convert.ByteArrayToString(bytes);
TTCN3.CharstringValue val = new TTCN3.CharstringValue();
val.setString(str);
return val;
}
else {
// should not be reached, signal error
TTCN3.CodecSupport.tciErrorReq("unexpected typeclass");
return null;
}
}
}
Using Adapter and Codec
Before we can run a test suite using our adapter and codec
we have to compile and register them.
Adpter and codec use classes from the TTCN3 namespace
that are located in the dll ttcn3.dll.
This must be referenced when compiling adpter and codec.
Assume that the variable %TTCN3% contains the path of this dll
(the dll is located in the bin directory of the distribution,
its path can be obtained by the command ttxp /ref).
Then our example adpter and codec can be compiled with these commands:
csc /target:library Convert.cs
csc /target:library CoffeeMachine.cs /r:Convert.dll
csc /target:library CoffeeCodec.cs /r:Convert.dll,$TTCN3
csc /target:library StimulusAdapter.cs ResponseAdapter.cs \
/r:Convert.dll,CoffeeMachine.dll,$TTCN3
We also have to register adapter and codec.
This is done by the commands:
ttxp /adapter StimulusAdapter
ttxp /codec CoffeeCodec
This tells ttxp, that the adapter (resp. codec)
is implemented by a class StimulusAdapter
(resp. CoffeeCodec),
located in a dll with the same name.
Finally we can run our example:
ttxp /run TestCoffeeMachine TwoCoffeesPlease
You may download the files:
TestCoffeeMachine.ttcn3
CoffeeMachine.cs
StimulusAdapter.cs
ResponseAdapter.cs
CoffeeCodec.cs
Convert.cs