TTCN-3 TUTORIAL
          

TTCN-3 TUTORIAL

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 analyze the responses.

As a simple case study of a reactive system we will manufacture and test a coffee machine.

Overview

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 API that defines 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(5.0);
      alt {
         [] InputPort.receive(charstring:"coffee") {
            Count := Count+1;
            repeat;
         }
         [] t.timeout {
         }
      }
      log("Received " & int2str(Count) & " cup of coffee.");
      if (Count == 2) {
         setverdict(pass);
      }
      else {
         setverdict(fail);
      }   
   }
The component emits 100 cents (the price of two coffees) by
OutputPort.send(100);
It then waits 5.0 seconds for coffee. This is done by declaring and starting a timer that runs for 1.0 seconds:
timer t;
t.start(5.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 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:

   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(6.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 6.0 seconds and shut it down:

timer t; t.start(6.0); t.timeout;
CoffeeMachine.stop;
Here is the complete module.
module CoffeeSuite
{
   // 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(5.0);
      alt {
         [] InputPort.receive(charstring:"coffee") {
            Count := Count+1;
            repeat;
         }
         [] t.timeout {
         }
      }
      log("Received " & int2str(Count) & " cup of coffee.");
      if (Count == 2) {
         setverdict(pass);
      }
      else {
         setverdict(fail);
      }   
   }

   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(6.0); t.timeout;
      CoffeeMachine.stop;

   }

   control {
      execute( TwoCoffeesPlease() );
   }
}
The connection of ports can be depicted as follows:

+-------------------------------------------+
|                                           |
| +----------+                  +---------+ |
| |          |                  |         | |
| |        OutputPort --> InputPort       | |
| |Coffee    |                  |   Coffee| |
| |Drinker   |                  |  Machine| |
| |        InputPort <-- OutputPort       | |
| |          |                  |         | |
| +----------+                  +---------+ |
|                                           |
+-------------------------------------------+

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;

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.

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;
   }

}

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 CoffeeSuite
{

   // 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(5.0);
      alt {
         [] InputPort.receive(charstring:"coffee") {
            Count := Count+1;
            repeat;
         }
         [] t.timeout {
         }
      }
      log("Received " & int2str(Count) & " cup of coffee.");
      if (Count == 2) {
         setverdict(pass);
      }
      else {
         setverdict(fail);
      }   
   }

   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);
   }

   control {
      execute( TwoCoffeesPlease() );
   }
}
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 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 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 Etsi.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 the the value TriStatus.TRI_OK.

Here is our definition of triMap:

   public override TriStatus triMap (
      TriPortId compPortId,
      TriPortId tsiPortId
   )
   {
      if (compPortId.getPortName() == "InputPort") { 
         CoffeeMachine.SwitchOn();
         ResponseAdapter.SwitchOn(tsiPortId, compPortId.getComponent());
      }

      return TriStatus.TRI_OK;
   }
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 TriStatus triUnmap (
      TriPortId compPortId,
      TriPortId tsiPortId
   )
   {
      if (compPortId.getPortName() == "InputPort") { 
         CoffeeMachine.SwitchOff();
         ResponseAdapter.SwitchOff();
      }
      return TriStatus.TRI_OK;
   }
