OpenTTCN/Developer corner/Creating adapter with C SDK

From OpenTTCN
Jump to: navigation, search

  OpenTTCN DocZone

  Home | Developer's corner | Knowledge base | Working documents | Documentation | OpenTTCN IDE | Tutorials | Training | How do I | Frequently asked questions | Technical support


Last modified March 28, 2012

Creating adapter using ANSI C



In this article we discuss creation of the most simple TTCN-3 compliant adapter using standard ETSI TTCN-3 TRI and TCI-CD interfaces and OpenTTCN SDK for C.

A bit of terminology

TTCN-3 adapter is a piece of software implemented using standard TTCN-3 API interfaces. It is typically located in between TTCN-3 execution (TE) that runs TTCN-3 code and the System Under Test (SUT). TTCN-3 adapter is responsible for two major tasks:

  • encoding and decoding abstract TTCN-3 data to and from concrete protocol frames (this is what the TCI-CD part does)
  • sending and receiving concrete protocol frames to and from the SUT over communication medium (this is what the TRI part does). Frames received from the SUT are decoded and forwarded to the TE.

TRI stands for TTCN-3 Runtime Interface and is a standard ETSI TTCN-3 API for defining adapter network interface code and interfacing this code with TE. The TRI API is defined in ETSI ES 201 873-5: Methods for Testing and Specification (MTS); The Testing and Test Control Notation version 3; Part 5: TTCN-3 Runtime Interface (TRI).

TCI stands for TTCN-3 Control Interface. TCI data and TCI-CD are standard ETSI TTCN-3 interfaces for introspecting and constructing TTCN-3 abstract values and for providing encoding and decoding of these abstract values. They are defined in ETSI ES 201 873-6: Methods for Testing and Specification (MTS); The Testing and Test Control Notation version 3; Part 6: TTCN-3 Control Interface (TCI).


Contents


What do we need

To be able to develop and compile our adapter example in C, we need the following:

This tutorial focuses on adapter development for Windows. Under Linux you need a decent C++ compiler such as gcc. For inquiries regarding availability of OpenTTCN SDK for ANSI C for other platforms please contact us at support@openttcn.fi.

Starting from version 4.1.2, OpenTTCN Tester 2011 ships with OpenTTCN SDK for C for both Microsoft Visual Studio 2008 and Microsoft Visual Studio 2010. Configuring Microsoft Visual Studio 2010 is similar to configuring Microsoft Visual Studio 2008. Library path for Microsoft Visual Studio 2010 is sdk4c\lib\vs2010 and for Microsoft Visual Studio 2008 the library path is sdk4c\lib\vs2008.

Creating Visual Studio 2008 project

(thumbnail)
Creating new solution in Visual Studio 2008

While there are examples bundled with OpenTTCN SDK for ANSI C that can be used as a starting point, let's assume a clean plate and create a Visual Studio 2008 project from scratch. This will show us what project options we need to set to make it work.

In the following we assume that you have installed OpenTTCN SDK for ANSI C to the following directory (64 bit version of Windows):

C:\Program Files (x86)\OpenTTCN\Tester2011\sdk4c

For 32 bit version of Windows the installation path can be:

C:\Program Files\OpenTTCN\Tester2011\sdk4c

Your actual project settings may slightly differ from the settings presented in this tutorial if your SDK installation directory is different.

Create a Win32 Console Application through File|New|Project. We recommend that you check the box for "Empty project" so that Visual Studio does not add its own files. Let's call our new solution SimpleAdapter.

Now assuming we have created SimpleAdapter solution, Visual Studio has created the SimpleAdapter folder and has put all its files there.

Create the src subfolder in the SimpleAdapter folder where we will put all our adapter source code. Now we have a new folder: SimpleAdapter\src.

Adding main.c to the project

Create a blank main.c file in the src folder and add it to the SimpleAdapter project using Add|Existing item menu item of the pop-up window.

Add the following initial code to main.c:

/* for Sleep() call */
#include <windows.h>

#include <tri/tri.h>
#include <isl/TTCN3.h>

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int status = otInitAdapter(0);

    if (status)
    {
        printf("main(): otInitAdapter() failed.\n");
        exit(status);
    }

    /*
     Backwards-compatibility mode: set TRI callbacks to their hard-coded values
     and keep the existing code the way it is. This is required starting with
     OpenTTCN Tester 4.1beta5.3.
     */
    otSetDefaultCallbacks();

    /* Register the adapter to OpenTTCN server */

    status = otRegisterAdapter("simple", "TRI");

    if (status)
    {
        printf("main(): otRegisterAdapter() failed.\n");
        exit(status);
    }

    /* Enable printout of library diagnostic information */
    otSetSAFlags(OT_SA_FLAG_VERBOSE);

    printf("Init OK!\n");

    otAdapterStartupComplete("simple"); /* new in 4.2.0 */

    /* Enter the infinite loop */
    while (1) Sleep(100000);

    /* Avoid compiler complaints */
    return 0;
}

We call otInitAdapter() and otRegisterAdapter() in the code snippet above. The first one establishes a communication path between the adapter which is a dedicated process in its own right, and OpenTTCN server containing TTCN-3 runtime environment. The second one registers this adapter into a particular session so that OpenTTCN runtime knows where to find the adapter when necessary.

