概要
これまで、でアジャイル開発に対するテスト駆動の必要性、CUnitによるテスト実行の自動化を行ってきました。
今回は、CMakeを導入することでビルドプロセスを自動化していきます。
記事の全体像
- C言語によるアジャイル開発とテスト駆動開発(CI入門) ~ 1.概念 ~
- C言語によるアジャイル開発とテスト駆動開発(CI入門) ~ 2.CUnit導入 ~
- C言語によるアジャイル開発とテスト駆動開発(CI入門) ~ 3.CMake導入 ~
- C言語によるアジャイル開発とテスト駆動開発(CI入門) ~ 4.Jenkins導入 ~
環境
OS : Linux ubuntu 18.04.5 LTS
テストツール : CUnit 2.1-3
コンパイラ : gcc 7.5.0
ビルドツール:CMake 3.10.2 → 3.19.4(追記より)
ビルドツール
そもそもビルドツールとは
実機(PCであったり組み込みマイコンであったり)でプログラムを動かすためのこれまでの遍歴を書くと、
手動ビルド → ビルドスクリプト → ビルドツール という流れで進化してきています。
手動ビルド
ビルドに必要な処理を全て手動で行う。
ソースファイルが1つしか無い場合は良いが、多数存在する場合に正しい手順で作業するのは至難の業となる。
時間がかかる上に、ミスも起こりうる可能性が高いので色々と問題。
ビルドスクリプト
ビルドに必要な処理をスクリプト化して実行する。
手動ビルドの課題を色々と改善はしたが、人間は欲深き存在。
スクリプトは基本的に上から順に全て実行していくので、最後の方の工程でエラーが起きると最初からやり直しになる。
また、大きいシステムになるとコンパイルそのものに時間がかかる。
ビルドツール
ビルドスクリプトの課題を解決してくれるもの。
ビルドタスク(解析、コンパイル、リリース用ビルド、テスト用ビルド等)を定義して実行します。
ビルドツール自身が、ファイルの更新やファイルの依存関係を監視しタスクの最適化を行ってくれたりします(ツールによる)。
つまり、
ファイルが更新されていなければコンパイルを行わずに以前行ったコンパイル結果を使用してくれたり、
デプロイ用のビルドタスクではテスト用のファイルは不要なので、そこのコンパイルを省いてくれたりするというわけです。
これを__インクリメンタルビルド__といいます
代表的なツール
- make (
makefile
に記述されたルールに沿って、ビルドを行う ※makefile
を記述する) - autotools (アジャイル開発とテスト駆動 ~ 2.CUnit導入 ~の)
- CMake (
makefile
等のファイルを自動で生成してくれる ※CMakeLists.txt
というテキストファイルを記述する) - Meson
- Bazel
など
make vs CMake
どちらも記載内容は下記のようになりますが、これらが楽してかけたほうが良いですよね。
記載内容 | 例 |
---|---|
ターゲットの分離 | ・リリース用 ・デバッグ用 |
コンパイルの自動化 | ・ソースコードの一覧 ・コンパイルオプション ・インクルードパス |
リンクの自動化 | ・ライブラリパスの指定 ・リンカオプションの設定 |
makeのデメリット
makefile
は独特で暗黙のルールが多く、学習コストや保守面で大変なようです。また、コンパイラに依存します。
すなわち、開発環境が異なる場合makefile
が使えないということです。
CMakeのメリット
一方CMake
は、コンパイラに依存しないツールです(マルチプラットフォーム対応のビルドツール)。
makefile
とは若干位置づけが異なります。
CMake
はコンパイラに依存するビルド手順(makefile
相当)を生成し、ビルドの実行はその生成した手順を元に他のツール(make
相当)が行います。
この__相当__というのが肝で、他の環境ではmakefile
ではないビルド手順ファイルが生成され、make
でないツールがビルドを行います。
つまり、CMake
は各種ビルドツールのUIを担うと言って良いかもしれませんね。
ちなみに上で上げたmeson
やBazel
もこれにあたります。
また、CMake
はmakefile
と比べてわかりやすくかけます。
ですので、今回はCMakeを使用します。
CMake導入
CMakeのインストール
sudo apt install cmake -y
CMakeLists.txtの作成
インストール出来たので、早速CMakeLists.txt
ファイルを作成します。
# CMakeのバージョンを設定
cmake_minimum_required(VERSION 3.10.2)
# プロジェクト名と使用する言語を設定
project(unitTest_cmake C)
# unitTest.outという実行ファイルをadd.cとunitTest.cから作成
add_executable(unitTest.out add.c unitTest.c)
# unitTest.out作成時に libcunit.aをリンク
target_link_libraries(unitTest.out cunit)
これまで作成したファイルを整理しておきます。
また、ビルド時に中間ファイルが色々と生成されるので、build用のフォルダを追加しています。
$ mkdir build
$ tree
.
├── CMakeLists.txt
├── add.c // テスト対象ソースコード
├── add.h
├── build // build用のフォルダ
└── unitTest.c // テストソースコード
これまでに作成してきたファイル
#include "add.h"
// バグのあるadd関数(テスト対象)
int add(int x, int y) {
return 0;
}
// #ifndef ADD_H_
// #define ADD_H_
int add(int x, int y);
// #endif /* ADD_ */
#include <CUnit/CUnit.h>
// #include <CUnit/Console.h>
#include <CUnit/Automated.h>
#include "add.h"
/**
* addテストスイート
**/
// テスト関数001
void test_add_001(void) {
CU_ASSERT_EQUAL(add(1, 2), 3);
}
// テスト関数002
void test_add_002(void) {
CU_ASSERT_EQUAL(add(-1, -2), -3);
}
// main関数
int main() {
CU_pSuite add_suite;
CU_initialize_registry(); // 2.テスト構造の初期化
add_suite = CU_add_suite("add", NULL, NULL); // 3.テストスイートの追加
CU_add_test(add_suite, "test_001", test_add_001); // 4.テスト関数の追加
CU_add_test(add_suite, "test_002", test_add_002); // 4.テスト関数の追加
CU_automated_run_tests();
// CU_console_run_tests(); // 5.適切なインターフェースを使用してテストを実行
CU_cleanup_registry(); // 6.テスト構造のクリーン
}
ビルドと実行
bulid
フォルダに入ってcmake
コマンドを実行します。
build
フォルダ内にmakefile
が生成されたのが確認できるはずです。
$ cd build/
$ cmake ..
-- The C compiler identification is GNU 7.5.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /path/to/projectDir/build
$ tree -L 1
.
├── CMakeCache.txt
├── CMakeFiles
├── Makefile // これ
└── cmake_install.cmake
ビルドします。 次のコマンドでCMakeがビルドツールを選んで実行してくれます。
目的ファイルであるunitTest.out
が作成されているので、
実行すると、レポートファイルが吐き出されていることが確認できるはずです。
$ cmake --build .
Scanning dependencies of target unitTest.out
[ 33%] Building C object CMakeFiles/unitTest.out.dir/add.c.o
[ 66%] Building C object CMakeFiles/unitTest.out.dir/unitTest.c.o
[100%] Linking C executable unitTest.out
[100%] Built target unitTest.out
$ tree -L 1
.
├── CMakeCache.txt
├── CMakeFiles
├── Makefile
├── cmake_install.cmake
└── unitTest.out // これ
$ ./unitTest.out
$ tree -L 1
.
├── CMakeCache.txt
├── CMakeFiles
├── CUnitAutomated-Results.xml // これ
├── Makefile
├── cmake_install.cmake
└── unitTest.out
以上で、CMakeを使用して、ビルドプロセスを自動化することが出来ました。
おまけ
フォルダを分ける
フォルダを分けたりして、本番環境に少しだけ寄せておこうと思います。
$ tree
.
├── CMakeLists.txt
├── add // フォルダを追加
│ ├── include // フォルダを追加
│ │ └── add.h
│ └── src // フォルダを追加
│ └── add.c
├── main.c // ファイルを新規作成 (アプリケーション側のメイン処理入る)
└── test // フォルダを追加 (テスト側のメイン処理が入る)
└── unitTest.c
別に大したことはやっていません。気になる方は見てください。
各種ファイルの中身
CMakeLists.txt
の最初の4行は同じです。
主に下記3点を変更しています。
-
./add/include
をインクルードフォルダとして登録 - 実際のアプリケーションの作成指示
- フォルダが変更に伴うパスの変更
# CMakeのバージョンを設定
cmake_minimum_required(VERSION 3.10.2)
# プロジェクト名と使用する言語を設定
project(unitTest_cmake C)
# ./add/include をincludeフォルダに指定
include_directories(./add/include) // 1.ココ
# Main という実行ファイルを main.cと add/src/add.c から作成
add_executable(Main main.c add/src/add.c) // 2.ココ
# UnitTest という実行ファイルを test/unitTest.c と add/src/add.c から作成
add_executable(UnitTest test/unitTest.c add/src/add.c) // 3.ココ
# unitTest.out作成時に libcunit.aをリンク
target_link_libraries(UnitTest cunit)
main.c
のみ今回新規に追加しました。
実行するアプリケーションを想定したファイルです。
#include <stdio.h>
#include <add.h>
int main() {
int a = 1;
int b = 2;
int c = add(a,b);
printf("1 + 2 = %d\n", c);
}
その他の変更点ですが、
#include "add.h"
を
#include <add.h>
に変更しています。
これはCMakeLists.txt
でインクルードディレクトリを指定したためです。
#include <add.h>
// バグのあるadd関数(テスト対象)
int add(int x, int y) {
return 0;
}
// #ifndef ADD_H_
// #define ADD_H_
int add(int x, int y);
// #endif /* ADD_ */
#include <CUnit/CUnit.h>
#include <CUnit/Automated.h>
#include <add.h>
/**
* addテストスイート
**/
// テスト関数001
void test_add_001(void) {
CU_ASSERT_EQUAL(add(1, 2), 3);
}
// テスト関数002
void test_add_002(void) {
CU_ASSERT_EQUAL(add(-1, -2), -3);
}
// main関数
int main() {
CU_pSuite add_suite;
CU_initialize_registry(); // 2.テスト構造の初期化
add_suite = CU_add_suite("add", NULL, NULL); // 3.テストスイートの追加
CU_add_test(add_suite, "test_001", test_add_001); // 4.テスト関数の追加
CU_add_test(add_suite, "test_002", test_add_002); // 4.テスト関数の追加
CU_automated_run_tests(); // 5.適切なインターフェースを使用してテストを実行
CU_cleanup_registry(); // 6.テスト構造のクリーン
}
再度ビルドと実行
再度ビルドを行います。
Mainと、UnitTestの2つの実行ファイルが作成されているのが確認できました。
$ cd build
$ cmake ..
$ cmake --build .
$ tree -L 1
.
├── CMakeCache.txt
├── CMakeFiles
├── Main
├── Makefile
├── UnitTest
└── cmake_install.cmake
それぞれ実行してみます。
$ ./Main
1 + 2 = 0
$ ./UnitTest
$ tree -L 1
.
├── CMakeCache.txt
├── CMakeFiles
├── CUnitAutomated-Results.xml // ココ
├── Main
├── Makefile
├── UnitTest
└── cmake_install.cmake
ちゃんと出来てますね。
よかった。
参考資料
今回の記事の趣旨から若干ずれてしまいますが、今回の勉強をするのに役立った参考資料のリンクです。
特にCMakeの使い方に関しては、本記事の趣旨から外れるので省略しますので、参考資料を参照していただければと思います。
そもそもビルドって??
make関連の全体像
makeとconfigureと、もっとナウいやつ
CMakeについて悪く書かれてますが、とりあえず自分で使ってみて判断しようかなと。
CMake関連の参考資料
-
使い方概要
-
詳細
-
サンプル(使い方のイメージが湧くと思います)
その他参考資料
ありきたりなCMakeのプロジェクト作成 for C++
make (UNIX)
追記
カバレッジレポートを出力させる
最新版のCMakeをインストールする
CMakeのバージョンが古く途中で引っかかったので、最新版をインストールします。
すでにインストールしてしまった方、申し訳ないですが、アンインストールしてください。
sudo apt remove cmake -y
こちらの記事を参考に最新版をインストールします (執筆時の CMake -version = 3.19.4)
Ubuntu 18.04 に Cmake の Latest Release をインストールする
./bootstrapでエラーが出た場合
私の環境ではOpenSSL
が見当たらないといったエラーが発生しました。
Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the system variable OPENSSL_ROOT_DIR (missing: OPENSSL_CRYPTO_LIBRARY OPENSSL_INCLUDE_DIR)
libssl-dev
をインストールして再度./bootstrapを実行します。
sudo apt-get install libssl-dev
gcovによるカバレッジの計測
GCCに付属しているgcovというツールを使用し、ステートメントカバレッジ(C0)を計測します。
そのために、CMakeLists.txtを修正します。
先程 CMakeのバージョンを上げたのは、target_link_options
が未対応のバージョンだったためです。
# CMakeのバージョンを設定
cmake_minimum_required(VERSION 3.10.2) // ココの記載はあまり気にする必要は無いらしい
# プロジェクト名と使用する言語を設定
project(unitTest_cmake C)
# ./add/include をincludeフォルダに指定
include_directories(./add/include)
# Main という実行ファイルを main.cと add/src/add.c から作成
add_executable(Main main.c add/src/add.c)
# UnitTest という実行ファイルを test/unitTest.c と add/src/add.c から作成
add_executable(UnitTest test/unitTest.c add/src/add.c)
# unitTest.out作成時に libcunit.aをリンク
target_link_libraries(UnitTest cunit)
# コンパイルとリンクオプションに -coverageを追加 // ココ
target_compile_options(UnitTest PUBLIC -coverage) // ココ
target_link_options(UnitTest PUBLIC -coverage) // ココ
./buildフォルダを一旦消し、再度CMakeを使用してビルドします。
rm -rf build
mkdir build
cd build
cmake ..
cmake --build .
すると、CMakeFiles/UnitTest.dir
の add/src
とtest
ディレクトリの中に *.gcno
ファイルが出来ていると思います。
$ tree ./CMakeFiles/UnitTest.dir/
/
./CMakeFiles/UnitTest.dir/
├── C.includecache
├── DependInfo.cmake
├── add
│ └── src
│ ├── add.c.gcno
│ └── add.c.o
├── build.make
├── cmake_clean.cmake
├── depend.internal
├── depend.make
├── flags.make
├── link.txt
├── progress.make
└── test
├── unitTest.c.gcno
└── unitTest.c.o
ここで、./UnitTest
を実行すると、先程と同じディレクトリに*.gcda
ファイルが出力されます。
このファイルを後ほど使用します。
$ ./UnitTest
$ tree ./CMakeFiles/UnitTest.dir/
./CMakeFiles/UnitTest.dir/
├── C.includecache
├── DependInfo.cmake
├── add
│ └── src
│ ├── add.c.gcda
│ ├── add.c.gcno
│ └── add.c.o
├── build.make
├── cmake_clean.cmake
├── depend.internal
├── depend.make
├── flags.make
├── link.txt
├── progress.make
└── test
├── unitTest.c.gcda
├── unitTest.c.gcno
└── unitTest.c.o
出力結果の確認
出力結果を確認します。
gcovで簡易的な出力結果を確認後、add.c.gcov
が吐き出されるので中身を確認しています。
add.c.gcov
にはadd.cのファイルの内容が記載されており、各行の左側の数字が、テストで何回実行されたかが記載されています。
$ gcov CMakeFiles/UnitTest.dir/add/src/add.c.gcda
File '/home/yusuke/Desktop/cunitSample/add/src/add.c'
Lines executed:100.00% of 2
Creating 'add.c.gcov'
$ cat add.c.gcov
-: 0:Source:/home/yusuke/Desktop/cunitSample/add/src/add.c
-: 0:Graph:CMakeFiles/UnitTest.dir/add/src/add.c.gcno
-: 0:Data:CMakeFiles/UnitTest.dir/add/src/add.c.gcda
-: 0:Runs:1
-: 0:Programs:1
-: 1:
-: 2:#include <add.h>
-: 3:
-: 4:// バグのあるadd関数(テスト対象)
2: 5:int add(int x, int y) {
2: 6: return 0;
-: 7:}
-: 8:
おわりに
次回はいよいよ Jenkinsを用いて、テストの自動化を行います。
またJenkinsで今回までに出力してきた、テスト結果とテストカバレッジの結果も表示できるようにします。
もし、間違いや指摘事項等あれば、コメントしていただけると大変助かります。