- Move one directory at a time
- Put the name of the directory with spaces in double quotes: eg. 'cd "Documents and settings"\desktop'
- Figure out which is which is to use the "dir" command (which lists the names of the files in the directory). Say I have two folders, "Documents and settings" and "Documents to give to my cat to shred". I can type "dir DOCUME~1*" and the system will return "Documents and Settings", or type "dir DOCUME~2*" and get back "Documents to give to my cat to shred" Notice the aterisk at the end of the commands. Asterisk is a "wildcard" that will match any collection of characters. Using it is especially important for names that have periods in the ("jython-2.5.2"). It is important to include it.
Wednesday, March 31, 2010
Efficient Bulk Processing: The Command Line
Sunday, February 14, 2010
Seedy Random Number Generation
- explicitly provide a seed value
- implicitly have the seed generated for us
- creates a backup copy of the file--"seed.bak",
- reads the N values in the seed file,
- removes the top value, and
- writes N-1 values (without the top value) back to same file (seed.dat) for the next replication's use.
- I can don't have to keep the individual data sets around, reducing the possibility that I'l run into storage problems
- I can re-generate any specific data set by simply using the corresponding seed value
- Finally, when I go to archive the files associated with the study, I only have to save the seed files, secure in the knowledge that I can recreate anything (from a single run to the entire study) at a moment's notice.
Saturday, February 13, 2010
Doing the Replications: (Again, and again and again...)
There are two schools of though there:
- One BIG RUN. It goes for 4 weeks, performs all 50,000 replications, and then prints out a 7. This kind of thing is routinely done in the physical sciences; I've read stories about work on subatomic particles that do this one-long-run approach.
- 50,000 small runs. Then we pull the results from all of the files, and create a big data set.
- I have had the experience of the BIG RUN go for 3 weeks, 6 days, and facilities turns off the power for some needed maintenance. This always results in me saying a long spew of bad words, that my mother would not approve of.
- Somewhere during the process, a few runs hiccup. It may be that iterations fail to converge; that the dataset is defective; something. Now I have 49,936 replications, and no idea what happened to the other 64 runs. And actually, it may be that the 64 runs are trying to tell me something (maybe more interesting that what I thought I was looking for). However, I don't know what dataset produced the problem, why, or anything else. In short, I'm SOL [1].
- There may be something else about the data, like the skewness of the statistic, or the standard error of the estimated standard errors; (it's always weird) that a journal reviewer wants to know about before approving the manuscript. If you've done the BIG RUN, you have little choice but to redo it--more bad words. If I have the dataset of individual runs, I can re-analyze it. Sometimes, the reviewer is on to something, or they may be full of hot air. Either way, I can go back to the data. With the BIG RUN, it's not an option.
- It is easier to monitor the progress. If the conference paper is due in 3 days and I'm only on replication 17, it is time to re-group.
- (and this is my favorite), if I structure the replications so that each of the conditions is executed in round-robin fashion, I can do some preliminary analysis after 10 or 50 replications. Based on these, I can do things like power analyses for the effects I'm looking for and realize that I need more (or fewer!) replications. Or, things may look different than I expected, and I can shut it down, call it a pilot, and go off in another direction.
- Disk space can become a problem,
- You often can't use simulators that are baked into the software,
- Figuring out how to run a commercial program (especially one that is based on a GUI) can pose a major problem,
- With so many tiny pieces, it is easy to get lost in the detail.
- Pulling the results together from lots of individual files can be a major undertaking.
Sunday, January 24, 2010
Fortran Unit Testing Framework (part 6): Removing Some Duplication
- Open a scratch file for our results
- Change the output unit to a scratch file,
- Call the SUBROUTINE that we are testing with the argument .TRUE.
- Call the SUBROUTINE that we are testing with the argument .FALSE.
- Rewind the scratch file
- Read back the results from the scratch file,
- Compare what was written to our expectation, outputting the result character "." or "F",
- Clean up by closing the scratch file and re-setting the output
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
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
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
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:
- 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.
Wednesday, January 6, 2010
Fortran Unit Testing Framework (part 5): Writing Assertions
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.
- 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
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
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
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
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:
- LOGICAL OK
OK = ASRTT(.TRUE.) - CALL ASRTT(.TRUE.)
- 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:
- 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.
- 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.
- 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.
- 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.
- 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:
Finally, we used our knowledge of TDD to create our first tiny piece of the framework, the RESCHR subroutine. We
- Wrote the test.
- Wrote the minimum code to get the test to run and fail.
- Write the minimum code to get the test to pass.
- Refactored the framework code to make it cleaner, ensuring all along that the tests still passed.
- 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.