Skip to content

Commit 7eaf673

Browse files
committed
markdown source builds
Auto-generated via `{sandpaper}` Source : 8309e2c Branch : main Author : Connor Aird <c.aird@ucl.ac.uk> Time : 2025-08-05 10:17:52 +0000 Message : Merge pull request #20 from UCL-ARC/4-testing-parallel-code Add testing parallel code lesson
1 parent e716a59 commit 7eaf673

4 files changed

Lines changed: 202 additions & 3 deletions

File tree

5-testing-parallel-code.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
title: "Testing parallel code"
3+
teaching:
4+
exercises:
5+
---
6+
7+
:::::::::::::::::::::::::::::::::::::: questions
8+
9+
- How do I unit test a procedure which makes MPI calls?
10+
- How do I easily test different numbers of MPI ranks?
11+
- How do I test a procedure which uses OMP directives?
12+
- How do I easily test different numbers of OMP threads?
13+
14+
::::::::::::::::::::::::::::::::::::::::::::::::
15+
16+
::::::::::::::::::::::::::::::::::::: objectives
17+
18+
- Understand what is different when testing parallel vs serial code.
19+
20+
::::::::::::::::::::::::::::::::::::::::::::::::
21+
22+
## What's the difference?
23+
24+
Depending on the parallelisation tool and strategy employed, the implementation of parallel code can be very different
25+
to that of serial code. This is especially true for code which utilises the message passing interface (MPI). These codes
26+
almost always contain some functionality in which processes, or ranks, communicate by exchanging messages. This message
27+
passing is often complex and will always benefit from testing.
28+
29+
There is added complexity when testing MPI code compared to serial as the logical path through the code is changed
30+
depending on the number of ranks with which the code is executed. Therefore, it is important that we test for a range
31+
of numbers of ranks. This will require controlling the number of ranks running the src and is not something we want
32+
to implement ourselves. This limits the tools available to us. pFUnit is currently the only tool which supports
33+
testing MPI code. Therefore, we will be focusing on pFUnit for this section.
34+
35+
## Tips for writing testable MPI code
36+
37+
### Where possible, separate calls to the MPI library into units (subroutines or functions).
38+
39+
If a procedure does not contain any calls to the MPI library, then it can be tested with a serial unit test. Therefore,
40+
separating MPI calls into their own units makes for a simpler test suite for most of your logic. Only, procedures with
41+
MPI library calls will require the more complex parallel pFUnit tests.
42+
43+
### Pass the MPI communicator information into each procedure to be tested.
44+
45+
If we pass the MPI communicator into a procedure, we can define this to be whatever we wish in our tests. This allows us
46+
to use the communicator provided by pFUnit or some other communicator specific to our problem.
47+
48+
Creating types to wrap this information along with any other MPI specific information (neighbour ranks, etc) can be a
49+
convenient approach.
50+
51+
## Syntax of writing MPI enabled pFUnit tests
52+
53+
Firstly, we must change how we define our test parameters:
54+
55+
- We now use `MPITestParameter` instead of `AbstractTestParameter`.
56+
- `MPITestParameter` inherits from `AbstractTestParameter` and provides an additional parameter in its constructor which
57+
corresponds to the number of processors for which a particular test should be ran.
58+
- We can't know for certain the rank of each process for the pFUnit communicator until the test case runs. Therefore, we
59+
now need to build arrays of input parameters with the rank of a process matching the index of the parameter array. For
60+
example, rank 0 would access index 1 of the input array during testing, rank 1 would access index 2 and so on. See below
61+
for an example.
62+
63+
```F90
64+
@testParameter(constructor=new_exchange_boundaries_test_params)
65+
type, extends(MPITestParameter) :: my_test_params
66+
integer, allocatable :: input(:), expected_output(:)
67+
contains
68+
procedure :: toString => my_test_params_toString
69+
end type my_test_params
70+
```
71+
72+
We therefore need to update how we populate our test parameters to take into account the rank indexing:
73+
74+
```F90
75+
function my_test_suite() result(params)
76+
type(my_test_params), allocatable :: params(:)
77+
integer, allocatable :: input(:), expected_output(:)
78+
integer, max_number_of_ranks
79+
80+
max_number_of_ranks = 2
81+
allocate(params(max_number_of_ranks))
82+
allocate(input(max_number_of_ranks))
83+
allocate(expected_output(max_number_of_ranks))
84+
85+
! Tests with one rank
86+
input(1) = 1
87+
expected_output(1) = 2
88+
params(1) = my_test_params(1, input, expected_output)
89+
90+
! Tests with two ranks
91+
! rank 0
92+
input(1) = 1
93+
expected_output(1) = 1
94+
! rank 1
95+
input(2) = 1
96+
expected_output(2) = 1
97+
params(2) = my_test_params(2, input, expected_output)
98+
end function my_test_suite
99+
```
100+
101+
We also need to change how we define our test case:
102+
103+
- We now use `MPITestCase` instead of `ParameterizedTestCase`
104+
- `MPITestCase` provides several helpful methods for us to use whilst testing
105+
- `getProcessRank()` returns the rank of the current process allowing per rank selection of inputs and expected outputs.
106+
- `getMpiCommunicator()` returns the MPI communicator created by pFUnit to control the number of ranks per test.
107+
- `getNumProcesses()` returns the number of MPI ranks for the current test.
108+
109+
```F90
110+
@TestCase(testParameters={my_test_suite()}, constructor=my_test_params_to_my_test_case)
111+
type, extends(MPITestCase) :: my_test_case
112+
type(my_test_params) :: params
113+
end type my_test_case
114+
```
115+
116+
Finally, we ensure each process accesses the correct rank index parameters during the test
117+
118+
```F90
119+
@Test
120+
subroutine TestMySrcProcedure(this)
121+
class (my_test_case), intent(inout) :: this
122+
123+
integer :: actual_output, rank_index
124+
125+
rank_index = this%getProcessRank() + 1
126+
127+
call my_src_procedure(this%params%input(rank_index), actual_output)
128+
129+
@assertEqual(this%params%expected_output(rank_index), actual_output, "Unexpected output from my_src_procedure")
130+
end subroutine TestMySrcProcedure
131+
```
132+
133+
::::::::::::::::::::::::::::::::::::: challenge
134+
135+
### Challenge 1: Testing MPI parallel code
136+
137+
Take a look at [5-testing-parallel-code/challenge](https://github.com/UCL-ARC/fortran-unit-testing-exercises/tree/main/episodes/5-testing-parallel-code/challenge) in the exercises repository.
138+
139+
:::::::::::::::::::::::::::::::: solution
140+
141+
A solution is provided in [5-testing-parallel-code/solution](https://github.com/UCL-ARC/fortran-unit-testing-exercises/tree/main/episodes/5-testing-parallel-code/solution).
142+
143+
:::::::::::::::::::::::::::::::::::::::::
144+
::::::::::::::::::::::::::::::::::::::::::::::::

config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ episodes:
7070
- 2-intro-to-fortran-unit-tests.md
7171
- 3-fortran-unit-test-syntax.md
7272
- 4-debugging-a-broken-test.md
73+
- 5-testing-parallel-code.md
7374

7475
# Information for Learners
7576
learners:

md5sum.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"file" "checksum" "built" "date"
22
"CODE_OF_CONDUCT.md" "c93c83c630db2fe2462240bf72552548" "site/built/CODE_OF_CONDUCT.md" "2022-08-05"
33
"LICENSE.md" "16e8eaad880865bc4a41811b3e8fa945" "site/built/LICENSE.md" "2025-02-03"
4-
"config.yaml" "771ca11448c55dcc1b56a10e9fc67738" "site/built/config.yaml" "2025-07-30"
4+
"config.yaml" "dcf0220d637e299bc7585265b36af2b1" "site/built/config.yaml" "2025-08-05"
55
"index.md" "56da64ff09e4ecabeb2f4e14a69bde10" "site/built/index.md" "2025-07-30"
66
"links.md" "8184cf4149eafbf03ce8da8ff0778c14" "site/built/links.md" "2022-04-22"
77
"episodes/1-intro-to-unit-tests.md" "6192f8883214eb1ed24e4b1cb800f808" "site/built/1-intro-to-unit-tests.md" "2025-08-05"
88
"episodes/2-intro-to-fortran-unit-tests.md" "db6d130fc585b07f9786ced22e19b11e" "site/built/2-intro-to-fortran-unit-tests.md" "2025-08-05"
99
"episodes/3-fortran-unit-test-syntax.md" "985339fc735a2ae67b83a94e59039ae4" "site/built/3-fortran-unit-test-syntax.md" "2025-08-05"
1010
"episodes/4-debugging-a-broken-test.md" "a492b0a1b576f95baa4d1baa0aa67712" "site/built/4-debugging-a-broken-test.md" "2025-07-30"
11+
"episodes/5-testing-parallel-code.md" "7b7425010bc9a1413d1a8891bf34a7c5" "site/built/5-testing-parallel-code.md" "2025-08-05"
1112
"instructors/instructor-notes.md" "cae72b6712578d74a49fea7513099f8c" "site/built/instructor-notes.md" "2023-03-16"
1213
"learners/reference.md" "1c7cc4e229304d9806a13f69ca1b8ba4" "site/built/reference.md" "2023-03-16"
13-
"learners/setup.md" "ccfecb39370c85c66c1e4363d9c9a887" "site/built/setup.md" "2025-07-30"
14+
"learners/setup.md" "cc772de0561c5f3fdb617c6dc91d3af1" "site/built/setup.md" "2025-08-05"
1415
"profiles/learner-profiles.md" "12807933e1001fc9684969fa58fb90dd" "site/built/learner-profiles.md" "2025-05-16"

setup.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,60 @@ $ cmake --version
5656
cmake version 3.27.0
5757

5858
$ ./build-pfunit.sh -t
59-
TODO: Add output from tetsing pfunit and implement testing pFUnit.
59+
[ 0%] Built target posix_predefined.x
60+
[ 0%] Built target generate_posix_parameters
61+
[ 19%] Built target funit-core
62+
[ 33%] Built target fhamcrest
63+
[ 55%] Built target asserts
64+
[ 56%] Built target funit-main
65+
[ 56%] Built target funit
66+
[ 57%] Built target pfunit-core
67+
[ 58%] Built target pfunit
68+
[ 59%] Built target new_ptests
69+
[ 60%] Built target new_ptests.x
70+
[ 62%] Built target other_shared
71+
[ 67%] Built target funit_tests
72+
[ 68%] Built target funit_tests.x
73+
[ 69%] Built target robust
74+
[ 70%] Built target remote.x
75+
[ 72%] Built target robust_tests.x
76+
[ 88%] Built target new_tests.x
77+
[ 97%] Built target fhamcrest_tests.x
78+
[ 99%] Built target pfunittests
79+
[100%] Built target parallel_tests.x
80+
[100%] Built target build-tests
81+
Start 1: unit_test_processor
82+
1/14 Test #1: unit_test_processor ........................ Passed 0.09 sec
83+
Start 2: processor_test_MpiParameterizedTestCaseC
84+
2/14 Test #2: processor_test_MpiParameterizedTestCaseC ... Passed 0.32 sec
85+
Start 3: processor_test_MpiTestCaseB
86+
3/14 Test #3: processor_test_MpiTestCaseB ................ Passed 0.08 sec
87+
Start 4: processor_test_ParameterizedTestCaseB
88+
4/14 Test #4: processor_test_ParameterizedTestCaseB ...... Passed 0.06 sec
89+
Start 5: processor_test_TestA
90+
5/14 Test #5: processor_test_TestA ....................... Passed 0.07 sec
91+
Start 6: processor_test_TestCaseA
92+
6/14 Test #6: processor_test_TestCaseA ................... Passed 0.06 sec
93+
Start 7: processor_test_beforeAfter
94+
7/14 Test #7: processor_test_beforeAfter ................. Passed 0.06 sec
95+
Start 8: processor_test_simple
96+
8/14 Test #8: processor_test_simple ...................... Passed 0.06 sec
97+
Start 9: old_tests
98+
9/14 Test #9: old_tests .................................. Passed 0.05 sec
99+
Start 10: robust_tests.x
100+
10/14 Test #10: robust_tests.x ............................. Passed 0.02 sec
101+
Start 11: new_tests.x
102+
11/14 Test #11: new_tests.x ................................ Passed 0.03 sec
103+
Start 12: fhamcrest_tests.x
104+
12/14 Test #12: fhamcrest_tests.x .......................... Passed 0.02 sec
105+
Start 13: mpi-tests
106+
13/14 Test #13: mpi-tests .................................. Passed 0.19 sec
107+
Start 14: new_ptests
108+
14/14 Test #14: new_ptests ................................. Passed 0.12 sec
109+
110+
100% tests passed, 0 tests failed out of 14
111+
112+
Total Test time (real) = 1.26 sec
60113
```
61114

62115
:::::::::::::::::::::::::::::::::::::

0 commit comments

Comments
 (0)