概要
CTestを使ってFortranのプログラムをテストするために,テスト用の関数を作成する方法をまとめました.
- テスト関数をどのように作成するか
- 実行するテストをどのように登録するか,そのためにCMakeLists.txtをどう書くか
が主な内容です.
例はUnit testing with Fortran and CTestを参考にしていますが,モダンFortranにおいて考慮不要な項目を排除しています.
追記
- Fortran-stdlibでのテストの方法を解説した記事を作成.こちらを推奨.FortranでCTestを用いる際の一つの実践例
環境
- Windows 10 Pro 20H2
- cmake 3.14.5
- GNU Make 3.8.1
- gfortran 8.1.0
- VSCode 1.52.0
追記時の環境
- cmake 3.20.3
- gfortran 10.3.0
CTest
CTestは,CMakeの一部として提供されているテスト用ツールです.テストを行う関数(以降,テスト関数)をある規則に従って作成した後,CMakeLists.txt内でテスト関数を登録することで,テストを実行します.登録された関数を全て実行するC言語のソースファイル(テストドライバー)を作成してくれますし,テストの成否や経過時間などの報告もしてくれます.
CTestは,既にCMakeを利用しているのであれば,追加のインストールなしで利用できます.また,テスト関数の戻り値でテストの成否を判別するという,非常に簡単な形式なので,理解が容易です.
テスト関数の作成
CTestで実行するテスト関数するには,いくつかの規則を守る必要があります.
- 一つのテスト関数は一つのファイルに作成する必要があります.複数のテスト関数を一つのファイルにまとめて記述することはできません.
- テスト関数の名前は,CMakeLists.txtから見た相対パスを含むファイル名と一致している必要があります.ただし,拡張子は不要で,ディレクトリの区切り記号は
_
に置き換えます. - テスト関数は,テストが成功したら
0
を返し,テストが失敗したら0
以外の値を返します.本記事では1
を返すようにします.
CTestで実行するテスト関数を,下記のような配置で作成します.
ctest_fortran
├── test
│ ├── failure.f90
│ └── success.f90
└── CMakeLists.txt
名前の通り,success.f90
に成功する(0
を返す)テスト,failure.f90
に失敗する(1
を返す)テストを作成します.
関数の名前
テスト関数の名前は,MakeLists.txtから見た相対パスを含むファイル名と一致している必要があると上述しました.本記事の例では,success.f90
はtest
ディレクトリの中にあるため,CMakeLists.txtから見た相対パスを含む名前はtest\success.f90
となります.この名前と一致するように関数名を決定します.ただし,拡張子は不要で,ディレクトリの区切り記号は_
に置き換えるので,関数名はtest_success
と一意に決まります.これ以外の名前を付けると,リンクエラーになります.
1ファイルに1テスト関数とは決まっていますが,モジュールの中に入れてはいけないという規則はないので,モジュールを作成します.このとき,fpm (Fortran Package Manager)のプロジェクト構造に合わせると,モジュール名もtest_success
となって名前が衝突してしまいます.対策として接尾辞に情報を付け足していくことにしました.今回は単純にTest
と付けているだけですが.
module test_successTest
use, intrinsic :: iso_c_binding
implicit none
private
public :: test_success
contains
!| 成功するテストの例
function test_success() result(test_result) bind(c, name="test_success")
implicit none
integer(c_int) :: test_result
!! テスト結果
test_result = 0
end function test_success
end module test_successTest
CTestは0
が返ってくると成功と判断するので,戻り値となる変数に0
を代入しています.ただし,このテストはC言語のソースから呼ばれるので,この0
はC言語の整数型でなければなりません.そのため,戻り値となる変数はinteger(c_int)
型としています.
integer(c_int) :: test_result
!! テスト結果
test_result = 0
C言語の整数を意味するkind定数c_int
を利用するために,モジュール冒頭でiso_c_binding
をuse
しています.
さて,関数名も戻り値も規則通り定めたので,これで実行できると思われるかも知れませんが,Fortranではもう一つ考慮しなければならない事項があります.
Fortranの関数をC言語から呼ぶ場合,Fortranの関数名を全て小文字にし,末尾に_
を付ける必要がありました(モジュールに属さない外部関数の場合).また,モジュール内に関数を置いた場合は,モジュールの情報が関数名に付与されます.例えば上記のtest_success
関数の内部的な名前は,gfortranでコンパイルすると__test_successtest_MOD_test_success
,intel Fortranではtest_successtest_mp_test_success__
といった形になります.
モジュール名の情報を取得したり,異なるコンパイラに対応するのは非常に大変です.シンボル名を一意に定めるために,関数定義の際にbind(c)
属性を付与し,C言語側から見える名前をname=
で指定しています.
同様に,失敗するテストも一つ作成します.
module test_failureTest
use, intrinsic :: iso_c_binding
implicit none
private
public :: test_failure
contains
!| 失敗するテストの例
function test_failure() result(test_result) bind(c, name="test_failure")
implicit none
integer(c_int) :: test_result
!! テスト結果
test_result = 1
end function test_failure
end module test_failureTest
モジュール名,サブルーチン名,戻り値が異なるだけで,構造や考え方はtest_failure
と全く同じです.
テストの有効化とテストの登録
CMakeを使ったビルドの過程で,テストドライバーを作成したりテスト用の実行ファイルを作成したりする必要があります.
その設定を有効化するために,CMakeLists.txtに追加の記述が必要になります.Fortranのプログラムをビルドするには,CMakeLists.txtに4項目)を記述するのでした1.
- CMakeのバージョン
- Fortranの有効化
- プロジェクト名
- コンパイルするファイルと作成される実行ファイル名
CTestを利用するには,2項目を追加で設定します.
- テストの有効化
- テストの登録
テストの有効化
ビルドの過程で,テストドライバーを作成したりテスト用の実行ファイルを作成するには,
enable_testing()
を追加します.
また,テストドライバーはC言語のソースファイルなので,Fortranを有効化するenable_language(Fortran)
に,C言語を利用することを追記します.つまり,
enable_language(C Fortran)
とします.
テストの登録
CTestでテストをするために,前節で作成したテストを登録します.テストを登録するには,add_test()
を用います.ただし,add_test()
だけでは駄目で,登録するために色々とやることがあります.
テストドライバーの作成
まず,テストの名前をリストTest_Names
に追加し,そのリストとテストドライバーのファイル名(main.c
)をcreate_test_sourcelist()
に渡します.そうすると,リストにあるテストを呼び出すテストドライバーmain.c
が作成されます.create_test_sourcelist()
を実行すると,テストドライバーとテストの名前を持つリスト(ここではTest_Src_List
)が作成されますが,これは利用しません.
set (Test_Names)
list (APPEND Test_Names "test_failure")
list (APPEND Test_Names "test_success")
create_test_sourcelist (Test_Src_List main.c ${Test_Names})
# Test_Namesに登録されたテストを呼び出すテストドライバーがmain.cという名前で作成される.
# テストドライバーとテストの名前を持つリスト Test_Src_List が作られるが,これは利用しない.
テスト関数を集めたライブラリの作成
次に,テスト関数を集めたライブラリを作成します.テスト関数を定義したソースファイル名をリストTest_Files
に追加したあと,add_library()
にライブラリの名前とそのライブラリをビルドするのに必要なソースファイルのリストTest_Files
を渡します.ここで,テストを実行するファイルの名前をtestsuite
とし,テスト関数を集めたライブラリをtestsuite_fortran
と定めています.
set (Test_Files)
list (APPEND Test_Files "test/success.f90")
list (APPEND Test_Files "test/failure.f90")
set (Target testsuite)
add_library (${Target}_fortran ${Test_Files})
テスト用実行ファイルの作成
テストドライバーmain.c
をコンパイルして実行ファイルを作成します.add_executable()
でコンパイルするソースmain.c
と実行ファイル名testsuite
を指定しています.その後,target_link_libraries()
を呼び出して,先に作成したテスト関数を集めたライブラリtestsuite_fortran
をリンクします.
add_executable (${Target} main.c)
target_link_libraries (${Target} ${Target}_fortran)
テストの登録
ここでようやくadd_test()
の出番です.
NAME
はテストの名前です.この名前は呼び出したいテスト関数の名前ではなく,CTestでテストを実行した際に画面に表示されるテストの名前です.特に変える必要もないので,関数名と同じにしています.
COMMAND
に実行するコマンドを指定します.ここでは,先にビルドしたテスト用実行ファイルtestsuite
を指定しています.実行コマンドの引数として,テスト関数の名前を渡しています.add_test()
を眺めると,同じ名前が2回出てきていますが,重要なのは後者(実行コマンドの引数となるテスト関数名)です.
add_test ( NAME "test_failure"
COMMAND $<TARGET_FILE:${Target}> "test_failure")
add_test ( NAME "test_success"
COMMAND $<TARGET_FILE:${Target}> "test_success")
CMakeLists.txt
できあがったCMakeLists.txtをまとめて表示します.
CMakeLists.txt
cmake_minimum_required (VERSION 3.14)
# CMakeのバージョン
enable_language (C Fortran)
# Fortran向け設定の有効化
enable_testing ()
# テストの有効化
set (Test_Names)
list (APPEND Test_Names "test_failure")
list (APPEND Test_Names "test_success")
# テスト関数名の一覧
create_test_sourcelist (Test_Src_List main.c ${Test_Names})
# Test_Namesに登録されたテストを呼び出すテストドライバーがmain.cという名前で作成される.
# テストドライバーとテストの名前を持つリスト Test_Src_List が作られるが,これは利用しない.
set (Test_Files)
list (APPEND Test_Files "test/failure.f90")
list (APPEND Test_Files "test/success.f90")
# テスト関数を定義したソースファイル名一覧
set (Target testsuite)
# テスト用実行ファイル名の設定
add_library (${Target}_fortran ${Test_Files})
# テスト関数を集めたライブラリを作成
add_executable (${Target} main.c)
target_link_libraries (${Target} ${Target}_fortran)
# テスト用実行ファイルの作成とテスト関数を集めたライブラリのリンク
add_test ( NAME "test_failure"
COMMAND $<TARGET_FILE:${Target}> "test_failure")
add_test ( NAME "test_success"
COMMAND $<TARGET_FILE:${Target}> "test_success")
# テストの登録
このCMakeLists.txtを使ってビルドすると,プロジェクトの名前がないと警告が出る場合があります.気になった場合は,プロジェクト名を追加しておいてください.
本記事ではテスト関数のみをビルドしていますが,実際にはビルドしたいプログラムは別にあるはずで,ビルドしたいプログラムの設定でプロジェクトの指定もすることになると思われます.
ビルドとテストの実行
CMakeLists.txtができたら,ビルドをしてテストを実行します.ディレクトリの構造を再掲します.
ctest_fortran
├── test
│ ├── failure.f90
│ └── success.f90
└── CMakeLists.txt
順次コマンドを実行して,ビルドします.Windowsのコマンドプロンプトで作業しているので,プロンプトの記号が>
になっています.
ctest_fortran> mkdir build
ctest_fortran> cd build
ctest_fortran\build> cmake ..
-- The C compiler identification is GNU 8.1.0
-- The CXX compiler identification is GNU 8.1.0
(省略)
ctest_fortran\build> make
Scanning dependencies of target testsuite_fortran
[ 20%] Building Fortran object CMakeFiles/testsuite_fortran.dir/test/failure.f90.obj
[ 40%] Building Fortran object CMakeFiles/testsuite_fortran.dir/test/success.f90.obj
[ 60%] Linking Fortran static library libtestsuite_fortran.a
[ 60%] Built target testsuite_fortran
Scanning dependencies of target testsuite
[ 80%] Building C object CMakeFiles/testsuite.dir/main.c.obj
[100%] Linking C executable testsuite.exe
[100%] Built target testsuite
環境によっては,cmake
を実行する際に,cmake .. -G "Unix Makefiles" -DCMAKE_Fortran_COMPILER=gfortran
のように細かいオプションが必要になる場合もあります.
make
まで無事完了したら,ctest
を実行します.
ctest_fortran\build> ctest .
Test project ctest_fortran/build
Start 1: test_failure
1/2 Test #1: test_failure .....................***Failed 0.01 sec
Start 2: test_success
2/2 Test #2: test_success ..................... Passed 0.01 sec
50% tests passed, 1 tests failed out of 2
Total Test time (real) = 0.04 sec
The following tests FAILED:
1 - test_failure (Failed)
Errors while running CTest
ctest
を実行すると,登録したテストが全て実行されます.表示された結果を見ると,一つのテストが失敗し,一つのテストが成功しています.
小ネタ
画面に表示されるテストの名前(test_failureおよびtest_success)は,add_test()
のNAME
で指定した名前です.この名前を,テスト関数を定義したソースファイル名へのフルパスに変更すると,テストが失敗したときの対応が少し楽になります.
VSCodeの統合ターミナルでは,フルパスをCtrl+左クリックすると,そのファイルをVSCodeで開くことができます.VSCodeでプログラムを作成し,統合ターミナルでテストを実行する場合,テスト名をファイルへのフルパスにしておくと,失敗したテスト関数をすぐに確認できます.
このとき,Windowsであってもディレクトリのセパレータは/
で問題ありません.
再利用性のあるCMakeLists.txtの作成
上で作成したCMakeLists.txtは,ファイルパスやソースファイル名,テスト関数名を直接指定しているので,テスト関数が増えたりパスが変わったりすると,大幅な変更が必要になります.ある程度再利用性のあるCMakeLists.txtを作成し,使い回せるようにします.
変更するといっても,最初の方は変更はありません.
cmake_minimum_required (VERSION 3.14)
# CMakeのバージョン
enable_language (C Fortran)
# Fortran向け設定の有効化
enable_testing ()
# テストの有効化
テスト関数が定義されているソースファイルの一覧を,CMakeLists.txtが存在するディレクトリからの相対パスで取得し,そのソースファイル名から拡張子を取り除くことで,テスト関数名のリストを作成します.
FILE(GLOB_RECURSE Test_Srcs RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test/*.f90")
# CMakeLists.txtが存在するディレクトリからの相対パスで,テスト関数が定義されたソースファイルの一覧を取得
# Test_Srcs=[test/failure.f90;test/success.f90]
set (Test_Names)
foreach (Test_Src ${Test_Srcs})
string (REGEX REPLACE ".f90$" "" C_TestName_with_Slash ${Test_Src})
# ソースファイル名末尾の拡張子を除去し,C_TestName_with_Slashに保存
list (APPEND Test_Names ${C_TestName_with_Slash})
endforeach ()
# create_test_sourcelistに渡すテスト関数名を設定
# Test_Names=[test/failure;test/success]
set (Driver_C main.c)
# テストドライバーのソースファイル名を設定
create_test_sourcelist (Test_Src_List ${Driver_C} ${Test_Names})
# Test_Namesに登録されたテストを呼び出すテストドライバーがmain.cという名前で作成される.
# テスト関数名に/が含まれているが,create_test_sourcelistは内部的に/を_に置き換えるらしい.
# テストドライバーとテストの名前を持つリスト Test_Src_List が作られるが,これは利用しない.
コメントにも書いていますが,create_test_sourcelist()
は内部的に/
を_
に置き換えるらしいので,テスト関数名のリストTest_Names
に/
が含まれたままでも問題ありません.
テスト関数のライブラリ作成や,テスト用実行ファイルの作成については,大きな変化はありません.
set (Target testsuite)
# テスト用実行ファイル名の設定
set (Test_Lib_Name ${Target}_fortran)
# テスト関数を集めたライブラリ名を設定
add_library (${Test_Lib_Name} ${Test_Srcs})
# テスト関数を集めたライブラリを作成
add_executable (${Target} ${Driver_C})
target_link_libraries (${Target} ${Test_Lib_Name})
# テスト用実行ファイルの作成とテスト関数を集めたライブラリのリンク
最後にテストを登録します.ややこしくなっているように見えますが,それはCMakeの繰り返し処理の都合です.やっていることは,Index
を0からLen
まで1ずつ増やしながら,Test_SrcsとTest_Namesの中から要素を一つ取り出し,add_test()
を呼び出してテストの名前とテスト関数名を追加しているだけです.
set (Index 0)
list (LENGTH Test_Srcs Len)
while (${Len} GREATER ${Index})
list (GET Test_Srcs ${Index} Test_Src)
list (GET Test_Names ${Index} Test_Name)
set (Path_to_Test_Src "${CMAKE_CURRENT_SOURCE_DIR}/${Test_Src}")
# ソースファイルへのフルパスを作成
# Index=0 Test_Src=test/failure.f90, Test_Name=test/failure, Path_to_Test_Src=(中略)ctest_fortran/test/failure.f90
# Index=1 Test_Src=test/success.f90, Test_Name=test/success, Path_to_Test_Src=(中略)ctest_fortran/test/success.f90
add_test (
NAME ${Path_to_Test_Src}
COMMAND $<TARGET_FILE:${Target}> ${Test_Name})
# テストの登録
math (EXPR Index "${Index} + 1")
# Indexの値を1増やす
endwhile ()
テストの名前をソースへのフルパスにしないのであれば,${Path_to_Test_Src}
を${Test_Name}
に置き換えることができ,さらに簡潔になります.
CMakeLists.txtの完成版
cmake_minimum_required (VERSION 3.14)
# CMakeのバージョン
enable_language (C Fortran)
# Fortran向け設定の有効化
enable_testing ()
# テストの有効化
FILE(GLOB_RECURSE Test_Srcs RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test/*.f90")
# CMakeLists.txtが存在するディレクトリからの相対パスで,テスト関数が定義されたソースファイルの一覧を取得
set (Test_Names)
foreach (Test_Src ${Test_Srcs})
string (REGEX REPLACE ".f90$" "" C_TestName_with_Slash ${Test_Src})
# ソースファイル名末尾の拡張子を除去し,C_TestName_with_Slashに保存
list (APPEND Test_Names ${C_TestName_with_Slash})
# create_test_sourcelistに渡すテスト関数名を設定
endforeach ()
set (Driver_C main.c)
# テストドライバーのソースファイルを設定
create_test_sourcelist (Test_Src_List ${Driver_C} ${Test_Names})
# Test_Namesに登録されたテストを呼び出すテストドライバーがmain.cという名前で作成される.
# テスト関数名に/が含まれているが,create_test_sourcelistは内部的に/を_に置き換えるらしい.
# テストドライバーとテストの名前を持つリスト Test_Src_List が作られるが,これは利用しない.
set (Target testsuite)
# テスト用実行ファイル名の設定
set (Test_Lib_Name ${Target}_fortran)
# テスト関数を集めたライブラリの名前を設定
add_library (${Test_Lib_Name} ${Test_Srcs})
# テスト関数を集めたライブラリを作成
add_executable (${Target} ${Driver_C})
target_link_libraries (${Target} ${Test_Lib_Name})
# テスト用実行ファイルの作成とテスト関数を集めたライブラリのリンク
set (Index 0)
list (LENGTH Test_Srcs Len)
while (${Len} GREATER ${Index})
list (GET Test_Srcs ${Index} Test_Src)
list (GET Test_Names ${Index} Test_Name)
set (Path_to_Test_Src "${CMAKE_CURRENT_SOURCE_DIR}/${Test_Src}")
# ソースファイルへのフルパスを作成
add_test (
NAME ${Path_to_Test_Src}
COMMAND $<TARGET_FILE:${Target}> ${Test_Name})
# テストの登録
math (EXPR Index "${Index} + 1")
# Indexの値を1増やす
endwhile ()
まとめ
CTestを使ってFortranのプログラムをテストするために,テスト用の関数を作成・登録する方法をまとめました.
CTestで実行するテスト関数の作成については,Fortran 2003の機能を使えば簡単で困ることはありません.また,テストの成否はテスト関数の戻り値で判定されるので,作るのも簡単です.
一方で,テスト関数を登録するためにCMakeLists.txtを書くのが大変です.これは再利用性のある書き方を見つければ,何とかなりそうです.
テスト関数を作ること自体は簡単ですが,どのようなテストを作るかは,しっかりと考えなければなりません.一つのテスト関数を簡潔にして,複数のテストを実行するのが想定される使い方だと思いますが,テストが多くなるとソースファイルの数も増えて,新たなテストを作るのも既存のテストを管理するのも大変になりそうです.assert
文で止める方法とは異なり,テストが失敗したことは判りますが,どこで失敗しているか等は,テスト結果からはわからなさそうです.
CTestの利点として,CMakeを使っていれば追加のインストールが不要,テストの形式が簡単の2点を挙げましたが,他のFortran向けテストフレームワークと比較して有利なのは,
- プログラムのビルドの複雑さ自体は変わらないこと
- Fortran標準から外れずにテストを作成できること(テストドライバーを除く)
だと思います.他のテストフレームワークを用いると,独自の拡張子のファイルを作成する必要があり,テストフレームワークを一度通さないとコンパイル自体ができません.それに伴ってプログラムのビルドが複雑になりますが,CTestはそれがありません.総合的にみて,CTestは悪くない選択肢だと思います.
CTestを利用した,物理シミュレーションプログラムのテストのベストプラクティスが蓄積されていくことを期待します(他力本願).