OpenTTCN/Language reference

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 December 9, 2010

TTCN-3 language reference



Contents

What is TTCN-3?

TTCN-3 is a strongly typed test scripting language used in conformance testing of communicating systems. If you have to deal with testing of a network protocol or communicating entity, TTCN-3 is the way to go. TTCN-3 is also suitable for software testing to a certain extent. In addition, it has been used in automotive industry, as well as in testing IEEE and IETF protocols.

The following set of statements would arguably give you a better idea about what TTCN-3 is and is not:

  • TTCN-3 is a suite of technologies related to each other allowing end users and test system vendors to create complete test systems. It was developed by ETSI that maintains TTCN-3 official home page.
  • TTCN-3 is best suited for black-box conformance testing of communicating systems. It was not specifically designed for performance testing, although performance extensions to the language are actively researched. TTCN-3 is well complemented by interoperability testing.
  • TTCN-3 core language is a convenient interchange format for collaborative development of large test suites performed by multiple organizational entities, such as companies, research institutions, subcontractors and so on. Combined together with standard APIs such as TRI and TCI, it facilitates distribution and coordination of work between team members, making large-scale test projects manageable and complex test systems feasible to implement within time and budget, due to the fact that the work can be split into well-defined subtasks with clear boundaries within the team or partner organizational entities.
  • TTCN-3 defines a dictionary of commonly used and agreed terms. Once you learn the dictionary and concepts behind the terms, you can start expressing yourself in powerful yet compact ways within the team of members who know the same set of terms. You start speaking common language with them, and you all save time discussing project details in more clear terms.
  • TTCN-3 is not a general-purpose programming language. Despite it having all common control flow constructs like for, while, if, and natural ways to express test behaviour through function calls, variables etc, it also has very unique concepts that need to be studied to understand TTCN-3 and that cannot be found in any other language.
  • TTCN-3 has a long history. Development of its predecessor, TTCN-2, started in 80-s, and TTCN-3 inherits all the good concepts from TTCN-2, it just makes them easier to use by having TTCN-2 streamlined, unnecessary or heavyweight constructs and concepts thrown away and syntax simplified. TTCN-2 was also very OSI-centric, while TTCN-3 takes into account needs of the Internet community. If TTCN-2 syntax can be compared to that of HTML in complexity (hence you needed editors for TTCN-2 much like you needed FrontPage or Dreamweaver for HTML), TTCN-3 syntax can be compared to C or Java, i.e. it looks much like a "normal" general-purpose programming language from syntactic viewpoint. In fact, many syntactic constructs of TTCN-3 are borrowed from Pascal and C, some from VB, and regular expressions and type system is borrowed from ASN.1. Hence, you need only your favourite text editor to start developing test cases at the very extreme. On the other hand, modern tools like OpenTTCN Tester 2010 come bundled with IDEs that help developing and executing test cases in a visual environment. OpenTTCN Tester 2011 scheduled for release in the first quarter of 2011 also includes additional facilities like debugger of TTCN-3 source code and GFT log viewer.
  • Examples of successfully deployed test systems that use TTCN-3 internally as their test scripting engine are: SIP, WiMAX.

Getting started

Code example

Consider the following excerpt of TTCN-3 code. Line numbers are for convenience only and are not an integral part of the code:

 1 module MyModule
 2 {
 3
 4 type port PortType mixed { inout all; }
 5
 6 type component MainComponent
 7 {
 8     port PortType p;
 9     var integer compVar := 5;
10     const boolean compConst = true;
11     timer T_WAIT := 1.0;
12 }
13
14 type component TSI_Type
15 {
16     port PortType tsiPort;
17 }
18
19 testcase TC_send() runs on MainComponent system TSI_Type
20 {
21     map(mtc:p, system:tsiPort);
22
23     p.send(10);
24     T_WAIT.start;
25
26     alt
27     {
28         [] p.receive(10)
29         {
30             setverdict(pass);
31         }
32
33         [] p.receive /* any */
34         {
35             setverdict(fail);
36         }
37
38         [] T_WAIT.timeout
39         {
40             setverdict(fail);
41         }
42     }
43 }
44
45 }

There is much to tell already to explain what the test case TC_send in module MyModule does and how it does it. Good news is that TTCN-3 is not much more complex than this. OK, just a bit more complex maybe, but once you understand the code above, you will know enough already about TTCN-3 to start writing your test cases.

Conceptual TTCN-3 test system

Before we jump into explaining what every line of the excerpt above means, we need to have a bigger picture about how TTCN-3 based test systems work. Figure 1 gives you an idea. Some abbreviations we need to decipher here are:

(thumbnail)
Figure 1: Conceptual TTCN-3 test system
  • MTC - main test component
  • PTC - parallel test component
  • TSI - test system interface
  • TE - test execution
  • SUT - system under test
  • p, tsiPort - communication ports

You typically write a TTCN-3 module like the one we saw above, then translate it with a TTCN-3 translator (colloquially often referred to as TTCN-3 compiler) into C or Java source code or into some proprietary bytecode, implement an adapter in a general-purpose programming language and compile it, too, using your favourite C, C++, C# or Java compiler, then bundle the whole thing together, and then you are prepared to run it, i.e. execute test cases.

If you got a TTCN-3 compiler generating C or Java, you end up having an executable (in OS terms) generated from your TTCN-3 source that is linked against some proprietary TTCN-3 runtime library. If you got a TTCN-3 interpreter, you end up having some form of Java-looking bytecode that is interpreted by a TTCN-3 virtual machine, much like JVM interprets Java classes or jar files.

No matter what is the approach, you end up having some form of executable TTCN-3 code; this is called ETS what stands for executable test suite, as opposed to ATS (abstract test suite) which needs to be compiled and bundled with an adapter before it can be run. ETS is executed by TE, what in real terms means proprietary TTCN-3 runtime library in case of a TTCN-3 compiler or JVM-looking virtual machine executing Java-looking TTCN-3 bytecode in case of a TTCN-3 interpreter.

Ways of running executable TTCN-3 code are vendor-specific.

MTC and 'runs on' clause

Now we get back to our example. What happens when execution of the TC_send test case is started by TE? The very first thing that happens implicitly in the beginning of every test case, even before the very first statement of the test case is reached, is that MTC and TSI are instantiated. As we talk about TSI instantiation and its role a bit later, we will now focus on MTC and its role.

MTC type is referenced in the 'runs on' clause of the test case declaration, and in our example it is MainComponent type. So an instance of MainComponent type is created in the very beginning of test case execution. This also means that component variables, timers and ports defined in the MainComponent type are instantiated. Variables are also initialized to their initial values if variable initializer is present.

Understanding what implications presence of the 'runs on' clause has for a test case is instrumental in understanding the founding principles behind TTCN-3 dynamic behaviour. The 'runs on' clause and its associated component type can be viewed in two ways:

  • it defines a main thread of execution for a test case, an MTC (what can be implemented as a separate process by a tool). Note that a test case can dynamically create more threads of execution running in parallel, known as PTCs.
  • it opens a visibility scope for component variables, constants, timers and ports. All these items that we will often collectively refer to as simply 'component variables' are defined in the component type and are instantiated when the MTC (i.e. relevant component type mentioned in the 'runs on' clause) is implicitly instantiated in the beginning of a test case, as we recall. So all these component variables become visible inside the functional entity that declares a 'runs on' clause. In other words, a function, test case or altstep that has 'runs on' in its declaration can use variables from the relevant component type instance by simply referencing them using unqualified form. This is how we access component timer T_WAIT in TC_send on line 24 in our example. Note that it is declared on line 11 inside declaration of the MainComponent type and it is made visible inside statement block of the TC_send test case by having MainComponent type referenced in the 'runs on' clause of that test case on line 19. Note that we use unqualified form of T_WAIT reference on line 24.

Having component variables is a TTCN-3 way of having variables that can be reused within the same thread of execution. If you are familiar with C++, you could think of a 'runs on' clause as a way to tell the compiler that some kind of 'this' or 'self' pointer is implicitly passed to the function, test case or altstep that uses 'runs on', much like it happens for class instance methods in C++. And this 'this' pointer refers to a local component instance, MTC or PTC. Local component instance remains the same within the same thread of execution, so if function A() calls B(), and they both have 'runs on' referring to the same test component, then they operate on the very same set of component variables (same 'this' pointer you could think), so changing a value of component variable in B() would lead to having its value updated also in A(). In a way, you could think of a functional entity having 'runs on' in its declaration as an instance method of component "class" mentioned in that 'runs on' clause in OOP terms that operates on an instance of that "class". Then again, functional entities without 'runs on' can be viewed as global, non-OOP, C-style functions. Note that it is mandatory to have 'runs on' for every test case declaration.

TTCN-3 has no notion of global variables, and it has good reasons to do so. Whenever you want to have a global variable, you need to define a component variable instead. And there is no way to have a variable that is shared between multiple threads of execution, that is, test components. This is because test component is seen as a self-contained entity and partially because implementation choices for a test component include implementing it as a process with its own address space. There are tricks and workarounds how to do sharing of variables between multiple test components, but there is no standard way to do this at the core language level. All communication between test components is supposed to happen through ports by sending and receiving messages, not through shared variables.

It is important to understand what a lifecycle of MTC, PTCs and their contained component variables is. When a test case is started, MTC mentioned in its 'runs on' clause is created implicitly, and the dynamic behaviour of the test case defined in the test case statement block starts running on that MTC. PTCs can be created from inside MTC or from inside other PTCs dynamically, much like you can create new threads and processes in a general-purpose programming language. When test case execution is finished, either normally or due to an error, MTC and all PTCs are implicitly destroyed and all their resources are released.

TSI and adapter