The execution of a statement
Port.send(Value)
triggers a call
triSend(componentId, tsiPortId, address, sendMessage)
componentId (of type TriComponentId) identifies the sending component; tsiPortId (of type TriPortId) identifies the test system port to which Port has been mapped; address (of type 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 TriStatus triSend (
      TriComponentId componentId, // sending test component
      TriPortId      tsiPortId,   // port via which the msg is sent
      TriAddress     address,     // optional destination address
      TriMessage     sendMessage  // encoded msg to be sent
   )
   {
      byte[] bytes = sendMessage.getEncodedMessage();
      CoffeeMachine.Input.Enqueue(bytes);
      return TriStatus.TRI_OK;
   }
Here is the complete stimulus adapter (file StimulusAdapter.cs):
using Etsi.Ttcn3;

public class StimulusAdapter : SystemAdapter
{

   public override TriStatus triMap (
      TriPortId compPortId,
      TriPortId tsiPortId
   )
   {
      if (compPortId.getPortName() == "InputPort") { 
         CoffeeMachine.SwitchOn();
         ResponseAdapter.SwitchOn(tsiPortId, compPortId.getComponent());
      }

      return TriStatus.TRI_OK;
   }

   public override TriStatus triUnmap (
      TriPortId compPortId,
      TriPortId tsiPortId
   )
   {
      if (compPortId.getPortName() == "InputPort") { 
         CoffeeMachine.SwitchOff();
         ResponseAdapter.SwitchOff();
      }
      return TriStatus.TRI_OK;
   }

   public override TriStatus triSend (
      TriComponentId componentId, // sending test component
      TriPortId      tsiPortId,   // port via which the msg is sent
      TriAddress     address,     // optional destination address
      TriMessage     sendMessage  // encoded msg to be sent
   )
   {
      byte[] bytes = sendMessage.getEncodedMessage();
      CoffeeMachine.Input.Enqueue(bytes);
      return TriStatus.TRI_OK;
   }
}

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 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

Port.receive(Template)
it can accept a message that has been passed to it by a call
ToTestSystem.triEnqueueMsg
   (tsiPortId, SUTaddress, componentId, receivedMessage)
where ToTestSystem is an object of the class implementing TriCommununication; it can be obtained by Framework.GetTriCommunicationTE(). Here tsiPortId (of type TriPortId) identifies the test system port to that Port has been mapped. SUTaddress (of type 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 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
         TriMessage msg = Framework.GetFactory().TriMessage();
         msg.setEncodedMessage(bytes);
         MyTTS.triEnqueueMsg (PortId, null, ComponentId, msg);
Note that the type TriMessage id defined by the TRI as an interface, not a class. An object of a class implementing this interface is obtained by a factory method of the same name:
Framework.GetFactory().TriMessage()
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;
using Etsi.Ttcn3;

public class ResponseAdapter {
   
   static TriCommunicationTE MyTTS =
      Framework.GetTriCommunicationTE();
   static Thread Receiver;

   static TriPortId PortId;
   static TriComponentId ComponentId;

   public static void SwitchOn(TriPortId pid, 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();
         TriMessage msg = Framework.GetFactory().TriMessage();
         msg.setEncodedMessage(bytes);
         MyTTS.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 implements the TCI interface TciCDProvided (for example by deriving from class Codec) and defines the methods encode and decode:

public virtual TriMessage encode (
   Value value
)
and
public virtual Value decode (
   TriMessage message,
   Type decodingHypothesis
)
The method encode is invoked (e.g. when a send statement is executed) with a parameter value of type Value. This must be converted into a value of type TriMessage. This value is then passed to a method such as triSend.

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.

   public override TriMessage encode(Value value)
   {
      int i;

      Type type = value.getType();
      int typeclass = type.getTypeClass();

      if (typeclass == (int) TciTypeClass.INTEGER) {
         i = ((IntegerValue)value).getInteger();
         byte[] bytes = Convert.IntToByteArray(i);
         TriMessage msg =
            Framework.GetFactory().TriMessage();
         msg.setEncodedMessage(bytes);
         return msg;
      }
      else {
         // should not be reached, signal error
         Framework.GetTciCDRequired().tciErrorReq
            ("unexpected typeclass");
         return null;
      }
   }
The method decode is called when incoming data (passed to the test system e.g. by 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 TriMessage, this type is passed as a parameter decodingHypothesis (of type 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 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.

   public override Value decode
      (TriMessage message, Type decodingHypothesis)
   {
      int typeclass = decodingHypothesis.getTypeClass();
      if (typeclass == (int) TciTypeClass.CHARSTRING) {

         byte[] bytes = message.getEncodedMessage();
         string str = Convert.ByteArrayToString(bytes);

         CharstringValue val =
            Framework.GetFactory().CharstringValue();
         val.setString(str);
         return val;
      }
      else {
         // should not be reached, signal error
         Framework.GetTciCDRequired().tciErrorReq
            ("unexpected typeclass");
         return null;
      }
   }
Here is the complete codec (file CoffeeCodec.cs):
using Etsi.Ttcn3;

public class CoffeeCodec : Codec {

   public override TriMessage encode(Value value)
   {
      int i;

      Type type = value.getType();
      int typeclass = type.getTypeClass();

      if (typeclass == (int) TciTypeClass.INTEGER) {
         i = ((IntegerValue)value).getInteger();
         byte[] bytes = Convert.IntToByteArray(i);
         TriMessage msg =
            Framework.GetFactory().TriMessage();
         msg.setEncodedMessage(bytes);
         return msg;
      }
      else {
         // should not be reached, signal error
         Framework.GetTciCDRequired().tciErrorReq
            ("unexpected typeclass");
         return null;
      }
   }

   public override Value decode
      (TriMessage message, Type decodingHypothesis)
   {
      int typeclass = decodingHypothesis.getTypeClass();
      if (typeclass == (int) TciTypeClass.CHARSTRING) {

         byte[] bytes = message.getEncodedMessage();
         string str = Convert.ByteArrayToString(bytes);

         CharstringValue val =
            Framework.GetFactory().CharstringValue();
         val.setString(str);
         return val;
      }
      else {
         // should not be reached, signal error
         Framework.GetTciCDRequired().tciErrorReq
            ("unexpected typeclass");
         return null;
      }
   }
}

Compiling and Running the Example

To 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:

csc
   /out:CoffeeSupport.dll
   /target:library
   /reference:C:\ESPRESSO\ttcn-3\etsi.dll
   StimulusAdapter.cs ResponseAdapter.cs CoffeeCodec.cs
   CoffeeMachine.cs Convert.cs
This creates the library CoffeeSupport.dll which must be present when running the module.

To run the example then type:

C:\ESPRESSO\ttcn-3\ttxp /run CoffeeSuite


The TTCN-3 Release for Microsoft .Net that was published in 2005 by Fraunhofer FIRST. It is no longer maintained.