Document revision date: 30 March 2001

## Guide to Creating OpenVMS Modular Procedures

Previous | Contents | Index

#### 3.3.4.4 Disabling AST Interrupts

A procedure is also considered AST reentrant if AST interrupts are disabled while critical sections of code execute. However, Compaq does not recommend this method of maintaining AST reentrancy.

Sometimes the only way to avoid race conditions is to disable AST interrupts during the access to static storage, and restore the state of the AST enable once the critical section of code has finished executing. However, this technique could adversely affect performance of real-time programs using AST interrupts. The $SETAST system service, which is used to enable and disable AST interrupts, is time consuming. Therefore, you should avoid disabling AST interrupts whenever you can by using the techniques described in Section 3.3.4.1 to Section 3.3.4.3.

Try to minimize the number of instructions during which the AST interrupts are disabled. Before disabling AST interrupts, establish a condition handler to restore the AST level in case an exception or stack unwind occurs.

Example 3-7 shows how you can use $SETAST to disable ASTs and then restore the previous state of the enable.

Example 3-7 FORTRAN Program Disabling and Restoring ASTs

    !+
    ! This program demonstrates using the System
    ! Service SYS$SETAST to disable and then
    ! reenable AST interrupts.
    !-
            INCLUDE '($SSDEF)'
            INTEGER*4 SYS$SETAST
    !+
    ! Turn off ASTs and remember the previous setting.
    !-
            ISTAT = SYS$SETAST (%VAL(0))
    !+
    ! The statements in the program during whose
    ! execution you want ASTs disabled.
    !
    ! If ASTs were previously enabled,
    ! reenable them.
    !-
            IF (ISTAT .EQ. SS$_WASSET) CALL SYS$SETAST( %VAL(1))
            END

### 3.3.5 Performing Input/Output at AST Level

If your procedure performs I/O using OpenVMS RMS (RMS), you must use the following coding techniques for your procedure to be AST reentrant:

- When opening process-permanent files --- such as SYS$INPUT, SYS$OUTPUT, SYS$COMMAND, or SYS$ERROR --- check for the RMS error status RMS$_ACT (active) after each $CREATE or $OPEN service. This error indicates that a record operation has already started for the process-permanent file. The error does not occur for files that are not process permanent, and the $OPEN service follows the constraints of shared access to the file that may have been imposed by a previous $OPEN service. If the error occurs, perform a $WAIT using the same file access block (FAB). When control returns to your procedure, try the $CREATE or $OPEN service again. Repeat this sequence until it succeeds.
- When performing record I/O to any type of file, check for the RMS error status RMS$_RSA (record stream active) or RMS$_BUSY (structure in use) after each $GET and $PUT service. This error indicates that a record operation has already been started for the file. If the error occurs, perform a $WAIT using the same record access block (RAB). When control returns to your procedure, try the $GET or $PUT service again. Repeat this procedure until it succeeds.
- Avoid storing data in an RAB that RMS could still be accessing. You can avoid this situation by doing either of the following:
  - Allocate the RAB on the stack so the AST and non-AST level have separate RABs.
  - Allocate RAB in heap or static storage along with a busy bit. The busy bit is tested and set using a BBSS instruction before the RAB is accessed. If the RAB is already busy, your procedure executes a $WAIT using that RAB.
  For synchronous I/O (always completed before returning control to your procedure), you can allocate the RAB in either of these ways. However, the first method is more reliable, because it does not use static storage and therefore does not become corrupted if an exception is signaled.
  For asynchronous I/O (when control is returned to your procedure before I/O is completed), you must use the second technique.

### 3.3.6 Condition Handling at AST Level

You should not allow an exception to propagate out of an AST handler because the exception might be caught by any procedure that is active at the time of the AST. Condition handlers for other active procedures might react as if the exception was caused by a procedure that they had called.

Another reason for not allowing exceptions to propagate out of an AST handler is that, for run-time environments that use multiple threads in a process such as Ada, it cannot be determined which stack of the threads of execution is used to deliver the AST. (The AST is delivered on the stack of whichever thread is active at the time of the AST interrupt.)

It is best to catch all exceptions in the AST handler and not allow them to propagate.

## Chapter 4
Testing Modular Procedures

