概要
以前の記事(FortranでCTest用のテストを作成する)において,CTestを使ってFortranのプログラムをテストする方法を紹介しました.
一つのソースファイルに一つのテスト関数を書き,CMakeLists.txt内でテストドライバーを自動生成してそこからテスト関数を呼び出す方法です.
明確ではありましたが,テストが多くなるとソースファイルの数も増えて,新たなテストを作るのも既存のテストを管理するのも大変になりそうだと感じていました.
fortran-lang communityによって開発が進められている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_
を付けており,これはファイル名と同じです.
テスト手続名は,主プログラム名+具体的にテストしたい関数やサブルーチンの名前とすることを想定しています.
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_minimum_required(VERSION 3.15.0)
# CMakeのバージョン
project(ctest_multitest Fortran)
# プロジェクト名の設定
enable_language (Fortran)
# Fortran向け設定の有効化
### テストの設定
enable_testing()
#テストの有効化
add_subdirectory(tests)
# テスト用のディレクトリを確認し,テストのビルドと登録を行う
プロジェクトのルートに置いてあるCMakeLists.txt
では,通常のプロジェクト設定に加えて,
enable_testing()
でテストを有効化しています.その後,
add_subdirectory(tests)
でテスト用のディレクトリtests
の内容を見に行き,そこに置いてあるCMakeLists.txtに従って処理を続けます.
testsディレクトリのCMakeLists.txt
add_subdirectory(feat1)
# テストを追加する場合は,ここにadd_subdirectory()を追記していく
tests
ディレクトリのCMakeLists.txt
は,ビルドするテストを管理しています.管理といっても,tests
ディレクトリにあるテスト用のディレクトリを見に行くよう,add_subdirectory
が書かれているだけです.
ここでコメントアウト/アンコメントすることで,実行するテストを選択できます.
各機能のディレクトリの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
)をビルドして実行ファイルを作り,テストを登録するための設定が書かれています.
add_executable(test_${name} test_${name}.f90)
でtest_feat1.f90
からtest_feat1.exe
を作るよう設定しています.
テストの登録では,NAME
でテストの名前,COMMAND
でテストを実行する実行ファイル名とその場所を指定し,WORKING_DIRECTORYでテストを実行するディレクトリを指定しています.
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
)で作業をする前提でコマンドを示します.
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にオプションが色々と加わり,ビルドディレクトリの作成やビルドディレクトリへの移動をする必要がなくなりました.
設定,ビルド,テストを,それぞれ対応するコマンド+オプションで実行できるようになり,判りやすくなりました.
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
に変更すると,テストは結果は下記のように変化します.
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
を追加します.
add_subdirectory(feat1)
add_subdirectory(feat2)
feat2
以下にあるファイルは,今回は楽をするために,feat1
の内容と同じにして,feat1
と書かれている部分をfeat2
に置き換え,テストは成功するようにしました.
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
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}
)
先ほどと同様にビルドを実行してテストを行うと,二つの機能に対するテストが実行されていることが確認できます.
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
を全て関数に置き換えます.
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)
ADDTEST(feat1)
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
が定義されています.
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
して,呼び出しています.
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
はライブラリとしてビルドし,主プログラムとリンクします.
### 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
の戻り値と正解を比較しています.
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
関数にそれらの設定を追記します.
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)
ADDTEST(integerOperation)
プロジェクトのビルド設定
プロジェクトルートにあるCMakeLists.txt
は,上に示した例とほとんど同じですが,モジュールファイルをsrc
およびtests
のソースから参照できるように,モジュールファイルが出力されるディレクトリのパスLIB_MOD_DIR
を設定します.
### プロジェクトの設定
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
ファイルが参照できず,コンパイルエラーが生じます.
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
をビルドして実行ファイルが作られているので,それも実行できます.
ctest_integerOperation>build\src\ctest_integerOperation.exe
1 + 2 = 3
3 - 4 = -1
まとめ
FortranでCTestによるテストの実践例として,stdlibで使われている方法を解説しました.この実践方法は,テストドライバーの作成が不要で,一つのソースファイルに複数のテストを記述できます.一つのファイルに一つのテスト関数を設けるよりも簡便で,十分実用に耐えうると思います.
CMakeのバックエンドとしてVisual Studioを用いる場合は設定やコマンドが変わります.そのうち記事にまとめます.