Sunday, January 24, 2010

Fortran Unit Testing Framework (part 6): Removing Some Duplication

As we look back at our progress, there is a substantial amount of duplication between the three test methods in our suite of tests for fxxUnit. The routines are:
TSTOUT, TASRTT, and TASRTF. Now we're going to refactor to clean up that duplication.
All 3 SUBROUTINEs perform the same set of actions:
  1. Open a scratch file for our results
  2. Change the output unit to a scratch file,
  3. Call the SUBROUTINE that we are testing with the argument .TRUE.
  4. Call the SUBROUTINE that we are testing with the argument .FALSE.
  5. Rewind the scratch file
  6. Read back the results from the scratch file,
  7. Compare what was written to our expectation, outputting the result character "." or "F",
  8. Clean up by closing the scratch file and re-setting the output
To remind ourselves of the structure, here is the TSTOUT SUBROUTINE,


C
C TEST CASE FOR THE ROUTINE THAT PRINTS OUT THE
C CHARACTER BASED ON THE VALUE OF THE TEST
SUBROUTINE TSTOUT()
COMMON /OUTP/KOUT
INTEGER KOUT
INTEGER I
INTEGER OUTPUT
INTEGER EXPECT
INTEGER RESULT
INTEGER KTEMP
DIMENSION RESULT(2)
DIMENSION EXPECT(2)
DATA (EXPECT(I), I = 1, 2)/1H., 1HF/,OUTPUT/10/
KTEMP = KOUT
KOUT = OUTPUT
CALL RESOUT(.TRUE.)
CALL RESOUT(.FALSE.)
REWIND(OUTPUT)
READ(OUTPUT, 100) (RESULT(I), I = 1, 2)
100 FORMAT(2A1)
CLOSE(OUTPUT)
OPEN(UNIT = OUTPUT, STATUS = 'SCRATCH')
DO 600 I = 1, 2
IF (EXPECT(I) .EQ. RESULT(I)) GO TO 300
WRITE(KTEMP, 200)
200 FORMAT(1HF, $)
GO TO 500
300 CONTINUE
WRITE(KTEMP, 400) IPASS
400 FORMAT(1H., $)
500 CONTINUE
600 CONTINUE
KOUT = KTEMP
RETURN
END


First, we pull out step 7 (the loop) into its own SUBROUTINE. Taking a look at the actual code, we see that it is comparing two INTEGER arrays. So we name it IAASRT:

C
C COMPARES THE RESULT OF TWO INTEGER ARRAYS, WRITING THE RESULT TO
C *KTEMP*. ONE RESULT IS WRITTEN PER ELEMENT OF THE ARRAY.
SUBROUTINE IAEQUL(EXPECT, RESULT, N, KTEMP)
INTEGER EXPECT
INTEGER RESULt
INTEGER N
INTEGER KTEMP
DIMENSION EXPECT(1)
DIMENSION RESULT(1)
DO 500 I = 1, N
IF (EXPECT(I) .EQ. RESULT(I)) GO TO 200
WRITE(KTEMP, 100)
100 FORMAT(1HF, $)
GO TO 400
200 CONTINUE
WRITE(KTEMP, 300)
300 FORMAT(1H., $)
400 CONTINUE
500 CONTINUE
RETURN
END


We replace the original logic with calls to the SUBROUTINE in TSTOUT, TASRTT, and TASRTF. Then we run the tests to verify that we haven't broken anything.

Next, we pull the remaining steps into a second SUBROUTINE, called COMPTF, because it will call a SUBROUTINE that we provide it, with .TRUE., and then with .FALSE. and compare the results written out to our expectations.


