GoogleTest TEST_P使い方

ChatGPTにきいた 後で触ってまとめ直す予定

A

Google Test(gtest)は、C++で書かれたコードをテストするためのフレームワークです。TEST_P()マクロは、パラメータ化されたテストを定義する際に使用します。パラメータ化されたテストは、複数の入力値を持つテストケースを効率的に作成できるため、コードの再利用性が向上します。

それでは、新人エンジニアの皆さんにTEST_P()の使い方を説明しましょう。例として、文字列を逆にする関数reverse_stringのテストを考えます。

  1. まず、Google Testをインクルードし、必要な名前空間を使用します。
#include <gtest/gtest.h>using testing::TestWithParam;
using testing::Values;
  1. 次に、テストしたい関数reverse_stringを定義します。
std::string reverse_string(const std::string &input) {
    std::string reversed(input.rbegin(), input.rend());
    return reversed;
}
  1. テストケースを定義するために、TestWithParamを継承したReverseStringTestクラスを作成します。このクラスは、テスト対象の関数に渡すパラメータと期待される結果を持つペア(std::pair)を使用します。
class ReverseStringTest : public TestWithParam<std::pair<std::string, std::string>> {
};
  1. TEST_Pマクロを使用して、パラメータ化されたテストを定義します。この例では、ReverseStringTestクラスのCheckテストを定義しています。
TEST_P(ReverseStringTest, Check) {
    std::pair<std::string, std::string> test_data = GetParam();
    std::string input = test_data.first;
    std::string expected_output = test_data.second;
    EXPECT_EQ(reverse_string(input), expected_output);
}
  1. 最後に、INSTANTIATE_TEST_SUITE_Pマクロを使って、テストに使用するパラメータのセットを定義します。
INSTANTIATE_TEST_SUITE_P(
    ReverseStringTestCases, ReverseStringTest,
    Values(std::make_pair("abc", "cba"),
           std::make_pair("123", "321"),
           std::make_pair("あいう", "ういあ")));
  1. テストを実行するために、main関数を追加します。
