Wednesday, January 6, 2010

Fortran Unit Testing Framework (part 5): Writing Assertions

In this installment, we finally get around to writing the assertions. Recall that from part 2 we determined that the assertions would be SUBROUTINEs, not FUNCTIONs. We also decided to adopt the Fortran convention that the first letter will indicate what type of argument is expected. The middle 4 letters will be ASRT (for assert) and the final letter will contain information about the assertion ("T" for .TRUE., "F" for .FALSE., or "E" for .EQ.).

Before we begin, we notice that RESOUT requires the output unit number. So, we are going to have to write
ASRTT(RESULT, OUTPUT)
That gets intrusive pretty quickly. Let's pull the output unit out, into a COMMON block named OUTP:


C
C
PROGRAM MAIN
COMMON /OUTP/KOUT
INTEGER KOUT
...
STOP
END
...
C
C SUBROUTINE RESOUT
C WRITES OUT THE RESULT OF THE TEST
C '.' IF THE ARGUMENT IS .TRUE.
C 'F' OTHERWISE
C PARAM - LOGICAL *RESLT* WHETHER OR NOT THE TEST HAS PASSED
SUBROUTINE RESOUT(RESLT)
COMMON /OUTP/KOUT
INTEGER KOUT
LOGICAL RESLT
INTEGER RESCHR
INTEGER CHARA
CHARA = RESCHR(RESLT)
WRITE(KOUT, 100) CHARA
100 FORMAT(A1, $)
RETURN
END


We only include the block in the routines what need access to it, effectively limiting its scope. Now we can write assertions along the following lines:


    LOGICAL TEST
...
TEST = .TRUE.
...
CALL ASRTT(TEST)
...


which is much more natural.
With the refactoring done, we begin with the test for assert true (
ASRTT). The test code is pretty simple:


C
C TEST CASE FOR ASRTT, THE ASSERTION THAT THE ARGUMENT IS TRUE
SUBROUTINE TASRTT()
COMMON /OUTP/KOUT
EXTERNAL ASRTT
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
OPEN(UNIT = OUTPUT, STATUS = 'SCRATCH')
CALL ASRTT(.TRUE.)
CALL ASRTT(.FALSE.)
REWIND(OUTPUT)
READ(OUTPUT, 100) (RESULT(I), I = 1, 2)
CLOSE(OUTPUT)
100 FORMAT(2A1)
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


This is essentially a repeat of the test we developed for RESOUT. Making the test pass simply requires a call to RESOUT.


C
C BASIC ASSERT_TRUE
C
C PARAM: LOGICAL TSTVAL THE VALUE TO BE TESTED
C PARAM: INTEGER OUTPUT THE UNIT TO WRITE THE RESULT TO
SUBROUTINE ASRTT(TSTVAL)
LOGICAL TSTVAL
CALL RESOUT(TSTVAL, OUTPUT)
RETURN
END


The test for ASRTF is the same, with the sole exception that the elements of EXPECT array are reversed.


C
C TEST CASE FOR ASRTF, THE ASSERTION THAT THE ARGUMENT IS TRUE
SUBROUTINE TASRTF()
COMMON /OUTP/KOUT
EXTERNAL ASRTT
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
OPEN(UNIT = OUTPUT, STATUS = 'SCRATCH')
CALL ASRTF(.TRUE.)
CALL ASRTF(.FALSE.)
REWIND(OUTPUT)
READ(OUTPUT, 100) (RESULT(I), I = 1, 2)
CLOSE(OUTPUT)
100 FORMAT(2A1)
DO 600 I = 1, 2
IF (EXPECT(I) .EQ. RESULT(I)) GO TO 300
WRITE(KOUT, 200)
200 FORMAT(1HF, $)
GO TO 500
300 CONTINUE
WRITE(KOUT, 400) IPASS
400 FORMAT(1H., $)
500 CONTINUE
600 CONTINUE
KOUT = KTEMP
RETURN
END


Making the test pass involves simply negating the argument and passing it to ASRTT.


C
C BASIC ASSERT_FALSE
C
C PARAM: LOGICAL TSTVAL THE VALUE TO BE TESTED
C PARAM: INTEGER OUTPUT THE UNIT TO WRITE THE RESULT TO
      SUBROUTINE ASRTF(TSTVAL)
      LOGICAL TSTVAL
      CALL ASRTT(.NOT. TSTVAL)
      RETURN
      END


At this point we have repeated the code to set the output unit, set our expectations, and read the results back in 3 times. This is a major violation of the DRY principle, and so we really should refactor the code to remove the duplication. We will make a note to fix that next time.

As a second point, we really would like to drop the writing and reading back in part of the tests. We tested that as part of RESOUT; it seems foolish to keep testing it. Instead, let's factor out the common functionality into the the test for RESOUT, and just call that for each of our assertions. Our main logic will reside in a set of LOGICAL functions, that can be tested separately. The basis of each function name will be EQUAL, with the prefix letter that indicates the type of the argument:
  • IEQUAL - compares 2 INTEGER values
  • LEQUAL - compares 2 LOGICAL values
  • AEQUAL - compares 2 REAL values
  • DEQUAL - compares 2 DOUBLE PRECISION values
  • CEQUAL - compares 2 COMPLEX values


C
C TESTS THE FUNCTIONING OF THE INTEGER COMPARISON,
C LOGICAL FUNCTION IEQUAL
      SUBROUTINE TESTI()
      LOGICAL IEQUAL
      CALL ASRTT(IEQUAL(1, 1))
      CALL ASRTF(IEQUAL(1, 2))
      RETURN
      END



The tests verify that 2 integers are equal return .TRUE., and two that are not equal return .FALSE. We then make use of the functionality that we have created already in RESCHR and RESOUT. The code to make these pass is simple:


C
C DETERMINE IF TWO INTEGERS ARE EQUAL:
C
C INPUT EXPECT (INTEGER) - THE EXPECTED VALUE
C INPUT ACTUAL (INTEGER) - THE VALUE TO BE COMPARED TO EXPECT
C OUTPUT EXPECT .EQ. ACTUAL
      LOGICAL FUNCTION IEQUAL(EXPECT, ACTUAL)
      INTEGER EXPECT
      INTEGER ACTUAL
      IEQUAL = EXPECT .EQ. ACTUAL
      END


