Using Assertions in MQL5 Programs

Sergey Eremin | 24 November, 2015

Introduction

Assertion is a special construction that enables checking arbitrary assumptions in the program's arbitrary places. They are typically embodied in the form of a code (mostly as a separate function or macro). This code checks a true value of a certain expression. If it appears to be false, then a relevant message is displayed, and the program is stopped, given the implementation provides for it. Accordingly, if the expression is true, it implies that everything operates as intended - the assumption is met. Otherwise, you can be certain, that the program has located errors and is clearly notifying about it.

For example, if it's expected that a certain value X within the program under no circumstance should be less than zero, then the following statement can be made: "I confirm that a value of X exceeds or equals zero". If X happens to be less than zero, then a relevant message will be displayed, and a programmer will be able to adjust the program.

Assertions are particularly useful in big projects, where their component parts may be reused or modified with time.

Assertions should cover only those situations that shouldn't occur during the program's regular operation. As a rule, assertions can be applied only at the program's development and debugging stages, i.e. they shouldn't be present in the final version. All assertions must be removed during the final version's compilation. This is usually achieved through conditional compilation.


Examples of the assertion mechanism in MQL5

The following capabilities are normally provided by the assertion mechanism:

  1. Display of an expression text to be checked.
  2. Display of a file name with a source code where an error is detected.
  3. Display the name and signature of a function or a method where an error is detected.
  4. Display of a line number in a source file where an expression is reviewed.
  5. Display of an arbitrary message specified by a programmer at the code writing stage.
  6. Program termination after finding errors.
  7. Possibility to exclude all assertions from the compiled program using conditional compilation or a similar mechanism.

Almost all capabilities can be implemented using standard functions (apart from point 6 — see further), macro and conditional compilation mechanisms of MQL5 language. For example, two out of all possible options might look the following way:

Option N1 (soft version, without terminating the program)

#define DEBUG

#ifdef DEBUG  
   #define assert(condition, message) \
      if(!(condition)) \
        { \
         string fullMessage= \
                            #condition+", " \
                            +__FILE__+", " \
                            +__FUNCSIG__+", " \
                            +"line: "+(string)__LINE__ \
                            +(message=="" ? "" : ", "+message); \
         \
         Alert("Assertion failed! "+fullMessage); \
        }
#else
   #define assert(condition, message) ;
#endif 

Option N2 (hard version, with terminating the program)

#define DEBUG

#ifdef DEBUG  
   #define assert(condition, message) \
      if(!(condition)) \
        { \
         string fullMessage= \
                            #condition+", " \
                            +__FILE__+", " \
                            +__FUNCSIG__+", " \
                            +"line: "+(string)__LINE__ \
                            +(message=="" ? "" : ", "+message); \
         \
         Alert("Assertion failed! "+fullMessage); \
         double x[]; \
         ArrayResize(x, 0); \
         x[1] = 0.0; \
        }
#else 
   #define assert(condition, message) ;
#endif


The assert macro

First, the DEBUG identifier is declared. If this identifier remains declared, then the #ifdef branch of the conditional compilation expression will be valid, and a full-featured macro assert will be included in the program. Otherwise, (#else branch) the assert macro that will not lead to the execution of any operation will be included in the program.