Now when we talked about MTC, we need to talk about another source of initial confusion, TSI. Recall that we had TSI_Type referenced on line 19 in the 'system' clause in our code example. While having 'system' clause is syntactically optional, omitting it is identical to saying that TSI is of the same type as MTC type, so TSI type is always present in the test case declaration, either explicitly or implicitly.

If you ever were a fan of Gamma's design patterns, telling you that TSI does for a collection of test components what Facade does for a collection of smaller and heterogeneous APIs would help. TSI interface hides from an adapter the implementation details of a test suite and of test execution mechanics, like interactions of multiple test components and details of their dynamic behaviour like variable assignments that are not externally observable. All these details are hidden behind a single and simple interface. From the perspective of an adapter, exact configuration of a test case is transparent, including the number of instantiated test components and their interactions with each other. All what an adapter knows about TE is the TSI interface that hides test components behind its back much like a firewall would protect computer nodes in the intranet from the outside world.

Conceptually there is only one TSI instance per test case which is shared among multiple test components, so TSI is also a Singleton. TSI is created implicitly when a test case is started before the very first statement and it is destroyed implicitly when the test case execution is finished.

Another way to think of TSI is to say that TSI is a middleman between test components and an adapter. As we have seen on Figure 1, designers of TTCN-3 adopted a somewhat layered design. As you go from top to bottom, your test system becomes more and more concrete in what it does. Behaviour of test components is rather abstract if taken in isolation from the rest of the test system. Test components operate on abstract values, messages, and communication ports. TSI is a glue between abstract TE and concrete adapter. Adapters do very concrete things in a way that they are responsible for encoding abstract TTCN-3 values into concrete communication protocol frames and for sending these frames to the SUT over real communication channel. Same happens when receiving a frame from the SUT. Adapter decodes it and forwards the decoded abstract value to one or many test components through the TSI interface.

So not only the adapter is isolated from the TE by dealing with it exclusively through TSI, but also abstract test suite in compiled form that is running in TE on test components is freed from the necessity to do coding and decoding and taking care of all the details of the network interface code. This makes it possible to split complex test projects into well-defined parts, when one part of the team is responsible for writing an abstract test suite, and another is responsible for the adapter implementation that makes this abstract test suite concrete.

You could also think of TSI as a special "lightweight" case of a regular test component instance. In fact, definition of a TSI type looks identical to definition of a regular component type from syntactic viewpoint, although there are semantic differences. For example, the only thing that matters in a definition of TSI type is a collection of defined ports, as this is the only thing that gets instantiated when a TSI type get instantiated. Component variables and timers are not instantiated, because there is no TTCN-3 dynamic behaviour that is associated with a TSI type instance. In this respect, TSI differs from a regular test component that usually has dynamic behaviour in the form of a statement block associated with it. So while regular test component represents a thread of execution in addition to being a container of ports, timers and variables, TSI merely acts as a container of ports without its own thread of execution attached to it.

Mapping of ports

Our code example on line 8 contained a definition of port p in component type MainComponent used as an MTC in TC_send and on line 16 it contained a definition of port tsiPort in component type TSI_Type used as a TSI in TC_send. Note that it is possible to define multiple ports per component type. Now we need to tell the TE how ports in MTC relate to ports in TSI, so that a message sent to a specific port in MTC would be observed in the adapter as a message sent over corresponding TSI port. Recall that the adapter does not see anything on the test execution side but TSI, and TSI is merely a collection of ports.

You could think of a communication port in a test component or TSI as a border crossing point. Traffic in the form of messages or other kinds of communication primitives is allowed to flow only through such border crossing points. You can cross a border between two countries only at certain points specifically equipped for that, right? For network programmers, analogy between a port and a socket may also be helpful.

(thumbnail)
Figure 2: Communication over mapped ports

The process of establishing a binding between an MTC port and a TSI port is called port mapping. Such binding can be established dynamically using TTCN-3 map keyword, as we have seen on line 21 of our code example that maps MTC port p to TSI port tsiPort. You could think of it so that when port mapping is established, the two ports are merged and become one, so sending a message outside of a test component port is seen by an adapter as having the message sent outside of the relevant mapped TSI port. When an adapter forwards a message received from an SUT to a TSI port, it is inserted into port queue of the relevant mapped test component port. This is in contrast to connecting ports that belong to different test components, in which case a duplex communication channel between the connected ports is established, so a message sent over one port in a pair is received by the peer port and is inserted into its message queue.

Figure 2 illustrates the mapping of an MTC port to a TSI port that was established after line 21 of our code example has been executed. This figure is in fact an excerpt from Figure 1. It is also possible to map PTC ports to TSI ports and to connect ports of different test components to each other, as Figure 1 illustrates.

Sending message

(thumbnail)
Figure 3: Message m sent to the SUT

Let's see what happens in the test system when we instruct TE to send a message to the SUT. This is done when p.send(10) statement on line 23 of our code example is reached during test execution. Figure 3 illustrates the path that the message traverses on its way to the SUT. In our example the message is simply an integer value 10, but it could be a far more complex constructed value containing tens of fields and in real test suites it is. Most interesting and characteristic control points of the send instruction execution are numbered on Figure 3 and these numbers put inside circles are explained below.

  1. Executor, a TTCN-3 virtual machine or runtime library, reaches statement p.send(m) while executing statements of the TTCN-3 code, where m is a message represented as abstract value that is to be encoded and sent to the SUT as a concrete frame of data.
  2. Abstract message m leaves port p of the MTC.
  3. Because port p is mapped to TSI port tsiPort, message m goes through tsiPort of TSI.
  4. Before we can send a message to the SUT, we need to encode the abstract value m into concrete bit pattern m-enc that represents a final frame or the portion thereof. This is done by calling used-implemented tciEncode() function. User who implements an adapter sees this as a callback invocation. User shall implement a callback handler tciEncode() in C or Java. When the user-defined callback handler is invoked by the framework, it shall encode abstract value m and return the result of encoding as m-enc. Encoding and decoding is implemented using standard TTCN-3 TCI-CD API that allows dynamic introspection and construction of abstract TTCN-3 values. This API has standardized mappings for C and Java.
  5. After message is encoded, user-implemented callback handler for triSend() is called. User shall provide implementation of triSend() callback handler. It typically performs preparation of a final frame to be sent to the SUT and sending it over physical communication medium. For example, a UDP frame is prepared and it is sent over UDP to the SUT.
  6. In case of UDP, OS sending code does not wait until the frame reaches its destination, so it returns early.
  7. User-implemented triSend() callback handler returns a status code back to the framework that called it. Error status can be returned to indicate an error, in which case a test case error is reported to the end user running the test case and the test case is terminated.
  8. Execution of p.send(m) instruction is finished, so TTCN-3 executor can proceed to execution of subsequent statements.

Note the separation of code responsible for encoding and decoding from the network interface code responsible for sending and receiving.

Receiving message

(thumbnail)
Figure 4: Message m received from the SUT

After we have seen what happens when a message is sent from the TE to the SUT, let's see what happens when the test adapter receives an incoming frame from the SUT. SUT adapter (SA) is usually implemented so that there is some kind of listener thread waiting for incoming frames to arrive from the SUT. When a frame arrives, the listener thread forwards it to the TE.

Figure 4 illustrates the path that the message traverses on its way to the TE. Characteristic control points passed by the message are numbered and are explained below.

  1. SUT sends a data frame to the communication peer which is simulated by the test system. If the frame is sent over unreliable datagram service like UDP, SUT implementation does not wait until the frame is actually delivered to the remote party, so sending code on the SUT side returns early.
  2. Frame sent by the SUT is received by the SA adapter listener thread, so the sleeping thread wakes up. Listener thread is typically implemented by the end user.
  3. SA adapter listener thread forwards the received frame in encoded form as m-enc to the TE. This is done by invoking triEnqueueMsg() function that is provided by the framework to the end user.
  4. The framework calls used-implemented tciDecode() callback handler. End user code sees this as a callback invocation of tciDecode() performed by the framework. The purpose of tciDecode() is to decode encoded message m-enc into abstract TTCN-3 value. This decoded value is returned back to the caller (framework) as m.
  5. The decoded message is forwarded by the framework to the TSI port tsiPort.
  6. Because tsiPort is mapped to the MTC port p, message m goes through tsiPort and it is appended to the end of the message queue of the MTC port p. Message queue of a port in a test component is organized as a FIFO queue. Messages are inserted into this queue by an adapter and they can be removed from top of the queue by the TTCN-3 receive statement.
  7. Standalone TTCN-3 p.receive(T) statement works roughly as follows: TTCN-3 executor (a runtime library or virtual machine) peeks message m from top of the message queue of the MTC port p without removing it from the queue. If the actual received message m matches template specification defined by T, message m is removed from the port queue and TTCN-3 code execution proceeds forward. If there are no messages in the port queue by the time the receive statement is reached and executed, the executor blocks on the receive statement (goes to sleep) until some event happens in the TE, for example a new message arrives to the port queue, in which case the executor wakes up and re-evaluates the receive statement where it is at the moment. If message m does not match T, this may lead to a deadlock, because the receive statement always deals with the top message of the port queue, and a message can be removed from top of the queue only by a receive statement that matches it. While there are many more details about how the receive statement actually works, this should give you a quick start with it.

'Alt' construct

Have you noticed this magic alt keyword on line 26 of our code example? Ever wondered what it could mean? We did, actually.

The alt construct on line 26 enumerates the list of possible events that according to the test suite writer judgement can happen in the test system by the time when the corresponding alt construct is reached and handling of which should be made explicit in his or her opinion. These events, collectively known as receiving operations, are defined as a list of alternatives, each after its own alt guard []. Usually only one of these events actually happens at a time. Each alternative usually has a statement block attached to it that is used as an event handler. If an event matching one of the alternatives in the list actually occurs in the test system and it is indeed matched by that alternative in the due course of evaluation of the containing alt construct, then the statement block associated with the alternative in question is executed and the alt construct is exited.