Both functions are OpenTTCN specific. Proprietary extensions are needed only during adapter setup phase, because adapter setup is outside of the scope of the TTCN-3 standard. The main part of the adapter code is standard compliant.

Customizing project settings

Now if you push F7 in the main.c window, the code above will not compile. We need to adjust project settings first to make it work.

Switch to building a Release version of the binary:

  • Build|Configuration Manager|Active solution configuration: Release, then Close. In this way, we will configure everything for Release build only. In practice you may need to configure both Debug and Release.

Right-click on the SimpleAdapter project name in the left tab, then select Properties from the pop-up window. Make sure that you are editing the Release configuration.

  • Add this to Configuration Properties|C/C++|General|Additional Include Directories (64-bit version of Windows):
C:\Program Files (x86)\OpenTTCN\Tester2011\sdk4c\include

Example of include directory for 32-bit version of Windows:

C:\Program Files\OpenTTCN\Tester2011\sdk4c\include
  • Select Multi-threaded (/MT) in Configuration Properties|C/C++|Code Generation|Runtime Library. It is important that you do so, because libraries may not work correctly for the /MD build.
  • Add this to Configuration Properties|Linker|General|Additional Library Directories (64-bit version of Windows):
C:\Program Files (x86)\OpenTTCN\Tester2011\sdk4c\lib\vs2008

Example of lib directory for 32-bit version of Windows:

C:\Program Files\OpenTTCN\Tester2011\sdk4c\lib\vs2008
  • Add this to Configuration Properties|Linker|Input|Additional Dependencies:
omniDynamic4.lib omniORB4.lib omnithread.lib pthreads-2.7.0-mt.lib openttcn-sdk-mt.lib ws2_32.lib

Finally, click Apply, then OK. Now try compiling the code. Got linker errors? Move on and read the next section.



IMPORTANT! Note for Linux users: When using GCC/Linux, libraries must be specified to the linker in certain order to avoid linker errors. Here is an example of the linker command:

g++ SA_impl.o CD_impl.o main.o -o adapter –lOpenTTCNsdk –lomniDynamic4 –lomniORB4 –lomnithread -lpthread -lm -ldl -static -L/usr/local/OpenTTCN/sdk4c/lib

Under GCC/Linux, you need to specify libraries to the linker in the same order as described in the example above to avoid linker errors.



Adding default callback handlers

After we tried to compile our main.c, linker has complained about some unresolved external symbols. These are the callback handlers that are invoked by the SDK library in the background when TE wants to communicate something to the adapter. In this case the adapter needs to handle the TE request and do something for it. These callback handlers shall be implemented by the end user, meaning us.

For now, let's add some dummy default implementation of these callbacks. For this we will need to add two more source files to the project, SA_impl.c and PA_impl.c. SA_impl.c will contain implementation of the SUT adapter and PA_impl.c will contain implementation of platform adapter, meaning in practice external functions.

Content of SA_impl.c:

#include <tri/tri.h>

/***************************************************************************
 * Implementation of TRI interface (SA, user-provided part).
 */

TriStatus triSend
(const TriComponentId* componentId,
 const TriPortId* tsiPortId,
 const TriAddress* sutAddress,
 const TriMessage* sendMessage)
{
    return TRI_ERROR;
}

TriStatus triSAReset()
{
    return TRI_OK;
}

TriStatus triExecuteTestCase
(const TriTestCaseId* testCaseId,
 const TriPortIdList* tsiPortList)
{
    return TRI_OK;
}

TriStatus triMap
(const TriPortId* compPortId,
 const TriPortId* tsiPortId)
{
    return TRI_OK;
}

TriStatus triUnmap
(const TriPortId* compPortId,
 const TriPortId* tsiPortId)
{
    return TRI_OK;
}

TriStatus triCall
(const TriComponentId* componentId,
 const TriPortId* tsiPortId,
 const TriAddress* sutAddress,
 const TriSignatureId* signatureId,
 const TriParameterList* parameterList)
{
    return TRI_ERROR;
}

TriStatus triReply
(const TriComponentId* componentId,
 const TriPortId* tsiPortId,
 const TriAddress* sutAddress,
 const TriSignatureId* signatureId,
 const TriParameterList* parameterList,
 const TriParameter* returnValue)
{
    return TRI_ERROR;
}

TriStatus triRaise
(const TriComponentId* componentId,
 const TriPortId* tsiPortId,
 const TriAddress* sutAddress,
 const TriSignatureId* signatureId,
 const TriException* exception)
{
    return TRI_ERROR;
}

TriStatus triSUTActionInformal
(const char* description)
{
    return TRI_ERROR;
}

TriStatus triSUTActionTemplate
(const TriActionTemplate* templateValue)
{
    return TRI_ERROR;
}

Content of PA_impl.c:

#include <tri/tri.h>

/***************************************************************************
 * Implementation of TRI interface (PA, user-provided part).
 */

TriStatus triPAReset()
{
    return TRI_OK;
}

TriStatus triExternalFunction
(const TriFunctionId* functionId, /* in parameter */
 TriParameterList* parameterList, /* inout parameter */
 TriParameter* returnValue /* out parameter */)
{
    return TRI_ERROR;
}

TriStatus triStartTimer
(const TriTimerId* timerId,
 TriTimerDuration timerDuration)
{
    return TRI_ERROR;
}