A successful test system is one that uncovers errors. To ensure successful testing, plan how to test your procedures while you are designing them, and begin testing while you are coding. You should test for the following:

- Ensure that the procedure you developed fulfills your requirements or specifications.
  Carefully test the functionality to ensure that the procedure does everything that it is supposed to do. The methods you use to test this aspect of your procedure depend upon the functions your procedure performs.
- Ensure that the procedure is modular and executes without error.

This chapter focuses on testing procedures for modularity. Modularity is especially important to procedures that will be included in a library facility. A procedure that is not modular can adversely affect the results and performance of other procedures that call it.

To ensure modularity within procedures, perform at least the following tests:

- Unit testing
- Language-independence testing
- Integration testing

This chapter discusses methods for designing and administering these types of tests. It also describes reentrancy, performance analysis, and RTL procedures for time and resource monitoring.

## 4.1 Unit Testing

Before you begin combining units of code (such as subprograms, subroutines, and internal procedures) to form your new procedure, it is essential to ensure that each of these units works separately. Thorough unit testing is important for the following reasons:

- Testing small units separately decreases the level of complexity within the tests.
- It is easier and faster to debug a small unit of code than it is to find an error within several units and their interfaces.
- It makes the integration stage that follows much easier if each of the separate units has been thoroughly tested and the problems corrected.
- The earlier an error is found in development, the less expensive it is to fix.

Unit testing includes the following steps:

1. Review the goals of your procedure.
2. Choose test cases.
3. Run the tests.

You determine the goals of your procedure at the requirements or specifications stage. As mentioned earlier, this topic is not discussed in this manual because it does not have a significant effect on modularity. However, it does have a significant effect upon whether your final product can be considered successful. If your product does not perform the functions or meet the requirements decided upon at the requirements or specifications stage, it is not a successful project. You should have at least one test for each of the requirements that your procedure was designed to fulfill.

You can use the following two types of tests:

- Black box tests
- White box tests

Black box tests assume that you know nothing about the internal workings of the procedure that you are testing. All that you are interested in is the output that you receive for given sets of input.

White box tests (also called clear box tests) are more complicated because they are designed to step through particular sections of code or algorithms internal to the procedure. They assume that you know, in great detail, the internal workings of the procedure being tested.

### 4.1.1 Black Box Testing