For example, if we send message 10 and we expect that the correct response should also be 10, we can set the pass verdict in a statement block that is attached to the corresponding [] p.receive(10) alternative, as we do on line 30 of our code example. If any other response is considered to be incorrect, we can set the fail verdict in a statement block that is attached to the corresponding p.receive alternative, as we do on line 35 of our code example. In this way, if a message is received, and it is not 10, then the alternative p.receive on line 33 will match, as it catches just any received message, no matter what its type and actual content is.

Timers and timeouts

Remember we started timer T_WAIT on line 24 of our code example. Timers are useful to limit maximum duration of certain activity, for example maximum time that can pass after a request was sent and before a response is received. When timer expires, a timeout event is fired, and it can be caught by the timeout receiving operation. This helps us avoiding deadlocks in the test suites. If we did not add a timeout event handler on line 38, our test case could get stuck in the absence of a response from the SUT. Adding a timeout handler makes sure that if a response is not received within one second, then the timer T_WAIT will expire, in which case the T_WAIT.timeout alternative on line 38 will match, its statement block will get executed and the containing alt construct will be exited.

Setting verdicts

Test verdict can be set during test execution using the setverdict instruction. Possible verdicts are: none, pass, inconc (inconclusive), fail, error. Verdicts are case-sensitive and shall be written all lowercase:

  • none is implicitly assigned in the beginning of every test case by default and is reported as a final verdict in the absence of any other verdict assignment during the test case execution;
  • pass means that everything is OK;
  • inconc means that neither pass nor fail can be reliably assigned, for example due to a network connection failure;
  • fail means that something definitely went wrong;
  • error means that there was an error in the test harness; this verdict cannot be assigned by the end user directly.

The list of verdicts above was presented in the order of their strength, weakest first. When a final verdict is assigned for a test case, the strongest verdict ever assigned during the test case execution is selected as a final verdict. For example, if you assign fail using setverdict(fail) and then pass using setverdict(pass), then fail will be selected as a final verdict irrespective of the fact that pass was assigned later.

Tools will report test case final verdict and test campaign final verdict statistics in vendor-specific ways.

Code example concluded

So much we had to say to explain our code example! 45 lines of code, including empty lines, generated quite a wealth of explanations!

Let us now revisit our example to make sure we understood well what is going on there:

  • on line 20 MTC and TSI are implicitly instantiated: MTC ports, timers and variables are instantiated and TSI ports are instantiated
  • on line 21 MTC port p is mapped to TSI port tsiPort
  • on line 23 abstract message m (this time it has value 10) is sent to the SUT through MTC port p
  • on line 24 timer T_WAIT is started with the default duration of one second (see the timer declaration on line 11); it limits maximum time we can wait for a response from the SUT before a timeout is caught on line 38
  • on lines 26 - 42 we wait until one of the following events happens: (a) message having value 10 is received from the SUT (line 28), (b) just any message is received from the SUT (line 33), except 10 that is caught in the previous alternative already, (c) timer T_WAIT expires (line 38). Each alternative that catches the corresponding event has a statement block attached to it that is executed if the corresponding alternative matches.
  • on line 43 MTC and TSI and all their contained ports, variables and timers are implicitly destroyed and a final test case verdict is implicitly assigned

This is it! Congratulations, now you can start writing simple TTCN-3 code! To become more knowledgeable about TTCN-3 and its essentials, however, keep going.

Essentials of TTCN-3

Before we dive into learning TTCN-3 concepts and constructs, let's find out what we need to learn about TTCN-3 and why. To answer this question, let's first answer another one: what kind of things do we need in our test suite to make it work and to implement a test system if we are presented with a protocol specification to be tested? Recall that we are doing black-box conformance testing. Brief research of the topic gives us this rough list of items in the checklist:

  • protocol frame formats need to be mapped to type definitions that are best represented in abstract form for test suite readability and maintainability reasons
  • frame values participating in frame exchanges can be most conveniently represented as a library of abstract parameterizable template values. We talk about parameterizable template values, because you can usually easily identify regular repeating patterns in values you send and receive, and these patterns can be generalized using value parameterization, thus creating a library of reusable value templates.
  • you need to be able to describe your test setup or test configuration in abstract terms. Usually you know how many communication endpoints you have in a particular test configuration and what is the semantics of each endpoint.
  • you may want to be able to specify libraries of useful functions and reusable dynamic behaviour descriptors
  • naturally you want to be able to define test cases and their dynamic behaviour
  • additionally, you may want to be able to define in which order test cases are executed and which test cases are selected for execution for a particular SUT and test environment
  • as characteristics of the SUT and exact test environment may vary, it would be convenient to write a test suite in a generalized fashion that uses test suite parameters (also known as module parameters); actual values of these parameters that are individual for each combination of test setup and SUT are then provided last minute by the end user of the test system right before actual test execution commences
  • if you ever used multiple threads and processes in your programs written in general-purpose programming languages, you would appreciate a convenience of having an ability to specify concurrent dynamic behaviour
  • splitting a large test suite into multiple modules, one file per module, can streamline test suite development and make it more manageable when done in a team
  • you would also appreciate a convenience of having most of the control flow constructs that you can find in any other general-purpose programming language, as well as other "must have" things like variables, formal parameters, parameter passing by value and by reference and so on.

All these items bring us to an idea of test suite engineering driven by protocol specification. Let us see how all these requirements are addressed by TTCN-3 and how real test suites are usually structured so that definitions of the same class are grouped together, typically in the same module.

Classes of top-level definitions

TTCN-3 gives you an ability to define your test suite as a collection of multiple modules. Each module is usually put into its own file. It is customary to establish a correspondence between module names and file names, so that for example module ModuleA is put into file ModuleA.ttcn. Example of one such module we have seen already in our code example from the previous chapter. That module was a self-contained one, meaning that it had all the definitions necessary to actually run a test case contained in it and it did not import any definitions from any other module.

Each TTCN-3 module may contain certain classes of definitions at its top level (hence the terminology top-level definitions, module definitions, global definitions or simply TLDs for short). This is illustrated by Table 1.

Table 1: Classes of module definitions
TLD class Example What it does
type
type record Msg
{
    integer typeCode,
    charstring payload
}
Defines a value type similarly to strongly typed general-purpose programming languages, for example struct or typedef in C.
signature
signature AllocateChannel
(
    in charstring phoneNo,
    inout integer quality,
    integer options,
    out O_2 allocatedChannelId
)
return integer
exception
(
    CommunicationFailure,
    charstring,
    NoSuchPhoneNumber,
    GeneralFailure
);
Defines a function prototype for procedure-based communication. Remote procedure calls are simulated in TTCN-3 as frame exchanges in the form of call-reply pairs, with possibility of raising an exception by the called party instead of sending a reply.
port type
type port PortType message
{
    inout Msg1, Msg2;
    in Msg3;
    out Msg4;
}
Defines type of a communication endpoint known as a port. Port typedef defines class of a port (message-based communication, procedure-based communication, or both). It also restricts types of messages that can go in and out of the port.
component type
type component ComponentType
{
    port PortType p;
    var integer compVar := 5;
    const boolean compConst = true;
    timer T_WAIT := 1.0;
}
Defines test component type with contained ports, variables, constants and timers.
constant
const float PI := 3.14159265;
Defines a global constant.
external constant
external const integer PROTOCOL_MINOR_VERSION;
Declares a constant defined outside of the TTCN-3 test suite. Exact mechanism for provisioning of external constants is vendor-specific and is outside of the scope of the TTCN-3 standard.
module parameter
modulepar charstring SUT_IP_ADDR;
Defines a test suite parameter provisioned by the end user before the commencement of a test campaign.
template
template Msg Msg_40_s(charstring payloadPrefix) :=
{
    typeCode := 40,
    payload := payloadPrefix & "Hello, world!"
}
Defines effective or parameterized value for messages sent to the SUT, or defines a class of values expected from the SUT.
function
function calculateSquare(float x) return float
{
    return x * x;
}
Defines a function.
external function
external function md5sum(charstring content) return charstring;
Declares a function defined outside of the TTCN-3 test suite. External functions are implemented in a platform adapter (PA) by the end user using TTCN-3 TRI PA standard API (available in C and Java).
altstep
altstep DefaultAltstep() runs on ComponentType
{
    [] any port.check
    {
        setverdict(fail);
        stop;
    }
    [] T_GUARD.timeout
    {
        setverdict(fail);
        stop;
    }
}
Used to define a reusable library of recurring patterns of expected SUT behaviour or other test system internal events.
test case
testcase TC_basic() runs on ComponentType system TSI_Type
{
    preamble();

    p.send(Msg_40_s);
    p.receive(Rsp_200_OK_r);
    setverdict(pass);

    postamble();
}
Defines a test case.

TTCN-3 external keyword has little resemblance to the extern keyword in C. It simply states that the relevant declaration is defined outside of the TTCN-3 suite, not that it is defined in some other TTCN-3 module. There is no concept of separation into header files and source files in TTCN-3 that can be found in C or C++. In this respect TTCN-3 is closer to Java than to C or C++. Similarly to Java, TTCN-3 also allows forward references to declarations defined later in the same module.

Special module-level constructs

In addition to various classes of top-level definitions, there are several other special kinds of constructs that can be present in a module at the top level as illustrated by Table 2.