TriStatus triStopTimer
(const TriTimerId* timerId)
{
    return TRI_ERROR;
}

TriStatus triReadTimer
(const TriTimerId* timerId,
 TriTimerDuration* elapsedTime)
{
    return TRI_ERROR;
}

TriStatus triTimerRunning
(const TriTimerId* timerId,
 unsigned char* running)
{
    return TRI_ERROR;
}

The amount of default callback handlers may look overhelming at first sight. Fortunately, we will have to implement only a few of them to have a working adapter up and running, most notably, this one:

Surprisingly, this is enough to get started even with most of the real world test suites that you are going to deal with. In addition, we will need to add CD_impl.c source file to our project and add the following encoding and decoding handlers there:

Let's add CD_impl.c to the project right away. While this did not cause "unresolved external symbol" linker errors, let's do this for the sake of completeness. Here is the initial content of CD_impl.c:

#include <tci/tci.h>

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <string.h>

/***************************************************************************
 * Implementation of TCI interface (CD, user-provided encoding part).
 */

BinaryString tciEncode(TciValue value)
{
    BinaryString result;
    memset(&result, 0, sizeof(result));
    return result;
}

/***************************************************************************
 * Implementation of TCI interface (CD, user-provided decoding part).
 */

TciValue tciDecode(BinaryString message, TciType decHypothesis)
{
    TciValue result = 0;
    return result;
}

Now when we added empty encoding and decoding handlers, let's register them in the SDK library in the main() function. Add the following section of code to main.c:

otSetEncoder(0, &tciEncode);
otSetDecoder(0, &tciDecode);

before this line:

/* Register the adapter to OpenTTCN server */

Do not forget to add #include <tci/tci.h> to the main.c file.

The two functions, otSetEncoder() and otSetDecoder(), are used to initialize coding and decoding subsystem and to register coding and decoding handlers in the framework. As you might have noticed, in our most simple adapter configuration coding and decoding is collocated with SUT adapter in the same process, although this does not necessarily have to be so, as more complex configurations involving multiple processes are possible, even distributed across multiple hosts.

Preparing to run the adapter for the first time

Now we got a legitimate adapter that happens to do nothing useful. The code should compile, but before running the adapter binary we still need to do a couple of things:

  • Start OpenTTCN server, if you did not do this already. From the command line, this is done this way:
ot start

OpenTTCN server must be running at all times, e.g. when we start an adapter, compile a test suite, run a test case etc.

  • Create session called simple. Remember we are registering our adapter to the session with this name in main.c using otRegisterAdapter(). From the command line, session can be created this way:
session create simple
(thumbnail)
Windows Security Alert popup

Running the adapter for the first time

Now we can run our adapter binary. From Visual Studio, you can launch the adapter using Ctrl+F5. If Windows pops up a security alert, click "Unblock". Adapter tries to set up a network connection with the local OpenTTCN server installation, hence the alert.

If everything goes fine, you should see a terminal window with the following text message: "Init OK!" Now you can type session status simple from the command line. You should be able to see the following fragment of output:


Adapters
-----------------------------------------------------------------------------
Name                            Status          Description

TRI                             ALIVE           TRI SUT and platform adapter

This means that we have our adapter successfully registered to the simple session with TRI role and the adapter is reachable from within the session (has alive status).

Now we can move forward and add real content to our dummy adapter implementation. To do this, we need to agree on what kind of protocol and SUT we are testing.

SUT definition

To simplify our example, we will be testing a simple UDP-based protocol with a few defined frames, simple behaviour, and simple network interface. Let us call this protocol R2-D2.

Here is how the protocol is defined:

R2-D2 Protocol
==============

Preamble
--------

Peer-to-peer communication is UDP based. Two peer roles are defined: client
and server. After initiating a request client shall wait for a response from
the server.

Protocol frames are encapsulated in the payload part of UDP frames. There
are no provisions for frame fragmentation, reassembly and retransmissions.

Frame structure
---------------

All peer-to-peer exchanges between the client and the server shall be in
frames of two possible format types:

- format A for messages with type code field only

- format B for messages with type code, length, and content

Format A:

Octet                  Frame
       +----------------------------------+
  1    |         Message type code        |
       +----------------------------------+

Format B:

Octet                  Frame
       +----------------------------------+
  1    |         Message type code        |
       +----------------------------------+
  2    |  Length (most significant byte)  |
       +----------------------------------+
  3    |  Length (least significant byte) |
       +----------------------------------+
  4    |         Message content          |
       |                                  |
  N    |                                  |
       +----------------------------------+

Length field occupies 2 octets in network byte order.

Message content is a non-null-terminated ASCII-coded character
string (8-bit encoding applies). Character codes above 127 shall
not be used.

Note that in Format B message content may occupy 0 octets if the
length field is zero.

Currently defined type codes and their meaning is explained below
(type codes are in hexadecimal format):

+-------+-------------------+-------------------------------------------------+
| Code  |  Allowed formats  |   Message semantics                             |
+-------+-------------------+-------------------------------------------------+
|       |                   |                                                 |
| 0x01  |         B         |   Greeting request                              |
|       |                   |                                                 |
| 0x02  |         B         |   Greeting response                             |
|       |                   |                                                 |
| 0xFF  |         A         |   Response to unrecognized or malformed request |
|       |                   |                                                 |
+-------+-------------------+-------------------------------------------------+