The test for LOGICAL variables is similarly simple:


C
C TESTS THE FUNCTIONING OF THE LOGICAL COMPARISON,
C LOGICAL FUNCTION LEQUAL
      SUBROUTINE TESTL()
      LOGICAL LEQUAL
      CALL ASRTT(LEQUAL(.TRUE., .TRUE.))
      CALL ASRTT(LEQUAL(.FALSE., .FALSE.))
      CALL ASRTF(LEQUAL(.TRUE., .FALSE.))
      CALL ASRTF(LEQUAL(.FALSE., .TRUE.))
      RETURN
      END


The assertion code has s similarly simple implementation:


C
C DETERMINE IF TWO LOGICALS ARE EQUIVALENT:
C
C INPUT EXPECT (LOGICAL) - THE EXPECTED VALUE
C INPUT ACTUAL (LOGICAL) - THE VALUE TO BE COMPARED TO EXPECT
C OUTPUT EXPECT .EQ. ACTUAL
C
C NOTE: BECAUSE F77 WANTS ".EQV." INSTEAD OF
C THE F66 ".EQ." WHEN COMPARING LOGICALS, THE
C COMPARISON IS WRITTEN IN A SOMEWHAT CONVOLUTED WAY
      LOGICAL FUNCTION LEQUAL(EXPECT, ACTUAL)
      LOGICAL EXPECT
      LOGICAL ACTUAL
      LEQUAL = (EXPECT .AND. ACTUAL) .OR.
     & (.NOT. EXPECT .AND. .NOT. ACTUAL)
      END


As the comment indicates, there is one slight complication. FORTRAN 77 prefers the use of the .EQV. relation for comparing two LOGICAL variables. To silence the compiler warnings (from g77), the routine is written in the current, somewhat unintuitive way.
For the comparison of REALs,there is a slight twist. Floating point arithmetic is imprecise, and so the exact comparison of 2 floating point numbers (REAL, DOUBLE PRCISION, or COMPLEX) is a bad idea; we may get two comparisons that should be equal that are not. For example:

C
C DEMONSTRATION OF FLOATING POINT
      PROGRAM COMP
      REAL ONE
      REAL THIRD
      REAL THREE
      LOGICAL RESULT
      ONE = 1.0
      THIRD = 1.0 / 3.0
      WRITE (6, 100) ONE, THIRD
  100 FORMAT(6HONE = , F15.10, 1X, 13H 1.0 / 3.0 = , F15.10)
      THREE = THIRD * 3.0
      WRITE (6, 200) THIRD
  200 FORMAT(20H 3.0 * (1.0 / 3.0) = , F15.10)
      RESULT = ONE .EQ. THIRD
      WRITE (6, 300) RESULT
  300 FORMAT(32H 1.0 .EQ. (3.0 * (1.0 / 3.0)) = ,L1)
      STOP
      END


yields the output


ONE =    1.0000000000  1.0 / 3.0 =    0.3333333433
3.0 * (1.0 / 3.0) = 0.3333333433
1.0 .EQ. (3.0 * (1.0 / 3.0)) = F


To account for this, xUnit framework assertions for floating point values add a third parameter, the positive value EPSILN. If the two values are within EPSILN of each others, the assertion returns true. Otherwise, it is false. This gives us the test cases for AEQUAL and DEQUAL:

C
C TESTS THE FUNCTIONING OF THE COMPARISON OF REALS,
C LOGICAL FUNCTION AEQUAL
      SUBROUTINE TESTA()
      LOGICAL AEQUAL
      REAL EXPECT
      REAL ACTUL1
      REAL ACTUL2
      REAL ACTUL3
      REAL EPS1
      REAL EPS2
      DATA EXPECT/1.0/, ACTUL1/1.0/, ACTUL2/1.00001/, ACTUL3/0.99999/
      DATA EPS1/1.E-1/, EPS2/1.E-7/
      CALL ASRTT(AEQUAL(EXPECT, ACTUL1, EPS1))
      CALL ASRTT(AEQUAL(EXPECT, ACTUL1, EPS2))
      CALL ASRTT(AEQUAL(EXPECT, ACTUL2, EPS1))
      CALL ASRTF(AEQUAL(EXPECT, ACTUL2, EPS2))
      CALL ASRTT(AEQUAL(EXPECT, ACTUL3, EPS1))
      CALL ASRTF(AEQUAL(EXPECT, ACTUL3, EPS2))
      RETURN
      END
C
C TESTS THE FUNCTIONING OF THE COMPARISON OF DOUBLES,
C LOGICAL FUNCTION EQUALD
      SUBROUTINE TESTD()
      LOGICAL DEQUAL
      DOUBLE PRECISION EXPECT
      DOUBLE PRECISION ACTUL1
      DOUBLE PRECISION ACTUL2
      DOUBLE PRECISION ACTUL3
      DOUBLE PRECISION EPS1
      DOUBLE PRECISION EPS2
      DATA EXPECT/1.0D0/, ACTUL1/1.0D0/, ACTUL2/1.00000001D0/,
     & ACTUL3/0.99999999D0/
      DATA EPS1/1.D-1/
C EPS2 SEEMS LIKE IT SHOULD BE 1.D-8, BUT DOUBLE PRECISION
C REPRESENTATION OF *ACTUL3* IS SLIGHTLY LESS THAN THE DECIMAL
C REPRESENTATION, SO THE TEST WILL FAIL IF 1.D-8 IS USED
      DATA EPS2/5.D-9/
      CALL ASRTT(DEQUAL(EXPECT, ACTUL1, EPS1))
      CALL ASRTT(DEQUAL(EXPECT, ACTUL1, EPS2))
      CALL ASRTT(DEQUAL(EXPECT, ACTUL2, EPS1))
      CALL ASRTF(DEQUAL(EXPECT, ACTUL2, EPS2))
      CALL ASRTT(DEQUAL(EXPECT, ACTUL3, EPS1))
      CALL ASRTF(DEQUAL(EXPECT, ACTUL3, EPS2))
      RETURN
      END



