Posts Tagged Debugging

First test case using get_median

Here is a simple test script that will run get_median seven times, passing in a variety of parameters along the way. The only valid case occurs when the first parameter of “1″ is passed and parameters two and three are valid. This seemingly mindless test case (which many astute and seasoned programmers will scoff at) actually does a lot for me. First, it helps to ensure that any future changes I make to this or similar functions will run when unexpected parameters are passed in. Second, it allowed me to create a few useful functions that can be re-used on other test cases. Third, it forms the basis of a testing project that can be built upon as time and resources permit. The premise is that all my test cases can be chained together (preferably in a table) and run just before the project reaches QA or system testing.

What is left to do? For starters, this test script can be enhanced to not only pass in a string of values, but also an array or cursor. It is possible that doing so will introduce other issues that will need to be fixed. The end result, nevertheless, will be one bullet-proof get_median function that is as flexible as it is safe! In addition, this code will form the basis of a test class and be added to a testing project. For now, I’ll leave it in PRG form for simplicity.

Tomorrow, I’ll post an updated get_median that fixes many of the bugs, while incorporating many of my boundary testing examples from prior entries.

*-- The following code is my first test of get_median
ON ERROR ;
    STRTOFILE(capture_error(ERROR( ), MESSAGE( ), MESSAGE(1),;
    PROGRAM( ), LINENO( )),"test_err.log",1)

*-- What if the driver is unexpected, or para 2 and 3 are invalid?
FOR x = -2 TO 4
    ePara2 = get_string(8)
    epara3 = ","
    eRetVal = get_median_clean(x,ePara2,ePara3)
 
    STRTOFILE(TTOC(DATETIME())+ " get_median " + ;
        PADL(TRANSFORM(x),4) + CHR(9) + ;
        TRANSFORM(ePara2) + CHR(9) + ;
        TRANSFORM(epara3) + CHR(9) + ;
        TRANSFORM(eRetVal) + CHR(13) + CHR(10) ,"test_run.log",1)
 
NEXT
 
FUNCTION get_string()
    LPARA tnSize
 
    tnSize = IIF(VARTYPE(tnSize)<>"N",0,tnSize)
    LOCAL lcString
    lcString = ALLTRIM(STR(get_int()))
    FOR n = 2 TO tnSize
        lcString = lcString + "," + ALLTRIM(STR(get_int()))
    NEXT
    RETURN lcString
ENDFUNC
 
FUNCTION get_int
    RETURN (RAND()*1000) - 500
ENDFUNC 
 
FUNCTION capture_error (merror, mess, mess1, mprog, mlineno)
    LOCAL cMess, cCRLF
    cCRLF = CHR(13) + CHR(10)
    cMess = 'Error number: ' + LTRIM(STR(merror)) + cCRLF + ;
            'Error message: ' + mess + cCRLF + ;
            'Line of code with error: ' + mess1 + cCRLF + ;
            'Line number: ' + LTRIM(STR(mlineno)) + cCRLF + ;
            'Program with error: ' + mprog + cCRLF + ;
            REPLICATE("=",30) + cCRLF
    RETURN cMess
ENDFU
*-- What if the driver is unexpected, or para 2 and 3 are invalid?
FOR x = -2 TO 4
    ePara2 = get_string(8)
    epara3 = ","
    eRetVal = get_median_clean(x,ePara2,ePara3)
 
    STRTOFILE(TTOC(DATETIME())+ " get_median " + ;
        PADL(TRANSFORM(x),4) + CHR(9) + ;
        TRANSFORM(ePara2) + CHR(9) + ;
        TRANSFORM(epara3) + CHR(9) + ;
        TRANSFORM(eRetVal) + CHR(13) + CHR(10) ,"test_run.log",1)
 
NEXT
 
FUNCTION get_string()
    LPARA tnSize
 
    tnSize = IIF(VARTYPE(tnSize)<>"N",0,tnSize)
    LOCAL lcString
    lcString = ALLTRIM(STR(get_int()))
    FOR n = 2 TO tnSize
        lcString = lcString + "," + ALLTRIM(STR(get_int()))
    NEXT
    RETURN lcString