Protocol definition
-------------------

Upon receipt of a request with message content "Hello, world!", server shall respond with a response having message content "Hello, mankind!". Upon receipt of any other valid request, server shall respond with a response having message content "Reformulate". Upon receipt of any other valid, unrecognized or malformed frame, server shall respond with type A frame bearing response to unrecognized or malformed request (type code 0xFF).

In our test configuration we will be testing a server, meaning that SUT has server role and test harness simulates a client.

TTCN-3 message type definitions

This time it is our responsibility to define abstract messages using TTCN-3 core language that are capable of representing frames of R2-D2 protocol in abstract form. This is done by taking "Frame structure" section of the protocol definition from the previous section as a base. Usually this is done by a test suite writer, so you only have to find correspondence between abstract message definitions of a test suite and concrete frame formats and then implement encoding and decoding based on found correspondence. Usually test suite writers are reasonable and you can find such correspondence rather easily. With ASN.1-based protocols, encoding and decoding may come automatically from frame type definitions in ASN.1, so in this case you have to write very few if any encoding and decoding code by yourself.

Create C:\wikiex\example01\TestSuite folder (remember we have C:\wikiex\example01\SimpleAdapter already) and add to this folder Main.ttcn file with the following content:

module Main
{

/*******************************************************************
 * Message type definitions
 */

type integer uint8(0..255);
type integer uint16(0..65535);

type record Message
{
    uint8 typeCode,
    Payload payload optional
}

type record Payload
{
    uint16 len,
    charstring content
}

type octetstring Raw;

}

Type Message is used to represent requests and responses in both format A and format B.

Type Raw is introduced to simplify construction of invalid frames and to provide sensible means for an adapter to communicate to test execution those frames that it is unable to decode.

Adding example test case

Before writing more code for the adapter, it would be convenient to have an idea about how and in which setup it is going to be used. For this, we need to write the most simple test case and then work on test harness realization based on the specification provided by the test case. If you are writing an adapter that is based on the existing test suite, you already have all test cases at your disposal. If you are writing an adapter based on some work in progress, you need to have at least a complete set of message type definitions to get started with coding and decoding, and then you implement a simple tryout test case by yourself to get a proof of concept. Such proof-of-concept tryout test case we are going to add right now. Here are the definitions that you need to add to Main.ttcn:

/*******************************************************************
 * Other definitions
 */

// Address type used for UDP packets.
type record address
{
    charstring host,
    integer portField
}

modulepar
{
    // IP address and UDP port number of the SUT.
    charstring PX_SUT_IP_ADDR := "127.0.0.1";
    integer PX_SUT_PORT := 7431;
}

type port PortType mixed
{
    inout all;
}

type component ComponentType
{
    port PortType p;
    timer T_GUARD := 10.0;

    var address sut_addr :=
    {
        host := PX_SUT_IP_ADDR,
        portField := PX_SUT_PORT
    };
}

type component SystemInterfaceType
{
    port PortType tsiPort;
}

template Message GreetingRequest
(in template charstring phrase) :=
{
    typeCode := 1,
    payload :=
    {
        len := lengthof(phrase),
        content := phrase
    }
}

template Message GreetingResponse
(in template charstring phrase)
modifies GreetingRequest :=
{
    typeCode := 2
}

altstep DefaultAltstep() runs on ComponentType
{
    [] any port.check
    {
        setverdict(fail);
        stop;
    }
    [] T_GUARD.timeout
    {
        setverdict(fail);
        stop;
    }
}

testcase TC_R2D2_001()
    runs on ComponentType
    system SystemInterfaceType
{
    activate(DefaultAltstep());
    T_GUARD.start;
    map(mtc:p, system:tsiPort);

    p.send(GreetingRequest("Hello, world!")) to sut_addr;
    p.receive(GreetingResponse("Hello, mankind!")) from sut_addr;

    p.send(GreetingRequest("Hello, gentlemen!")) to sut_addr;
    p.receive(GreetingResponse("Reformulate")) from sut_addr;

    setverdict(pass);
}

Building test suite

Add compile.bat file to the same folder where Main.ttcn resides, with the following content:

importer3 load simple Main.ttcn

If you have Cygwin or Linux environment, you may as well consider creating a Makefile for building the whole test suite or its modified portions using the importer3 -t option. This can be particularly handy if you have a large complex test suite with multiple modules that needs frequent rebuilds. Here is an example of Makefile for this particular project:

.PHONY: all clean

OBJS = \
Main.tsf

SESSION=session
IMPORTER3=importer3

all: $(OBJS)

clean:
	rm -f *.tsf
	$(SESSION) delete simple

.SUFFIXES:
.SUFFIXES:	.ttcn .tsf

.ttcn.tsf:
	$(IMPORTER3) load -t simple $<

Run compile.bat. It should compile Main.ttcn and add its precompiled version to the simple session without complaints. You can run session status simple command from the command line to make sure that the Main module was indeed added to the session.

Running test case (first try)

Now launch the adapter binary if it is not running, either from Visual Studio using Ctrl+F5, or by some other means, and run the test case that we have just added by typing this from the command line:

tester run simple TC_R2D2_001