Table 2: Special constructs in TTCN-3 module
Construct Example What it does
control part
control
{
    execute(TC_basic_1());

    if (PC_FEATURE)
    {
        execute(TC_basic_2());
    }
}
The control part defines a sequence of test cases to be executed. It's like an entry point to the program, similar to main() in C, which is useful if you do not want to execute a sequence of test cases directly in some vendor-specific way. It also allows to select or deselect certain test cases for execution depending on the values of test suite parameters.
attributes
type record RecType
{
    bitstring field1,
    octetstring field2
}
with
{
    encode "PER:1997",
    variant "UNALIGNED"
}
Attributes are handy if you want more power in specifying encoding rules and encoding variations for your types and values. While definition of attributes usually has not direct effect on the test suite itself, it may greatly simplify writing a TCI-compliant codec or even help automate codec construction.
group
group A
{

const integer CONST_1 := 256;
type octetstring O_2 length(2);

group B
{
const integer CONST_2 := 512;
type octetstring O_3 length(3);
}

}
Grouping of definitions can be done for readability reasons. It may also help you better organize your definitions in the test suite. GUI front-ends may make use of grouping information to make rendered lists of test cases look more structured. Apart from the fact that a 'with' statement can be attached to a group, groups have no scoping or any other effect on the contained definitions. In particular, top-level definition identifiers still have to be unique throughout the whole module even if they are put inside a group. Groups can be nested, i.e. a group can be present inside another group.
import
import from ModuleA all;
import from ModuleB { type MyType; }
Makes imported symbols visible in the importing module. Set of imported symbols is determined by the import filter, an expression defined in the import statement.

Typedefs, literals, and value notation

Apart from component and port typedefs, regular typedefs look very similar to what you can find in a general-purpose programming language like C or Pascal, with several exotic additions aimed at harmonizing TTCN-3 with ASN.1. Definition of a regular value type in TTCN-3 looks very similar to typedef in C and it has similar semantics. Unlike C typedef, you always do introduce a new symbol when you define a type or subtype using the TTCN-3 type keyword. This, however, does not imply that values of a subtype are incompatible with values of the base type. Strict type equivalence is required only for matching a template against a received value. Various typedef classes in TTCN-3 are illustrated by Table 3.

Table 3: Value type classes in TTCN-3
Type class Example typedef Example value Equivalent ASN.1 type
Basic types
integer
type integer MyInt;
10
INTEGER
float
type float MyFloat;
10.0
REAL
boolean
type boolean MyBool;
true
BOOLEAN
bitstring
type bitstring MyBitstring;
'1101'B
BIT STRING
hexstring
type hexstring MyHexstring;
'AF3'H
-
octetstring
type octetstring MyOctetstring;
'AF31'O
OCTET STRING
charstring
type charstring MyCharstring;
"Hello, world!"
IA5String
char (obsolete)
type char MyChar;
"A"
IA5String(SIZE(1))
universal charstring
type universal charstring MyUC;
"Ελληνικά"
UniversalString
universal char (obsolete)
type universal char MyUChar;
"λ"
UniversalString (SIZE(1))
objid
type objid MyObjid;
objid { itu_t(0) 4 etsi(0) }
OBJECT IDENTIFIER
Constructed types
record
type record MyRecord
{
   BIT_4 protVersion,
   integer typeCode,
   charstring payload
}
{
    protVersion := '0010'B,
    typeCode := 25,
    payload := "Hello, world!"
}
SEQUENCE
record of
type record of integer MyRoi;
{ 1, 2, 3, 4, 5 }
SEQUENCE OF
set
type set MySet
{
   BIT_4 protVersion,
   integer typeCode,
   charstring payload
}
{
    typeCode := 25,
    payload := "Hello, world!",
    protVersion := '0010'B
}
SET
set of
type set of integer MySoi;
{ 2, 3, 4, 1, 5 }
SET OF
union
type union MyUnion
{
    integer variant1,
    BIT_1 variant2,
    OCT_2 variant3
}
{
    variant2 := '1'B
}
CHOICE
enumerated
type enumerated MyEnum
{
    E_REQ,
    E_IND(1),
    E_ERR
}
E_IND
ENUMERATED
Special value types
verdicttype

-

pass
-
default

-

- -

It is possible to define a subtype of basic or user-defined type like this:

type integer MyInt;
type MyInt MyInt2;
type MyRecord MyRecord2;

Several special type identifiers are reserved in TTCN-3, namely address and anytype.

Integer, float, boolean

There is really nothing new about integer, float and boolean types. Although TTCN-3 integers in theory are unbounded, in practice they are restricted to a 64-bit physical representation (as of most major tool vendors), and the standard Java mapping of the TCI interface further restricts integers to 32 bits by mapping them to Java int type. C mapping of the TCI interface does not have this limitation.

TTCN-3 does not impose any requirements concerning storage of values of the TTCN-3 float type in memory. Java mapping of the TCI interface effectively maps TTCN-3 float to Java float type. C mapping maps it to C double type.

The TTCN-3 standard does not define any implicit conversions between variables of cardinal types. In particular, you cannot use integers and booleans interchangeably as you used to do it in C. In this respect TTCN-3 behaves the same way as Java that requires strong typing for boolean expressions. You also cannot perform conversion between integer and float values implicitly, you need to explicitly use TTCN-3 predefined functions int2float() and float2int() if you want to do so.

Charstring

Charstring literals are also quite common in other languages. Because TTCN-3 has no pointers, you do not have to care about memory allocation and deallocation issues for character strings like you would in C. In this respect, handling of character strings in TTCN-3 is closer to interpreted languages like Java, Python and PHP rather than compiled languages like C or C++ (of course, we also have std::string in the C++ STL these days that lets us manipulate strings as easily).

You can concatenate strings in TTCN-3 using & operator like this: "Hello, " & "world!" (was this adopted from VB). It is also possible to access individual elements of a string using array subscript notation, like in myStr[i]. Prior to TTCN-3 Edition 3, the return value of the subscript operator would be of char type, but as of revision 3.2.1 it returns the value of charstring type containing exactly one character. This new behaviour is consistent with the semantics of the array subscript operator for bitstrings, hexstrings and octetstrings.

TTCN-3 does not define any complex escaping schemes for character string literals like C does. To represent a double quote inside a character string you need to duplicate it, thus character string literal written as "Hi ""Junior"" Lennon" yields effective value Hi "Junior" Lennon. If you want to represent a non-printable character like CR or LF, use predefined function oct2str() as illustrated by the example below:

const charstring CR := oct2str('0D'O);
const charstring LF := oct2str('0A'O);
const charstring CRLF := CR & LF;

var charstring v := "This is line #1" & CRLF & "This is line #2";

Bitstring, hexstring, octetstring

Bitstrings, hexstrings and octetstrings were introduced to have a proper built-in support for ASN.1 and bit protocols.

Bitstring literals may only consist of ones and zeros like in '1101'B.

Hexstring literals shall consist of hexadecimal digits like in 'AF3'H and may contain even number of hexadecimal digits.

Octetstring literals are similar to hexstrings, except that an octetstring literal must always consist of an even number of hexadecimal digits like in 'AF30'O, hence the smallest building unit of an octetstring is an octet consisting of two hex digits. This affects:

  • the way how array subscript operator works for octetstrings; e.g. if octetstring variable myOS contains '1215ACA8EF'O, then myOS[3] will access octet 'A8'O, the third octet in the octetstring counting from zero;
  • the way how length is calculated for octetstrings; given the example above, lengthof(myOS) will return 5 (number of octets), not 10 (number of hex digits); note that lengthof('1215ACA8EF'H) will return 10, because this time we have hexstring as an argument, so length is returned in hex digits;
  • the way how matching symbols are interpreted; for example, '15??27'O will match '15ACBC27'O and '15182C27'O and it will not match '15AC27'O and '151827'O; thus a question mark matches the whole octet (two hex digits), not just a single hex digit.

By defining abstract messages using these three types, you have a convenience of manipulating abstract data at the bit level. It is also not uncommon to have a straightforward direct encoder that iterates elements of an abstract value of constructed type depth-first and appends every leaf value of primitive type to the resulting encoding "as is".

For example, '1101'B will get directly encoded into bits 1101, 'AF3'H will get encoded into bits 1010 1111 0011, and 'AF35'O will get encoded into bits 1010 1111 0011 0101. Hence, given the following example of an abstract value containing two nested records (record is like a struct in C):

{
    field1 := '1101'B,
    field2 := '000'B,
    field3 := '1'B,
    field4 :=
    {
        field41 := 'AF3'H,
        field42 := '5'H,
        field43 := 'C7'O
    }
}

we will have the following bit stream after direct encoding is applied to the abstract value above:

1101 0001 1010 1111 0011 0101 1100 0111

and if the same encoding is represented in octets, we obtain:

D1 AF 35 C7

As we can see, having messages of bit protocols defined using bitstrings, hexstrings and octetstrings may greatly simplify implementation of the encoder. It may also simplify decoder implementation.

Objid

Objid literals are introduced to harmonize TTCN-3 with ASN.1 and to facilitate testing of protocols like SNMP that use object identifiers rather extensively. Exact definition of what an object identifier (OID) value is, still remains one of the most confusing parts of TTCN-3.

We would recommend that you think of an OID value simply as an ordered collection of integers. This seems to be the only sane way to think of OID representation in practical and definitive ways, especially for the purposes of value comparison. Optional identifier that can be attached to each OID element shall be treated as an informal non-normative definition of that element that eventually must be resolved to a concrete integer value in this way or that way.

To explain you why identifier attached to an objid element, if present alone without "backbone" supporting integer, cannot be generally regarded as a normative and definitive reference for that element, consider the following examples of objid notation, the third and the fourth of them being rather hypothetical:

// OK:
v1 := objid { itu_t(0) 4 0 ptcc(3) etsi_doc(117) }; // 1
v2 := objid { ccitt(0) 4 1 }; // 2

// TTCN-3 translator may not like the following two statements.
// v3 and v4 are identical and (v3 == v4) must return true, but
// how can we know for sure?

v3 := objid { itu_t 4 1 }; // 3
v4 := objid { ccitt 4 1 }; // 4

