通用makefile是如何煉成的(X)—— 導入單元測試

經過這麼長的時間,我們的通用makefile已基本構建完成,下面使用一個具體的Examle,作爲如何使用的說明。

我們這個例子爲之前寫的hello world編寫單元測試。單元測試的工具使用gtest,比較簡單嘛。

1. 導入gtest文件,生成libgtest.a

從官網上下載gtest源碼包,解壓後,裏面有個fused-src目錄,在裏面就是gtest文件夾,包括1個頭文件gtest.h, 兩個源文件gtest_all.cc, gtest_main.cc。我們直接使用這三個文件,它與主src目錄基本等效的,只是將各種頭文件、源文件打包到一個文件中而已,也正是因爲這樣,使用gtest時,也更方便一些,不用include一堆的頭文件。


話不多說,下面開始工作

第一步是重命名gtest_all.cc, gtest_main.cc爲gtes_all.cpp, gtest_main.cpp. 因爲我們通用makefile還沒有那麼聰明,目前只能識別.cpp後綴。

第二步,我們來編譯一下這三個頭文件,看看是否可以正常編譯。

一般習慣的,我喜歡單獨爲測試文件建立一個獨立的文件夾,這裏我就直接src目錄同等位置下新建一個test目錄,當查看gtest_all.cpp, gtest_main.cpp中發現“include "gtest/gtest.h"”這樣的語句,這就暗示我們需要將gtest_main.cpp, gtest_all.cpp 放在與gtest目錄同等位置下。

然後添加一個module.mk,( 在build/templates目錄下有module.mk的模板,拷貝一份到這裏,免得寫得辛苦)。修改MODULE_NAME, SRC_FILES, 以及最後的編譯目標是可執行文件gtest.exe。然後記得在main.mk中 include test/moduel.mk



於是我們的第一個應用,編譯一個可執行文件已經成功搞定了,很簡單,只要加入源文件,稍微改一下module.mk就可以了。

細心的同學可能已經發現了,編譯起來有點慢,主要是gtest_all.cpp實在是有點大,所以接下來我們要將它變成靜態lib庫,以後編譯test時只要鏈接一下lib庫就可以了,不用再重新編譯。

基本不用修改,只要增加一個module-gtestlib.mk

#module-gtestlib.mk
MODULE_PATH			:= $(call current_path)
MODULE_NAME			:= gtest

## 定義product定製文件
PRODUCT_SPECS 		:= #TODO
## 定義platform定製文件
PLATFORM_SPECS 		:= #TODO
## 定義target定製文件
TARGET_SPECS 		:= #TODO

## 生成產品、平臺相關的信息,在此之前不要使用相關的變量,如CXX, CPPFLAGS,會直接覆蓋。
include $(BUILD_CONFIGURE)

## 定義源文件列表
SRC_FILES:= $(MODULE_PATH)/gtest-all.cpp

## 調用目標編譯規則
$(call build_target,$(BUILD_STATIC_LIBRARY),$(MODULE_PATH)/lib/libgtest.a)

注意到它與module.mk的不同,它的編譯目標是生成靜態庫lib/libtest.a。同樣將這個makefile include到main.mk中,就可以編譯生成libgtest.a

爲了可以測試上面編譯生成的靜態庫是否有效,我們修改原先的module.mk, 改爲直接鏈接libgtest.a

# module.mk

...
## 定義源文件列表
SRC_FILES:= $(MODULE_PATH)/gtest_main.cpp
LDLIBS += $(MODULE_PATH)/lib/libgtest.a

## 調用目標編譯規則
$(call build_target,$(BUILD_EXECUTABLE),$(MODULE_PATH)/gtest.exe)

驗證結果當然是ok啦,要不然哥純粹是找磚拍

2. 在單獨的test目錄下做單元測試

從現在開始,我們在單元測試中將直接使用gtest.h, 和libgtest.a。下面就爲helloworld編寫一個測試用例。因爲想不到什麼好內容,所以乾脆將makefile中產品信息和版本號打印出來,

// hello.cpp
std::string product_vendor()
{
	return PRODUCT_VENDOR;
}

std::string product_version()
{
	return PRODUCT_VERSION;
}
正確輸出分別是Sample 和 1.0

在test目錄下新增一個HelloTest.cpp,內容如下

#include "gtest/gtest.h"
#include <string>
#include "../src/hello/hello.h"

TEST(hello, product_vendor)
{
	ASSERT_TRUE(product_vendor() == std::string("Sample"));
}

TEST(hello, product_version)
{
	ASSERT_TRUE(product_version() == "1.0");
}

同時修改module.mk,將HelloTest.cpp, 以及他所依賴的源文件hello.cpp包含進去 (委屈, 這裏包括上面引用hello.h,着實比較醜陋,但個人表示也很無奈,有哪位大俠知道的話,還望指點迷津)

## 定義源文件列表
SRC_FILES:= $(MODULE_PATH)/gtest_main.cpp \
			$(MODULE_PATH)/HelloTest.cpp \
			$(MODULE_PATH)/hello.cpp
LDLIBS += $(MODULE_PATH)/lib/libgtest.a

## 調用目標編譯規則
$(call build_target,$(BUILD_EXECUTABLE),$(MODULE_PATH)/gtest.exe)

編譯後運行,就會提示2 test ok

 3. 將測試文件與源文件放在一起