int main(int argc, char **argv) {
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

これで、TEST_P()を使ってパラメータ化されたテストを作成し、実行する準備が整いました。今回の例では、reverse_string関数が正しく文字列を逆にするかどうかを3つの異なる入力値を使って確認できます。

Q

引数が5つある関数をテストする例を教えてくれませんか?

A

もちろんです。引数が5つある関数のテスト例を示します。この例では、5つの整数を引数にとり、それらの整数の合計を計算するsum_of_integers関数をテストします。

  1. まず、Google Testをインクルードし、必要な名前空間を使用します。
#include <gtest/gtest.h>using testing::TestWithParam;
using testing::Values;
  1. 次に、テストしたい関数sum_of_integersを定義します。
int sum_of_integers(int a, int b, int c, int d, int e) {
    return a + b + c + d + e;
}
  1. テストケースを定義するために、TestWithParamを継承したSumOfIntegersTestクラスを作成します。このクラスは、テスト対象の関数に渡すパラメータと期待される結果を持つタプルを使用します。
class SumOfIntegersTest : public TestWithParam<std::tuple<int, int, int, int, int, int>> {
};
  1. TEST_Pマクロを使用して、パラメータ化されたテストを定義します。この例では、SumOfIntegersTestクラスのCheckテストを定義しています。
TEST_P(SumOfIntegersTest, Check) {
    auto [a, b, c, d, e, expected] = GetParam();
    EXPECT_EQ(sum_of_integers(a, b, c, d, e), expected);
}
  1. 最後に、INSTANTIATE_TEST_SUITE_Pマクロを使って、テストに使用するパラメータのセットを定義します。
INSTANTIATE_TEST_SUITE_P(
    SumOfIntegersTestCases, SumOfIntegersTest,
    Values(std::make_tuple(1, 2, 3, 4, 5, 15),
           std::make_tuple(-1, 2, -3, 4, -5, -3),
           std::make_tuple(0, 0, 0, 0, 0, 0)));
  1. テストを実行するために、main関数を追加します。
int main(int argc, char **argv) {
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

これで、引数が5つある関数のパラメータ化されたテストを作成し、実行する準備が整いました。今回の例では、sum_of_integers関数が正しく5つの整数の合計を計算するかどうかを3つの異なる入力値を使って確認できます。

Q

テスト対象のコードはシステムの実装として別で存在しており、 テスト実行コードはそれとは別でファイルを拵えるようにしたいです。 そのような構成にする場合どのようなコードになりますか?

A

テスト対象のコードとテスト実行コードを別々のファイルに分ける場合、以下のような構成になります。

ファイル構成:

project/
├── src/
│   ├── sum_of_integers.cpp
│   └── sum_of_integers.h
└── tests/
    ├── sum_of_integers_test.cpp
    └── CMakeLists.txt (オプション: CMakeを使用する場合)

src/sum_of_integers.h:

#pragma once

// テスト対象の関数
int sum_of_integers(int a, int b, int c, int d, int e);

src/sum_of_integers.cpp:

#include "sum_of_integers.h"int sum_of_integers(int a, int b, int c, int d, int e) {
    return a + b + c + d + e;
}

tests/sum_of_integers_test.cpp:

#include <gtest/gtest.h>#include <tuple>#include "sum_of_integers.h"// 必要な名前空間を使用
using testing::TestWithParam;
using testing::Values;

// パラメータ化されたテストケースを定義するクラス
class SumOfIntegersTest : public TestWithParam<std::tuple<int, int, int, int, int, int>> {
};

// パラメータ化されたテストを定義
TEST_P(SumOfIntegersTest, Check) {
    auto [a, b, c, d, e, expected] = GetParam();
    EXPECT_EQ(sum_of_integers(a, b, c, d, e), expected);
}

// テストに使用するパラメータのセットを定義
INSTANTIATE_TEST_SUITE_P(
    SumOfIntegersTestCases, SumOfIntegersTest,
    Values(std::make_tuple(1, 2, 3, 4, 5, 15),
           std::make_tuple(-1, 2, -3, 4, -5, -3),
           std::make_tuple(0, 0, 0, 0, 0, 0)));

// テストを実行するためのmain関数
int main(int argc, char **argv) {
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

この構成では、sum_of_integers.hsum_of_integers.cppがテスト対象のコードで、sum_of_integers_test.cppがテスト実行コードです。テスト対象の関数sum_of_integersを別ファイルに定義し、テスト実行コードではそのヘッダーファイルをインクルードしています。

プロジェクトのビルドと実行には、ビルドシステムを選択して設定する必要があります。例えば、CMakeを使用してビルドとテストを実行するためのCMakeLists.txtファイルを作成できます。

カレントディレクトリ配下のfileをtouchしたいとき

結論

こんな感じでxargsを使えばOK

find -name *.cpp | xargs -I {} touch {}

解説

  • このコマンドは、カレントディレクトリ以下のすべての .cpp ファイルを検索し、それらのファイルに対して touch コマンドを実行するもの。
  • findコマンドは、指定された条件に一致するファイルを検索するために使用される。
    • この場合、 -name オプションを使用してファイル名のパターンを指定している。
    • *.cpp は、カレントディレクトリ以下のすべての .cpp ファイルにマッチするように指定されている。
  • | (パイプ)は、前のコマンドの出力を次のコマンドに渡すために使用される。
  • xargs コマンドは、前のコマンドの出力を引数として次のコマンドを実行するために使用される。
  • -I {} オプションは、 xargs に置換文字列 {} を使用するよう指示しています。
  • touch コマンドは、指定されたファイルにタイムスタンプを追加するために使用される。
    • この場合、 xargs によって渡されたファイル名が touch コマンドの引数として使用され、それらのファイルにタイムスタンプが追加される

疑問と回答

awkコマンド使えないの?

これでいける

find -name "*.cpp" | awk '{ system("touch " $0) }'

  • ただし、 xargs コマンドと異なり、 awk コマンドは一度に1つのファイルしか処理できないため、実行時間が長くなる可能性がある。
  • awkコマンド単体ではファイルに対する操作は行なえない。
    • そのためsystem関数を使用して、外部コマンドのtouchを実行することができる
    • findの出力はパスのみなのでその結果は$0に与えられる。($1じゃないよ)

xargsの{}なかったらどうなるの?

  • xargs コマンドを使用する場合、{} を使用することで、xargs が受け取った入力をコマンドライン引数に展開することができる。つまり、 find コマンドの出力に対して、touch コマンドを実行することができる。
  • 一方、{} を使用しない場合、 xargs が受け取った入力を単純にコマンドライン引数として扱う。つまり、findの出力結果複数行をまるっとtouchの引数にすることになってしまう。
    • だから{}が必要。{}があればfindの結果一行ずつをtouchの引数に渡せる。らしい

階層化された複数のCMakeLists.txtで同じコンパイルオプションを共有する方法

ChatGPTさんに質問した情報をメモに残す。
TODO: あとで自分で試して推敲する。

メモ

最も一般的な方法は、最上位のCMakeLists.txtで共通のコンパイルオプションを設定し、それらを下位のディレクトリに伝播させることです。 最上位のCMakeLists.txtで、以下のように共通のコンパイルオプションを設定することができます。

cmake_minimum_required(VERSION 3.2)
project(TopLevelProject)

# 共通のコンパイルオプションを設定
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -std=c++11")

# サブディレクトリを追加
add_subdirectory(subdir1)
add_subdirectory(subdir2)

この方法では、CMAKE_CXX_FLAGS変数にコンパイルオプションを追加し、その後でadd_subdirectoryを使ってサブディレクトリを追加します。これにより、設定されたコンパイルオプションがサブディレクトリのCMakeLists.txtにも適用されます。

また、target_compile_options コマンドを使用して、特定のターゲットに対してコンパイルオプションを設定することもできます。この方法では、最上位のCMakeLists.txtでインターフェイスライブラリを作成し、そのライブラリにコンパイルオプションを設定します。その後、サブディレクトリのターゲットでそのインターフェイスライブラリをリンクすることで、コンパイルオプションを共有できます。

最上位のCMakeLists.txt:

cmake_minimum_required(VERSION 3.2)
project(TopLevelProject)

# インターフェイスライブラリを作成
add_library(common_options INTERFACE)

# 共通のコンパイルオプションを設定
target_compile_options(common_options INTERFACE -Wall -Wextra -std=c++11)

# サブディレクトリを追加
add_subdirectory(subdir1)
add_subdirectory(subdir2)

サブディレクトリのCMakeLists.txt:

# サブディレクトリのターゲットを作成
add_executable(subdir1_target main1.cpp)

# 共通のコンパイルオプションをリンク
target_link_libraries(subdir1_target common_options)

この方法では、common_options インターフェイスライブラリに設定されたコンパイルオプションが、サブディレクトリのターゲットに適用されます。

シェルワンライナー: 問題5

問題

ファイル ntp.confについて、poolの項目( 1列目にpoolと書かれている行)にあるサーバの名前 を抽出してみましょう。

回答

# 自分の回答
cat ntp.conf | awk '$1=="pool" {print $2}'
# ちなみにテキストの答えはこちら
# こっちのが書きやすい。
# スピード意識しなくていいので書きやすい方で良いよね。とういことらしい。
cat ntp.conf | awk '$1=="pool"' | awk '{print $2}'

調べたこと

grep について

  • grepのパターン指定ってどうやるんだっけ?
    • grep 'red.*apple' example.txt
    • 'でくくればよい

awkについて

  • awkってどういうコマンドだっけ?
    • awk ‘/正規表現/’grep ‘正規表現’ と同じ意味
  • awk の $1ってどういう意味?
    • 入力の一列目ってことらしい
    • 列っていうのはスペースで区切られる?
      • デフォルトでは スペースとタブ
      • -f を使えば任意の区切り文字を指定できる。
  • {} でくくらないとだめなんでしたっけ?
    • そもそもawk$awk pattern { action } っていう形式
    • patternはフィルタ。正規表現とかで入力の処理する部分を指定する
    • actionは実際に実行するコマンド
  • ‘ hoge ‘ で囲わないとだめなんでしたっけ?
    • 文字列定数と呼ばれるらしい
    • こいつで囲っておくと特殊文字がでてきてもバックスラッシュが不要になるので便利らしい。

感想

  • 答え見ずに正解できた。やったねぇ
  • ググる代わりにchatGPTはやっぱりかなり有効な気がする
    • 無駄な情報の中から、必要な情報を探す必要がないし
    • 必要な情報だけを教えてくれるから、頭にも入りやすい
    • 間違った情報かどうかは、こういう答えがすぐわかったり、枯れた技術に関することではあまりリスクが無いと思う。
      • あとで、正式ドキュメントを追って確認すればいいし。
    • 初心者が、階段登って行くようなフェーズにおいてはかなり有効な気がする。

シェルワンライナー160本ノック: 問題4

問題

まず、ランダムな数字が入ったファイルをたくさん作る

$ mkdir ./tmp 
$ cd ./tmp 
$ seq 1000000 | sed 's/^/ echo $ RANDOM > /' | bash

これで1000000個のファイルが作成され、各ファイルに0-32767のうち一つの整数がランダムに記述されている。という状態ができる

このファイルの中から10という数字が書かれているファイルを削除する。

答え

まずは対象ファイルのみを出力してみる

$ grep -l '^10$' -R
  • -l オプション
    • 一致する行が見つかった場合、ファイル名のみを出力する。デフォルトでは、一致する行の内容も出力されるが、このオプションを使用するとファイル名のみが表示される。
  • '^10$
    • ^は先頭の意味。 $ は末尾の意味。なので行は 10 のみが書かれているかどうか。ということ
  • -R
  • 検索対象のディレクトリが何も指定されていないが、もし何も指定されない場合GNU grepではカレントディレクトリが指定されるらしい。

このコマンドで消したいファイル名が抽出できるので、これをxargsで繋いでrmコマンドを叩けばOK

$ grep -l '^10$' -R | xargs rm

解説

■問題でやっていること

  • seq 1000000
    • 1 - 1000000まで繰り返し整数を生成
  • sed 's/^/echo $RANDOM >/'
    • sedコマンドを使って、入力された各行の先頭に、 echo $RANDOM > を追加する
    • 即ち、各行は echo $RANDOM > [seqで生成される数値]ということになる。
      • 「seqで生成される数値」が名前のファイルにRANDOMな値がリダイレクトされる
      • というコマンドになる。
  • | bash
    • パイプでsedで作ったコマンド文がbashに渡されて、実行される。

感想

  • 3日もあけてしまった。
  • 全然答えを自分で導ける気がしない。
  • manとかで読んで、このオプションね。とか
  • じゃあこれはなんでだろう?とかを自分で調べて補完し始めてきたので、ポジティブに捉えてコツコツ解いてみる。
  • 少なくとも20問ぐらいやってみて、振り返る感じかなぁ。

静的リンク(静的ライブラリ)、動的リンク(共有ライブラリ)について

静的リンク、動的リンクについて、理解できてなかったので調べた。

まとめ

静的リンク

  • ライブラリのコードがアプリケーションの実行ファイルに直接リンクされる。
  • ライブラリは、静的ライブラリ(.aファイル)の形式で提供されることが一般的。
  • 実行ファイルは、必要なライブラリが含まれているため、必要なライブラリがインストールされていなくても実行できる。
  • ライブラリを更新する場合は、アプリケーションの再コンパイルが必要になる。
  • 実行ファイルのサイズが大きくなる。
  • バージョンの問題が発生することはない。

動的リンク

  • ライブラリのコードが実行ファイルに直接リンクされず、実行時に必要な場合に動的にロードされる。
  • ライブラリは、共有ライブラリ(.soファイル)の形式で提供されることが一般的。
  • 実行ファイルは、必要なライブラリがインストールされている場合にのみ実行できる。
  • ライブラリを更新する場合は、アプリケーションの再コンパイルが必要なくなる。
  • 実行ファイルのサイズが小さくなる。
  • バージョンの問題が発生する場合がある。

参考

ライブラリのリンク方法をきっちり区別しよう - Qiita

CMakeのfind_package()とadd_difinitions()について

find_package

概要

CMakeのfind_package()は、ライブラリやパッケージの検索を行い、その検索結果を利用してプロジェクトのビルド設定を行うための関数。find_package()は、指定されたライブラリやパッケージがシステムにインストールされているかどうかを確認し、検出した場合は必要な情報を設定ファイルから読み込む。

通常、find_package()は以下のような形式で使用される

find_package(<パッケージ名> <バージョン> [REQUIRED] [COMPONENTS <コンポーネント1> <コンポーネント2> ...])
  • <パッケージ名>には、検索するライブラリやパッケージの名前を指定する。

    • <バージョン>には、使用したいバージョン番号を指定する。
    • REQUIREDを指定すると、必須であることを示す。
    • COMPONENTSには、検索するコンポーネントの名前を指定する。
    • 必要に応じて、他のオプションを指定することもできる。
  • find_package()は、パッケージの設定ファイル(例えば、<パッケージ名>Config.cmake<パッケージ名>-config.cmake<パッケージ名>-config-version.cmakeなど)を検索し、それらから必要な情報を読み取る

    • 設定ファイルには、パッケージが提供するインクルードファイルやライブラリの場所、コンパイルオプションなどが含まれている。
  • find_package()によって検出された情報は、include_directories()target_include_directories()target_link_libraries()などのCMakeコマンドで使用される。

  • find_package()を使用することで、CMakeはシステムにインストールされたライブラリやパッケージを簡単に検出し、ビルドプロセスを自動化することができる。

疑問と調べた結果

1: CMake側が勝手にパッケージを探してくれる理解で良いか?

  • Yes。CMakeは一般的にパッケージを自動的に探索し、検出することができる。
  • パッケージの場所や構成は、各パッケージに応じて異なるが、多くのパッケージは、CMakeが検索できる標準的な場所にインストールされている。
    • たとえば、Linuxでは、多くのパッケージは/usr/libや/usr/local/libにインストールされている

2: BoostとかOpenCVとかのパッケージはライブラリ開発側が用意してくれているの?

  • 少なくともOpenCVはYes。
  • find_package()関数によるOpenCVの検出では、OpenCVConfig.cmakeという名前のCMakeモジュールが使用される。
  • このモジュールには、OpenCVのインストール場所や、使用するOpenCVのバージョン、コンポーネントなどが含まれている。
  • Linuxの場合、OpenCVをapt-getなどのパッケージマネージャーでインストールした場合、OpenCVConfig.cmakeは通常、/usr/lib/x86_64-linux-gnu/cmake/opencv4のようなディレクトリにインストールされる。

add_difinitions

概要

  • CMakeのadd_definitions()関数は、コンパイラに対してコンパイルオプションを渡すことができる。
  • 通常、この関数はプロジェクトの定義済みマクロを設定するために使用される。
  • add_definitions()関数には、以下のように定義済みマクロを設定することができる。
add_definitions(-DDEFINE_NAME=DEFINE_VALUE)
  • 上記の例では、DEFINE_NAMEという名前のマクロにDEFINE_VALUEという値を設定している。
  • また、add_definitions()関数は、複数のオプションを指定することもできる。
  • 例えば、以下のように複数の定義済みマクロを設定することができる。
add_definitions(-DDEFINE_NAME1 -DDEFINE_NAME2=DEFINE_VALUE2)
  • 上記の例では、DEFINE_NAME1という名前のマクロを定義し、DEFINE_NAME2という名前のマクロにDEFINE_VALUE2という値を設定している。
  • add_definitions()関数は、target_compile_definitions()関数を使用することで、特定のターゲットに対して定義済みマクロを設定することもできる。
  • この方法を使用すると、ターゲットごとに異なるマクロを設定することができる。

疑問と調べた結果

add_definitionsではオプションの頭にDをつける必要があるの?

  • Yes。add_definitions()関数で定義済みマクロを設定する場合、-Dオプションを使用してマクロ名と値を指定する必要がある。
  • -Dオプションを使用することで、CMakeはコンパイラに対して、定義済みマクロを設定するためのオプションを渡す。
  • オプションの先頭にDをつけることにより、CMakeがマクロ名と値を認識し、正しくコンパイラに渡されるようになる。
/* -----codeの行番号----- */