Using NAMELIST to create a "Capture and Replay" unit test for a routine.
This is extracted from the results of specially processed files that largely automate the play and replay steps. Hopefully I will get some time to make a simpler example that shows this powerful unit testing concept using NAMELIST groups!
The NAMELIST names should probably be $PROCEDURENAME_in and $PROCEDURENAME_out
Cheating a bit and using a sample file that was run through a preprocessor, but it is shown here expanded to standard Fortran. It is usually a good idea to have the writing of the file done with statements that are conditionally compiled, as the I/O can impact runtimes on heavily called routines.
! Generated with preprocessor directives of the form (preprocessor not provided) ... ! $CAPTURE START name [-echo] ! $CAPTURE NAMELIST IN|OUT|INOUT variable-declaration ! put back to seperate lines because of line length ! $CAPTURE IN ! can have significant timing impact, of course ! $CAPTURE OUT ! can have significant timing impact, of course ! $CAPTURE FINISH [name] ! $CAPTURE TEST name ! ! Recently being rewritten to handle contained routines, modules, user-defined types, optional arguments program testit_exe real :: x, y, z, value logical testit testit=.false. ! normal calls that do not generate a replay file with testit=.false. call sub(10.0,20.0,z,value) testit=.true. ! actually environment variable called CAPTURE ! generate a test file by calling routine with testit=.true. ! this would normally be output saved from running multiple ! cases of the actual application call sub(10.0,20.0,z,value) call sub(10.0,-20.0,z,value) call sub(0.0,0.0,z,value) call sub(-11.0,0.0,z,value) ! replay the calls and compare them to the numbers you just got ! normally of course, you would be reading a template file call capture_and_play_sub() contains subroutine sub(x,y,z,value) !$CAPTURE START SUB implicit none !$CAPTURE NAMELIST IN real,intent(in) :: x real,intent(in) :: x; namelist /sub_in/ x !$CAPTURE NAMELIST IN real,intent(in) :: y real,intent(in) :: y; namelist /sub_in/ y !$CAPTURE NAMELIST INOUT real :: z real :: z; namelist /sub_in/ z;namelist /sub_out/ z real :: check !$NAMELIST OUT real,intent(out) :: value real,intent(out) :: value; namelist /sub_out/value !========================= !$CAPTURE IN if(testit)then capture_in : block ! namelist cannot be declared here integer :: io logical :: ifopened inquire(file='capture_and_play_sub',opened=ifopened,number=io) if(.not.ifopened)then open(newunit=io,file='capture_and_play_sub',position='append') endif write(io,nml=sub_in) end block capture_in endif !========================= ! actual routine check=x**2+y**2 if(check.ge.0)then value=sqrt(check) z=z+1 else z=-1 endif !========================= !$CAPTURE OUT if(testit)then capture_out : block ! namelist cannot be declared here integer :: io logical :: ifopened ! duplication not required if user uses correctly if io defined globally inquire(file='capture_and_play_sub',opened=ifopened,number=io) if(.not.ifopened)then open(newunit=io,file='capture_and_play_sub',position='append') endif write(io,nml=sub_out) end block capture_out endif !========================= !$CAPTURE FINISH SUB end subroutine sub !$CAPTURE TEST SUB !========================= subroutine capture_and_play_sub implicit none real :: x real :: y real :: z real :: value integer :: ios, igood, ibad, io logical :: answer, ifopened namelist /sub_in/ x,y,z,value real :: z__ real :: value__ namelist /sub_out/ z,value character(len=255) :: message !set environment variable testit to FALSE igood=0 ibad=0 testit=.false. inquire(file='capture_and_play_sub',opened=ifopened,number=io) if(.not.ifopened)then open(newunit=io,file='capture_and_play_sub') else rewind(unit=io) endif do message='' read(io,nml=sub_in,iostat=ios,iomsg=message) if(ios.ne.0)write(*,*)'IOSTAT=',ios,trim(message) if(ios.ne.0)exit call sub(x=x,y=y,z=z,value=value) ! requires interface to use names but not order-dependent z__=z value__=value read(io,nml=sub_out,iostat=ios,iomsg=message) if(ios.ne.0)write(*,*)'IOSTAT=',ios,trim(message) if(ios.ne.0)exit ! call compare function instead of actual equality test as appropriate answer=all([z.eq.z__,value.eq.value__]) if(answer)then igood=igood+1 else ibad=ibad+1 write(*,*)'failed test ',igood+ibad, z,z__,value,value__ endif enddo if(igood.gt.0.and.(igood.eq.ibad))then write(*,*)'*sub* tested',igood,' failed ',ibad elseif(igood.eq.0)then write(*,*)'*sub* untested' else write(*,*)'*sub* tests passed',igood,' failed ',ibad endif end subroutine capture_and_play_sub end program testit_exe
The input file intentionally has two errors in it for demonstration purposes. Hopefully the template would have good answers!
&SUB_IN X= 10.0000000 , Y= 20.0000000 , Z= 2.27811682E+26, / &SUB_OUT Z= 2.27811682E+26, VALUE= 22.3606796 , / &SUB_IN X= 10.0000000 , Y= 20.0000000 , Z= -2.95650935E-33, / &SUB_OUT Z= 1.00000000 , VALUE= 22.3606796 , / &SUB_IN X= 10.0000000 , Y= -20.0000000 , Z= 1.00000000 , / &SUB_OUT Z= 2.00000000 , VALUE= 22.3606796 , / &SUB_IN X= 0.00000000 , Y= 0.00000000 , Z= 2.00000000 , / &SUB_OUT Z= 3.00000000 , VALUE= 0.00000000 , / &SUB_IN X= -11.0000000 , Y= 0.00000000 , Z= 3.00000000 , / &SUB_OUT Z= 4.00000000 , VALUE= 11.0000000 , / &SUB_IN X= 10.0000000 , Y= 20.0000000 , Z= -1.72845205E-22, / &SUB_OUT Z= 1.00000000 , VALUE= 22.3606796 , / &SUB_IN X= 11.0000000 , Y= -20.0000000 , Z= 1.00000000 , / &SUB_OUT Z= 2.00000000 , VALUE= 22.3606796 , / &SUB_IN X= 0.00000000 , Y= 0.00000000 , Z= 2.00000000 , / &SUB_OUT Z= 3.00000000 , VALUE= 0.00000000 , / &SUB_IN X= -11.0000000 , Y= 0.00000000 , Z= 3.00000000 , / &SUB_OUT Z= 4.00000000 , VALUE= 11.0000000 , /
Intentionally editted the test input and put two errors in it so the test would look more interesting ...
failed test 7 2.00000000 2.00000000 22.3606796 22.8254242 failed test 9 2.00000000 4.00000000 22.3606796 11.0000000 IOSTAT= -1 End of file *sub* tests passed 7 failed 2