OpenTTCN/Developer corner/Creating adapter with OpenTTCN SDK for CPP
From OpenTTCN
Home | Developer's corner | Knowledge base | Working documents | Documentation | OpenTTCN IDE | Tutorials | Training | How do I | Frequently asked questions | Technical support |
Creating adapter using C++
In this article we create an SUT adapter and codec in C++ with OpenTTCN SDK for C++ which is a part of OpenTTCN Tester 2011. We use Microsoft Visual Studio 2008 to develop the adapter in question and we use OpenTTCN Tester 2011 IDE to run the example test suite.
Developed adapter encodes and decodes messages and sends and receives them between test execution (TE) and system under test (SUT). This basic functionality is usually enough for running real-world test suites if we take external functions out of consideration.
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 C++ for other platforms please contact us at support@openttcn.fi.
Creating Visual Studio 2008 project
SDK directories in OpenTTCN Tester 2011 come bundled with adapter examples that can be compiled and tried out of the box.
In this article we create a Visual Studio project and solution from scratch to see what needs to be set in the project properties to enable successful adapter compilation.
In the following we assume that you have installed OpenTTCN Tester 2011 to the following directory:
C:\Program Files (x86)\OpenTTCN\Tester2011
Your actual project settings may slightly differ from the settings presented in this tutorial if your actual installation directory is different.
Go to File | New | Project and opt for creating a Win32 Console Application. Pick a location of your liking for the newly created project and call it SimpleAdapter in the Name field.
Deselect Precompiled header and select Empty project in the Additional options section of the Application Settings tab. Click Finish.
Adding Main.cpp to the project
Right-click on the SimpleAdapter project and select Add | New Item | C++ File(.cpp). Enter Main.cpp in the Name field and click Add.
Add the following template code to Main.cpp:
#include <StartHere.h>
using OpenTTCN::SDK::StartHere;
#include <iostream>
int main(int argc, char *argv[])
{
StartHere::initialize("127.0.0.1", 5500);
std::cout << std::endl << "OpenTTCN SDK for C++ " << StartHere::getVersion() << std::endl;
std::cout << "Adapter for 'hello' example." << std::endl << std::endl;
// Register codec:
// TBD
// Start UDP listener:
// TBD
// Register adapter to a session at OpenTTCN server:
// TBD
std::cout << "Adapter initialisation complete." << std::endl << std::endl;
// Enter the infinite loop:
while (true) StartHere::sleepInSecs(100);
return 0;
}
Customizing project settings
The code from the previous section will not compile until you adjust project settings accordingly.
Switch to building a Release version of the binary:
- Build|Configuration Manager|Active solution configuration: Release, then Close.
Right-click on the SimpleAdapter project name in the left tab and 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:
C:\Program Files (x86)\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 MT libraries may not work correctly for the /MD build.
- Add this to Configuration Properties|Linker|General|Additional Library Directories:
C:\Program Files (x86)\OpenTTCN\Tester2011\sdk4c\lib
- Add this to Configuration Properties|Linker|Input|Additional Dependencies:
omniDynamic4.lib omniORB4.lib omnithread.lib pthreads-2.8.0-mt.lib openttcn-sdk-mt.lib ws2_32.lib
Finally, click Apply, then OK. The adapter code should now compile, although adapter does not do anything useful at this moment.
NOTE: Prior to OpenTTCN Tester 4.1beta5.3 the following additional library was also required: openttcn-sdk4cpp-mt.lib
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 SUT adapter callback handler class (SA_impl)
Add two new files, SA_impl.cpp and SA_impl.h, to the SimpleAdapter project.
The SA_impl class contains initial dummy implementation of callback handlers that are invoked by the SDK library in the background when TE wants to communicate something to the adapter or to the SUT through the adapter. In this case the adapter needs to handle the TE request and do something for it.
SA_impl.h:
#ifndef __OT_SIMPLE_SA_IMPL_H__
#define __OT_SIMPLE_SA_IMPL_H__
#include <tri/TriCommunicationSA.h>
using namespace ORG_ETSI_TTCN3_TRI;
class SA_impl : public TriCommunicationSA
{
public:
SA_impl() { }
virtual ~SA_impl() { }
virtual TriStatus triSAReset();
virtual TriStatus triExecuteTestCase(
const TriTestCaseId *testCaseId,
const TriPortIdList *tsiPortList);
virtual TriStatus triEndTestCase();
virtual TriStatus triSend(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddress *SUTaddress,
const TriMessage *sendMessage);
// Boiler plate implementations:
virtual TriStatus triMap(
const TriPortId *comPortId,
const TriPortId *tsiPortId);
virtual TriStatus triUnmap(
const TriPortId *comPortId,
const TriPortId *tsiPortId);
virtual TriStatus triSendBC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriMessage *sendMessage);
virtual TriStatus triSendMC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddressList *SUTaddresses,
const TriMessage *sendMessage);
virtual TriStatus triCall(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddress *sutAddress,
const TriSignatureId *signatureId,
const TriParameterList *parameterList);
virtual TriStatus triCallBC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriSignatureId *signatureId,
const TriParameterList *parameterList);
virtual TriStatus triCallMC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddressList *sutAddresses,
const TriSignatureId *signatureId,
const TriParameterList *parameterList);
virtual TriStatus triReply(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddress *sutAddress,
const TriSignatureId *signatureId,
const TriParameterList *parameterList,
const TriParameter *returnValue);
virtual TriStatus triReplyBC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriSignatureId *signatureId,
const TriParameterList *parameterList,
const TriParameter *returnValue);
virtual TriStatus triReplyMC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddressList *sutAddresses,
const TriSignatureId *signatureId,
const TriParameterList *parameterList,
const TriParameter *returnValue);
virtual TriStatus triRaise(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddress *sutAddress,
const TriSignatureId *signatureId,
const TriException *exc);
virtual TriStatus triRaiseBC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriSignatureId *signatureId,
const TriException *exc);
virtual TriStatus triRaiseMC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddressList *sutAddresses,
const TriSignatureId *signatureId,
const TriException *exc);
virtual TriStatus triSUTactionInformal(
const Tstring *description);
};
#endif
SA_impl.cpp:
#include "SA_impl.h"
#include <iostream>
TriStatus SA_impl :: triSAReset()
{
std::cout << "triSAReset() callback received" << std::endl;
return TRI_OK;
}
TriStatus SA_impl :: triExecuteTestCase(
const TriTestCaseId *testCaseId,
const TriPortIdList *tsiPortList)
{
std::cout << "triExecuteTestCase() callback received" << std::endl;
return TRI_OK;
}
TriStatus SA_impl :: triEndTestCase()
{
std::cout << "triEndTestCase() callback received" << std::endl;
return TRI_OK;
}
TriStatus SA_impl :: triSend(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddress *SUTaddress,
const TriMessage *sendMessage)
{
std::cout << "triSend() callback received." << std::endl;
return TRI_ERROR;
}
TriStatus SA_impl :: triMap(
const TriPortId *comPortId,
const TriPortId *tsiPortId)
{
std::cout << "triMap() callback received" << std::endl;
return TRI_OK;
}
TriStatus SA_impl :: triUnmap(
const TriPortId *comPortId,
const TriPortId *tsiPortId)
{
std::cout << "triUnmap() callback received" << std::endl;
return TRI_OK;
}
TriStatus SA_impl :: triSendBC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriMessage *sendMessage)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triSendMC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddressList *SUTaddresses,
const TriMessage *sendMessage)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triCall(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddress *sutAddress,
const TriSignatureId *signatureId,
const TriParameterList *parameterList)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triCallBC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriSignatureId *signatureId,
const TriParameterList *parameterList)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triCallMC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddressList *sutAddresses,
const TriSignatureId *signatureId,
const TriParameterList *parameterList)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triReply(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddress *sutAddress,
const TriSignatureId *signatureId,
const TriParameterList *parameterList,
const TriParameter *returnValue)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triReplyBC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriSignatureId *signatureId,
const TriParameterList *parameterList,
const TriParameter *returnValue)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triReplyMC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddressList *sutAddresses,
const TriSignatureId *signatureId,
const TriParameterList *parameterList,
const TriParameter *returnValue)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triRaise(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddress *sutAddress,
const TriSignatureId *signatureId,
const TriException *exc)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triRaiseBC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriSignatureId *signatureId,
const TriException *exc)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triRaiseMC(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddressList *sutAddresses,
const TriSignatureId *signatureId,
const TriException *exc)
{
return TRI_ERROR;
}
TriStatus SA_impl :: triSUTactionInformal(const Tstring *description)
{
std::cout << "triSUTactionInformal() callback was received. "
<< "Description: " << *description << std::endl;
return TRI_OK;
}
You can now update Main.cpp as follows:
...
#include "SA_impl.h"
#include <StartHereSA.h>
using OpenTTCN::SDK::StartHereSA;
int main(int argc, char *argv[])
{
...
// Register adapter to a session at OpenTTCN server:
// Tell TE that SA_impl will handle port called "tsiPort" in session called "hello".
StartHereSA::registerPort("hello", "tsiPort", new SA_impl());
std::cout << "Adapter initialisation complete." << std::endl << std::endl;
...
}
...
This registers a new instance of the SA_impl callback handler in the SDK which forwards the registration to OpenTTCN server, so when test cases in the hello session are run, instance of SA_impl registered in the SDK will receive notifications of relevant events.
For real-world adapters, you normally need to provide a sensible implementation of the SA_impl::triSend() callback handler only. It overrides a triSend() virtual abstract method defined in ORG_ETSI_TTCN3_TRI::TriCommunicationSA abstract class. This will be done later in this article.
The triSend method is called in the background by the library to indicate that the test execution has a frame to be sent to the SUT.
Running adapter for the first time
The hello session needs to exist prior to starting the adapter after the code modification described in the previous section is made.
You can create a session from OpenTTCN Tester 2011 IDE by creating a hello project, then adding Main.ttcn containing the following:
module Main
{
}
and then compiling the hello project using OpenTTCN TTCN-3 compiler from the IDE.
To add a new project to the current workspace in OpenTTCN Tester 2011 IDE, right-click on an empty area in the Navigator tab (make sure you use TTCN-3 perspective if you do not see the Navigator tab), then select New | TTCN-3 project, then enter hello in the Project name field and click Finish.
To add Main.ttcn to the hello project, right-click on the project name in the Navigator tab, then select New | TTCN-3 file, then enter Main in the Module name field and click Finish.
If Project | Build Automatically option is enabled in OpenTTCN Tester 2011 IDE, then the test suite shall build automatically after you save the .ttcn file. If this option is disabled, then the test suite can be built manually using Project | Build Project command.
You can create a session from the command-line as follows:
session create hello
Prior to creating a session, you need to make sure that OpenTTCN server (openttcnd) is started. From the command line, OpenTTCN server can be started this way:
ot start
You can now start the new version of the adapter. From Visual Studio, you can launch it 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.
You can now use the following command to check the hello session status:
session status hello
and see that an adapter registration has appeared in the session:
Adapters ----------------------------------------------------------------------------- Name Status Description TRI:tsiPort ALIVE TRI SA implementing TSI port
This means that we have our adapter successfully registered to the hello session with TRI role and the adapter is reachable from within that session (has alive status).
Adding coding request callback handler class (CD_impl)
Add two new files, CD_impl.cpp and CD_impl.h, to the SimpleAdapter project.
The CD_impl class contains initial dummy implementation of callback handlers that are invoked by the SDK library in the background when there is a need to encode or decode some data.
CD_impl.h:
#ifndef __OT_SIMPLE_CD_IMPL_H__
#define __OT_SIMPLE_CD_IMPL_H__
#include <tci/TciCdProvided.h>
using namespace ORG_ETSI_TTCN3_TCI;
using namespace ORG_ETSI_TTCN3_TRI;
class CD_impl : public TciCdProvided
{
public:
virtual ~CD_impl();
virtual TciValue* decode(const TriMessage* p_message,
const TciType* p_decodingHypothesis);
virtual TriMessage* encode(const TciValue* p_value);
};
#endif
CD_impl.cpp:
#include "CD_impl.h"
CD_impl :: ~CD_impl()
{
}
TciValue* CD_impl :: decode(const TriMessage* p_message,
const TciType* p_decodingHypothesis)
{
return 0;
}
TriMessage* CD_impl :: encode(const TciValue* p_value)
{
return 0;
}
You can now update Main.cpp as follows:
...
#include "CD_impl.h"
#include <StartHereCD.h>
using OpenTTCN::SDK::StartHereCD;
int main(int argc, char *argv[])
{
...
// Register codec:
StartHereCD::registerCodecImpl(new CD_impl());
// Start UDP listener:
...
}
...
This registers an instance of the CD_impl callback handler in the SDK so that SDK knows where to dispatch its coding requests.
Normally you need to implement both TciCdProvided::encode() and TciCdProvided::decode() abstract virtual methods in the CD_impl derived class. This will be done later in this article.
SUT definition
We will be testing a simple UDP-based protocol with a few defined frames, simple behaviour, and simple network interface.
For precise protocol definition, please read the main article of this section.
In our test configuration SUT has server role and test harness simulates a client. Frame exchange between the two peers, client and server, uses UDP communication.
SUT pre-compiled binary can be downloaded here. Source code is also included for reference.
TTCN-3 message type definitions
In OpenTTCN Tester 2011 IDE, create a hello project and add Main.ttcn file to it if you did not do so already.
To add a new project to the current workspace in OpenTTCN Tester 2011 IDE, right-click on an empty area in the Navigator tab (make sure you use TTCN-3 perspective if you do not see the Navigator tab), then select New | TTCN-3 project, then enter hello in the Project name field and click Finish.
To add Main.ttcn to the hello project, right-click on the project name in the Navigator tab, then select New | TTCN-3 file, then enter Main in the Module name field and click Finish.
Add the following content to Main.ttcn instead of what it currently contains:
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
Add the following content to Main.ttcn in addition to what it currently contains:
/*******************************************************************
* 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
If Project | Build Automatically option is enabled in OpenTTCN Tester 2011 IDE, then the test suite shall build automatically after you save the .ttcn file. If this option is disabled, then the test suite can be built manually using Project | Build Project command.
From the command line, test suite compilation can be done as follows:
importer3 load hello Main.ttcn
Read here for more hints on organizing your Makefiles using command-line tools.
Preparing to run test case
In OpenTTCN Tester 2011 IDE we need to create two launch configurations, one for starting the adapter binary automatically when a test campaign starts, another one for running test cases. We also need to bind two launch configurations together.
Create an Eclipse run configuration for the adapter:
- in the toolbar select External Tools | External Tools Configurations...
- click the OpenTTCN Adapter section
- click the New launch configuration button icon
- enter
hello Adapterin the Name field
- enter location of the adapter binary (absolute path) in the Location field, for example:
C:\Users\Alexey\tutorials\SimpleAdapter\Release\SimpleAdapter.exe
- enter working directory in the Working Directory field, for example:
C:\Users\Alexey\tutorials\SimpleAdapter\Release\
- click Apply, then Close to save changes
Create an Eclipse run configuration for the test suite:
- right-click on the hello project name and select Run As | Run Configurations...
- click the OpenTTCN Test Suite section
- click the New launch configuration button in the top-left corner of the window
- enter
hello_runin place of configuration name
- enter
helloin place of project name
- click the Test Plan tab, select Run selected test cases, select
Main.TC_R2D2_001test case
Establish a binding between run configuration for the adapter and run configuration for the test suite:
- in the same
hello_runrun configuration editor window select the Adapters tab
- check the External adapters | Start the following adapters automatically with Test Campaign box
- check the hello Adapter item in the list
- click Apply, then Close to finalize changes
Running test case (first try)
Make sure you have rebuilt the adapter binary.
Run the test case as follows:
- right-click on the
helloproject and select Run As | Run Configurations... | hello_run | Run
If you see adapter executable output and not the campaign text log output in the Console tab of the IDE, you can switch between consoles by clicking the Display Selected Console icon in the top right corner of the Console tab and selecting the hello_run console from the list.
The console output for the test campaign shall complain with this kind of error message:
mtc : {11:01:44.396} : // VERDICT Main.TC_R2D2_001 TEST CASE ERROR: E4205: triSend() return status is not TRI_OK.
Indeed, we return TRI_ERROR from SA_impl::triSend() to indicate that we did not implement the 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.
From the command line, test case can be run as follows:
tester run hello TC_R2D2_001
Creating encoder
Currently, functionality for encoding message content and address field is not implemented in our version of CD_impl::encode().
Before we go for implementing SA_impl::triSend(), we need to implement the encoding part first, because triSend() receives message and address parameters in already encoded form.
Here is the code that needs to replace the earlier version of encode() in CD_impl.cpp.
Header includes and using directives:
#include <StartHereCD.h> #include <tri/TriTypes.h> #include <tci/TciValues.h> #include <isl/FormatConverter.h> #include <iostream> #ifndef _WIN32 #include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h> #else #include <winsock2.h> #endif using namespace ORG_ETSI_TTCN3_TCI; using namespace ORG_ETSI_TTCN3_TRI; using OpenTTCN::ISL::FormatConverter; using OpenTTCN::SDK::StartHereCD;
The code itself:
TriMessage* CD_impl :: encode(const TciValue* p_value)
{
TriMessage* result = 0;
std::cout << "Encoding value of " +
p_value->getType().getName() << " type." << std::endl;
const TciType& valType = p_value->getType();
const std::string& typeName = valType.getName();
TciTypeClass typeClass = valType.getTypeClass();
if (typeClass == TCI_ADDRESS)
{
// Cast the address value to record:
const RecordValue* addrRecord =
dynamic_cast<const RecordValue*> (p_value);
// Extract relevant fields 'host' and 'port':
const CharstringValue* hostField =
dynamic_cast<const CharstringValue*> (&(addrRecord->getField("host")));
const IntegerValue* portField =
dynamic_cast<const IntegerValue*> (&(addrRecord->getField("portField")));
// Construct the encoded representation in an octet array:
unsigned char buffer[6];
// Encode address:
std::string host = hostField->getString();
host = host.substr(1, host.size() - 2);
unsigned long ipAddr = inet_addr(host.c_str());
if (ipAddr == INADDR_NONE)
{
std::cout << "encode(): Error: Cannot resolve actual valid "
"IP address from the address host field: " << host.c_str() << std::endl;
return 0;
}
memcpy(buffer, &ipAddr, 4);
// Encode port:
long port = portField->getInt();
unsigned short convPort = htons((unsigned short) port);
memcpy(buffer + 4, &convPort, 2);
std::cout << "Address encoded to: " <<
FormatConverter::charstringToOctetstring(buffer, 6) << std::endl;
result = TriMessage::create();
result->setData(buffer, 48);
return result;
}
else if (typeName == "Message")
{
// Encode Message (record type):
const RecordValue* msgRecord =
dynamic_cast<const RecordValue*> (p_value);
// Start by encoding the type code:
const IntegerValue* tcField =
dynamic_cast<const IntegerValue*> (&(msgRecord->getField("typeCode")));
const TciValue& pl = msgRecord->getField("payload");
if (pl.notPresent())
{
// Payload is not present, the encoded representation only
// contains the type code:
unsigned char buffer[1];
buffer[0] = (unsigned char) tcField->getInt();
result = TriMessage::create();
result->setData(buffer, 8);
return result;
}
// Proceed to encode also the payload:
std::cout << "Payload field present." << std::endl;
std::string encoded;
encoded += std::string(1, (char) tcField->getInt());
const RecordValue* plRec = dynamic_cast<const RecordValue*> (&pl);
const IntegerValue* lenField =
dynamic_cast<const IntegerValue*> (&(plRec->getField("len")));
const CharstringValue* contentField =
dynamic_cast<const CharstringValue*> (&(plRec->getField("content")));
long length = lenField->getInt();
// MSB first == big endian == network byte order:
encoded += std::string(1, (char) (length >> 8));
encoded += std::string(1, (char) (length >> 0));
const std::string& s = contentField->getString();
encoded += s.substr(1, s.size() - 2);
std::cout << "Message encoded to: " <<
FormatConverter::charstringToOctetstring(encoded) << std::endl;
result = TriMessage::create();
result->setData((const unsigned char *) encoded.c_str(), encoded.size() << 3);
return result;
}
else if (typeName == "Raw")
{
// 'Raw' data, process octet by octet:
const OctetstringValue* rawData =
dynamic_cast<const OctetstringValue*> (p_value);
int lenInOctets = rawData->getLength();
std::string s = rawData->getString();
s = s.substr(1, s.size() - 3);
s = FormatConverter::octetstringToCharstring(s);
result = TriMessage::create();
result->setData((const unsigned char *) s.c_str(), s.size() << 3);
return result;
}
std::cout << "Error: Unable to encode a message of type '"
<< typeName << "'" << std::endl;
return 0;
}
NOTE: If you get build error telling "LINK : fatal error LNK1104: cannot open file 'SimpleAdapter\Release\SimpleAdapter.exe'", make sure that adapter is not running already. In OpenTTCN Tester IDE you may need to switch to adapter console window and push the red Stop button to stop the adapter.
Sending frame to the SUT
Now everything is ready to add a proper implementation of the triSend() handler to our code. It should extract IP address and UDP port of the SUT from the address parameter, and then send encoded data frame contained in the sendMessage parameter to the SUT using extracted address fields.
Here is a new triSend() handler for SA_impl.cpp that does all this:
TriStatus SA_impl :: triSend(
const TriComponentId *componentId,
const TriPortId *tsiPortId,
const TriAddress *SUTaddress,
const TriMessage *sendMessage)
{
std::cout << "triSend() callback received." << std::endl;
// Send the message to SUT
// Address contains the address of the SUT as IP + PORT (4 + 2 bytes).
// Both port and address are in network byte order.
unsigned long ipAddr = 0;
unsigned short portNumber = 0;
if (SUTaddress->getBitsDataLen() != 48)
{
std::cerr << "Invalid address received, length " <<
SUTaddress->getBitsDataLen() << " != 48" << std::endl;
return TRI_ERROR;
}
unsigned char buffer[6];
memcpy(buffer, SUTaddress->getEncodedData(), 6);
memcpy(&ipAddr, &buffer[0], 4); // Get address
memcpy(&portNumber, &buffer[4], 2); // Get port
int result = sendDatagramPacket(
ipAddr,
portNumber,
(const char *) sendMessage->getData(),
sendMessage->getBitsDataLen() >> 3);
return (!result) ? TRI_OK : TRI_ERROR;
}
To use this code, you will need to #include "Utilities.h" to SA_impl.cpp and add files Utilities.h and Utilities.cpp 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.
SUT implementation
SUT implementation is available both in source (C/C++) and binary form. Main article of this section explains how to download, install, and run the SUT. Follow the instructions contained in the main article of this section before moving forward. You do not have to recompile the SUT because the downloadable package already contains the SUT binary.
SUT pre-compiled binary can be downloaded here. Source code is also included for reference.
Running test case (second try)
Before running the test case make sure that SUT is up and running (use start_sut.bat to launch it).
Now run the test case as it was explained in Running test case (first try) section.
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.
Here is what the SUT window shall contain (actual value of the remote UDP port may vary):
-------------------------------------------------------------------------- RECEIVED REQUEST MESSAGE (UDP PACKET) FROM PEER Remote host (source): 127.0.0.1 Remote UDP port (source): 58139 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): 58139 >>>>> 02 00 0F 48 65 6C 6C 6F 2C 20 6D 61 6E 6B 69 6E 64 21 >>>>>
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() internally. This function blocks until a UDP frame arrives from the remote peer.
We use pthreads to launch a dedicated thread.
Add SocketListener.cpp and SocketListener.h to the Visual Studio project.
SocketListener.h:
#ifndef __OT_SIMPLE_SOCKET_LISTENER_H__
#define __OT_SIMPLE_SOCKET_LISTENER_H__
#include "Utilities.h"
class SocketListener
{
public:
SocketListener();
/// Utility function enqueue a datagram to TE.
void enqueueMessage(
unsigned long host_,
unsigned short port_,
const char* data_,
long length_);
private:
// Implements a UDP port listener thread. This function runs an infinite loop
// in a separate thread listening for incoming UDP packets to arrive.
void runSocketListener();
public:
// Wrapper for runSocketListener();
static void *runSocketListenerWrapper(void *p_);
void start();
};
#endif
SocketListener.cpp:
#include "SocketListener.h"
#include "Utilities.h"
#include <StartHereSA.h>
#include <tri/TriCommunicationSA.h>
using OpenTTCN::SDK::StartHereSA;
#include <isl/FormatConverter.h>
using OpenTTCN::ISL::FormatConverter;
#include <iostream>
#include <string>
#include <sstream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <iomanip>
#include <sys/types.h>
#include <cassert>
#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
using namespace std;
const int MAX_BUFFER_SIZE = 65536;
string _portName = "tsiPort"; // hard-coded
static string local_inet_ntoa(unsigned long host_)
{
struct sockaddr_in saddr;
memset (&saddr, 0, sizeof saddr);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = host_;
return ::inet_ntoa(saddr.sin_addr);
}
SocketListener :: SocketListener()
{
if (!_socketDescInitialized)
{
int rval = bindSocketToAnyPort(&_socketDesc);
if (rval)
{
printf("sendDatagramPacket(): Cannot bind socket.\n");
return;
}
_socketDescInitialized = 1;
}
}
void SocketListener :: enqueueMessage(
unsigned long host_,
unsigned short port_,
const char* data_,
long length_)
{
cout << "----------------------------------------------------------"
"----------------" << endl;
cout << "Received from IUT:" << endl;
cout << endl;
cout << " Remote host (source): " << local_inet_ntoa(host_) << endl;
cout << " Remote UDP port (source): " << ::ntohs(port_) << endl;
cout << endl;
cout << "<<<<<" << endl;
cout << FormatConverter::formatHexMessage(FormatConverter::
charstringToOctetstring((const unsigned char *) data_, length_)) << endl;
cout << "<<<<<" << endl;
ORG_ETSI_TTCN3_TRI::Tbyte buffer[6];
memcpy(&buffer[0], &host_, 4); // Get address
buffer[4] = ((char *) &port_)[0]; // Get port
buffer[5] = ((char *) &port_)[1];
auto_ptr<ORG_ETSI_TTCN3_TRI::TriAddress> sutAddr(ORG_ETSI_TTCN3_TRI::TriAddress::create());
sutAddr->setEncodedData(buffer, 48);
auto_ptr<ORG_ETSI_TTCN3_TRI::TriMessage> recvMsg(ORG_ETSI_TTCN3_TRI::TriMessage::create());
recvMsg->setData((const ORG_ETSI_TTCN3_TRI::Tbyte *) data_, length_ * 8);
auto_ptr<ORG_ETSI_TTCN3_TRI::TriPortId> tsiPort(ORG_ETSI_TTCN3_TRI::TriPortId::create());
tsiPort->setPortName(_portName);
StartHereSA::getTriCommunicationTE().triEnqueueMsg(tsiPort.get(), sutAddr.get(), 0, recvMsg.get());
}
void SocketListener :: runSocketListener()
{
int rval;
unsigned char buff[MAX_BUFFER_SIZE];
struct sockaddr_in src_addr;
socklen_t addrlen = sizeof src_addr;
unsigned long host;
unsigned short port;
// Loop forever:
while (true)
{
// Receiving 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 (-1 == rval)
{
#ifndef _WIN32
cout << "runSocketListener(): recvfrom(): "
"Error: " << strerror(errno) << endl;
#else
int errorCode = WSAGetLastError();
cout << "runSocketListener(): recvfrom(): "
"Error: error code = " << errorCode << endl;
#endif
break;
}
host = src_addr.sin_addr.s_addr;
port = src_addr.sin_port;
buff[rval] = 0;
enqueueMessage(host, port, (const char*) buff, rval);
}
cout << "***UDP PACKET RECEIVING DISABLED***" << endl;
}
void *SocketListener :: runSocketListenerWrapper(void *p_)
{
((SocketListener*)p_)->runSocketListener();
return 0;
}
void SocketListener :: start()
{
cout << "Starting UDP listener." << endl;
pthread_t thread_ptr;
if (pthread_create(&thread_ptr, 0, &SocketListener::runSocketListenerWrapper, this) != 0)
{
cout << "SocketListener::start(): Unable to start a new thread" << endl;
}
}
We also need to modify Main.cpp as follows:
...
#include "SocketListener.h"
int main(int argc, char *argv[])
{
...
// Start UDP listener:
SocketListener sl;
sl.start();
// Register adapter to a session at OpenTTCN server:
...
}
Note use of StartHereSA::getTriCommunicationTE().triEnqueueMsg() in SocketListener.cpp which does the actual sending to TE.
Now if we try our test case, we will see that a response from the SUT is received by the adapter, but it is not forwarded to TE due to missing decoder, so TE will complain with the following error message:
mtc : {12:59:12.091} : // CASE TC_R2D2_001 STARTED
...
adapter : {12:59:12.118} : // VERDICT Main.TC_R2D2_001 TEST CASE ERROR: E4207: Adapter error: [TRI:tsiPort]: triEnqueueMsg(): No message content, message will not be sent.
mtc : {12:59:12.123} : // CASE Main.TC_R2D2_001 FINISHED WITH ERROR
Creating decoder
Our listener implementation enqueues frames in encoded form. They need to be decoded prior to supplying them to the TE.
Here is a replacement for the dummy implementation of decode() in CD_impl.cpp:
TciValue* CD_impl :: decode(
const TriMessage* p_message,
const TciType* p_decodingHypothesis)
{
const Tbyte* data = p_message->getData();
int lenInBits = p_message->getBitsDataLen();
int lenInOctets = (lenInBits >> 3) + ((lenInBits & 0x7) ? 1 : 0);
std::cout << std::endl << "Decoding: " +
FormatConverter::charstringToOctetstring(data, lenInOctets) << std::endl;
// Determine the course of action based on the decoding hypothesis:
if (!p_decodingHypothesis)
{
std::cout << "No decoding hypothesis, "
"assuming decoding of a value of Message type." << std::endl;
return decodeMessage(data, lenInOctets);
}
else if (p_decodingHypothesis->getTypeClass() == TCI_ADDRESS)
{
std::cout << "Decoding a value of address type." << std::endl;
return decodeAddress(data, lenInOctets);
}
std::cout << "Error: Unable to decode a message of type '" <<
p_decodingHypothesis->getName() << "'" << std::endl;
return 0;
}
TciValue* CD_impl :: decodeMessage(const Tbyte* data, int lenInOctets)
{
TciCdRequired* cdReq = StartHereCD::getTciCdRequired();
if (!lenInOctets)
{
std::cout << "decodeMessage(): Error: Message content too short, "
"returning value of Raw type." << std::endl;
return makeRawValueFromBuffer(data, lenInOctets);
}
if (lenInOctets == 2)
{
std::cout << "decodeMessage(): Error: Message content is malformed, "
"returning value of Raw type." << std::endl;
return makeRawValueFromBuffer(data, lenInOctets);
}
// Construct a TCI record value for the message:
RecordValue* msgValue = dynamic_cast<RecordValue*>
(cdReq->getTypeForName("Message")->newInstance());
// The typeCode field is always present and is represented using
// octetstring. Construct a new value, initialize it and assign to
// the containing record:
IntegerValue* tcValue = dynamic_cast<IntegerValue*>
(cdReq->getTypeForName("uint8")->newInstance());
tcValue->setInt((long) (unsigned long) data[0]);
msgValue->setField("typeCode", *tcValue);
// Check for received frame length to detect whether payload is present
// or not:
if (lenInOctets < 2)
{
msgValue->setFieldOmitted("payload"); // no payload
return msgValue;
}
// Otherwise construct the decoded payload:
RecordValue* plValue = dynamic_cast<RecordValue*>
(cdReq->getTypeForName("Payload")->newInstance());
// Length field:
long length =
(((unsigned long) (unsigned char) data[1]) << 8) |
(((unsigned long) (unsigned char) data[2]) << 0);
IntegerValue* lenField = dynamic_cast<IntegerValue*>
(cdReq->getTypeForName("uint16")->newInstance());
lenField->setInt(length);
// Content:
CharstringValue* contentField =
dynamic_cast<CharstringValue*> (cdReq->getCharstring().newInstance());
contentField->setString("'" + std::string((char *) data + 3, lenInOctets - 3) + "'");
// Construct the final value:
plValue->setField("len", *lenField);
plValue->setField("content", *contentField);
msgValue->setField("payload", *plValue);
std::cout << "Message decoded to a value of Message type." << std::endl;
return msgValue;
}
TciValue* CD_impl :: decodeAddress(const Tbyte* data, int lenInOctets)
{
TciCdRequired* cdReq = StartHereCD::getTciCdRequired();
RecordValue* addrVal = dynamic_cast<RecordValue*>
(cdReq->getTypeForName("address")->newInstance());
// Decoding address information:
struct sockaddr_in saddr;
memset (&saddr, 0, sizeof saddr);
memcpy(&(saddr.sin_addr.s_addr), data, 4);
// Decode 'host' field:
char* host = inet_ntoa(saddr.sin_addr);
CharstringValue* hostField =
dynamic_cast<CharstringValue*> (cdReq->getCharstring().newInstance());
hostField->setString("'" + std::string(host) + "'");
// Decode 'port' field:
unsigned short port = 0;
memcpy(&port, data + 4, 2);
IntegerValue* portField =
dynamic_cast<IntegerValue*> (cdReq->getInteger().newInstance());
portField->setInt(ntohs(port));
// Append both fields to the resulting record value that
// represents the fully-qualified address:
addrVal->setField("host", *hostField);
addrVal->setField("portField", *portField);
std::cout << "Address decoded to a value of address type." << std::endl;
return addrVal;
}
TciValue* CD_impl :: makeRawValueFromBuffer(const Tbyte* data, int lenInOctets)
{
TciCdRequired* cdReq = StartHereCD::getTciCdRequired();
OctetstringValue* rawValue = dynamic_cast<OctetstringValue*>
(cdReq->getTypeForName("Raw")->newInstance());
rawValue->setLength(lenInOctets);
for (int i = 0; i < lenInOctets; i++) rawValue->setOctet(i, data[i]);
return rawValue;
}
We need to update CD_impl.h accordingly:
class CD_impl : public TciCdProvided
{
...
private:
// Helper functions:
ORG_ETSI_TTCN3_TCI::TciValue* decodeMessage(
const ORG_ETSI_TTCN3_TRI::Tbyte* data, int lenInOctets);
ORG_ETSI_TTCN3_TCI::TciValue* decodeAddress(
const ORG_ETSI_TTCN3_TRI::Tbyte* data, int lenInOctets);
ORG_ETSI_TTCN3_TCI::TciValue* makeRawValueFromBuffer(
const ORG_ETSI_TTCN3_TRI::Tbyte* data, int lenInOctets);
}
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 Console tab corresponding to the test campaign:
*****************************************************************************
*** MODULE PARAMETER LIST
modulepar
{
charstring PX_SUT_IP_ADDR := "127.0.0.1";
integer PX_SUT_PORT := 7431;
} // End of module parameter list.
*****************************************************************************
*** RUNNING TEST CASE Main.TC_R2D2_001
Tester : {13:16:08.787} : // Time: 13:16:08.787. Date: 24/Jan/2011. MOT version: TC: 4.1beta3.1.
mtc : {13:16:08.811} : // CASE TC_R2D2_001 STARTED
mtc : {13:16:08.811} : hello\Main.ttcn : 000102 : T_GUARD.start(10.0); // Timer is started: duration 10 s.
mtc : {13:16:08.812} : hello\Main.ttcn : 000103 : map(mtc:p, system:tsiPort);
mtc : {13:16:08.814} : hello\Main.ttcn : 000105 : p.send(Message GreetingRequest := {
typeCode := 1,
payload :=
{
len := 13,
content := "Hello, world!"
}
}) to {
host := "127.0.0.1",
portField := 7431
};
mtc : {13:16:08.838} : hello\Main.ttcn : 000106 : p.receive(Message GreetingResponse := {
typeCode := 2,
payload :=
{
len := 15,
content := "Hello, mankind!"
}
}) from {
host := "127.0.0.1",
portField := 7431
};
mtc : {13:16:08.839} : hello\Main.ttcn : 000108 : p.send(Message GreetingRequest := {
typeCode := 1,
payload :=
{
len := 17,
content := "Hello, gentlemen!"
}
}) to {
host := "127.0.0.1",
portField := 7431
};
mtc : {13:16:08.862} : hello\Main.ttcn : 000109 : p.receive(Message GreetingResponse := {
typeCode := 2,
payload :=
{
len := 11,
content := "Reformulate"
}
}) from {
host := "127.0.0.1",
portField := 7431
};
mtc : {13:16:08.862} : hello\Main.ttcn : 000111 : setverdict(pass);
mtc : {13:16:08.870} : // CASE Main.TC_R2D2_001 FINISHED WITH PASS
/////////////////////////////////////////////////////////////////////////////
/// TEST CASE Main.TC_R2D2_001 COMPLETE VERDICT PASS
*****************************************************************************
*** TEST EXECUTION SUMMARY
Pass Fail Inconc None Error Total Duration
1 0 0 0 0 1 00:00:00
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
Rawdata 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;
- integrating adapter code with third-party GUI test management software, if this is required by your project;
- disabling debug output to improve overall system performance;
- etc.
Adapter source code
You can download the full source code of the adapter example developed in this tutorial here.