A full-featured macro assert is built the following way. First, an incoming expression condition is executed. If it is false, then a fullMessage message is formed and displayed. The fullMessage is constructed from the following elements:

  1. Text of an expression to be checked (#condition).
  2. File name with a source code from which the macro was called (__FILE__).
  3. Signature of a function or method from which the macro was called (__FUNCSIG__).
  4. Line number in a file with a source code where a macro call is placed (__LINE__).
  5. Message transfered to a macro, if it's not empty (message).

After displaying a message (Alert) an attempt to assign a value to a non-existing array element is made in the second macro type which leads to a time execution error and causes the program to crash immediately.

This method of stopping the program has side effects for indicators that operate in their subwindows: they remain in the terminal, therefore, need to be manually closed. Furthermore, there may be artifacts in the form of unremoved graphical objects, terminal's global variables, files, etc. that were created during the program's operation until it crashed. If this behavior is completely unacceptable, then the first macro should be used.

Explanation. At the moment of writing this article in MQL5 there was no mechanism for the program to perform an emergency stop. As an alternative, the runtime error was triggered, which guaranteed the program to crash.

This macro can be placed in the separate included file assert.mqh, which will be placed, for example, in <data folder>/MQL5/Include. This file (option N2) is attached to the article.

A code provided below contains an example of using assertion and its operation results.

Example of using the assert macro in the Expert Advisor's code

#include <assert.mqh>

int OnInit()
  {
   assert(0 > 1, "my message")   

   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {  
  }

void OnTick()
  {
  }

Here you can find an assertion that literally means "I confirm that 0 is greater than 1". The assertion is obviously false which leads to a display of an error message:

Fig. 1. Example of an assertion

Fig. 1. Example of an assertion


General principles for using assertions

Assertions should be used to identify the program's unforeseen situations and to document and control the execution of accepted assumptions. For example, assertions can be used to check the following conditions:

There are certainly more types of conditions worth checking using assertions, and in each case they are absolutely unique. Some of them are shown above.

Use assertions for checking preconditions and postconditions. There is an approach for software design and development called "Design by contract". According to this approach, every function, method and class signs a contract with a remaining program's part using preconditions and postconditions.

Preconditions are agreements which a client code, calling method or class agrees to implement before calling a method or creating an object instance. In other words, if it is assumed that a method should take a certain parameter with values ​​greater than 10, then a programmer must take care of the calling code to ensure that under no circumstances it should pass a value less than or equal to 10 to this parameter.

Postconditions are agreements that a method or class agrees to implement before finishing their work. Thus, if it's expected that a method shouldn't return values below 100, then a programmer must take care of the returned value so it wouldn't exceed or equal 100.

It is very convenient to document preconditions and postconditions, and also monitor their compliance at the program's development and debugging stages. Unlike conventional comments, assertions will not simply declare expectations, but will also constantly monitor their fulfillment. An example of using assertions for checking and documenting preconditions and postconditions is shown above, read "Example of using assertions to check input and output method values".

If there is an opportunity, try to use assertions to check programming errors, that is, errors that are not affected by external factors. Thus, it's best not to use assertions for checking correctness of opening deals and availability of a quotation history for a certain requested period etc. It would be more appropriate to handle and log such errors.

Avoid placing an executable code in assertions. Since all assertions can be deleted at the compilation stage of the program's final version, they shouldn't affect the program's behavior. For example, this problem is frequently linked to calling a function or method inside the assert.

Assertion that can affect the program's behavior after disabling all assertions

void OnTick()

  {
   CMyClass someObject;

//--- to check some calculations for correctness
   assert(someObject.IsSomeCalculationsAreCorrect(),"")
  
   ...
   ...
   ...
  }

In this case you should place the function call before the assertion, save its result in a certain status variable and then check it in the assertion:

Assertion that can't affect the program's behavior after disabling all assertions

void OnTick()
  {
   CMyClass someObject;

//--- to check some calculations for correctness
   bool isSomeCalculationsAreCorrect = someObject.IsSomeCalculationsAreCorrect();
   assert(isSomeCalculationsAreCorrect,"")
  
   ...
   ...
   ...
  }

Don't confuse assertions with handling expected errors. Assertions are used for searching errors in programs at the development and debugging stages (searching of programing errors). Processing of expected errors, on the other hand, enables smooth operation of the program's released version (ideally shouldn't be programing errors). Assertions are not meant to handle errors, instead they should shout: "Hey, buddy, you have an error here!".

For example, in the event that a certain class method demands a value exceeding 10 to be handed over, and a programmer tries to pass 8, then this is an obvious error of a programmer and he should be warned about it:

Example of calling a method with an unacceptable value of input parameter (assertions are used for checking parameter's values)

void CMyClass::SomeMethod(const double a)

  {
//--- we are checking, if a exceeds 10
   assert(a>10,"")
  
   ...
   ...
   ...
  }

void OnTick()
  {
   CMyClass someObject;

   someObject.SomeMethod(8);
  
   ...
   ...
   ...
  }

Now, that a programmer runs a code with passing a value 8, the program clearly informs him: "I state that values below or equal to 10 can't be transfered to this method".

Figure 2. Operation results of calling a method with an unacceptable value of input parameter ​​(assertions are used to check parameter values)

Fig. 2. Operation results of calling a method with an unacceptable value of input parameter ​​(assertions are used to check parameter values)

After receiving such message a programmer can quickly correct the error.

In turn, if for a program's operation it is necessary that the history of an instrument exceeds 1000 bars, then the reverse situation can't be considered as a programmer's mistake, as the available history volume doesn't depend from him. It would be logical to handle a situation with less than 1000 bars available for an instrument as an expected error:

Example of handling a situation when the available history for an instrument is less than required (error processing is applied in this situation)

void OnTick()
  {
   if(Bars(Symbol(),Period())<1000)
     {
      Comment("Insufficient history for correct operation of the program");
      return;
     }
  }

For maximum stability of a final version please use assertions to check expectations and then, nevertheless, handle errors:

Example of a combination of assertions and error handling

double CMyClass::SomeMethod(const double a)
  {
//--- check value of an input parameter with assertion
   assert(a>=10,"")
   assert(a<=100,"")
  
//--- check value of an input parameter and correct it, if required
   double aValue = a;

   if(aValue<10)
     {
      aValue = 10;
     }
   else if(aValue>100)
     {
      aValue = 100;
     }

//--- calculate the resulting value
   double result=...;

//--- check a resulting value with assertion
   assert(result>=0,"")

//--- check a resulting value and correct it, if necessary
   if(result<0)
     {
      result = 0;
     }

   return result;
  } 


Thus, assertions help tracking a certain number of errors before the release of a final version. Handling errors in the final version will enable the program to operate correctly even with those mistakes, that couldn't be found at the development and debugging stages.

There are multiple methods of handling errors: from the correction of wrong values (as in the example above) to a full stop of an operation. Studying all of them would fall beyond the scope of this article.

It is also believed, that if the program reacts at a programmer's mistake with a correct crashing and indicates a problem, then assertions are redundant in this case. For example, at the division by zero the program in MQL5 terminates its execution and displays the relevant message in the log. In principle, such approach helps to find problems better than assertions do. However, assertions allow to add the important information about assumptions to the code, that will be more noticeable (in case of a violation) and appropriate than classic comments in the source code, and that can greatly assist in the further support and the code's development.


Conclusion

This article has studied the mechanism of assertions, provided an example of its implementation in MQL5, and also offered general recommendations about its application. Properly applied assertions can greatly simplify the software development and debugging stages.

Please remember that assertions are firstly targeting the search of programming errors (errors made by programmers), rather than errors not depending on programmers. Assertions are not included in the program's final version. For possible errors that are not depending on a programmer, it is best to use error handling.

The assertion mechanism is closely associated with a matter of software testing. Both error handling and software testing are vast subjects that deserve additional attention and should be covered in separate articles.