通过上篇博客的学习,我知道了leveldb的测试程序主函数是如何利用宏TEST和容器tests调用测试程序,今天来看看测试程序,也就是写进_Test_name类的_Run( )究竟实现了什么。
TEST(DBTest, ReadWrite) {
do {
ASSERT_OK(Put("foo", "v1"));
ASSERT_EQ("v1", Get("foo"));
ASSERT_OK(Put("bar", "v2"));
ASSERT_OK(Put("foo", "v3"));
ASSERT_EQ("v3", Get("foo"));
ASSERT_EQ("v2", Get("bar"));
} while (ChangeOptions());
}
db_test几乎每一个测试函数都是一个循环结构,在testharness.h找到宏定义:
#define ASSERT_TRUE(c) ::leveldb::test::Tester(__FILE__, __LINE__).Is((c), #c)
#define ASSERT_OK(s) ::leveldb::test::Tester(__FILE__, __LINE__).IsOk((s))
#define ASSERT_EQ(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsEq((a),(b))
#define ASSERT_NE(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsNe((a),(b))
#define ASSERT_GE(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsGe((a),(b))
#define ASSERT_GT(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsGt((a),(b))
#define ASSERT_LE(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsLe((a),(b))
#define ASSERT_LT(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsLt((a),(b))
Tester是一个类,FILE, LINE 是C定义的宏,表示当前文件名和行号。Tester定义如下:
// An instance of Tester is allocated to hold temporary state during
// the execution of an assertion.
class Tester {
private:
bool ok_;
const char* fname_;
int line_;
std::stringstream ss_;
public:
Tester(const char* f, int l)
: ok_(true), fname_(f), line_(l) {
}
~Tester() {
if (!ok_) {
fprintf(stderr, "%s:%d:%s\n", fname_, line_, ss_.str().c_str());
exit(1);
}
}
Tester& Is(bool b, const char* msg) {
if (!b) {
ss_ << " Assertion failure " << msg;
ok_ = false;
}
return *this;
}
Tester& IsOk(const Status& s) {
if (!s.ok()) {
ss_ << " " << s.ToString();
ok_ = false;
}
return *this;
}
stringstream是C++标准库里实现字符转换的字符串流,用于取代c的sprintf()函数,stringstream无需像sprintf那样确保证目标缓冲区有足够大空间,以及确保使用正确的格式化符。只要把需要转换成字符的对象输入stringstream,再通过str()函数得到字符串输出。
注释上写着,tester类是用来保存状态,所谓state就是测试指令的返回值,还是上面的例子:
TEST(DBTest, ReadWrite) {
do {
ASSERT_OK(Put("foo", "v1"));
ASSERT_EQ("v1", Get("foo"));
ASSERT_OK(Put("bar", "v2"));
ASSERT_OK(Put("foo", "v3"));
ASSERT_EQ("v3", Get("foo"));
ASSERT_EQ("v2", Get("bar"));
} while (ChangeOptions());
}
这里的Get、Put就是需要测试的指令(函数)。好了,在这里总结一下:
leveldb的测试代码首先把需要测试的接口写入_TEST_ReadWrite类的成员函数_Run中,并登记这个类,以等待测试程序的主函数的调用。这里的test做的是一系列的Put和Get操作,而ASSERT_OK、ASSERT_EQ等是一个断言宏,代码置换后,它产生一个Tester类临时对象。如果PUT、GET操作没有返回理想的state值,断言失败,则在_Run函数结束时,通过Tester对象的析构函数,输出错误信息,包括定位出错的文件名和行号、错误信息。
在Tester类定义中教大家一个批量写成员函数的方法:
#define BINARY_OP(name,op) \
template <class X, class Y> \
Tester& name(const X& x, const Y& y) { \
if (! (x op y)) { \
ss_ << " failed: " << x << (" " #op " ") << y; \
ok_ = false; \
} \
return *this; \
}
BINARY_OP(IsEq, ==)
BINARY_OP(IsNe, !=)
BINARY_OP(IsGe, >=)
BINARY_OP(IsGt, >)
BINARY_OP(IsLe, <=)
BINARY_OP(IsLt, <)
当然我们还不能忘了大部分TEST宏定义的_Test_name类派生自DBTest类,测试的函数(如上文中的Put、Get)来自DBTest。注意DBTest本身只是一个封装类,不含leveldb的实现代码,只是调用leveldb的接口,但它教你了使用leveldb的方法。例如:
DBTest中有一个指针:
DB* db_;
它告诉你把leveldb使用进自己的项目时,你需要定义一个DB对象或指针。还有:
Status Put(const std::string& k, const std::string& v) {
return db_->Put(WriteOptions(), k, v);
}
Status Delete(const std::string& k) {
return db_->Delete(WriteOptions(), k);
}
std::string Get(const std::string& k, const Snapshot* snapshot = NULL) {
ReadOptions options;
options.snapshot = snapshot;
std::string result;
Status s = db_->Get(options, k, &result);
if (s.IsNotFound()) {
result = "NOT_FOUND";
} else if (!s.ok()) {
result = s.ToString();
}
return result;
}
这些都是使用leveldb进行读、写、查询的规范代码,值得借鉴。
再如:
// Return a string that contains all key,value pairs in order,
// formatted like "(k1->v1)(k2->v2)".
std::string Contents() {
std::vector<std::string> forward;
std::string result;
Iterator* iter = db_->NewIterator(ReadOptions());
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
std::string s = IterStatus(iter);
result.push_back('(');
result.append(s);
result.push_back(')');
forward.push_back(s);
}
教你如何遍历leveldb数据库。诸如此类,我们还可以从db_test这一测试程序中学到很多,对于leveldb的用户来说,有些时候这些代码比说明文档还有用,当然前提是你测试程序的结构。
好了,db_test的代码有2000多行,在这里我就不在继续解读下去了,我认为看完我的两篇分析,弄清test程序的流程和结构,其余的代码理解应该不难,欢迎有兴趣的同学一起来补充和讨论。