Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
5
Help us understand the problem. What are the problem?

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

概要

以前の記事(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_を付けており,これはファイル名と同じです.

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

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

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

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

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

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

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

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)をビルドして実行ファイルを作り,テストを登録するための設定が書かれています.

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 stopif (.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を追加します.

tests/CMakeLists.txt
add_subdirectory(feat1)
add_subdirectory(feat2)

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

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
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}
)

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

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を全てマクロに置き換えます.

macro(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})
endmacro(ADDTEST)
tests/feat1/CMakeLists.txt
ADDTEST(feat1)
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が定義されています.

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

主プログラムでは,モジュールintegerOperationuseして,呼び出しています.

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はライブラリとしてビルドし,主プログラムとリンクします.

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の戻り値と正解を比較しています.

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の呼出しには,当然ですが,モジュールintegerOperationuseします.その状態で正しくビルドするには,モジュールファイルおよびライブラリが参照できなければなりません.

そこで,テスト登録用のマクロADDTESTマクロにそれらの設定を追記します.

tests/CMakeLists.txt
macro(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})
endmacro(ADDTEST)

add_subdirectory(integerOperation)
tests/integerOperation/CMakeLists.txt
ADDTEST(integerOperation)

プロジェクトのビルド設定

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

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ファイルが参照できず,コンパイルエラーが生じます.

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を用いる場合は設定やコマンドが変わります.そのうち記事にまとめます.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
5
Help us understand the problem. What are the problem?