C
C COMPARES THE CHARACTERS WRITTEN OUT BY THE SUBROUTINE *FUNCT*
C TO THOSE EXPECTED.
SUBROUTINE COMPTF(FUNCT, PRINTS)
COMMON /OUTP/KOUT
INTEGER KOUT
INTEGER PRINTS
INTEGER KTEMP
INTEGER OUTPUT
INTEGER RESULT
INTEGER I
DIMENSION PRINTS(2)
DIMENSION RESULT(2)
DATA OUTPUT/10/
KTEMP = KOUT
KOUT = OUTPUT
OPEN(UNIT = OUTPUT, STATUS = 'SCRATCH')
CALL FUNCT(.TRUE.)
CALL FUNCT(.FALSE.)
REWIND(OUTPUT)
READ(OUTPUT, 100) (RESULT(I), I = 1, 2)
CLOSE(OUTPUT)
100 FORMAT(2A1)
CALL IAEQUL(PRINTS, RESULT, 2, KTEMP)
KOUT = KTEMP
RETURN
END


The unusual piece here is the first argument, FUNCT. FORTRAN 66 spec allows us to pass a symbolic reference to a SUBROUTINE or FUNCTION. It is similar to a function pointer in C or C++. We give the routine a symbolic name FUNCT in this case, and call it from within our COMPTF SUBROUTINE. The only restriction is that we have to specify the name of the routine being passed as EXTERNAL within the calling code.

With these two routines pulled out, the code to TSTOUT, TASRTT, and TASRTF becomes quite simple:


C
C TEST CASE FOR THE ROUTINE THAT PRINTS OUT THE
C CHARACTER BASED ON THE VALUE OF THE TEST
SUBROUTINE TSTOUT()
EXTERNAL RESOUT
INTEGER EXPECT
DIMENSION EXPECT(2)
INTEGER I
DATA (EXPECT(I), I = 1, 2)/1H., 1HF/
CALL COMPTF(RESOUT, EXPECT)
RETURN
END
C
C TEST CASE FOR ASRTT, THE ASSERTION THAT THE ARGUMENT IS TRUE
SUBROUTINE TASRTT()
EXTERNAL ASRTT
INTEGER EXPECT
DIMENSION EXPECT(2)
INTEGER I
DATA (EXPECT(I), I = 1, 2)/1H., 1HF/
CALL COMPTF(ASRTT, EXPECT)
RETURN
END
C
C TEST CASE FOR ASRTF, THE ASSERTION THAT THE ARGUMENT IS FALSE
SUBROUTINE TASRTF()
EXTERNAL ASRTF
INTEGER EXPECT
DIMENSION EXPECT(2)
DIMENSION ARGS(2)
INTEGER I
DATA (EXPECT(I), I = 1, 2)/1HF, 1H./
CALL COMPTF(ASRTF, EXPECT)
RETURN
END


Summary:

In this installment we refactored our code that tests our testing framework to remove some duplicate logic.
  • We pulled out the comparison into one SUBROUTINE, and, realizing that its logic was more general than the original context, gave it a name that reveled the general function
  • We then moved the rest of the duplicate code into a second SUBROUTINE.
  • We made use of the EXTERNAL capacity of FORTRAN 66 to simplify the code even further.

There are couple more things that we could change. Two possibilities that immediately come to mind are
Pulling the code to swap output units, and
Replacing the output characters "." and "F" with meaningful names that we use throughout.

The first only occurs in the COMPTF SUBROUTINE, so it doesn't bother me. The second is worth considering for another day.

One final, important point:
The discussion above is quite sanitized compared to the actual process I used in these refactorings. I omitted all of the intermediate steps to save space and increase the pace to something that would not be too annoying. Every small change (creating new code, calling it, removing the original code, etc.) was followed by running the tests. Several times (at least 5) I discovered that I had introduced a error, usually by forgetting to declare the type of a variable in the SUBROUTINE, or by omitting a variable in the argument list of the new SUBROUTINE. FORTRAN is a major pain here, because the compiler creates new variables and sets their types according the the implicit conventions.

In each case, it introduced a bug that I would have missed if I hadn't had a suite of tests. In the microcosm of this little example, I had the value of unit testing reinforced to me again and again.

Without a good test suite, writing correct code is hideously hard. In the context of a simulation, in which we have so many moving parts, recognizing errors based upon the final output is nearly impossible. Unit testing is a vital part of conducting high quality, professional simulations.

No comments:

Post a Comment