leveldb學習:測試程式碼
leveldb中帶有了大量的測試程式,凡是檔案命名中帶有_test的檔案都是測試程式碼。就我的理解,閱讀測試程式碼非常要,因為作為一個庫,leveldb並不帶有main函式,這也不利於我們理解leveldb的結構和流程,而測試程式碼相當程度上彌補了一點。並且,作為一個在校學生,我們在平時的專案中也極少考慮過寫測試程式碼,今天我就拋磚引玉,以我的水平來剖析下leveldb的測試部分。
leveldb的測試程式碼還是比較容易讓人摸不著頭腦的,至少我第一次看是這樣。下面我選用db_test.cc檔案來舉例分析。
測試程式碼必然有一個主函式入口,在db_test.cc的最下面有main函式:
int main(int argc, char** argv) {
if (argc > 1 && std::string(argv[1]) == "--benchmark") {
leveldb::BM_LogAndApply(1000, 1);
leveldb::BM_LogAndApply(1000, 100);
leveldb::BM_LogAndApply(1000, 10000);
leveldb::BM_LogAndApply(100, 100000);
return 0;
}
return leveldb::test::RunAllTests();
}
如果你看過其他的leveldb測試程式,你就會發現這個leveldb::test::RunAllTests()函式很關鍵。
這個test是何物?是巢狀在名稱空間leveldb下的子名稱空間還是一個類,我們利用sublime編輯器搜尋test關鍵字。在檔案testharness下發現:
namespace leveldb {
namespace test {
繼續鎖定testharness檔案,有:
extern int RunAllTests();
事實上testharness.h和.cc檔案是理解leveldb測試程式的關鍵,搜尋關鍵字RunAllTests:
int RunAllTests() {
const char* matcher = getenv("LEVELDB_TESTS");
int num = 0;
if (tests != NULL) {
for (size_t i = 0; i < tests->size(); i++) {
const Test& t = (*tests)[i];
if (matcher != NULL) {
std::string name = t.base;
name.push_back('.');
name.append(t.name);
if (strstr(name.c_str(), matcher) == NULL) {
continue;
}
}
fprintf(stderr, "==== Test %s.%s\n", t.base, t.name);
(*t.func)();
++num;
}
}
fprintf(stderr, "==== PASSED %d tests\n", num);
return 0;
}
以及:
namespace {
struct Test {
const char* base;
const char* name;
void (*func)();
};
std::vector<Test>* tests;
}
所謂runalltest就是把容器tests裡的函式指標func指向的函式都跑一遍,tests容器裡的元素是Test結構,內有函式指標func,字元base、name,會用來做一個匹配判斷,先不管,注意:test、tests、Test的區別。
回到db_test.cc,定義的兩個類class SpecialEnv、class DBTest先不管,我反正是被
TEST(DBTest, Empty) {
do {
ASSERT_TRUE(db_ != NULL);
ASSERT_EQ("NOT_FOUND", Get("foo"));
} while (ChangeOptions());
}
這種扎眼的結構吸引了,像函式但又不是,在這裡TEST是一個巨集定義。在testharness.h下找到了
#define TEST(base,name)
class TCONCAT(_Test_,name) : public base {
public:
void _Run();
static void _RunIt() {
TCONCAT(_Test_,name) t;
t._Run();
}
};
bool TCONCAT(_Test_ignored_,name) =
::leveldb::test::RegisterTest(#base, #name, &TCONCAT(_Test_,name)::_RunIt);
void TCONCAT(_Test_,name)::_Run()
注意,原始碼裡是有“\”續行符的,csdn編輯器有點頭疼,所以為了顯示效果,就被我刪了。
讓我們看看這個巨集幹了什麼,定義了一個TCONCAT(Test,name)類,繼承自類base,base是傳入的字元。
#define TCONCAT(a,b) TCONCAT1(a,b)
#define TCONCAT1(a,b) a##b
知TCONCAT(Test,name)等於_Test_name,##為字元連線符。
繼續,將兩段程式碼拼上:
#define TEST(base,name)
class TCONCAT(_Test_,name) : public base {
public:
void _Run();
static void _RunIt() {
TCONCAT(_Test_,name) t;
t._Run();
}
};
bool TCONCAT(_Test_ignored_,name) =
::leveldb::test::RegisterTest(#base, #name, &TCONCAT(_Test_,name)::_RunIt);
void TCONCAT(_Test_,name)::_Run()
{
do {
ASSERT_TRUE(db_ != NULL);
ASSERT_EQ("NOT_FOUND", Get("foo"));
} while (ChangeOptions());
}
這樣利用巨集,讓我想起了MFC。MFC為了實現動態建立物件,定義了一個執行型錄,用一個連結串列儲存所有類的RuntimeClass資訊,而RuntimeClass是一個類,成員變數有MFC類的資訊,包括名字,建立物件函式的指標以及連結串列指標,並用巨集的形式完成了RuntimeClass物件的構造和複製。
接著,全域性bool變數TCONCAT(Test_ignored,name)放置了函式RegisterTest(#base, #name, &TCONCAT(Test,name)::_RunIt)的返回值,找到RegisterTest函式
bool RegisterTest(const char* base, const char* name, void (*func)()) {
if (tests == NULL) {
tests = new std::vector<Test>;
}
Test t;
t.base = base;
t.name = name;
t.func = func;
tests->push_back(t);
return true;
}
如果你還記得前面的全域性容器tests,你就明白這是在幹什麼了。
總結下:
TEST巨集利用傳入的字元引數,定義了一個類Test##name,並寫入了成員函式Run()的操作,然後利用RegisterTest登記_Test##name,將其類名類名、基類名以及靜態成員函式_RunIt()指標放入全域性容器tests中,等待測試程式main函式呼叫。
未完待續!