ASSERT macro (and some background about asserts usage)

Share your advanced PureBasic knowledge/code with the community.
User avatar
luis
Addict
Addict
Posts: 3895
Joined: Wed Aug 31, 2005 11:09 pm
Location: Italy

ASSERT macro (and some background about asserts usage)

Post by luis »

My version of an ASSERT macro.
Nothing really difficult to write but I find ASSERT in general an invaluable tool while developing a new program, and this one has some nice little touches.

First of all I'll give a brief introduction to the benefits of ASSERT in general for the people who never used it.
Skip to the specifics of this one if you already know what an ASSERT is.

The ASSERT macro is a simple way to program defensively.
You basically say: at this point in my code this expression must be true or what follow does not make sense, can't operate as envisioned.

It's a way to double check the assumptions you make while developing your program, to test for all the things that never should go wrong but actually can when the code you are writing is expecting something to be in a certain state, and for some reason (an error somewhere else maybe) that's not the case.

An ASSERT constantly check if your program's environment is the way it should, and it will save you countless of times, spotting a problem before it can invisibly creep in a bigger problem apparently unrelated, one you'll end up discovering a lot later.

For example, if you know a pointer shouldn't be null, you ASSERT this before use it.
You *know* that pointer will not be null, right ?
But if you made a mistake ? You'll catch it immediately while still developing your program.

A good use for ASSERT is to check all the parameters and dependencies a procedure needs to work as expected.
The parameters part is obvious. With dependencies I mean all the rest. For example some procedures inside a library could be usable only if the library has been initialized. You could enforce that inside each procedure with an ASSERT.
Or a particular procedure can successfully operate on a object only if the object has been set to a valid state by another procedure first. Another job for an ASSERT.

Another good use is to check values you are about to return to the caller before exiting a procedure.
If you know the proc, when working correctly, must return values within a certain range, or values which must satisfy some conditions, ASSERT it just before the ProcedureReturn.

All those checks are time consuming ? They could be, but when you are ready to release your program you can make them vanish by defining a single constant.
You had all the benefit of strict checking without a permanent penalty, and your software will be a lot more robust from the start.
When you take the habit to use ASSERT it become a second nature, and you'll ask yourself how you missed it until now.
Well... if you already find tedious EnableExplicit probably you will hate all this, and I cannot save you :)

Obviously not all must be exclusively checked with an ASSERT. For example input by the user can contain something unexpected, or if your program access external data who knows what you may end up reading. So run time checks (and error handling) in the release build still have their place.
But with ASSERT you can check all the rest, and only in the debugging/developing phase.

ASSERT role is essentially to protect you from yourself, from your errors and to validate your assumptions.

The other runtime checks are there to protect your program from the unexpected (a missing file, a gibberish data input, etc.)

All this must be calibrated depending on the type of software you are developing.

One big important note: since the ASSERT macros are not compiled in the final release of the program, it's important you take the habit of not calling procs inside an ASSERT, or alter a value of a var. You must not alter the state of your program or removing the ASSERTs will make it behave differently.
This is exactly like using the debug command in PB with a function as its parameter. Same problem.

OK, now the specifics of this version.

You enable the ASSERTs with #ASSERT_ENABLED = 1

and set the optional title of the ASSERT window with #ASSERT_TITLE$ = "Test program 1.0"

Then you include the ASSERT code with IncludeFile "Assert.pb"

And finally you write your program.


The usage is:

ASSERT(<expression>)

or

ASSERT(<expression>, "text message")

The ASSERTs are included in the program with #ASSERT_ENABLED = 1 and completely removed with #ASSERT_ENABLED = 0.

When the program is running with the debugger enabled, and an ASSERT fails, you will see a window like this one:

Image

Continue will continue the execution.

Disable this ASSERT will disable *ONLY* this specific ASSERT for the entire execution of the program.
This is very useful if the failed ASSERT is inside a loop to avoid to click Continue forever to test another section without quitting.

Call Debugger will invoke the debugger on the line of the failed ASSERT.

When the program is running with the debugger disabled, the Call Debugger option is not available and is replaced by
End, which will close the program.

Simple test:

Code: Select all

; TEST

EnableExplicit

#ASSERT_ENABLED = 1
#ASSERT_TITLE$ = "Test program 1.0"

IncludeFile "Assert.pb"

Procedure Test1 (a,b,c)
 ASSERT(a > 0)
 ASSERT(b > 0)
 ASSERT(c > a + b)
EndProcedure

Procedure Test2 (val, *ptr)
 Protected a$
 ASSERT(val > 0) 
 ASSERT(*ptr > 0, "Pointer to string is null, PeekS() will fail !")  
 a$ = PeekS(*ptr)