// This would be a consistent way to rewrite 3 and 4:
v3b := objid { itu_t(0) 4 1 }; // 5
v4b := objid { ccitt(0) 4 1 }; // 6

// This would be also OK:
v3c := objid { 0 4 1 }; // 7
v4c := objid { 0 4 1 }; // 8

// And also this would be OK:
const integer itu_t := 0;
const integer ccitt := 0;
...
v3d := objid { itu_t 4 1 }; // 9
v4d := objid { ccitt 4 1 }; // 10

// And even this would be OK:
const objid itu_t := objid { 0 };
const objid ccitt := objid { 0 };
...
v3d := objid { itu_t 4 1 }; // 11
v4d := objid { ccitt 4 1 }; // 12

// 5, 6, 7, 8, 9, 10, 11, 12 all mean the same thing,
// as they all resolve to the same value: objid { 0 4 1 }

As we can see, itu_t and ccitt identifiers in the context of being the first node of an object identifier value shall resolve to the same integer value 0, so for example two OIDs objid { itu_t 4 1 } and objid { ccitt 4 1 } are supposed to be identical, as they effectively resolve to the same value. In theory, knowledge about integer bindings of OID nodes is hard-coded in some sort of a global database that maintains "world" tree of OIDs and that is supposed to be accessible by applications willing to perform identifier resolution, much like you would resolve IP address of a host given its name using DNS. However, no ultimate and authoritative source of the full OID tree exists. Moreover, no protocol similar to DNS query mechanism is defined, effectively making consistent resolution of integer bindings of OID identifier elements from the world OID tree database a sweet dream that does not seem to ever come true. So unless you have itu_t and ccitt identifiers defined in your test suite elsewhere, you leave your tool vendor with the necessity of hard-coding all possible OID element integer bindings into the tool, a move that can hardly be deemed practical.

Having said that, we need to add to this that in cases when identifier has been defined earlier as a variable or constant and bound to an integer value, you can use this variable or constant reference in your OID value notation like that:

var integer myIntVar := 15;

// The following is OK, resolves to objid { 0 4 0 15 }:
var oidVar := objid { itu_t(0) identified_organization(4) etsi(0) myIntVar };

There is still one thing to keep in mind. ASN.1 allows overloading of the meaning of an identifier element in cases when it is the first element of an OID value notation. In addition to allowing it to refer to an integer constant, ASN.1 allows it to refer to another OID value. Consider the following example:

-- This is an excerpt from an ASN.1 module --

myIntConst INTEGER ::= 15

-- resolves to { 11 3 5 } --
myOidConst OBJECT IDENTIFIER ::= { 11 b(3) 5 }

-- resolves to { 11 3 5 4 3 15 15 } --
myOidConst2 OBJECT IDENTIFIER ::= { myOidConst 4 org(3) alt-org(myIntConst) myIntConst }

-- resolves to { 11 3 5 4 3 15 15 20 10 } --
myOidConst3 OBJECT IDENTIFIER ::= { myOidConst2 20 10 }

As we can see in the ASN.1 example above, when myOidConst is used as the first element of myOidConst2, it is regarded as a prefix for the latter, so the effective value of myOidConst2 is obtained by concatenating the list of integers defined by myOidConst and the list of integers defined by the remainder of myOidConst2 value notation.

Same overloading is also allowed in TTCN-3:

// This is an excerpt from a TTCN-3 module

const integer myIntConst ::= 15;

// resolves to objid { 11 3 5 }
const objid myOidConst ::= objid { 11 b(3) 5 };

// resolves to objid { 11 3 5 4 3 15 15 }
const objid myOidConst2 ::= objid { myOidConst 4 org(3) alt_org(myIntConst) myIntConst };

// resolves to objid { 11 3 5 4 3 15 15 20 10 }
const objid myOidConst3 ::= { myOidConst2 20 10 };

To sum up: write your test suites so that every element of your OID value notation can be resolved to an integer or list of integers using only the information available in the test suite alone, without the need to use external resources or magic values hard-coded into the tool. As you can see, there is a lot of ambiguity and confusion related to objid value notation, and the best we can do is to try to keep up with all this mess.

Record

Record is like a C struct. Record is defined as an ordered collection of fields. Fields of a record value must be defined in the same order as they were declared in the relevant type definition.

It is also possible to declare fields as optional in the record type definition, in which case a special omit value can be assigned to the optional field when the record value is defined. This is convenient if protocol specification says that certain fields may be absent from the frame.

It is also possible to have nested records.

Consider the following set of type definitions:

type record TypeA
{
    integer fld1,
    bitstring fld2 optional
}

type record TypeB
{
    integer f1,
    TypeA f2 optional
}

Possible values of type TypeB are illustrated by Table 4.

Table 4: Examples of record values
№1 №2 №3 №4 №5
{
    f1 := 1,
    f2 :=
    {
        fld1 := 5,
        fld2 := '11'B
    }
}
{
    f1 := 1,
    f2 :=
    {
        fld1 := 5,
        fld2 := omit
    }
}
{
    f1 := 1,
    f2 := omit
}
{ 1, { 5, '11'B } }
{
    f1 := 1
}

Value notation in which field names are omitted from the declaration is called value list notation, as illustrated by the example №4 of Table 4. Value list notation can be used only if the value is fully defined, i.e. all fields are assigned a value or omit, because only in this case the translator can find correspondence between elements of the value list and field names in the corresponding record type definition.

You may omit definition of certain fields from the value notation altogether, in which case relevant fields remain uninitialized, as illustrated by the example №5 of Table 4. This is not the same as assigning a special value omit to the field, as illustrated by the example №3 of Table 4. If some fields are not present in the value as in the example №5 of Table 4, you cannot use value list notation to define the value, because in this case you need to explicitly specify name of every field to avoid ambiguity.

It is possible to have record type definition with no fields, like this: type record EmptyRecType { }. Empty record values are also perfectly legal, like this: { }.

Set

Set is the same as record, except that fields can be defined in any order, not necessarily in the same order as they were declared in the corresponding type definition. This naturally prohibits use of value list notation to define set values, as we would have possibilities for ambiguity if it were allowed. Field with the same name may still not appear in the same set value more than once.

The fact that set is an unordered collection of fields affects the way how comparison and matching works for set values. This is illustrated by the following example:

...

type set MySetType
{
    integer f1,
    boolean f2,
    bitstring f3
}

...

var MySetType v1 := { f1 := 10, f2 := true, f3 := '1101'B };
var MySetType v2 := { f2 := true, f1 := 10, f3 := '1101'B };

var template MySetType t1 :=
{ f2 := true, f3 := ? /* matches any value */, f1 := 10 };

var boolean b1 := (v1 == v2); // result shall be true
var boolean b2 := match(v1, t1); // result shall be true

...

It is possible to have:

  • optional fields in a set like in a record
  • empty set type with no fields defined
  • empty set value even if the relevant set type is not empty, in which case all value fields are undefined

Record of

Record of is formally defined as an ordered collection of elements of the same type. It is tempting to say that record of is what is used to be an array in other languages, but this is not exactly correct. It is more realistic to think of record of as a growable array with some unusual properties. Because record of can be unbounded (and by default it is), here is what is a bit unusual about record of:

...

// This defines an unbounded growable array of integers.
type record of integer RoiType;

...

var RoiType v1;
var integer v2;

// At this point variable v1 is uninitialized. Indexing in arrays starts
// at 0.

v1[3] := 5; // #1

// After the assignment above, the variable v1 has automatically grown
// and it now contains { -, -, -, 5 }, where "-" in this context means
// "uninitialized", so 4 elements in total of which 3 are uninitialized.

v2 := v1[5]; // #2

// The above statement is disallowed and it shall produce a test case
// error, due to an out-of-bounds array element access in the context
// of r-value.

...

So if you use an out-of-bounds array index on the right hand side of the assignment or otherwise in the context of r-value, you will get a run-time error. If you use an out-of-bounds array index on the left hand side of the assignment or otherwise in the context of l-value, record of array will automatically grow on demand up to the size necessary to have enough space for the accessed array element.

Note that some tool vendors might have relaxed the requirement for throwing a run-time error in case of access violation in the r-value context, possibly to gracefully handle execution of badly written test suites, so instead of throwing a test case error in case #2 of the example above, v1 might grow much the same way as it did in case #1 above, disregarding its r-value context.

It is possible to have value notation for record, set, record of and set of mixed rather freely in the same value. Here are some more examples of record of value notation:

...

var RoiType v1 := { 1, 2, 3 },
            v2 := { 1, 2, 3, 4, 5 };

...

type record RecType
{
    integer f1,
    charstring f2
}

type record of RecType RecOfRecType;

...

var RecOfRecType v3 :=
{
    { f1 := 10, f2 := "AB" },
    { f1 := 20, f2 := "CD" },
    { 30, "EF" } // value list notation used for record fields
}

...

It is possible to have an empty record of value, like this: { }.

Set of

Set of is the same as record of, except that order of elements in the collection is insignificant, i.e. collection is unordered. This affects the way how comparison and matching operators work for set of, as illustrated by the following example:

...

type set of integer SoiType;

...

var SoiType v1 := { 1, 2, 3 };
var SoiType v2 := { 2, 1, 3 };

var template SoiType t1 :=
{ 2, ? /* matches any value */, 1 };

var boolean b1 := (v1 == v2); // result shall be true
var boolean b2 := match(v1, t1); // result shall be true

...

Despite set of being an unordered collection of elements of the same type, it is nonetheless possible to use array subscript operator for set of both in the context of l-value and r-value, in which case set of behaves identically to record of and internal in-memory ordering of elements is assumed for the purposes of array element access.

It is possible to have an empty set of value, like this: { }.

Union

