SqlDriveUT使用说明
SqlDriveUT 功能介绍
关于单元测试(Unit Testing),业界有很多通用的测试工具,如Gtest、CppUnit、Junit等,这些工具框架的基本原理都是一致的:
- 框架提供一套接口,用户直接使用这些接口书写用例,进行测试时,框架会自动执行用户的用例。
- 程序员在用例中需要构造函数被测函数的参数,并且调用被测函数,最后检查被测函数的结果。
如下为Gtest的一个用例,Factorial是被测函数,-5、-1、10是函数参数,EXPECT_EQ检查函数的返回值是否符合预期。用户只需要写出下面的用例,运行测试时,框架会调用Factorial(-5),并且把返回值与1做对比。如果返回值等于1,则EXPECT_EQ为真。如果所有的EXPECT_EQ都为真,则这个用例通过,否则这个用例失败。
TEST(FactorialTest, Negative) {
EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);
}
上面的示例中,函数的参数是-5、-1等整数,这种参数的共同点是:构造简单。假如被测函数的入参是多个复杂结构体,并且函数内部依赖很多全局变量,则构造这些入参和全局变量的代价会很大。那对于数据库系统,代码逻辑复杂,要完成以上初始化,对于部分模块代价比较高。
SqlDriveUT(Query Driven Unit Test)为高斯开发的一款新UT测试框架工具,以Query为驱动,降低函数初始化成本,可以进行UT的基本测试。
SqlDriveUT与传统的UT框架不同的是,SqlDriveUT需要整个系统正常运行。等到系统运行到被测函数时,为用户提供修改函数入参的功能。由于系统已经正常运行到被测函数,因此被测函数的参数均已经构造完毕。用户可以在此时对函数的参数做有针对性的修改。
与传统UT框架从零构造参数相比,SqlDriveUT在此时修改函数的参数,所需的工作量大幅降低,因为SqlDriveUT只需要修改此用例关心的部分;使用传统框架时,所有函数的入参均需要构造,无论入参是否与此UT用例是否有关。
目前SqlDriveUT 支持对DB数据库进行UT测试,支持的功能点包括如下:
- 可以测试在CN、DN执行的函数
- 支持用例按模块执行,即指定用例所述的模块,如sql模块、存储模块、通信模块等
- 支持用例的分层,即可以指定用例为Level 1、Level 2、Level 3等。
- 支持修改函数的入参、全局变量
- 支持使用SQLDRIVEUT_ASSERT_TRUE判断函数的执行结果
SqlDriveUT架构介绍
基本原理
SqlDriveUT最大的特点是支持用户修改函数的入参,这是通过实时替换被调函数实现的。最基本的原理是:在被测函数的入口处设置一个gdb的断点,当DB命中此断点时,直接jmp到一个新的函数入口,从而达到用新函数替换被测函数的目的。
考虑下面一段代码。假设func_foo为被测函数,而且假设func_foo的参数很难构造。
【重点】使用示例 重点
int main()
{
……
res = func_foo(1, 2)
……
}
int wrap_func_foo(int a, int b)
{
a = a*2;
res = func_foo(a, b)
SQLDRIVEUT_ASSERT_TRUE(res == 5)
}
在func_foo入口处设置一个临时断点(只命中一次),当系统正常运行命中此断点时,通过gdb的命令set $pc=wrap_func_foo使得系统直接去执行wrap_func_foo函数。在wrap_func_foo中修改函数的入参,并且调用func_foo(此时不再命中断点),拿到func_foo的返回值之后,可以使用SQLDRIVEUT_ASSERT_TRUE对返回值进行校验。
DB的主要功能是执行SQL语句,为了让系统运行到func_foo,需要先给DB发送一条SQL语句,因此此框架属于基于SQL驱动的单元测试框架。
【重点】用例实现
用例实现指的是实现一个wrapper函数,此函数完成参数修改、调用被测函数、检查被测函数返回值等功能。一个函数的示例如下。
bool
wrap_lazyagg_check_parentquery_feasibility(Query *parentParse, Index *targetRTEIndex, lazyagg_query_context *parentContext)
{
parentParse->hasAggs = NULL;
parentParse->groupClause = NIL;
bool ret = lazyagg_check_parentquery_feasibility(parentParse, targetRTEIndex, parentContext);
SQLDRIVEUT_ASSERT_TRUE(ret == false);
ereport(ERROR, (errmsg("[SQLDUT] Finish Normally.")));
}
wrap_lazyagg_check_parentquery_feasibility与函数lazyagg_check_parentquery_feasibility的参数需要完全相同。wrap_lazyagg_check_parentquery_feasibility对函数的参数做了一些修改,调用lazyagg_check_parentquery_feasibility之后,使用XY_ASSERT_TRUE检查函数的结果。由于完成此次测试之后,参数被改坏,如果继续执行,可能会报错,因此直接调用ereport(ERROR)停止执行此SQL语句。
Wrapper函数的代码在Code/src/test/SqlDriveUT/test/wrapper目录下,每个测试文件所在的子目录位置,与源代码的目录对应。比如Code/src/backend/optimizer/prep/prepnonjointree.cpp 对应测试文件Code/src/test/SqlDriveUT/test/wrapper/backend/optimizer/prep/prepnonjointree.cpp。在Code/src/test/SqlDriveUT/test/wrapper中添加测试文件,需要修改或添加新的Makefile。添加方法可参考已有的测试用例。
架构介绍
![ee754641d25beba50e7dc667e2ae587f.png](https://img-blog.csdnimg.cn/img_convert/ee754641d25beba50e7dc667e2ae587f.png)
SqlDriveUT的整体架构如上图。
DB、SqlDriveUT、GDB、gsql是4种不同的进程:
- DB进程中包含了wrapper函数(如上一节的wrap_func_foo)的代码。
- SqlDriveUT进程是整个测试流程的指挥中心,负责读取用例配置、指挥gdb给DB打断点、读取gsql的输出、分析用例的执行结果等。
- GDB进程负责给DB打断点,让DB在执行fun_foo时跳转到wrap_func_foo函数
- Gsql进程负责给DB发送SQL语句,并读取DB的输出结果。
SqlDriveUT在执行时,首先读取用例的配置信息,如被测函数名称、wrapper函数名称、使DB执行到被测函数的SQL语句、用例的模块、用例的Level等。SqlDriveUT根据用户的要求,选择需要执行的用例模块、用例Level,开始执行用例,主要步骤有:
- SqlDriveUT通过gdb给DB在被测函数(如func_foo)设置断点B,并让DB继续执行
- 通过Gsql给DB发送SQL语句,使DB运行到断点(func_foo)处
- 给gdb下发set $pc=wrap_func_foo,然后继续执行DB,从而使得DB执行wrap_func_foo
- SqlDriveUT读取gsql的输出,即可读取到wrapper函数中SQLDRIVEUT_ASSERT_TRUE的结果,根据这些结果判定用例是否执行成功。
SqlDriveUT使用介绍
使用流程概述
根据SqlDriveUT的架构及执行流程易知,使用SqlDriveUT进行单元测试,需要五个步骤:编写wrapper函数、编写SqlDriveUT用例、编译带wrapper函数的DB、运行SqlDriveUT进行测试、分析SqlDriveUT的执行结果。
编写Wrapper函数
Wrapper函数的作用有三个,一是修改被测函数的入参,二是调用被测函数,三是判断被测函数的返回结果是否符合预期。实现wrapper函数时需要注意几个事项。
函数签名:由于DB会从被测函数直接jmp到wrapper函数,因此wrapper函数的签名需要与被测函数完全相同,即函数的入参的个数、类型、顺序需要与被测函数完全吻合。
存放位置:Wrapper函数存放在Code/src/test/SqlDriveUT/test/wrapper下,此目录下的目录层次与DB内核目录层次相同。请将wrapper文件放在相应的目录中。
Mafefile:如果需要添加新文件/目录,需要修改/添加相应的makefile,参考已有代码修改/添加即可。
- 每个文件夹下都需要有一个makefile文件,添加新文件夹时,需要在父文件夹makefile中SUBDIRS中添加新文件夹名字、并且在新文件夹中添加makefile文件(详细请参见已有代码)。
- 添加新文件时,需要在本目录下的makefile中OBJS后添加新文件名称(详细请参见已有代码)。
头文件:Wrapper函数所在的文件中,需要加入wrapper函数所依赖的头文件。包括:
- Wrapper函数依赖的内核头文件。
- #include "utils/SqlDriveUT_assert.h",以便wrapper函数使用SQLDRIVEUT_ASSERT_TRUE等用作断言的宏。
报错处理:建议wrapper函数完成SQLDRIVEUT_ASSERT_TRUE判断之后,使用ereport(ERROR)中断SQL语句的执行流程,节省资源。
- 如果此用例可能会破坏数据库的一致性,则必须进行报错处理,以免影响后续用例。
bool
wrap_lazyagg_check_parentquery_feasibility(Query *parentParse, Index *targetRTEIndex, lazyagg_query_context *parentContext)
{
parentParse->hasAggs = NULL;
parentParse->groupClause = NIL;
bool ret = lazyagg_check_parentquery_feasibility(parentParse, targetRTEIndex, parentContext);
SQLDRIVEUT_ASSERT_TRUE(ret == false);
ereport(ERROR, (errmsg("[SQLDRIVEUT] Finish Normally.")));
}
上面是一个wrapper函数的示例。Wrapper函数首先对参数进行一些修改,然后调用被测函数,最后使用SQLDRIVEUT_ASSERT_TRUE检查被测函数的返回结果。由于此用例把参数修改之后,此SQL语句无法正常运行,因此在用例的结尾使用ereport(ERROR)退出服务线程。
SqlDriveUT的用例的实现
从架构中可以看出SqlDriveUT是一个独立的可执行程序,它的作用是控制gsql给DB发送SQL语句,控制gdb给DB设置断点,控制DB执行wrapper函数,判断用例的执行结果等。因此,SqlDriveUT这个可执行程序需要知道触发每个用例所适用的SQL语句、被测试函数的名称(如func_foo)、wrapper函数的名称(如func_foo_wrapper)等。
这些配置项都会作为C++中类的成员实现,QTEST(xxx)会定义一个描述测试用例的类,这个类的成员变量表示这个用例的相关配置。配置项与类成员的对应关系如下表。
配置项
类的成员名称
前置操作,如创建表,导入数据等SQL语句
m_prepare_sql
触发被测函数断点的SQL语句
m_driven_sql
后置操作,如删除表等清理操作
m_post_sql
被测试的函数名称
m_test_unit
对应的wrapper函数名称
m_wrap_unit
用例所属的模块
m_moudle
用例所属的Level
m_level
执行用例的节点类型(CN、DN)
m_nodetype
下面是Code/src/test/SqlDriveUT/test/utcase/backend/commands/analyze.cpp中的部分代码。这是一个完整的用例,供给SqlDriveUT执行所使用。
#include "stdio.h"
#include "kernel/SqlDriveUT_main.h"
#include "utils/utils.h"
QTEST(get_total_width_1)
{
m_prepare_sql = "timing onn"
"create table t(a int, b int);"
"insert into t values(1, 2);";
m_driven_sql = "analyze t;";
m_post_sql = "drop table t;";
m_test_unit = "get_total_width";
m_wrap_unit = "wrap_get_total_width";
m_moudle = TEST_MODULE_SQL;
m_level = TEST_LEVEL_2;
m_nodetype = TEST_NODETYPE_DN;
}
为了对大量的用例进行管理,SqlDriveUT支持对用例按模块、层次进行划分。
目前支持的模块包括:
TEST_MODULE_BASE
TEST_MODULE_SQL
TEST_MODULE_STORAGE
TEST_MODULE_CBB
TEST_MODULE_CM
TEST_MODULE_COMM
TEST_MODULE_SECURITY
支持的层次包括:
TEST_LEVEL_0 ???
TEST_LEVEL_1 ???
TEST_LEVEL_2 ???
Makefile:
把用例文件添加到Code/src/test/SqlDriveUT/test/utcase下的某个子文件夹中即可完成新用例的添加,不需要添加/修改makefile。现有的makfile会自动搜索该文件夹下的所有cpp文件。
编译DB
对DB进行configure时需要启用--enable-SqlDriveUT选项,才能将wrapper函数编译进DB。
make distclean -s -j48
./configure CC=g++ CFLAGS='-O0 -ggdb3' --prefix=/your/path --without-zlib --enable-debug --enable-cassert --enable-SqlDriveUT
make install -s -j48;
注意事项:请使用debug版本进行测试,即启用--enable-debug
执行SqlDriveUT测试
测过过程共分为两个子步骤,一是启动带有wrapper函数的DB集群,二是执行SqlDriveUT程序开始测试。
SqlDriveUT选项介绍
SqlDriveUT程序支持下列选项,执行./SqlDriveUT即可查看
必须指定的选项
- –cp DB集群中CN的端口
- –dp DB集群中DN的端口
可选的选项
- –m 需要执行的用例模块列表,各个模块以逗号分隔。默认执行所有模块的用例
- –l 需要执行的用例Level列表,各个Level以逗号分隔。默认执行所有Level的用例
- –node 需要执行用例的节点类型。默认执行CN、DN上的所有用例。
- –logdir SqlDriveUT的日志目录,默认为当前工作目录的log文件夹
SqlDriveUT执行方式
DB的开发人员可以使用两种方式启动SqlDriveUT进行SqlDriveUT测试。
第一种方式,需要开发人员自己编译SqlDriveUT、启动集群、运行SqlDriveUT。这种方式的优点是:很快就可以出结果。
- 启动DB集群。
- 编译、运行SqlDriveUT:
cd Code/src/test/SqlDriveUT/src && make && ./SqlDriveUT –cp cn_port –dp dn_port
-
-
- cn_port指某个CN的端口,dn_port指某个DN的端口
-
- 日志在当前工作目录下,即Code/src/test/SqlDriveUT/src/log
第二种方式,像运行fastcheck一样,直接运行make SqlDriveUTcheck p=xxxx即可。这条命令会集成编译SqlDriveUT、启动集群、运行测试。日志在Code/src/test/SqlDriveUT/log下。这种方式启动集群的速度较慢。
建议开发人员在开发阶段使用第一种方式。在特性代码合入主线前使用第二种方式。
两种方式启动的集群,某些参数可能不同,暂时以fastcheck集群中的参数为准。参数配置不同,DB走的代码逻辑可能不同,可能导致某些代码无法测试。
make SqlDriveUTcheck并不执行fastcheck中的用例,SqlDriveUTcheck与fastcheck不能同时执行。
结果分析
手工执行SqlDriveUT时,如果不指定SqlDriveUT日志的目录,则SqlDriveUT会把日志存放在当前目录下的log文件夹内。
使用make SqlDriveUTcheck的方式执行时,日志会存放在SqlDriveUT的结果在Code/src/test/SqlDriveUT/test/log中。out_resultxxxxxxx.log中可以看出用例的执行的结果。从结果中可以看出用例总数,成功的用例总数,失败的用例等信息。
[ info ] [ 10-30 10:26:04 ] ============ testcases start run =================
[ info ] [ 10-30 10:26:04 ] [info] <cn> SqlDriveUT start run cn testcases.
[ info ] [ 10-30 10:26:09 ] [SQLDRIVEUT] lazyagg_check_parentquery_feasibility_1 ...PASSED TIME TAKEN:[0.550]
[ info ] [ 10-30 10:26:09 ] [info] <cn> SqlDriveUT end run cn testcases.
[ info ] [ 10-30 10:26:09 ] [info] <dn> SqlDriveUT start run dn testcases.
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] get_total_width_1 ...PASSED TIME TAKEN:[0.250]
[ info ] [ 10-30 10:26:14 ] [info] <dn> SqlDriveUT end run dn testcases.
[ info ] [ 10-30 10:26:14 ] ============ testcases run finished =================
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Total testcase <2>
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Skipped testcase <0>
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Actual testcase <2>
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Runned testcase <2>
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Passed testcase <2>
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Failed testcase <0>
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Abnormal testcase <0>
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] All cost time <9.600000 s>
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Run module:base,sql,storage,cbb,cm,comm,security,
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Run level:0,1,2,
[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Run node:all,cn,dn,
上面的内容节选自out_resultxxxxxxx.log,从中可以看出一共执行了两个用例,均执行成功。两个用例的名称分别为lazyagg_check_parentquery_feasibility_1、get_total_width_1。
更详细的日志信息可以在out_resultxxxxx.log中查看。
注:
- 用例中的SQL语句仅仅是为了让DB运行到被测函数单元测试的结果
- 单元测试只关心被测函数的返回值,即wrap_func中SQLDRIVEUT_ASSERT_TRUE是否成立
- 不关心SQL语句的运行结果
用例失败的常见错误:
- 没有把wrapper函数编译进DB。请检查nm db | grep SqlDriveUT的输出,如果输出为空,则说明没有DB没有包含wrapper函数。Configure时请启用—enable-SqlDriveUT选项。
- 集群状态异常,如果使用kill -9的方式结束gdb可能导致DB异常。请重启集群测试。
- GDB版本过低,请升级gdb
- Wrapper函数实现错误:
请重点检查相关的SQL语句。
SqlDriveUT改进点
由于SqlDriveUT现有的架构所限制,存在一些使用约束,包括:
- 不支持测试DN与DN连接时执行的函数。Gsql连接CN之后,CN会有一个服务线程CN_T与此gsql对应,如果被测函数需要在CN执行,则必须在CN_T线程内执行,才能被测试。如果被测的函数需要在DN执行,则CN_T会与DN进行连接,DN会启动一个线程DN_T与CN_T对应,被测函数只能在DN_T执行才能被测试。不支持测试在DN其它线程中执行的函数。
- 暂时不支持mock。
- 暂时不支持安全认证等登陆前验证的函数接口。
除了上面的使用约束之外,还有一些改进点,需要在后续版本中解决,包括:
- 断点需要命中N次
- Make SqlDriveUTcheck启动集群太慢
- 所有elog都会被框架匹配,可能存在错误匹配的情况,可以通过新加日志级别解决。
- 对于递归函数,暂时只支持第一次替换。
- 暂时不支持测试C++函数
- 对于异常场景,被测函数在反正结果之前,已经报错退出,则无法判断结果,暂不支持这种场景。
FAQ
- 如何确认wrapper函数是否被编译到DB中?
nm Code/src/backend/db | grep SqlDriveUT_case_number,或grep相应的wrapper函数名称。
- 在用例中如何切换数据库?
“c postgres n”
- 如果测试失败,如何重启测试?
重启集群,重新执行SqlDriveUT