Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Julienne aims to be a more lightweight alternative that is more portable across

Getting Started
---------------
Please see the [example-test-suite README.md](./example/example-test-suite/README.md).
Please see the demonstration test suite in [demo README.md](./demo/README.md).

Supported Compilers
-------------------
Expand Down
132 changes: 73 additions & 59 deletions example/example-test-suite/README.md → demo/README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,56 @@
Example Test Suite Classes
==========================

Getting Started
---------------
Likely the fastest way to get started with Julienne is to copy the source code in this directory and modify it for your purposes:

1. If you build your project with the Fortran Package Manager ([`fpm`](https://github.com/fotran-lang/fpm)), then you might move the `main.F90` and `specimen_test_m.F90` files from this subdirectory to a `test/` subdirectory in the root of your project's source tree.
===============
To get started with Julienne, review and test the demonstration project in this directory.
Then copy the `main.F90` and `specimen_test_m.F90` files to your project's test directory.
Finally, modify the files as described below to adapt them to your project.

Testing the Demonstration Project
--------------------------------
This demonstration project defines a trivial library named "specimen" in the `src`
subdirectory and a test suite the `test` subdirectory. The test suite includes five tests:

1. Two pass.
2. One intentionally fails to demonstrate diagnostic output.
3. One test is skipped to the reporting and tallying of skipped tests.
4. One test passes with three compilers but is skipped with GCC due to a compiler bug.

Test Julienne by setting your present working directory to the `demo/` subdirectory in a
terminal window and then building and running the demonstration project's test suite using
the command corresponding to your compiler in the table below.

|Vendor | Version/Build | Example shell command |
|---------|-------------------------|------------------------------------------------------|
|LLVM | 20.1.4 (Homebrew) | `fpm test --compiler flang-new` |
|GCC | 14.2.0_1 (Homebrew) | `fpm test --compiler gfortran --profile release` |
|NAG | 7.2 Build 7227 | `fpm test --compiler nagfor --flag -fpp` |
|Intel | 2025.1.0 Build 20250317 | `fpm test --compiler ifx --flag "-fpp -O3 -coarray"` |

Setting Up Your Project's Test Suite
------------------------------------

1. If you build your project with the Fortran Package Manager ([`fpm`](https://github.com/fotran-lang/fpm)), then you might copy the `main.F90` and `specimen_test_m.F90` files from this subdirectory to a `test/` subdirectory in the root of your project's source tree.
2. Rename the `specimen_test_m.F90` file, the `specimen_test_m` module, and the `specimen_test_t` derived type and any references thereto, replacing `specimen` with the name of an entity that you intend to test -- most likely a module containing procedures or derived type with type-bound procedures.
3. Similarly replace occurrences of `specimen` in the resulting`test/main.F90` file.
4. Modify the `test_descriptions_t` array constructor in your new `*_test_m.F90` file, adding elements for each test to be performed:
```fortran
test_descriptions = [ &
test_description_t("the type-bound function zero() producing a result of 0", check_zero) &
]
```
5. Replace the above string (`"the type-bound..."`) with a description of your intended test. The test output will read most naturally if your description contains a gerund: a verb ending in "ing" and used as a noun, such as `producing` above.
6. Replace the `check_zero` function name with the name of a function that will perform your test.
7. Edit the correspondingly-renamed function to perform the test. The function must take no arguments and define a `test_diagnosis_t` result. An example result might be the following:
```fortran
test_diagnosis = test_diagnosis_t( &
test_passed = actual_value == expected_value &
,diagnostics_string = "expected value " // string_t(expected_value) //", actual value " // string_t(actual_value) &
)
```
The above `test_diagnosis_t` constructor function invocation demonstrates the recommended pattern for writing tests with Julienne:
4. In the `results()` function body of your new `*_test_m.F90` file, replace the `test_descriptions_t` array constructor elements with your own test descriptions. The test output will read most naturally if your description string (the first argument) contains a gerund: a verb ending in "ing" and used as a noun, such as `producing` above
5. Replace the function name (the second argument) with the name of a function that will perform your test.
7. Edit the correspondingly-renamed function to perform the test. The function must take no arguments and define a `test_diagnosis_t` result.

The functions in `specimen_test_m` demonstrate several common options for constructing a `test_diagnosis_t` as the diagnosis function result.
The options include

* Define the `test_passed` keyword argument by writing an expression that will evaluate to `.true.` if and only if the test succeeds.
* Define the `diagnostics_string` keyword argument from character literal values interspersed with`string_t` constructor, all strung together by instances of the string concatenation operator `//`.
1. Writing an expression using Julienne's operators such as `.approximate.`, `.within`., and `.equalsExpected.`.
2. Invoking the `test_diagnosis_t` constructor and using Julienne's `string_t` constructors to form a diagnostic string.

`String_t` is a generic interface to various specific functions, each of which takes an argument of a different data type, kind, and rank (TKR) and defines a `string_t` result containing a charater representation of the function argument.
Please see Julienne's online [documentation](https:///berkeleylab.github.io/julienne/) for the currently supported TKR.
Please see Julienne's online [documentation] for the currently supported TKR.
Please submit an issue to request support for additional TKR or submit a pull request to contribute such support.

#### Forming diagnostic strings from array data

An especially useful pattern for forming diagnostic string involves invoking Julienne's `operator(.csv.)` to produce a string of comma-separated values (CSV) from a one-dimensional (1D) array.
An especially useful pattern for forming diagnostic strings involves invoking Julienne's `operator(.csv.)` to produce a string of comma-separated values (CSV) from a one-dimensional (1D) array.
For example, consider the following test description:
```fortran
test_description_t(" returning the counting numbers up to 3", check_counting_numbers)
test_description_t("returning the counting numbers up to 3", check_counting_numbers)
```
and the following corresponding test:
```fortran
Expand All @@ -59,13 +72,8 @@ FAILS on returning the counting numbers up to 3
```
To support a common array notation, Julienne also supports bracketing strings.

**Exercise 1:** Make `check_counting_numbers` more robust by testing the equivalence of `expected_array` and `actual_array` only if the array sizes match and by treating a size-mismatch as a test failure.

**Exercise 2:** Revise `check_counting_numbers` by defining CSV strings `expected_string` and `actual_string` _before_ invoking `test_diagnostics_t`.
Bracket the CSV strings in the `diagnostics_string` keyword argument by invoking `bracket` type-bound procedure, e.g., `expected_string%bracket()`.

Scalar Diagnosis Function
-------------------------
Diagnosis Functions
-------------------
The Unified Modeling Language ([UML](https://wikipedia.org/Unified_modeling_langauge)) class diagram below depicts the class relationships involved in making the above example work:

```mermaid
Expand Down Expand Up @@ -104,33 +112,10 @@ class test_diagnosis_t{
}
```

Vector Diagnosis Function
-------------------------
The UML class diagram below depicts the class relationships involved when test function performs multiple checks and defines a result containing an array of corresponding `test_diagnosis_t` objects:
```mermaid
%%{init: { 'theme':'default', "class" : {"hideEmptyMembersBox": true} } }%%
classDiagram

class vector_test_description_t{
vector_test_description_t(description : string_t[1..*], vector_diagnosis_function : vector_diagnosis_function_i)
run() test_result_t[1..*]
}
vector_test_description_t --> test_diagnosis_t : run() invokes vector_diagnosis_function to construct array of
vector_test_description_t --> test_result_t : run() uses test_diagnostics_t array to construct array of

class test_result_t{
test_result_t(test_passed : logical, diagnosis : test_diagnosis_t)
}

class test_diagnosis_t{
test_diagnosis_t(test_passed : logical, diagnostics_string : string_t)
}
```

Skipping Tests
--------------
When a test is known to cause a compile-time or runtime crash in a specific scenario, e.g., with a specific compiler or compiler version, including that test will prevent the test suite from building or running to completion.
It can be useful to skip a test with the problematic compiler but to report the test as skipped and account for the skipped tests in the tally of test results..
It can be useful to skip a test with the problematic compiler but to report the test as skipped and account for the skipped tests in the tally of test results.
For this purpose, the `test_description_t` and `vector_test_description_t` constructor functions have optional second arguments `diagnosis_function` and `vector_diagnosis_function`, respectively.
When these arguments are not `present`, the `test_t`'s `report` procedure will report the test as skipped but will terminate normally as long as the sum of the passing tests and skipped tests equals the total number of tests.
One might accomplish this with the compiler's predefined preprocessor macro:
Expand Down Expand Up @@ -175,3 +160,32 @@ class string_t{
base_name(string_t) string_t
}
```

Deprecated: Vector Diagnosis Function
-------------------------------------
Julienne's `vector_diagnosis_function_i` abstract interface and the corresponding `vector_test_description_t` type were developed before Julienne's `operator(.all.)` and `operator(.and.)`.
Because the operators replace the interface and type with simpler functionality, it is likely that a future release will remove the `vector_*` entities.

The Unified Modeling Language ([UML]) class diagram below depicts the class relationships involved when test function performs multiple checks and defines a result containing an array of corresponding `test_diagnosis_t` objects:
```mermaid
%%{init: { 'theme':'default', "class" : {"hideEmptyMembersBox": true} } }%%
classDiagram

class vector_test_description_t{
vector_test_description_t(description : string_t[1..*], vector_diagnosis_function : vector_diagnosis_function_i)
run() test_result_t[1..*]
}
vector_test_description_t --> test_diagnosis_t : run() invokes vector_diagnosis_function to construct array of
vector_test_description_t --> test_result_t : run() uses test_diagnostics_t array to construct array of

class test_result_t{
test_result_t(test_passed : logical, diagnosis : test_diagnosis_t)
}

class test_diagnosis_t{
test_diagnosis_t(test_passed : logical, diagnostics_string : string_t)
}
```

[documentation]: https://berkeleylab.github.io/julienne
[UML]: https://wikipedia.org/Unified_modeling_language
4 changes: 4 additions & 0 deletions demo/fpm.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name = "Example-Test-Suite"

[dependencies]
julienne = {git = "https://github.com/berkeleylab/julienne", tag = "2.1.0-rc5"}
38 changes: 38 additions & 0 deletions demo/include/language-support.F90
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
! Copyright (c) 2024-2025, The Regents of the University of California
! Terms of use are as specified in LICENSE.txt

#ifndef _JULIENNE_LANGUAGE_SUPPORT_H
#define _JULIENNE_LANGUAGE_SUPPORT_H

! If not already determined, make a compiler-dependent determination of whether Julienne may pass
! procedure actual arguments to procedure pointer dummy arguments, a feature introduced in
! Fortran 2008 and described in Fortran 2023 clause 15.5.2.10 paragraph 5.
#ifndef HAVE_PROCEDURE_ACTUAL_FOR_POINTER_DUMMY
# if defined(__GFORTRAN__)
# define HAVE_PROCEDURE_ACTUAL_FOR_POINTER_DUMMY 0
# else
# define HAVE_PROCEDURE_ACTUAL_FOR_POINTER_DUMMY 1
# endif
#endif

! If not already determined, make a compiler-dependent determination of whether Julienne may use
! multi-image features such as `this_image()` and `sync all`.
#ifndef HAVE_MULTI_IMAGE_SUPPORT
# if defined(__flang__)
# define HAVE_MULTI_IMAGE_SUPPORT 0
# else
# define HAVE_MULTI_IMAGE_SUPPORT 1
# endif
#endif

! If not already determined, make a compiler-dependent determination of whether Julienne may use
! kind type parameters for derived types.
#ifndef HAVE_DERIVED_TYPE_KIND_PARAMETERS
# if defined(__GFORTRAN__)
# define HAVE_DERIVED_TYPE_KIND_PARAMETERS 0
# else
# define HAVE_DERIVED_TYPE_KIND_PARAMETERS 1
# endif
#endif

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ module specimen_m
type specimen_t
contains
procedure, nopass :: zero
procedure, nopass :: one
end type

contains

pure function zero() result(incorrect_value)
integer incorrect_value
incorrect_value = 1
pure function zero() result(correct_value)
real correct_value
correct_value = 0
end function

pure function one() result(correct_value)
integer correct_value
correct_value = 1
end function

end module
File renamed without changes.
130 changes: 130 additions & 0 deletions demo/test/specimen_test_m.F90
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
! Copyright (c) 2024-2025, The Regents of the University of California and Sourcery Institute
! Terms of use are as specified in LICENSE.txt

#include "language-support.F90"

module specimen_test_m
!! Example unit test for the specimen_t test subject
use specimen_m, only : specimen_t
use julienne_m, only : &
string_t &
,test_t &
,test_result_t &
,test_description_t &
,test_description_substring &
,test_diagnosis_t &
,operator(.approximates.) &
,operator(.within.) &
,operator(.all.) &
,operator(.equalsExpected.) &
,operator(.greaterThan.) &
,operator(.lessThan.)
#if defined(__GFORTRAN__)
use julienne_m, only : diagnosis_function_i ! work around gfortran's missing Fortran 2008 feature
#endif

implicit none

private
public :: specimen_test_t

type, extends(test_t) :: specimen_test_t
contains
procedure, nopass :: subject
procedure, nopass :: results
end type

contains

pure function subject() result(specimen_description)
character(len=:), allocatable :: specimen_description
specimen_description = "A specimen_t object"
end function

#if ! defined(__GFORTRAN__)

function results() result(test_results)
type(test_result_t), allocatable :: test_results(:)
type(test_description_t), allocatable :: test_descriptions(:)

test_descriptions = [ &
test_description_t("diagnosing the zero function using Julienne operators", check_zero_using_operators) &
,test_description_t("diagnosing the zero function using a diagnosis constructor", check_zero_using_constructor) &
,test_description_t("aggregating diagnoses of the zero and one functions using operator(.all.)", check_aggregate_diagnosis) &
,test_description_t("(intentional failure to demonstrate diagnostic output)", check_print_diagnosis) &
,test_description_t("skipping a test when no diagnosis function is specified") &
]
test_descriptions = pack( &
array = test_descriptions &
,mask = test_descriptions%contains_text(test_description_substring) .or. index(subject(), test_description_substring)/=0 &
)
test_results = test_descriptions%run()
end function

#else

function results() result(test_results)
!! work around missing Fortran 2008 feature in gfortran versions earlier than 15
type(test_result_t), allocatable :: test_results(:)
type(test_description_t), allocatable :: test_descriptions(:)
procedure(diagnosis_function_i), pointer :: check_operators_ptr => check_zero_using_operators
procedure(diagnosis_function_i), pointer :: check_constructor_ptr => check_zero_using_constructor
procedure(diagnosis_function_i), pointer :: check_aggregate_ptr => check_aggregate_diagnosis
procedure(diagnosis_function_i), pointer :: check_print_diagnosis_ptr => check_print_diagnosis

! Omitting the optional 2nd argument in the 3rd test_description_t constructor below skips the described
! test. When the test suite runs, it reports the test as skipped and reports a tally of skippped tests.

test_descriptions = [ &
test_description_t("diagnosing the zero function using Julienne operators", check_operators_ptr) &
,test_description_t("diagnosing the zero function using a diagnosis constructor", check_constructor_ptr) &
,test_description_t("aggregating diagnoses of the zero and one functions using operator(.all.)") &
,test_description_t("(intentional failure to demonstrate diagnostic output)", check_print_diagnosis_ptr) &
,test_description_t("skipping a test when no diagnosis function is specified") &
]
test_descriptions = pack( &
array = test_descriptions &
,mask = test_descriptions%contains_text(test_description_substring) .or. index(subject(), test_description_substring)/=0 &
)
test_results = test_descriptions%run()
end function
#endif

function check_zero_using_operators() result(test_diagnosis)
!! Construct a test diagnosis using Julienne's operator(.approximates.) and operator(.within.)
type(test_diagnosis_t) test_diagnosis
type(specimen_t) specimen
real, parameter :: expected_value = 0., tolerance = 1E-02
test_diagnosis = specimen%zero() .approximates. expected_value .within. tolerance
end function

function check_zero_using_constructor() result(test_diagnosis)
!! Construct a test diagnosis using Julienne's string_t constructor to define a custom diagnostic string
type(test_diagnosis_t) test_diagnosis
type(specimen_t) specimen
real, parameter :: expected_value = 0., tolerance = 1E-02
associate(actual_value => specimen%zero())
test_diagnosis = test_diagnosis_t( &
test_passed = abs(actual_value - expected_value) < tolerance &
,diagnostics_string = "expected " // string_t(expected_value) // "; actual " // string_t(actual_value) &
)
end associate
end function

function check_aggregate_diagnosis() result(test_diagnosis)
!! Aggregate two test diagnoses using Julienne's operator(.all.)
type(test_diagnosis_t) test_diagnosis
type(specimen_t) specimen
real, parameter :: expected_ceiling = 10.
integer, parameter :: expected_integer = 1
associate(actual_real => specimen%zero(), actual_integer => specimen%one())
test_diagnosis = .all. [actual_integer .equalsExpected. expected_integer, actual_real .lessThan. expected_ceiling]
end associate
end function

function check_print_diagnosis() result(test_diagnosis)
type(test_diagnosis_t) test_diagnosis
test_diagnosis = 2 .lessThan. 1 ! intentional test failure
end function

end module
5 changes: 5 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Examples
========
This directory focuses on examples that demonstrate Julienne's string-handling capabilities.
For a demonstration of how to use Julienne for unit testing, please see the [`demo`](../demo)
subdirectory of this repository.
Loading