A look at FORTRAN unit test frameworks
Posted on 22 July 2014
A look at FORTRAN unit test frameworks
By Mike Jackson, Software Architect.
As part of our open call collaboration with TPLS I was to develop a suite of unit tests. TPLS is written in FORTRAN and while there are de-facto standard unit test frameworks for Java (JUnit) or Python (PyUnit), for FORTRAN there are none. In this blog post I look at the test frameworks that are available for FORTRAN, compare two, FRUIT and pFUnit, and explain why I opted to use FRUIT for TPLS.
There are a few test frameworks available for FORTRAN listed on Wikipedia and Fortran Wiki. These include:
Name | Implementation | Licence | Active |
---|---|---|---|
FORTRAN Unit Test Framework (FRUIT) | FORTRAN with Ruby extensions | BSD-style | 2006-2014 |
pFUnit | FORTRAN with Python extensions | NASA open source licence 1.3 | 2014 |
ObjecxxFTK - Objecxx Fortran ToolKit | FORTRAN with Python extensions | Commercial licence for a "modest licensing fee" | 2014 |
flibs | FORTRAN with Tcl Tk extensions | BSD-style | 2005-2008 |
Two others, FUnit (last active 2009) and FortUnit (last active 2004) had no code available and seem to be dead projects.
Out of the four available, I decided to look at FRUIT and pFUnit in more detail, as they are both actively maintained, unlike flibs, and are available under an open source licence, unlike ObjecxxFTK (See our guide on "Choosing the right open source software for your project" for guidance on choosing between alternative offerings.)
I'll discuss FRUIT and pFUnit shortly, after a digression to comment on...
Why are Ruby, Python or Tcl/Tk needed?
Unit test frameworks for Java and Python require only Java or Python to compile and run unit tests written to use them. So why do the FORTRAN test frameworks require these additional languages?
Java and Python support reflection, by which code can introspect upon itself to find functions or classes at runtime. Test frameworks such as JUnit or PyUnit use reflection to automatically find test classes or methods to run. They might look for functions named in a special way (e.g. prefixed by "test_"), methods that are members of a class that sub-classes a unit test class provided by the framework, or functions that are marked up by a framework-specific annotation (e.g. "@test").
For languages that do not support reflection, such as FORTRAN, or C, test frameworks require a developer to write test "driver" functions to explicitly call each of their test functions. This incurs a maintenance overhead - every time a developer writes a new test function, they need to remember to add it to a test driver function too.
Test framework developers write extensions in Ruby, Python or Tcl/Tk to pre-process FORTRAN code and identify test functions that are marked up according to a framework-specific convention. They then automatically generate FORTRAN code that implements these driver functions.
FORTRAN Unit Test Framework (FRUIT)
Here, I provide an overview of FRUIT, based on my experiences with FRUIT 3.3.4 downloaded on 02/06/2014.
FRUIT consists of two FORTRAN files which contain functions can be used to write tests. These contain "assert" sub-routines which can be used for different types of test e.g.
call assert_true(1 == 1, "Boolean test") call assert_false(1 == 1, "Boolean test fails") call assert_equals(123, 123, "Integer equality") call assert_equals(1.23, 1.23, "Double equality") call assert_equals(1.23, 1.3, 0.1, "Double equality within a tolerance") call assert_equals("abc", "abc", "String equality") call assert_equals(a, b, 3, 2, d, "2D array equality within a tolerance")Test functions, which use these assert sub-routines, can be placed within a module e.g.
module twophase_io_fruit_test use fruit subroutine test_assert_examples() ... end subroutine test_assert_examples subroutine test_row_to_grid() ... end subroutine test_row_to_grid subroutine test_row_to_grid_phi() ... end subroutine test_row_to_grid_phi end module twophase_io_fruit_testDevelopers can write their own test drivers e.g.:
program manual_fruit_driver use fruit use twophase_io_fruit_test call init_fruit call test_assert_examples() call test_row_to_grid() call test_row_to_grid_phi() call fruit_summary end program manual_fruit_driverWhen a test driver is compiled along with the test modules and the two FRUIT FORTRAN files, a stand-alone executable is produced. This standalone executable, when run, executes the tests and summarises the results:
$ ./test_fruit_driver Test module initialized . : successful assert, F : failed assert .F.F.F.F.F.F.F........................................................ Start of FRUIT summary: Some tests failed! -- Failed assertion messages: [_not_set_]:Expected [T], Got [F]; User message: [True fails] [_not_set_]:Expected Not [T], Got [T]; User message: [False fails] [_not_set_]:Expected [123], Got [321]; User message: [Equals fails] [_not_set_]:Expected [1.2300000], Got [3.3199999]; User message: [Equals fails] [_not_set_]:Expected [1.2300000], Got [1.3000000]; User message: [Fudge equals fails] [_not_set_]:Expected [abc], Got [cba]; User message: [Equals fails] [_not_set_]:Expected [4.0000000000000000], Got [123.00000000000000]; User message: [2d array has difference, Equals fails] -- end of failed assertion messages. Total asserts : 446 Successful : 439 Failed : 7 Successful rate: 98.43% Successful asserts / total asserts : [ 439 / 446 ] Successful cases / total cases : [ 0 / 0 ] -- end of FRUIT summarySet-up and tear-down functions can be used to perform initialisation and finalisation operations common to all tests within a module:
module twophase_io_fruit_test use fruit subroutine setup() ... end subroutine setup subroutine teardown() ... end subroutine teardown subroutine test_assert_examples() ... end subroutine test_assert_examples subroutine test_row_to_grid() ... end subroutine test_row_to_grid subroutine test_row_to_grid_phi() ... end subroutine test_row_to_grid_phi end module twophase_io_fruit_testTo invoke these requires introducing a new module, termed a "basket" in FRUIT, that invokes the set-up and tear down functions, as well as the test functions themselves e.g.
module manual_fruit_basket use fruit contains subroutine twophase_io_fruit_test_all_tests use twophase_io_fruit_test call setup write(*,'(/A)') " ..running test: test_assert_examples" call set_unit_name('test_assert_examples') call run_test_case(test_assert_examples, "test_assert_examples") if (.not. is_case_passed()) then call case_failed_xml("test_assert_examples", "twophase_io_fruit_test") else call case_passed_xml("test_assert_examples", "twophase_io_fruit_test") end if call teardown ... end subroutine test_twophase_io_fruit_all_tests subroutine fruit_basket call twophase_io_fruit_test_all_tests end subroutine fruit_basket end module manual_fruit_basketIf using a FRUIT basket approach, the driver invokes the functions of the basket e.g.
program manual_fruit_basket_driver use fruit use manual_fruit_basket call init_fruit call init_fruit_xml call fruit_basket call fruit_summary call fruit_summary_xml call fruit_finalize end program manual_fruit_basket_driverThe above driver also contains additional calls that allow an XML report of the tests run and the successes and failures to be output. The XML report conforms to a standard XML schema used by a number of unit test frameworks including JUnit, Python's nosetests or CUnit.
FRUIT can create the FORTRAN test drivers automatically. This requires Ruby, Ruby's Gem package manager, and the Ruby rake build tool. FRUIT uses its Ruby pre-processor to parse a FORTRAN file containing test functions and auto-generate both basket ("fruit_basket_gen.F90") and driver ("fruit_driver_gen.F90") files. The constraints on each test file is that it:
- Ends in the suffix "_test.f90" e.g. "twophase_io_fruit_test.f90".
- Declares a module that ends in the suffix "_test" e.g. "twophase_io_fruit_test".
- Contains subroutines that take zero arguments and have the prefix "test_" e.g. "test_assert_examples".
The auto-generation can be done via running a simple Ruby script within the same directory as the test files. The Ruby script is quite concise, for example:
require 'rubygems' require 'fruit_processor' fp = FruitProcessor.new fp.pre_processCompiling the tests in this scenario needs:
- The file with the tests e.g. "twophase_io_fruit_test.f90".
- Any files needed by these e.g. the code being tested.
- The auto-generated basket and driver files e.g. "fruit_basket_gen.f90" and "fruit_driver_gen.f90".
- FRUIT's two FORTRAN files.
The same test files can be used with the Ruby pre-processor or as part of the simple standalone FORTRAN approach described earlier. The advantage of using the pre-processor is it removes the need to maintain the basket and driver files and keep them up to date.
Some of the issues with FRUIT are that:
- A failed "assert" call does not cause a test function to exit immediately. The remaining lines of the test function are executed. This differs from other test frameworks which stop execution of a test function as soon as an assertion fails.
- A "." is printed when each "assert" call succeeds which can lead to an overwhelming number of "."s being displayed. Other test frameworks print a "." when each test function successfully completes. As FRUIT is open source, commenting out one line can suppress this!
- A large amount of boiler-plate code must be written to support "setup" and "teardown" if there is a desire to avoid using Ruby.
- FRUIT's own examples use Ruby Rake build files, not only to auto-generate the FORTRAN driver code but also to compile all source and test code. I think it is unreasonable to expect existing projects to migrate to a new build tool just to use FRUIT. However, as described, a simple Ruby script can be used to manage the auto-generation of driver code. This can then be run, manually or via a Makefile, for example.
pFUnit
Here, I provide an overview of pFUnit, based on my experiences with pFUnit 2.2 downloaded on 02/06/2014 due to issues with more recent releases and platforms available, described below.
With pFUnit, tests are written in FORTRAN-style ".pf" files with set-up, tear-down and test functions annotated e.g.
module twophase_io_pfunit_test use twophase_initialisation_wave use twophase_io use pfunit_mod implicit none contains @Before subroutine setup() ... end subroutine setup @After subroutine teardown() ... end subroutine teardown @Test subroutine test_assert_example_fail() ... end subroutine test_assert_example_fail @Test subroutine test_assert_fudge_fail() ... end subroutine test_assert_fudge_fail @Test subroutine test_assert_examples() ... end subroutine test_assert_examples end module twophase_io_pfunit_testTest assertions are also expressed via annotations e.g.
@assertTrue(1 == 1, "Boolean test") @assertFalse(1 == 2, "Boolean test") @assertEqual(123, 123, "Integer equality") @assertEqual(1.23, 1.23, "Double equality") @assertEqual(1.23, 1.3, "Double equality within a tolerance", 0.1) @assertEqual("abc", "abc", "String equality") @assertEqual(a, b, "2D array equality within a tolerance", d) @assertEqual(a, b, "3D array equality within a tolerance", d)The ".pf" files are pre-processed into FORTRAN files using pFUnit's Python pre-processor. The auto-generated FORTRAN code does the following:
#include "testSuites.inc"So, developers must also write this "testSuites.inc" file listing the test modules, each with a "_suite" suffix e.g.
ADD_TEST_SUITE(twophase_io_pfunit_test_suite)Compiling the tests in this scenario needs:
- The FORTRAN file auto-generated from the ".pf" file e.g. "twophase_io_pfunitTests_gen.F90".
- Any files needed by these e.g. the code being tested.
- The directory that contains "testSuites.inc".
- pFUnit's own source files, compiled libraries and other files.
Some of the issues with pFUnit are that:
- pFUnit needs a current version of the gfortran compiler, ideally 4.8.3+. Attempting to build the current release of pFUnit on a Scientific Linux 6 machine supporting gfortran 4.4.7 fails.
- However, attempting to use gfortran 4.8.2 gives an execution error also, but this is a known problem.
- pFUnit needs to be built and installed so that its libraries and include files are available. FRUIT, by contrast has no such requirement.
- Developers need to write and maintain not only FORTRAN files but also pFUnit-specific ".pf" and ".inc" files.
Picking FRUIT
For test frameworks based on languages that do not support reflection there is a trade-off between:
- Explicitly writing and maintaining test driver functions, ensuring these call all the test functions and updating these as new test functions are added.
- Using dependencies (e.g. Ruby or Python) outwith the language in which the code and associated tests are implemented.
The former can be a deterrent to writing tests due to the time and effort involved every time a test is written. The latter can prove a deterrent to writing tests in the first place due to the time to set up the additional dependencies required as well as having to learn additional tools, languages or file formats. This is on top of starting to write the tests.
For a software development project, involving software developers, I would recommend pFUnit as it offers a wider range of in-built test functions and generally feels like a more comprehensive and polished product.
However, for TPLS, a research project, where the developers are primarily researchers who may not be used to writing tests I picked FRUIT. FRUIT does not require any non-FORTRAN languages (whether these by Ruby, Python or pFUnit's own syntax) to be learned. It has a lower barrier to uptake.
FRUIT's core functionality is held within its two FORTRAN files, and there is no need to build and install any separate components. This, and its licensing under a BSD-style licence, allows them to be held within TPLS's Subversion repository so that all researchers have access to them when downloading TPLS. Though, I have recommended that TPLS developers keep an eye on FRUIT's repository for updates to its FORTRAN core and the copies in TPLS be updated accordingly, so TPLS can benefit from bug fixes or enhancements to FRUIT.
Code samples of both FRUIT and pFUnit are in the ssi branch of the TPLS SourceForge repository.
If you have any advice, hints and tips concerning unit testing in FORTRAN then please feel free to share these below.