FortranでCTestを用いる際の一つの実践例

# 概要

以前の記事（[FortranでCTest用のテストを作成する](https://qiita.com/implicit_none/items/e4e0f627b15ca40165c8)）において，CTestを使ってFortranのプログラムをテストする方法を紹介しました．

一つのソースファイルに一つのテスト関数を書き，CMakeLists.txt内でテストドライバーを自動生成してそこからテスト関数を呼び出す方法です．

明確ではありましたが，テストが多くなるとソースファイルの数も増えて，新たなテストを作るのも既存のテストを管理するのも大変になりそうだと感じていました．

fortran-lang communityによって開発が進められている[stdlib](https://github.com/fortran-lang/stdlib)のテストは，テストドライバーの作成が不要で，一つのソースファイルに複数のテストを記述していました．

この記事ではその方法を紹介します．

# 環境

- Windows 10 Pro 20H2
- cmake 3.20.3
- GNU Make 3.8.1
- gfortran 10.3.0
- VSCode 1.55.2

# 実践例の背景知識
いきなり実践例を紹介するのではなく，もっと簡単な例でどのようにテストや`CMakeLists.txt`を記述するか説明します．

本記事で紹介する実践例では，テストを複数実行するプログラムを作成し，その終了コードによってテストの成否を判断します．CTestにはプログラム名を登録するので，以前の記事で紹介した制約（テスト関数名がCMakeLists.txtから見た相対パスと一致している必要がある）がなくなります．

下記のような配置でテストを作成します．

```
ctest_multitest
├── tests
│   ├── feat1
│   │   ├── CMakeLists.txt
│   │   └── test_feat1.f90
│   └── CMakeLists.txt
└── CMakeLists.txt
```

一つのソースファイルに複数のテストが書けるようになったので，機能名にちなんだディレクトリでテストを分類します．ここでは，feat1という機能をテストする想定です．
`CMakeLists.txt`が複数ありますが，それぞれ下記のような分担があります．

- プロジェクトのルートに置いてある`CMakeLists.txt`: プロジェクトの設定に加えて，`tests`ディレクトリを参照する
- `tests`ディレクトリの`CMakeLists.txt`: ビルドするテストを管理する
- `feat1`ディレクトリの`CMakeLists.txt`: テスト（`test_feat1.f90`）をビルドする

## テストの作成
テストを複数実行するプログラムを作成します．Fortranの主プログラムを作成し，テストは内部手続として実装します．

主プログラム名は機能名に接頭辞`test_`を付けており，これはファイル名と同じです．

テスト手続名は，主プログラム名＋具体的にテストしたい関数やサブルーチンの名前とすることを想定しています．

```Fortran:tests/feat1/test_feat1.f90
program test_feat1
    use, intrinsic :: iso_fortran_env
    implicit none

    print '(A)', "feature 1 test"

    call test_feat1_component1()
    call test_feat1_component2()

contains

    subroutine test_feat1_component1()
        print *, "test_feat1_component1"

        if (.false.) error stop
    end subroutine test_feat1_component1

    subroutine test_feat1_component2()
        print *, "test_feat1_component2"

        if (.false.) error stop
    end subroutine test_feat1_component2
end program test_feat1
```

テスト用の実行ファイルを作成する場合，CTestは当該プログラムの終了コードを見ているようなので，テストする手続が想定通りの動作をしないのであれば，`error stop`文でプログラムを終了させます．

Fortranでは，プログラムを途中で止める場合は`stop`文が広く使われていますが，これは終了コードとして`0`が返るので，失敗扱いになりません．これは昔の文法の名残です．

## テストの有効化とテストの登録
CMakeを使ったビルドの過程で，テストを実行するプログラムを作成し，テストに登録する必要があります．

複数の`CMakeLists.txt`の役割は上で説明したので，それぞれ内容を見ていくことにします．

### プロジェクトルートのCMakeLists.txt
```CMake:CMakeLists.txt
### プロジェクトの設定
cmake_minimum_required(VERSION 3.15.0)
# CMakeのバージョン

project(ctest_multitest Fortran)
# プロジェクト名の設定

enable_language (Fortran)
# Fortran向け設定の有効化

### テストの設定
enable_testing()
#テストの有効化

add_subdirectory(tests)
# テスト用のディレクトリを確認し，テストのビルドと登録を行う
```

プロジェクトのルートに置いてある`CMakeLists.txt`では，通常のプロジェクト設定に加えて，

```cmake
enable_testing()
```

でテストを有効化しています．その後，

```cmake
add_subdirectory(tests)
```

でテスト用のディレクトリ`tests`の内容を見に行き，そこに置いてあるCMakeLists.txtに従って処理を続けます．

### testsディレクトリのCMakeLists.txt

```CMake:tests/CMakeLists.txt
add_subdirectory(feat1)
# テストを追加する場合は，ここにadd_subdirectory()を追記していく
```

`tests`ディレクトリの`CMakeLists.txt`は，ビルドするテストを管理しています．管理といっても，`tests`ディレクトリにあるテスト用のディレクトリを見に行くよう，`add_subdirectory`が書かれているだけです．

ここでコメントアウト/アンコメントすることで，実行するテストを選択できます．

### 各機能のディレクトリのCMakeLists.txt

```CMake:tests/feat1/CMakeLists.txt
set(name feat1)

add_executable(test_${name} test_${name}.f90)

add_test(NAME ${name}
    COMMAND $<TARGET_FILE:test_${name}> ${CMAKE_CURRENT_BINARY_DIR}
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
```

各機能のディレクトリ（ここでは具体的に`feat1`）の`CMakeLists.txt`には，テスト（`test_feat1.f90`）をビルドして実行ファイルを作り，テストを登録するための設定が書かれています．

```cmake
add_executable(test_${name} test_${name}.f90)
```

で`test_feat1.f90`から`test_feat1.exe`を作るよう設定しています．

テストの登録では，`NAME`でテストの名前，`COMMAND`でテストを実行する実行ファイル名とその場所を指定し，WORKING_DIRECTORYでテストを実行するディレクトリを指定しています．

```cmake
add_test(NAME ${name}
    COMMAND $<TARGET_FILE:test_${name}> ${CMAKE_CURRENT_BINARY_DIR}
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
```

プロジェクトのルートに`build`ディレクトリを作成する想定なので，`${CMAKE_CURRENT_BINARY_DIR}`は，`ctest_multitest/build`です．
`${CMAKE_CURRENT_SOURCE_DIR}`は現在処理しているCMakeLists.txtのある場所なので，この場合は`ctest_multitest/tests/feat1`です．

## ビルドとテストの実行
ここまで設定すれば，ビルドとテストが実行できるようになります．

```
ctest_multitest
├── tests
│   ├── feat1
│   │   ├── CMakeLists.txt
│   │   └── test_feat1.f90
│   └── CMakeLists.txt
└── CMakeLists.txt
```

プロジェクトのルート（`ctest_multitest`）で作業をする前提でコマンドを示します．

```console
ctest_multitest> mkdir build
ctest_multitest\build> cd build
ctest_multitest\build> cmake ..
-- The Fortran compiler identification is GNU 10.3.0
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
（省略）

ctest_multitest> make
Scanning dependencies of target test_feat1
[ 50%] Building Fortran object tests/feat1/CMakeFiles/test_feat1.dir/test_feat1.f90.obj
[100%] Linking Fortran executable test_feat1.exe
[100%] Built target test_feat1

ctest_multitest> ctest .
Test project （略）/ctest_multitest/build
    Start 1: feat1
1/1 Test #1: feat1 ............................   Passed    0.02 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.02 sec
```

少し前のバージョンから，CMakeにオプションが色々と加わり，ビルドディレクトリの作成やビルドディレクトリへの移動をする必要がなくなりました．
設定，ビルド，テストを，それぞれ対応するコマンド＋オプションで実行できるようになり，判りやすくなりました．

```console
ctest_multitest> cmake -B build
-- The Fortran compiler identification is GNU 10.3.0
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
（省略）

ctest_multitest> cmake --build build
Scanning dependencies of target test_feat1
[ 50%] Building Fortran object tests/feat1/CMakeFiles/test_feat1.dir/test_feat1.f90.obj
[100%] Linking Fortran executable test_feat1.exe
[100%] Built target test_feat1

ctest_multitest> cmake --build build --target test
Running tests...
Test project （略）/ctest_multitest/build
    Start 1: feat1
1/1 Test #1: feat1 ............................   Passed    0.02 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.02 sec
```

全てのテストがパスすると，Passedとして表されます．ここで，例えば`test_feat1_component2`が必ず失敗するように`if (.false.) error stop`を`if (.true.) error stop`に変更すると，テストは結果は下記のように変化します．

```console
ctest_multitest> cmake --build build --target test
Running tests...
Test project （略）/ctest_multitest/build
    Start 1: feat1
1/1 Test #1: feat1 ............................***Failed    0.06 sec

0% tests passed, 1 tests failed out of 1

Total Test time (real) =   0.07 sec

The following tests FAILED:
          1 - feat1 (Failed)
Errors while running CTest
Output from these tests are in: （略）/ctest_multitest/build/Testing/Temporary/LastTest.log
Use "--rerun-failed --output-on-failure" to re-run the failed cases verbosely.
make.exe: *** [test] Error 8
```

## 複数の機能に対するテストの実行
ここまで，一つの機能に対するテストだけを実行してきました．複数の機能に対するテストも問題無くできる事を確認します．

下記のように，`feat1`に加えて`feat2`のテストも行います．ディレクトリおよびファイルの構成は同じです．

```
ctest_multitest
├── tests
│   ├── feat1
│   │   ├── CMakeLists.txt
│   │   └── test_feat1.f90
│   ├── feat2
│   │   ├── CMakeLists.txt
│   │   └── test_feat2.f90
│   └── CMakeLists.txt
└── CMakeLists.txt
```

`tests`ディレクトリの`CMakeLists.txt`に，`feat2`を追加します．

```CMake:tests/CMakeLists.txt
add_subdirectory(feat1)
add_subdirectory(feat2)
```

`feat2`以下にあるファイルは，今回は楽をするために，`feat1`の内容と同じにして，`feat1`と書かれている部分を`feat2`に置き換え，テストは成功するようにしました．

```Fortran:tests/feat2/test_feat2.f90
program test_feat2
    use, intrinsic :: iso_fortran_env
    implicit none

    print '(A)', "feature 2 test"

    call test_feat2_component1()
    call test_feat2_component2()

contains

    subroutine test_feat2_component1()
        print *, "test_feat2_component1"

        if (.false.) error stop
    end subroutine test_feat2_component1

    subroutine test_feat2_component2()
        print *, "test_feat2_component2"

        if (.false.) error stop
    end subroutine test_feat2_component2
end program test_feat2
```

```CMake:tests/feat2/CMakeLists.txt
set(name feat2)

add_executable(test_${name} test_${name}.f90)

add_test(NAME ${name}
    COMMAND $<TARGET_FILE:test_${name}> ${CMAKE_CURRENT_BINARY_DIR}
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
```

先ほどと同様にビルドを実行してテストを行うと，二つの機能に対するテストが実行されていることが確認できます．

```console
ctest_multitest> cmake -B build
（省略）

ctest_multitest> cmake --build build
Scanning dependencies of target test_feat1
[ 25%] Building Fortran object tests/feat1/CMakeFiles/test_feat1.dir/test_feat1.f90.obj
[ 50%] Linking Fortran executable test_feat1.exe
[ 50%] Built target test_feat1
Scanning dependencies of target test_feat2
[ 75%] Building Fortran object tests/feat2/CMakeFiles/test_feat2.dir/test_feat2.f90.obj
[100%] Linking Fortran executable test_feat2.exe
[100%] Built target test_feat2

ctest_multitest> cmake --build build --target test
Running tests...
Test project （略）/ctest_multitest/build
    Start 1: feat1
1/2 Test #1: feat1 ............................***Failed    0.07 sec
    Start 2: feat2
2/2 Test #2: feat2 ............................   Passed    0.07 sec

50% tests passed, 1 tests failed out of 2

Total Test time (real) =   0.14 sec

The following tests FAILED:
          1 - feat1 (Failed)
Errors while running CTest
Output from these tests are in: （略）/ctest_multitest/build/Testing/Temporary/LastTest.log
Use "--rerun-failed --output-on-failure" to re-run the failed cases verbosely.
make.exe: *** [test] Error 8
```

## テストを登録する関数
テストの登録は，全く同じ書式で`add_test`を実行するだけです．機能ごとに毎回書くのは煩わしいので，stdlibで採用されている関数を利用します．

`tests`ディレクトリの`CMakeLists.txt`に，下記の関数を追加し，各機能の`CMakeLists.txt`内に書いていた`add_test`を全て関数に置き換えます．

```cmake
function(ADDTEST name)
    ### テスト用プログラムのビルド設定
    add_executable(test_${name} test_${name}.f90)

    # モジュールを参照するときは，ここにtarget_link_librariesやset_target_propertiesを追加する

    ### テストの登録
    add_test(NAME ${name}
             COMMAND $<TARGET_FILE:test_${name}> ${CMAKE_CURRENT_BINARY_DIR}
             WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endfunction(ADDTEST)
```

```CMake:tests/feat1/CMakeLists.txt
ADDTEST(feat1)
```
```CMake:tests/feat2/CMakeLists.txt
ADDTEST(feat2)
```

# 実践例
テストのみを作成してきましたが，実際の問題では，何らかの機能を実装し，それが正しいかを確認するためにテストを記述するはずです．

そこで，簡単な機能をモジュールとして実装し，そのモジュールをテストから呼び出す方法を説明します．

## テスト問題とプロジェクトの構造
整数に対する演算手続を定義したモジュール`integerOperation`を作成し，そのテストを記述します．ディレクトリの構造は下記のようにしました．

```
ctest_integerOperation
├── src
│   ├── CMakeLists.txt
│   ├── main.f90
│   └── mod_integerOperation.f90
├── tests
│   ├── integerOperation
│   │   ├── CMakeLists.txt
│   │   └── test_integerOperation.f90
│   └── CMakeLists.txt
└── CMakeLists.txt
```

stdlibでは，`src`ディレクトリの下に`tests`ディレクトリを置いているので，全く同じではありませんが，やることはほとんど変わりません．

## モジュールと主プログラム
モジュール`integerOperation`では，整数に対する加算および減算を行う手続`add_int_int`, `sub_int_int`が定義されています．

```Fortran:src/mod_integerOperation.f90
module integerOperation
    use, intrinsic :: iso_fortran_env
    implicit none
    private
    public :: add_int_int
    public :: sub_int_int

contains

    function add_int_int(x_l, x_r) result(added)
        integer(int32), intent(in) :: x_l
        integer(int32), intent(in) :: x_r

        integer(int32) :: added

        added = x_l + x_r
    end function add_int_int

    function sub_int_int(x_l, x_r) result(subtracted)
        integer(int32), intent(in) :: x_l
        integer(int32), intent(in) :: x_r

        integer(int32) :: subtracted

        subtracted = x_l - x_r
    end function sub_int_int

end module integerOperation
```

主プログラムでは，モジュール`integerOperation`を`use`して，呼び出しています．

```Fortran:src/main.f90
program main
    use :: integerOperation
    implicit none

    print '(A, I0)', "1 + 2 = ", add_int_int(1, 2)
    print '(A, I0)', "3 - 4 = ", sub_int_int(3, 4)
end program main
```

### 主プログラムのビルド設定
主プログラムおよびモジュールのビルドは，`src`ディレクトリにある`CMakeLists.txt`で設定します．
モジュール`integerOperation`はライブラリとしてビルドし，主プログラムとリンクします．

```CMake:src/CMakeLists.txt
### integerOperationの設定
set(LIB_SRC
    mod_integerOperation.f90
)

add_library(integerOperation ${LIB_SRC})

set_target_properties(integerOperation
    PROPERTIES
    Fortran_MODULE_DIRECTORY ${LIB_MOD_DIR}
)

### 主プログラムの設定
add_executable(${PROJECT_NAME} main.f90)
target_link_libraries(${PROJECT_NAME} integerOperation)
```

`integerOperation`をビルドする際，`*.mod`ファイルを`LIB_MOD_DIR`に出力するようにプロパティを設定します．`LIB_MOD_DIR`は，プロジェクトルートの`CMakeLists.txt`で設定しており，`src`および`tests`のビルドの際に参照します．

## テストの作成
モジュール`integerOperation`内の手続`add_int_int`, `sub_int_int`を呼び出し，正しい結果が得られるかを確認するテストを記述します．

ここまで説明した内容に従って，テスト用の主プログラムを作成し，内部手続としてテストを記述します．演算子の左側の数を`int_l = 7`，右側の数を`int_r = 13`として，`add_int_int`および`sub_int_int`の戻り値と正解を比較しています．

```Fortran:tests/test_integerOperation.f90
program test_integerOperation
    use, intrinsic :: iso_fortran_env
    use :: integerOperation, only:add_int_int, sub_int_int
    implicit none

    integer(int32) :: int_l, int_r

    int_l = 7
    int_r = 13

    print '(A,I0)', "integer to left  of operator: ", int_l
    print '(A,I0)', "integer to right of operator: ", int_r

    call test_add_int_int()
    call test_sub_int_int()

contains

    subroutine test_add_int_int()
        print *, "test_add_int_int"

        if (add_int_int(int_l, int_r) /= 20) error stop
    end subroutine test_add_int_int

    subroutine test_sub_int_int()
        print *, "test_add_int_int"

        if (sub_int_int(int_l, int_r) /= -6) error stop
    end subroutine test_sub_int_int
end program test_integerOperation
```

`add_int_int`および`sub_int_int`の呼出しには，当然ですが，モジュール`integerOperation`を`use`します．その状態で正しくビルドするには，モジュールファイルおよびライブラリが参照できなければなりません．

そこで，テスト登録用の`ADDTEST`関数にそれらの設定を追記します．

```CMake:tests/CMakeLists.txt
function(ADDTEST name)
    ### テスト用プログラムのビルド設定
    add_executable(test_${name} test_${name}.f90)

    ### 参照するmodファイルのディレクトリとリンクするライブラリの設定
    target_link_libraries(test_${name} ${name})
    set_target_properties(test_${name}
        PROPERTIES
        Fortran_MODULE_DIRECTORY ${LIB_MOD_DIR}
    )

    ### テストの登録
    add_test(NAME ${name}
             COMMAND $<TARGET_FILE:test_${name}> ${CMAKE_CURRENT_BINARY_DIR}
             WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endfunction(ADDTEST)

add_subdirectory(integerOperation)
```

```CMake:tests/integerOperation/CMakeLists.txt
ADDTEST(integerOperation)
```

## プロジェクトのビルド設定
プロジェクトルートにある`CMakeLists.txt`は，上に示した例とほとんど同じですが，モジュールファイルを`src`および`tests`のソースから参照できるように，モジュールファイルが出力されるディレクトリのパス`LIB_MOD_DIR`を設定します．

```CMake:CMakeLists.txt
### プロジェクトの設定
cmake_minimum_required(VERSION 3.15.0)
# CMakeのバージョン

project(ctest_integerOperation Fortran)
# プロジェクト名の設定

enable_language (Fortran)
# Fortran向け設定の有効化

### モジュールファイルの出力ディレクトリパスの設定
set(LIB_MOD_DIR ${CMAKE_CURRENT_BINARY_DIR}/mod_files/)

### テストの設定
enable_testing()
#テストの有効化

add_subdirectory(src)
add_subdirectory(tests)
# src, testsのビルドとテストの登録を行う
```

## ビルドとテストの実行
既に説明したコマンドで設定，ビルド，テストを実行します．`LIB_MOD_DIR`を設定しないと，テストをビルドする際に`*.mod`ファイルが参照できず，コンパイルエラーが生じます．

```console
ctest_integerOperation> cmake -B build
-- The Fortran compiler identification is GNU 10.3.0
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
中略

ctest_integerOperation> cmake --build build
Scanning dependencies of target integerOperation
[ 16%] Building Fortran object src/CMakeFiles/integerOperation.dir/mod_integerOperation.f90.obj
[ 33%] Linking Fortran static library libintegerOperation.a
[ 33%] Built target integerOperation
Scanning dependencies of target ctest_integerOperation
[ 50%] Building Fortran object src/CMakeFiles/ctest_integerOperation.dir/main.f90.obj
[ 66%] Linking Fortran executable ctest_integerOperation.exe
[ 66%] Built target ctest_integerOperation
Scanning dependencies of target test_integerOperation
[ 83%] Building Fortran object tests/integerOperation/CMakeFiles/test_integerOperation.dir/test_integerOperation.f90.obj
[100%] Linking Fortran executable test_integerOperation.exe
[100%] Built target test_integerOperation

ctest_integerOperation> cmake --build build --target test
Running tests...
Test project （略）/ctest_integerOperation/build
    Start 1: integerOperation
1/1 Test #1: integerOperation .................   Passed    0.07 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.07 sec
```

また，`src`をビルドして実行ファイルが作られているので，それも実行できます．

```console
ctest_integerOperation>build\src\ctest_integerOperation.exe
1 + 2 = 3
3 - 4 = -1
```

# まとめ
FortranでCTestによるテストの実践例として，[stdlib](https://github.com/fortran-lang/stdlib)で使われている方法を解説しました．この実践方法は，テストドライバーの作成が不要で，一つのソースファイルに複数のテストを記述できます．一つのファイルに一つのテスト関数を設けるよりも簡便で，十分実用に耐えうると思います．

CMakeのバックエンドとしてVisual Studioを用いる場合は設定やコマンドが変わります．そのうち記事にまとめます．