ENDFUNC
 
FUNCTION get_int
    RETURN (RAND()*1000) - 500
ENDFUNC 
 
FUNCTION capture_error (merror, mess, mess1, mprog, mlineno)
    LOCAL cMess, cCRLF
    cCRLF = CHR(13) + CHR(10)
    cMess = 'Error number: ' + LTRIM(STR(merror)) + cCRLF + ;
            'Error message: ' + mess + cCRLF + ;
            'Line of code with error: ' + mess1 + cCRLF + ;
            'Line number: ' + LTRIM(STR(mlineno)) + cCRLF + ;
            'Program with error: ' + mprog + cCRLF + ;
            REPLICATE("=",30) + cCRLF
    RETURN cMess
ENDFUNC

Tags: , , , ,

No Comments

Boundary Testing — CASE: Get Median function

Last week I talked about boundary testing parameters with a promise to discuss testing methods this week. I’m going to adjust this promise a little due to the magnitude of the task. I felt it would be beneficial to review a case first.

This week, I present a simple program that I wrote several years ago that calculates the median of a range of values. I have enhanced (famous last words) the program to accept a variety of inputs that will help to demonstrate my boundary testing example next week. It has a driver parameter (tnType) and two additional parameters (teName, teColDel) that hold different data types depending on the driver.

There is no concept of overloading functions in VFP. The technique I am using for the get_median fucntion below simulates this concept. Finding the median of a cursor, array or delimeted string is not particularly hard to do, but developing a bullet-proof solution that handles each case in one shot, is.

The following program works, but has several flaws. Next week I will present an improved get_median function that addresses these flaws. I’ll expose some of them using boundary testing routines.

*-- The following creates a simple test to run get_median
cString = "1,2,3,4,5"
CREATE CURSOR crTemp (col1 c(1), value n(1))
INSERT INTO crTemp VALUES ("1",1)
INSERT INTO crTemp VALUES ("2",2)
INSERT INTO crTemp VALUES ("3",3)
INSERT INTO crTemp VALUES ("4",4)
 
DIMENSION aTestMedian[5,2]
aTestMedian[1,1] = 1
aTestMedian[2,1] = 2
aTestMedian[3,1] = 3
aTestMedian[4,1] = 4
aTestMedian[5,1] = 5
 
? get_median(1,cString,',')
? get_median(2,'crTemp','value')
? get_median(3,'aTestMedian',1)

*-- The following is the first version of get_median
FUNCTION get_median
    LPARAMETERS tnType, teName, teColDel
 
    *-- local declerations
    LOCAL lcSelCol AS String
    LOCAL lnItems, lnHalf, lnInc AS Integer
    LOCAL lnMedian AS Number
 
    *-- create the aMedian Array
    DO CASE
    CASE tnType = 1        && use a delimted string
        lnItems= ALINES( aMedian , STRTRAN(teName ,teColDel,CHR(13)) )
        FOR lnInc = 1 TO lnItems
            aMedian[lnInc] = VAL(aMedian[lnInc])
        NEXT
    CASE tnType = 2       && calculate using an alias
        lcSelCol = FIELD(teColDel,teName)
        lcSQL = "SELECT " + lcSelCol + " FROM " + teName + ;
              " ORDER BY " + lcSelCol + " INTO ARRAY aMedian"
        &lcSQL
        lnItems= _TALLY
    CASE tnType = 3        && calculate using an array
        lnItems= ALEN(&teName,1)
        DIMENSION aMedian[lnItems]
        FOR lnInc = 1 TO lnItems
            aMedian[lnInc] = &teName.[lnInc,teColDel]
        NEXT
    ENDCASE
 
   *-- find the medianASORT(aMedian)
    lnHalf = CEILING( lnItems / 2)
    IF MOD(lnHalf,2) = 0
        lnMedian = ( aMedian[lnHalf] + aMedian[lnHalf+1] ) / 2
    ELSE
        lnMedian = aMedian[lnHalf]
    ENDIF
 
    RETURN lnMedian
 