When you perform black box testing, you are interested only in the output you receive for particular input values. Execute the procedure repetitively using input from different classes. The best way to do this is to write a command procedure or test driver program to execute the procedure a given number of times using test data that you supply. (For information about writing command procedures, see the OpenVMS User's Manual.)

You should execute your procedure with test cases from each of the following categories:

- Expected inputs
  These include the values that you expect your procedure to receive most of the time.
- Boundary values
  If your procedure expects an input value from 1 to 999, use 1 and 999 as test cases to make sure that your procedure returns the expected results for the boundary cases.
- Illegal values
  Using the boundary values example, what happens if your procedure receives as input a value that is less than 1 or greater than 999? Does the user receive a useful error message? Does the procedure simply stop, or does it attempt to use values outside its limitations and simply return an incorrect answer? It is essential that you run the procedure using illegal input values to determine the answers to these questions.

Figure 4-1 summarizes the methods of black box testing.

Figure 4-1 Black Box Testing Methods

### 4.1.2 White Box Testing

When performing white box testing, unlike black box testing, you must understand the internal workings of the procedure. Keep in mind that you are testing internal workings---the specific lines of code.

To perform white box testing, do the following:

1. Test each statement.
   You provide sets of test values to ensure that every statement in the procedure is executed at least once. This includes all statements --- even those executed only when optional arguments, user-supplied arguments, subroutines, user-action routines, or specific error codes are present.
2. Test each decision.
   You provide test cases to ensure that each branch of a decision is executed at least once. In the case of a standard Boolean decision, this typically requires providing two values; however, this number may be much greater in the case of compound or nested decisions.
3. Test each condition.
   Condition testing requires writing test cases that ensure each condition in a decision takes all possible outcomes at least once, and each point of entry to the program or subroutine is invoked at least once. You must supply multiple test values in cases of compound and nested loops. In testing the entry points, remember to invoke any optional routines (either internal or external), as well as error handlers. If your procedure contains a JSB entry point, that entry point should also be tested.

Figure 4-2 summarizes white box testing.

Figure 4-2 White Box Tests

Note that each white box test finds a specific type of error. For example, statement testing does not find an error on a negative value for a condition if the statement is given a positive input the only time it is executed. Therefore, you must perform all three white box tests.

## 4.2 Language-Independence Testing

For your procedures to be as useful as possible, they must be able to be called by programs in any language. Providing for language independence is essential to producing a useful procedure.

Testing for language independence is a specific type of unit testing. It ensures that your program executes correctly, regardless of the language from which it is called.

To test your procedures for language independence, write several driver programs in languages you have chosen randomly. The driver program need only contain a call to the procedure being tested.

If you do find that your procedures are not language independent, make sure that they conform to the following rules:

- All atomic data must be passed by reference, and all strings must be passed by descriptor.
  Adherence to this single guideline is the most important factor in achieving language independence.
- Statements that assume a specific language environment are not allowed.
  For example, the statement ON ERROR GO BACK in a BASIC procedure assumes that the calling program is also written in BASIC.

## 4.3 Integration Testing

Integration testing is the next logical step following unit testing. Unit testing is designed to test each separate component. Depending on your procedure, that component might be a module, a subprogram, a subroutine, an internal procedure (fac_ <double_uscore> name), or an intrinsic piece of code. Once you have determined that each unit works separately, you need to determine that the units also work together to form the complete procedure.

Integration testing can be completed by either of the two methods described in Section 4.3.1 and Section 4.3.2.

### 4.3.1 All-at-Once Approach to Integration Testing

One method of integration testing is the all-at-once approach. In this method, you finish all the units, link them, and test the completed structure all at once. Compaq strongly discourages use of this method, because it makes it difficult to find the location of errors. For example, look at the organization of the units in the sample procedure shown in Figure 4-3. Assume that this procedure used the all-at-once approach and found an error; the procedure did not work. There is no way of knowing whether the error was in unit A, unit B, unit C, or unit D.

Figure 4-3 Sample Procedure for Integration Testing

### 4.3.2 Incremental Approach to Integration Testing

The recommended approach to integration testing is called incremental testing. Incremental testing involves testing the procedure by starting with one unit and building on it one unit at a time. Each unit should always be subjected to thorough unit testing before it is included in the integration tests.

Incremental integration testing is especially useful for finding the following types of error:

- Problems with the calling interface between units (for example, inconsistent ordering of arguments between the calling and called unit)
- Incorrect assumptions about what values are returned and the units to which they are returned
- Unexpected transfer of control between units

Using the sample procedure in Figure 4-3, complete the test of unit A on level 1 before proceeding to level 2, where you test units A and B in combination. At each level, you correct any errors before proceeding to the next level. When you have completed the last step, you know that the entire procedure works correctly.

Because you started at the top of the sample procedure and added units incrementally from lower levels, you were using the top-down approach to integration testing. You could just as easily have started at Level 3 and used the bottom-up approach.

As you can see from the example, there are several distinct advantages to incremental integration testing:

- It is not necessary to wait until the procedure is complete to begin integration testing.
- Debugging is simplified by incremental testing because the modules and interfaces can be tested as the system grows.
- Programming errors in the interfaces and incorrect assumptions between units are discovered at an early stage.
- Because previously tested units are retested when new units are added, the probability of discovering less obvious errors is increased substantially.

## 4.4 Testing for Reentrancy

It is important to test your procedures for reentrancy before placing them into a library facility. Because ASTs can occur at any time, procedures that are not AST reentrant may exhibit unexpected behavior. In particular, an AST occurring during storage modification in a procedure that is not AST reentrant can corrupt the contents of the procedure's storage. (For further information about AST reentrancy, see Section 3.3.)

Full reentrancy is important to multithread tasking environments, such as the environment used by Ada.

To avoid problems with reentrancy, carefully read and follow the coding guidelines described in Section 3.3.

### 4.4.1 Checking for AST Reentrancy

There are two methods of checking a procedure for AST reentrancy. You can use the OpenVMS Debugger or perform a manual desk check.

#### 4.4.1.1 Using the Debugger to Check for AST Reentrancy

When using the debugger to check for AST reentrancy, do the following:

1. Create an activation of the procedure.
2. Set watchpoints on all storage used by the procedure.
3. Create a second activation of the procedure using the CALL command. Allow this second activation to run to completion. (The second activation represents the AST-level thread.)
   Check to be sure that the AST-level thread of execution does not modify the storage accessed by the non-AST level thread of execution. If the AST-level thread of execution does modify any of that storage, check to ensure that it does not cause any unwanted side effects for the non-AST level thread of execution.
4. Execute one instruction in the first activation using the debugger STEP command.
5. Repeat Steps 3 and 4 until the end of the procedure for the first activation.

For more information about the debugger, refer to the OpenVMS Debugger Manual.

#### 4.4.1.2 Using Desk Checking to Check for AST Reentrancy

Desk checking is the term for tracing through a procedure's execution manually. Performing a desk check for AST reentrancy consists of the following four steps:

1. Create an activation of the procedure being tested and its data using the method you normally use for manually tracing through a procedure.
   This activation represents the non-AST level of your procedure's execution.
2. Create a second activation of the procedure using the process you used in step 1. This second activation represents the AST-level thread of your procedure's activation.
   Trace through the AST-level thread's execution to completion, one statement at a time.
   Remember to update the contents of all storage locations and variables for each instruction of the procedure.
   Check to be sure that the AST-level thread of execution does not modify the storage accessed by the non-AST level thread of execution. If the AST-level thread of execution does modify any of that storage, check to ensure that it does not cause any unwanted side effects for the non-AST level thread of execution.
3. Step through a single statement of the non-AST-level thread of execution, remembering to update the contents of all storage locations.
4. Repeat steps 2 and 3 until you have stepped through every statement in the non-AST level thread of execution. (Note that every statement of the AST-level thread is stepped through in each pass through step 2.)

What you are actually doing in the process is testing between the execution of every two statements in the procedure. The most rigorous method of applying this type of desk checking for AST reentrancy is to step through the procedure at the assembly language level and test between each assembly language instruction.

### 4.4.2 Checking for Full Reentrancy

Full reentrancy differs from AST reentrancy in the number of threads of execution. An AST-reentrant environment can support only two threads of execution: the AST-level thread and the non-AST-level thread. Full reentrancy is important in environments that can support many threads of execution, such as Ada.

A procedure is fully reentrant if any number of threads of execution can execute to completion without affecting any of the other threads of execution.

Typically, a procedure that is AST reentrant is also fully reentrant. For further information on full reentrancy and environments supporting multiple threads of execution, refer to the documentation for Compaq Ada.

## 4.5 Performance Analysis

All timer and resource allocation procedures should make statistics available for performance evaluation and debugging. You should code timer and resource allocation procedures with the following two entry points:

`LIB_SHOW_name LIB_STAT_name`

### 4.5.1 SHOW Entry Point

A SHOW entry point provides formatted strings containing the information you need. The calling sequence for a SHOW entry point is as follows:

`LIB_SHOW_name [code [,action-routine [,user-arg]]]`

> #### code

> An optional code (in the form LIB_K_code) designating the statistic you need. Define a separate code for each statistic available; the codes should be the same for the SHOW and STAT entry points. The values associated with the codes start at one for each procedure. The functional specification in the procedure's documentation should list the codes used. If the code is omitted, or zero, the procedure provides all statistics.

> #### action-routine

> The address of an action routine. This is an optional argument. If omitted, statistics are written to SYS$OUTPUT.

> #### user-arg

> An optional user argument to be passed to the action routine. If omitted, a shortened list is passed to the action routine. The user-arg argument, if present, is copied to the argument list passed to the action routine. That is, the argument list entry passed by the calling program is copied to the argument list entry passed to the action routine. The access type, data type, argument form, and passing mechanism can be arbitrary, as agreed between the calling program and the action routine.

The optional-action routine should have the following form:

`ACTION-ROUTINE (string [,user-arg])`

See Section 3.1.4 for an example of the code to invoke a user-action routine.

### 4.5.2 STAT Entry Point

A STAT procedure returns the information you want as binary results. The calling sequence is as follows:

`LIB_STAT_name (code ,value)`

> #### code

> A code designating the statistic you want. A separate code is defined for each statistic available; the codes are the same for the SHOW and STAT entry points. Codes start at one.

> #### value

> The value of the returned statistic.

Previous | Next | Contents | Index

| privacy and legal statement |
4518PRO_005.HTML