A Fortran 2023 correctness-checking framework supporting expressive idioms for writing assertions and tests
Find us on…
The Julienne framework offers a unified approach to unit testing and runtime assertion checking. Julienne defines idioms for specifying correctness conditions in a common way when writing tests that wrap the tested procedures or assertions that conditionally execute inside procedures to check correctness. Julienne's idioms center around expressions built from defined operations: a uniquely flexible Fortran capability allowing developers to define new operators in addition to overloading Fortran's intrinsic operators. The following table provides examples of the expressions Julienne supports:
Example expressions | Operand types |
---|---|
x .approximates. y .within. tolerance |
real , double precision |
x .approximates. y .withinFraction. tolerance |
real , double precision |
x .approximates. y .withinPercentage. tolerance |
real , double precision |
.all. ([i,j] .lessThan. k) |
integer , real , double precision |
.all. ([i,j] .lessThan. [k,m]) |
integer , real , double precision |
.all. (i .lessThan. [k,m]) |
integer , real , double precision |
(i .lessThan. j) .also. (k .equalsExpected. m)) |
integer , real , double precision |
x .lessThan. y |
integer , real , double precision |
x .greaterThan. y |
integer , real , double precision |
i .equalsExpected. j |
integer , character , type(c_ptr) |
i .isAtLeast. j |
integer , real , double precision |
i .isAtMost. j |
integer , real , double precision |
s .isBefore. t |
character |
s .isAfter. t |
character |
.expect. allocated(A) // " (expected allocated A)" |
logical |
where
* .isAtLeast.
and .isAtMost.
can alternatively be spelled .greaterThanOrEqualTo.
and .lessThanOrEqualTo.
, respectively;
* .equalsExpected.
uses ==
, which implies that trailing blank spaces are ignored in character operands;
* .equalsExpected.
with integer operands supports default integers and integer(c_size_t)
;
* .isBefore.
and .isAfter.
verify alphabetical and reverse-alphabetical order, respectively; and
* .all.
aggregates arrays of expression results, reports a consensus result, and shows diagnostics only for failing tests, if any;
* .equalsExpected.
generates asymmetric diagnostic output for failures, denoting the left- and right-hand sides as the actual value and expected values, respectively; and.
* //
appends the subsequent string to diagnostics strings, if any.
Any of the above expressions can be the actual argument in an invocation
of Julienne's call_julienne_assert
function-line preprocessor macro:
call_julienne_assert(x .lessThan. y)
which a preprocessor will replace with a call to Julienne's assertion subroutine
when compiling with -DASSERTIONS
. Otherwise, the preprocessor will remove the
above line entirely when -DASSERTIONS
is not present.
The above tabulated expressions can also serve as function results in unit tests.
All operands in an expression must be compatible in type and kind as well as
conformable in rank, where the latter condition implies that the operands must
be all scalars or all arrays with the same shape or a combination of scalars and
arrays with the same shape. This constraint follows from each of the binary
operators being elemental
. The unary .all.
operator applies to operands of
any rank.
Each tabulated expression above produces a test_diagnosis_t
object with two
components:
logical
indicator of test success if .true
. or failure if .false.
andFor cases in which Julienne's operators do not support the desired correctness
condition, the framework provides string-handling utilities for use in crafting
custom diagnostic messages. The string utilities center around Julienne's
string_t
derived type, which offers elemental
constructor functions, i.e.,
functions that one invokes via the same name as the derived type: string_t()
.
The string_t()
constructor functions convert data of numeric type to
character
type, storing the resulting character
representation in a private
component of the constructor function result. The actual argument provided to
the constructor function can be of any one of several types, kinds, and ranks.
Julienne provides defined operations for concatenating string_t
objects
(//
), forming a concatenated string_t
object from an array of string_t
objects (.cat.
), forming a separated-value list (.separatedBy.
or
equivalently .sv.
), including a comma-separated value list (.csv.)
. The
table below shows some expressions that Julienne supports with these defined
operations.
Example expression | Result |
---|---|
s%bracket() , where s=string_t("abc") , |
string_t("[abc]") |
s%bracket("_") , where s=string_t("abc") |
string_t("_abc_") |
s%bracket("{","}") , where s=string_t("abc") |
string_t("{abc}") |
string_t(["a", "b", "c"]) |
[string_t("a"), string_t("b"), string_t("c")] |
.cat. string_t([9,8,7]) |
string_t("987") |
.csv. string_t([1.5,2.0,3.25]) |
string_t("1.50000000,2.00000000,3.25000000") |
"-" .separatedBy. string_t([1,2,4]) |
string_t("1-2-4") |
string_t("ab") // string_t("cd") |
string_t("abcd") |
"ab" // string_t("cd") |
string_t("abcd") |
string_t("ab") // "cd" |
string_t("abcd") |
One can use such expressions to craft a diagnostic message when constructing a custom test function result:
type(test_diagnosis_t) test_diagnosis
test_diagnosis = test_diagnosis_t( &
test_passed = i==j, &
diagnostics_string = "expected " // string_t(i) // "; actual " //string_t(j) &
)
Arrays of string_t
objects provide a convenient way to store a ragged-length
array of character
data. Julienne's file_t
derived type has a private
component that is a string_t
array, wherein each element is one line of a text
file. By storing a file in a file_t
object using the file_t
derived type's
constructor function one can confine a program's file input/output (I/O) to one
or two procedures. The resulting file_t
object can be manipulated elsewhere
without incurring the costs associated with file I/O. For example, the following
line reads a file named data.txt
into a file_t
object and associates the name
file
with the resulting object.
type(file_t) file
associate(file => file_t("data.txt"))
end associate
This style supports functional programming patterns in two ways. First, the rest
of the program can be comprised of pure
procedures, which are precluded from
performing I/O. Second, an associate name is immutable when associated with an
expression, including an expression that is simply a function reference.
Functional programming revolves around creating and using immutable state.
(By contrast, when associating a name with a variable or array instead of with
an expression, only certain attributes, such as the entity's allocation status,
are immutable. The value of such a variable or array can be redefined.)
Functional programming patterns centered around pure
procedures enhance
code clarity, ease refactoring, and encourage optimization. For example,
the constraints on pure
procedures make it easier for a developer or a
compiler to safely reorder program statements. Moreover, Fortran allows
invoking only pure
procedures inside do concurrent
, a construct that
compilers can automatically offload to a graphics processing unit (GPU).
Julienne lowers a widely stated barrier to writing pure
procedures (including
simple
procedures): the difficulty in printing values while debugging code.
The Julienne philosophy is that printing a value for debugging purposes implies
an expectation about the value. Assert such expectations by writing Julienne
expressions inspired by natural language. A program will proceed quietly past
a correct assertion. An incorrect assertion produces either automated or custom
diagnostic messages during error termination.
Please see demo/README.md for a detailed demonstration of test setup.
To write a Julienne assertion, insert a function-like preprocessor macro
call_julienne_assert
on a single line as in each of the two macro
invocations below:
#include "julienne-assertion-macros.h"
program main
use, julienne_m, only : call_julienne_assert_
implicit none
real, parameter :: x=1., y=2., tolerance=3.
call_julienne_assert(x .approximates. y .within. tolerance)
call_julienne_assert(abs(x-y) < tolerance)
end program
where inserting -DASSERTIONS
in a compile command will expand the macros to
call call_julienne_assert_(x .approximates. y .within. tolerance, __FILE__, __LINE__)
call call_julienne_assert_(allocated(a), __FILE__, __LINE__)
and where dots (.
) delimit Julienne operators. The above expression containing
Julienne operators evaluates to a Julienne test_diagnosis_t
object, whereas
expression allocated(a)
on the subsequent line evaluates to a logical
value.
If an assertion containing a Julienne expression fails, Julienne inserts diagnostic
information into the stop code in an ultimate error stop
. If an expression
evaluates to a logical
value of false.
, the error stop code will contain a
literal copy of the expression (e.g., allocated(a)
). In either case, Julienne
also inserts the file and line number into the stop code using via the __FILE__
and __LINE__
macros, respectively. Most compilers write the resulting stop code
to error_unit
.
Julienne's name derives from the term for vegetables sliced into thin strings: julienned vegetables. The Veggies and Garden unit-testing frameworks inspired the structure of Julienne's tests and output. Initially developed in the Sourcery repository as lightweight alternative with greater portability across compilers, Julienne's chief innovation now lies in the expressive idioms the framework supports.
When built with the compiler versions tabulated below, all Julienne tests pass.
Compiler | Version(s) Tested | Known Issues |
---|---|---|
LLVM flang-new |
19, 20, 21 | none |
NAG nagfor |
7.2 Build 7235 | none |
Intel ifx |
2025.2.1 | none |
GCC gfortran |
13.4.0, 14.3.0, 15.1.0 | see below |
With gfortran
13 through 14.2.0,
- The test_description_t
constructor's diagnosis_function
actual argument
must be a procedure pointer declared with procedure(diagnosis_function_i)
.
- The string_t
type-bound function bracket
crashes.
flang-new
) compilerWith version 20 or later, please run
fpm test --compiler flang-new --flag "-O3"
With version 19, please run
fpm test --compiler flang-new --flag "-O3 -mmlir -allow-assumed-rank"
to enable support for assumed-rank dummy arguments.
nagfor
) compilerfpm test --compiler nagfor --flag "-fpp -O3"
To execute build and run parallel tests in 2 images, please run
export NAGFORTRAN_NUM_IMAGES=2
fpm test --compiler nagfor --flag "-fpp -O3 -coarray"
Replace the "2" above with any number up to the compiler limit of 1000 images.
gfortran
) compilerFor compiler versions 14 or higher, plese use
fpm test --compiler gfortran --profile release
For version 13, please append --flag "-ffree-line-length-none"
to the above
command to enable the Fortran 2023 line-length maximum of 5000 characters.
With OpenCoarrays installed, please replace --compiler gfortran
with
--compiler caf
and please append --runner "cafrun -n 2"
to the above
command.
ifx
) compilerfpm test --compiler ifx --flag "-fpp -O3" --profile release
To execute build and run parallel tests in 2 images, please run
export FOR_COARRAY_NUM_IMAGES=2
fpm test --compiler ifx --flag "-fpp -O3 -coarray" --profile release
Replace the "2" above with any desired number of images.
See our online documentation or build the documentation locally by installing FORD and executing ford ford.md
.