with the obvious implementations:


C
C DETERMINE IF TWO REALS ARE "ESSENTIALLY" EQUAL:
C ABS(EXPECT - ACTUAL) <= EPSILN
C
C INPUT EXPECT (REAL) - THE EXPECTED VALUE
C INPUT ACTUAL (REAL) - THE VALUE TO BE COMPARED TO EXPECT
C INPUT EPSILN (REAL) - THE TOLERANCE
C OUTPUT RESULT
      LOGICAL FUNCTION AEQUAL(EXPECT, ACTUAL, EPSILN)
      REAL EXPECT
      REAL ACTUAL
      REAL EPSILN
      AEQUAL = ABS(EXPECT - ACTUAL) .LE. EPSILN
      END
C
C DETERMINE IF TWO DOUBLES ARE "ESSENTIALLY" EQUAL:
C ABS(EXPECT - ACTUAL) <= EPSILN
C
C INPUT EXPECT (DOUBLE) - THE EXPECTED VALUE
C INPUT ACTUAL (DOUBLE) - THE VALUE TO BE COMPARED TO EXPECT
C INPUT EPSILN (DOUBLE) - THE TOLERANCE
C OUTPUT RESULT
      LOGICAL FUNCTION DEQUAL(EXPECT, ACTUAL, EPSILN)
      DOUBLE PRECISION EXPECT
      DOUBLE PRECISION ACTUAL
      DOUBLE PRECISION EPSILN
      DEQUAL = DABS(EXPECT - ACTUAL) .LE. EPSILN
      END



Finally, the COMPLEX data type has a similar implementation. The only difference is that EPSILN will also be of type COMPLEX. This allows the user to specify separate epsilon values for the real and imaginary parts of the comparison. The FORTRAN 66 spec specifies that both the real and imaginary parts of a COMPLEX are REALs. So, we come up with the test:

C
C TESTS THE FUNCTIONING OF THE COMPARISON OF COMPLEX VALUES,
C LOGICAL FUNCTION CEQUAL
C THIS IS SOMEWHAT INELEGANT, BUT COVERS ALL OF THE POSSIBLITIES
      SUBROUTINE TESTC
      LOGICAL CEQUAL
      COMPLEX EXPECT
      COMPLEX ACTUAL
      COMPLEX EPS1
      COMPLEX EPS2
      COMPLEX EPS3
      COMPLEX EPS4
      REAL ONE
      REAL ONEPLS
      REAL ONEMNS
      REAL EPSLN1
      REAL EPSLN2
      DATA ONE/1.0/, ONEPLS/1.00001/, ONEMNS/0.99999/
      DATA EPSLN1/1.E-1/, EPSLN2/1.E-7/
C
      EXPECT = CMPLX(ONE, ONE)
      ACTUAL = CMPLX(ONE, ONE)
      EPS1 = CMPLX(EPSLN1, EPSLN1)
      EPS2 = CMPLX(EPSLN2, EPSLN2)
      EPS4 = CMPLX(EPSLN1, EPSLN2)
      EPS3 = CMPLX(EPSLN2, EPSLN1)
C
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS1))
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS2))
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS3))
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS4))
C
      ACTUAL = CMPLX(ONEPLS, ONE)
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS1))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS2))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS3))
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS4))
C
      ACTUAL = CMPLX(ONEMNS, ONE)
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS1))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS2))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS3))
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS4))
C
      ACTUAL = CMPLX(ONE, ONEPLS)
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS1))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS2))
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS3))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS4))
C
      ACTUAL = CMPLX(ONE, ONEMNS)
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS1))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS2))
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS3))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS4))
C
      ACTUAL = CMPLX(ONEPLS, ONEPLS)
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS1))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS2))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS3))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS4))
C
      ACTUAL = CMPLX(ONEPLS, ONEMNS)
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS1))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS2))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS3))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS4))
C
      ACTUAL = CMPLX(ONEMNS, ONEPLS)
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS1))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS2))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS3))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS4))
C
      ACTUAL = CMPLX(ONEMNS, ONEMNS)
      CALL ASRTT(CEQUAL(EXPECT, ACTUAL, EPS1))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS2))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS3))
      CALL ASRTF(CEQUAL(EXPECT, ACTUAL, EPS4))
      RETURN
      END


We could implement this inline (with calls to the ABS function). Instead, we again apply the DRY principle and call AEQUAL.


C
C DETERMINE IF TWO COMPLEX VALUES ARE "ESSENTIALLY" EQUAL:
C ABS(EXPECT - ACTUAL) <= EPSILN FOR BOTH THE REAL
C AND IMAGINARY PARTS
C
C INPUT EXPECT (COMPLEX) - THE EXPECTED VALUE
C INPUT ACTUAL (COMPLEX) - THE VALUE TO BE COMPARED TO EXPECT
C INPUT EPSILN (COMPLEX) - THE TOLERANCE
C OUTPUT RESULT
      LOGICAL FUNCTION
      CEQUAL(EXPECT, ACTUAL, EPSILN)
LOGICAL AEQUAL
      COMPLEX EXPECT
      COMPLEX ACTUAL
      COMPLEX EPSILN
      CEQUAL = AEQUAL(REAL(EXPECT), REAL(ACTUAL), REAL(EPSILN))
     & .AND. AEQUAL(AIMAG(EXPECT), AIMAG(ACTUAL), AIMAG(EPSILN))
      END


The next step is to write our xASRTE SUBROUTINEs in terms of xEQUAL and ASRTT:


C
C IASRTE
C ASSERTS THAT 2 INTEGERS ARE EQUAL
      SUBROUTINE IASRTE(EXPECT, ACTUAL)
      INTEGER EXPECT
      INTEGER ACTUAL
      LOGICAL RESULT
      RESULT = IEQUAL(EXPECT, ACTUAL)
      CALL ASRTT(RESULT)
      RETURN
      END
C
C LASRTE
C ASSERTS THAT 2 LOGICAL VARIABLES ARE EQUAL
      SUBROUTINE LASRTE(EXPECT, ACTUAL)
      LOGICAL EXPECT
      LOGICAL ACTUAL
      LOGICAL RESULT
      RESULT = LEQUAL(EXPECT, ACTUAL)
      CALL ASRTT(RESULT)
      RETURN
      END
