Code Monkey home page Code Monkey logo

zofu's Introduction

Unit tests

Zofu (Zofu is Object-oriented Fortran Unit-testing) is a framework for carrying out unit testing of Fortran code modules. As the name suggests, it makes use of the object-oriented features of Fortran 2003.

Unit testing with Zofu

Unit testing with Zofu is module-based, so unit tests are gathered into Fortran modules. Typically a unit test module will contain all the unit tests for particular module in the code itself. Each unit test source file should contain only one unit test module.

Each unit test module may contain a number of test subroutines, or cases. Usually a case will test one aspect of a code module, e.g. a particular function or subroutine. Within each case, a number of assertions are made, i.e. tests of whether a specified condition is satisfied or not. A simple example would be testing the output of a function against expected values, for a range of given inputs.

The Zofu unit test type

Zofu provides a unit_test_type derived type for making assertions within the unit test cases. Objects of this type also keep track of how many cases have been run in the test, how many have passed and failed, together with how many assertions have been made, and how many assertions have passed and failed.

Writing test modules using Zofu

Using Zofu and declaring a test

To use Zofu the test module must include a use zofu statement. A unit test object may then be declared, e.g.:

use zofu
type(unit_test_type) :: test

Test case subroutines

This test object is then passed in to each test case subroutine, e.g.:

subroutine test_foo(test)
  ! Tests a foo
  class(unit_test_type), intent(in out) :: test
  ! test code goes here...
end subroutine test_foo

Each test case subroutine must have a name beginning with test_. This convention allows the test module to contain other subroutines which are not tests, but may be called by the test case subroutines.

If a test case subroutine contains a comment with a description of the test case as its first non-blank line, this description will be used in the test output (e.g. if an assertion in the case fails). If no description comment is given, the subroutine name is used for the description.

Making assertions

Within a test case subroutine, assertions may be made using the assert() method of the unit_test_type object. This method takes either a single logical argument, or two arguments which the assertion tests for equality. These two arguments may be of type logical, integer (4- or 8-byte), real, double precision, complex, or character. They may also be arrays of these types, of rank one or two.

For example, the statement:

  call test%assert(OK)

asserts that the logical variable OK (declared in the test case subroutine) is true. The statement:

  call test%assert(x, y)

asserts that the two variables x and y are equal. These variables could be of any of the types listed above.

For floating point (real, double precision or complex) variables, equality is defined up to a given tolerance. Each unit test object has a built-in default tolerance given by its tolerance property. This is a relative tolerance (not absolute) and set by default at 1e-6. For a particular assertion the default tolerance can be overridden by adding a tol argument to the assert() method, e.g.:

  call test%assert(x, y, tol = 1.e-9)

For character variables, leading and trailing blanks are ignored, but case is respected.

The assert() method can also take an optional name argument. This is a character string with a description of the assertion, which is used for output of failed assertions. For example:

  call test%assert(x, y, name = 'test x = y')

Setup and teardown routines

Each module may optionally contain special subroutines called setup and teardown. The setup subroutine is called before any tests are run, and the teardown routine is called after all tests are finished.

These can be used to initialize and finalize global variables or other settings for the test. Unlike the test case subroutines, they do not take the unit test object (or any other variables) as an argument.

Test setup routine

Each module may also contain another optional special subroutine called setup_test, to be called after the test is initialized but before any test cases are run. It can be used for modifying properties of the test, e.g. the default tolerance for floating point equality tests. The test object must be passed in to this subroutine, in the same way as it is passed in to the test case subroutines. In the example below:

  subroutine setup_test(test)

    class(unit_test_type), intent(in out) :: test

    test%tolerance = 1.e-8

  end subroutine setup_test

the default floating point equality tolerance is set to 1e-8.

Driver programs

Once a unit test module has been written, the zofu-driver utility can be used to create the Fortran source code for a driver program which calls all test case subroutines in the test module. The driver program source also contains the appropriate use statements, declares a unit test object, and calls any setup, setup_test or teardown routines at the start and end of the test.

The zofu-driver utility can be called from the command line as follows:

zofu-driver module driver [--mpi]

Here module is the filename of the unit test module, and driver is the filename of the driver source code to be written. (The optional --mpi argument is used for parallel unit tests.)

