Special Edition Using Visual FoxPro 6
Special Edition Using Visual FoxPro 6
Error Detection and Handling
- Getting Started When You Have a Problem
- Recognizing Common Coding Errors
- Modularizing Code to Minimize Errors
- Using Proper Parameter Passing
- Eliminating Multiple Exits and Returns
- Developing Libraries of Testing Routines and Objects
- Handling Corruption in Files
Designing a Test Plan
- Understanding Data-Driven Versus Logic-Driven Testing
- Defining Testing Techniques
- Determining When Testing Is Complete
- Creating a Test Environment
- Defining Test Cases That Exercise All Program Paths
- Defining Test Cases Using Copies of Real Data
- Documenting Test Cases
- Using Additional Testing Guidelines
- Asking Questions During Testing
- Understanding Methods for Tracking Down Errors
- Using Error Handlers
- Using Error Events in Objects
- Other Resources
A program with errors in it cannot solve problems, save someone
time, or make an organization more profitable. The bad news is
that you will write few, if any, programs totally error-free right
from the start. The good news is that you can learn how to detect
and remove errors from code using a process called debugging.
The first case of debugging allegedly occurred many years ago when an insect caused some tubes to fail in one of the first computers. Thus, the term debugging came into use for getting the "bugs" out of a system. Whatever the origin, the purpose of debugging in the software development process is to locate problems that cause programs to fail or produce incorrect results. Often, this process resembles a detective's investigation. You run the program and collect clues such as the following:
- Which procedures execute and in what order?
- What tables does the program open?
- What was the last record processed? Were any skipped?
- Which index controls the table order?
- Why do certain variables contain the values they do?
In fact, the more clues you collect, the more likely you will solve "The Case of the Program-Killing Error."
There are hundreds of possible errors you can make when writing Visual FoxPro applications, at least if the number of error messages is any indication. (Visual FoxPro has more than 700 defined error messages.) Some errors are more common than others, and some are pretty esoteric. Many are syntax-related and often result from simple typing mistakes. Others result from under-developed programming practices such as failure to check for EOF() before reading or writing a record. With so many possible errors, your common errors might not be the same as mine or your fellow programmers. In fact, every developer has a slightly different set of common errors that they seem to be constantly correcting. You might find yourself encountering your own subset repeatedly, based on the types of errors you tend to make. You might also have to do a little digging into the reasons for a particular error. What can start out as syntax problem or a data type that is not what you intend might not be invalid as far as VFP is concerned. It will attempt to run with whatever you supply, and the condition that develops might result in an error that appears to make little sense when presented to you.
There are various sources within the language and its help system for gathering information about errors. The Reference section under Visual FoxPro Documentation contains an entire section on the 700 or so possible error messages: an alphabetical listing of messages, a list of those same messages in numerical order by error code, and a list of messages that supply additional information (known as error message parameters) via the SYS(2108) function.
Unfortunately, in most cases, there is really nothing your program can do after the error has occurred. Most error conditions cannot be fixed in a live application. The best that you can hope for is to document what happened and then roll back transactions, close tables, and exit the program. The real challenge of error handling is to prevent the errors from occurring in the first place by writing proactive code that anticipates possible errors. You can also become more aware of common errors and thereby avoid them.
This section looks at a fairly representative set of common errors that I have seen while working with other developers. Maybe you will recognize a few of them in your own coding; don't worry if you do. Recognition is the first step in developing better habits that will eliminate common errors from your coding habits.
There are three major classes of errors in programming: logic, syntax, and exceptions. Of these, syntax errors are the easiest to find and correct. In fact, compiling your code is the fastest way to locate most of them, and the latest versions of Visual FoxPro have become even more stringent in their evaluation of syntax errors than earlier versions. Some errors that are syntax-related do not show up during compilation, however. Common syntax errors include
- Forgetting an equal sign or other operator in an expression:
* Missing = sign after gnTotalDue gnTotalDue pnNetDue * (1 + lnTaxRate)
- Spelling a command or function name incorrectly:
* Missing second 'A' in DATABASE OPEN DATBASE ptofsale
The VFP color-coded editor is quick to pick up spelling errors. As you type in the Command window or in your program, VFP shows reserved keywords in color. If you misspell a command, argument, or clause, it changes to normal black text. Make sure that you configure Tools, Options, Syntax Coloring to show Keywords in a color you will notice, and that you check Syntax Coloring in the Edit, Properties, Edit Properties dialog box. For more information on these settings, see Chapter 1 "Quick Review of the Visual FoxPro Interface."
- Mispairing quotes around strings. This includes the common error of including the same type of quote inside a string as is used to delimit the string:
'This is Bill's statement' && Fails "This is Bill's statement" && Succeeds
- A variation on this error puts a single quote at one end of the string and a double quote on the other end.
Once again, the VFP color-coded editor can help you out. Setting up strings to appear in some jazzy way can help keep you from getting caught by mismatched quotes. I like to show strings highlighted by a background color, such as yellow or light green. This lets me know immediately if I fail to terminate a string properly.
Visual FoxPro recognizes 'single quotes,' "double quotes," and [brackets] as string delimiters. If you need to use any of these characters inside the string, pick a different delimiter.
- Mispairing parentheses in complex expressions:
REPLACE pnboxno WITH PADL(VAL(pnboxno+1,'0',5)
You can pick up some of these errors by clicking Verify in the Expression Builder dialog box, which is available through the design surfaces. The Expression Builder won't be able to evaluate UDFs or references to fields in unopened tables, however.
- Using a reserved word for a memory variable or field name. Words Visual FoxPro uses for commands, functions, and keywords should not be used as variable names. This includes the use of four-character variable names that match the first four characters of a reserved word. When this happens, Visual FoxPro might try to interpret the variable or field name as the equivalent command. Usually, the command is out of context, resulting in an error. For example, the following statement generates the error Invalid use of a Visual FoxPro function as an array:
- Not matching CASE ENDCASE, DO ENDDO, FOR ENDFOR, IF ENDIF, or SCAN ENDSCAN commands, or using the wrong terminator (ending a DO structure with ENDIF). Visual FoxPro calls this problem a nesting error:
IF gnTotalDue > 100 = MESSAGEBOX("Get a supervisor's signature") ENDDO && ENDDO should be ENDIF
A common syntax error not found during compilation is the passing of variables of the wrong type to a procedure. Because Visual FoxPro is not a strong typed language, memory variables can assume any variable type during execution. Therefore, VFP has no way to determine during compilation that the calling program will pass a numeric value when the procedure is expecting a character string. In any case, the program fails with a syntax error when this procedure call is run.
Although most of the chapter refers to compilation occurring as a separate step, you can have VFP compile your program code (.PRG) every time you save it. This option can be set in the Edit Properties dialog box discussed in Chapter 1 Code stored as methods in forms, classes, and DBCs is compiled when these windows are closed or saved, regardless of any preference settings.
When you run a program, Visual FoxPro converts the source code to object code, if not previously compiled, and in the process detects references to other programs, procedures, and functions. It attempts to resolve these references within the current file or in other files included in the project. Suppose you attempt to call procedure SOMETHIN. If Visual FoxPro cannot resolve it, it interrupts the compilation to display the following message:
Unable to find Proc./Function SOMETHIN
VFP also displays four buttons labeled Locate, Ignore, Ignore All, and Cancel.
If you choose Locate, VFP displays an Open dialog box that enables you to find the missing file. Note, however, that this solves the problem only for the current compile. (Well, maybe. Suppose that you have a file with the same name in more than one directory or on more than one server drive on a network system. Can you be sure that you or your users will select the correct file? I think not!) If you recompile the program later without correcting this reference, either within the code or by adding the file to the Project Manager, you get another error.
Sometimes, you can ignore one or more errors during compilation if you know that the referenced procedure or function exists as a separate file or if it is an external reference. This often occurs when the compiler confuses an array reference with a function call. This problem can often be fixed by adding an EXTERNAL ARRAY arrayname in the program.
|Resolving External References|
EXTERNAL can be used to resolve references to classes, forms, labels, libraries, menus, procedures, queries, reports, screens, and tables. The most common reason for requiring EXTERNAL to resolve the reference is because the program code uses macro expansion to define the object. The following code shows an example of memory variables required to determine which of three queries and reports to open. The EXTERNAL command is used to resolve these references to the compiler.
You can also choose to note, but ignore, unresolved references to permit VFP to complete the compile and log syntax errors into an error file. To log errors, choose Tools, Options, and the General tab. Select the option Log Compilation Errors. (You can also use the command SET LOGERRORS ON.) Visual FoxPro then writes errors to a separate text file with the root name of the compiled file and an extension of .ERR. After compiling, you can open Project, Errors to see this error file (if you are compiling from the project). If you compile individual programs, you can view the error log by typing the following:
MODIFY FILE filename.ERR
If you do not log the errors, VFP merely shows the number of errors in the status bar at the end of the compile. This is not a very informative solution, especially because this message disappears after a few seconds. Information reported with each syntax error in the log includes the following:
- The program line that caused the error
- A line number where FoxPro determines that the error exists
- An error message
When you recompile the corrected program, Visual FoxPro automatically removes this file as the program compiles without error.
An important point about the line number is that it does not always point to the line containing the error. For missing parentheses or operators, the line number points to the exact line of the error. For a missing or mismatched loop terminator (for example, ENDDO, ENDIF, ENDSCAN, or ENDFOR), the line number will probably be the end of the procedure. Visual FoxPro might not realize that an error exists until the procedure or program ends and the loop has not terminated properly.
|Always SET LOGERRORS ON|
Closing a method-editing window in a form object also performs a syntax check. For syntax errors discovered during a compile, the alert box displays three options. You can Cancel the compile and return to the code to correct the problem. You can Ignore the error and check the balance of the code-until it finds another error or reaches the end of the code. Finally, you can Ignore All, which saves the code with all its errors. If you log errors, however, you can check the error log file to list the errors and resolve them one at a time.
If Visual FoxPro finds an error during execution that it does not find during compilation, it displays four options: Cancel, Suspend, Ignore, and Help. Cancel, of course, cancels the program. Suspend stops execution of the program and lets you enter the debugger to get more information about the error. You can also use the Command window to print values of memory variables or table fields to determine the problem. The Ignore button lets you ignore the problem; however, this is seldom a good choice. It might get you past this particular error, but chances are good that the application's memory has been compromised. Finally, the Help button opens VFP Help to give you more information about the error message.
There are some syntax errors that Visual FoxPro cannot detect until the program executes. These errors can result from mixing the order of parameters, passing the wrong number of parameters, or other similar problems. They are usually fairly easy to detect and fix. The following code lines show some runtime syntax errors:
- Mixing the order of parameters passed to a function. Visual FoxPro recognizes this error only if the switched parameters have a different type. It then responds with the Invalid function argument value, type, or count error. Mixing parameters of the same type generates logic errors at runtime.
? TRANSFORM('$$$,$$$.99', gnTotalDue) && Parameters are switched
- Entering an incorrect parameter to a command that has a limited number of possible values. For example, the ON KEY LABEL command expects specific key names. Anything else generates an error, but only at compile time, as would be the case with the following statement:
ON KEY LABEL FJ9 ZOOM WINDOW PROPERTIES NORM FROM 0,0 ; TO 10,10
- Using the wrong number of parameters or no parameters in FoxPro functions. Passing too few parameters to a UDF is not automatically flagged as an error, but the procedure might fail if it attempts to use the missing parameters because VFP initializes them to .F.. This could result in a type error in subsequent statements. On the other hand, if you pass too many arguments to a procedure, you get the following error:
Must specify additional parameters
- This might sound confusing, but look at it from the point of view of the procedure, which needs additional parameters to complete the request. The following code line attempts to use the LEFT() function to display a portion of a character string, but it does not pass the number of desired characters. This statement must fail because VFP will not know how long a string to return:
?LEFT(lcLastName) && Number of characters to return is required
- Entering a comma instead of a period, or vice versa. The difference between these two characters is hard to see on many monitors. For example, the following statement fails because a period rather than a comma separates two parameters:
LPARAMETERS lnErrNum. lcErrMessage
- This problem is made worse by the fact that these two characters occur next to each other on the keyboard. But, they are not the only character pairs likely to cause problems. Another commonly confused pair includes the zero and uppercase O. Again, both characters occur close to each other on the keyboard and look almost identical on the screen. Another frequently confused pair is the number one and lowercase l.
This list represents some common syntax errors, but not all of them. As you develop programs, you might recognize other common syntax errors of your own.
Logic errors are the second major category of errors. These errors are more difficult to detect and resolve. Compiling the program does not locate them. Sometimes, a serious logic error stops a program from executing, but not always; for example, referencing an array, or one of its elements, that does not exist results in a logic error. Perhaps the program defined the array in another procedure, but gave it a private scope rather than global. When VFP encounters any variable that does not exist, it stops the program's execution.
A similar type of logic error occurs when you overwrite existing variables when restoring variables from a memory variable file or a table. The RESTORE FROM command enables a program to use variables previously stored to a .MEM file. The following code line not only restores the .MEM file variables, it also erases all current memory variables. SCATTER MEMVAR performs a similar function when retrieving fields from a table and saving their values into memory variables. If a prior memory variable had the same name as a field in the table, its value will be replaced by a corresponding field in the current record.
RESTORE FROM Savevars.mem
USE CUSTOMER SCATTER MEMVAR
If your program uses similar code and attempts to reference a previously defined variable, it could fail or at least generate incorrect results. The RESTORE FROM command actually causes more of a problem because it wipes out all prior memory variables before loading the ones from file. Fortunately, RESTORE FROM supports a clause to restore memory variable values without losing those already in memory. Simply include the ADDITIVE clause, as shown in the following snippet:
RESTORE FROM Savevars.mem ADDITIVE
You can still overwrite an existing variable that also exists in the .MEM file, but you won't lose the uniquely named ones.
Some logic errors create obviously wrong results. For example, you might see a string of asterisks in a field (****), which indicates a field overflow. In this case, either the field is too small or the value calculated is too large. Of course, errors do not have to be this dramatic. If a sales report calculates the total tax due as greater than the net cost of the items purchased, there is a problem. This error should be obvious to anyone who simply compares the sales totals before and after tax.
On the other hand, some errors are not obvious at all without an independent check of the calculations. Suppose that you use the following equation to calculate the number of acres in a square plot of land:
gnTotalAcres = (pnFront * pnSide) / 9 / 4480
This equation is valid and does not generate an error when executed. It multiplies the length of the front in feet by the length of the side in feet to get the area in square feet. It then divides by 9 feet per square yard. It divides the number of square yards by 4,480 rather than 4,840, however. Merely looking at the result might not reveal the fact that two digits were transposed. This type of error can go unnoticed for a long time.
Another difficult logic error to find occurs when using REPLACE. Suppose you try to change a field in a different table while the record pointer in the current table points to EOF. This error is subtle, as shown in the following code:
SELECT CUST IF !SEEK(m.pcCustId) REPLACE ORDER.cCustId WITH 'NONE' ENDIF
You might be surprised to find that this code fails. When FoxPro searches for the customer ID in table CUST and does not find it, it leaves the record pointer at the end of CUST. Even though the REPLACE statement clearly uses an alias to replace cCustId in table ORDER, the default scope of NEXT 1 actually refers to the current work area, which is CUST, not ORDER. Because the record pointer in the current work area points to EOF, REPLACE fails. Obviously, the solution here is to precede REPLACE with a SELECT Order statement.
This "feature" has haunted FoxPro developers since its earliest versions. Visual FoxPro now fixes the condition with a new IN clause to the REPLACE command, which scopes the command to that other work area. You must specifically name the other work area, not just use it as an alias. Change the above code to the following.REPLACE Order.cCustID WITH 'NONE' IN Order
Because VFP doesn't require that you qualify a field name with its alias, it's sometimes easy to forget that you are working with table fields instead of memory variables. You assign values to memory variables with an equal sign, but you put data into fields with REPLACE. Using names from the previous example, the following line will not fail, but it also won't give you the results you are looking for. VFP simply assumes you want to store the value into a variable:
cCustID = 'NONE'
Exception errors, the third type of coding errors, occur due to circumstances outside of the program's direct control. For example, a program may fail because it cannot find a file it needs. Perhaps the file has been deleted or moved. Of course, if someone or some process deleted the file, there is no way for the program to continue. Even if someone has merely moved the file, you might not want to make the user responsible for finding it.
Said another way, you might not want users roaming around the file server looking for a "likely" table. First, they do not know where to look; second, they might open the wrong file, causing even more problems. Yet an expert user such as yourself might be very qualified and able to perform this search. Even experts, however, make mistakes. In any case, your program should use the FILE() function to determine whether a file exists before attempting to use it. Then, based on the user level (assigned elsewhere in your application), the code in Listing 23.1 shows one way to determine how to continue when the file does not exist. You might consider always calling a shutdown procedure when a file is not found by a program as protection against the ever-present possibility of accessing the wrong file. Alternatively, you might find some other suitable way of returning the user to the application without accessing the code that's looking for the missing file.
Listing 23.1 23CODE01.PRG-A Basic Way of Dealing with a File-Not-Found Exception
IF FILE('\VFP6BOOK\DATA\MYFILE.DBF) * File found, open it USE \VFP6BOOK\DATA\MYFILE.DBF ELSE * File not found. WAIT WINDOW 'File: \VFP6BOOK\DATA\MYFILE.DBF NOT Found!' + ; CHR(13) + 'Press any key to continue' * Can this user search for it? IF gnUserLevel > 1 lcNewFile = GETFILE('DBF', 'Find MYFILE.DBF', 'SELECT') IF !EMPTY(lcNewFile) USE (lcNewFile) ELSE DO SHUTDOWN && Or run another part of the application ENDIF ELSE DO SHUTDOWN && Or run another part of the application ENDIF ENDIF
The previous code can very easily be converted to a generalized form and made into a function that you can call from any program or procedure. Listing 23.2 shows one possible implementation.
Listing 23.2 23CODE02.PRG-A Generalized Function for Dealing with a File Not-Found Exception
IF FILE('\VFP6BOOK\DATA\MYFILE.DBF) * File found, open it USE \VFP6BOOK\DATA\MYFILE.DBF ELSE * File not found. lcGetFile = FINDFILE('MYFILE.DBF') ENDIF *** REST OF PROGRAM CONTINUES FUNCTION FINDFILE LPARAMETER lcFileNam * Tell user what is happening. WAIT WINDOW 'File: &lcFileNam. NOT Found!' + ; CHR(13) + CHR(10) + 'Press any key to continue' * Can this user search for it? IF gnUserLevel > 1 lcNewFile = GETFILE('DBF', 'Find '+lcFileNam, 'SELECT') IF !EMPTY(lcNewFile) USE (lcNewFile) ELSE DO SHUTDOWN && Or continue in another part of the application ENDIF ELSE DO SHUTDOWN && Or continue in another part of the application ENDIF RETURN lcNewFile
Another type of exception occurs when an index does not exist. If the table is part of a database, the program could retrieve the definitions for that table's indexes from the .DBC file. It could then attempt to create the missing index. The potential problem here is that indexing requires an exclusive lock on the file. If the program cannot obtain that lock, the program should shut down gracefully.
Not all exception errors are as easy to deal with. In some cases, the best alternative might be to use an ON ERROR routine to document the system at the time of the error, roll back pending transactions, close all tables, and cancel the program.
ON ERROR executes a single command when Visual FoxPro encounters any error condition. That command typically uses DO to execute an error-handling procedure. ON ERROR still provides a global mechanism for handling errors in Visual FoxPro.
The rest of this chapter shows additional methods of avoiding errors when possible, and how to track down errors that inevitably sneak in anyway.
The number of errors in any program or procedure tends to increase as it grows in size. The reason is that it becomes increasingly difficult to remember all the details in the code. The demands on software grow daily as well. Very quickly, every programmer realizes that writing large single programs with hundreds or thousands of code lines causes more problems than several smaller programs that call one another. Obviously, the more errors, the greater the amount of time spent finding and removing them. On the other hand, dividing code into smaller functional units called procedures or functions reduces overall errors. It soon becomes obvious that each procedure or function should handle no more than one task. This process is known as code modularization.
Visual FoxPro facilitates code modularization. Any application can be thought of as a series of tasks. Tasks consist of data-entry screens, reports, or procedures that manipulate data. Menu choices provide access to each task. Each task often has two or more subtasks.
How do you go about breaking a task down into smaller units or subtasks? Think of a customer form as a single task. Within it, individual data fields represent subtasks. Individual pieces of information on that form might hold the customer's name, address, or telephone number. Visual FoxPro represents subtasks as control objects. Looking deeper into a control, you find individual methods that respond to various events. Object methods are used to accomplish the following:
- Set default values
- Determine a user's rights to a field
- Create pick lists
- Validate field entries
- Save or retrieve data
- Respond to mouse clicks or moves
- Display related forms
- Send messages to other objects instructing them to alter their own characteristics, or run some method they are responsible for
See the chapters in Part IV, "Object-Oriented Programming," for more information on interobject communication.
You have already seen how to generate forms, where each form represents a block of code functionally independent of the rest of the application. Within a form, methods further modularize the code needed to handle individual object events. You can create and store the code needed for a method in a variety of ways.
First, when you open a method in Visual FoxPro's form design mode, you can enter code directly. Although this approach works, it has some drawbacks. Mainly, it limits the use of that code to one method in one form. In order to use a similar routine in another form, you might resort to cut-and-paste. You could easily end up with the complicated task of maintaining many slightly different versions of the same routine in a variety of places. You could instead store the code in a separate file. The event method then simply references it with a user-defined function (UDF) call. That's the easy part. The hard part is deciding where to store this separate code. Again, several possibilities exist.
In FoxPro 2.x, form generation produced real code that you could view and use. In VFP, the process is quite different, and there is no actual code. You can pretend that you are generating code as you did in FoxPro 2.x by directing the Class Browser to produce a listing for you. However, a VFP form is simply an .SCX file in either design mode or run mode. This difference, plus application of object-oriented principles, affects how you share coded routines among your application's objects.
In FoxPro 2.x it was a commonly accepted practice to use a separate .PRG with the same name as the form to store the needed procedures and functions used by that form. This .PRG was also responsible for calling the form.
You could run this .PRG directly, or have the application's main menu call this form-launching program. Although not the recommended object-oriented approach, this methodology can be adapted to use with Visual FoxPro, which would then have no trouble finding the functions stored in the main program that calls the form.
This technique worked well in FoxPro 2.x because it enabled quicker editing of the code and did not require the form designer just to change the code for a method. However, it is not in the OOP spirit to code this way. It does not enable sharing of code between objects with similar methods in different forms. This is a big negative when you begin to develop class libraries. The UDF call will be inherited when you subclass from the base object, but the source UDF might not be available.
Another technique stores all procedure code in a single, separate file. This file establishes a library of procedures and functions. Then, any screen with a similar method, even a subclassed screen, can call a common function. This technique makes changing the code easier. If you need a new property or method, you add it in one place. Place the reference to the code in the appropriate method of the base class and place the code in the procedure library. If more than one form uses the same method code, it still appears in one place, not everywhere the object appears.
A variation of this traditional concept is to turn a procedure library into a class, where each custom method is a commonly used routine. Simply drop this library object onto your application objects to make those methods available. There can be performance tradeoffs in referencing class methods versus functions in procedure files, so this technique might be better used to group more specialized routines related to a common activity.
With VFP objects, you can build a subclass of the original class and include common method code in its definition. By encapsulating the method's code in the definition of the class itself, you don't have to worry about separate code libraries. By sharing the object through a class library, you still code it only once. This approach works best when many forms use the same object with the same methods. An example might be VCR buttons used to control movement through a table's records.
Before you store objects as custom classes in a class library, make sure to test them and all their associated code thoroughly. You can create a simple form and place the control on the form to test it. After you are satisfied with it, select the object and choose File, Save As Class. The resulting Save Class dialog box enables you to assign a class name and library file and save just the selected controls without the form. Debugging and modifying code after storing it in a class library requires more steps.
The point to this discussion is that object methods force you to divide code into individual tasks that are more easily comprehended and maintained. The theory is that if individual tasks work properly, the sum of the tasks works properly.
Of course, you can achieve many of these same benefits in standard code by using procedures and functions that accomplish a single task. If a procedure performs more than one task, it can probably be split into two or more procedures.
Code modularization at the task level leads to another benefit: reusability. This has been mentioned before in terms of objects and object libraries, but it applies just as strongly to other common code tasks. If you write a procedure to handle movement through a table's records using a set of VCR buttons, you can use that same procedure with every table. The code remains unchanged. Developing this concept further, consider developing a library of common procedures to use in any application. You benefit from time saved by not having to write and test the same code repeatedly. Your applications also exhibit a consistent look and feel.
To learn more about using an object-oriented approach in your applications, see Part IV, "Object-Oriented Programming."
Another common error occurs during planned and unplanned parameter passing. First you might wonder what unplanned parameter passing means. Remember that Visual FoxPro makes any variable defined as a public or private variable automatically available to routines it calls. This can unintentionally redefine a variable when calling a lower-level routine that uses a variable with the same name. Visual FoxPro will not flag this as an error. After all, it assumes that you intended to do it.
As a general rule, never use the same variables in programs that call one another or in different procedures in the same program. If you must use a variable in a called procedure, pass the parameter by value and assign it a local name in the called procedure. Accidentally redefining variables in lower routines results in errors that are extremely difficult to find.
If you are not sure whether a variable has been used in a higher procedure, define the scope of the variable as LOCAL or PRIVATE in the called procedure.
Another consideration is whether you pass parameters to procedures by value or by reference. When you pass a parameter by reference, the procedure uses the actual original variable. If the procedure changes the parameter value, the original variable's value changes also (see Listing 23.3).
Listing 23.3 23CODE03.PRG-An Example of Passing Parameters by Reference
* Default scope for these variables is PRIVATE a = 5 b = 6 c = 7 DO NewVal WITH a ? 'a = ' + STR(a,1) && After the subroutine, a contains 1 ? 'b = ' + STR(b,1) && b has been protected, still contains 6 ? 'c = ' + STR(c,1) && c has also been changed, value is now 8 PROCEDURE NewVal PARAMETER b b = 1 c = 8 RETURN
On the other hand, if you pass the parameter by value, the procedure creates a new variable to store the parameter value. It does not pass changes back to the original. The equivalent code is shown in Listing 23.4.
Listing 23.4 23CODE04.PRG-An Example of Passing Parameters by Value
a = 5 DO NewVal WITH (a) ? 'a = ' + STR(a,1) && Original value of 5 has been preserved PROCEDURE NewVal PARAMETER b b = 1 RETURN
When calling a procedure, Visual FoxPro's default method passes parameters by reference, except for any that you enclose in parentheses. However, when calling a function, the default method passes parameters by value (observe, functions enclose parameters in parentheses). To pass a parameter to a function by reference, you can do either of the following:
- Type SET UDFPARMS TO REFERENCE.
- Precede the parameter name with the @ character.
If you need to pass an array to a procedure or function, pass it by reference. If you attempt to pass it by value, it passes only the first element.
There is a limit of 27 passed parameters to procedures and functions.
Not too many years ago, the "new" programming paradigm promoted the elimination of all GOTO statements. FoxPro developers have never had a problem with GOTOs because the language does not support a GOTO branch statement; the VFP GOTO command only navigates data files. Instead you use IF, CASE, and DO structures for conditional code processing and loops. As a result, few FoxPro developers today miss GOTO statements and the resulting tangled code they created.
Unfortunately, many FoxPro developers still use multiple exits from structured loops and multiple returns from procedures and functions. In almost all cases, there is no need for this practice. All that is usually required is a minor revision to the loop's logic. Examine the following code example:
PROCEDURE GetProdID IF EMPTY(m.lcProdId) RETURN ELSE < Code to test if lcProdId exists in PRODUCT.DBF > RETURN ENDIF USE RETURN
This procedure has three exit points where only one is required. A restructured version of the same code follows:
PROCEDURE GetProdID IF !EMPTY(m.lcProdId) < Code to test if lcProdId exists in PRODUCT.DBF > ENDIF USE RETURN
Why be concerned about multiple exit and return points? First, it adds a level of unnecessary complexity to the code that makes tracing its path more difficult. But more important, it sometimes causes the program to skip critical code segments that it should execute. For example, in the earlier code, should the procedure close the current table (the USE command)? The first code example never closes the current file and the second one always does.
This illustrates another danger with multiple EXIT or RETURN commands. It is easy to orphan code, isolating it so that it never executes. The first example ends each branch of the IF statement with a RETURN. As a result, the USE statement never executes.
The EXIT command exits a DO ENDDO, FOR ENDFOR, or SCAN ENDSCAN loop prior to completing the loop based on the loop condition. For example, the following program segment loops through rental product records to find the serial number of a product still in stock. A simple SEEK finds a record that matches the product ID. Then, it must loop through all the records for that product until it finds the first one in stock.
USE RentProd ORDER ProdID pcSerial = SPACE(4) SEEK m.pcFindProdId SCAN WHILE m.pcFindProdId = cProdId IF lInStock pcSerial = cSerial EXIT ENDIF ENDSCAN
Observe that this code segment has two possible exits from the SCAN loop. If it finds at least one product in stock with a specific ID, it stores its serial number in variable pcSerial and exits. Otherwise, it continues to loop until the product ID changes.
A better way to write this code eliminates the extra exit. First, you need to recognize that you cannot simply eliminate the EXIT command. This would cause the loop to read through all records of the same product ID. The net effect would be to return the last available serial number rather than the first. The following code solves this problem by adding an extra conditional test to the SCAN:
USE RentProd ORDER ProdID pcSerial = SPACE(4) SEEK m.pcFindProdId SCAN WHILE EMPTY(pcserial) AND m.pcFindProdId = cProdId IF lInStock pcSerial = cSerial ENDIF ENDSCAN
Observe, in this case, that SCAN tests for an empty serial number memory variable first. If this field changes before the product ID does, an in-stock item has been found. This new condition also guarantees that the loop returns the first available serial number or a blank if there is no in-stock product.
A case might be made that adding the extra condition test slows the loop and thus the extra EXIT actually improves the code performance. The programming world is loaded with tradeoffs.
A similar case can be made for eliminating the use of EXIT in other loop structures. Thus, by careful use of IF blocks and additional conditional statements, you can eliminate most, if not all, multiple exits from programs. I hope that this will also eliminate another potential source of errors in your code.
Using libraries, which are program files containing common procedures and functions, is one of the most effective methods of reducing the number of errors in code. Of course, you must first thoroughly test routines before adding them to a library. Once tested, however, new applications can use them with virtual assurance that they do not contain errors.
This concept of building common libraries of functions has grown up in the object world in the form of class libraries.
In fact, Visual FoxPro's builders and wizards provide the equivalent of a standard library of creation routines. They automate the building of forms, form controls, menus, queries, reports, data tables, and the like. You do not have to write the basic code for these objects. Visual FoxPro provides error-free objects for you; all you have to do is tweak them to fit your particular application's needs.
VFP 6 comes with a greater selection than ever of new and enhanced wizards, and a complete set of source code to re-create them. See Chapter 27, "The Visual FoxPro Wizards," for more information about them. Completely new additions to this version are the Foundation Classes and the Component Gallery, which provide an expanded selection of pre-built tools you can use directly in your applications. You can count on these offerings being enhanced in future versions of VFP. For more information on these new features, see Chapter 18, "The Visual FoxPro Foundation Classes" and Chapter 19, "The Visual FoxPro Component Gallery."
You can also build your own libraries of functions and procedures to call from any program or object method. To create a library, store the functions and procedures in a single file and save it. Then, in programs that use them, simply add a SET PROCEDURE statement before referencing them. The syntax for SET PROCEDURE is
SET PROCEDURE TO [FileName1 [,FileName2,...]] [ADDITIVE]
Visual FoxPro enables programs to reference more than one library. You can even reference additional libraries later in a program by including the ADDITIVE clause. Forgetting this clause closes any prior procedure libraries when opening the new one.
Procedure libraries provide an excellent way to store and use common routines. Create common routines to open files, create sorts, display messages, and perform other common functions.
In addition to procedure files, Visual FoxPro also lets you create object libraries of your own classes. To begin, subclass one of Visual FoxPro's base classes or an existing subclass in a .VCX file. Then add additional custom properties or methods to it. For example, you could create a class consisting of a group of buttons to navigate through a file. You can then use this custom class on any form that needs file navigation buttons.
It is important to distinguish between a custom class and a customized class. VFP provides a base class of type custom that comes with its own native properties, events, and methods. Any subclass, however, can be customized with the addition of user-defined properties and methods.
To open and use a class library, use SET CLASSLIB. Its syntax is
SET CLASSLIB TO ClassLibraryName [ADDITIVE] [ALIAS AliasName]
As with SET PROCEDURE, you can have multiple class libraries open at the same time if you open each one with the ADDITIVE clause. Omitting this clause closes any previously opened class libraries.
Data files can easily become corrupted. Because data files often are larger than the amount of RAM memory in a machine, Visual FoxPro constantly moves some of the files between memory and disk. Normally, everything works fine. If, however, the user turns off the computer without properly exiting the program, the file might be incompletely written. (This could be his frustrated reaction when your program fails without a proper error handling routine, and leaves tables open.) Unfortunately, the best protection against this sort of action and its resultant data corruption is a hardware UPS (uninterruptible power supply). You can take some precautions in your applications, though, primarily by tracking errors when they do occur.
In Chapter 1 "Quick Review of the Visual FoxPro Interface," an example code called a procedure, REALQUIT, to prevent the user from accidentally exiting FoxPro by clicking the wrong Close box. It proposed running a program called RealQuit when Visual FoxPro enters its ON SHUTDOWN event. Alternatively, you can add the ON SHUTDOWN command to any program to prevent the user from exiting Visual FoxPro or Windows prematurely. The procedure in Listing 23.5 should be called by ON SHUTDOWN. It includes some additional commands to properly close down Visual FoxPro. If called from an error handler, you could even build a procedure consisting of the core code inside the IF ENDIF block.
Listing 23.5 23CODE05.PRG-A Procedure for Properly Closing Down VFP
PROCEDURE SHUTDOWN * Include any commands in this routine needed to return the system * to a default environment. Applications running interactively * require more attention than compiled standalone applications. * Standalone applications might merely need to close down all * files safely. IF MESSAGEBOX('Do you really want to exit Visual FoxPro?', 292) = 6 * Reset common ON KEY definitions that the application may be using. ON KEY LABEL BACKSPACE ON KEY LABEL ENTER ON KEY LABEL SPACEBAR ON KEY LABEL ESCAPE * Turn printer and file redirection off SET ALTERNATE OFF SET ALTERNATE TO SET PRINT OFF SET CONSOLE ON * Close transaction - TRAP ERRORS IF TXNLEVEL() > 0 ROLLBACK END TRANSACTION CLOSE ALL * Release all variables (and objects) RELEASE ALL * Deactivate windows DEACTIVATE WINDOWS ALL DEACTIVATE WINDOW DEBUG DEACTIVATE WINDOW TRACE ACTIVATE WINDOW COMMAND * Deactivate any application menus SET SYSMENU TO DEFAULT * Clear macros RESTORE MACROS ELSE * Let VFP terminate * QUIT ENDIF RETURN
Of course, the easiest way to fix a corrupted file is to restore a backup copy from tape or disk. If you perform daily backups, the most you lose is one day's worth of data. Even this minimal loss can be unacceptable, however. Whereas tools like dSalvage from Comtech and FoxFix from Hallogram (www.hallogram.com) worked well repairing individual tables from previous versions of FoxPro, the considerably more complex database structures in VFP require suitably sophisticated products to maintain their integrity. The best of these management and repair tools is Stonefield Database Toolkit from Stonefield Systems Group, Inc. (www.stonefield.com). Another excellent choice for database design and maintenance is xCase for Fox from RESolution Ltd. (available at www.f1tech.com). Because of the interrelated nature of database components, you need to supplement your data backup and restore procedures with some higher-level functionality that is provided by these database tools. The sorts of things you need to be able to do are as follows:
- Fix many errors in the file header.
- Recover lost memo field data.
- Reindex tables in the DBC without losing persistent relationships or referential integrity code.
- Manage the database container so that table header changes, such as table names, field names, and field properties, remain synchronized with the DBC.
- Rebuild entire databases or update individual structures at your own or client sites.
A structural .CDX file is the easiest, safest-and therefore, most common-type of index to use and maintain. You should reserve the use of individual .IDX files for infrequently run, ad-hoc operations.
A corrupted index can occur when someone copies a backup .DBF to the current directory without copying its indexes. In this case, changes made to the original .DBF exist in the index, but not in the copied .DBF. Thus, the index might point to the wrong record, no record, or beyond the end of the table. This is further complicated by the fact that the database container itself (the DBC) tracks information about both the table and its index file.
You can fix simple index file problems by reindexing the table; however, even REINDEX doesn't help if the index file header is corrupted. Before Visual FoxPro, when life was simpler, you could save index definitions in a separate table, a database, a data dictionary, or simply on a piece of paper. You could then delete the corrupted index files, open the .DBF, and re-create them. This simplistic approach, however, creates new headaches in a VFP database, because you can lose other information about your files, such as persistent relationships with other files and referential integrity procedures.
The DBC, although containing a record to reference each tag name in the structural compound index of the table, does not store the index expression, nor does it store information about any other index (standalone or nonstructural). This means that copying backup tables into the current directory is a good way to get them out of synch with the DBC. You can find out more about this condition by issuing the VALIDATE DATABASE command in the Command window.
If your index file contains a primary key reference, it is not easy to simply delete an index file and re-create it. The problem is that the database container stores a reference to the primary key in two places, a tag record and in the properties of the table record itself. Removing the tag record from the DBC is easy. Removing the primary key reference from the table properties is not. See additional information about working with the database container in Chapter 4 "Advanced Database Management Concepts."
Corrupted form, label, and report files present an additional
challenge. Visual FoxPro stores information for these objects
in both a .DBF-type file and a memo file. Therefore,
they are susceptible to the same types of damage and you can use
the same tools and methods to fix them. Table 23.1 shows the corresponding
file extensions for Visual FoxPro's key components.
It might be possible to edit and remove some data corruption with programs. The safest solution, however, is to keep backup copies of all program and data files on cartridge, tape, or some other removable medium that can be stored offsite.
Testing your application should be considered a separate and very high priority planned task in its development. If you use a project planner such as Microsoft Project, identify testing as a separate task. There are many ways to design a test plan. Some developers test only after they completely write an application. The interactive nature of Visual FoxPro, however, makes concurrent testing during development easier and more productive. The problem is that it is harder for management to track the time spent on concurrent testing. This section looks at various testing techniques and analyzes their pros and cons.
Testing an application consists of two elements: validity and coverage. Validity testing checks to see whether the application generates the expected results for a specific set of inputs. Coverage testing checks to see whether all code statements have been executed by the tests. Any code not executed could harbor a hidden bug. I will talk more about coverage testing later. First, let's examine validity testing.
There are two basic approaches to validity testing. The first approach is data-driven. It does not assume a prior knowledge of the way the program works, rather, it focuses on selecting a variety of test data sets based on a random sampling of real-world or fabricated data. It then runs the program with the data to see whether it generates the expected results.
The second approach is logic-driven and requires extensive knowledge of the program's coding. It attempts to test every path the program can execute. It also tests how the program handles data limits by using data that pushes and exceeds known physical limitations.
Each method has advantages and disadvantages. The advantages to a data-driven approach include the fact that it does not consciously or subconsciously make assumptions about the program. Often a person "assumes" that a program never behaves a certain way and therefore fails to test it completely. Frequently, the parts of the program assumed to be correct are the very ones that fail. The primary disadvantage to a data-driven approach is that there is no guarantee that the test data sets cover all program paths and loops.
A logic-driven approach overcomes the weakness of data-driven testing. When properly designed, it tests every line of code in the entire system. The obvious disadvantage is that for a major application, fully testing every line of code requires multiple data tests. It takes time to develop the necessary data sets to ensure testing of each line of code. Further, logic-driven testing using an interactive interface can be hugely time-consuming.
There are almost as many techniques for testing and debugging as there are programmers. Most approaches involve variations of just a few major techniques. The first several methods described here involve the design stage of a project. The more errors found and removed during the design phase, the less expensive the overall project becomes because it results in fewer false starts and less rework of code. This translates into reduced manhours and, thus, reduced cost.
Checking design documents involves a review of forms, reports, table layouts, and relations developed during the design phase. This occurs prior to any coding.
An informal group design review involves a group of programmers, users, and designers meeting to discuss various aspects of the project. It does not require a formalized step-through of the design specifications.
Formal design inspection analyzes the critical parts of the system design and attempts to determine whether it accounts for all possible situations. It often uses decision-tree analysis diagrams to ensure coverage. Decision-tree analysis traces each major path and operation in an application and graphically displays them on paper. Due to the usual branching of options, the resulting diagram resembles the limbs of a tree.
Personal desk-checking involves reviewing code listings and walking through the process on paper (or onscreen) without actually running the program. It often requires performing hand calculations using sample data in the actual code to check the results. Some developers refer to this technique as a walkthrough; however, the term walkthrough can also apply to other review types. This technique was more popular years ago when computer time was expensive and programmers were relatively cheap. Today, the reverse is true. Thus, this method might be called on mainly to find non-trivial errors such as complex logic errors.
Test often and test early. The longer an error exists, the more expensive it becomes to find and remove.
After coding begins, the focus shifts from paper and thought reviews to formal code reviews and actual physical testing. The following paragraphs describe a few of these techniques.
Formal code inspection involves careful scrutiny of critical code segments. It provides feedback about the code, standards, use of comments, variable naming, variable scoping issues, and so forth.
Modeling or prototyping uses available tools to quickly create an application shell. Its purpose is to show overall functionality of the system. With Visual FoxPro, this involves creating basic forms and reports linked together with a simple menu. Although the forms appear functional, they might not include processing of all the business rules. Similarly, reports might not include selection and sorting options. A common term for this technique today is RAD (Rapid Application Development).
Syntax checking tests the basic correctness of the code. It checks the spelling of commands and text strings, the validity of expressions, and the basic structure of commands. FoxPro does most of this during compilation, although some syntax problems become evident only when you run the program.
Visual FoxPro does not check spelling or syntax within strings, form captions, ToolTips, status bar text, or messages. Yet these are the most visible parts of the program to users and could reflect on the entire system.
Unit testing exercises individual groups of statements. For example, when designing a new class definition, use unit testing to check the code associated with its methods. To perform unit testing, you must write a special program called a driver. The driver does not become part of the final code. Rather, you use it to set up the necessary program conditions, such as initializing variables, to test the code segment.
Even though you might test each procedure and function individually, their correct functioning does not guarantee that the program as a whole will work. However, it does narrow down the error possibilities. System testing specifically checks the interface connections between modules. Its purpose is to ensure that data and logic pass correctly to and from each module. This includes using proper parameter types and sizes. It can also look for unexpected changes to public or private variables redefined by a called procedure or function.
Functional testing checks that the major features of the program work as expected. When you select a report option from the menu, do you get the report you selected or another one, or perhaps even a form? If a form displays a message to press F2 for a list of possible values, it checks that the list really appears when you press F2. It does not necessarily include verification of the report results or that the list that appears when you press F2 contains the correct data.
Stress testing checks boundary conditions of the program, such as how the program responds to extreme data values. For example, if the program tracks weekly payroll, this form of testing might check what the program does if you enter 170 hours for the number of hours the employee worked this week (seven times 24 hours is only 168). Stress testing on a network concerns itself with how the program performs when multiple users run it. Can the program handle simultaneous updates to data? Do record and file locks perform correctly while minimizing the time when other users cannot access the data?
The speed of a program falls under performance testing. Visual FoxPro provides the flexibility to code most features in several ways, but all ways do not perform equally. In fact, some methods substantially outperform others. Performance testing looks for areas to improve program speed by identifying which portions of the program require the most time. Then you can try alternative programming methods to improve the speed.
Visual FoxPro itself is extremely fast. There are a lot of things built into the product to enhance performance (Rushmore Technology) and to diagnose bottleneck conditions (functions like SYS(3050) to adjust memory buffer size and SYS(3054) to analyze optimization in SQL SELECT statements). But most information about what to make of all this is shared anecdotally among developers: at conferences, in technical articles, and online. Among the best ongoing sources of help on that sort of issue are FoxPro Advisor magazine, FoxTalk newsletter from Pinnacle Publishing, CompuServe's VFOX and FoxUser forums, and www.universalthread.com. Annual large- and small-scale conferences are sponsored by Microsoft, Advisor Publications, various user groups, and companies around the world.
Finally, compatibility testing looks at how the program works in different environments. This can include different display monitor modes, different printers, and different directory structures. It can even involve the way the program displays regional or international fields such as time, dates, and money. Even the collating sequence of sorted data and the codepage of tables become important issues when internationalizing an application. Of course, the extent you need to concern yourself with these sorts of issues is determined by how widely your application will be distributed.
If yours is an international application, you might consider purchasing a third-party product to handle all the language, font, currency, and data type issues. The INTL Toolkit from Steven Black Consulting (www.stevenblack.com) is such a product that isolates the cross-cultural issues into a library. You simply include this library as an object, and then go on to address the business questions of your application.
Macros can be very handy in the testing process. They can be especially useful when you have a single set of commands that doesn't rely on decisions and optional logic branches. Select Tools, Macros from the system menu to bring up a dialog box for recording and managing user-defined macros. As you issue the keystrokes in your test, VFP interprets them into a code string, which you assign to a hotkey combination. In the future, typing the hotkey alone reruns the entire set of keystrokes.
One might successfully argue that testing is never complete until the application becomes obsolete. There are always new combinations of data. Furthermore, most programs change over time, adding a feature here or a field there. With each new feature, you introduce the possibility of errors. Program paths might change, new variables might overwrite similar variables in other routines, and other side effects might affect existing code. For this reason, it is important to go back and retest working portions when you introduce new features. This is called regression testing.
Some developers use a bug rate factor to determine when to stop debugging. In other words, they measure the number of bugs found over time. Although this gives a nice, neat, numeric way to determine when to cut off testing, several variations exist. For example, you could monitor the cost of looking for bugs compared to the cost of leaving them in. For example, the cost of leaving a spelling error in a text label is very low. However, the cost of an error that subtracts sales tax from total due rather than adding it is higher. Another error at a higher cost level is one that returns the wrong product price when selecting products. Therefore, time is allocated for testing different parts of the program based on how serious an error would be in those parts.
No matter what method you use, it is nearly impossible to determine when all bugs have been found. Declaring testing complete just to meet a schedule, however, is dangerously shortsighted. Testing should continue until everyone involved feels confident in the application's performance.
The last section defined several techniques for testing software.
The reason for so many different methods is that no one technique
is perfect. In fact, a survey by Capers Jones, published in Programming
Productivity, shows the effectiveness of the 10 most common
techniques. Table 23.2 reproduces this table.
|Checking of design documents|
|Informal group design reviews|
|Formal design inspection|
|Formal code inspection|
|Personal desk-checking of code|
|All of the above (used together)|
In this table, the three columns represent the minimum number of errors, as a percentage, found using each technique, the average number found, and the maximum number. These values assume one person is assigned to debug the code using one technique. The interesting point to the preceding table is that no one method guarantees error-free code. In fact, only a combination of methods finds close to all the errors.
Glenford J. Myers performed an interesting related study. He found that every programmer approaches debugging slightly differently. As a result, even the combination of as few as two programmers in the debugging process greatly improves the overall detection rate.
There are two important sets of issues concerned with creating a test environment: hardware issues and people issues. When testing an application, you should test it on the same hardware configuration that you plan to implement it on. If the application will run on a network, testing it on a standalone system can miss many of the potential problems related to record and file sharing.
You can simulate a multiuser environment even on standalone machines by opening multiple instances of VFP. Of course, having additional memory helps improve the performance of this technique.
A similar problem occurs when the software must run in various display resolutions. The same "perfect" form on one display might have fields and labels that fall off the screen when run with different display options.
Visual FoxPro gives some help in this regard. Choose Tools, Options from the system menu. On the Forms tab, use the Maximum design area combo box to select the resolution in which your application will run.
It is important to use real data to test the system when it nears completion. For that reason, a recommended mode of development creates and implements those modules needed to collect data first. Then users can begin using them to enter real data. This data can then serve as test data for subsequent development.
Of course, all development should occur physically separated from a live system and live data. If your new system is replacing an old one, you can continue the testing phase even after implementation by maintaining a parallel environment, including a duplicate set of live data files. This enables you to compare results from your new system against output as it is currently produced.
In spite of precautions, errors can occur during development that could destroy live data. For example, you could mistakenly enter the path of the live data rather than the test data and ZAP it. This also means that you should periodically back up even your test data and test programs.
Not everyone is good at testing. In fact, some people enjoy testing more than others. (These are often the same people who enjoy bureaucratic detail such as income tax forms.) Often the developers of an application make the worst testers. They know the program too well and tend to enter only correct data and keystrokes. They subconsciously don't want to make their own program fail, even if consciously they recognize the need for testing. They also don't necessarily approach the application the same way a user might. So, in order to improve an application's robustness, it is important to assign the task of testing to others besides those who created it.
On the other hand, other staff members might not have the emotional attachment to a program and therefore appear ruthless in their testing. Although "ruthless" testers might find more problems, they must exercise tact in discussing them with the developer. After all, a program is the product of someone's creativity, and you do not want to suppress that. Developers can assist the process by not taking it personally when testers uncover bugs. In order to succeed, testers and developers must maintain a mutual goal of making the application stronger.
Furthermore, Visual FoxPro provides many different ways to perform a task. Therefore, don't criticize a person's methods just because you might do it differently. If a better, more efficient way exists to perform a task, find a way to show the other developers how it is better. Help them to learn, don't force them to learn.
Test data can consist of white box testing or black box testing. White box testing thoroughly checks all program paths. Because it requires knowing the logic of the program, it is also called logic-driven testing. Black box testing requires no knowledge of the program logic. It just gathers test data and runs it. There is no inherent assumption that the test data actually tests all the program paths because the paths are not known.
A side benefit of creating white box test data is the need to review the program carefully to identify these paths. This process often uncovers problems even before testing begins. There are two goals with white box testing:
- Identify and test all defined paths required for the application.
- Ensure that no possible path is missing or unaccounted for by the application.
Whenever programmers try to "generate" test data, they almost always, without fail, miss at least one special case. For that reason, a random sample of real data generally provides just as good a test of the real operating environment. Thus, you should try to collect real data as early in the project as possible.
The best way to do this in a completely new system is to develop data entry forms or other programs that collect data with minimal functionality as early as possible. The Form Designer makes this relatively painless. Forms should include at least minimal data validation but might not include all look-up tables, context-sensitive help, or other advanced features implemented through custom methods. Alternatively, if this is a rewrite, conversion, or extension of an existing application, you must find a way to convert existing data to the format your new modules will expect. This probably means writing a data export routine that will create new versions of files for you to work with while leaving the existing ones in place.
If the application involves several processes, the task of collecting real data becomes more complex. In a real system, the processing of data takes place continuously. Therefore, you really need to capture an instantaneous snapshot of the data. Any time delay that enables users to process data could cause the data to be unsynchronized. This causes false errors (errors due to bad data, not due to program logic) when testing the program. Significant time can be lost tracking down and resolving these "false" errors. If possible, collect a copy of live data from all tables when no one else is on the system, such as at 3:00 a.m.
If you just randomly run test cases, you really don't know how thoroughly they test the procedure or application. Not only do you need to know how thoroughly the cases cover all possible program paths, you need to know what each case tests. Documenting test cases provides historical case results to compare future changes to the application against.
Although not specifically designed for Visual FoxPro, it is possible to create a series of scripts to test your application with Microsoft Test.
Testing is so important that the IEEE (Institute of Electrical and Electronic Engineers) has developed standards for documenting tests. They are summarized in the following list:
- Identify the test case, its purpose, and what features it tests.
- Identify features not involved in the test.
- Identify any requirements for the test (such as data files, indexes, relations, drivers, and so on).
- Identify any additional hardware or software requirements. For example, OLE tests require other programs, such as Excel, Word, or Mail.
- Identify any assumptions made in the test (such as minimum memory, hard disk requirements, and available floppy drives).
- Identify the steps needed to run the test.
- Identify the criteria for what constitutes passing or failing the test.
- Identify where the tester might want to suspend execution to examine the program path or variable values.
- Identify any conditions that could cause the program to stop prematurely and detail what the tester should do at that point.
- Maintain a history of tests performed for each case, who performed it, the results, and any corrective action made to the code.
- Make an estimate of the amount of time needed to set up and perform the test. After running the test, document the actual times.
Following are some additional testing guidelines:
- Test early and test often. Basically, this means test code at the lowest level possible. If you use the Expression Builder, always click the Verify button to check the expression's syntax. If you are building custom classes, build a small driver program or form to test them before using them in an application. Test individual procedures and functions. Create macros to test interactive elements such as menu selections and form behavior.
- Create a flow chart to diagram the major program paths.
- Identify calculations that require special consideration to test more thoroughly. For example, a routine that calculates the number of workdays between any two dates has more complexity than a calculation of total days between two dates. This routine can be made even more complex if it considers the effect of holidays.
- Be able to verify test results by some other independent means. This requires either the capability to perform the calculation by hand or to compare it to other results.
- Use generalized routines and class libraries as much as possible. After these are developed and tested, you need not test them again unless you change them. This saves considerable time that you can invest in other parts of the program.
Also, examine each input field and determine whether it needs any of the following:
- A default
- A picture clause
- Special formatting
- A lookup option
- Special validation or range checking
If you are going to require this same type of input field in more than one place in your application, consider making it a class.
Although it might never be possible to absolutely guarantee that you have found all errors, by asking several questions about each module you write, you can minimize the number of errors. The following list provides just a few of the possible questions you might ask. Over time, you'll undoubtedly expand this list with your own questions.
- Examine the relations between fields. Do some fields require information from others, thus making the entry order important?
- Should the user be able to exit an incomplete form? If so, what happens to the data?
- What does the program do if a required program does not exist? If a required index does not exist? If a form or report does not exist?
- Where does the program expect to find data tables?
Remember that even if there is only one location for data, it's not a good idea to hard-code this path throughout the program. At the very least, you will want to maintain a separate location for test files. Devise a method to inform the program which path you expect it to use each time you run it.
- Must the user define the SET commands before running the program? If so, how? For example, do you know that in VFP, many of the SET commands are scoped to the current DATASESSION? This means that each form can support its own SET environment by simply setting its DataSession property to 2 (private data session).
Despite all the preceding preparation to avoid common errors and the development of test sets, errors still happen. Your first priority when an error occurs is to find it and fix it. You can later go back to your test cases to determine why they didn't discover it in the first place.
When an error occurs during program execution, Visual FoxPro displays an error box with a simple error message. Beneath the message, it has four buttons: Cancel, Suspend, Ignore, and Help. Most of the time, you do not want to ignore an error. If you write programs for other users, you never want them to ignore an error. In fact, you probably don't want them to see this default error message. Rather, you want to trap all errors with your own error handler, which documents the system at the time of the error and gracefully exits the application.
As is true of any rule, however, there might be a few exceptions. If the program fails because it cannot find a particular bitmap on the system where it's currently running, you might be willing to ignore that error. Most other errors you cannot ignore. If VFP cannot locate a table, ignoring the problem does not enable the program to run anyway. It needs that table for a reason. So just document the problem and quit the program.
As a developer, you will find that Suspend is a valuable option during testing. It stops program execution without removing current variables from memory. Any tables currently open remain open with their record pointers in place. Most important, you can then open the Trace window of the debugger. The Trace window displays the source code being executed, highlighting the current line. While suspended, time stops for the application, enabling you to examine details of the program and its variables. You can even use the Trace window to execute the program line-by-line.
Visual FoxPro includes several built-in functions to provide clues when errors occur. To use them, you must interrupt the default error handling of Visual FoxPro. Rather than display a sometimes cryptic message to the user, you want to pass control to an error-handling routine. Listing 23.6 shows a simple error routine that you should consider. It displays a little more information than the standard error window by using a multiline Wait window. You could use the same approach to log errors, along with some user identification, to a file instead of onscreen. That way you can review the entries to get a better picture of how frequently, and how universally, the errors occur. The user information enables you to track down who is having the problem so that you can get more information about what they were doing at the time.
Listing 23.6 23CODE06.PRG-A Routine to Move Beyond the Standard Error Window to a More Informative Approach
ON ERROR DO ErrLog WITH ; ERROR(), MESSAGE(), MESSAGE(1), LINENO(1), PROGRAM() ** Rest of the application ** PROCEDURE ErrLog LPARAMETER lnErrorNo, lcMessage, lcErrorLine, ; lcErrLineNo, lcModule WAIT WINDOW ; 'An error has occurred in: ' + lcModule + CHR(13) + ; 'ERROR: ' + STR(lnErrorNo, 6) + ' ' + lcMessage + CHR(13) + ; 'On Line: ' + STR(lcErrLineNo, 6) + ' ' + lcErrorLine RETURN
The ERROR() function returns a number that represents the error. The appendix in the Developer's Guide lists all error numbers along with a brief description, as does the Error Messages section of VFP's online Help.
Perhaps more useful is the MESSAGE() function. When used without a parameter, MESSAGE returns an error message associated with the error. This message is usually the same one that appears in the Error dialog box displayed by Visual FoxPro. As a bonus, MESSAGE(1) returns the program line that caused the error.
The function LINENO() returns the program line number that suspended the program. By default, this line number is relative to the first line of the main program. Because a typical application calls many procedures and functions, this value has less importance than the one returned by LINENO(1). This function returns the line number from the beginning of the current program or procedure. Use this value when editing a program by opening the Edit pull-down menu and selecting Go to Line. After entering the line number at the prompt, the editor places the insert cursor at the beginning of the error line. The default Visual FoxPro text editor can display line numbers if you check Show Line/Column Position in the dialog that appears when you select Edit, Properties. If you want to see line numbers in the Trace window, select Tools, Options, Debug and choose the Trace option button.
PROGRAM(lnLevel) returns the name of the executing program if lnLevel is 0, and the main program if it equals 1. It reports deeper call levels as the value of lnLevel increases until it returns an empty string. Visual FoxPro supports nested program calls up to 128 levels deep. This function is similar to SYS(16) except that SYS(16) also includes the path. When the error occurs in a procedure or function, SYS(16) begins with its name. Then it displays the path and the parent program name.
SYS(16,lnLevel) also supports a second parameter that tells it to print the program names at a specific level in the calling sequence. If lnLevel is equal to zero, the function returns the name of the currently executing program. An lnLevel value of 1 begins with the main program. Sequential values step through the procedure calling sequence until reaching the procedure or function containing the error. At that point, subsequent values of the parameter return an empty string. The code that follows traces the calling sequence up to the error:
lnLevel = 1 DO WHILE !EMPTY(SYS(16, lnLevel)) ? SYS(16, lnLevel) lnLevel = lnLevel + 1 ENDDO
The most obvious place to start tracking down reasons for an error is at the point where the program fails. You might not know exactly where that point is, but you probably have a good idea of which routine is giving you a problem. Maybe you see a hard crash every time you enter data into a particular field or click a certain button on the form. Or it might be more subtle than that. Maybe the crash occurs while you're closing the form, and parenthetically, saving data and cleaning up from previous tasks.
The first thing to do is run the program up to a point as close to the failure as you can get and then suspend execution. You accomplish suspension in any of a variety of ways. Many developers set up a hotkey that enables them a backdoor into debugging mode. A command such as the following enables you to suspend your program even at points that don't appear interruptible-for example, when you are in a "wait state" because the current command is READ EVENTS:
ON KEY LABEL F12 SUSPEND
If you are successful in getting to a point where things have not yet gone off track, you will have an environment with all memory variables and tables intact and open. You can then use the Command window, the View window, and the debugger to display all sorts of information to lead you to the error.
You can print things such as variable values to the screen, checking for undefined or unusual types. If you find an undefined variable, you at least know what to look for. You can examine the code preceding the current line for any clues as to why the variable has not been defined.
Remember to issue an ACTIVATE SCREEN command before displaying any values on the desktop. The default for commands such as DISPLAY MEMORY, DISPLAY STATUS, and ?lcVariable is to use the currently active output window, which is probably one of your application's screens.
If all the variables look correct, you can enter and test the line that failed. If the line contains a complex expression, test portions of it to determine where it fails. If you find a simple error, it might be possible to correct and test it while you are still in the Command window.
Copying and pasting ensures that you don't add new syntax errors by retyping lines.
Suppose that the expression looks correct and all variables are defined, but the line still fails. You might have the wrong syntax for a function, be missing a parenthesis, or have some other syntax error. In this case, use FoxPro's online help to check the proper syntax for any command or function.
To get fast help on a Visual FoxPro command or function, highlight it in the program edit or Command window and press the F1 key.
Bring Up the Data Session Window There are a lot of things you can do while your program is suspended. You can bring up the Data Session window (known as the View window in FoxPro 2.x) and check out much of the data picture through it. You can browse files, check sort orders, inspect structure, and even open additional files.
If the system menu has been replaced by an application menu, you might not have access to Window, Data Session. If that's the case, simply enter SET in the Command window.
You might be surprised to see unexpected files or even no files at all listed in the window when it first appears. Chances are you are looking at the environment for the default data session, but you have set up your form in a private data session. Check the entry in the drop-down list labeled Current Session. Pull down the list and select the one corresponding to your application form and you should see the expected files.
Tricks in the Command Window VFP provides you with some extremely powerful new SYS() commands that you can use for debugging. In particular, SYS(1270) has become the darling of many developers of my acquaintance. To use it, position the mouse over something on the screen, and type the following in the Command window:
ox = SYS(1270)
The variable ox then contains an object reference to whatever was pointed to. That object can be a running form or a control on the form, depending on just what was under the mouse. Or, the object reference might be to a class or a form or a control in design mode. You can tell exactly what that reference is by typing ?ox.Name in the Command window. If you're interested in something further up or down the containership hierarchy, reposition the mouse and reissue the SYS(1270) command.
So, why is this useful? After you have a reference to some object, you can find out anything you want about that object. You can check its ControlSource property, for example, to make sure you're getting data from the right place. Maybe you notice that the object just isn't behaving as you expect. Try displaying ox.ParentClass or ox.ClassLibrary to make sure you're working with the intended version. Better yet, you can change any enabled properties on the object, or fire events, simply by typing appropriate lines into the Command window, like this:
ox.AutoCenter = .T. ox.Hide()
SYS(1270) is another command you might want to attach to a hotkey. Save yourself having to type it every time you want to change the object reference by instead typing the following line once:
Sometimes an error occurs in a section of code that you cannot test separately. BROWSE and SELECT statements often extend over many lines. When Visual FoxPro reports an error in these commands, it cannot tell you which line within the statement contains the error. For these long commands, it can be difficult to quickly spot the error. The SELECT statement that follows illustrates this situation with a complex SELECT that includes several tables as well as a union between two separate SELECTs:
SELECT A.cStoreId, A.cTicket, A.cItemId, A.nQuantity, ; A.nUnitPrice, A.nExtPrice, ; B.cEmplId, B.cCompanyId, B.cDeptNo, B.cBatch, ; B.dDate AS DATE, ; C.cCenterNo, D.cLastName, ; LEFT(E.cProdDesc,25) as ProdDesc, ; G.cCoName, H.cDeptName ; FROM TKTDETL A, TICKET B, CENTERS C, CUSTOMER D, ; PRODUCT E, COMPANY G, DEPARTMT H ; WHERE A.cStoreId+A.cTicket = B.cStoreId+B.cTicket AND ; A.cStoreId+A.cTicket+A.cItemId = ; C.cStoreId+C.cTicket+C.cItemId AND ; &FiltStr1 ; UNION ALL ; SELECT A.cStoreId, A.cTicket, A.cItemId, A.nQuantity, ; A.nUnitPrice, A.nExtPrice, ; B.cEmplId, B.cCompanyId, B.cDeptNo, B.cBatch, ; B.dDate AS DATE, ; SPACE(10) AS cCenterNo, D.cLastName, ; LEFT(E.cProdDesc,25) as ProdDesc, ; G.cCoName, H.cDeptName ; FROM TKTDETL A, TICKET B, CUSTOMER D, ; PRODUCT E, COMPANY G, DEPARTMT H ; WHERE A.cStoreId+A.cTicket = B.cStoreId+B.cTicket AND ; A.cStoreId+A.cTicket NOT IN ; (SELECT F.cStoreId+F.cTicket from CENTERS F) AND ; &FiltStr2 ; INTO CURSOR MPA
Debugging a SELECT statement this complex is difficult without taking advantage of another technique called code reduction or decomposition. The purpose of code reduction is to reduce the amount of code used in a test. In this case, an obvious first attempt at reducing the test code is to split the individual SELECT statements and test each one separately. After you determine which SELECT statement causes the error, you can remove additional code from it until it finally works. With a SELECT, further reduction can mean removing one table at a time along with its related fields and relations. Or, you could begin by removing sorts or groupings. With any method you choose, at some point the SELECT statement begins to work. It is then a relatively simple matter to determine what is wrong with the code just removed.
After finding and correcting an error, you can proceed in several ways. You could take a pessimistic approach and incrementally rebuild the SELECT one table or feature at a time making sure it continues to work. Or you could take an optimistic approach and test the changes in the original complete SELECT statement. Of course, there are levels in between. In this case, you might want to test the individual SELECT statement with the correction before copying it back into the union.
You can apply this same approach to any code, not just single commands like SELECT. Take any program or procedure that does not work and comment out functionality until it does. Whatever you mark as comments probably contains the error.
A review of recent program changes provides another good clue to an error's cause. The most likely place to look when an error occurs in an existing program that ran fine previously is in any recent code changes. This is not a guarantee. After all, the error might reside in a code path that was not executed before. However, it is a good place to begin.
This brings up another point. When you make changes to your programs, it's a good idea to leave the old code in place for reference, at least until you're certain that the replacement code runs correctly. Of course, you don't want the program to run both the old and the new code. You should insert a comment identifying the nature of the change and comment out the old code.
Although you can comment out individual program lines by adding an asterisk in front of each one, this can become tedious for blocks of code. As a shortcut, select the lines of code you want to comment by clicking the mouse at the beginning of the first line and dragging through the lines to highlight everything you want to skip. Then choose Format, Comment from the system menu or right-click and choose Commen
Not all errors point to obvious lines of code. Sometimes a logic error originates in an entirely different section of the code from where the program finally fails or displays erratic behavior. In these cases, you need to spot check the program's activity at various points.
One way to check a program's path is to embed WAIT statements throughout the code using the following format:
WAIT WINDOW 'Beginning of PROCEDURE COPYDATA' NOWAIT
Every time execution passes a statement like this, a Wait window appears in the upper-right corner. The NOWAIT option enables the program to continue without pausing. (If you want the program to pause, skip the NOWAIT option or use the MESSAGEBOX() function with only the first argument to display a message.) Adding WAIT statements has an additional advantage for programs that perform extensive background processing. Even when used in a production environment, they assure the user that the program is still executing. Users who think a program has stopped might reboot the computer, which can lead to data corruption.
You can also halt the program at any point with the SUSPEND command or add commands to print or list information about the program such as those shown in Listing 23.7.
Listing 23.7 23CODE07.PRG-Commands to Output Information About the Current Program State
lcMemFile = 'MEM' + LEFT(CTOD(DATE()),2) + ; SUBSTR(CTOD(DATE()), 4, 2) + '.TXT' lcStatFile = 'STAT' + LEFT(CTOD(DATE()),2) + ; SUBSTR(CTOD(DATE()), 4, 2) + '.TXT' LIST MEMORY TO FILE &lcMemFile LIST STATUS TO FILE &lcStatFile
Of course, you don't want these commands to execute every time a user runs the program. If you enter them as shown previously, there is a high risk that you will forget and leave them in the final user version. Because users do not need to see this output, you might want to bracket these commands with an IF ENDIF such as the following:
IF glDebugMode << Place any debug command here >> ENDIF
You could then initialize the variable glDebugMode at the beginning of the program to turn these commands on or off. By using a variable and defining it in the main program, you need to change and compile only one routine to turn these statements on. A better technique stores the value for glDebugMode outside the program, perhaps in a configuration table or memory variable file. Then you can activate the debug mode without having to recompile the system at all.
Even when these commands are inactive, they require memory, both for the additional program lines and the memory variable. An alternative uses the #DEFINE and #IF #ENDIF directives to include or exclude the debug code at compile time.
#DEFINE glDebugMode .T. #IF glDebugMode << Place any debug commands here >> #ENDIF
A variation on the idea of displaying information in windows throughout the application is to use a feature that was introduced in VFP 5. Instead of WAIT WINDOW or MESSAGEBOX() calls, code ASSERT commands at strategic points using this syntax:
ASSERT lExpression MESSAGE cMessageText
When the program encounters this command, it evaluates the expression you've coded. If the expression is True, execution continues. If it evaluates to False, VFP displays your message in a dialog box with the options Debug, Cancel, Ignore, and Ignore All. Your message is optional. If you omit it, VFP issues a default: Assertion failed on line n of procedure cProcedureName.
There's a catch. In order to get VFP to test your assertions, you must issue SET ASSERTS ON. Maybe you've tested the code extensively and you're pretty certain it's working as expected. You can leave the individual ASSERT commands in place and simply issue SET ASSERTS OFF to keep your program from having to continue running through the assertion tests each time.
Put ASSERT commands in logic paths you believe will never be reached. This might be an OTHERWISE in a DO CASE ENDDO where you believe all conditions are accounted for, like this:
When you have reason to believe that your code is straying into places it shouldn't be, simply use SET ASSERTS ON before running the program.
Visual FoxPro provides an excellent tool called the debugger, which actually consists of a series of tools. One of these is the familiar Trace window used in previous versions. This window is joined by the Watch, Locals, Call Stack, and Debug Output windows. The Watch window resembles the Debug window of previous versions, but it has a few significant enhancements.
See Chapter 1 "Quick Review of the Visual FoxPro Interface," for information on setting up the debugger. If you configure the new debugger to be housed in its own environment, the Debug Frame, you will see something like Figure 23.1 when you start the debugger.
Of course, you are not limited to keeping the debugger in its own frame or to having the individual windows docked. You configure the first option in VFP itself via Tools, Options, Debug. Choose FoxPro Frame in the Environment drop-down list to keep all the debugging windows within the VFP desktop. In this case you are free to open any or all of its individual windows as you see fit.
You can only change your choice of debugging environment when the debugger is not active.
When you configure the debugger to be in its own frame, you are less likely to encounter situations in which VFP confuses the debugging windows with its open application windows. This problem was prevalent with the debugger in all releases of FoxPro prior to VFP 5. It caused actions in the Trace and Debug windows to be intermixed with application events.
When the debugger is in its own frame, it can seem to disappear when your VFP application is active. It has not closed. The window that currently has focus might simply be covering it. You can switch between FoxPro and the debugger using any of the methods for switching among Windows applications, such as Alt+Tab.
If you right-click any of the five windows, you see a context-sensitive menu appropriate to that window. All five allow you to enable docking view. When checked, you should be able to position that window alongside any of the four walls of its frame, adjacent to any of the other debug windows, or docked to another toolbar.
Using the Trace Window The debugger provides the Trace window, a powerful tool, to debug code. Trace opens a window that displays the program's commands as they execute. It also enables you to step through the program one command at a time so that you can see the exact path being followed. Figure 23.2 shows a typical Trace window.
Visual FoxPro executes programs referenced by a path in a different directory. However, it cannot reference source code in any but the current directory. If it cannot find the source code, it displays the message Source Not Found.
There are several ways to enter Trace mode. One way is to place the command
SET STEP ON
in the code, recompile the program, and run it. When the program executes this command, it stops and displays a Trace window. Early in the debug process, you might be tempted to put this command throughout the program. As with SUSPEND or hard-coded print statements, the danger is in forgetting to remove one or more of them.
Another way to open the Trace window is to select Debugger from the Tools menu, or simply enter DEBUG in the Command window before executing the program. This opens the debugger, from which you can open the Trace window. If it is not already open, open the Trace window by selecting Windows, Trace or by clicking the Trace button in the toolbar. Notice that the Trace window activates other options in the debugger's menu bar. Figure 23.3 defines the toolbar buttons.
To select an application or program, choose File, Open. This displays the Open dialog box. By default, it shows .PRG, .FXP, and .MPR files in the current directory. It also shows .APP and .EXE files. If you select an .APP file, a dialog box appears so that you can select a program from within the application. If only .FXP files can be found for the application, they are shown with dimmed text, because they cannot be used in the Trace window. You must have access to the source files.
|Activating and Deactivating Debugger Windows|
If the Visual FoxPro debugger window is open, you also can open the Trace window with the command
If you select a program (.PRG) file, Visual FoxPro displays the source code in the Trace window. Notice that the Trace window also supports syntax coloring, just like the editor.
After selecting a program or application module, use the vertical scrollbar to display different lines of the module. If the line is wider than the width of the Trace window, you can scroll horizontally to see it all.
This capability to horizontally scroll the Trace window is great, but you should still try to limit the length of each line to the width of the editor window (assuming that the window is as wide as possible on your screen). Long lines are harder to read onscreen because of the need to keep switching from the vertical scrollbar to the horizontal one. Similarly, printed listings of the code with long lines are difficult to read. If you have a long command, split it across several lines by ending each line, except the last, with a semicolon. This punctuation tells Visual FoxPro that the command continues on the next line.
To mark a line with a breakpoint, double-click in the shaded margin bar next to the line or place the cursor anywhere in it and press the Spacebar. You can also use the Toggle breakpoint button in the toolbar after selecting the line. If the line is an executable line, Visual FoxPro places a red circle to the immediate left of the line in the shaded margin bar, which serves as a visual reminder of the breakpoint. If you click a comment line, Visual FoxPro marks the next executable line after the comment. If you click one of a command's continuation lines, VFP marks the current command with the breakpoint. The circle is always placed on the last line of continuation lines.
Mark as many breakpoints as you need. Each time the program reaches a breakpoint, it treats it like a SUSPEND statement in the program and interrupts execution. It does not cancel the program. If you mark the wrong line, simply double-click it again (or highlight it and press Enter) to remove the mark.
While the program is suspended during a breakpoint, you can do any of the following:
- Read forward or backward through the code using the scrollbar.
- Choose to view a different object or routine using list boxes in the Trace or Locals window headers or point to a different procedure in the Call Stack.
- Open the Data Session window to check the status of open files, their indexes, current record status, or even browse the data.
- Enter commands in the Command window.
- Select options from the main menu.
- Check or change the current value of any memory or table variable.
- View the contents of entire arrays and change individual cells if necessary.
- See the hierarchy of routines that has called the currently running procedure.
- Change statement execution order.
The one thing you cannot do is change the executing program's source code.
The new debugger is extremely powerful in what it enables you to change while your application is still active. Make changes with caution, however, because you can easily leave the environment in a compromised state from which it can't recover. If you move a record pointer or change the current work area, for example, you must return everything to its original state. Otherwise, attempts to resume stepping through the program could fail. If you change the contents of memory, you might inadvertently leave out other settings that your program expects to go along with that value.
If you want to trace an application, but do not want to set a breakpoint, just click the OK button when VFP displays the application module list. Visual FoxPro always begins execution of an application from the main module regardless of which module appears in the Trace window.
When you finish debugging your application, you need to remove all breakpoints. You can do this one breakpoint at a time in the Trace window. A more efficient method uses the Clear All Breakpoints toolbar button, which removes all breakpoints from the entire application. This saves you from the trouble of having to remember where you set breakpoints. After making a change to a module, Visual FoxPro recompiles it before running it. Recompilation removes breakpoints. Therefore, you might have to mark them again.
The Debug menu contains an option, Throttle, that controls the length of a pause between each executed line. The default of 0 means that there is no delay. This does not mean that programs executing with the Trace window active run at the same speed as a normal program. In fact, their speed is greatly reduced. The video drivers cannot keep up with the speed at which Visual FoxPro usually executes code. However, you still might not be able to follow even this reduced pace. Therefore, you can increase the delay between each command from 0 to 5 seconds. While the trace is running, you can cut the speed in half by pressing any of the following buttons:
A mouse button
To cut it in half again, press any two of these at the same time.
You can also set the throttle by changing the value of the system memory variable _THROTTLE, as in:
If you click the Resume button in the toolbar, select Debug, Resume, or press F5, the program begins execution. Visual FoxPro highlights each code line in the Trace window as it executes. Of course, unless you can speed-read program code, you probably cannot keep up with it without increasing the Throttle value.
You can interrupt program execution while the Trace window is open by pressing the Esc key as long as SET ESCAPE is not OFF. You can set this feature by checking Cancel Programs on Escape in the General tab of Tools, Options. This feature is useful to stop Trace mode without running the entire program.
Strongly consider adding the command SET ESCAPE OFF before distributing the application. This prevents users from aborting a program prematurely by pressing Esc.
While you are tracing a program, the toolbar provides four ways to step through your code, each with a keyboard alternative:
Steps into a function (F8)
Steps over a function (F6)
Steps out of the current function (Shift+F7)
Runs current program to line containing the cursor (F7)
The most used toolbar button is the Step Into button. It tells Visual FoxPro to execute the next line of the code and then to pause again. Use it to step through a program one line at a time at your own pace. At any time, you can stop to check variables or tables. To continue, activate the Trace window and click this button again. If the next line happens to be a call to a procedure or function, Trace continues stepping through the code in that procedure or function.
If you want to step through code only in the current procedure and not trace a procedure or function call, click the Step Over button. This tells VFP to execute the procedure or function, but not to trace the code. If you accidentally step into a procedure or function, use the Step Out button to execute the rest of the code in the current procedure or function and then return to trace mode when it returns to the calling program.
Another way to execute a block of code without tracing through it is to place the cursor in the line where you want to begin tracing again and then click the Run to Cursor button to execute the code without tracing until it reaches the line with the cursor. Note that if you do not select carefully, you could select a line that is not executed due to program loops or conditional statements. Thus, the program will not suspend again.
If you decide to stop tracing the code but want to finish executing it, click the Resume button. It tells Visual FoxPro to continue executing the program until it encounters either another breakpoint or reaches the end of the program. Click this option after stepping through a suspected problem area to complete the program normally. You also can terminate the program by clicking the Cancel button or by selecting Cancel from the Debug menu.
A new feature is the capability to set the next executable line while in trace mode. Normally, FoxPro executes each line one after the other sequentially. Only conditional statements, loops, and function/procedure calls change this sequential execution of code. Suppose, however, that you are testing a program that suspends due to an error in a line. You might not be able to change the line to correct it (with the Debug Fix option) without canceling the program. Suppose that you can enter the correct command line through the Command window and then change the program line pointer to skip the problem line to continue execution without having to cancel the program. That is exactly how you can use the Set Next Statement option in the Debug menu. Just remember to go back and correct the program line later before running it again.
Another use for the Set Next Statement feature is the capability to bypass unwanted settings. For example, some routines set THISFORM.LockScreen = .T. while they manipulate a lot of information on the current form. VFP continues its processing, without stopping to refresh the display, until it encounters a THISFORM.LockScreen = .F. command. This makes the screen display appear snappier, but it makes debugging more difficult because you don't see any of the changes as they are taking place. Use the debugger to change the execution path and go around the LockScreen = .T. setting.
Another new feature is the Breakpoints dialog box. Click the third toolbar button from the right or select Breakpoints from the Tools menu. This displays the dialog box shown in Figure 23.4.
This dialog box defines options for each breakpoint. Notice the list box at the bottom of the dialog box. It lists all the breakpoints in the current module. Initially, no breakpoints are selected. Click one to see information about it.
The Location text box begins with the name of the program or function that contains the breakpoint and the line number from the beginning of that program or function. The File text box displays the name of the file where the program, procedure, or function is found. This includes the full path.
If you added the breakpoint by double-clicking to the left of the line or by using the Breakpoint toolbar button, the default breakpoint type is Break at Location. This means that the program merely pauses execution when it encounters this line. You have other options, however.
Even with the default breakpoint type, you can specify the Pass Count at which trace begins. Suppose that you have an error inside of a loop. You know that the first 100 times through the loop, the calculation is valid; however, an error occurs shortly after that. You certainly don't want to step through the loop 100 times before reaching the error. An easier way is to set Pass count to 100. This tells the debugger to start trace mode only after it has reached the 100th pass through the loop.
You can change the breakpoint type by opening the Type drop-down list. The options include the following:
Break at Location
Break at Location if Expression Is True
Break When Expression Is True
Break When Expression Has Changed
The second option is similar to the first but relies on an expression value rather than a pass count. For example, you might know that the error occurs when a specific variable is set to a specific value, but you don't know in which pass. Suppose that the variable TOTAL is zero when the expression attempts to use it as the dividend in an equation. Dividing by zero is not allowed. Therefore, you might want to set a breakpoint on the equation but set the expression to TOTAL = 0. Note that you can also use the Expression builder button to the right of the field to help build the necessary expression.
Perhaps you would rather know when the variable TOTAL is set to zero. In this case, change the breakpoint type to Break When Expression Is True. Enter the expression TOTAL = 0 again and then click the Add button along the bottom right of the dialog box. This adds a new breakpoint to the list, but one that is not represented by a specific line in a specific code module. This option is equivalent to the way breakpoints were set to debug expressions in previous versions of FoxPro and Visual FoxPro.
Similarly, you can use the type Break When Expression Has Changed to cause the program to enter trace mode when the value of the expression entered in the Expression text box changes. Again, this breakpoint type cannot be represented by a specific line in the code.
If you set your breakpoint to fire when an expression has changed, it will also fire when it goes out of scope. It can be pretty annoying to have your program stop every time you enter another method when you are trying to track a local variable.
As you can see by the buttons in the bottom-right corner of the dialog box, you can also remove individual breakpoints by selecting them and clicking the Remove button. A new feature is the capability to disable a breakpoint without removing it. Click the Disable button while a breakpoint is selected to toggle the disable feature. Notice that this adds and removes the check in the box before the breakpoint description in the list box. This makes it easy to add breakpoints and selectively turn them on and off. Finally, the Clear All button lets you remove all breakpoints. You can also clear breakpoints in the Watch window that you have set in that window.
Canceling a program while you are in trace mode can leave files and windows open, updates partially complete, and records and files locked. In addition, environment SET variables might have changed the environment and might not have been reset.
Using the Locals Window The Locals window remains empty until you run the program. At that point, it displays all the variables as they are defined, along with their current value and type. This list is by procedure or function. So local, in this context, means all variables that are "seen" by the currently selected procedure or function. Figure 23.5 shows some of the local variables defined early in the execution of the Tastrade example.
As you can see in Figure 23.5, the Locals window also shows object variables. Notice also in the Trace window, at the top of the figure, that the cursor is on the variable luRetVal. Beneath the cursor is a ToolTip-like box with the current value of this variable. The Trace window lets you position the cursor over any variable in the code and display the value of that variable. Notice that this is the same variable and value that appears at the bottom of the Locals list. If your program is long and has dozens of defined variables, this ToolTip-type method of checking a variable value can be quite useful.
Back in the Locals window, the box with the + in it, before the variable name for an object, indicates that you can open this object to look inside. In fact, if you click this box, you can open the object to see the values for all its properties. If the object is a container object, you can see the objects it contains. These, too, can be opened. In fact, you can drill down through as many levels as you like to get to the base objects and their properties. Figure 23.6 shows three levels open in one of the objects from Tastrade. If you examine this code further, you will find that there are many more than three levels, and you can see them all.
The drop-down list at the top of the Locals window enables you to look at the variables available in any of the procedures or functions that have executed. Just open the list and pick a different procedure or function to see what variables it can "see." Global variables are seen in all procedures after they are defined. Private variables can be seen in the procedure in which they are defined and any called procedures. Local variables can be seen only in the procedure in which they are defined.
Using the Watch Window Sometimes, you don't want to see all the variables. Rather, you want to see only one or two to determine why they don't get the values you expect. Or maybe you want to set an expression that is not in the Locals window variables list. In this case, it makes more sense to use the Watch window. Simply enter the variable names in the Watch text box, or if you prefer, you can drag variables or expressions directly from the Trace or Locals windows to either part of the Watch window. When you enter each variable or expression, it appears in the list below the text box. After VFP has enough information to calculate a value, it is displayed in the second column, and its type appears in the third column. If the variable has not yet been defined or it is out of scope, the message Expression could not be evaluated appears in the Value column.
Figure 23.7 shows an example of this window using some of the fields from the Locals window. Notice that although the Locals window can look at any routine and what variables are defined there, the Watch window sees only the variables in the current procedure or function.
Note the circle in the left column next to the last watch variable. This shows another way to set a breakpoint on a variable. In this case, the program breaks whenever the value of luRetVal changes. To prove this, you can open the Breakpoints dialog box to see that the breakpoint appears in the list at the bottom of the dialog box.
The Watch window is also monitoring some expressions. You can set breakpoints on expressions as well as memory variables; for example, one that looks for a particular method name as part of the name of the current program.
Using the Call Stack Window When you are executing an application, it is very easy to get lost in the number of procedure and function call levels. In fact, sometimes the problem is that the program is calling procedures or functions in a sequence different from what you expected. One way to determine exactly how the program got to the current line is to open the Call Stack window, as shown in Figure 23.8.
Just as the arrow in the Trace window shows the current line being executed, the arrow in the Call Stack window shows the currently executing procedure or function.
When you click any of the procedures or functions higher in the calling hierarchy, the debugger automatically changes the procedure looked at by the Locals window to show the variables known in that procedure. While you are looking at a procedure other than the current one, a black right-pointing arrow appears to the left of the procedure name. These changes do not affect which line is executed next.
Using the Debug Output Window The Debug Output window displays character strings defined by the DEBUGOUT command. It also shows the names of system events that occur when event tracking is enabled.
The DEBUGOUT command has this syntax:
This command can be placed in your program to print a string to the Debug Output window. Because output goes only to the Debug Output window, you can leave these commands in the code even when distributing the application to end users. Unless users have the full version of Visual FoxPro and have the Debug Output window of the debugger open, they will never know that these commands are there. Yet, any time you need to debug the application, you can simply open the Debug Output window to watch the values that print.
Typical uses of DEBUGOUT include:
- Print the current value of any variable.
- Print messages to indicate where the program is (at the beginning of procedures, object methods, and so on).
Using Event Tracking Sometimes, it helps to know in what order events are fired for different objects. The real secret to object-oriented programming is knowing what code to place in the methods of each event so that they execute in the correct order. Sometimes, when your program just isn't working right, the easiest way to determine whether you have code in the correct method is to turn on event tracking.
While in the debugger, select Tools, Event Tracking to display the Event Tracking dialog box, as shown in Figure 23.9.
The top portion of this dialog box lets you select which events you want to track. By default, all events are preselected, but event tracking is not enabled. Remember to click the check box at the top of the dialog box to enable Event Tracking. To move an event from one list to the other, select it and then click one of the buttons between the two lists. You can also double-click an event to move it from one list to the other.
The bottom portion of the dialog box determines where VFP sends the output from event tracking. By default, the messages are sent to the Debug Output window of the debugger. For large programs, this list can get rather long, and you might want to send it to a file instead and use the FoxPro Editor to search for specific event occurrences. Figure 23.10 shows an example of the Debug Output window with several event tracking messages.
VFP 6 comes with an all-new Coverage Profiler application. Making use of it is a multi-step process. First you have to turn Coverage Logging on by choosing Tools, Coverage Logging from the Debugger menu. You will be asked for a filename to store the raw data into, and whether you want to Append or Overwrite. Make your selections here, or with a SET COVERAGE TO command, and start up the application. When your application finishes, issue a SET COVERAGE TO with no filename to stop sending raw data to the log file.
Next you have to start the Coverage Profiler application. Do this by selecting Tools, Coverage Profiler from the system menu or by typing the following command:
DO (_COVERAGE) WITH YourLogFileName
You should see a window split into three panes like the one shown in Figure 23.11.
In the upper-left pane are objects. In the upper-right pane you see the actual path to those objects. In the bottom pane is a listing of the method that fired for the highlighted object. The marks to the left of some of the lines of code indicate that those lines were not run. You can change the nature of the marks, and you can change whether they appear on lines that run or lines that are skipped. Make those option selections by clicking the Options toolbar button.
Instead of Coverage Mode, you could choose Profile Mode also by clicking a toolbar button. You would then see statistics about the number of hits alongside the program instructions, as shown in Figure 23.12.
VFP 5 had the beginnings of coverage analysis in it. You could
turn coverage on and produce a stream of raw data about the runtime
environment. It was up to you to parse and analyze those log files.
VFP 6 has made many improvements to the process. It comes to you
now as a fully realized, customizable application. But it also
comes to you with all the components to modify, subclass, and
re-create it, as shown in Table 23.3.
|Coverage.prg||A program to instantiate the object|
|Coverage.vcx/vct||Coverage Engine classes|
|Coverage.h||Header file for all Coverage code|
|Graphics files||Various .ICO, .BMP, and .MSK files|
No amount of diligence in checking for common syntax, logic, or exception errors finds all errors. No amount of testing, even with the help of live data and the use of the debugger window, can guarantee an error-free program. Sometimes the application encounters a situation that you could not foresee and plan for. In many complex systems in use today, the number of combinations of possible data exceeds the national debt. Furthermore, no matter how foolproof you make a system, "fools" can be remarkably ingenious. It takes only one user to turn off the computer in the middle of a reindex to trash an entire file.
You have to accept the fact that at some point the program will fail. When it does, you do not want it to display a cryptic message to the user, who will probably just turn the machine off before calling you. At the very least, you want to direct the program to an error-handling routine using the ON ERROR trigger. The primary purpose of this error routine is to identify and probably record information about the error. This command redirects execution to the procedure ERRLOG:
ON ERROR DO errlog WITH ERROR(), MESSAGE(), MESSAGE(1), SYS(16), ; LINENO()
Once in the procedure, you can determine the error type by checking the error number.
There are three primary classes of errors from the user's standpoint. First are trivial errors. These include errors as simple as a printer or floppy drive that is not ready. In some cases, you can simply log the error and then skip the command causing the error and continue execution.
In other cases, a simple message to the user followed by a RETRY or RETURN command handles it. Examples of when to use this approach might be when the program attempts to read a file from a floppy drive and there is either no disk in the drive or the one the system is trying to write to is write-protected. A message to users telling them how to correct the situation along with a RETRY button makes sense here.
For another example, suppose that while using SKIP to move through records, you overshoot the end of file. Visual FoxPro reports this error as End of file encountered. More specifically, it is error number 4. There is no need to cancel the program for this type of error. To correct the problem, the program can simply reset the record pointer on the last record in the table. The version of procedure ErrLog shown in Listing 23.8 presents an outline of these techniques.
Listing 23.8 23CODE08.PRG-The ErrLog Procedure to Handle End-of-File Situations
**************** PROCEDURE ErrLog LPARAMETERS lnErrorNo, lcMessage, lcErrorLine, ; lcmodule, lnErrorLineNo * Check if beyond end of file, place on last record IF lnErrorNo = 4 WAIT WINDOW 'AT LAST RECORD' TIMEOUT 2 GOTO BOTTOM ELSE CANCEL ENDIF RETURN
This example checks whether the error number equals 4. If so, it displays a window telling the user that it has moved the record pointer to the last record. It then exits the routine and returns to the line immediately after the one that caused the error. Presumably, this enables the program to continue executing. If any other error occurs, this routine cancels the program.
When Visual FoxPro encounters RETURN in an error-handling routine, it attempts to continue execution from the line immediately after the one that caused the error. The RETRY command tells it to try to continue execution by reperforming the line that caused the error. You must determine which, if any, of these recovery methods apply to each error handled.
In addition to checking whether the record pointer is beyond the end of file, you can check other conditions. Listing 23.10, at the end of this chapter, shows a few more. However, it is not meant to be an all-inclusive example. Rather, it merely shows the types of ways you can handle selected errors.
A more serious error level is one that requires either more calculations or assistance from the user, but is still recoverable. It might involve files or indexes. Sometimes, files get deleted or moved, especially with systems on a network. In these cases, a program stops with an error as soon as it attempts to open a file but cannot find it. Listing 23.9 uses the GETFILE() command to prompt the user to locate the file. Because GETFILE() uses the Open dialog box, users can search any drive or directory they have access to in order to find it.
Listing 23.9 23CODE09.PRG-The ErrLog Procedure to Enable the User to Locate a Missing File
**************** PROCEDURE ErrLog LPARAMETERS lnErrorNo, lcMessage, lcErrorLine, ; lcModule, lnErrorLineNo * No table in use or table not found IF lnErrorNo = 1 OR lnErrorNo = 52 LOCAL lcNewFile SELECT 0 lcNewFile = GETFILE('DBF', 'Select a DBF:', 'SELECT') IF EMPTY(lcNewFile) CANCEL ELSE USE (lcNewFile) SHARED IF lnErrorNo = 1 RETURN ELSE RETRY ENDIF ENDIF ELSE CANCEL ENDIF RETURN
This example checks for two error values. Error number 1 indicates that the file does not exist. This means that the named file does not exist in the current directory or in the directory referenced by the program. The second error code, 52, says that no table is in use. This error occurs when the program attempts to perform any table related command while in an empty work area. In both cases, the program needs a file.
The preceding code prompts the user to select a file using GETFILE(). Of course, you might not want to do this for all users because they could easily load the wrong file; however, note that it is a possible solution. When the user selects a table, the program opens it. If the original error number is 1, the program probably is trying to open a file with the wrong name or directory. In this case, you want to continue execution on the line immediately after the error because the error handler has just allowed you to open a file manually. Otherwise, the program would continue to try to open the file in the wrong directory or with the wrong name.
On the other hand, if the program originally attempts an IO function in an empty work area, FoxPro generates error 52. In this case, once you've manually selected a file, you would want to continue execution on the same line that failed.
The authors do not recommend this technique as a general solution. It is too easy for the user to specify any file in any directory and potentially cause even greater damage. It is also possible that the filename could exist in more than one directory on the server and contain different data sets. In this case, they might continue executing, but with the wrong data and thus produce incorrect results. Generally, the best thing to do is to use the FILE() command to check for the existence of the file in the expected directory. If it is not found, display a message to the user, log the error, and exit the program.
Unfortunately, most errors are not recoverable from a user's standpoint. These errors range from coding errors to corrupted files. The best that you can do as a programmer is to document as much as possible about the system when the error occurs. Listing 23.10 shows one way to do this. Then, have the routine display a message to users telling them what has happened and terminate the program.
This listing shows a more complete version of an error-handling routine. Observe the code after the comment Unrecoverable Errors. This segment captures information about the system at the time of the error and saves it to a file. The filename is coded with the month, day, hour, and minute the error occurred. It handles even the possibility of multiple errors at the same time by changing the extension.
Listing 23.10 23CODE10.PRG-A More Complete Error-Handling Routine That Effectively Deals with Trivial, Recoverable, and Unrecoverable Errors
* Test driver for PROCEDURE ErrLog CLOSE ALL ON ERROR DO ErrLog WITH ERROR(), MESSAGE(), ; MESSAGE(1), SYS(16), LINENO(1) * Test reindex SET DEFAULT TO \VFP6Book\Data OPEN DATABASE PtOfSale USE Empl2 SET ORDER TO TAG Empl2 * Create a cursor and attempt to pack it SET DEFAULT TO \VFP6Book\Data OPEN DATABASE PtOfSale USE Empl2 SET ORDER TO TAG Empl2 SELECT * FROM Empl2 INTO CURSOR mpa PACK * Call for RESUME without a SUSPEND RESUME * Use a file that does not exist USE Mickey RETURN **************** PROCEDURE ErrLog LPARAMETERS lnErrorNo, lcMessage, lcErrorLine, ; lcModule, lnErrorLineNo ********************************************************* * * * PROCEDURE ERRLOG * * * * This routine demonstrates 3 ways to handle errors. * * * * Parameters: * * lnErrorNo - Error Number * * lcMessage - Error Message * * lcErrorLine - Line of code where error occurs * * lcModule - Name of procedure where error occurs * * lnErrorLineNo - Line number where error occurs * * * ********************************************************* LOCAL lcError, lnExitMethod lnExitMethod = 0 WAIT WINDOW 'Error: ' + STR(lnErrorNo) TIMEOUT 1 * Avoid recursive loop if errlog contains an error lcError = ON('ERROR') ON ERROR * Each case in this structure represents one error type * It handles trivial errors first, followed by recoverable * errors. Finally, all other errors generate an ASCII text * file with information about the system and error. DO CASE *** Check for trivial errors * Check if beyond end of file, place on last record CASE lnErrorNo = 4 GOTO BOTTOM * Check if before beginning of file, place on first record CASE lnErrorNo = 38 GOTO TOP * Cannot pack a cursor CASE lnErrorNo = 1115 * Check for Resume without Suspend CASE lnErrorNo = 1236 *** Check for recoverable errors * No table in use or table not found CASE lnErrorNo = 1 OR lnErrorNo = 52 LOCAL lcNewFile SELECT 0 lcNewFile = GETFILE('DBF', 'Select a DBF:', 'SELECT') IF EMPTY(lcNewFile) lnExitMethod = 2 ELSE USE (lcNewFile) SHARED ENDIF * Record is out of range CASE lnErrorNo = 5 OR lnErrorNo = 20 LOCAL lcDBF, lcTagName, lcTagNo, lcTagExp, lcFilter, ; lcIndex, lcSafety, llExclusiveOn, lcUnique * Gather information about current DBF and index lcDBF = DBF() && DBF name lcTagName = TAG() && Tag or IDX name lcTagNo = SYS(21) && Index number lcUnique = IIF(UNIQUE(), 'UNIQUE', '') && Is index UNIQUE? IF VAL(lcTagNo) = 0 WAIT WINDOW "No tag has been set. I don't know what to do" lnExitMethod = 2 ELSE lcTagExp = KEY() && Index expression lcFilter = SYS(2021, VAL(lcTagNo)) && Index FOR condition lcIndex = ORDER(1,1) && Full Index name IF LEFT(lcIndex, 3) = 'IDX' * Open table without index USE (lcDBF) * Turn safety off to allow reindex lcSafety = SET('SAFETY') SET SAFETY OFF IF EMPTY(lcFilter) INDEX ON &lcTagExp TO (lcIndex) &lcUnique ADDITIVE ELSE INDEX ON &lcTagExp FOR &lcFilter TO (lcIndex) ; &lcUnique ADDITIVE ENDIF SET SAFETY (lcSafety) * Reopen table with new index USE (lcDBF) INDEX (lcIndex) ELSE * Open table exclusively to remove and recreate tag llExclusiveOn = ISEXCLUSIVE() IF !llExclusiveOn USE (lcDBF) EXCLUSIVE ENDIF DELETE TAG (lcTagName) IF EMPTY(lcFilter) INDEX ON &lcTagExp &lcUnique TAG (lcTagName) ELSE INDEX ON &lcTagExp FOR &lcFilter &lcUnique ; TAG (lcTagName) ENDIF IF !llExclusiveOn USE (lcDBF) SHARED SET ORDER TO TAG (lcTagName) ENDIF ENDIF lnExitMethod = 0 ENDIF *** Unrecoverable Errors * Redirect output to a file OTHERWISE lnExitMethod = 2 LOCAL lcChkDBC, lcCurDBC, lcErrorFile, lcSuffix, ; lnAnswer, lnCnt, lnWhichTrigger * Get a file name based on date and time lcErrorFile = SUBSTR(DTOC(DATE()), 1, 2) + ; SUBSTR(DTOC(DATE()), 4, 2) + ; SUBSTR(TIME(), 1, 2) + ; SUBSTR(TIME(), 4, 2) + '.ERR' * Make sure the file name is unique by changing the extension lcSuffix = '0' DO WHILE FILE(lcErrorFile) lcErrorFile = STUFF(lcErrorFile, ; LEN(lcErrorFile) - LEN(lcSuffix) + 1, ; LEN(lcSuffix), lcSuffix) lcSuffix = ALLTRIM(STR(VAL(lcSuffix)+1, 3)) ENDDO SET CONSOLE OFF SET ALTERNATE TO (lcErrorFile) SET ALTERNATE ON * Identify error ? 'DATE: ' + TTOC(DATETIME()) ? 'VERSION: ' + VERSION() ? 'FILE NAME: ' + lcErrorFile ? * Next identify the error ? 'Error:' = AERROR(laErrorArray) ? ' Number: ' + STR(laErrorArray, 5) ? ' Message: ' + laErrorArray IF !ISNULL(laErrorArray) ? ' Parameter: ' + laErrorArray ENDIF IF !ISNULL(laErrorArray) ? ' Work Area: ' + laErrorArray ENDIF IF !ISNULL(laErrorArray) lnwhichtrigger = laErrorArray DO CASE CASE lnwhichtrigger = 1 ? ' Insert Trigger Failed' CASE lnwhichtrigger = 2 ? ' Update Trigger Failed' CASE lnwhichtrigger = 3 ? ' Delete Trigger Failed' ENDCASE ENDIF IF laErrorArray = lnErrorNo ? ' Module: ' + lcModule ? ' Line: ' + lcErrorLine ? ' Line #: ' + STR(lnErrorLineNo) ENDIF RELEASE laErrorArray, whichtrigger ? * Next identify the basic operating environment ? 'OP. SYSTEM: ' + OS() ? 'PROCESSOR: ' + SYS(17) ? 'GRAPHICS: ' + LEFT(SYS(2006), AT('/', SYS(2006)) - 1) ? 'MONITOR: ' + SUBSTR(SYS(2006), AT('/', SYS(2006)) + 1) ? 'RESOURCE FILE: ' + SYS(2005) ? 'LAUNCH DIR: ' + SYS(2004) ? 'CONFIG.FP: ' + SYS(2019) ? 'MEMORY: ' + ALLTRIM(STR(MEMORY())), 'KB OR ' + ; SYS(12) + 'BYTES' ? 'CONVENTIONAL: ' + SYS(12) ? 'TOTAL MEMORY: ' ? 'EMS LIMIT: ' + SYS(24) ? 'CTRLABLE MEM: ' + SYS(1016) ? 'CURRENT CONSOLE:' + SYS(100) ? 'CURRENT DEVICE: ' + SYS(101) ? 'CURRENT PRINTER:' + SYS(102) ? 'CURRENT DIR: ' + SYS(2003) ? 'LAST KEY: ' + STR(LASTKEY(),5) ? * Next identify the default disk drive and its properties ? ' DEFAULT DRIVE: ' + SYS(5) ? ' DRIVE SIZE: ' + TRANSFORM(VAL(SYS(2020)), '999,999,999') ? ' FREE SPACE: ' + TRANSFORM(DISKSPACE(), '999,999,999') ? ' DEFAULT DIR: ' + CURDIR() ? ' TEMP FILES DIR: ' + SYS(2023) ? * Available Printers ? 'PRINTERS:' IF APRINTERS(laPrt) > 0 FOR lncnt = 1 TO ALEN(laPrt, 1) ? PADR(laprt[lncnt,1], 50) + ' ON ' + ; PADR(laprt[lncnt,2], 25) ENDFOR ELSE ? 'No printers currently defined.' ENDIF ? * Define Workareas ? 'WORK AREAS:' IF AUSED(laWrkAreas) > 0 = ASORT(laWrkAreas,2) LIST MEMORY LIKE laWrkAreas RELEASE laWrkAreas ? 'Current Database: ' + ALIAS() ELSE ? 'No tables currently open in any work areas.' ENDIF ? * Begin bulk information dump * Display memory variables ? REPLICATE('-', 78) ? 'ACTIVE MEMORY VARIABLES' LIST MEMORY ? * Display status ? REPLICATE('-', 78) ? 'CURRENT STATUS AND SET VARIABLES' LIST STATUS ? * Display Information related to databases IF ADATABASE(laDbList) > 0 lcCurDBC = JUSTSTEM(DBC()) FOR lncnt = 1 TO ALEN(laDbList, 1) lcChkDBC = laDbList[lncnt, 1] SET DATABASE TO (lcChkDBC) LIST CONNECTIONS ? LIST DATABASE ? LIST PROCEDURES ? LIST TABLES ? LIST VIEWS ? ENDFOR SET DATABASE TO (lcCurDBC) ENDIF * Close error file and reactivate the screen SET ALTERNATE TO SET ALTERNATE OFF SET CONSOLE ON ON KEY LABEL BACKSPACE ON KEY LABEL ENTER ON KEY LABEL ESCAPE ON KEY LABEL PGDN ON KEY LABEL PGUP ON KEY LABEL SPACEBAR SET SYSMENU TO DEFAULT WAIT WINDOW 'Check file: ' + SYS(2003) + '\' + lcErrorFile + ; CHR(13) + ' for error information' lnAnswer = MESSAGEBOX('View Error Log Now?', 292) IF lnAnswer = 6 MODIFY FILE (lcErrorfile) ENDIF ENDCASE * Type of exit DO CASE CASE lnExitMethod = 0 && Retry the same line RETRY CASE lnExitMethod = 1 && Execute the next line of code RETURN CASE lnExitMethod = 2 && Cancel the program ON ERROR &lcError CANCEL && SUSPEND during development ENDCASE ON ERROR &lcError RETURN
The preceding example shows the typical way to handle errors using the ON ERROR statement. This method was available in all earlier versions of FoxPro. Although it still works, you have a few more options in Visual FoxPro. The most important one is that each object has its own error event. VFP first looks for an Error method in the current object when an error occurs. If you did not add code to this method, VFP then executes the global ON ERROR routine mentioned earlier. If you don't use a global ON ERROR routine, VFP uses its default error handler.
The default VFP error handler is about as useful to your users as sunscreen is to Eskimoes in the winter. It merely displays a message box containing the text of the error message and four buttons. The first says Cancel. Without other code, this could leave data transactions hanging uncommitted. If they are running inside VFP rather than in a standalone environment, they can Suspend the program (and do what?). The third button says Ignore. Very seldom do ignored errors just go away. The last says Help. How many users want to press Help just to get a more detailed error message, which does not generally tell them what to do next? The point is that you want to avoid letting the user ever see this default error handler.
The first thing you find is that an object's Error method receives three parameters from VFP:
- The error number
- The name of the method where the error occurred
- The line number in the method where the error occurred
At this point, you could design a small routine to examine the error that occurred and display the information passed as parameters, possibly also writing these to an error log. Note, however, that you must keep this code as simple as possible because if another error occurs while you're in your error-handling code, VFP throws you into its default error handler, even if you have an ON ERROR statement defined. I previously said that you never want to let the user see the default error handler with its limited and unfriendly button choices.
Another useful feature of Visual FoxPro is the AERROR()
function. This returns an array with up to seven columns. The
return value of the function identifies the number of rows in
the array. This value is almost always 1, except possibly in the
case of ODBC. Table 23.4 defines the columns returned by AERROR()
when the error occurs in Visual FoxPro code.
|A numeric value of the error number. Same as ERROR().|
|A string value of the error message. Same as MESSAGE().|
|Typically null unless the error has an additional error parameter such as those returned by SYS(2018).|
|Typically null, but sometimes contains the work area where the error occurred.|
|Typically null, but if the error is the result of a failed trigger (error 1539), it returns one of the following values:
1 - Insert trigger failed; 2 - Update trigger failed; 3 - Delete trigger failed
Although it is true that most of this information can be obtained
without resorting to the AERROR() function, the resulting
array is easier to work with. Furthermore, this function returns
important information not otherwise available if the error is
the result of an OLE or ODBC error. Table 23.5 documents the
returned values for OLE errors.
|Character value with text of Visual FoxPro error message|
|Character value with text of OLE error message|
|Character value with name of OLE application|
|Null value typically-if a character value, holds the name of the application's help file|
|Null value typically-if a character value, holds the help context ID for an appropriate help topic in the application's help file|
|Numeric value with the OLE 2.0 exception number|
Table 23.6 shows the column definitions for an ODBC error.
|Character value with the Visual FoxPro error message|
|Character value with the ODBC error message|
|Character value with the ODBC SQL state|
|Numeric value with the ODBC data source error number|
|Numeric value with the ODBC connection handle|
Again, the point is not to fix the error, but to document it. Therefore, you will want to write formatted entries for each row in the array created by AERROR() to the error log.
When dealing with objects, you might not want to write error code in every method, of every instance, of the object you create and use. Rather, it is better to create your own class library from the base classes provided with VFP. In these classes, define your error-handler code (as well as any other unique changes you need in all instances of the object). Then build your forms and code from these base classes.
As you instantiate objects from your class library, each instance inherits the error method code from the parent class. Although it is okay to handle errors specific to an object in that instance, or in the subclass it is derived from, remember to include a line at the end of the method to reference the parent class code for those errors not specifically handled locally. To do this, simply add the following line to the end of the instance error method code:
DODEFAULT(nError, cMethod, nLine)
You can even add the following line to the classes in your class library to have them reference the error code from the containers in which you place them:
This.Parent.Error(nError, cMessage, nLineNo)
Ultimately, if the error gets passed up through the class structure and into the containers and still cannot be handled, the program should resort to a special error handler to document the error condition as much as possible before bailing out of the program.
Author's note: Thanks to Malcolm C. Rubel for outlining the preceding approach.
The amount of time you spend testing code before putting it into use depends on several factors:
- How quickly the users need it.
- How critical code failure is (such as software written to control aircraft or medical equipment).
- How management views the life cycle of projects. Do they expect it right the first time, or do they accept a break-in period?
Debugging can and should go on throughout the life of the application, although you might call it something different, such as "maintenance," once the software is in production.
There is no one correct answer for ensuring software quality. Similarly, there is no one correct way to write an error handler, especially when it comes to the complexities of handling objects, OLE, and ODBC. But the more methods employed, the better the overall system performs, and when errors do occur, the easier it is to find them.
Many of the notes and cautions throughout this book relate to things you need to watch closely to avoid errors in your code. This chapter has focused on additional concepts related to debugging and error handling. Reference texts in this area are relatively few. As a serious developer, you might want to check the few following listed references to learn more about debugging and software quality assurance:
- Glass, Robert L. Building Quality Software. Englewood Cliffs, N.J.: Prentice Hall, 1992.
- McConnell, Steve. Code Complete. Redmond, Washington: Microsoft Press, 1993.
- McConnell, Steve. Rapid Development: Taming Wild Software Schedules. Redmond, Washington: Microsoft Press, 1996.
- Maguire, Steve. Writing Solid Code. Redmond, Washington: Microsoft Press, 1993.
- Maguire, Steve. Debugging the Development Process. Redmond, Washington: Microsoft Press, 1994.
- McCarthy, Jim. Dynamics of Software Development. Redmond, Washington: Microsoft Press, 1995.
- Schulmeyer, G. Gordon, ed. Handbook of Software Quality Assurance (3rd Edition). New York: Van Nostrand Reinhold, 1998.
The following sources of information about Visual FoxPro occasionally feature information about debugging and error handling:
- FoxPro Advisor magazine (Advisor Publications)
- FoxTalk newsletter (Pinnacle Publishing)
- VFOX and FoxUser forums on CompuServe
Finally, check out upcoming Visual FoxPro conferences in your region and look for any sessions they might have on error handling.
© Copyright, Sams Publishing. All rights reserved.