C
C AASRTE
C ASSERTS THAT 2 REALS ARE ESSENTIALLY EQUAL
C ABS(EXPECT - ACTUAL) .LE. EPSILN
      SUBROUTINE AASRTE(EXPECT, ACTUAL, EPSILN)
      REAL EXPECT
      REAL ACTUAL
      REAL EPSILN
      LOGICAL RESULT
      RESULT = AEQUAL(EXPECT, ACTUAL)
      CALL ASRTT(RESULT)
      RETURN
      END
C
C DASRTE
C ASSERTS THAT 2 DOUBLE PRECISION VARIABLES ARE ESSENTIALLY EQUAL
C DABS(EXPECT - ACTUAL) .LE. EPSILN
      SUBROUTINE IASRTE(EXPECT, ACTUAL, EPSILN)
      DOUBLE PRECISION EXPECT
      DOUBLE PRECISION ACTUAL
      DOUBLE PRECISION EPSILN
      LOGICAL RESULT
      RESULT = DEQUAL(EXPECT, ACTUAL)
      CALL ASRTT(RESULT)
      RETURN
      END
C
C CASRTE
C ASSERTS THAT 2 DOUBLE PRECISION VARIABLES ARE ESSENTIALLY EQUAL
C ABS(REAL(EXPECT) - REAL(ACTUAL)) .LE. REAL(EPSILN)
C ABS(AIMAG(EXPECT) - AIMAG(ACTUAL)) .LE. AIMAG(EPSILN)
      SUBROUTINE CASRTE(EXPECT, ACTUAL, EPSILN)
      COMPLEX EXPECT
      COMPLEX ACTUAL
      COMPLEX EPSILN
      LOGICAL RESULT
      RESULT = CEQUAL(EXPECT, ACTUAL, EPSILN)
      CALL ASRTT(RESULT)
      RETURN
      END



Finally, we have a main program to run all of our tests:


      PROGRAM TCASES
      COMMON /OUTP/KOUT
      INTEGER KOUT
      KOUT = 6
      CALL TSTCHR()
      CALL TSTOUT()
      CALL TASRTT()
      CALL TASRTF()
      CALL TESTI()
      CALL TESTL()
      CALL TESTA()
      CALL TESTD()
      CALL TESTC()
      STOP
      END

Summary


In this installment, we've covered a lot of ground. We completed the basic version of our xUnit framework for FORTRAN 66.
We:
  • Wrote ASRTT by using RESOUT

  • Wrote ASRTF by using ASRTT

  • Created xEQUAL classes for each of the types
    • INTEGER
    • LOGICAL
    • REAL (accounting for the imprecision in floating point arithmetic)
    • DOUBLE PRECISION (again accounting for the imprecision in floating point arithmetic)
    • COMPLEX
    • (by using AEQUAL)

  • Wrote our xASRTE classes using xEQUAL and ASRTT

  • Did all of it using Test-driven development (TDD), so that we have a complete set of test cases to support refactoring.



Next time we will tighten up the code, cleaning up a a few remaining bits of duplication, and enhance the framework to include some useful feedback.

Friday, January 1, 2010

Creating A Fortran Unit Testing Framework (part 4): Outputting the Test Result