The driver program can then be compiled and linked to the unit test module and to the Zofu library. When run, the program will return a non-zero error code if the test failed (i.e if any assertions failed).

Output from Zofu

As it runs, a Zofu test program will also write details of any failed assertions to the standard output, together with a summary of all cases and assertions at the end of the test. The output is in YAML format, so it can be redirected to a file and parsed with scripts if required.

Here is the YAML output from an example (failed) test:

failed assertions:
- {"case": "foo", "assertion": "fred", "reason": "value", "values": [1, -2]}
- {"case": "bar", "assertion": "mary", "reason": "value", "values": [7., -7.01], "index": 3, "count": 1}
- {"case": "bar", "assertion": "eric", "reason": "shape", "values": [3, 4]}
cases: {"count": 8, "passed": 7, "failed": 2}
assertions: {"count": 12, "passed": 9, "failed": 3}
passed: false

The YAML output from a failed assertion (in the "failed assertions" array) is itself a dictionary which may contain the following keys:

  • "case": the number or name of the case
  • "assertion": the name of the assertion
  • "reason": "value" if the values being compared were not equal, or "shape" for array values which were not the same shape
  • "values": an array of the two values being compared: for array values, this shows scalar values for the first array indices where the values were not equal
  • "index": for rank-1 arrays, the integer index of the values shown in the "values" field (for rank-2 arrays, a 2-element integer array of indices)
  • "count": for array values, the total number of elements which were not equal
  • "rank": for tests parallelized with MPI, the processor rank of the failed assertion

(Note that the "failed assertions" array will be empty if the test passed.)

At the end of the test two further dictionaries are output, "cases" and "assertions", which summarise the total counts of cases and assertions, as well as how many passed and failed. Finally, the "passed" Boolean value records whether the test passed.

Running multiple tests

If a number of modules are to be tested, zofu-driver can build a separate driver program for each module. The tests can be run using a utility such as meson test or CTest, which will run all the test driver programs and produce summary output for the whole suite of tests.

Some unit testing systems create a single driver program which runs all tests in multiple modules. While this is perhaps simpler, it has the disadvantage that if one test crashes, the entire suite of tests stops and no other tests can be run. By contrast, if there is a separate driver program for each test module, the suite of tests can continue to run in the event of one module crashing. This approach also allows individual modules to be tested without recompiling the test driver program.

Parallel unit tests using MPI

Zofu can test modules that are parallelized using MPI. In this case, there is a modified derived type for the unit test, unit_test_mpi_type (which extends unit_test_type). This is in a separate Zofu module, zofu_mpi, so the test driver program must include the statement:

use zofu_mpi

and the test object is declared as follows:

type(unit_test_mpi_type) :: test

If you use the zofu-driver utility to generate your test driver program then using the --mpi switch will take care of this for you:

zofu-driver module driver --mpi

Note that the test module should still use zofu, and the test case subroutine interface is the same as for serial unit tests:

subroutine test_parallel_foo(test)

  class(unit_test_type), intent(in out) :: test

This works because unit_test_mpi_type extends unit_test_type, so is still of that class. (In fact it is necessary to keep the test case subroutine interface the same for both serial and parallel unit test cases, so declaring the test as class(unit_test_mpi_type) in your parallel test case will not work.)

The setup and teardown routines in each test module should include commands for initializing and finalizing MPI, e.g. mpi_init() in the setup() routine, and mpi_finalize() in the teardown() routine.

Building and installing Zofu

Zofu includes scripts for building and installing using either Meson or CMake.

Using Meson

Building with Meson

A script (meson.build) is included for building Zofu using the Meson build system. This in turn uses the Ninja tool to run the build. Meson and Ninja can be installed using your package manager or via pip. Zofu can be configured by running:

meson build

in the Zofu root directory. This will create and configure a build subdirectory called build (you can substitute a different name if you prefer). By default, a debug build is performed. If you want an optimized release build, you can specify the build type at configuration time, e.g.:

meson build --buildtype=release

Zofu can then be built using:

ninja -C build

Zofu will be built with MPI support (including the zofu_mpi module) if Meson detects that MPI is installed on your machine. There is usually no need to use a wrapper compiler (e.g. mpif90) to build it.