TTCN-3 union is defined as a collection of named fields of which at most one is effective at a time. Union is much like a C union, except that TTCN-3 union value internally maintains information about which field is currently effective while C does not. In this respect TTCN-3 union is closer to CORBA IDL union. In fact, C does not have the concept of "effective" or "selected" union field at all, because in C all union fields overlap in memory. Unlike C, TTCN-3 does not have any provisions for the layout of a union value and the contained fields in memory, while, as we said, C mandates that all union fields share the same memory area. Remember, TTCN-3 has no pointers, hence no direct memory access and manipulation.

TTCN-3 union is a container capable of storing at most one field at a time. If we ask the same container to start storing content for another field, its memory about content of the previously stored field gets instantly lost and it cannot be recovered by accessing the same container later. This also means that, unlike in C, values of different union fields in TTCN-3 are completely independent of each other.

Here are some examples of union value notation:

...

type RecType
{
    integer fld1,
    octetstring fld2
}

type union UnionType
{
    integer f1,
    bitstring f2,
    RecType f3
}

...

var UnionType v1 := { f1 := 15 };

// now v1 contains { f1 := 15 }

v1.f2 := '1101'B;

// the above is equivalent to writing:
// v1 := { f2 := '1101'B }

// now v1 contains { f2 := '1101'B }

v1 := { f3 := { fld1 := 1, fld2 := 'AE'O }}

// now v1 contains { f3 := { fld1 := 1, fld2 := 'AE'O }}

// the following will produce a test case error, because currently
// field f1 is not selected, so its value is undefined:

var integer v2 := v1.f1;

...

It is also possible to have recursion in a union type definition that is perfectly legal and resolvable at the value notation level. Here is an example:

...

type union MyRecursiveUnion
{
    integer leafValue,
    MyRecursiveUnion nestedPtr
}

...

var MyRecursiveUnion v1 :=
{
    nestedPtr :=
    {
        nestedPtr :=
        {
            nestedPtr :=
            {
                leafValue := 15
            }
        }
    }
}

var integer v2 := v1.nestedPtr.nestedPtr.nestedPtr.leafValue; // OK

// v2 now contains 15

var MyRecursiveUnion v3;

v3.nestedPtr.nestedPtr.leafValue := 10; // OK

// v3 now contains { nestedPtr := { nestedPtr := { leafValue := 10 } } }

...

Enumerated

TTCN-3 enumerated is like enum in C. Every enumeration identifier is assigned an integer value, either explicitly by the user or implicitly by the compiler through a well-defined enumeration number assignment algorithm that is consistent with the similar algorithm applicable in ASN.1. This means that numbers implicitly assigned to enumeration identifiers of TTCN-3 enumerated and ASN.1 ENUMERATED will be identical provided that two enumerated types are defined in the same way, because the same assignment algorithm is effectively used. Enumeration identifiers and assigned numbers must be unique within the same enumerated type definition. Here is an example of enumerated type and values:

...

type enumerated MyEnumType
{
    E_REQ,      // 0 - implicitly assigned
    E_IND,      // 4 - implicitly assigned
    E_ERR,      // 5 - implicitly assigned
    E_EST(1),   // 1 - explicitly assigned
    E_ACK(2),   // 2 - explicitly assigned
    E_NOAC,     // 6 - implicitly assigned
    E_REV(3),   // 3 - explicitly assigned
    E_RST       // 7 - implicitly assigned
}

...

var MyEnumType v1 := E_REQ,
               v2 := E_EST,
               v3 := v2; // now contains E_EST

...
Binding of enumeration identifier to an integer value as explained above is called default encoding of the enumeration. While the TCI interface does not provide any standard means to access default encoding of enumerations, as it lets you manipulate enumerations only by getting and setting enumeration identifier, tool vendors may offer additional support for enumerated values allowing obtaining default encoding given enumeration type and identifier.

Enumeration identifiers do not have to be globally unique. They may even overlap across different enumeration types. Identifier class and its type can be resolved during compile time by the translator from the context, what helps resolving ambiguity if the same enumeration identifier is defined in two different enumerated types. If identifier type cannot be reliably established from the context for an ambiguous enumeration identifier, "ambiguous identifier" error shall be reported by a sensible compiler. Here is an example illustrating this:

...

type enumerated MyEnumType1
{
    E_ACK(0),
    E_REQ(1),
    E_IND(2)
}

type enumerated MyEnumType2
{
    E_REQ(0), // this is OK
    E_IND(1), // this is OK
    E_ERR(2)
}

...

// OK. No ambiguity, E_ERR is defined only once:
p.send(E_ERR);

// OK. Translator can deduce from the type of the l-value that
// E_REQ is of type MyEnumType1, so ambiguity is resolvable:
var MyEnumType1 v1 := E_REQ;

// OK. Translator can deduce from the type of the l-value that
// E_IND is of type MyEnumType2, so ambiguity is resolvable:
var MyEnumType2 v2 := E_IND;

// "Ambiguous symbol" error. Which E_IND is meant here, the one
// from MyEnumType1 or from MyEnumType2? The compiler has no way
// to tell for sure, so a compile-time error shall be reported:
p.send(E_IND);

// OK. Explicit type qualification to resolve ambiguity:
p.send(MyEnumType1 : E_IND);

...

Two different enumerated types are never compatible with each other. You cannot convert value of enumerated type A into value of enumerated type B even if it is represented by enumeration identifier that is available in both types. This of course does not apply to cases when type B is a trivial subtype of type A, like here:

type enumerated A { ... }
type A B;

In the above case types A and B are of course compatible.

Verdicttype

The verdicttype type shall be regarded as a special part of the TTCN-3 type system. While there is no prohibition in the language to define for example a field of verdicttype type as a part of record or set, it is very uncommon to mix variables of verdicttype type with the rest of the TTCN-3 value type system. Variables of verdicttype type are most commonly defined as standalone variables in their own right that are used to store intermediate or final verdict value.

Variables of verdicttype type can have any of the following verdict values:

  • none
  • pass
  • inconc
  • fail
  • error

You can retrieve the current test case verdict while the test case is running from within the test case body using the getverdict keyword.

Here are some examples of using variables of verdicttype type:

testcase TC_test() runs on CompType
{
    ...
    var verdicttype v1 := getverdict;
    if (v1 == inconc) { stop; /* terminates test case */ }
    ...
    setverdict(fail);
}

control
{
    var verdicttype v1;

    v1 := execute(TC_test());

    if (v1 == fail) { stop; /* terminates control part */ }
    ...
}

Upon termination of a test case started from the control part, the execute() statement that initiated running the test case returns value of verdicttype type containing final verdict of the test case. This does not mean however that you can use something like:

return myVerdictVar;

in the test case statement block. Test case verdicts in TTCN-3 are assigned using the setverdict instruction. So for example instead of writing:

return fail;

you should write:

setverdict(fail);
stop;

if you want to assign a verdict and then terminate test case execution immediately thereafter. Also, you cannot define the return clause in the test case signature to declare a test case as returning verdicttype as you would do it for a function signature, for the following reasons:

  • it is not a test case itself that returns variable of verdicttype type, but the execute() statement that launches it;
  • having the return clause in the test case signature is syntactically disallowed, because formally test case cannot return any value.

Nested type definitions

Much like in ASN.1, you can have anonymous type definitions in places where type references are allowed in definitions of record, set and union fields and in definitions of record of and set of element types. Arbitrarily deep levels of nesting are also possible. This is illustrated by the following example:

type record MyRecType {

    set
    {
        bitstring f1,
        octetstring f2
    }
    field1,

    union
    {
        record of integer roiField,
        set of integer soiField,

        record
        {
            integer a,
            float b optional
        }
        recField
    }
    field2,

    enumerated
    {
        E_ACK(0),
        E_NOACK(1)
    }
    field3
}

type record of record of set of record { integer a, float b } MyCoolType;

...

// let's have the only one scalar element in the end
const MyCoolType MY_COOL_CONST :=
{
    {
        {
            {
                a := 1,
                b := 2.0
            }
        }
    }
}

Subtyping and type restrictions

It is possible to define a subtype of the parent type in TTCN-3. Parent type can be a basic predefined type or its subtype, or it can be a type reference to a user-defined constructed type or its subtype.

Here are some examples of unrestricted subtype definitions:

// user-defined constructed type definition

type record MyRecType
{
    integer f1,
    boolean f2
}

// subtype definitions

type integer MyInt; // parent is a predefined basic type
type MyInt MyInt2; // parent is a user-defined subtype of basic predefined type
type MyInt2 MyInt3; // defines a new integer type MyInt3 through subtyping

// defines a new type MyRecType2 which is a subtype of MyRecType
type MyRecType MyRecType2;

All this looks very similar to C typedef, except that C typedef does not introduce a new symbol, because it merely defines an alias for an existing type, while TTCN-3 type does.

Subtyping that leads to an infinite recursion is not allowed:

// The following shall produce a compile-time error.

// Type definitions for TypeA, TypeB and TypeC form
// an infinite dependency loop that cannot be properly
// resolved.

type TypeC TypeA; // TypeA is defined as a subtype of TypeC
type TypeA TypeB; // TypeB is defined as a subtype of TypeA
type TypeB TypeC; // TypeC is defined as a subtype of TypeB

It is possible to further restrict the set of allowed values of the parent type by attaching a subtype restriction to the subtype definition, what is also known as type constraint in ASN.1. Here are some examples of subtype restrictions:

// values of OCT_4 must contain exactly 4 octets
type octetstring OCT_4 length(4);

// values of byte shall be in the range from 0 to 255 inclusive
type integer byte(0..255);

// values of restricted_int shall be any of the following: -1, 2, 3, 4, 5
type integer restricted_int(-1, 2, 3..5);

// values of restricted_str shall be any of the following: "AB", "CD", "EF"
type charstring restricted_str("AB", "CD", "EF");