The tester command shall complain with this kind of error message:

mtc : {13:18:24.948} : // VERDICT TC_R2D2_001 TEST CASE ERROR: E4205: triSend() return status is not TRI_OK.

Indeed, we return TRI_ERROR from triSend() to indicate that we did not implement TTCN-3 send request.

We also did not implement proper encoding of the message content and address field. This will be done later in this article.

Creating encoder

Currently, functionality for encoding message content and address field is not implemented in our version of tciEncode(). Before we go for implementing triSend(), we need to implement the encoding part first, because triSend() receives message and address parameters in already encoded form.

As you can see, the TTCN-3 standard puts you in quite tight framework. Your triSend() handler always receives important parameters in encoded form, even if the business logic of your adapter implementation makes this undesirable. Such inconvenience may occur more often than it seems at first sight. For example, if you rely on a third-party library for communicating with the SUT, it may be more convenient to have the message in abstract form during triSend() call, and then extract abstract fields from the message container and supply them to the third-party library API than doing all frame encoding by yourself.

With encoding of the address field it is the same. We do not really need address in encoded form, because it is not an integral part of the payload of our protocol frame. We would prefer having it in abstract form in triSend(), because we are going to use socket API for sending UDP frames, and it accepts destination IP address and UDP port as parameters.

Different tools find different workarounds for this awkward peculiarity of the TTCN-3 standard. Some may even go as far as introducing new non-standard signatures of callback functions that pass value parameters in unencoded form. While such approach clearly simplifies end user life, it also raises concerns about portability of adapter implementation.

OpenTTCN decided to keep the standard set of callback interfaces as it is and introduce a workaround at a different, less noticeable level. This workaround does not even conflict with any of the current standards, so it can be considered as a programming practice rather than extension of the standard.

The idea behind such workaround is to return a TciValue pointer (handle) encapsulated into BinaryString instead of performing diligent encoding of such value and returning result of encoding. Here is a code snippet how to do this:

BinaryString tciEncode(TciValue value)
{
    int len;

    BinaryString result;
    memset(&result, 0, sizeof(result));

    len = sizeof(TciValue);
    result.data = (unsigned char *) malloc(len);
    memcpy(result.data, &value, len);

    result.bits = len << 3;

    return result;
}

Now the code inside triSend() handler can recover original TciValue handle quite easily:

TciValue addrValue = *((TciValue *) sutAddress->data);

And it can now process abstract TTCN-3 value in unencoded form.

So for our encoder we make two decisions:

  • pass address field in unencoded form to triSend() using the programming trick described above
  • encode message content as normally and pass encoded frame to triSend()

TTCN-3 TciValue introspection and construction API is a proper topic for a separate article, so for now we simply present the C code of the encoder and leave it to the reader as an exercise to figure out what it does.

Here is the code that needs to replace the earlier version of tciEncode() in CD_impl.c:

/* for otReportError() */
#include <isl/TTCN3.h>

#define MAX_BUFFER_SIZE 4096

BinaryString tciEncode(TciValue value)
{
    int len = 0;
    BinaryString result;

    TciType valType = tciGetType(value);
    String typeName = tciGetName(valType);
    TciTypeClassType typeClass = tciGetTypeClass(valType);

    memset(&result, 0, sizeof(result));

    if (typeClass == TCI_ADDRESS_TYPE)
    {
        len = sizeof(TciValue);
        result.data = (unsigned char *) malloc(len);
        memcpy(result.data, &value, len);
        result.bits = len << 3;
    }
    else if (!strcmp(typeName, "Message"))
    {
        long typeCode = 0, length = 0;
        unsigned char* buffer = 0;

        TciValue payload = 0;

        char* str = 0;
        long str_len = 0;

        buffer = (unsigned char *) malloc(MAX_BUFFER_SIZE);
        memset(buffer, 0, MAX_BUFFER_SIZE);

        len = 0;

        typeCode = tciValueToLong(tciGetRecFieldValue(value, "typeCode"));

        buffer[len++] = (unsigned char) typeCode;

        payload = tciGetRecFieldValue(value, "payload");

        /* 'payload' field is optional, so it may be omitted */
        if (!tciNotPresent(payload))
        {
            long length = tciValueToLong(tciGetRecFieldValue(payload, "len"));

            /* most significant byte first */
            buffer[len++] = (unsigned char) (length >> 8);
            buffer[len++] = (unsigned char) (length >> 0);

            tciValueToCharstring(&str, &str_len,
                tciGetRecFieldValue(payload, "content"));

            memcpy(buffer + len, str, str_len);
            len += str_len;

            free(str);
        }

        result.data = buffer;
        result.bits = len << 3;
    }
    else
    {
        otReportError("tciEncode(): Unrecognized value type.");
    }

    return result;
}

To use this code, you will need to #include Utilities.h to CD_impl.c and add files Utilities.h and Utilities.c to your Visual Studio project.

You will need to copy the code of Utilities.h and Utilities.c files referred to in the links of the previous paragraph into your Visual C++ project. Click on the links of the previous paragraph to see the content of both files. Note that these are not the same as in the examples\util directory shipped with the C SDK.

Sending frame to the SUT

Now everything is ready to add proper implementation of the triSend() handler to our code. It should basically extract IP address and UDP port of the SUT from the sutAddress parameter that effectively contains TciValue handle, and then send encoded data frame contained in the sendMessage parameter to the SUT using extracted address fields. Does not sound overhelming, isn't it?