In this installment, we are going to implement the next set of functionality, wiring out the results of the tests. Recall that last time we wrote RESCHR, the routine that determined what to print out ("." or "F") depending upon the result of the test. Our next piece of functionality will be to write that character out, a routine we call RESOUT.
As with RESCHR, we will write RESOUT using test-driven development TDD). The first problem is how to figure out whether something is printing correctly. The simple answer is to just print it to the screen and look at it (and frankly is the very first approach I use). The problem with this approach is that it requires us to look at the output for every test every time we run them. This gets old in a hurry. To be really effective, unit tests must be quick and easy to run. The more effort required to run and evaluate them, the less often they will be used. So, we want the testing framework to automatically figure out whether or not the right thing is being printed out.
To make this automatic, we need to print to a place other than the default output and then compare what was written to our expectation. Most modern languages have some sort of internal representation of a string or file. In FORTRAN 66, we could use Hollerith fields encoded into integer arrays to achieve that effect, but it seems way too much effort of this particular job (we'll tackle that another day). Instead, I'm going to take the easy way out. I'll create a scratch file, write out to it, read the results back in, and compare them to my expectation. After a couple of false starts, here is the test that I came up with:



C
C TEST CASE FOR THE ROUTINE THAT PRINTS OUT THE
C CHARACTER BASED ON THE VALUE OF THE TEST
      SUBROUTINE TSTOUT()
      INTEGER I
      INTEGER PASS
      INTEGER FAIL
      INTEGER RESULT
      DIMENSION RESULT(2)
      DATA PASS/1H./, FAIL/1HF/
      OPEN(UNIT=10, STATUS='SCRATCH')
      CALL RESOUT(.TRUE.)
      CALL RESOUT(.FALSE.)
      REWIND(10)
      READ(10, 3000) (RESULT(I), I = 1, 2)
      IF (RESULT(1) .EQ. PASS) GO TO 100
          WRITE(6, 1000)
          GO TO 200
  100 CONTINUE
          WRITE(6, 2000)
  200 CONTINUE
      IF (RESULT(2) .EQ. FAIL) GO TO 300
          WRITE(6, 1000)
          GO TO 400
  300 CONTINUE
          WRITE(6, 2000)
  400 CONTINUE
 1000 FORMAT(1HF, $)
 2000 FORMAT(1H., $)
 3000 FORMAT(2A1)
      RETURN
      END



The next TDD step is to write our minimal code that will compile, and cause the test to fail:



C
C WRITES OUT '.' IF THE ARGUMENT IS .TRUE.
C 'F' OTHERWISE
      SUBROUTINE RESOUT(A)
      LOGICAL A
      WRITE(10, 100)
  100 FORMAT(2HXX)
      RETURN
      END



We run it, and see the expected output:


FF



Next, we remove the hard-coded unit (10), and take it an argument to the routine, and revise the code to make the tests pass (I actually do these in separate steps, but from the last post you get the idea of TDD, so I'm picking up the pace a bit):



C
C WRITES OUT '.' IF THE ARGUMENT IS .TRUE.
C 'F' OTHERWISE
      SUBROUTINE RESOUT(A, OUTPUT)
      LOGICAL A
      INTEGER OUTPUT
      IF (A) GO TO 200
          WRITE(OUTPUT, 100)
  100     FORMAT(1HF, $)
          GO TO 400
  200 CONTINUE
          WRITE(OUTPUT, 300)
  300     FORMAT(1H., $)
  400 CONTINUE
      RETURN
      END


And the output becomes:


..


indicating that both of the tests now pass.

Now, we can leave this version as it is, and some purists would say we should. However, those purists would be wrong. As it stands, RESOUT replicates the logic of RESCHR (choosing which character to print, based on the value of the LOGICAL argument A. The replication is a violation of the "Don't repeat yourself" rule (know as DRY, from The Pragmatic Programmer). There is no reason to have that code twice, so we modify RESOUT to make use of RESCHR routine that we developed last time. So, our final version of RESOUT becomes:



C
C WRITES OUT '.' IF THE ARGUMENT IS .TRUE.
C 'F' OTHERWISE
C
C LOGICAL A: WHETHER OR NOT THE TEST PASSED
C INTEGER OUTPUT: THE UNIT TO WRITE THE RESULT TO
C
      SUBROUTINE RESOUT(A, OUTPUT)
      LOGICAL A
      INTEGER OUTPUT
      INTEGER RESCHR
      INTEGER OUTCHR
      OUTCHR = RESCHR(A)
      WRITE(OUTPUT, 100) OUTCHR
  100 FORMAT(A1, $)
      RETURN
      END


We run this, and again the tests pass.
Finally, we tighten up the test code:



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


Wednesday, December 30, 2009

Creating A Fortran Unit Testing Framework (part 3): The First Routine

The Form of Our Assertions


We’ll begin by implementing ASRTT. However, it will take us a bit of time to get there. One point that wasn’t made clear in the previous post is whether our assertions are subroutines or subroutine functions. The main difference is that subroutine functions return a value, while subroutines do not. Functions are used to return a single value, while subroutines are return no result. Subroutines are also used when we want to return multiple values. Fortran uses pass by reference semantics.

In our case, the chief difference is one of usage:

  1. LOGICAL OK
    OK = ASRTT(.TRUE.)
  2. CALL ASRTT(.TRUE.)
  3. LOGICAL OK
    CALL ASRTT(.TRUE., OK)

In the first and third versions, the logical variable OK holds the result of the test. In the second version, we ignore the result of the test; the subroutine doesn’t return any value.

The first form seems more natural in terms of the intent of the assertion-—returning .TRUE. or .FALSE. according to the assertion. However, Fortran requires that we always assign the result of the function call to some variable. In most other languages, we can ignore the returned value and write

ASRTT(.TRUE.).

This won’t compile in Fortran; we have to assign the result:

OK = ASSTT(.TRUE.)

The third format removes the need to assign the result. However, it makes the signature of the subroutine more complicated, requiring the user to add a LOGICAL variable to the list of arguments.

The second form is closest to the usage in xUnit frameworks. We generally process the test outside of the main flow (either by updating a data structure or by throwing an exception). Handling the outcome of the test is the work of the framework, and shouldn’t intrude into the user’s code by requiring a dummy variable to accept the return value from a function.

I went back and forth several times, but eventually selected the second form. It is closest to other xUnit frameworks. It has the simplest semantics in using the framework, and so is the least intrusive to the user. Finally, it reflects a proper division of labor by pushing the complication of handling the result of the assertion onto the testing framework where it belongs.

Test-Driven Development

I’m going to take my own advice and write unit tests as we develop the framework. Each aspect of the framework will have one or more tests associated with it, verifying that it works. These tests will be written before each routine is added, a practice called “Test-driven development” (TDD). The idea behind TDD is that writing the tests before the code gives two advantages:

  1. We make sure that the code implements only the functionality that is needed. If it isn’t required to make one of the tests pass it doesn’t get written.
  2. By forcing us to write code that exercises the routines that we are developing, we create an application programming interface (API) that makes sense from the outside, and therefore is simpler to use. An API is the collection of subroutines and functions that can be called by outside code.
  3. We have a suite of tests that allow us to refactor with confidence. Our test suite will need maintenance, just like any other code, and so we are kind to ourselves by writing those tests now.
  4. Writing the test at the same time serves as a form of documentation. The tests show how the framework’s API is intended to be used.
  5. Writing the tests at the same time as the code, the tests are in sync with the code; we don’t have to recall what we were thinking days (or weeks, or months) later.

The philosophy of TDD goes beyond what we will explore here. There are many who feel that TDD is an inappropriate name. They have rechristened it “Behavior-driven development,” to shift focus from the “tests” to the specification of the “behavior” of the code being written. This difference in focus leads to some interesting differences in outlook and focus. I encourage you to take a look at Dan North’s blog introducing BDD, and the behavior-driven development site (note the spelling "behaviour", rather than the American "behavior"). There are several open source projects dedicated to BDD. The oldest/best known are JBehave and easyB for Java, and RSpec for Ruby are a few of the most active. There is also a book coming out on RSpec (currently available in "beta") that is well-worth reading.


Implementing RESCHR Using TDD

The simplest, most basic piece of functionality that we want is to print out “.” if the test has passed, and “F” if the test has failed. So how do we test that? We break it into 2 pieces:

  • A routine that chooses “.” If it receives a .TRUE. argument and “F” if the argument is .FALSE. This second routine will then call the first routine with the correct character.
  • A routine that prints out the results of the test

We begin by assuming that we have a function that does what we want: RESCHR (for result character). It takes a LOGICAL and returns the correct character. The final twist is that FORTRAN 66 doesn’t have characters. Instead, we have to encode the character as a Hollerith constant in an integer, using something like



J = 1HF


We begin by writing a test to verify that expectation.




      PROGRAM TCASES

      CALL TSTCHR

      STOP

      END

C

C TEST CASE FOR THE ROUTINE THAT RETURNS THE CORRECT

C CHARACTER TO PRINT OUT

      SUBROUTINE TSTCHR

      INTEGER RESCHR

      INTEGER ACTUAL

      INTEGER EXPECT

      EXPECT = 1H.

      ACTUAL = RESCHR(.TRUE.)

      IF (EXPECT .EQ. ACTUAL) GO TO 100

      WRITE(6, 1000)

      GO TO 300

  100 CONTINUE

      WRITE(6, 2000)

  300 CONTINUE

      EXPECT = 1HF

      ACTUAL = RESCHR(.FALSE.)

      IF (EXPECT .EQ. ACTUAL) GO TO 400

      WRITE(6, 1000)

      GO TO 500

  400 CONTINUE

      WRITE(6, 2000)

  500 CONTINUE

 1000 FORMAT(1HF, $)

 2000 FORMAT(1H., $)

      RETURN

      END




Basically, we check that the integer returned matches our expectation. The return value ACTUAL is compared to EXPECT. If they agree, a "." is printed; if they don’t agree "F" is printed. The WRITE statement uses a hardcoded reference to unit 6 (the default output), which is the screen on my laptop. [1].

The next step in TDD is to do the minimum possible to get this to compile and see the test fail. This step seems silly. I know the test is going to fail. However, when we are testing more complex code, I have had tests that should fail pass. It is rare, but always provides extremely valuable feedback; there is something important that I don’t understand, and I need to figure that ASAP. Anyway, the minimal code is:




C

C RETURNS THE CHARACTER '.' IF THE ARGUMENT IS .TRUE.

C 'F' OTHERWISE

      INTEGER FUNCTION RESCHR(A)

      RESCHR = 1HX

      RETURN

      END


So, I compile and run, and the test does indeed fail; I get



FF



as output. The next step is to get the test to pass. We change RESCHR:




C

C RETURNS THE CHARACTER '.' IF THE ARGUMENT IS .TRUE.

C 'F' OTHERWISE

      INTEGER FUNCTION RESCHR(A)

      LOGICAL A

      IF (.NOT. A) GO TO 100

        RESCHR = 1H.

        GO TO 200

  100 CONTINUE

        RESCHR = 1HF

  200 CONTINUE

      RETURN

      END


And we are rewarded with passing tests:


..



(trust me, it is much more exciting in person…)

The final step is to tighten up the code—refactor. I pull the constants 1HF and 1H. into variables, to make the code more communicative:




C

C RETURNS THE CHARACTER '.' IF THE ARGUMENT IS .TRUE.

C 'F' OTHERWISE

      INTEGER FUNCTION RESCHR(A)

      INTEGER PASS, FAIL

      DATA PASS/1H./, FAIL/1HF/

      LOGICAL A

      IF (.NOT. A) GO TO 100

        RESCHR = PASS

        GO TO 200

  100 CONTINUE

        RESCHR = FAIL

  200 CONTINUE

      RETURN

      END




I run the tests, and both still pass. The other piece is to tighten up the test code as well. We are going to be running this a lot, so we might as well be nice to ourselves.




      PROGRAM TCASES

      CALL TSTCHR()

      STOP

      END

C

C TEST CASE FOR THE ROUTINE THAT RETURNS THE CORRECT

C CHARACTER BASED ON THE VALUE OF THE TEST

      SUBROUTINE TSTCHR()

      INTEGER RESCHR

      INTEGER I

      INTEGER PASS

      INTEGER FAIL

      INTEGER ACTUAL

      INTEGER EXPECT

      LOGICAL VALUES

      DIMENSION EXPECT(2)

      DIMENSION VALUES(2)

      DATA (VALUES(I), I=1,2)/.TRUE., .FALSE./,

     &    (EXPECT(I), I=1, 2)/1H., 1HF/,

     &    PASS/1H./, FAIL/1HF/

  DO 300 I = 1, 2

      ACTUAL = RESCHR(VALUES(I))

      IF (EXPECT(I) .NE. ACTUAL) GO TO 100

        WRITE(6, 200) PASS

        GO TO 300

  100 CONTINUE

        WRITE(6, 200) FAIL

  200 FORMAT(A1, $)

  300 CONTINUE

      RETURN

      END




There are three things about this code that may seem unusual. One is the empty parentheses after the call to TSCHR. This is not idiomatic Fortran. However, I like to do it because it is consistent with other languages, and makes the calls stand out more in the code. Secondly, I have put each variable’s definition on a separate line. This makes the listings somewhat longer, but also makes it easier for me to find each variable, or to change their type if I need to. Similarly, the DIMENSION statements are on their own line, for the same reason. These are simply my stylistic, idiosyncracies; do whatever feels right for you.

One thing I do suggest is that you explicitly declare every variable, rather than relying on Fortran’s implicit typing. I have been bitten by this repeatedly, and strongly recommend it. Even more useful is the IMPLICIT NONE in later versions of the language. Why? When (not if) I misspell a variable’s name, the compiler blissfully ignores my blunder and creates a new variable. These kinds of typos can be a nightmare to track down, because I tend to see what I intended to type, rather than what is actually there.

We looked at whether to make our assertions functions or subroutines. I somewhat arbitrarily opted for subroutines, largely because Fortran demands that I assign the results of a function to a variable. I find that an annoying intrusion, so I went with subroutines

We took a look at test driven-driven development. Here the tests are written before the actual code. In my humble opinion, TDD has several advantages, but the two that are most important to me are:

  • creating the tests forces me to think through what the routine should do. If I can’t figure out how to test it, then I don’t understand it well enough to code it. The example we looked at was trivial, but as we get into more complicated cases, my experience is that it is well-worth the effort to figure out what the code should do before I write it.
  • TDD also makes us look at the code from the point of view of a user of our routines (from the outside), rather than focusing simply on how they work (from the inside). This seems like a trivial difference, but I have found that it consistently helps me write better designed code.

Finally, we used our knowledge of TDD to create our first tiny piece of the framework, the RESCHR subroutine. We

  1. Wrote the test.
  2. Wrote the minimum code to get the test to run and fail.
  3. Write the minimum code to get the test to pass.
  4. Refactored the framework code to make it cleaner, ensuring all along that the tests still passed.
  5. Refactored the test code, again making sure that all of the tests passed.

Next time we’ll develop a bit more, reinforcing these steps in a slightly more complicated setting.



[1] Unit 6 is typically mapped to the default output device, and I will assume throughout this series that we are using a version of Fortran that allows us to specify file names and descriptors directly. If you are working in a different environment (IBM mainframe for example) you know only too well that you have to map the units in a different way, such as JCL. You have my deepest condolences.

Sunday, December 27, 2009

Creating A Fortran Unit Testing Framework (part 2)

Today we'll take a quick look at the existing open source frameworks for unit testing Fortran code. I'll briefly mention why none of these exactly fits the needs of our problem. Finally,we'll begin designing the skeleton of our framework.

Existing Possibilities for a Fortran Testing Library

There are a few open source alternatives for testing Fortran available at the time of this writing; the Wikipedia entry for unit testing frameworks lists four:

  1. FRUIT,
  2. FUnit,
  3. Ftnunit, and
  4. pfUnit.

The first two projects (FRUIT and FUnit), rely on the Ruby language. While Ruby is wonderful for many things, using a framework based on it requires that the language be installed and that the developer has at least basic familiarity how it works (installing Ruby Gems, etc.).

Ftnunit is closest in spirit to what we want. It is part of flibs. Unfortunately it is written in Fortran 90, which makes it less than ideal for our goal of testing old Fortran as we modernize it. Mixing old and modern code gives many compilers conniptions. Some features found in FORTRAN IV or FORTRAN 66 have been removed from the language. I know little of pfUnit. It appears to be under active development (the latest upload was 89 days ago from 12/27/2009). However, there are no files available for download thus far.


Beginning the Framework

I’ve decided to begin with Fortran 66. It was the first version to be standard (ANSI), contains most of the oldest and creakiest features. It is also weird in ways that make it a cool technical challenge from a coding perspective.


xUnit Frameworks

The vast majority of testing frameworks are based on a Java framework called JUnit[www.junit.org], which was in turn developed based on and older framework written in the SmallTalk language called SUnit. JUnit has been re-written (“ported”) to a huge variety of languages. Wikipedia contains one list; another can be found on Ron Jeffries site in the table titled “Unit Tests”. Collectively these implementations are termed “xUnit”, because of the tendency to prefix “unit” with some indication of the language that it is written in.

At its root, xUnit is built on the concept for an assertion; we assert that some property of the code is correct. The xUnit framework then evaluates that assertion. If it is true, the test passes. If it is false, the framework signals that it fails [1]. The traditional way of signaling that a test passed is to print a period (“.”); the way of signaling a failure is to print an “F” to the screen. Thus, a collection of 10 tests might display:

….F…..

In this case, the fifth test failed.

Unit tests are meant to evaluate very small parts of the code. Thus a unit test for the ABS intrinsic function might be

assert (1, ABS(-1))

The first number is the expected value, and the second number is what we are testing.

In object-oriented languages, each test typically evaluates a single function/subroutine (also termed “methods”). These assertions are wrapped in a function. Each function can contain one or more assertions (whether a function should contain more than one assertion is an issue that sparks near-religious debate). Each test function is called, and produces a single “.” or “F”. Taking into consideration the nature of older FORTRAN, we will write out a “.” or “F” based on the result of each assertion.


Assertions

The names of the assertions will follow the xUnit conventions as closely as possible. However, FORTRAN 66 only allows 6 characters per name, which limits our ability to choose expressive names. Furthermore, many programming languages are case sensitive, which allows us to be more expressive. We can combine multiple words, with the convention that a capital letter indicates a new word (called “camel case” because the capital letter in the middle pokes up like a camel’s hump). So, it we want to create a function that asserts that its logical argument is true, we would call it assertTrue(arg). Older FORTRAN is not case-sensitive, and is typically written in all capital letters, which limits us even further. We will adopt the convention that the root name for “assert” will be abbreviated “ASRT”. That leaves us two letters to describe the assertion. The first letter traditionally describes the kind of argument a function takes:

  • I - INTEGER
  • A - REAL
  • D - DOUBLE PRECISION
  • C - COMPLEX
  • L - LOGICAL

Our first two assertions will simply determine whether their argument is true or false. The final letter will convey our expectation, “T” for .TRUE., “F” for .FALSE.

  • ASRTT(arg) asserts that the LOGICAL arg is .TRUE.
  • ASRTF(arg) asserts that the LOGICAL arg is .FALSE.

The next set of assertions will compare an expected value with the actual value to see whether they two are equal. The final letter of these assertions will be “E”, to convey we are testing for equality. The first letter will reflect the type of the argument. So:

  • LASRTE(EXPECT , ACTUAL) asserts that the LOGIAL value ACTUAL is equal to the value that we EXPECT.
  • IASRTE(EXPECT , ACTUAL) asserts that the INTEGER value ACTUAL is equal to the EXPECT value.
  • AASRTE(EXPECT, ACTUAL, EPSILN) asserts that the REAL value ACTUAL is equal to the EXPECT value. Due to the inherent imprecision of floating point numbers, we do not actually compare the two values for equality. Instead we compute whether ABS(EXPECT – REAL) .LE. EPSILN, where EPSILN is a small positive number, such as 1.E-4 or 1.E-6.
  • DASRTE(EXPECT, ACTUAL, EPSILN) asserts that the DOUBLE PRECISION value ACTUAL is within EPSILN (DOUBLE PRECISION) of the EXPECTED value (see the description of AASRTE).
  • CASRTE(EXPECT, ACTUAL, EPSILN) asserts that the COMPLEX value ACTUAL is within EPSILN (COMPLEX) of the EXPECT value. Determining “within EPSILN” for COMPLEX numbers is a bit tricky. We let the user choose, comparing
    ABS(REAL(EXPECT) – REAL(ACTUAL)) .LE. REAL(EPSILN) and
    ABS(IMAG(EXPECT) – IMAG(ACTUAL)) .LE. IMAG(EPSILN)

  • If both are true, the assertion passes; if either is false, it fails.
In the next installment, we’ll look at implementing each of these assertions. I’ll also introduce test-driven development, a style of programming that leads to very robust code with a minimum of errors.



[1] Previous versions of JUnit (e.g., 3.8) contained a third outcome "E" for error/exception. This signaled a problem with the code, often a compilation problem or unexpected runtime error. The most recent version (4) of JUnit removed "E"; only "." and "F" are outcomes of tests.

Saturday, December 26, 2009

Creating A Fortran Unit Testing Framework (part 1)

I’ve decided to develop a simple unit testing framework for Fortran[1]. The next several posts will document how the library is built, what decisions I’m faced with, and how they are resolved. At the end of the series, we will have a library that we can use to help us to develop new code, and to modernize existing code.

Here’s the problem that started the whole thing off. I am working on a series of simulations of the distribution of a statistic. We have derived the asymptotic distribution of the statistic, and want some idea of what sample size is sufficiently large to allow us to use the result. The distribution in question involves the sum of weighted chi-squared variates. This isn’t simple (or obvious) to compute, and there aren’t easily available, ready-made implementations to grab off the shelf and use. Moreover, I don’t much care about how to compute this quantity. It is a necessary piece of a larger puzzle, but not a piece I care much about.

Doing the obvious web searches yielded a couple of good alternatives. One in particular on Statlib seemed to be just perfect solution. The problem was that the original was in ALGOL or PASCAL, neither of which I work in on a daily basis. I could do the translation into something else (Fortran or Java or C# or any of the other, more modern languages that I know), but I am very leery of introducing a bug during the translation. A bit more web work revealed that the author of the original article had already done a translation into Fortran. So I downloaded it, happy to be able to move on to my real question. But, when I opened up the file, I found that it was written in an old version of Fortran, one that my compiler wasn’t going to compile without a lot of complaints, if it compiled it at all. So, same problem, different language. SPAM!

Modernizing Old (“Legacy”) Code

There is a lot of legacy code out there, much of it written in ancient versions of Fortran. I have code that I want to use that is written in FORTRAN 66, and there is even FORTRAN IV (or FORTRAN II!) floating around. A quick Google search also reveals a small but ongoing problem of being handed code that works but is written in an archaic version of Fortran.

As my experience demonstrates, finding one of these examples is always a pain. What starts as joy over finding a subroutine or library that does what you want turns into despair as your realize that you can’t use it. At this point, you know you have to convert it to a more modern version or the language (Fortran 95 or 2003) to incorporate into your program. Or, if you want to translate it into another language, you have the same problems, plus the differences between how one does things in Fortran and how they are done in your language of choice.

In general, these algorithms are sound, but the code often won’t even compile under a modern compiler. For example, the venerable Hollerith constant was deprecated in Fortran 90, and removed from the language entirely in Fortran 95.

So what to do? One possibility (if the code is based on an article in a professional journal or conference paper) is to roll up your sleeves, dig into the equations/derivations and write a program in your language of choice. While this certainly works, it defeats the whole benefit of finding the solution to our problem. In general, this solution requires that we deeply understand the logic underlying the original implementation, which is a lot of work that feels unnecessary, and is usually tangential to the real goal of our study.

The other alternative is to re-write the code in a modern version of the language. This always has the risk of introducing an error into the code. Because we intend to treat this code pretty much as a black box (we send values in, and new values come out, and we don’t much want to know how it does its work). We don’t want to burrow into the logic.

This is a well-known problem in professional (as opposed to academic/scientific) computing. Companies rely on code written years (or in some cases, decades) before. Often these programs are key to the business’ functioning, perhaps handing all of its billing, or being the central to a key inventory control system. The function often isn’t sexy, but it is vital to the company’s survival. Those who are old enough to have paid any attention during the late 1990’s will recall the “Y2K” problem. This was the classic maintenance nightmare. Code that was often decades old had to understood and modified. The original programmers had often retired (or died).

The defnitive book on dealing with this problem is "Working Effectively with Legacy Code" by Michael Feathers. It is brilliant, and well worth 10 times whatever Addison-Wesley is charging for it. In a nutshell, Feathers describes legacy code as code without tests. The ideal is to have a blanket of tests completely cover the code we want to change. Each test is small, and test a relatively small portion of the code. These are called "unit tests." We also want tests of the overall function ("functional tests"), but unit tests give us a fine-grained picture of what the code should do. Feathers' book is dedicated to ways that we can create unit tests.

Refactoring

The goal of modernizing any program is to change its internal structure (e.g., to update to a more modern version of a language). But as we do this restructuring, we want to insure that we don't change the function of the code. This process of changing the internal state while maintaining the external behavior is called "refactoring." One of the key ideas in refactoring is that we make very small changes, and verify that we haven't broken anything after each step. To do this, we need a good set of unit tests.

But why is this important? With a good set of unit tests, we can safely make changes to the code. If our change introduces an error, running the unit tests will let us know; a passing test will now fail. Because we have only made a small change since the last time we ran the tests, we know exactly what the change was that introduced the error. Fixing the error we introduced will make all of tests pass again.

To summarize:

  1. We have legacy code that we want to convert to a modern version of the same language (Fortran)
  2. Modernizing the code is a form or refactoring. We want to change the internal
  3. To refactor the code with confidence, we need to create a good set of unit tests. Each unit test examines a small piece of functionality; the set of tests provides good coverage of the all of the functionality of the legacy code.
  4. In refactoring, we proceed by making a series of very small changes to the code. After each, we run the tests. If we introduce a bug, one or more of the tests will fail. Because we have made only small changes, we can quickly identify the bug. Once we've fixed it, all of the tests will pass again.
  5. My web surfing hasn't turned up a good unti testing framework for old Fortran, so I'm going to write one. I'll document the development here, describing the decisions made and why I made them.
Next time, I will take a look at existing unit testing frameworks for Fortran, and start developing my framework.


[1] Fortran v. FORTRAN: The original name of the language was "FORTRAN", an abbreviation fo FORmula TRANslation. The name was written all in capital letters. As of Fortran 90, the name of the language has been changed to "Fortran", with only the first letter capitalized. I'll use Fortran when speaking generically about the language. When I'm speaking of a specific version, I will use the correct capitalization for that version.