Discussion of article "Using Assertions in MQL5 Programs"

 

New article Using Assertions in MQL5 Programs has been published:

This article covers the use of assertions in MQL5 language. It provides two examples of the assertion mechanism and some general guidance for implementing assertions.

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.

Fig. 1. Example of an assertion

Fig. 1. Example of an assertion

Author: Sergey Eremin

 

1. Why macros? They are inconvenient, not all conditions can be fed to them, and it is extremely difficult to debug them if something goes wrong. It was easier to implement trivial procedures.

2. Some too "dirty trick" with the array. Couldn't you divide it by zero?

 
Andrey Shpilev:

1. Why macros? They are inconvenient, not all conditions can be fed to them, and it is extremely difficult to debug them if something goes wrong. It was easier to implement trivial procedures.

2. Some too "dirty trick" with the array. Couldn't you divide it by zero?

1. In order not to be unsubstantiated, show me an example of a condition that cannot be fed to my macro (I'm not being sarcastic, it's really important for me to know about all the subtle points, as I use this macro all the time). So, please explain what are the difficulties of debugging?

And in general, yes, you can do it with a procedure, I showed only two possible examples. But to be fair, I don't know a graceful way to get all this data in a procedure:

  1. The text of the expression passed to the check(#condition).
  2. The name of the source code file from which the macro was called(__FILE__).
  3. Signature of the function or method from which the macro was called(__FUNCSIG__).
  4. Line number in the source code file on which the macro call is located(__LINE__).

I would be very grateful (and I am probably not the only one) if you could show me your variant in the form of a procedure that would implement all this (of course, "out of the box" and on the machine, and not by manually passing all this as parameters). In principle, 2...4 can be passed as input parameters, and it will be more or less universal (in the sense that it will always be the same thing passed, you will not need to manually set something), but how to get item. 1 to get in the procedure I have no ideas at all

Plus all the same usually, as in the same C++, statements are written on macros, on the same way and I went the same way. The only weak point I see: if an input parameter or variable named x is declared in the procedure/function where we use such a macro, we will get a warning. The solution is simple: in the macro name the array something more unique, for example assertionFailedArray.


2. I don't see the difference. An execution error is an execution error, it will crash the programme and it will not be executed further. However, I'll answer why I went this way: at first it was division by zero, but when I was testing such a macro, for some reason code execution was not interrupted when calling it in methods. If it was called in OnTick, OnInit, etc., then yes, the execution stopped. If inside some method of an arbitrary class, then no. Whether it was an MQL5 error, I did not bother to look into it, I just started calling another execution error :).

I will try to see what is wrong with division by zero in methods.

 
I don't know why (talking about debugging, after all), I'll just leave this code here:
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict
//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
struct CFormatOutEol       { uchar dummy; };
struct CFormatOutFmtDigits { int digits;  };  
struct CFormatOutFmtSpace  { bool space;  };  
//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
class CFormatOut
  {
   string            m_line;
   string            m_dbl_fmt;
   bool              m_auto_space;

public:

   //---- constructor
                     CFormatOut(int dbl_fmt_digits=4,bool auto_space=false):m_dbl_fmt("%."+(string)dbl_fmt_digits+"f") { }

   //--- output data
   CFormatOut *operator<<(double x) { auto_space(); m_line+=StringFormat(m_dbl_fmt,x); return(GetPointer(this)); }
   CFormatOut *operator<<(string s) { auto_space(); m_line+=s;                         return(GetPointer(this)); }
   CFormatOut *operator<<(long   l) { auto_space(); m_line+=(string)l;                 return(GetPointer(this)); }

   //--- output end of line (real output/call Print function)
   CFormatOut *operator<<(CFormatOutEol &eol) { Print(m_line); m_line=NULL; return(GetPointer(this)); }

   //--- change output format for real numbers
   CFormatOut *operator<<(CFormatOutFmtDigits &fmt) { m_dbl_fmt="%."+(string)fmt.digits+"f"; return(GetPointer(this)); }
   
   //--- add/remove auto space insetring
   CFormatOut *operator<<(CFormatOutFmtSpace  &fmt) { m_auto_space=fmt.space; return(GetPointer(this)); }

protected:
   void              auto_space() { if(m_line!=NULL && m_auto_space) m_line+=" "; }
  };

CFormatOut           OUT;
//--- specal object for inserting EndOfLine to output
CFormatOutEol        EOL;
//--- setting digits for numbers output
CFormatOutFmtDigits  DBL_FMT_DIGITS(int digits) { CFormatOutFmtDigits fmt; fmt.digits=digits; return(fmt); }
//--- on/off inserting spaces between outputs
CFormatOutFmtSpace   AUTO_SPACE(bool enable)    { CFormatOutFmtSpace  fmt; fmt.space =enable; return(fmt); }
//--- shorty function to convert enums to string
template<typename T> string EN(T enum_value)    { return(EnumToString(enum_value)); }
Usage:
OUT << AUTO_SPACE(true) << M_PI << "Test" << DBL_FMT_DIGITS(6) << M_PI << EN(PERIOD_M1) << EOL;
Result:
2015.09.01 18:04:49.060    Test EURUSD,H1: 3.1416 Test 3.141593 PERIOD_M1

CAUTION:
The parameters of the expression OUT << ... in reverse order, from right to left, a side effect is possible!
 
Ilyas:
I don't know why (we're talking about debugging), so I'll just leave this code here:

If in your code you can specify where to output (to the log via Print, to the alert, to a file, etc.), it may be even more useful, it seems to me. Especially since it is not difficult to do it at all.


P.S. can I criticise/praise the article? :)

 
Sergey Eremin:

If in your code you can specify where to output (to the log via Print, to the alert, to a file, etc.), it may be even more useful, it seems to me. Especially since it is not difficult to do it at all.


P.S. can I criticise/praise the article? :)

The article discusses only DEBUG ASSERT, it's good to have it on hand. The topic is covered.

But, IMHO! For users (large-scale use of the article material) you need not only DEBUG ASSERT, but also a logger.

A good logger should have a logging level:
  1. FATAL - ошибка, дальнейшее выполнение программы невозможно
  2. ERR   - ошибка, выполнение программы можно продолжить
  3. ATT   - предупреждение
  4. MSG   - сообщение
The logging level can be controlled by a parameter of the MQL program.
When the program is debugging, DebugBreak is called in the logger - you can stop and look at the environment(state) of the MQL program.
When the program is running at the end user, the logger messages are saved to a file(print/alert).
By default, the logger outputs only ERR and FATAL errors and the user can always change the logging level when running the programme to see all the messages of the programme (ATT and MSG).
If used correctly, the log can be used to identify/find an error in the programme.
 
Here's your bones, put some meat on them:
#property script_show_inputs

enum EnLogLevel
  {
   __LOG_LEVEL_FATAL,   // fatal errors only
   __LOG_LEVEL_ERR,     // errors only
   __LOG_LEVEL_ATT,     // warnings and errors
   __LOG_LEVEL_MSG,     // all messages
  };

input EnLogLevel LogLevel=__LOG_LEVEL_MSG;   // logger level
//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
#define __LOG_OUT(params) ExtTrueLogger.Out params
#define __LOG(level,params) do{ if(level<=LogLevel) __LOG_OUT(params); }while(0)
#define  LOG_MSG(msg)    __LOG(__LOG_LEVEL_MSG,(__FUNCSIG__,__FILE__,__LINE__,msg))
#define  LOG_ATT(msg)    __LOG(__LOG_LEVEL_ATT,(__FUNCSIG__,__FILE__,__LINE__,msg))
#define  LOG_ERR(msg)    __LOG(__LOG_LEVEL_ERR,(__FUNCSIG__,__FILE__,__LINE__,msg))
#define  LOG_FATAL(msg)  __LOG(__LOG_LEVEL_FATAL,(__FUNCSIG__,__FILE__,__LINE__,msg))
//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
class CTrueLogger
  {
public:
   void              Out(string func,string file,int line,string msg)
     {
      Print(func," ",func," ",file," ",line," ",msg);
     }
  } ExtTrueLogger;
//+------------------------------------------------------------------+
//| Script programme start function|
//+------------------------------------------------------------------+
void OnStart()
  {
   LOG_MSG("Hello MSG world!");
   LOG_ATT("Hello ATT world!");
   LOG_ERR("Hello ERR world!");
   LOG_FATAL("Hello FATAL world!");
  }
 
Ilyas:
The article only discusses DEBUG ASSERT, having it on hand is good. The topic is covered.

But, IMHO! For users (large-scale use of the article's material) you need not only DEBUG ASSERT, but also a logger.

A good logger must have a logging level:
  1. FATAL - ошибка, дальнейшее выполнение программы невозможно
  2. ERR   - ошибка, выполнение программы можно продолжить
  3. ATT   - предупреждение
  4. MSG   - сообщение
The logging level can be controlled by a parameter of the MQL program.
When the program is debugging, DebugBreak is called in the logger - you can stop and look at the environment(state) of the MQL program.
When the program is running at the end user, the logger messages are saved to a file(print/alert).
By default, the logger generates only ERR and FATAL errors and the user can always change the logging level when running the programme to see all the messages of the programme (ATT and MSG).
If used correctly, the log can be used to identify/find errors in the programme.

Just in the next article (if Rashid approves) I am planning the processing of "expected" errors already in release versions of software (as a logical continuation after approvals), which will include the disclosure of the logging issue.

Thank you very much for these two comments, if you don't mind, I will use them for this article.

 
Sergey Eremin:

In the next article (if Rashid approves) I am going to cover "expected" errors in the release versions of the software (as a logical continuation after the approvals), which will also cover the issue of logging.

Thank you very much for these two comments, if you don't mind, I'll use them for this article.

Of course, I will wait for the article, I will subscribe to the draft and help in its development, good luck.
 
Sergey Eremin:

Interesting topic.

Just shortly before reading this article I was thinking for myself about ways of detecting by preconditions and immediately interrupting program execution in case of a possible code loop in one of its blocks.

Opps.

At the same time, having downloaded the assert.mqh file, I added a line there:

#define  TEST_TEXT "Line: ",__LINE__,", ",__FUNCTION__,", "

And then in the code it looks like this:

  Print(TEST_TEXT,"a = ",a);

That is, that and simply when constructing the code to apply the output of information with the expectation that by the end of work on the code then this output of "working" information can be easily removed (as many, I suppose, probably did and do with the output of information at the stages of code construction).

 
Dina Paches:

Interesting topic.

Just shortly before reading this article I was thinking for myself about ways of detecting by preconditions and immediately interrupting program execution in case of a possible code loop in one of its blocks.

Opps.

At the same time, having downloaded the assert.mqh file, I added a line there:

And then in the code it looks like this:

That is, that and simply when constructing the code to apply the output of information with the expectation that by the end of work on the code then this output of "working" information can be easily removed (as many, I believe, probably did and do with the output of information at the stages of code construction).

Thanks for the feedback!

For TEST_TEXT to be really easy to remove by conditional compilation, I would consider putting Print inside the macro. In the current version, I think it is easy to remove TEST_TEXT, but not the Prints themselves.