Here is a new triSend() handler for SA_impl.c that does all this:

TriStatus triSend
(const TriComponentId* componentId,
 const TriPortId* tsiPortId,
 const TriAddress* sutAddress,
 const TriMessage* sendMessage)
{
    TciValue addrValue = *((TciValue *) sutAddress->data);

    unsigned long ipAddr = 0;
    unsigned short portNumber = 0;

    int result =
        extractHostAndPortFromAddress(
            &ipAddr, &portNumber, addrValue);

    if (result) return TRI_ERROR;

    result = sendDatagramPacket(
        ipAddr,
        portNumber,
        (char*) sendMessage->data,
        sendMessage->bits >> 3);

    return (!result) ? TRI_OK : TRI_ERROR;
}

Again, you will need to include Utilities.h to make it work. See the end of the previous section on details about how to add Utilities.h and Utilities.c files to your Visual C++ project. Complexities of the network code are hidden behind the interface of utility functions.

SUT implementation

Now we need the SUT itself implementing the R2-D2 protocol. SUT comes in a ready package, both in source and binary form, and it can be downloaded here. Note that you will need to switch to Release configuration if you would like to rebuild it from source.

We recommend that you unzip this package to C:\wikiex\example01, what should add a new folder SystemUnderTest next to SimpleAdapter and TestSuite.

As the SUT implementation has some dependencies on the C SDK code, this may give you false impression that you need to inject some test tool vendor specific code into your SUT to make it testable. This is not true. Your SUT code can be completely decoupled from the SDK library. The only reason why it is used in this example is to simplify SUT implementation for us and to reuse some useful interfaces like pthreads that are normally not available under Windows.

This same unzipped SUT package contains start_sut.bat file that launches the SUT binary. It contains the following statement:

start Release\SystemUnderTest.exe 7431

7431 is a command-line parameter of the SUT binary specifying the UDP port on which it will be listening for incoming client requests. Note that this UDP port number setting is consistent with the PX_SUT_PORT module parameter value of our example test suite.

Launch the SUT binary using start_sut.bat. If Windows pops up a Windows Security Alert, click Unblock. This will allow the SUT to use local network connections and create a UDP socket.

Running test case (second try)

Before running the test case we need to make sure that the following conditions are met:

  • SUT is up and running (use start_sut.bat to launch it)
  • adapter is up and running (use Ctrl+F5 in Visual Studio or launch the adapter binary directly or using some script file)

Now run the test case from the command line:

tester run simple TC_R2D2_001

Sending now succeeds, but the test case fails after expiry of a guard timer started with the duration of 10 seconds. This happens because we currently do not handle responses from the SUT server in the adapter, so the test execution never sees them.

If you switch to the SUT window, you will see that it indeed received a valid encoded frame from the test harness containing a request and it responded back with a valid response according to the R2-D2 protocol specification.

Here is what the SUT window shall contain:

--------------------------------------------------------------------------
RECEIVED REQUEST MESSAGE (UDP PACKET) FROM PEER

  Remote host (source):     127.0.0.1
  Remote UDP port (source): 54758
  Local UDP port (dest):    7431

<<<<<
01 00 0D 48 65 6C 6C 6F 2C 20 77 6F 72 6C 64 21
<<<<<
--------------------------------------------------------------------------
SENDING RESPONSE MESSAGE (UDP PACKET) TO PEER

  Local UDP port (source): 7431
  Remote host (dest):      127.0.0.1
  Remote UDP port (dest):  54758

>>>>>
02 00 0F 48 65 6C 6C 6F 2C 20 6D 61 6E 6B 69 6E 64 21
>>>>>

You may also use the -h option of the tester run command if you wish to see the results of frame encoding produced by tciEncode() in the test log in hexadecimal format.

Creating UDP port listener thread

Now when we have the sending channel working, we need to implement a subsystem listening for incoming frames from the SUT. This is typically done by launching a dedicated thread that uses recvfrom() standard socket API function call available both under Windows and Linux. This function blocks until a UDP frame arrives from the remote peer.

To launch a dedicated thread, we will be using pthreads. Decisions related to thread programming are important ones and once done, they are difficult to revert in the future. We will be using pthreads, because they bring us portable code that can run under multiple platforms. Under Linux, pthread.h is included into standard gcc compiler distribution and -lpthread linker option can be used to link with the pthreads library. Under Windows, OpenTTCN SDK comes with a ready pthread.h header and library taken from Pthreads-Win32 project that can be safely used for adapter programming.

Use of pthreads can be mixed with use of other threading APIs in the same binary.

Let us add new SocketListener.c file to the SimpleAdapter project with the following content:

#include "Utilities.h"

#include <tri/tri.h>
#include <tci/tci.h>

#include <isl/TTCN3.h>

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <assert.h>
#include <pthread.h>

#ifndef _WIN32
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#else
#include <winsock2.h>
typedef int socklen_t;
#endif

#define MAX_BUFFER_SIZE 65536