However, if you are building on a system with an unusual compiler and/or MPI library setup (e.g. some types of compute cluster), Meson may not be able to detect MPI. In this case you can use a wrapper compiler. You can specify that you want to use a wrapper compiler by setting the -Dmpi_wrapper_compiler build option to true, and specify the wrapper compiler using the FC environment variable. For example:

FC=ftn meson build -Dmpi_wrapper_compiler=true

will configure the Zofu build to use an MPI wrapper compiler called ftn.

Installing with Meson

Zofu can be installed as follows:

ninja -C build install

By default this will install the Zofu shared library to a standard location, e.g. /usr/local/lib on Linux (for which you will generally need administrator privileges) . If you want to install it somewhere else, you can specify a 'prefix' using the --prefix and --libdir options at configure time, e.g.:

meson build --prefix=/home/bob/ --libdir=lib

would configure Zofu to install into the /home/bob/lib directory. The zofu-driver utility will similary be installed by default to the bin subdirectory after the prefix; the subdirectory name can be changed using the --bindir option.

The Fortran module files (e.g. *.mod for the gfortran compiler) will also be installed (to make them available when building your test code), by default to the include subdirectory after the prefix. If you want to install the module files somewhere else, you can specify the --includedir option at configure time, e.g.:

meson build --prefix=/home/bob/ --libdir=lib --includedir=finclude/zofu

Meson will also write a pkg-config file to make it easier for other software (e.g. your test driver programs) to link to Zofu. The pkg-config file is installed to the pkgconfig subdirectory under the directory where the Zofu library is installed.

Using CMake

Building with CMake

A script (CMakeLists.txt) is included for building Zofu using CMake. CMake can be installed using your package manager. CMake will by default use the make tool to run the build, but can also use Ninja instead if you configure it to do so.

The Zofu CMake build can be configured by running:

mkdir build
cd build
cmake ..

in the Zofu root directory. Zofu can then be built by executing make in the build directory.

Installing with CMake

Zofu can be installed by executing:

make package
make install

in the build directory.

Customizing a CMake build

To specify a release type (Release or Debug for example), pass -D CMAKE_BUILD_TYPE=Debug or -D CMAKE_BUILD_TYPE=Release to cmake. If not specified, Release is assumed.

The installation path for can be specified for each component of Zofu - the zofu-driver executable, the library, Fortran /.mod files, and HTML documentation. The following parameters are optional:

  • ZOFU_BINARY_INSTALL_DIR sets the relative path to zofu-driver under the root install directory. If not specified, it typically defaults to ./bin.
  • ZOFU_LIBRARY_INSTALL_DIR sets the relative path to the library (.a, .dll, .dylib). If not specified, it typically defaults to ./lib; see https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html for details.
  • ZOFU_FORTRAN_MODULE_INSTALL_DIR sets the relative path to the Fortran .mod files; the default is ./finstall/zofu
  • ZOFU_DOCUMENTATION_INSTALL_DIR sets the relative path to the HTML documentation generated by FORD; the default is ./doc/html

These variables are passed to CMake using the -D option. For example:

cd build
cmake -D CMAKE_BUILD_TYPE=Debug \
    -D ZOFU_BINARY_INSTALL_DIR:PATH=debug/bin  \
    -D ZOFU_LIBRARY_INSTALL_DIR:PATH=debug/lib \
    -D ZOFU_FORTRAN_MODULE_INSTALL_DIR:PATH=debug/finclude \
    -D ZOFU_DOCUMENTATION_INSTALL_DIR:PATH=debug/html \
    ..
make
make test
make package

Similarly for Windows:

cd build
cmake.exe -G Ninja -D CMAKE_BUILD_TYPE=Debug ^
    -D ZOFU_BINARY_INSTALL_DIR:PATH=debug/bin  ^
    -D ZOFU_LIBRARY_INSTALL_DIR:PATH=debug/lib ^
    -D ZOFU_FORTRAN_MODULE_INSTALL_DIR:PATH=debug/finclude ^
    -D ZOFU_DOCUMENTATION_INSTALL_DIR:PATH=debug/html ^
    ..