Subtype restrictions affect matching results. If for example we receive a value from the SUT that was decoded by the decoder so that the decoded value itself or, if the value is constructed, any of its subelements violate any of the applicable subtype restrictions of the expected value type, then a mismatch will occur and the corresponding receiving operation will not succeed.

ASN.1 syntax for defining type constraints is more powerful than TTCN-3 syntax for defining subtype restrictions, as it allows arbitrarily complex expressions for defining value sets involving set arithmetic. Therefore, it is always possible to express a TTCN-3 subtype restriction in ASN.1 as type constraint, but not vice versa.

Values of the same base type are compatible with each other as long as subtype restrictions are satisfied, so assignments of variables of different subtypes to each other and their joint use in expressions is possible. The only case when TTCN-3 demands stronger typing is during matching of a received value against the expected value. If we expect to receive a value of type A and we actually receive the value of type B, then we will have a mismatch, even if A and B resolve to the same root type or if B is a subtype of A or vice versa. If we expect to receive a value of type A in the receiving operation, then only value of type A will do.

Classes of subtype restrictions

There exist several classes of subtype restrictions in TTCN-3. Some subtype restrictions are applicable only to certain classes of types, while others are applicable to all types. This is illustrated by Table 5.

Table 5: Classes of subtype restrictions in TTCN-3
Restriction class Applicability Examples of restricted subtypes
Basic restrictions
value list all types1)
type integer MyInt(1, 2, 3);
type float MyFloat(1.0, 2.0);
type boolean MyBool(not true); // same as writing (false)
type bitstring MyBitstring(''B, '1'B, '101'B);
type hexstring MyHexstring('F'H, '5AE'H);
type octetstring MyOctetstring('A4'O, '15AC'O);
type charstring MyCharstring("AB", "CD", "EFG");
type universal charstring MyUC("Ελληνικά", "Русский");
type objid MyObjid(objid { 1 2 3 }, objid { m(0) 1 });

type MyBaseRecord MyRestrictedRecord
({ f1 := 1, f2 := 5 }, { f1 := 3, f2 := 4 }, MyRecConst);

type MyBaseRecOfInt MyRestrictedRecOfInt
({ 1, 2, 3 }, { 4, 5 });

type MyBaseSet MyRestrictedSet
({ f1 := 1, f2 := 5 }, { f2 := 4, f1 := 3 });

type MyBaseSetOfInt MyRestrictedSetOfInt
({ 1, 2, 3 }, { 5, 4 });

type MyBaseUnion MyRestrictedUnion
({ u1 := 5 }, { u2 := '11'B }, MyUnionConst);

type MyBaseEnum MyRestrictedEnum(E_REQ, E_IND);

type verdicttype MyVerdictType(pass, fail);

type MyDefinedIntType MyRestrictedInt(10, 20);
value range
  • integer
  • float
type integer MyInt(0 .. 255);
type integer NaturalNumbers(1 .. infinity);
type integer NegativeNumbers(-infinity .. -1);

type float MyFloat(0.0 .. 1.0);
type float NonNegative(0.0 .. infinity);
type float NonPositive(-infinity .. 0.0);
character range all character string types4)
// each character in MyCharstring must be within
// the range of ["a" .. "z"] inclusive:
//
type charstring MyCharstring("a" .. "z");
length restriction
  • all string types2) 3)
  • record of
  • set of
type bitstring MyBitstring length(5);
type hexstring MyHexstring length(2..4);
type octetstring MyOctetstring length(0..10);
type charstring MyCharstring length(10..infinity);
type universal charstring MyUC length(5);

type record length(5..infinity) of integer MyRecOfInt;
type set length (15) of integer MySetOfInt;
pattern all character string types4)
type charstring MyStr1(pattern "abc*xyz");

// same as: type charstring MyStr2("a" .. "z");
type charstring MyStr2(pattern "[a-z]#(0,)");
Composite restrictions
mixed value list and value range
  • integer
  • float
type integer MyInt(-1000, -100..-90, -10, 1..infinity);
type float MyFloat(-1E3, -1E2..-90.0, -10.0, 1.0..infinity);
postfixing with length restriction all string types2) 5)
// length restriction can be appended to any arbitrary base
// restriction applicable to a string subtype

type charstring MyStr1(pattern "abc*xyz") length(10..20);
type charstring MyStr2(pattern "abc*xyz") length(15);

// same as: type charstring MyStr3(pattern "[a-z]#(10,20)");
type charstring MyStr3("a" .. "z") length(10..20);

// same as: type charstring MyStr4(pattern "[a-z]#(1,)");
type charstring MyStr4("a" .. "z") length(1..infinity);

// same as: type charstring MyStr5(pattern "[a-z]#(,10)");
type charstring MyStr5("a" .. "z") length(0..10);

// same as: type charstring MyStr6(pattern "[a-z]#(10)");
type charstring MyStr6("a" .. "z") length(10);

NOTES

1) all types means all basic and constructed types

2) all string types means charstring, universal charstring, bitstring, hexstring, octetstring

3) length restriction applies units of measure to values of string types according to Table 6

4) all character string types means charstring, universal charstring

5) the only practical cases of postfixing with length restriction are applicable to pattern and character range restrictions of character string types only, what is illustrated by the Examples column; there is however no syntactic or semantic prohibition to apply such postfixing to other string restrictions

Units of measure for string types are presented in Table 6.

Table 6: Units of measure for string types
Type class Unit of measure Unit example
bitstring bit '1'B
hexstring hex digit 'F'H
octetstring octet '1F'O
charstring character "a"
universal charstring universal character "λ"

It is legal to have the only one element in the value list restriction.

It is possible to have constant expressions in subtype restrictions.

The pattern restriction restricts the base character string type to a set of allowed character strings that shall match a regular expression defined after the pattern keyword according to the TTCN-3 regular expression syntax.

Placement of subtype restrictions

Subtype restrictions may not only appear in trivial subtype definitions, but also in places where definition of a nested anonymous type is allowed, i.e. in definitions of record, set and union fields and in definitions of record of and set of element types. This is illustrated by the following example:

// trivial subtype restriction present in the definition of subtype
// MyInt
type integer MyInt(0..255);

type record MyRecType // here subtype restrictions are attached to
                      // record fields
{
    integer f1(0..255) optional, // values of f1 must be within the
                                 // range of [0 .. 255] inclusive,
                                 // or f1 may be omitted altogether
    charstring f2 length(5),

    record // nested anonymous type definition that
           // defines type of field f3
    {
        integer a,
        float b
    }
    f3
    ( // value list subtype restriction applicable
      // to field f3

        { a := 1, b := 2.0 },
        { a := 3, b := 4.0 }
    ),

    record of integer f4, // field with nested type definition but no
                          // subtype restriction attached

    integer f5 // trivial field definition

    // Note that subtype restriction may appear in the context where
    // nested anonymous type definition is generally allowed, like
    // for field f3, although it does not have to, like for field f4.

    // Similarly, nested typedef does not have to be present in place
    // where nested subtype restriction is defined, like for fields
    // f1 and f2, although it can, like for field f3.
}

type record length(10) of set length(5..15) of charstring length(20);

It is possible to mix subtype restriction and optionality notation for record and set fields.

Operators

TTCN-3 defines a number of unary and binary operators that can be used in expressions. List of operators is presented in Table 7.

Table 7: List of TTCN-3 operators
Operator Applicability Result type Examples and notes
Arithmetic operators
+ (unary)
  • integer
  • float

root type3) of the operand(s)

+5

+5.0

- (unary)

-5

-5.0

+ (binary)

5 + 2

2.0 + 4.0

2 + 4.0 (error, incompatible types)

- (binary)

5 - 2

2.0 - 4.0

2 - 4.0 (error, incompatible types)

*

5 * 2

2.0 * 4.0

2 * 4.0 (error, incompatible types)

/

5 / 2 (yields 2)

5 / -2 (yields -2)

-5 / 2 (yields -2)

-5 / -2 (yields 2)

2.0 / 4.0 (yields 0.5)

2 / 4.0 (error, incompatible types)

rem
  • integer
  • integer
x rem y = x - y * (x/y)
mod
x mod y = x rem |y|         when x >= 0
        = 0                 when x < 0 and x rem |y| = 0
        = |y| + x rem |y|   when x < 0 and x rem |y| < 0
String operators
&

all string types1)

root type3) of the operands

"Hello, " & "world!" (yields "Hello, world!")

"Ελληνικά " & "Русский"

'1101'B & '11100'B

'3EF'H & 'AC'H

'15EF'O & '2844'O

'15EF'O & '2844'H (error, incompatible types)

Relational operators
==

all compatible types2)

  • boolean

5 == 2 (yields false)

true == false (yields false)

5 == 5 (yields true)

myVar == { a := 1, b := 2.0 } (OK, type of the right-hand side operand can be deduced from the type of the left-hand side operand)

{ a := 1, b := 2.0 } == myVar (OK, type of the left-hand side operand can be deduced from the type of the right-hand side operand)

{ a := 1, b := 2.0 } == { a := 1, b := 2.0 } (error, types of operands cannot be deduced from the context)

!=

5 != 2 (yields true)

true != false (yields true)

5 != 5 (yields false)

<
  • integer
  • float
  • enumerated4)
5 < 10 (yields true)
> 5.0 > 10.0 (yields false)
<= E_ACCEPT <= E_DENY (comparison is performed according to enumeration default encoding)
>= 1 >= 2.0 (error, incompatible types)
Logical operators
not
  • boolean
  • boolean

not true (yields false)

not false (yields true)

and

true and false (yields false)

or

true or false (yields true)

xor

true xor false (yields true)

Bitwise operators
not4b
  • bitstring
  • hexstring
  • octetstring

root type3) of the operand(s)

not4b '1010'B (yields '0101'B)

not4b 'A'H (yields '5'H)