EndProcedure

Define s$ = "string"

Test1(5,0,10) ; fails one assert 
Test1(5,0,1) ; fails two assert
Test1(1,2,5) ; ok

Test2(1, @s$) ; ok
Test2(5, #Null) ; fails one assert

Obviously in a real scenario you can do a lot of things and not only this trivial ones.
For example, you could check if a pointer to structured data is not null with an ASSERT.
If it pass the test, you could inspect the first bytes of that buffer to see if the structured data start with a magic number you know must be present.
This would give you a greater reassurance the pointer not only is not null, but with a high level of probability is pointing to valid data.

So you could stack the ASSERTs to check something more when the previous ASSERT validated an assumption for the second ASSERT.

Hope this will give you some ideas.

A link to wikipedia

That's it, the include is in the next post.


EDIT: if you want to have this code in module's form, see the conversion made here by Little John -> http://www.purebasic.fr/english/viewtop ... 39#p424239
Last edited by luis on Mon Jun 22, 2015 3:04 pm, edited 5 times in total.
"Have you tried turning it off and on again ?"
A little PureBasic review
User avatar
luis
Addict
Addict
Posts: 3895
Joined: Wed Aug 31, 2005 11:09 pm
Location: Italy

Re: ASSERT macro (and some background about asserts)

Post by luis »

Assert.pb

Code: Select all

;// ********************************************************************************
;// Assert.pb
;// An ASSERT macro on steroids.
;// by Luis, PB 5.10
;//
;// Use #PB_Editor_WordWrap (PB 5.10) instead of API, and a bigger font size for OSX
;//
;// forum: http://www.purebasic.fr/english/viewtopic.php?f=12&t=50842
;// ********************************************************************************


; if #ASSERT_ENABLED is not defined in your main program, automagically defaults to #ASSERT_ENABLED = 0
CompilerIf Defined(ASSERT_ENABLED, #PB_Constant) = 0
#ASSERT_ENABLED = 0
CompilerEndIf


CompilerIf (#ASSERT_ENABLED)

 ; if #ASSERT_TITLE$ is not defined in your main program, automagically defaults to "ASSERT"
 CompilerIf Defined(ASSERT_TITLE$, #PB_Constant) = 0
 #ASSERT_TITLE$ = "ASSERT" 
 CompilerEndIf

 Global NewMap AssertFlags.i()
 
 Macro _ASSERT_DQ_
  "
 EndMacro

Procedure.i AssertProcedure (Exp$, File$, Proc$, iLine, Msg$)
 ; This is called when Exp$ is false (see ASSERT macro)

 Protected Text$, StopText$, Title$
 Protected iRetCode, iEvent, nWin
 Protected nBtnContinue, nBtnSkipAsserts, nBtnStop, nEditor, nLabel
 Protected nFontEdit, nFontTitle, flgExit
 Protected w = 400, h = 240
 
 iRetCode = 0
  
 If FindMapElement(AssertFlags(), File$ + "_" + Str(iLine)) = 0 
    ; This is better than 
    ; If AssertFlags(File$ + "_" + Str(iLine)) = 0
    ; because it does not allocate data if an ASSERT has not been disabled.
    
    Title$ = #ASSERT_TITLE$
    
    CompilerIf (#PB_Compiler_Debugger = 1)
        StopText$ = " Call Debugger "
        Title$ + " (debug)"
    CompilerElse
        StopText$ = " End "
    CompilerEndIf
    
    If Proc$ : Proc$ + "()" : EndIf
           
    nWin = OpenWindow(#PB_Any, 0, 0, w, h, Title$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
    
    If nWin
        StickyWindow(nWin, 1)
        
        nFontTitle = LoadFont(#PB_Any, "Arial", 16, #PB_Font_Bold)
        
        CompilerIf (#PB_Compiler_OS = #PB_OS_MacOS)
         nFontEdit = LoadFont(#PB_Any, "Courier New", 12) ; suggested by WilliamL
        CompilerElse
         nFontEdit = LoadFont(#PB_Any, "Courier New", 9)   
        CompilerEndIf                  
              
        nLabel= TextGadget(#PB_Any, 10, 8, w-20, 20, "ASSERT FAILED !", #PB_Text_Center)
        
        nEditor = EditorGadget(#PB_Any, 10, 40, w-20, 155, #PB_Editor_ReadOnly | #PB_Editor_WordWrap)
        nBtnContinue = ButtonGadget(#PB_Any, 10, h-35, 120, 30, " Continue ")
        nBtnSkipAsserts = ButtonGadget(#PB_Any, 140, h-35, 120, 30, " Disable this ASSERT ")
        nBtnStop = ButtonGadget(#PB_Any, 270, h-35, 120, 30, StopText$, #PB_Button_Default)
        

        Text$ = "Expr: " + Exp$ + #LF$ + #LF$ 
        Text$ + "File: " + GetFilePart(File$) + #LF$ +#LF$         
        Text$ + "Proc: " + Proc$ + #LF$ + #LF$  
        Text$ + "Line: " + Str(iLine)    
        
        If Msg$
            Text$ + #LF$ + #LF$ + Msg$ 
        EndIf
            
        SetGadgetFont(nLabel, FontID(nFontTitle))
        SetGadgetFont(nEditor, FontID(nFontEdit))
        SetGadgetText(nEditor, Text$)
                
        Repeat 
            iEvent = WaitWindowEvent()
            
            Select iEvent
                Case #PB_Event_CloseWindow 
                    iRetCode = 0
                    flgExit = 1                
                Case #PB_Event_Gadget
                    Select EventGadget()
                        Case nBtnContinue
                            iRetCode = 0
                            flgExit = 1
                        Case nBtnSkipAsserts
                            AssertFlags(File$ + "_" + Str(iLine)) = 1
                            iRetCode = 0
                            flgExit = 1
                        Case nBtnStop
                           CompilerIf (#PB_Compiler_Debugger = 1)
                            iRetCode = 1
                            flgExit = 1
                           CompilerElse
                            End
                           CompilerEndIf                                            
                    EndSelect
            EndSelect        
        Until flgExit = 1
             
        CloseWindow(nWin)
        
        FreeFont(nFontTitle)
        FreeFont(nFontEdit)        
     EndIf         
 EndIf
   
 ProcedureReturn iRetCode
EndProcedure

CompilerEndIf


Macro ASSERT (exp, msg = "")
; [DESC]
; Check the validity of the expression and if not true stops the execution showing a warning window.
;
; [INPUT]
; exp : The expression to be checked.
; msg: An optional message to show if the assert fails.
;
; [NOTES]
; Asserts can be used both in the debugged program and the final exe, and are inserted in the code only if #ASSERT_ENABLED = 1
;
; If a check fails the user have three options:
; - continue 
; - continue skipping further notifications by this specific assert
; - stop the program at the offending line through a call to CallDebugger() or End it if the debugger is disabled.
;
; You can't use string literals in the expression to be evaluated, shouldn't be a problem in practice.
; You can't use more than one ASSERT for each line.

 CompilerIf (#ASSERT_ENABLED)
  If Not (exp) 
    If AssertProcedure (_ASSERT_DQ_#exp#_ASSERT_DQ_, #PB_Compiler_File, #PB_Compiler_Procedure, #PB_Compiler_Line, msg) 
        CallDebugger
    EndIf
  EndIf
 CompilerEndIf
EndMacro 

Last edited by luis on Thu Jan 17, 2013 4:47 pm, edited 1 time in total.
"Have you tried turning it off and on again ?"
A little PureBasic review
User avatar
idle
Always Here
Always Here
Posts: 5844
Joined: Fri Sep 21, 2007 5:52 am
Location: New Zealand

Re: ASSERT macro (and some background about asserts)

Post by idle »

looks very handy Luis, thanks
Windows 11, Manjaro, Raspberry Pi OS
Image
yrreti
Enthusiast
Enthusiast
Posts: 546
Joined: Tue Oct 31, 2006 4:34 am

Re: ASSERT macro (and some background about asserts)

Post by yrreti »

Thanks also Luis,
This could definitely come in handy, especially with lots of
code lines to follow through, trying to find what shouldn't have
but did go wrong.
Thanks for sharing this.

yrreti
User avatar
luis
Addict
Addict
Posts: 3895
Joined: Wed Aug 31, 2005 11:09 pm
Location: Italy

Re: ASSERT macro (and some background about asserts)

Post by luis »

You are welcome :)
If anyone know how this could be modified to support string literals too in the expressions

for example: ASSERT(a$ <> "ABC")

I would like to know it. I never really missed it probably because I always need to test numeric values (pointers, return values, memory data and so on), but it would be nice nevertheless.
"Have you tried turning it off and on again ?"
A little PureBasic review
Little John
Addict
Addict
Posts: 4779
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: ASSERT macro (and some background about asserts usage)

Post by Little John »

Hi Luis,

I just discovered this code.
Very useful, thank you!!

Best regards, Little John
c4s
Addict
Addict
Posts: 1981
Joined: Thu Nov 01, 2007 5:37 pm
Location: Germany

Re: ASSERT macro (and some background about asserts usage)

Post by c4s »

Little John wrote:I just discovered this code.
Very useful, thank you!!
Same here!
If any of you native English speakers have any suggestions for the above text, please let me know (via PM). Thanks!
Little John
Addict
Addict
Posts: 4779
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: ASSERT macro (and some background about asserts usage)

Post by Little John »

Hi Luis,

I've changed your fine code a little, so that it is inside a module and thus can be used by other modules.

For using this version of the macro, we have to write #ASSERT_ENABLED = #True in the main program scope, as before. Then we have to use Assert::... or UseModule Assert. In order to keep things simple, #ASSERT_TITLE$ can't be changed here by the main program, and is always "ASSERT".
This code is just a suggestion. If you don't like it, it's easy for me to remove it from the forum. :-)

Code: Select all

;// ********************************************************************************
;// Assert_520.pbi
;// An ASSERT macro on steroids.
;// by Luis, PB 5.10
;// > slightly changed by Little John for usage with modules (PB 5.20+)
;//
;// Use #PB_Editor_WordWrap (PB 5.10) instead of API, and a bigger font size for OSX
;//
;// forum: http://www.purebasic.fr/english/viewtopic.php?f=12&t=50842
;// ********************************************************************************


; if #ASSERT_ENABLED is not defined in your main program, automagically defaults to #ASSERT_ENABLED = 0
CompilerIf Defined(ASSERT_ENABLED, #PB_Constant) = 0
   #ASSERT_ENABLED = 0
CompilerEndIf


CompilerIf #ASSERT_ENABLED
   
   DeclareModule Assert
      #ASSERT_TITLE$ = "ASSERT"
      
      Global NewMap AssertFlags.i()
      
      Macro _ASSERT_DQ_
         "
      EndMacro
      
      Declare.i AssertProcedure (Exp$, File$, Proc$, iLine, Msg$)
      
      
      Macro ASSERT (exp, msg = "")
         ; [DESC]
         ; Check the validity of the expression and if not true stops the execution showing a warning window.
         ;
         ; [INPUT]
         ; exp : The expression to be checked.
         ; msg: An optional message to show if the assert fails.
         ;
         ; [NOTES]
         ; Asserts can be used both in the debugged program and the final exe, and are inserted in the code only if #ASSERT_ENABLED = 1
         ;
         ; If a check fails the user has three options:
         ; - continue
         ; - continue skipping further notifications by this specific assert
         ; - stop the program at the offending line through a call to CallDebugger() or End it if the debugger is disabled.
         ;
         ; You can't use string literals in the expression to be evaluated, shouldn't be a problem in practice.
         ; You can't use more than one ASSERT for each line.
         
         If Not (exp)
            If AssertProcedure (_ASSERT_DQ_#exp#_ASSERT_DQ_, #PB_Compiler_File, #PB_Compiler_Procedure, #PB_Compiler_Line, msg)
               CallDebugger
            EndIf
         EndIf
      EndMacro
   EndDeclareModule
   
   
   Module Assert
      Procedure.i AssertProcedure (Exp$, File$, Proc$, iLine, Msg$)
         ; This is called when Exp$ is false (see ASSERT macro)
         
         Protected Text$, StopText$, Title$
         Protected iRetCode, iEvent, nWin
         Protected nBtnContinue, nBtnSkipAsserts, nBtnStop, nEditor, nLabel
         Protected nFontEdit, nFontTitle, flgExit
         Protected w = 400, h = 240
         
         iRetCode = 0
         
         If FindMapElement(AssertFlags(), File$ + "_" + Str(iLine)) = 0
            ; This is better than
            ; If AssertFlags(File$ + "_" + Str(iLine)) = 0
            ; because it does not allocate data if an ASSERT has not been disabled.
            
            Title$ = #ASSERT_TITLE$
            
            CompilerIf #PB_Compiler_Debugger = 1
               StopText$ = " Call Debugger "
               Title$ + " (debug)"
            CompilerElse
               StopText$ = " End "
            CompilerEndIf
            
            If Proc$ : Proc$ + "()" : EndIf
            
            nWin = OpenWindow(#PB_Any, 0, 0, w, h, Title$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
            
            If nWin
               StickyWindow(nWin, 1)
               
               nFontTitle = LoadFont(#PB_Any, "Arial", 16, #PB_Font_Bold)
               
               CompilerIf #PB_Compiler_OS = #PB_OS_MacOS
                  nFontEdit = LoadFont(#PB_Any, "Courier New", 12) ; suggested by WilliamL
               CompilerElse
                  nFontEdit = LoadFont(#PB_Any, "Courier New", 9)   
               CompilerEndIf                 
               
               nLabel= TextGadget(#PB_Any, 10, 8, w-20, 20, "ASSERT FAILED !", #PB_Text_Center)
               
               nEditor = EditorGadget(#PB_Any, 10, 40, w-20, 155, #PB_Editor_ReadOnly | #PB_Editor_WordWrap)
               nBtnContinue = ButtonGadget(#PB_Any, 10, h-35, 120, 30, " Continue ")
               nBtnSkipAsserts = ButtonGadget(#PB_Any, 140, h-35, 120, 30, " Disable this ASSERT ")
               nBtnStop = ButtonGadget(#PB_Any, 270, h-35, 120, 30, StopText$, #PB_Button_Default)
               
               
               Text$ = "Expr: " + Exp$ + #LF$ + #LF$
               Text$ + "File: " + GetFilePart(File$) + #LF$ +#LF$         
               Text$ + "Proc: " + Proc$ + #LF$ + #LF$ 
               Text$ + "Line: " + Str(iLine)   
               
               If Msg$
                  Text$ + #LF$ + #LF$ + Msg$
               EndIf
               
               SetGadgetFont(nLabel, FontID(nFontTitle))
               SetGadgetFont(nEditor, FontID(nFontEdit))
               SetGadgetText(nEditor, Text$)
               
               Repeat
                  iEvent = WaitWindowEvent()
                  
                  Select iEvent
                     Case #PB_Event_CloseWindow
                        iRetCode = 0
                        flgExit = 1               
                     Case #PB_Event_Gadget
                        Select EventGadget()
                           Case nBtnContinue
                              iRetCode = 0
                              flgExit = 1
                           Case nBtnSkipAsserts
                              AssertFlags(File$ + "_" + Str(iLine)) = 1
                              iRetCode = 0
                              flgExit = 1
                           Case nBtnStop
                              CompilerIf #PB_Compiler_Debugger = 1
                                 iRetCode = 1
                                 flgExit = 1
                              CompilerElse
                                 End
                              CompilerEndIf                                           
                        EndSelect
                  EndSelect       
               Until flgExit = 1
               
               CloseWindow(nWin)
               
               FreeFont(nFontTitle)
               FreeFont(nFontEdit)       
            EndIf         
         EndIf
         
         ProcedureReturn iRetCode
      EndProcedure
   EndModule
   
CompilerElse
   
   DeclareModule Assert
      Macro ASSERT (exp, msg = "")
      EndMacro
   EndDeclareModule
   
   Module Assert
   EndModule
CompilerEndIf
User avatar
luis
Addict
Addict
Posts: 3895
Joined: Wed Aug 31, 2005 11:09 pm
Location: Italy

Re: ASSERT macro (and some background about asserts usage)

Post by luis »

Little John wrote: This code is just a suggestion. If you don't like it, it's easy for me to remove it from the forum. :-)
I'm not the Forum's Dictator yet ! :mrgreen:
Anyone is free to propose a different version of a T&T of mine as long as mine is not affected, but thanks for your consideration :)

In the specific: good idea, I've updated my original "TEST" for completeness using your variant, see if it looks ok to you too.

Code: Select all


; TEST

EnableExplicit

#ASSERT_ENABLED = 1
IncludeFile "Assert_520.pbi"

DeclareModule x
 Declare 	Test1 (a,b,c)
 Declare 	Test2 (val, *ptr)
EndDeclareModule

Module x ; module x use the Assert module
 UseModule Assert
 
Procedure Test1 (a,b,c)
 ASSERT(a > 0)
 ASSERT(b > 0)
 ASSERT(c > a + b)
EndProcedure

Procedure Test2 (val, *ptr)
 Protected a$
 ASSERT(val > 0)
 ASSERT(*ptr > 0, "Pointer to string is null, PeekS() will fail !") 
 a$ = PeekS(*ptr)
EndProcedure
EndModule



UseModule x

Define s$ = "string"

Test1(5,0,10) ; fails one assert
Test1(5,0,1) ; fails two assert
Test1(1,2,5) ; ok

Test2(1, @s$) ; ok
Test2(5, #Null) ; fails one assert
I left the original post as it was so we can have both versions, I've just added a link to your post above.
"Have you tried turning it off and on again ?"
A little PureBasic review
Little John
Addict
Addict
Posts: 4779
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: ASSERT macro (and some background about asserts usage)

Post by Little John »

Thank you.
Post Reply