ninja
ninja test
ninja package

More information can be found in contrib/cmake/README.md

Testing Zofu

Zofu has its own unit tests. If you are using Meson, these can be run from a command line in the build directory using the command:

meson test

If you are using CMake with make, the tests can be run using the command:

make test

Running tests with Meson

If you are using Meson to build your code, you can add the tests to your build and have Meson run them. Because the test driver source code files do not exist at configure time, but only after they have been created using zofu-driver, they can be declared in your meson.build file using the configure_file() function.

The Meson build script below builds a shared library from a source file adder.F90, uses zofu-driver to create the driver source for a test module adder_tests.F90 and creates a test driver program from it.

project('adder', ['fortran', 'c'])

zofu = dependency('zofu', required: true)

src_dir = join_paths(meson.current_source_dir(), 'src')

adder = shared_library('adder', join_paths(src_dir, 'adder.F90'))

test_src_dir = join_paths(meson.current_source_dir(), 'test')

test_name = 'adder_tests'
test_src = join_paths(meson.current_source_dir(),
                      'test', test_name + '.F90')
driver_src_name = test_name + '_driver.F90'

test_driver_src = configure_file(
  output: driver_src_name,
  command: ['zofu-driver', test_src, driver_src_name])

test_exe = executable('adder_tests',
                      [test_driver_src, test_src],
                      link_with: adder,
                      dependencies: zofu)
test('adder_tests', test_exe)

After building your code, all the tests can then be run from the build directory as follows:

meson test

Meson will run the tests and output a summary of how many tests passed, based on the return code from each test driver program. You can also run individual tests by adding the test name as defined in the build script:

meson test foo

If your code and tests are parallelized using MPI, you can run the tests in parallel by "wrapping" them with the mpiexec command, e.g.:

meson test --wrap='mpiexec -np 4'

would run all the tests on four processes. For MPI testing you should also declare your tests with the is_parallel option set to false, e.g.:

test(test_name, unit_test, is_parallel: false)

otherwise Meson will by default attempt to run different tests on different processes at the same time.

Documentation

This readme page is the main user documentation. In addition, it is possible to build HTML pages detailing the Zofu library API using the FORD documentation tool. Typing ford doc.md at the command line in the main Zofu directory will generate a doc/ subdirectory containing a main HTML page index.html, which may be viewed in a web browser.

Licensing

Zofu is open-source software, licensed under the GNU Lesser General Public License (LGPL).

zofu's People

Contributors

acroucher avatar apthorpe avatar scivision avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

zofu's Issues

.mod files not installed to --includedir?

I've successfully built zofu and installed it locally using:

cd <zofu project root> # The project root created from git clone
meson build --prefix /home/bob/.local/ --libdir=lib --includedir=finclude/zofu
ninja -C build
ninja -C build install

I confirmed installation by looking under ~/.local (aka /home/bob/.local) and found

~/.local/bin/zofu-driver
~/.local/finclude/zofu
~/.local/lib/libzofu.so
~/.local/lib/pkgconfig/zofu.pc

All looks reasonable except ~/.local/finclude/zofu is an empty directory; I expected to find zofu's .mod files installed there. They appear to be generated properly under <zofu project root>/build/libzofu.so.p

I can manually compile, link, and run test_integer_asserts.F90 with

gfortran check.F90 test_integer_asserts.F90 -L/home/bob/.local/lib -I<zofu project root>/build/libzofu.so.p -o test_integer_asserts -lzofu
LD_LIBRARY_PATH=/home/bob/.local/lib ./test_integer_asserts

Just wondering if there's an issue with ninja install not properly installing the .mod files generated in build/libzofu.so.p to --includedir? If it helps any, I'm running on Linux Mint 19.3 (ubuntu-ish) with the most recent release of meson (0.55.3). Let me know if there are further tests I can do on my end to help resolve this.

ninja failure - meson 0.57.1

ninja -C build fails when using meson 0.57.1. All meson versions work up to 0.57.0. This issue is related to the module files. Here is the log