ENDFU
*-- The following is the first version of get_median
FUNCTION get_median
    LPARAMETERS tnType, teName, teColDel
 
    *-- local declerations
    LOCAL lcSelCol AS String
    LOCAL lnItems, lnHalf, lnInc AS Integer
    LOCAL lnMedian AS Number
 
    *-- create the aMedian Array
    DO CASE
    CASE tnType = 1        && use a delimted string
        lnItems= ALINES( aMedian , STRTRAN(teName ,teColDel,CHR(13)) )
        FOR lnInc = 1 TO lnItems
            aMedian[lnInc] = VAL(aMedian[lnInc])
        NEXT
    CASE tnType = 2       && calculate using an alias
        lcSelCol = FIELD(teColDel,teName)
        lcSQL = "SELECT " + lcSelCol + " FROM " + teName + ;
              " ORDER BY " + lcSelCol + " INTO ARRAY aMedian"
        &lcSQL
        lnItems= _TALLY
    CASE tnType = 3        && calculate using an array
        lnItems= ALEN(&teName,1)
        DIMENSION aMedian[lnItems]
        FOR lnInc = 1 TO lnItems
            aMedian[lnInc] = &teName.[lnInc,teColDel]
        NEXT
    ENDCASE
 
   *-- find the medianASORT(aMedian)
    lnHalf = CEILING( lnItems / 2)
    IF MOD(lnHalf,2) = 0
        lnMedian = ( aMedian[lnHalf] + aMedian[lnHalf+1] ) / 2
    ELSE
        lnMedian = aMedian[lnHalf]
    ENDIF
 
    RETURN lnMedian
 
ENDFUNC

Tags: , , , ,

No Comments

Boundary Testing Parameters

Programs (method code, procedures, etc.) must be checked for invalid parameters. This type of testing falls within the Black Box Testing category. There are many good arguments to support the notion that boundary testing is essential. The counter argument is that a developer, through the analysis of business logic, form design, and database management has already handled boundary issues at higher levels and that testing within a program would be redundant. This argument is flawed in my opinion. In an age where code reuse is stressed and when multiple developers work on the same project, boundary tests become invaluable. In addition, boundary testing will catch hidden problems in the source code, saving you and your clients time and aggravation.

So now you’re on the boundary test bandwagon; let’s consider the simplicity of the task. There are actually two elements to these tests: preparing programs for out-of-bounds values and writing test scripts that will exercise these conditions. I’ll tackle the former in this article and the latter next week.

In many cases a simple IF construct will suffice at the top of the program to verify that the parameter passed is within the expected range. It is a good practice to consider the range of known good values and work from there. Using functions such as EVL, BETWEEN, INLIST, ISNULL, and EMPTY are a good way to start. But before we go any further, it is worth mentioning that not all parameters should be treated equally. Your boundary tests will need to consider the purpose of the parameter and not just its value. For example, does the parameter control the flow of the procedure (aka a “driver”) or is it data that will be inserted into a table or used in a calculation? Boundary tests should also be clever enough to account for parameters passed by reference or value.

Processing speed should not be an issue. Many of these checks are simple enough not to bring your application to a grinding halt. If a function is to be called repeatedly in a loop, then you may consider wrapping your boundary tests in pre-processor directives. You can compile different testing and productions versions quite easily using this method.

To test parameter boundaries at the top of a program, consider some of the following techniques:

*-- using the conditional if
tnNumber = IIF(BETWEEN(tnNumber,nLowValue,nHighValue),;
        tnNumber,nLowValue)
tcFormMode = IIF(INLIST(tcFormMode,"add","edit","readonly"),;
        tcFormMode,"readonly")

*-- using EVL() to make sure a parameter is not empty
*-- Note: The numeric value of 0 evaluates to empty
tcPara1 = EVL(tcPara1,"Processing...")
tnPara2 = EVL(tnPara2,1)
tdPara3 = EVL(tdPara3,DATE())
 
*-- using the if block
IF LEN(ALLTRIM(tcLastName)) > 20
    MESSAGEBOX("The last name is too long!")