/* UDP port listener thread implementation */
void *runSocketListener(void *p_)
{
    int rval;
    unsigned char buff[MAX_BUFFER_SIZE];

    int len;
    int errorCode = 0;

    struct sockaddr_in src_addr;
    socklen_t addrlen = sizeof src_addr;

    char* host = 0;
    unsigned short port = 0;

    TciValue sutAddrHandle = 0;
    TciValue hostField = 0;
    TciValue portField = 0;

    TriAddress sutAddr;
    TriMessage recvMsg;
    TriPortId tsiPort;

    memset(&tsiPort, 0, sizeof(TriPortId));

    if (!_socketDescInitialized)
    {
        rval = bindSocketToAnyPort(&_socketDesc);

        if (rval)
        {
            otReportError("runSocketListener(): Cannot bind socket.");
            return 0;
        }

        _socketDescInitialized = 1;
    }

    /* Loop forever */
    while (1)
    {
        /* Receive a packet from the SUT */

        memset(&src_addr, 0, sizeof src_addr);

        rval = recvfrom(
            _socketDesc,
#ifndef _WIN32
            buff, 
#else
            (char *) buff, 
#endif
            sizeof buff, 
            0, 
            (struct sockaddr *) &src_addr, 
            &addrlen);

        /* We will proceed further only if the packet has been received OK */

        if (rval == -1)
        {
#ifndef _WIN32
            printf("runSocketListener(): "
                "recvfrom(): Error: %s\n", strerror(errno));
#else
            errorCode = WSAGetLastError();

            printf("runSocketListener(): "
                "recvfrom(): Error: error code = %i\n", errorCode);
#endif
            continue;
        }

        /* Put SUT address information directly into TciValue */

        host = inet_ntoa(src_addr.sin_addr);
        hostField = charstringToTciValue(host);

        port = ntohs(src_addr.sin_port);
        portField = longToTciValue(port);

        sutAddrHandle = tciNewInstance(tciGetTypeForName("address"));

        tciSetRecFieldValue(sutAddrHandle, "host", hostField);
        tciSetRecFieldValue(sutAddrHandle, "portField", portField);

        memset(&sutAddr, 0, sizeof(sutAddr));

        len = sizeof(TciValue);
        sutAddr.data = (unsigned char *) malloc(len);
        memcpy(sutAddr.data, &sutAddrHandle, len);
        sutAddr.bits = len << 3;

        /* Put raw encoded frame received from the SUT "as is" to recvMsg */

        recvMsg.data = buff;
        recvMsg.bits = rval << 3;

        buff[rval] = 0;

        tsiPort.portName = "tsiPort"; /* hard-coded */
        tsiPort.portIndex = -1;

        /* Append the message to the port queue on the test execution side */

        triEnqueueMsg(&tsiPort, &sutAddr, 0, &recvMsg);

        free(sutAddr.data);
    }

    /* Unreachable code, added to avoid compiler complaints */
    return 0;
}

As you might have noticed, we do the same trick with handling SUT address field as we did on the sending side of the adapter in tciEncode(). Instead of delegating to tciDecode() construction of decoded address given some pseudo-encoding, we construct the address abstract value already here and then put the resulting TciValue handle into BinaryString that is passed later to tciDecode(). We do not have to worry about memory allocation issues for the underlying TciValue, because it will we returned from tciDecode() later, so the framework will own the handle eventually and release its associated resources when they are no longer needed.

Name of the test system interface port to which we send the frame is hard-coded ("tsiPort"). It is taken from the SystemInterfaceType component type definition of our example test suite.

Another point worth noticing is that we pass 0 (NULL) as the third parameter for triEnqueueMsg(). This parameter identifies the test component that the message should be sent to. When NULL is passed as the component identifier, the message is delivered to the correct component based on TSI port mappings. Thus, just passing zero is typically the easiest solution. We could also copy the contents of a TriComponentId struct during triMap() callback and pass the value here. Note that currently our implementation only cares about the compInst field, meaning that compName and compType are ignored. The compInst field contains a dynamic unique identifier of the test component which is typically of form "ptcN.ComponentName".

This is not everything what we need to do for our UDP port listener thread. Not only we need to create its implementation, but we also need to create the thread and start it. Therefore we need to add the following block of code to our main.c before printf("Init OK!\n"); line:

/* Create UDP listener thread */

{
    pthread_t thread_ptr;

    extern void *runSocketListener(void *p_);

    if (pthread_create(&thread_ptr, 0, &runSocketListener, 0) != 0)
    {
        printf("main(): Unable to start a new thread.\n");
        exit(1);
    }
}

Do not forget to add #include <pthread.h> to the beginning of the main.c file.

Creating decoder

Now if you compile the adapter, start it and run the test case while the SUT is also running, you should see this kind of test case error in the test log:

adapter : {15:10:18.526} : // VERDICT TC_R2D2_001 TEST CASE ERROR: E4207: Adapter error: triEnqueueMsg(): No message content, message will not be sent.

This may sound like magic, but the idea is simple: SDK library complains that there is no decoded abstract TTCN-3 message to be enqueued into the port queue. Indeed, recall we have had only a dummy implementation of tciDecode() in CD_impl.c so far that always returns 0. Hence "no message content" error message.

This also means that our UDP port listener has received a response from the SUT and attempted to enqueue it into the TSI port message queue. This is good news to see it working.

Now we need to add the decoder for the received frame to finalize our adapter implementation. Here is a replacement for the dummy implementation of tciDecode(). Note how we handle values of address type:

TciValue tciDecode(BinaryString message, TciType decHypothesis)
{
    int len;
    TciValue result = 0;

    TciValue typeCode = 0;
    TciValue lenField = 0;
    TciValue contentField = 0;
    TciValue payload = 0;

    long length = 0;

    String typeName = "";

    if (decHypothesis)
    {
        typeName = tciGetName(decHypothesis);
    }

    if (!strcmp(typeName, "address"))
    {
        return *((TciValue *) message.data);
    }
    else if (!decHypothesis)
    {
        /* Assuming decoding of value of Message type */

        char* buffer = (char *) message.data;
        len = message.bits >> 3;

        if (!len)
        {
            otReportError("tciDecode(): Error: Message content is too short.");
            return 0;
        }

        if (len == 2)
        {
            otReportError("tciDecode(): Error: Message content is malformed.");
            return 0;
        }

        typeCode = longToTciValue((unsigned char) buffer[0]);

        result = tciNewInstance(tciGetTypeForName("Message"));

        tciSetRecFieldValue(result, "typeCode", typeCode);

        if (len > 2)
        {
            length =
                (((unsigned long) (unsigned char) buffer[1]) << 8) |
                (((unsigned long) (unsigned char) buffer[2]) << 0);

            lenField = tciNewInstance(tciGetTypeForName("uint16"));
            assignLongToTciValue(lenField, length);

            contentField = sizedCharstringToTciValue(buffer + 3, len - 3);

            payload = tciNewInstance(tciGetTypeForName("Payload"));

            tciSetRecFieldValue(payload, "len", lenField);
            tciSetRecFieldValue(payload, "content", contentField);

            tciSetRecFieldValue(result, "payload", payload);
        }
    }
    else
    {
        printf("tciDecode(): Error: Unrecognized type '%s'.\n", typeName);
    }

    return result;
}

Running test case (third try)

It works now! The test case passes, meaning that we have implemented everything correctly. Here is the output of the tester run command:

C:\wikiex\example01\TestSuite>tester run simple TC_R2D2_001

*****************************************************************************
*** RUNNING TEST CASE TC_R2D2_001

Tester : {16:20:32.484} : // Time: 16:20:32.484. Date: 26/Nov/2007. MOT version: TC: 2.57.0.RC2.0.
mtc : {16:20:32.526} : // CASE TC_R2D2_001 STARTED
mtc : {16:20:32.526} : T_GUARD.start(10.0); // Timer is started: duration 10 s.
mtc : {16:20:32.545} : map(mtc:p, system:tsiPort);
mtc : {16:20:32.580} : p.send(Message GreetingRequest := { typeCode := 1, payload := { len := 13, content := "Hello, world!" } }) to { host := "127.0.0.1", portField := 7431 };
mtc : {16:20:32.616} : p.receive(Message GreetingResponse := { typeCode := 2, payload := { len := 15, content := "Hello, mankind!" } }) from { host := "127.0.0.1", portField := 7431 };
mtc : {16:20:32.641} : p.send(Message GreetingRequest := { typeCode := 1, payload := { len := 17, content := "Hello, gentlemen!" } }) to { host := "127.0.0.1", portField := 7431 };
mtc : {16:20:32.686} : p.receive(Message GreetingResponse := { typeCode := 2, payload := { len := 11, content := "Reformulate" } }) from { host := "127.0.0.1", portField := 7431 };
mtc : {16:20:32.686} : setverdict(pass);
mtc : {16:20:32.691} : // CASE TC_R2D2_001 FINISHED
mtc : {16:20:32.691} : // VERDICT TC_R2D2_001 PASS

*****************************************************************************
*** TEST EXECUTION SUMMARY

Pass    Fail    Inconc    None    Error    Total    Duration
1       0       0         0       0        1        00:00:01

C:\wikiex\example01\TestSuite>

What next

There are many ways to improve the adapter code that we have developed up to this moment. Things to consider:

  • encoding and decoding of invalid frames using Raw data type definition;
  • adding support for multiple test system interface ports; currently we have the only one tsiPort port, what greatly simplified our design; having more TSI ports may mean more UDP port listeners, more threads, and more complexity;
  • introducing a printing mutex lock to synchronize on printf() statements; if there are multiple printf()s running in parallel, you will see garbage on your screen rather than meaningful output;
  • integrating adapter code with GUI test management software, if this is required by your project; TCI-TM interface might be of great help in implementing a test management GUI;
  • making the adapter code portable across multiple platforms and adding Makefiles for Linux;
  • disabling debug output and decoded value validation to improve overall system performance;
  • etc.


You can download the final version of the adapter, test suite and the SUT that we developed in this article, all in one package, by following this link.

NOTE: Users of OpenTTCN Tester 2011 and Visual Studio 2008 can download SimpleAdapter.sln and SimpleAdapter.vcproj here. These two files contain upgraded Visual Studio 2008 solution and project settings. They need to replace the existing .sln and .vcproj files in the downloadable package in the SimpleAdapter folder. OpenTTCN Tester 2011 installation path is assumed to be C:\Program Files (x86)\OpenTTCN\Tester2011 and if it is different, then SimpleAdapter.vcproj Visual Studio project file can be edited manually in a text editor with search and replace to set the correct installation path.
Personal tools