Guru: TryIT – You’ll Like It
October 2, 2023 Ted Holt
I have watched children play Whac-a-mole, but I’ve never played it myself, perhaps because the game unpleasantly reminds me of programs that I have had to work on. I fix one bug, only to see another bug rear its ugly little head. Life’s too short to endure such nonsense. Besides, it is embarrassing for someone to tell me that the program I supposedly just fixed is still broken.
Suppose you’re working on a 4,000-line RPG program and you comment out lines 650-660. What you don’t realize at the time is that a variable used in line 2755 has to have the value assigned to it in line 654. Instead, it has the value assigned to it on line 1435. Whac-a-mole!
One thing’s for sure: the law of unintended consequences will be obeyed. For that reason, I’ve thought a lot about two questions. (1) Why does program maintenance sometimes cause undesirable side effects? and (2) What programming practices can I adopt to avoid introducing bugs?
As for the first question, I’m convinced that the primary reason for undesirable side effects is the use of global variables. These are variables that can be referenced throughout an entire program, as opposed to local variables, which are available to only a part of a program. The early RPG and COBOL compilers that I used supported global variables only.
If my reasoning is correct, then the answer to the second question is to avoid global variables. And how do I do that? By writing subprocedures. And that is how I write programs. My programs are a collection of subprocedures under the control of a main driver routine. Data is passed between subprocedures through parameters, and working data is defined within the subprocedure so that the rest of the program doesn’t know about it.
Every subprocedure requires rigorous testing, and that’s difficult to pull off in a large program. How can you be assured that your test data will exercise all possible combinations of input data? I get that assurance from a subprocedure I call TryIt.
Here’s the TryIt template source member, TRYIT_T.
**free // This program . . . (does what?) . . . ctl-opt option(*srcstmt: *nodebugio) dftactgrp(*no) actgrp(*new); dcl-c LINE_LENGTH 132; dcl-f qsysprt printer(LINE_LENGTH); dcl-ds PrintLine len(LINE_LENGTH) qualified inz; xxx char ( 1); *n char ( 1); yyy char ( 1); *n char ( 1); end-ds; *inlr = *on; // print column headings PrintLine.xxx = 'xxx'; evalr PrintLine.yyy = 'yyy'; writeln (PrintLine); TryIt ( 'x' : 0 ); return; dcl-proc TryIt; dcl-pi *n; inxxx char (1) const; inyyy packed (1) const; end-pi; SomeRoutine ( inxxx: inyyy ); // print results of call to the subprocedure clear Printline; PrintLine.xxx = inxxx ; evalr PrintLine.yyy = %editc( inyyy : 'X' ); writeln (PrintLine); end-proc; dcl-proc SomeRoutine; dcl-pi *n; inxxx char ( 1 ) const; inyyy packed ( 1 ) const; end-pi; end-proc SomeRoutine; dcl-proc writeln; dcl-pi *n; inString varchar(LINE_LENGTH) const; inPosition uns(3) const options(*nopass); end-pi; dcl-ds ReportLine len(LINE_LENGTH) end-ds; dcl-s Position uns(3); if %parms() >= %ParmNum(inPosition); Position = inPosition; else; Position = 1; endif; %subst(ReportLine: Position) = inString; write qsysprt ReportLine; end-proc writeln;
SomeRoutine is a stub for the procedure that I am developing for a production environment. For lack of a better term, I refer to this as the target subprocedure. If you have a better name, let me know.
Suppose you and I work for a company that sells products. Each product has a catalog price (also known as the list price), but that price is not necessarily the one a customer will pay. The price at which we agree to sell a product depends on three criteria.
- Who the customer is. Type A customers, who account for most of our revenue, get a 10% discount. Type B customers, whose business we also value and want to keep, get a 5% discount. Type C customers, occasional or undesirable customers, get no discount.
- How many units the customer buys. He pays full price for onesies and twosies. If he’ll let us ship him a lot of them, we’ll give him a bulk discount.
- How soon the customer wants the product delivered. If he wants it tomorrow, there’ll be a surcharge.
This is an ideal task for a subprocedure. The input parameters are obvious – customer ID, product ID, order quantity, and due date. The subprocedure needs to return a price, which could be done through a parameter or the return value. The subprocedure also needs a way to indicate that an error has occurred, which could be done through a parameter, the return value, or an escape message. To keep it simple, I’ll use parameters for everything. We give it a clever name, such as CalculateUnitPrice.
dcl-proc CalculateUnitPrice; dcl-pi *n; inCustomerID like(CustomerID_t) const; inProductID like(ProductID_t) const; inOrderQuantity packed (7) const; inDueDate date const; ouPrice packed (7: 2); ouErrorCode uns (3); end-pi;
The first two parameters are based on templates in copybook DATADEFS.
dcl-s CustomerID_t packed (5) template; dcl-s ProductID_t char (6) template;
So that’s the procedure interface for the target subprocedure. To test this subprocedure, we call it from the TryIt subprocedure. TryIt usually has the same input parameters as the target subprocedure. The output parameters of the target subprocedure become local variables within TryIt.
dcl-proc TryIt; dcl-pi *n; inCustomerID like(CustomerID_t) const; inProductID like(ProductID_t) const; inOrderQuantity packed (7) const; inDueDate date const; end-pi; dcl-s Price packed (7: 2); dcl-s ErrorCode uns (3); CalculateUnitPrice ( inCustomerID: inProductID: inOrderQuantity: inDueDate: Price: ErrorCode);
The main routine needs to call TryIt with parameters that cover every possible test case. I usually don’t test every case from the start. I prefer to use some form of test-driven development (TDD). This means that I develop the target subprocedure a piece at a time. I’ll start by writing the code to validate the input parameters.
TryIt ( 0 : *blank : 0 : d'2000-01-01' ); TryIt ( 100 : *blank : 0 : d'2000-01-01' ); TryIt ( 100 : 'AB-101' : 0 : d'2000-01-01' ); return; dcl-proc TryIt; dcl-pi *n; inCustomerID like(CustomerID_t) const; inProductID like(ProductID_t) const; inOrderQuantity packed (7) const; inDueDate date const; end-pi; dcl-s Price packed (7: 2); dcl-s ErrorCode uns (3); CalculateUnitPrice ( inCustomerID: inProductID: inOrderQuantity: inDueDate: Price: ErrorCode); end-proc; dcl-proc CalculateUnitPrice; dcl-pi *n; inCustomerID like(CustomerID_t) const; inProductID like(ProductID_t) const; inOrderQuantity packed (7) const; inDueDate date const; ouPrice packed (7: 2); ouErrorCode uns (3); end-pi; clear ouPrice; clear ouErrorCode; // validate the parameters if inCustomerID <= *zero; ouErrorCode = 1; return; endif; if inProductID = *blanks; ouErrorCode = 2; return; endif; end-proc CalculateUnitPrice;
That’s enough code to exercise these two tests.
The return code should be 1, 2, and zero after the calls to TryIt. The price will always be zero because I haven’t written any code to set the price.
How will you know whether that was indeed the case? Well, you can crank up your favorite interactive debugger. There’s nothing wrong with that. I prefer to build a report. I wrote about this technique a couple of years ago. (See Guru: Quick And Handy RPG Output, Take 2.)
**free dcl-c LINE_LENGTH 132; dcl-f qsysprt printer(LINE_LENGTH); dcl-ds PrintLine len(LINE_LENGTH) template; CustomerID char ( 5); *n char ( 1); ProductID char ( 6); *n char ( 1); OrderQuantity char ( 8); *n char ( 1); DueDate char (10); *n char ( 1); Price char ( 9); *n char ( 1); ErrorCode char ( 3); end-ds; /copy copybooks,datadefs *inlr = *on; // print column headings PrintLine.CustomerID = 'Cus'; PrintLine.ProductID = 'Prod'; evalr PrintLine.OrderQuantity = 'Qty '; PrintLine.DueDate = 'Due'; evalr PrintLine.Price = 'Price '; PrintLine.ErrorCode = 'Err'; writeln (PrintLine); TryIt ( 0 : *blank : 0 : d'2000-01-01' ); TryIt ( 100 : *blank : 0 : d'2000-01-01' ); TryIt ( 100 : 'AB-101' : 1 : d'2000-01-01' ); return; dcl-proc TryIt; dcl-pi *n; inCustomerID like(CustomerID_t) const; inProductID like(ProductID_t) const; inOrderQuantity packed (7) const; inDueDate date const; end-pi; dcl-s Price packed (7: 2); dcl-s ErrorCode uns (3); CalculateUnitPrice ( inCustomerID: inProductID: inOrderQuantity: inDueDate: Price: ErrorCode); // Print the results of calling the subprocedure. clear Printline; evalr PrintLine.CustomerID = %editc( inCustomerID : 'X'); PrintLine.ProductID = inProductID ; evalr PrintLine.OrderQuantity = %editc( inOrderQuantity : 'L'); PrintLine.DueDate = %char ( inDueDate ); evalr PrintLine.Price = %editc( Price : 'L'); PrintLine.ErrorCode = %char ( ErrorCode ); writeln (PrintLine); end-proc; dcl-proc CalculateUnitPrice; dcl-pi *n; inCustomerID like(CustomerID_t) const; inProductID like(ProductID_t) const; inOrderQuantity packed (7) const; inDueDate date const; ouPrice packed (7: 2); ouErrorCode uns (3); end-pi; clear ouPrice; clear ouErrorCode; // validate the parameters if inCustomerID <= *zero; ouErrorCode = 1; return; endif; if inProductID = *blanks; ouErrorCode = 2; return; endif; end-proc CalculateUnitPrice; dcl-proc writeln; dcl-pi *n; inString varchar(LINE_LENGTH) const; inPosition uns(3) const options(*nopass); end-pi; dcl-ds ReportLine len(LINE_LENGTH) end-ds; dcl-s Position uns(3); if %parms() >= %ParmNum(inPosition); Position = inPosition; else; Position = 1; endif; %subst(ReportLine: Position) = inString; write qsysprt ReportLine; end-proc writeln;
Here’s the output.
Cus Prod Qty Due Price Err 00000 0 2000-01-01 .00 1 00100 0 2000-01-01 .00 2 00100 AB-101 1 2000-01-01 .00 0
Look at price and error code. Everything is correct so far.
Let’s continue to build the pricing routine. Let’s add code to retrieve the necessary customer data and product data.
dcl-proc CalculateUnitPrice; dcl-pi *n; inCustomerID like(CustomerID_t) const; inProductID like(ProductID_t) const; inOrderQuantity packed (7) const; inDueDate date const; ouPrice packed (7: 2); ouErrorCode uns (3); end-pi; dcl-s Price like(ouPrice); dcl-s CustomerType char(1); clear ouPrice; clear ouErrorCode; // validate the parameters if inCustomerID <= *zero; ouErrorCode = 1; return; endif; if inProductID = *blanks; ouErrorCode = 2; return; endif; // Start with the list price. exec sql select p.price into :Price from products as p where p.id = :inProductID; if SqlState >= '02000'; ouErrorCode = 3; return; endif; exec sql select c.type into :CustomerType from customers as c where c.id = :inCustomerID; if SqlState >= '02000'; ouErrorCode = 4; return; endif; ouPrice = Price; end-proc CalculateUnitPrice;
We need additional test cases.
TryIt ( 0 : *blank : 0 : d'2000-01-01' ); TryIt ( 100 : *blank : 0 : d'2000-01-01' ); TryIt ( 100 : 'AB-101' : 1 : d'2000-01-01' ); TryIt ( 100 : 'smurf' : 1 : d'2000-01-01' ); TryIt ( 999 : 'smurf' : 1 : d'2000-01-01' ); TryIt ( 999 : 'AB-101' : 1 : d'2000-01-01' );
Assuming customer 100 and product AB-101 are valid data and 999 and smurf are not, these tests cover all the bases so far.
Cus Prod Qty Due Price Err 00000 0 2000-01-01 .00 1 00100 0 2000-01-01 .00 2 00100 AB-101 1 2000-01-01 1.80 0 00100 smurf 1 2000-01-01 .00 3 00999 smurf 1 2000-01-01 .00 3 00999 AB-101 1 2000-01-01 .00 4
All the error codes are accurate, and the only price retrieved is in fact the catalog price for item AB-101. So far so good!
I’ll stop here, as I’ve covered all the concepts. If this were a production program, I would add the code to calculate the discount and we’d add the necessary TryIt calls to test each piece of code. Eventually I would have a bug-free pricing routine that works exactly as I want it to. Since it has no global data, I could place it into a program or service program without fear of it causing undesirable side effects.
“But wait,” I hear you say. “Aren’t the first two parameters defined with global data?” And the answer is no, they are based on global data definitions, and global data definitions are a wonderful way to define data consistently throughout a software application, especially when they’re in a copybook.
“And,” you continue, “isn’t the use of the QSYSPRT file global?” Yes, of course it is, but it makes the code simpler and lets me concentrate on the task of building the target subprocedure. Because of the global printer file, I can put writeln calls in the target subprocedure if I need to. Of course, I remove them before moving the target subprocedure into the destination production source member.
Building a subprocedure inside a copy of the TryIt template makes it easy for me to develop a subprocedure that does exactly what it needs to do without affecting the rest of a program, and allows me to easily test the code with all possible input values. That gives me peace of mind.
Besides, I don’t have time to play Whac-a-mole at work.