ENDIF
 
*-- using one parameter to test another (VFP9 example with ICASE)
*-- this example checks a security parameter and returns the account
*-- number if security is 1 (an administrator), if security is 2, a
*-- masked  account will be returned. Otherwise, nothing is returned.
tcAccount = ;
    ICASE(tnSecurity=1,tcAccount,;
        tnSecurity=2,CHRTRAN(cNum,lcString,"**********"),'')
 
*-- Directories and filenames should be put through the ringer.
tcFileName = DEFAULTEXT(tcFileName,"txt")
tcFileName = FORCEEXT(tcFileName,"txt")
tcFileName = IIF(FILE(tcFileName),tcFileName,GETFILE(
*-- using EVL() to make sure a parameter is not empty
*-- Note: The numeric value of 0 evaluates to empty
tcPara1 = EVL(tcPara1,"Processing...")
tnPara2 = EVL(tnPara2,1)
tdPara3 = EVL(tdPara3,DATE())
 
*-- using the if block
IF LEN(ALLTRIM(tcLastName)) > 20
    MESSAGEBOX("The last name is too long!")
ENDIF
 
*-- using one parameter to test another (VFP9 example with ICASE)
*-- this example checks a security parameter and returns the account
*-- number if security is 1 (an administrator), if security is 2, a
*-- masked  account will be returned. Otherwise, nothing is returned.
tcAccount = ;
    ICASE(tnSecurity=1,tcAccount,;
        tnSecurity=2,CHRTRAN(cNum,lcString,"**********"),'')
 
*-- Directories and filenames should be put through the ringer.
tcFileName = DEFAULTEXT(tcFileName,"txt")
tcFileName = FORCEEXT(tcFileName,"txt")
tcFileName = IIF(FILE(tcFileName),tcFileName,GETFILE(
*-- Note: The numeric value of 0 evaluates to empty
tcPara1 = EVL(tcPara1,"Processing...")
tnPara2 = EVL(tnPara2,1)
tdPara3 = EVL(tdPara3,DATE())

*-- using the if block
IF LEN(ALLTRIM(tcLastName)) > 20
    MESSAGEBOX("The last name is too long!")
ENDIF
 
*-- using one parameter to test another (VFP9 example with ICASE)
*-- this example checks a security parameter and returns the account
*-- number if security is 1 (an administrator), if security is 2, a
*-- masked  account will be returned. Otherwise, nothing is returned.
tcAccount = ;
    ICASE(tnSecurity=1,tcAccount,;
        tnSecurity=2,CHRTRAN(cNum,lcString,"**********"),'')
 
*-- Directories and filenames should be put through the ringer.
tcFileName = DEFAULTEXT(tcFileName,"txt")
tcFileName = FORCEEXT(tcFileName,"txt")
tcFileName = IIF(FILE(tcFileName),tcFileName,GETFILE(
*-- using the if block
IF LEN(ALLTRIM(tcLastName)) > 20
    MESSAGEBOX("The last name is too long!")
ENDIF

*-- using one parameter to test another (VFP9 example with ICASE)
*-- this example checks a security parameter and returns the account
*-- number if security is 1 (an administrator), if security is 2, a
*-- masked  account will be returned. Otherwise, nothing is returned.
tcAccount = ;
    ICASE(tnSecurity=1,tcAccount,;
        tnSecurity=2,CHRTRAN(cNum,lcString,"**********"),'')
 
*-- Directories and filenames should be put through the ringer.
tcFileName = DEFAULTEXT(tcFileName,"txt")
tcFileName = FORCEEXT(tcFileName,"txt")
tcFileName = IIF(FILE(tcFileName),tcFileName,GETFILE(
*-- using one parameter to test another (VFP9 example with ICASE)
*-- this example checks a security parameter and returns the account
*-- number if security is 1 (an administrator), if security is 2, a
*-- masked  account will be returned. Otherwise, nothing is returned.
tcAccount = ;
    ICASE(tnSecurity=1,tcAccount,;
        tnSecurity=2,CHRTRAN(cNum,lcString,"**********"),'')
 
*-- Directories and filenames should be put through the ringer.
tcFileName = DEFAULTEXT(tcFileName,"txt")
tcFileName = FORCEEXT(tcFileName,"txt")
tcFileName = IIF(FILE(tcFileName),tcFileName,GETFILE(
*-- this example checks a security parameter and returns the account
*-- number if security is 1 (an administrator), if security is 2, a
*-- masked  account will be returned. Otherwise, nothing is returned.
tcAccount = ;
    ICASE(tnSecurity=1,tcAccount,;
        tnSecurity=2,CHRTRAN(cNum,lcString,"**********"),'')
 
*-- Directories and filenames should be put through the ringer.
tcFileName = DEFAULTEXT(tcFileName,"txt")
tcFileName = FORCEEXT(tcFileName,"txt")
tcFileName = IIF(FILE(tcFileName),tcFileName,GETFILE(
*-- number if security is 1 (an administrator), if security is 2, a
*-- masked  account will be returned. Otherwise, nothing is returned.
tcAccount = ;
    ICASE(tnSecurity=1,tcAccount,;
        tnSecurity=2,CHRTRAN(cNum,lcString,"**********"),'')
 
*-- Directories and filenames should be put through the ringer.
tcFileName = DEFAULTEXT(tcFileName,"txt")
tcFileName = FORCEEXT(tcFileName,"txt")
tcFileName = IIF(FILE(tcFileName),tcFileName,GETFILE(
*-- masked  account will be returned. Otherwise, nothing is returned.
tcAccount = ;
    ICASE(tnSecurity=1,tcAccount,;
        tnSecurity=2,CHRTRAN(cNum,lcString,"**********"),'')

*-- Directories and filenames should be put through the ringer.
tcFileName = DEFAULTEXT(tcFileName,"txt")
tcFileName = FORCEEXT(tcFileName,"txt")
tcFileName = IIF(FILE(tcFileName),tcFileName,GETFILE(
*-- Directories and filenames should be put through the ringer.
tcFileName = DEFAULTEXT(tcFileName,"txt")
tcFileName = FORCEEXT(tcFileName,"txt")
tcFileName = IIF(FILE(tcFileName),tcFileName,GETFILE())

Next week I will explore ways of writing test scripts to pound programs into oblivion, attempting to break them every which way. It may get bloody so perhaps it is appropriate that this entry will come out next week around Halloween!

Tags: , ,

No Comments

Using ASSERTS with VARTYPE to Test Parameters

I am always looking for ways to bullet-proof my code. One way to do this is to test parameter boundaries and types at the top of programs and methods to ensure that what we are getting is what is expected. Using VARTYPE with ASSERTS is a great way to get started.

PCOUNT(), according the documentation, returns the number of parameters passed to the current program, procedure, or user-defined function. If you are expecting 3 and are passed 2, you can use PCOUNT to handle the problem. PARAMETERS( ) does almost the same thing but can be thrown off by ON KEY LABELS and is reset every time a program, procedure, or user-defined function is called. In either case, relying on PCOUNT or PARAMETERS only gets you half the way there. What if the types are wrong? What if a developer sends in the correct number of parameters but in the wrong order?

VARTYPE() returns the data type of the expression passed to it. It can determine if an object was passed, a Date, or any of the other data types common to the language. If an array is passed, the first element is evaluated. In VFP9 the TYPE() command contains a second argument that you can use to check if a variable is an array. In VFP7, code using VARTYPE() will need to check for an individual element (or elements!). VARTYPE() also supports nulls and can return the data type of a null variable as well. VARTYPE( ) does not require the use of quotation marks around the variable name and is much faster than TYPE(). This makes it preferable in loops or at the top of code that may be run multiple times in succession.

So why use ASSERTS?

ASSERTS are used to verify assumptions you have about the run-time environment. They are great because they are ignored in production (when the exe is run directly from explorer). This means that you can use them in a development environment to bullet-proof your code and not suffer a significant performance hit when the code is shipped to the client (the command doesn’t disappear, it is just ignored). If performance is a major concern, stick #DEFINE ASSERT NOTE at the top of the function and your ASSERTS become comments.

There are situations that you will want to use VARTYPE without an ASSERT no matter what (an example would be in a case where the user’s action dictates the parameter types or order). I find these scenarios to be rare or at the least, easily identifiable.

Next entry, I’ll discuss some methods to boundary check parameters.

Tags: , ,

No Comments

FoxPro’s Sometimes-Mysterious Replace

“The English country gentleman galloping after a fox – The unspeakable in full pursuit of the uneatable.”
-Oscar Wilde

The REPLACE command is a powerful command that allows developers to update fields in a table using the currently selected record (NEXT 1), a scope, or a set of records. It is also faster than SQL-UPDATE. The basic syntax is easy to understand, but its simplicity (REPLACE SomeFIeld with SomeValue) can get some developers into trouble – especially when no error or warning is raised.

Here are some examples:

1. REPLACE will not replace anything under the following conditions:

a. When the currently selected work area is at EOF:

CREATE CURSOR myCursor (mydecimal N(8,2) )
APPEND BLANK IN myCursor
CREATE CURSOR myEOFCursor (foo c(1))
REPLACE myCursor.mydecimal WITH 111.00
*-- Nothing happe
*-- Nothing happens

To fix code like this, use the “IN” clause of the REPLACE statement.

REPLACE myCursor.mydecimal WITH 111.00 IN myCursor
*-- Works like a cha
*-- Works like a charm

b. Likewise, the following code doesn’t do a thing. Although expected, there is no warning or error that the REPLACE failed:

CREATE CURSOR myCursor (mydecimal N(8,2) )
REPLACE myCursor.mydecimal WITH 123.00

c. Consider this seemingly harmless code:

CREATE CURSOR myCursor (some_int i , some_char c(3))
INDEX ON some_int TAG some_int
FOR x = 1 TO 100
    APPEND BLANK
NEXT
LOCATE
SCAN
    REPLACE some_int WITH RECNO() IN myCursor
    REPLACE some_char WITH ALLT(STR(RECNO())) IN myCursor
ENDSCAN

You would be surprised to see the results (only the first record is replaced). Now comment out the line “INDEX ON some_int TAG some_int” and rerun the code. The moral here has more to do with how FoxPro handles indexes than how it handles the REPLACE command. It is better to turn off indexes (or create them after a replace) when indexed field rely on the replace to populate it. Someday I may take the time to explain what FoxPro is doing behind the scenes.

2.) REPLACE will automatically round decimals that do not fit:

CREATE CURSOR myCursor (mydecimal N(8,2))
APPEND BLANK IN myCursor
REPLACE mydecimal WITH 12.119 IN myCursor
*-- This stores the value 12.12 into the curs
*-- This stores the value 12.12 into the cursor

3.) If the value doesn’t fit at all, VFP will truncate the decimal!

REPLACE mydecimal WITH 12345678.119 IN myCursor
*-- This stores 12345678 into the field,
*-- eliminating the decimal entire
*-- This stores 12345678 into the field,
*-- eliminating the decimal entire
*-- eliminating the decimal entirely

Additional notes regarding the REPLACE command:

1.) If updating multiple fields, do not use this syntax:

REPLACE firstname WITH "Tod" IN contact
REPLACE lastname WITH "McKenna" IN contact

The above syntax forces foxpro to execute the command twice. Use this instead:

REPLACE firstname WITH "Tod" ,;
    lastname WITH "McKenna" IN contact

2.) You may only include up to 128 statements using the above technique

Summary

To summarize, follow these rules when using REPLACE:

  1. Always use the “IN” clause!
  2. Examine decimal data and decide if boundary checks are necessary before a REPLACE.
  3. _TALLY will let you know if a REPLACE occurred. If you want to be absolutely sure that a REPLACE happened, you can try to ASSERT _TALLY>0 after your REPLACE command. This will alert you while testing your application that a REPLACE didn’t occur.

Your questions and comments are welcome!

-Tod

Tags: ,

No Comments