OpenTTCN/Developer corner/Creating adapter with OpenTTCN SDK for C sharp
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 MonoDevelop 2.4.2 to develop the adapter in question and we use OpenTTCN Tester 2011 IDE to run the example test suite. References to Microsoft Visual Studio 2008 are made wherever appropriate for completeness, however MonoDevelop is the main development environment in this article.
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.
OpenTTCN Tester 2011 supports .NET development in Visual Studio 2008 and 2010 and Mono for Windows, Linux, and MacOS X.
Mono-powered C# and .NET is a versatile development platform available for a variety of platforms, including Windows and Linux, enabling cross-platform application development and deployment.
Creating MonoDevelop 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 MonoDevelop 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 | Solution and opt for creating a C# | Console Project. Pick a location of your liking for the newly created project and call it SimpleAdapter4DotNet in the Name field.
We deselected the Create directory for solution checkbox offered in the New Solution window.
Click Forward, then OK.
- NOTE: In Visual Studio 2008, you can create a project and solution from scratch by going to File | New | Project and opting for creating Other Languages | Visual C# | Console Application.
- We selected
SimpleAdapteras project and solution name in Visual Studio 2008.
Adding Main.cs to the project
MonoDevelop creates initial version of Main.cs after solution/project is created.
- NOTE: Visual Studio 2008 creates
Program.csafter solution/project is created. You can rename it toMain.csby right-clicking on the file name in Solution Explorer and selecting Rename.
Replace the initial content of Main.cs with the following:
using System;
using System.Net;
using System.Net.Sockets;
using OpenTTCN.Sdk;
namespace SimpleAdapter
{
class Program
{
public static void Main(string[] args)
{
try
{
// Set default values of OpenTTCN server IP + port:
String serverAddr = "127.0.0.1";
int serverPort = 5500;
StartHere.Initialize(serverAddr, serverPort);
Console.WriteLine("\nOpenTTCN SDK for Microsoft .NET Framework " + StartHere.Version);
Console.WriteLine("Adapter for 'hello' example.\n");
// Register codec:
// TBD
// Initialize UDP socket:
// TBD
// Start UDP listener:
// TBD
// Register adapter to a session at OpenTTCN server:
// TBD
Console.WriteLine("Adapter initialisation complete. Press enter to exit.");
Console.ReadLine();
}
catch (OpenTTCN.Sdk.SdkException e)
{
Console.WriteLine("Error occured: " + e.Message);
}
catch (Exception e)
{
Console.WriteLine("\nERROR: Exception caught:\n\n" + e.ToString());
}
}
}
}
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 .NET executable:
- select Project | Active Configuration | Release|x86
- NOTE: For Visual Studio 2008: Build | Configuration Manager | Active solution configuration: Release, then Close.
Add assembly reference to all OpenTTCN SDK assemblies:
- right-click Solution | SimpleAdapter4DotNet | References, then Edit References | .Net Assembly, then browse to:
C:\Program Files (x86)\OpenTTCN\Tester2011\sdk4cs\assemblies,- add
IIOPChannel.dll,OpenTTCN.SDK.dllandopenttcnidl.dllto the list of references, and click OK- prior to OpenTTCN Tester 4.1beta5.6: instead of the above add
OpenTTCN.SDK.Core.dll,OpenTTCN.SDK.dllandOpenTTCN.TTCN3.dllto the list of references, and click OK
- prior to OpenTTCN Tester 4.1beta5.6: instead of the above add
- NOTE: For Visual Studio 2008:
- right-click Solution Explorer | SimpleAdapter | References, then Add Reference | Browse, then browse to:
C:\Program Files (x86)\OpenTTCN\Tester2011\sdk4cs\assemblies,- select
IIOPChannel.dll,OpenTTCN.SDK.dllandopenttcnidl.dll, and click OK- prior to OpenTTCN Tester 4.1beta5.6: instead of the above select
OpenTTCN.SDK.Core.dll,OpenTTCN.SDK.dllandOpenTTCN.TTCN3.dll, and click OK
- prior to OpenTTCN Tester 4.1beta5.6: instead of the above select
The adapter code should now compile, although adapter does not do anything useful at this moment.
IMPORTANT: Prior to OpenTTCN Tester 4.1beta5.6 you needed to specify x86 as Platform target and .NET Framework 2.0 as Target Framework. This was due to having mixed-mode libraries as part of OpenTTCN SDK for C# which were 32-bit and were compiled for a specific target. This restriction is lifted starting with OpenTTCN Tester 4.1beta5.6.
For MonoDevelop (prior to OpenTTCN Tester 4.1beta5.6):
- right-click on the SimpleAdapter4DotNet project, select Options, and from the Project Options page select:
- Build | General | Runtime version | Mono / .NET 2.0
- Build | Compiler | General Options | Platform target | x86
- NOTE: For Visual Studio 2008 (prior to OpenTTCN Tester 4.1beta5.6):
- right-click on the SimpleAdapter project, select Properties, and from the Properties page select:
- Application | Target Framework | .NET Framework 2.0
- Build | Platform target | x86
- after switching to .NET 2.0 target in Visual Studio 2008, you start receiving compile-time warnings about unresolved
System.Xml.LinqandSystem.Data.DataSetExtensionsassemblies. These assemblies can be removed from the References section of the SimpleAdapter project.
- after switching to .NET 2.0 target in Visual Studio 2008, you start receiving compile-time warnings about unresolved
Adding SUT adapter callback handler class (SA_impl)
Add a new file, SA_impl.cs, to the SimpleAdapter4DotNet project:
- right-click on the SimpleAdapter4DotNet project, select Add | New File | General | Empty Class, enter
SA_implin the Name field, and click New
- NOTE: For Visual Studio 2008: right-click on the SimpleAdapter project, select Add | New Item | Visual C# Items | Code | Class, enter
SA_impl.csin the Name field, and click Add
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.cs:
using System;
using System.Net;
using System.Net.Sockets;
using OpenTTCN.Sdk;
using Etsi.Ttcn3.Tri;
namespace SimpleAdapter
{
public class SA_impl : ITriCommunicationSA
{
public SA_impl()
{
}
#region ITriCommunicationSA Members
public TriStatus TriSAReset()
{
Console.WriteLine("TriSAReset() callback received");
return TriStatus.TriOk;
}
public TriStatus TriExecuteTestCase(
ITriTestCaseId testCaseId,
ITriPortIdList tsiPorts)
{
Console.WriteLine("TriExecuteTestCase() callback received");
return TriStatus.TriOk;
}
public TriStatus TriMap(
ITriPortId compPortId,
ITriPortId tsiPortId)
{
Console.WriteLine("TriMap() callback received");
return TriStatus.TriOk;
}
public TriStatus TriUnmap(
ITriPortId compPortId,
ITriPortId tsiPortId)
{
Console.WriteLine("TriUnmap() callback received");
return TriStatus.TriOk;
}
public TriStatus TriEndTestCase()
{
Console.WriteLine("TriEndTestCase() callback received");
return TriStatus.TriOk;
}
public TriStatus TriSend(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriAddress address,
ITriMessage sendMessage)
{
Console.WriteLine("TriSend() callback received.");
// TBD: Add actual TE -> SUT sending code here.
return TriStatus.TriError;
}
public TriStatus TriSendBC(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriMessage sentMessage)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriSendMC(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriAddressList addresses,
ITriMessage sentMessage)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriCall(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriAddress sutAddress,
ITriSignatureId signatureId,
ITriParameterList parameterList)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriCallBC(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriSignatureId signatureId,
ITriParameterList parameterList)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriCallMC(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriAddressList sutAddresses,
ITriSignatureId signatureId,
ITriParameterList parameterList)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriReply(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriAddress sutAddress,
ITriSignatureId signatureId,
ITriParameterList parameterList,
ITriParameter returnValue)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriReplyBC(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriSignatureId signatureId,
ITriParameterList parameterList,
ITriParameter returnValue)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriReplyMC(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriAddressList sutAddresses,
ITriSignatureId signatureId,
ITriParameterList parameterList,
ITriParameter returnValue)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriRaise(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriAddress sutAddress,
ITriSignatureId signatureId,
ITriException exc)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriRaiseBC(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriSignatureId signatureId,
ITriException exc)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriRaiseMC(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriAddressList sutAddresses,
ITriSignatureId signatureId,
ITriException exc)
{
return TriStatus.TriError; // not implemented
}
public TriStatus TriSutActionInformal(string description)
{
Console.WriteLine("TriSutActionInformal() callback was received. " +
"Description: " + description);
return TriStatus.TriOk;
}
#endregion
}
}
You can now update Main.cs as follows:
...
// 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()); // "TRI:tsiPort"
Console.WriteLine("Adapter initialisation complete. Press enter to exit.");
Console.ReadLine();
...
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 implements TriSend() method defined in Etsi.Ttcn3.Tri.ITriCommunicationSA interface. 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 both MonoDevelop and 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 a new file, CD_impl.cs, to the SimpleAdapter4DotNet 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.cs:
using System;
using System.Text;
using System.Net;
using Etsi.Ttcn3.Tci;
using Etsi.Ttcn3.Tri;
using OpenTTCN.Sdk;
namespace SimpleAdapter
{
class CD_impl : ITciCDProvided
{
public ITciValue Decode(ITriMessage message, ITciType decodingHypothesis)
{
return null;
}
public ITriMessage Encode(ITciValue value)
{
return null;
}
}
}
You can now update Main.cs as follows:
...
// Register codec:
StartHereCD.RegisterCodecImpl(new CD_impl());
// Initialize UDP socket:
...
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 ITciCDProvided.Encode() and ITciCDProvided.Decode() interface methods in the CD_impl 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.
An implementation of the same SUT written in pure C# with almost identical behaviour in terms of frame exchange is available in source code form in the package directory:
C:\Program Files (x86)\OpenTTCN\Tester2011\sdk4cs\examples\hello\SystemUnderTest
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:\Lesha\tutorials\SimpleAdapter4DotNet\bin\Release\SimpleAdapter4DotNet.exe
- enter working directory in the Working Directory field, for example:
C:\Lesha\tutorials\SimpleAdapter4DotNet\bin\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 : {17:05:27.431} : // VERDICT Main.TC_R2D2_001 TEST CASE ERROR: E4205: triSend() return status is not TRI_OK.
Indeed, we return TriStatus.TriError 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.
Lack of encoder may have resulted in the following additional error message in the test campaign output:
adapter : {17:05:27.427} : setverdict(error); // Implicitly performed system call.
adapter : {17:05:27.427} : // VERDICT Main.TC_R2D2_001 TEST CASE ERROR: E4207: Adapter error: [TRI:tsiPort]: Received a null reference from the user-implemented encoder callback function.
adapter : {17:05:27.429} : setverdict(error); // Implicitly performed system call.
adapter : {17:05:27.429} : // VERDICT Main.TC_R2D2_001 TEST CASE ERROR: E4207: Adapter error: [TRI:tsiPort]: Received a null reference from the user-implemented encoder callback function.
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.cs:
public ITriMessage Encode(ITciValue value)
{
Console.WriteLine("Encoding: " + value.ToString());
if (value is ITciAddressValue)
{
// Extract the contained record value from the address value:
ITciRecordValue addrRecord =
(ITciRecordValue)((ITciAddressValue)value).Address;
// Extract relevant fields 'host' and 'port':
ITciCharstringValue hostField =
(ITciCharstringValue)addrRecord.GetField("host");
ITciIntegerValue portField =
(ITciIntegerValue)addrRecord.GetField("portField");
// Construct the encoded representation into bytes array:
Byte[] bytes = new Byte[6];
// Encode address
IPAddress addr = IPAddress.Parse(hostField.StringValue);
addr.GetAddressBytes().CopyTo(bytes, 0);
// Encode port
short a = IPAddress.HostToNetworkOrder((short)portField.IntegerValue);
BitConverter.GetBytes(a).CopyTo(bytes, 4);
Console.WriteLine("Address encoded to: " +
BitConverter.ToString(bytes) + Environment.NewLine);
return new TriMessage(bytes);
}
else if (value.Type.Name == "Message")
{
// Encode Message (record type)
ITciRecordValue msgRecord = (ITciRecordValue)value;
// Begin by encoding the type code.
ITciIntegerValue tcField =
(ITciIntegerValue)msgRecord.GetField("typeCode");
ITciRecordValue plRec =
(ITciRecordValue)msgRecord.GetField("payload");
if (plRec.NotPresent)
{
// Payload is not present, the encoded representation
// only contains the type code:
return new TriMessage((byte)tcField.IntegerValue);
}
// Proceed to encode also the payload:
Console.WriteLine("Payload field present.");
ITciIntegerValue lenField =
(ITciIntegerValue)plRec.GetField("len");
ITciCharstringValue contentField =
(ITciCharstringValue)plRec.GetField("content");
// MSB first == big endian == network byte order
Byte[] lenbytes = BitConverter.GetBytes(
IPAddress.HostToNetworkOrder((short)lenField.IntegerValue));
Byte[] databytes =
Encoding.ASCII.GetBytes(contentField.StringValue);
Byte[] bytes = new Byte[lenbytes.Length + databytes.Length + 1];
bytes[0] = (byte)tcField.IntegerValue;
lenbytes.CopyTo(bytes, 1);
databytes.CopyTo(bytes, 3);
Console.WriteLine("Message encoded to: " +
BitConverter.ToString(bytes) + Environment.NewLine);
return new TriMessage(bytes);
}
else if (value.Type.Name == "Raw")
{
// 'Raw' data, process octet by octet:
ITciOctetstringValue rawData = (ITciOctetstringValue)value;
int lenInBytes = rawData.Length;
Byte[] bytes = new Byte[lenInBytes];
for (int i = 0; i < lenInBytes; i++)
{
bytes[i] = rawData[i];
}
return new TriMessage(bytes);
}
throw new AdapterException(String.Format(
"Unable to encode value '{0}'.", value.ToString()));
}
NOTE: If you get build error telling "Error CS0016: Could not write to output file 'SimpleAdapter4DotNet.exe' -- 'The process cannot access the file because it is being used by another process. ' (CS0016) (SimpleAdapter4DotNet)", 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 SA_impl.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.cs that does all this:
public TriStatus TriSend(
ITriComponentId componentId,
ITriPortId tsiPortId,
ITriAddress address,
ITriMessage sendMessage)
{
Console.WriteLine("TriSend() callback received.");
// Send the message to the SUT:
// Address field contains SUT address as IP + PORT (4 + 2 bytes).
// Port is in network byte order.
Int16 port = IPAddress.NetworkToHostOrder(
(BitConverter.ToInt16(address.EncodedAddress, 4)));
// Get address.
UInt32 addr = BitConverter.ToUInt32(address.EncodedAddress, 0);
// Send the data.
IPEndPoint ep = new IPEndPoint(addr, port);
Console.WriteLine("Sending to: " + ep.ToString());
Program.UdpClient.Send(sendMessage.EncodedMessage,
sendMessage.EncodedMessage.Length, ep);
return TriStatus.TriOk;
}
To make the above work you need to update Main.cs as follows:
...
class Program
{
private static UdpClient _udpClient = null;
public static UdpClient UdpClient
{
get { return _udpClient; }
}
public static void Main(string[] args)
{
...
// Initialize UDP socket:
_udpClient = new UdpClient();
_udpClient.Client.SetSocketOption(
SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 0));
// Start UDP listener:
...
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.
An implementation of the same SUT written in pure C# with almost identical behaviour in terms of frame exchange is available in source code form in the package directory:
C:\Program Files (x86)\OpenTTCN\Tester2011\sdk4cs\examples\hello\SystemUnderTest
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): 49251 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): 49251 >>>>> 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 UdpClient.Receive() internally. This method blocks until a UDP frame arrives from the remote peer.
We use System.ComponentModel.BackgroundWorker to launch a dedicated thread.
Add SocketListener.cs to the MonoDevelop project.
SocketListener.cs:
using System;
using System.Net.Sockets;
using System.Net;
using System.ComponentModel;
using OpenTTCN.Sdk;
using Etsi.Ttcn3.Tri;
namespace SimpleAdapter
{
public class SocketListener
{
private ITriPortId _triPort;
private BackgroundWorker _bgworker = new BackgroundWorker();
public SocketListener(String tsiPort)
{
_triPort = new TriPortId(tsiPort);
// DoWork() will be called when the backgroundworker is started.
_bgworker.DoWork += new DoWorkEventHandler(DoWork);
}
public void Start()
{
Console.WriteLine("Starting UDP listener.");
_bgworker.RunWorkerAsync();
}
void DoWork(object sender, DoWorkEventArgs e)
{
try
{
// Infinite loop for receiving messages.
Byte[] receivedBytes;
IPEndPoint sourceAddr = new IPEndPoint(IPAddress.Any, 0);
while (true)
{
receivedBytes = Program.UdpClient.Receive(ref sourceAddr);
if (receivedBytes.Length > 0)
{
Console.WriteLine("Received " + BitConverter.
ToString(receivedBytes) + " from " + sourceAddr.ToString());
Byte[] addr = new Byte[6];
sourceAddr.Address.GetAddressBytes().CopyTo(addr, 0);
Int16 port2 = (Int16)sourceAddr.Port;
port2 = IPAddress.HostToNetworkOrder(port2);
BitConverter.GetBytes(port2).CopyTo(addr, 4);
ITriAddress triAddr = new TriAddress(addr);
StartHereSA.TriCommunicationTE.EnqueueMessage(
_triPort, triAddr, null, new TriMessage(receivedBytes));
}
}
}
catch (Exception exc)
{
Console.WriteLine(exc.ToString());
}
}
}
}
We also need to modify Main.cs as follows:
...
// Start UDP listener:
SocketListener sl = new SocketListener("tsiPort");
sl.Start();
// Register adapter to a session at OpenTTCN server:
...
Note use of StartHereSA.TriCommunicationTE.EnqueueMessage() in SocketListener.cs 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 : {13:00:15.626} : // CASE TC_R2D2_001 STARTED
...
adapter : {13:00:15.681} : setverdict(error); // Implicitly performed system call.
adapter : {13:00:15.681} : // VERDICT Main.TC_R2D2_001 TEST CASE ERROR: E4207: Adapter error: [TRI:tsiPort]: Received a null reference from the user-implemented decoder callback function.
adapter : {13:00:15.682} : setverdict(error); // Implicitly performed system call.
adapter : {13:00:15.682} : // VERDICT Main.TC_R2D2_001 TEST CASE ERROR: E4207: Adapter error: [TRI:tsiPort]: Received a null reference from the user-implemented decoder callback function.
adapter : {13:00:15.683} : setverdict(error); // Implicitly performed system call.
adapter : {13:00:15.683} : // VERDICT Main.TC_R2D2_001 TEST CASE ERROR: E4207: Adapter error: [TRI:tsiPort]: triEnqueueMsg(): No message content, message will not be sent.
mtc : {13:00:15.686} : // 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.cs:
public ITciValue Decode(ITriMessage message, ITciType decodingHypothesis)
{
Byte[] bytes = message.EncodedMessage;
Console.WriteLine("\nDecoding: " + BitConverter.ToString(bytes));
// Determine the choice of action based on the decoding hypothesis
if (decodingHypothesis == null)
{
Console.WriteLine(
"No decoding hypothesis given, assuming decode of a Message type.");
return decodeMessage(bytes);
}
else if(decodingHypothesis.TypeClass == TciTypeClass.Address)
{
Console.WriteLine("Decoding hypothesis was address.");
return decodeAddress(bytes);
}
throw new AdapterException(String.Format(
"Unable to decode a message of type '{0}'",
decodingHypothesis.ToString()));
}
ITciValue decodeMessage(Byte[] bytes)
{
// Construct a TCI record value for the message.
ITciRecordValue msgValue = TciValueFactory.NewRecordValue("Message");
// The typeCode field is always present and is represented using
// octetstring. Construct a new value, initialize it and assign to
// the containing record:
ITciIntegerValue tcValue = TciValueFactory.NewIntegerValue();
tcValue.IntegerValue = (long) (uint) bytes[0];
msgValue.SetField("typeCode", tcValue);
// Check for the special typecode that indicates that
// no payload is present:
if (bytes[0] == 0xFF)
{
msgValue.SetFieldOmitted("payload");
return msgValue;
}
// Otherwise construct the decoded payload:
ITciRecordValue plValue = TciValueFactory.NewRecordValue("Payload");
// Length field:
UInt16 lenInNetworkByteOrder = BitConverter.ToUInt16(bytes, 1);
UInt16 lenInHostByteOrder = (UInt16)
IPAddress.NetworkToHostOrder((short) lenInNetworkByteOrder);
ITciIntegerValue lenField =
TciValueFactory.NewIntegerValue(lenInHostByteOrder);
// Content:
ITciCharstringValue contentField = TciValueFactory.NewCharstringValue(
Encoding.ASCII.GetString(bytes, 3, lenInHostByteOrder));
// Construct the final value:
plValue.SetField("len", lenField);
plValue.SetField("content", contentField);
msgValue.SetField("payload", plValue);
Console.WriteLine("Message decoded to:" +
msgValue.ToString() + Environment.NewLine);
return msgValue;
}
ITciValue decodeAddress(Byte[] bytes)
{
Console.WriteLine("Decoding address...");
// Construct IPAddress from the address bytes in network byte order:
IPAddress addr = new IPAddress(BitConverter.ToInt32(bytes, 0));
// Construct an address value and obtain reference to the
// contained value of record type.
ITciAddressValue addrVal = TciValueFactory.NewAddressValue();
ITciRecordValue addrRec = (ITciRecordValue) addrVal.Address;
// Construct a TCI integer value and initialize it with the port number.
ITciIntegerValue portValue = TciValueFactory.NewIntegerValue(
IPAddress.HostToNetworkOrder((short)BitConverter.ToUInt16(bytes, 4)));
// Construct a TCI charstring value and initialize it with the hostname.
ITciCharstringValue hostValue =
TciValueFactory.NewCharstringValue(addr.ToString());
// Assign the record fields and we are done
addrRec.SetField("host", hostValue);
addrRec.SetField("portField", portValue);
Console.WriteLine("Address decoded to: " + addrVal.ToString() + Environment.NewLine);
return addrVal;
}
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:11:48.536} : // Time: 13:11:48.536. Date: 27/Jan/2011. MOT version: TC: 4.1beta4.0.
mtc : {13:11:48.556} : // CASE TC_R2D2_001 STARTED
mtc : {13:11:48.556} : hello\Main.ttcn : 000102 : T_GUARD.start(10.0); // Timer is started: duration 10 s.
mtc : {13:11:48.558} : hello\Main.ttcn : 000103 : map(mtc:p, system:tsiPort);
mtc : {13:11:48.582} : 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:11:48.618} : 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:11:48.620} : 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:11:48.640} : 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:11:48.640} : hello\Main.ttcn : 000111 : setverdict(pass);
mtc : {13:11:48.645} : // 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.
- NOTE: Visual Studio 2008 version of the same adapter example can be downloaded here.