not4b 'A0'O (yields '5F'O)

and4b

'1100'B and4b '1010'B (yields '1000'B)

'C'H and4b 'A'H (yields '8'H)

'CF'O and4b 'A0'O (yields '80'O)

or4b

'1100'B or4b '1010'B (yields '1110'B)

'C'H or4b 'A'H (yields 'E'H)

'CF'O or4b 'A0'O (yields 'EF'O)

xor4b

'1100'B xor4b '1010'B (yields '0110'B)

'C'H xor4b 'A'H (yields '6'H)

'CF'O xor4b 'A0'O (yields '6F'O)

'CF'O xor4b 'A0'H (error, incompatible types)

Shift operators
<<

LHS operand:

  • bitstring
  • hexstring
  • octetstring

RHS operand:

  • integer

root type3) of the LHS operand

'111001'B << 2 (yields '100100'B)

'12345'H << 2 (yields '34500'H)

'1122334455'O << 2 (yields '3344550000'O)

>>

'111001'B >> 2 (yields '001110'B)

'12345'H >> 2 (yields '00123'H)

'1122334455'O >> 2 (yields '0000112233'O)

Rotate operators
<@

LHS operand:

  • all string types1)

RHS operand:

  • integer

root type3) of the LHS operand

'111001'B <@ 2 (yields '100111'B)

'12345'H <@ 2 (yields '34512'H)

'1122334455'O <@ 2 (yields '3344551122'O)

"abcde" <@ 2 (yields "cdeab")

@>

'111001'B @> 2 (yields '011110'B)

'12345'H @> 2 (yields '45123'H)

'1122334455'O @> 2 (yields '4455112233'O)

"abcde" @> 2 (yields "deabc")

NOTES

1) all string types are as defined in Note 2 of Table 5

2) all compatible types are all types as defined in Note 1 of Table 5, provided that the type of the expression term on the left hand side of the operator is compatible to the type of the expression term on the right hand side of the operator

3) considering possible subtyping of operand values

4) operands must be instances of the same enumerated type

Operands of binary operators must be of compatible types, except shift and rotate operators.

Binary operators can form chains of expression terms with more than two terms in a chain that are evaluated from left to right if operators from the same precedence class are used throughout the whole chain. Otherwise expression terms are evaluated according to operator precedence rules. This is illustrated by the following examples:

Expression Result
5 + 2 + 3 10
"Hello " & "my " & "friend" "Hello my friend"
5 + 2 - 3 4
2 + 2 * 2 6

Relational operators cannot form chains of expression terms.

Boolean expressions are guaranteed to be evaluated using short-circuit evaluation.

You can use int2float(), float2int() to convert operands of arithmetic operators to make them compatible. You can use various predefined conversion functions to convert operands of concatenation operator & to make the operands compatible.

To apply a bitwise operator, you need to conceptually decompose operands into bit patterns (for bitstrings this is done already), then apply operator in question to the decomposed bit patterns, then reassemble the resulting bit pattern into original root type of the operands, what yields the final result.

Operator precedence rules are presented in Table 8. The table is sorted so that higher priority classes are closer to the top of the table.

Table 8: TTCN-3 operator precedence
Operator precedence class (highest first)
( ... )
+ (unary) - (unary)
* / mod rem
+ (binary) - (binary) &
not4b
and4b
xor4b
or4b
<< >> <@ @>
< > <= >=
== !=
not
and
xor
or

Templates

Templates in TTCN-3 are a powerful mechanism to specify test material as abstract messages sent to or received from the SUT. Role of templates is twofold. They can be used to define:

  • specific abstract messages sent to the SUT;
  • expected patterns in messages received from the SUT.

In the simplest case, expected pattern can be a specific message with all of its content explicitly defined, for example:

template MyMsgType myExpectedResponse :=
{
    typeCode := 1,
    payload := "200 OK"
}

In more complex cases, classes of expected values are defined using TTCN-3 matching mechanisms. In the following example a value list matching mechanism is used in the template specification:

template MyMsgType myExpectedResponse :=
{
    typeCode := (1, 2, 3), // typeCode field in the received message can be either 1, 2, or 3
    payload := "200 OK"
}

It is convenient to think of TTCN-3 templates as regular expressions that operate on structured data rather than on text strings. Continuing the analogy, TTCN-3 matching mechanisms can be compared to regular expression syntax rules.

Definition scope

Templates can be defined either globally in the module scope or locally in a statement block of a function, test case or altstep. Template definition visibility rules follow general scoping rules for other language elements. Here is an example:

module MyModule
{

...

// global template definition
template integer T1 := 5;

function F() runs on MyComp
{
    // local template definition - visible only inside F()
    template boolean T2(boolean arg) := arg xor true;

    p.send(T1);
    p.send(T2(true));
}

...

}

Template parameterization

A TTCN-3 template can be parameterized with the list of formal parameters:

template MyMsgType myExpectedResponse(template integer arg1, template charstring arg2) :=
{
    typeCode := arg1,
    payload := arg2
}

A parameterized template can be instantiated elsewhere in TTCN-3 code by supplying the list of actual parameters to the template reference:

p.receive(myExpectedResponse((1, 2, 3), "200 OK"));

As you can see from the example above, you can pass expressions that use matching mechanisms as template actual parameters in addition to specific values. In this case we have passed a value list (1, 2, 3) as template actual parameter. To enable matching mechanism passing for a specific template parameter you need to use the template keyword in the formal parameter specification. If you omit this keyword, then only specific values are allowed for actual parameters in place of this formal parameter.

When the TTCN-3 execution engine reaches the point when it needs to evaluate parameterized template reference, it constructs effective template value by substituting template formal parameters with actual parameters and then evaluating template content. This somewhat resembles calling a function.

Inline templates

In addition to defining a template in the global or local scope, a template can be implicitly defined at the point of its use, for example, in send or receive statement:

...

p.send(MyReqType : { typeCode := 1, payload := "Hi!" });
p.receive(MyRspType : { typeCode := (1, 2, 3), payload := "Hello!" });

...

You may have concluded from the example above that separation between templates and regular values has become a bit vague, since we could as well use a regular value or variable reference both in send and receive statements. There is however a clear boundary between the world of templates and the world of regular values in TTCN-3: templates may contain matching mechanisms while regular values cannot.

The following table illustrates allowed use of regular values and templates in sending and receiving operations in TTCN-3.

Table 9: Contextual use of templates and regular values
Context Regular value Template without matching mechanisms Template with matching mechanisms

sending operation

allowed

allowed

disallowed

receiving operation

allowed

allowed

allowed

Template variables

It is possible to define a template variable in a statement block or in component type definition:

var template MyType myLocalVar := { f1 := 5, f2 := true }

Unlike regular variables, template variables may additionally contain matching mechanisms.

Signature templates

It is possible to define a template for signature parameters. Signature template looks like a collection of record fields, each field representing the corresponding signature parameter:

signature MySigType(
    in integer par1,
    out bitstring par2,
    RecordType par3,
    inout OSType par4)
return integer
exception(BSType, integer, StrType);

template MySigType sigTemplate :=
{
    par1 := 1,
    par2 := '10'B,
    par3 := { f1 := '1100'B, f2 := 'EE'O },
    par4 := 'A8'O
}

...

p.call(sigTemplate);

Depending on the context in with signature template is used, content of either in fields or out fields may be insignificant.

Modified templates

It is possible to define modified templates by deriving their content from some base template. TTCN-3 has a convenient mechanism for constructing a new template definition by applying minor modifications to an otherwise massive base template definition:

template Message MyBaseTemplate :=
{
    f1 := 5,
    f2 :=
    {
        ff1 := 'AB'O,
        ff2 := '11'B
    }
}

template Message MyModifiedTemplate
modifies MyBaseTemplate :=
{
    f2 :=
    {
        ff2 := '00'B
    }
}

Now the effective value of MyModifiedTemplate is as follows:

{
    f1 := 5,
    f2 :=
    {
        ff1 := 'AB'O,
        ff2 := '00'B
    }
}

Modifications can be chained, i.e. a modified template may in its turn act as a base template.

If the base template is parameterized, then the modified template shall have at least the same set of parameters in the beginning of the parameter list, optionally appending more parameters to the end of the list.

Matching mechanisms

Omit

An omit symbol can be used to indicate that a record field declared as optional is missing from the record value. It can be regarded as a special case of value notation or its extension. Unlike other matching mechanisms, omit symbol can be present in non-template variables and it can participate in a template used in sending operations.

type Message
{
    integer typeCode,
    charstring payload optional
}

...

p.send(Message :
{
    typeCode := 1,
    payload := omit
});

Value list

Value list defines a set of allowed values.

p.receive(Message :
{
    typeCode := 1,

    payload :=
    (
        "200 OK",
        "404 Not Found",
        "301 Moved Permanently"
    )
});

In the example above, "200 OK", "404 Not Found", and "301 Moved Permanently" are the only acceptable values for the payload field that would trigger the template match.

Complement

Complement defines a set of disallowed values.

p.receive(Message :
{
    typeCode := 1,

    payload := complement
    (
        "200 OK",
        "404 Not Found",
        "301 Moved Permanently"
    )
});

In the example above, any payload content except "200 OK", "404 Not Found", and "301 Moved Permanently" will trigger the template match.

Any value

Any value means that any value is allowed. All applicable subtype restrictions shall nonetheless be satisfied to trigger the match.

p.receive(Message :
{
    typeCode := 1,
    payload := ?
});

In the example above, any payload content will trigger the template match.

The payload content cannot be omitted (i.e. may not contain omit).

p.receive(Message : ?);

In the example above, any incoming value of type Message will trigger the match.

Any value or none

Any value or none means that any value is allowed in place of a field or the field can be omitted. It makes sense to apply this matching mechanism to an optional field of a record or set.


Personal tools