Use database files with CL programs.
In our last column, "Determining What Program Is Being Tested," we left the SNDESCAPE program with the limitation of having to be modified and recompiled when changes such as the command to test, the escape message to send, or the person conducting the test changed. Today, we will look at how to easily change any of these characteristics and do so in a way that does not require any change to the SNDESCAPE program.
What we need is to store information--such as the command name, the message ID, the tester, and the program under test--externally from the SNDESCAPE program. While there are several ways to store this information (user indexes, keyed data queues, etc.), the i database will most likely be the easiest and most flexible. This is the source for the proposed physical file Active Command Tests (ACTCMDTSTS):
A..........T.Name++++++RLen++TDpB......Functions++++++++++++++++++
UNIQUE
R CMDTSTRCD
TESTCMD 10 TEXT('Command to test')
TESTUSR 10 TEXT('Tester')
TESTPGM 10 TEXT('Program to test')
TESTMSG 7 TEXT('Message to send')
TESTMSGF 10 TEXT('Message file')
TESTMSGFL 10 TEXT('Msg file library')
TESTRPLDTA 9B 0 TEXT('Msg replacement data')
K TESTCMD
K TESTUSR
K TESTPGM
Field TESTCMD will be the command being tested (for instance, the DONOTHING command of the previous articles), TESTUSR the user testing the command (for instance, VININGTEST), TESTPGM the program under test (SNDESCAPE currently sends the escape message CPF414E to any program that is called by VININGTEST and that is running the DONOTHING command, which may be inappropriate if multiple programs are in the job stream being tested), TESTMSG the escape message to be sent (for instance, CPF414E), TESTMSGF the message file containing the TESTMSG to be sent (QCPFMSG in the case of CPF414E), and TESTMSGFL the library where TESTMSGF is located (most likely *LIBL). Utilizing the UNIQUE DDS keyword, this file will only allow one record for any given command, tester, and program combination. This record will identify the message to be sent for testing purposes.
For a complete testing application, you would most likely want to add additional fields, such as planned date for testing, completion date for testing, introduce a second file to record completed test scenarios, etc., but the above ACTCMDTSTS file should be sufficient to give you the general idea. In a subsequent article, we will utilize the TESTRPLDTA field to provide a very flexible approach to testing with various message replacement data values, but for now we'll stick to the essentials. To create ACTCMDTSTS into the QGPL library, you can use the following command:
CRTPF FILE(QGPL/ACTCMDTESTS)
Before reviewing--and running--the modified SNDESCAPE program, the command Add Test Case (ADDTSTCASE) needs to be created so that you can add a test case. This is the command source:
CMD PROMPT('Add Test Case')
PARM KWD(PGM) TYPE(*NAME) LEN(10) MIN(1) +
PROMPT('Program to test')
PARM KWD(CMD) TYPE(*NAME) LEN(10) MIN(1) +
PROMPT('Command to test')
PARM KWD(MSGID) TYPE(*NAME) LEN(7) MIN(1) +
PROMPT('Message for testing')
PARM KWD(MSGF) TYPE(QUALNAME) PROMPT('Message file')
PARM KWD(USER) TYPE(*NAME) LEN(10) DFT(*CURUSR) +
SPCVAL((*CURUSR)) PROMPT('Profile running +
test')
QUALNAME: QUAL TYPE(*NAME) DFT(QCPFMSG)
QUAL TYPE(*NAME) DFT(*LIBL) SPCVAL((*LIBL *LIBL)) +
PROMPT('Library')
The ADDTSTCASE command requires three parameters: the program to be tested, the command to test within the program, and the escape message to test with. There are also two optional parameters: the qualified message file name for the escape message (keyword MSGF, which defaults to *LIBL/QCPFMSG) and the tester (keyword USER, which defaults to the user profile running the ADDTSTCASE command).
Assuming the name of the command processing program (CPP) for the ADDTSTCASE command is ADDTSTCPP, you can create this command into library VINING using the CRTCMD command:
CRTCMD CMD(VINING/ADDTSTCASE) PGM(VINING/ADDTSTCPP)
The CPP for ADDTSTCASE is shown below.
Pgm Parm(&TestPgm &TestCmd &TestMsg &MsgFile &TestUsr)
Dcl Var(&MsgFile) Type(*Char) Len(20)
Dcl Var(&MsgF) Type(*Char) Len(10) Stg(*Defined) +
DefVar(&MsgFile 1)
Dcl Var(&MsgFL) Type(*Char) Len(10) Stg(*Defined) +
DefVar(&MsgFile 11)
(A) DclFCLF FileID(ActCmdTsts)
(B) Dcl Var(&RNF) Type(*Lgl)
(C) OpnFCLF FileID(ActCmdTsts) AccMth(*Key) Usage(*Both)
If Cond(&TestUsr = *CURUSR) Then( +
RtvJobA CurUser(&TestUsr))
(D) ReadRcdCLF ActCmdTsts Type(*Key) KeyRel(*EQ) +
KeyList(&TestCmd &TestUsr &TestPgm) +
RcdNotFnd(&RNF)
(E) If Cond(&RNF) Then( Do)
ChgVar Var(&TestMsgF) Value(&MsgF)
ChgVar Var(&TestMsgFL) Value(&MsgFL)
(F) WrtRcdCLF ActCmdTsts
SndPgmMsg Msg('Test message' *BCat +
&TestMsg *BCat 'added for' *BCat +
&TestPgm *BCat &TestCmd *BCat &TestUsr)
EndDo
(G) Else Cmd(SndPgmMsg Msg('Test message' *BCat +
&TestMsg *BCat 'defined for' *BCat +
&TestPgm *BCat &TestCmd *BCat +
&TestUsr *TCat '. Use CHGTSTCASE'))
(H) CloFCLF ActCmdTsts
Return
EndPgm
This CPP could have been written using a language such as RPG or C, but as this is "The CL Corner," using CL seems more appropriate. Since CL does not have direct support for adding records to database files (and who wants to write an RPG or a COBOL program to perform the database operations?), the CPP is using the product Control Language for Files (CLF), which is introduced here. The example CPP is using a CLF precompiler with traditional CL syntax commands. Later in this article, I'll show you an example using a CLF precompiler with RPG-like CL commands and an example using the no-charge base run-time support of CLF.
At (A), the program uses the Declare File using CLF (DCLFCLF) command. There are several defaults being taken with the DCLFCLF command, but essentially this command is declaring the ACTCMDTSTS file in a manner similar to how the DCLF command might be used. The DCLFCLF command is documented here. At (B), the program also declares the logical variable &RNF. This variable is used later in the program to indicate a Record Not Found condition when accessing the ACTCMDTSTS file.
At (C), the program opens the ACTCMDTSTS file using the Open File using CLF (OPNFCLF) command documented here. The file is being opened for keyed access and for both reading and writing of records.
At (D), the program attempts to randomly read a record from the ACTCMDTSTS file where the key value for the record is equal to the name of the command to be tested, the name of the tester, and the name of the program to be tested. If no record with that key is found, logical variable &RNF is to be set to true; otherwise, variable &RNF is set to false. The command being used, Read Record using CLF (READRCDCLF), is documented here.
If no record is found (condition &RNF is true at (E)), the program then writes a new record at (F) to the ACTCMDTSTS file, using the Write Record using CLF (WRTRCDCLF) command documented here. Prior to writing the record, the program updates the test-record message-file-related fields to reflect the values passed by the ADDTSTCASE command in the &MSGFILE parameter. The other fields of the ACTCMDTSTS record being added do not need to be updated as they have the same names as the parameters passed to the ADDTSTCPP program. After writing the new record to ACTCMDTSTS, the program sends a message to the user indicating that the test case was successfully added.
If a record is found (condition &RNF is false), the ELSE at (G) runs and a message is sent to the user indicating that a planned test case currently exists for the specified command, program, and tester. In this case, the user is directed to use the command Change Test Case (CHGTSTCASE). This command does not currently exist but will be provided in a future article.
In either case, the program then closes the file at (H) using the Close File using CLF (CLOFCLF) command and returns. The CLOFCLF command is documented here.
ADDTSTCPP could have been written without the attempt to read a record from ACTCMDTSTS at (D). The program could have simply written the new record using the WRTRCDCLF command and then checked for various error situations. One possible error that could be returned would be a duplicate key situation due to the ACTCMDTSTS file being defined with unique keys. If this error was encountered, then ADDTSTCPP could have sent the message pointing the user to the CHGCMDTEST command. Likewise, the logical variable &RNF did not need to be used when running the READRCDCLF command. The program could have been written using the Monitor Message (MONMSG) command to detect a record-not-found condition.
To create ADDTSTCPP as an ILE program in library VINING, you can use the Create Bound Program using CLF (CRTBNDCLF) command CRTBNDCLF PGM(VINING/ADDTSTCPP), which is documented here. To create the CPP as an OPM program in VINING, you can use the Create CLF Program (CRTCLFPGM) command CRTCLFPGM PGM(VINING/ADDTSTCPP), which is documented here.
Having created the command and CPP, you can now use the following commands to add the two test cases used in the previous article:
ADDTSTCASE PGM(MONESCAPE) CMD(DONOTHING) MSGID(CPF414E) USER(VININGTEST)
ADDTSTCASE PGM(MONESCAPE) CMD(XXX) MSGID(CPF415B) USER(VININGTEST)
If you are familiar with free-form RPG, here is a version of the ADDTSTCPP program using a CLF precompiler and RPG-like CLF commands. The command names are changed from the previous example, but the notes (A) through (H) apply equally.
Pgm Parm(&TestPgm &TestCmd &TestMsg &MsgFile &TestUsr)
Dcl Var(&MsgFile) Type(*Char) Len(20)
Dcl Var(&MsgF) Type(*Char) Len(10) Stg(*Defined) +
DefVar(&MsgFile 1)
Dcl Var(&MsgFL) Type(*Char) Len(10) Stg(*Defined) +
DefVar(&MsgFile 11)
(A) File ActCmdTsts
(B) Dcl Var(&RNF) Type(*Lgl)
(C) Open ActCmdTsts AccMth(*Key) Usage(*Both)
If Cond(&TestUsr = *CURUSR) Then( +
RtvJobA CurUser(&TestUsr))
(D) Chain (&TestCmd &TestUsr &TestPgm) ActCmdTsts +
RcdNotFnd(&RNF)
(E) If Cond(&RNF) Then( Do)
ChgVar Var(&TestMsgF) Value(&MsgF)
ChgVar Var(&TestMsgFL) Value(&MsgFL)
(F) Write ActCmdTsts
SndPgmMsg Msg('Test message' *BCat +
&TestMsg *BCat 'added for' *BCat +
&TestPgm *BCat &TestCmd *BCat &TestUsr)
EndDo
(G) Else Cmd(SndPgmMsg Msg('Test message' *BCat +
&TestMsg *BCat 'defined for' *BCat +
&TestPgm *BCat &TestCmd *BCat +
&TestUsr *TCat '. Use CHGTSTCASE'))
(H) Close ActCmdTsts
Return
EndPgm
The documentation for the RPG-like commands FILE, OPEN, CHAIN, WRITE, and CLOSE can be accessed by clicking on the command name. This program would also be created into library VINING using either the precompiler command CRTBNDCLF PGM(VINING/ADDTSTCPP) for ILE or CRTCLFPGM PGM(VINING/ADDTSTCPP) for OPM CL.
For those of you with tight budgets, the following program demonstrates one way to write ADDTSTCPP using only the no-charge base run-time support of CLF. That is, this example does not utilize a CLF precompiler. Using the base run-time environment for program development does require more programming than a precompiler-based solution, but you have the choice of trading off programming resource with budgetary resource.
Pgm Parm(&TestPgm &TestCmd &TestMsg &MsgFile &TestUsr)
(A) Dcl Var(&TestPgm) Type(*Char) Len(10)
Dcl Var(&TestCmd) Type(*Char) Len(10)
Dcl Var(&TestMsg) Type(*Char) Len(7)
Dcl Var(&TestUsr) Type(*Char) Len(10)
Dcl Var(&MsgFile) Type(*Char) Len(20)
Dcl Var(&MsgF) Type(*Char) Len(10) Stg(*Defined) +
DefVar(&MsgFile 1)
Dcl Var(&MsgFL) Type(*Char) Len(10) Stg(*Defined) +
DefVar(&MsgFile 11)
(B) /* DclFCLF FileID(ActCmdTsts) */
(C) Dcl Var(&CmdTstRcd) Type(*Char) Len(61)
Dcl Var(&RNF) Type(*Lgl)
(D) OpnFCLF FileID(ActCmdTsts) AccMth(*Key) Usage(*Both) +
LvlChk(*No)
If Cond(&TestUsr = *CURUSR) Then( +
RtvJobA CurUser(&TestUsr))
(E) ReadRcdCLF ActCmdTsts Type(*Key) KeyRel(*EQ) +
KeyList(&TestCmd &TestUsr &TestPgm) +
RcdNotFnd(&RNF) RcdBuf(&CmdTstRcd)
If Cond(&RNF) Then( Do)
(F) ChgVar Var(&CmdTstRcd) Value( +
&TestCmd *Cat +
&TestUsr *Cat +
&TestPgm *Cat +
&TestMsg *Cat +
&MsgF *Cat +
&MsgFL)
ChgVar Var(%bin(&CmdTstRcd 58 4)) +
Value(0)
(G) WrtRcdCLF ActCmdTsts RcdBuf(&CmdTstRcd)
SndPgmMsg Msg('Test message' *BCat +
&TestMsg *BCat 'added for' *BCat +
&TestPgm *BCat &TestCmd *BCat &TestUsr)
EndDo
Else Cmd(SndPgmMsg Msg('Test message' *BCat +
&TestMsg *BCat 'defined for' *BCat +
&TestPgm *BCat &TestCmd *BCat +
&TestUsr *TCat '. Use CHGTSTCASE'))
CloFCLF ActCmdTsts
Return
EndPgm
The changes from the original precompiler-based version of ADDTSTCPP are highlighted above. At (A), the program explicitly declares the parameters being passed to it that are named the same as the fields of the ACTCMDTSTS record. These declare statements are not necessary in the original ADDTSTCPP program as the DCLFCLF command implicitly defines these fields. As the precompiler DCLFCLF command can no longer be used (shown at (B) with the DCLFCLF command being commented out) you need to provide these definitions. At (C), a 61-byte character field is declared. This character field represents a record from the ACTCMDTSTS file and, for simplicity, is named the same as the record format: &CMDTSTRCD. The length of 61 bytes for this field can be determined by using the Display File Field Description (DSPFFD) command DSPFFD FILE(ACTCMDTSTS) and finding the Record length entry.
At (D), ADDTSTCPP opens the ACTCMDTSTS file using the OPNFCLF command. This command is changed to add the LVLCHK(*NO) specification. CLF defaults to a level-check *Yes environment when opening a file, and, when not using the precompiler, the necessary level-check information is not available. At (E), the program uses the READRCDCLF command in the same manner as the previous version of the program. Here, however, it is necessary to add the Record Buffer (RCDBUF) keyword to identify what CL variable is to receive the record values being read. The program is using &CMDTSTRCD, the record buffer declared at (C). At (F), prior to writing the new record, ADDTSTCPP formats the record buffer (&CMDTSTRCD) with the parameter values passed to the program. ADDTSTCPP must also initialize the 4-byte binary field &TESTRPLDTA to 0. In the previous examples, this initialization was provided by the precompiler. The starting buffer position of 58 can also be determined by the previous DSPFFD command. At (G), the program writes the new record and, similar to how the RCDBUF keyword is added to the READRCDCLF command at (E), here you again add the RCDBUF keyword to identify where the record buffer is located. In this case, you are identifying where the field values can be found.
The remainder of the program is the same as in the previous example. This version of the program can be created into library VINING using either CRTBNDCL PGM(VINING/ADDTSTCPP) for ILE or CRTCLPGM PGM(VINING/ADDTSTCPP) for OPM CL.
Many alternative methods of writing the previous run-time-only example exist. Rather than using a series of *CAT operations at (F), the program could have used a series of %sst built-in operations to set the appropriate fields of the CMDTSTRCD record format. As the character-defined fields (up to &TESTRPLDTA) are contiguous, it is easier (for me anyway) to simply concatenate the fields directly. Another approach would be to use STG(*DEFINED) and DEFVAR support to declare the individual subfields of the &CMDTSTRCD record format variable. The program could then use a series of CHGVAR commands to set the individual *DEFINED subfields. This would certainly be more self-documenting than a series of *CAT or %sst operations. But in any case, you must use CHGVAR (or an equivalent) to set the subfields of the CMDTSTRCD record format prior to writing a new record to the ACTCMDTSTS file.
There are also alternatives to explicitly declaring at (A) the parameters being passed to the program. These approaches, however, run the risk of causing potential confusion in how various fields are actually being used. As this article is intended as a Tips 'n Techniques introduction to using the i database from CL, I have elected to not review these alternatives and hopefully avoid this possible source of confusion.
Below is a CLF precompiler-based version of the SNDESCAPE program from previous articles. The only commands used are those you have seen in earlier examples. For space reasons, as there are no changes required in the FindCaller subroutine of the previous version of the SNDESCAPE, the subroutine (and related declares) are not repeated here. You can find them in the previous article "Determining What Program Is Being Tested."
Pgm Parm(&Parm)
Dcl Var(&Parm) Type(*Char) Len(38)
Dcl Var(&CmdName) Type(*Char) Stg(*Defined) +
Len(10) DefVar(&Parm 29)
Dcl Var(&User) Type(*Char) Len(10)
Dcl Var(&Caller) Type(*Char) Len(10)
Dcl Var(&Count) Type(*Int)
Dcl Var(&MsgTxt) Type(*Char) Len(80)
DclFCLF ActCmdTsts
Dcl Var(&RNF) Type(*Lgl)
/* FindCaller related declares have been removed */
RtvJobA User(&User)
OpnFCLF ActCmdTsts AccMth(*Key)
/***********************************************************/
/* Check if user even testing this command before */
/* bothering to see what program is running the command */
/***********************************************************/
ReadRcdCLF ActCmdTsts Type(*Key) KeyRel(*EQ) +
KeyList(&CmdName &User) RcdNotFnd(&RNF)
If Cond(&RNF) Then(Do)
/************************************************/
/* User is not testing this command */
/************************************************/
CloFCLF ActCmdTsts
Return
EndDo
Else Cmd(Do)
/************************************************/
/* Find our caller so we can determine if the */
/* program is being tested for this command */
/************************************************/
CallSubr Subr(FindCaller)
/************************************************/
/* Unable to find our caller, a definite error */
/************************************************/
If Cond(&MsgTxt *NE ' ') Then(Do)
CloFCLF ActCmdTsts
SndPgmMsg MsgID(CPF9897) MsgF(QCPFMsg) +
MsgDta(&MsgTxt) ToPgmQ(*Prv) +
MsgType(*Escape)
EndDo
/************************************************/
/* Caller found. Are we testing the caller? */
/************************************************/
ReadRcdCLF ActCmdTsts Type(*Key) KeyRel(*EQ) +
KeyList(&CmdName &User &Caller) +
RcdNotFnd(&RNF)
If Cond(&RNF) Then(Do)
/*********************************************/
/* No. Program is not being tested for cmd */
/*********************************************/
CloFCLF ActCmdTsts
Return
EndDo
Else Cmd(Do)
/*******************************************/
/* Yes. So send the message to test with. */
/*******************************************/
CloFCLF ActCmdTsts
SndPgmMsg MsgID(&TestMsg) +
MsgF(&TestMsgFL/&TestMsgF) +
ToPgmQ(*Same (&Caller)) +
MsgType(*Escape)
EndDo
EndDo
Return
/* FindCaller subroutine has been removed */
As with the previous version of SNDESCAPE, the use of the malloc and free APIs within the FindCaller subroutine necessitates a two-step process to create SNDESCAPE if you are on V5R4:
- CRTCLFMOD MODULE(VINING/SNDESCAPE)
- CRTPGM PGM(VINING/SNDESCAPE) BNDDIR(QC2LE)
If your system is on V6R1, you can use this command:
CRTBNDCLF PGM(VINING/SNDESCAPE)
This latest version of the SNDESCAPE program is significantly more flexible than the SNDESCAPE program of previous articles. This version…
- has no hard-coding of the command to be tested
- has no hard-coding of the person doing the testing
- has no hard-coding of the message ID to test the command with
This is due to the use of a database file to store the run-time-related information. This in turn means no more maintenance of the SNDESCAPE program when you need to add, remove, or change test cases, freeing up your time. Moving outside of the SNDESCAPE scenario, I suspect you can probably think of a few existing (or planned) applications within your own shop that could take advantage of this database-driven approach.
In addition to the removal of hard-coded values within the program, this version of SNDESCAPE also introduces a new testing capability. That is, this version has the ability to test specific programs in the call stack of the job being tested. This last point can be important. Let's say you are testing the DONOTHING command within a job stream. In your job stream, program A calls program B, and both programs A and B run the DONOTHING command. If program A's recovery from escape message XYZ1111 is to end the job stream, then you would have a difficult time testing program B's handling of XYZ1111 if program A runs the DONOTHING command before calling program B. The previous version of SNDESCAPE would always send message XYZ1111 to program A if you were currently testing the DONOTHING command within your job. With the updated version of SNDESCAPE, you can explicitly test program B by using the following command:
ADDTSTCASE PGM(B) CMD(DONOTHING) MSGID(XYZ1111) USER(VININGTEST)
The ADDTSTCASE command is not, however, doing everything that it could. You may for instance want to test the command SOMETHING in program C with escape message XYZ1112. Running the command ADDTSTCASE PGM(C) CMD(SOMETHING) MSGID(XYZ1112) would add an active test record to the ACTCMDTSTS file, but you also need to update the QIBM_QCA_RTV_COMMAND exit point to reflect that exit program SNDESCAPE should be called when the SOMETHING command is run. In future articles, we will enhance the ADDTSTCPP command processing program to automatically add an exit program registration facility entry for you (and support customized message replacement data). Prior to these enhancements, though, we will implement additional commands such as Change Test Case (CHGTSTCASE), Display Test Cases (DSPTSTCASE), Remove Test Case (RMVTSTCASE), and Work with Test Cases (WRKTSTCASE).
More CL Questions?
Wondering how to accomplish a function in CL? Send your CL-related questions to me at
LATEST COMMENTS
MC Press Online