可能也有同學喜歡將測試文件與源文件放在一起,青菜蘿蔔各有所愛,我們的makefile雖然簡單,不過這樣的需求總是要想辦法滿足的。

因爲這時,gtest.h要被各個模塊的測試文件引用,爲了簡單起見,我們把gtest/gtest.h放到專門的頭文件目錄inc,在makefile中使用"-Iinc"選項,保證可以使用以include <gtest/gtest.h>的形式引用。對libgtest.a, 我們也放到專門的lib目錄下。最後在hello目錄下新建HelloTest.cpp, 內容同剛纔一樣。 添加相應的makefile,詳細內容看貼圖


我們的編譯目標只是lib庫,因爲可能有很多模塊,當然你也可以每個模塊一個單元測試,這其實也是我所中意的形式。這裏爲了說一個現象,稍微複雜一點點,我們在src目錄下同樣新建一個module-test.mk,這個module-test.mk負責生成最終的可執行文件test.exe

# module.mk
#
# Created on: 2013-12-21
#     Author: lenovo

MODULE_PATH			:= $(call current_path)
MODULE_NAME			:= test

## 定義product定製文件
PRODUCT_SPECS 		:= #TODO
## 定義platform定製文件
PLATFORM_SPECS 		:= #TODO
## 定義target定製文件
TARGET_SPECS 		:= #TODO

## 生成產品、平臺相關的信息,在此之前不要使用相關的變量,如CXX, CPPFLAGS,會直接覆蓋。
include $(BUILD_CONFIGURE)

## 定義源文件列表
SRC_FILES:= $(MODULE_PATH)/gtest_main.cpp
CXXFLAGS += -Iinc
LDLIBS += lib/libgtest.a lib/hellotest.a

## 調用目標編譯規則
$(call build_target,$(BUILD_EXECUTABLE))

編譯運行,發現確實有test.exe生成,但是HelloTest.cpp中的兩個測試並沒有執行,這是爲什麼呢? 這是因爲makefile好吃懶做,當他發現編程生成test.exe時,並不依賴於HelloTest.cpp中的任何內容時,就不會包含HelloTest.o,在這個例子中,也就是根本不會鏈接hellotest.a中的任何內容。知道原因之後,呵呵,那我們就只能給他下套了

/*
 * HelloTest.cpp
 *
 *  Created on: 2014-1-12
 *      Author: lenovo
 */

#include "gtest/gtest.h"
#include <string>
#include "hello.h"
// 增加誘導條件
void include_hellotest() {}
TEST(hello, product_vendor)
{
	ASSERT_TRUE(product_vendor() == std::string("Sample"));
}
。。。

// gtest_main.cpp

#include <iostream>

#include "gtest/gtest.h"
extern void include_hellotest();

GTEST_API_ int main(int argc, char **argv) {
  std::cout << "Running main() from gtest_main.cc\n";
  include_hellotest(); // 通過函數調用,強制引用HelloTest中的內容
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

再次編譯運行,2 test ok


4. 同時編譯生成test.exe 和產品

最後,可能有同學要問了,所有的單元測試文件,會構成一個個module-test.mk, 本身的源文件也有module.mk, 如果我想編譯生成正常的產品以及對應的單元測試,那需要怎麼做呢?

嗯,方法很多,

你可以在main.mk中按對添加 src/module.mk src/module-test.mk, src/hello/module.mk  src/hello/module-test.mk, 如此

可能覺得有麻煩,那也不要緊,修改每一個模塊下的module.mk, 將原來的module.mk重命名爲module-product.mk, 然後module.mk的內容修改爲

include module-product.mk module-test.mk
當include module.mk時,相當於同時編譯正常產品和單元測試。


每次都要兩個module-xxx.mk, 能不能就一個module.mk, 也可以,直接把module-test.mk的內容添加到module.mk末尾就可以了。只是注意刪除MODULE_PATH這一行

# module.mk
#
# Created on: 2013-12-21
#     Author: lenovo

MODULE_PATH			:= $(call current_path)
MODULE_NAME			:= hello

。。。
## 定義瞭如何生成靜態庫的通用規則
$(call build_target,$(BUILD_STATIC_LIBRARY), $(MODULE_PATH)/hello.a)

## 這裏不能再使用$(call current_path),會出錯。實際上您也沒有必要重新定義,MODULE_PATH沒有改變,還是上面的值。
#MODULE_PATH			:= $(call current_path)
MODULE_NAME			:= gtest

## 定義product定製文件
PRODUCT_SPECS 		:= #TODO
## 定義platform定製文件
PLATFORM_SPECS 		:= #TODO
## 定義target定製文件
TARGET_SPECS 		:= #TODO

。。。
## 定義源文件列表
SRC_FILES:= $(MODULE_PATH)/hello.cpp \
			$(MODULE_PATH)/HelloTest.cpp
CXXFLAGS += -Iinc

## 調用目標編譯規則
$(call build_target,$(BUILD_STATIC_LIBRARY),lib/hellotest.a)

一切就是這麼簡單,因爲這是一個通用的makefile, simple but flexible

over





整個commonmakefile代碼庫已經完整的上傳到github, 上面有完整詳細的修改記錄。

https://github.com/crylearner/CommonMakefile

其中gtest branch包含這個測試例子;默認分支是主幹master


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章