LoginSignup
6
4

More than 1 year has passed since last update.

FortranでCTest用のテストを作成する

Last updated at Posted at 2021-05-07

概要

CTestを使ってFortranのプログラムをテストするために,テスト用の関数を作成する方法をまとめました.

  1. テスト関数をどのように作成するか
  2. 実行するテストをどのように登録するか,そのためにCMakeLists.txtをどう書くか

が主な内容です.

例はUnit testing with Fortran and CTestを参考にしていますが,モダンFortranにおいて考慮不要な項目を排除しています.

追記

環境

  • 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.f90testディレクトリの中にあるため,CMakeLists.txtから見た相対パスを含む名前はtest\success.f90となります.この名前と一致するように関数名を決定します.ただし,拡張子は不要で,ディレクトリの区切り記号は_に置き換えるので,関数名はtest_successと一意に決まります.これ以外の名前を付けると,リンクエラーになります.

1ファイルに1テスト関数とは決まっていますが,モジュールの中に入れてはいけないという規則はないので,モジュールを作成します.このとき,fpm (Fortran Package Manager)のプロジェクト構造に合わせると,モジュール名もtest_successとなって名前が衝突してしまいます.対策として接尾辞に情報を付け足していくことにしました.今回は単純にTestと付けているだけですが.

test/success.f90
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_bindinguseしています.

さて,関数名も戻り値も規則通り定めたので,これで実行できると思われるかも知れませんが,Fortranではもう一つ考慮しなければならない事項があります.

Fortranの関数をC言語から呼ぶ場合,Fortranの関数名を全て小文字にし,末尾に_を付ける必要がありました(モジュールに属さない外部関数の場合).また,モジュール内に関数を置いた場合は,モジュールの情報が関数名に付与されます.例えば上記のtest_success関数の内部的な名前は,gfortranでコンパイルすると__test_successtest_MOD_test_success,intel Fortranではtest_successtest_mp_test_success__といった形になります.

モジュール名の情報を取得したり,異なるコンパイラに対応するのは非常に大変です.シンボル名を一意に定めるために,関数定義の際にbind(c)属性を付与し,C言語側から見える名前をname=で指定しています.

同様に,失敗するテストも一つ作成します.

test/failure.f90
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

  1. CMakeのバージョン
  2. Fortranの有効化
  3. プロジェクト名
  4. コンパイルするファイルと作成される実行ファイル名

CTestを利用するには,2項目を追加で設定します.

  1. テストの有効化
  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
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を利用した,物理シミュレーションプログラムのテストのベストプラクティスが蓄積されていくことを期待します(他力本願).

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4