作者:卢文双 资深数据库内核研发
序言:
以前对 MySQL 测试框架 MTR 的使用,主要集中于 SQL 正确性验证。近期由于工作需要,深入了解了 MTR 的方方面面,发现 MTR 的能力不仅限于此,还支持单元测试、压力测试、代码覆盖率测试、内存错误检测、线程竞争与死锁等功能,因此,本着分享的精神,将其总结成一个系列。
主要内容如下:
- 入门篇:工作机制、编译安装、参数、指令示例、推荐用法、添加 case、常见问题、异常调试
- 进阶篇:高阶用法,包括单元测试、压力测试、代码覆盖率测试、内存错误检测、线程竞争与死锁
- 源码篇:分析 MTR 的源码
- 语法篇:单元测试、压力测试、mysqltest 语法、异常调试
由于个人水平有限,所述难免有错误之处,望雅正。
本文是第四篇语法篇。
本文首发于 2023-07-05 21:53:21
MTR 系列基于 MySQL 8.0.29 版本,如有例外,会特别说明。
单元测试
简介
前文「MySQL 测试框架 MTR 系列教程(二):进阶篇 - 内存/线程/代码覆盖率/单元/压力测试」已介绍了单元测试的概念及使用方法,简单回顾一下:
- MySQL 使用 TAP(Test Anything Protocol) 和 Google Test Framework 来实现单元测试。
- TAP 是 Perl 与测试模块之间所使用的简单的基于文本的接口。
- 为了实现 C/C++ 的单元测试,MySQL 开发了一个用于生成 TAP 文本的库
libmytap.a
,源码路径位于unittest/mytap/
。
- 使用方法:在执行 cmake 的目录执行
make test
或make test-unit
指令(内容详细,更推荐)。 - 注意事项:在执行单元测试时,不建议启用 ASAN,否则会因 ASAN 检测到单元测试代码有内存泄漏而导致 case 失败。
unittest/
目录介绍:
1 | CMakeLists.txt |
如果新加的测试用例与存储引擎或插件有关,则分别存放在unittest/engine_name
和unittest/plugin_name
目录或它们的子目录中。
单元测试代码都位于源码目录/unittest/gunit/
下,其中有文件也有子目录,无论是当前目录还是子目录下的文件,都以xxxxx-t.cc
格式命名,每个xxxxx-t.cc
文件都是一个测试 case,编译后都会生成一个二进制文件 bin/xxxxx-t
。
下面举例说明如何添加单元测试 case 。
示例
比如在 源码目录/unittest/gunit/binlogevents/
目录下创建一个新的测试用例 myprint
。
一、创建文件unittest/gunit/binlogevents/myprint-t.cc
,内容如下:
1 |
|
二、修改 unittest/gunit/binlogevents/CMakeLists.txt
文件,添加myprint
用例:
1 | ...... |
三、重新执行 cmake
(需要设置-DWITH_DEBUG=1 -DWITH_UNIT_TESTS=1
)、编译,会生成二进制文件bin/myprint-t
。
四、运行make test
或make test-unit
,或者直接执行bin/myprint-t
,测试用例 passed :
1 | wslu@ubuntu:/data/work/mysql/mysql-server/console-build-debug$ ./bin/myprint-t |
参考:
代码覆盖率测试
目前涉及 gcov 的程序文件只有 mysys/dbug.cc
文件以及对应的单元测试文件unittest/gunit/dbug-t.cc
。
gcov 的用法就是在编译时添加选项来实现的:
1 | wslu@ubuntu:/data/work/mysql/mysql-server/console-build-debug/mysql-test$ grep "coverage" ../../CMakeLists.txt -nr |
综上,无需自行编写代码覆盖率测试代码。
压力测试
有两个地方涉及压力测试:
一、压力测试 suite 只有两个:
- stress
- innodb_stress
如需要添加新 case,参考对应 suite 已有 case 照猫画虎即可,语法可参考下一章节。
二、mysql-stress-test.pl
:被 mysql-test-run.pl
调用,参数是--stress
。
1 | stress=ARGS Run stress test, providing options to |
对于 mysql-stress-test.pl
,更便于自定义测试内容,主要包括:
--stress-init-file[=path]
file_name is the location of the file that contains the list of tests to be run once to initialize the database for the testing. If missing, the default file is stress_init.txt in the test suite directory.
--stress-tests-file[=file_name]
Use this option to run the stress tests. file_name is the location of the file that contains the list of tests. If file_name is omitted, the default file is stress-test.txt in the stress suite directory. (See
--stress-suite-basedir
).
这部分暂未尝试,不做赘述。
SQL 测试 - mtr/mysqltest 语法
之间文章介绍过 mtr 会将 *.test
和*.inc
等测试 case 的内容传给 mysqltest 来执行。
mysqltest 是 mysql 自带的测试引擎, 它实现了一种小语言,用来描述测试过程,并将测试结果与预期对比。
本节要讲解 mysqltest 语法格式,你可能会好奇学习这个语法有什么用,为了更直观的说明,首先我们看一下如何编写 mtr 的测试用例。
语法格式
mysqltest 解释的是以.test
为后缀的文件(包括其引用的.inc
文件)。
mysqltest 小语言按照语法大致分为三类:
- mysql command :用来控制运行时的行为。一般有两种写法:
1 | command; # 这是后面带;的 |
- SQL :就是普通的 SQL 语句,测试 case 里大部分是 SQL 语句。
- comment :注释一般用于描述测试过程,用
#
开头。
示例:借鉴「MySQL 测试框架 MTR 系列教程(一):入门篇」一文中的测试 case(路径是 mysql-test/t/mytest.test
),内容如下:
1 | --echo # |
command 列表
mysqltest 提供了几十个 command,本文只介绍最常用的,更多内容请查阅官方手册:MySQL: mysqltest Language Reference 。
error 处理预期错误
语法:
1 | error error_code [, error_code] ... |
有些 CASE 就是要验证 sql 失败的情况,在 sql 语句前面加上--error 错误码
就可以了。
- 如果 sql 报错且错误码等于
--error
指定的错误码,mysqltest 不会 abort,而是继续运行。 - 反之,如果 sql 报错且错误码不等于
--error
指定的错误码,mysqltest 会 abort 并报错退出。
--error
后面可以跟两种值:一种是error no,另外一种是sqlstate,如果是后者需要加上 S 做前缀。 他们分别是 C API 函数 mysql_errno()
和 mysql_sqlstate()
的返回值。
示例一:使用错误码
1 | --error 1050 |
等价于
1 | --error ER_TABLE_EXISTS_ERROR |
其中数字 1050 对应错误码,ER_TABLE_EXISTS_ERROR
对应错误的逻辑名。
这样在 mysqltest 运行后,会将返回的错误信息一起写入结果文件,这些错误信息就作为期望结果的一部分了。
示例二:使用 SQLSTATE
也可以使用 SQLSTATE
来指示期望有错误返回,例如与 MySQL 错误码 1050 对应的 SQLSTATE 值是 42S01,使用下面的方式,注意编码增加了 S 前缀:
1 | --error S42S01 |
示例三:指定多个错误码,满足其一则通过
在指令 error 后面是可以加入多个错误码作为参数的,使用逗号分隔即可:
1 | --error 1050,1052 |
如果该 SQL 报错,若错误码是 1050 或 1051,则符合预期,测试继续。
错误码参考 MySQL 安装包 include 子目录下的 mysqld_error.h
。
disable_abort_on_error / enable_abort_on_error
默认情况下(enable_abort_on_error
),sql 执行失败后 mysqltest 就退出了,后面的内容就不会执行,也不会生成 .reject
文件。
显示执行disable_abort_on_error
命令可以在 sql 失败后继续执行后面的内容,并生成 .reject
文件。
1 | --disable_abort_on_error |
disable_query_log / enable_query_log
默认情况下(enable_query_log
),所有的 sql 语句都会记录输出结果。
在一些情况下(比如,使用了循环,query 特别多)不想记录某些 sql 语句及结果,显示调用 disable_query_log
既可。
1 | --disable_query_log |
其他形如enable_xx/disable_xx
的命令还有很多,用法都类似。
connect
创建一个到 mysql server 的新连接并作为当前连接。
语法格式:
1 | connect (name, host_name, user_name, password, db_name [,port_num [,socket [,options [,default_auth [,compression algorithm, [,compression level]]]]]]) |
- name is the name for the connection (for use with the connection, disconnect, and dirty_close commands). This name must not already be in use by an open connection.
- host_name indicates the host where the server is running.
- user_name and password are the user name and password of the MySQL account to use.
- db_name is the default database to use. As a special case, NO-ONE means that no default database should be selected. You can also leave db_name blank to select no database.
- port_num, if given, is the TCP/IP port number to use for the connection. This parameter can be given by using a variable.
- socket, if given, is the socket file to use for connections to localhost. This parameter can be given by using a variable.
- options can be one or more of the following words, separated by spaces:
CLEARTEXT
: Enable use of the cleartext authentication plugin.COMPRESS
: Use the compressed client/server protocol, if available.PIPE
: Use the named-pipe connection protocol, if available.SHM
: Use the shared-memory connection protocol, if available.SOCKET
: Use the socket-file connection protocol.SSL
:Use SSL network protocol to have encrypted connection.TCP
: Use the TCP/IP connection protocol.
Passing PIPE or SHM on non-Windows systems causes an error, and, similarly, passing SOCKET on Windows systems causes an error.
- default_auth is the name of an authentication plugin. It is passed to the mysql_options() C API function using the MYSQL_DEFAULT_AUTH option. If mysqltest does not find the plugin, use the –plugin-dir option to specify the directory where the plugin is located.
- compression algorithm is the name of compression algorithm to be used to compress data transferred between client server. It is passed to the mysql_options() C API function using the MYSQL_OPT_COMPRESSION_ALGORITHMS option.
- zstd compression level is the extent of compression to be applied when zstd compression algorithm is used. It is passed to the mysql_options() C API function using the MYSQL_OPT_COMPRESSION_ALGORITHMS option.
- compression level is the extent of compression to be applied based on the compression algorithm used. It is passed to the mysql_options() C API function using the MYSQL_OPT_COMPRESSION_ALGORITHMS option.
示例:
1 | connect (conn1,localhost,root,,); |
connection
语法:
1 | connection connection_name |
选择 connection_name
作为当前连接。
示例:
1 | connection master; |
disconnect
语法:
1 | disconnect connection_name |
关闭连接connection_name
。
示例:
1 | disconnect conn2; |
测试 session 的时候会用到上述三个命令,以在多个 connection 之间切换。比如:
1 | connect (conn3,127.0.0.1,root,,test,25042); |
exec
执行 shell 命令。语法:
1 | exec command [arg] ... |
示例:
1 | --exec $MYSQL_DUMP --xml --skip-create test |
On Cygwin, the command is executed from cmd.exe, so commands such as rm cannot be executed with exec. Use system instead.
perl [terminator]
嵌入 perl 代码,以 EOF 为结束符,也可以自定义结束符。
受限于 mtr 语法,很多操作无法完成,嵌入 perl 脚本可以简化问题。
1 | perl; |
示例:
1 | perl; |
1 | perl END_OF_FILE; |
perl 内外的变量交互:
1、可以使用 let 设置环境变量
如果使用 let 时变量名不加 $
即为设置为环境变量,在 perl 中可以通过 $ENV{'name'}
获取和设置
1 | --let name = "env value" |
2、在 perl 中拼接 mtr 脚本,然后在 mtr 脚本中执行
1 | perl; |
vertical_results/horizontal_results
设置 SQL 语句结果的默认显示方式(vertical_results 表示纵向,horizontal_results 表示横向,默认是横向),功能跟 sql 语句的'\G'
类似。
示例:
1 | --vertical_results |
exit
退出当前测试 case,后续指令不再执行。
let
变量赋值,可支持整数、字符串。
语法:
1 | let $var_name = value |
示例:
1 | --let $1= 0 # 加 -- 前缀,就不用以分号结尾 |
inc/dec
为整数加 1/减 1 。
语法:
1 | inc $var_name/dec $var_name |
示例:
1 | --inc $i; |
eval
语法:
1 | eval statement |
执行sql 语句,支持变量的传递。示例:
1 | eval USE $DB; |
query
语法:
1 | query [statement] |
显示指定当前语句是 SQL 语句,而不是 command。即使 query 之后是 command(比如sleep
),也会当成 statement 来解析。
send
语法:
1 | send [statement] |
向 server 发送一条 query,但并不等待结果,而是立即返回,该 query 的结果必须由 reap
指令来接收。
在上一条 query 结果被 reap
指令接收之前,不能向当前 session 发送新的 SQL 语句。
如果 statement 省略了,则执行下一行的 SQL 语句。
示例:
1 | send SELECT 1; |
send_eval
语法:
1 | send_eval [statement] |
等效于 send
+ eval
,与send
不同在于支持变量传递。
如果 statement 省略了,则执行下一行的 SQL 语句。
示例:
1 | --send_eval $my_stmt |
reap
如果当前 session 之前有通过 send
指令向 server 发送 SQL 语句,reap
指令用来接收该 SQL 语句的执行结果。
如果之前没有通过 send
向 server 发送 SQL,则不要执行 reap
指令。
示例:在同一个 session 中用 send
后台执行,用 reap
恢复等待。
1 | --connection 1 |
echo
语法:
1 | echo text |
将 text
文本输出到测试 result 中。
示例:
1 | --echo Another sql_mode test |
query_get_value
语法:
1 | query_get_value(query, col_name, row_num) |
获得 query 返回的结果中某行某列的值。
示例:
假如 .test
文件内容如下:
1 | CREATE TABLE t1(a INT, b VARCHAR(255), c DATETIME); |
输出结果为:
1 | CREATE TABLE t1(a INT, b VARCHAR(255), c DATETIME); |
int(11)
就是 Type 列第一行的值。
source
语法:
1 | source file_name |
多个 case 可能共用一块代码,这块代码可以单独放到一个.inc
文件,再通过 source 导入。
示例:
1 | --source path/to/script.inc |
sleep
语法:
1 | sleep num |
Sleep num seconds.
示例:
1 | --sleep 10 |
replace_column
语法:
1 | replace_column col_num value [col_num value] ... |
将下一条语句的结果中的某些列的值进行替换,可以指定多组替换规则,列序号从 1 开始。
示例:
1 | --replace_column 9 # |
expr 命令 (MySQL 8 之后可用)
语法:
1 | expr $var_name= operand1 operator operand2 |
对数值变量进行运算,支持 +
, -
, *
, /
, %
, &&
, ||
, &
, |
, ^
, <<
, >>
1 | --let $val1= 10 |
在 5.7 版本中用 SQL 语句替代
1 | --let $val1= 10 |
if
语法:
1 | if (expr) |
与其他语言的 if 语句含义相同。
示例:
1 | let $counter= 0; |
while
语法:
1 | while (expr) |
与其他语言的 while 语句含义相同。
示例:
1 | let $i=5; |
其他命令
其他的命令还有:
1 | assert (expr) |
详见官方手册:MySQL: mysqltest Commands
编写规范
- 尽可能避免每行超过 80 个字符;
- 用
#
开头,作为注释; - 缩进使用空格,避免使用 tab;
- SQL 语句使用相同的风格,包括关键字大写,其它变量、表名、列名等小写;
- 增加合适的注释。特别是文件的开头,注释出测试的目的、可能的引用或者修复的 bug 编号;
- 为了避免可能的冲突,习惯上表命名使用 t1、t2…,视图命名使用 v1、v2…;
异常调试
本小节已添加到《MySQL 测试框架 MTR 系列教程(一):入门篇》,看过前文的可跳过本节。
分析日志
默认情况下,在目录 mysql-test/var/log/
中有日志生成(若指定 --vardir
参数,则以该参数路径为准),分析该日志也能得到一些有用信息。
比如 启动失败,则可以查看 bootstrap.log
文件,去掉命令中的 --bootstrap
并运行即可启动对应的 MySQL 服务来验证、调试。
verbose 参数
启动 mtr 时加 --verbose
参数,定位到引用的脚本位置后可以配置 --echo
命令修改调试。
如果加上 --verbose
打印的内容还不够详细,可以再加一个,即 --verbose --verbose
,能打印出 mtr perl 脚本中的日志信息。
示例:
1 | wslu@ubuntu:/data/work/mysql/mysql80-install.bak_asan_ubsan/mysql-test$ perl mysql-test-run.pl --timer --force --parallel=1 --vardir=var-rpl --suite=rpl --verbose |
脚本自身支持 debug 参数
如果引用(source
)的脚本支持 debug 参数,比如常用的 $rpl_debug
,则可以修改相应的 .inc
文件以获得更多的 debug 信息。
perl 的调试模式
添加-d
参数可进入 perl 语言的 debug 模式。示例:
1 | wslu@ubuntu:/data/work/mysql/mysql80-install.bak_asan_ubsan/mysql-test$ perl -d mysql-test-run.pl --timer --force --parallel=1 --vardir=var-rpl --suite=rpl |
调试模式常用命令:
1 | h 查看帮助文档 |
参考
MySQL: The MySQL Test Framework
MySQL: mysqltest Language Reference
MySQL: Creating and Executing Unit Tests
mysqltest 语法整理 - 叶落 kiss - 博客园 (cnblogs.com)
欢迎关注我的微信公众号【数据库内核】:分享主流开源数据库和存储引擎相关技术。
标题 | 网址 |
---|---|
GitHub | https://dbkernel.github.io |
知乎 | https://www.zhihu.com/people/dbkernel/posts |
思否(SegmentFault) | https://segmentfault.com/u/dbkernel |
掘金 | https://juejin.im/user/5e9d3ed251882538083fed1f/posts |
CSDN | https://blog.csdn.net/dbkernel |
博客园(cnblogs) | https://www.cnblogs.com/dbkernel |