ninja: Entering directory `build'
[1/35] Compiling Fortran object libzofu.so.p/_tmp_vincentm_zofu_src_zofu_scan.F90.o
FAILED: libzofu.so.p/_tmp_vincentm_zofu_src_zofu_scan.F90.o 
mpifort -Ilibzofu.so.p -I. -I.. -fdiagnostics-color=always -pipe -D_FILE_OFFSET_BITS=64 -Wall -O3 -fPIC -Wno-maybe-uninitialized -Jlibzofu.so.p -o libzofu.so.p/_tmp_vincentm_zofu_src_zofu_scan.F90.o -c /tmp/vincentm/zofu/src/zofu_scan.F90
/tmp/vincentm/zofu/src/zofu_scan.F90:21:6:

   21 |   use zofu_str_utils
      |      1
Fatal Error: Cannot open module file ‘zofu_str_utils.mod’ for reading at (1): No such file or directory
compilation terminated.
[3/35] Compiling Fortran object libzofu.so.p/_tmp_vincentm_zofu_src_zofu.F90.o
FAILED: libzofu.so.p/_tmp_vincentm_zofu_src_zofu.F90.o 
mpifort -Ilibzofu.so.p -I. -I.. -fdiagnostics-color=always -pipe -D_FILE_OFFSET_BITS=64 -Wall -O3 -fPIC -Wno-maybe-uninitialized -Jlibzofu.so.p -o libzofu.so.p/_tmp_vincentm_zofu_src_zofu.F90.o -c /tmp/vincentm/zofu/src/zofu.F90
/tmp/vincentm/zofu/src/zofu.F90:23:6:

   23 |   use zofu_str_utils
      |      1
Fatal Error: Cannot open module file ‘zofu_str_utils.mod’ for reading at (1): No such file or directory
compilation terminated.
[4/35] Compiling Fortran object libzofu.so.p/_tmp_vincentm_zofu_src_zofu_mpi.F90.o
FAILED: libzofu.so.p/_tmp_vincentm_zofu_src_zofu_mpi.F90.o 
mpifort -Ilibzofu.so.p -I. -I.. -fdiagnostics-color=always -pipe -D_FILE_OFFSET_BITS=64 -Wall -O3 -fPIC -Wno-maybe-uninitialized -Jlibzofu.so.p -o libzofu.so.p/_tmp_vincentm_zofu_src_zofu_mpi.F90.o -c /tmp/vincentm/zofu/src/zofu_mpi.F90
/tmp/vincentm/zofu/src/zofu_mpi.F90:23:6:

   23 |   use zofu
      |      1
Fatal Error: Cannot open module file ‘zofu.mod’ for reading at (1): No such file or directory
compilation terminated.
[5/35] Compiling Fortran object libzofu.so.p/_tmp_vincentm_zofu_src_zofu_str_utils.F90.o
ninja: build stopped: subcommand failed.

Can't open module file ‘zofu.mod’

I am trying to setup the "adder_tests" example. I created a module which defines the adder_type in ./src and the following meson.build file, which is a bit different than the one in the repo because of the zofu library location.

project('adder', ['fortran'])

# find zofu library: https://github.com/mesonbuild/meson/issues/3926
cmplr = meson.get_compiler('fortran')  
local_lib_dir = '/home/vince/.local/lib'
deps = []
foreach libname : ['zofu']
    deps += [cmplr.find_library(libname, dirs : local_lib_dir)]
endforeach

src_dir = join_paths(meson.current_source_dir(), 'src')
adder = shared_library('adder', join_paths(src_dir, 'adder_module.F90'))

test_dir = join_paths(meson.current_source_dir(), 'test')
test_name = 'adder_tests'
test = join_paths(meson.current_source_dir(),
                      'tests', test_name + '.F90')
driver_src_name = test_name + '_driver.F90'

test_driver_src = configure_file(
  output: driver_src_name,
  command: ['zofu-driver', test, driver_src_name])
test_exe = executable('adder_tests',
                      [test_driver_src, test],
                      link_with: adder,
                      dependencies: deps)
test('adder_tests', test_exe)

I then call

meson --wipe build && ninja -C build

The first command succeeds, but ninja fails as follows

Fatal Error: Can't open module file ‘zofu.mod’ for reading at (1): No such file or directory
compilation terminated.

I am wondering what is the correct way to solve this.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.