数据库内核月报 - 2019 / 09

PgSQL · 最佳实践 · 回归测试探寻

Postgres regress test

每当开发完一个新的功能后,要想让其稳定的上线的前提是必须完成回归测试,回归测试定义为一种软件测试,用于确认最近的程序或代码更改未对现有功能产生负面影响。回归测试只不过是已经执行的测试用例的全部或部分选择,这些测试用例被重新执行以确保现有功能正常工作。对于最先进的开源数据库Postgresql当然也提供了它的回归测试。我们在官方文档中可以找到其具体的使用方法。

make check   			 #  parallel running the Tests Against a Temporary Installation 
make installcheck  			    # serial running the Tests Against an Existing Installation 
make installcheck-paralle   # parallel  running the Tests Against an Existing Installation 

同样的,当我们开发完一个较重要的feature后,也需要添加一些对应新test case,以免以后开发的功能会影响到这个feature。不巧的是官方文档并没有给出添加test case的说明,添加完自己的test case后,我们如何单独的运行这个case,也无法在官网中找到答案,所以我们这次就来对于Postgres regress一探究竟,看一看Postgres的回归测试是如果做的。首先我们进入到Postgres 源码中的src/test/regress中,根据阅读源码的经验,我们首先读一下REDAME是个明智之举。

Documentation concerning how to run these regression tests and interpret the 
results can be found in the PostgreSQL manual, in the chapter "Regression Tests".

令人尴尬的是README将我们又推向了官方文档,所以我们不得不自己到代码中去一探究竟。既然官方让我们运行make check,那么我们先去Makefile是一个不错的选择。

pg_regress

在makefile中,我们很容易看到make installcheck 相当于编译源码后运行了下面的命令:

pg_regress --inputdir=. --bindir='tmp_basedir_polardb_pg_1100_bld/bin'  
--dlpath=. --max-concurrent-tests=20 --user=regress  --schedule=./serial_schedule 

我们可以从Makefile看出src/test/regress下面的 pg_regress.c pg_regress_main.c regress.c被编译成了一个二进制为 pg_regress。执行以下命令:

./pg_regress -h
####################################\
PostgreSQL regression test driver

Usage:
  pg_regress [OPTION]... [EXTRA-TEST]...

Options:
      --bindir=BINPATH          use BINPATH for programs that are run;
                                if empty, use PATH from the environment
      --config-auth=DATADIR     update authentication settings for DATADIR
      --create-role=ROLE        create the specified role before testing
      --dbname=DB               use database DB (default "regression")
      --debug                   turn on debug mode in programs that are run
      --dlpath=DIR              look for dynamic libraries in DIR
      --encoding=ENCODING       use ENCODING as the encoding
  -h, --help                    show this help, then exit
      --inputdir=DIR            take input files from DIR (default ".")
      --launcher=CMD            use CMD as launcher of psql
      --load-extension=EXT      load the named extension before running the
                                tests; can appear multiple times
      --load-language=LANG      load the named language before running the
                                tests; can appear multiple times
      --max-connections=N       maximum number of concurrent connections
                                (default is 0, meaning unlimited)
      --max-concurrent-tests=N  maximum number of concurrent tests in schedule
                                (default is 0, meaning unlimited)
      --outputdir=DIR           place output files in DIR (default ".")
      --schedule=FILE           use test ordering schedule from FILE
                                (can be used multiple times to concatenate)
      --temp-instance=DIR       create a temporary instance in DIR
      --use-existing            use an existing installation
      --no-restrict-auth        disable restricted authentication settings
  -V, --version                 output version information, then exit

Options for "temp-instance" mode:
      --no-locale               use C locale
      --port=PORT               start postmaster on PORT
      --temp-config=FILE        append contents of FILE to temporary config
      --temp-initdb-opts=OPTS   additional flags passed to initdb

Options for using an existing installation:
      --host=HOST               use postmaster running on HOST
      --port=PORT               use postmaster running at PORT
      --user=USER               connect as USER

The exit status is 0 if all tests passed, 1 if some tests failed, and 2
if the tests could not be run for some reason.

Report bugs to <support@enterprisedb.com>.

pg_regress的入口在pg_regress_main.c中:

int
main(int argc, char *argv[])
{
	return regression_main(argc, argv, psql_init, psql_start_test);
}

其中psql_init是初始化regress时连接的数据库,psql_start_test对于每一个test case调用执行。下面我就来简单列出regression_main中的逻辑,如果有兴趣的同学可以自己阅读源码。

  • 1.参数解析
  • 2.如果是在临时实例上运行,构建临时实例。
  • 3.create 测试数据库和测试用户
  • 4.读取schedule文件对于每一个schedule文件做:
    • 4.1. 读取每一行,以test开头,获取后面的每一个case。
    • 4.2. 每一个case启动一个进程 调用psql 执行case sql脚本。
    • 4.3. 将执行sql脚本的输出重定向到./results目录下。
    • 4.4. 将./results下的输出文件和./expected下的下相同文件名进行diff。
    • 4.5. 如果没有差异ok,有差异falied。
  • 5.如果是临时实例,关闭实例。
  • 6.结束。

以上为pg_regress的源码执行流程,其中读取schedule文件是关键的一个参数,我们知道在回归测试时,有并行和串行之分。本质上就是在–schedule 指定/test/regress下的serial_schedule 和parallel_schedule 文件的区别,我们打开这两个文件对比入下:

  • serial_schedule
    test: tablespace
    test: boolean
    test: char
    test: name
    test: varchar
    test: text   
    ......
    
  • parallel_schedule
    # ----------
    # The first group of parallel tests
    # ----------
    test: boolean char name varchar text int2 int4 int8 oid float4 float8 bit numeric txid uuid enum money rangetypes pg_lsn regproc
    ......
    

    很明了的发现串行schedule内是一个test对应一个case,而并行schedule中一个test对应这多个case。在源码中, pg_regress会对去每一行test中,每个case 启动一个进程,这就是串行和并行测试的区别了。

如何添加新的 regress test case。

经过前面的分析,我们可以发现添加新的test case 可以如下步骤:

  1. 在regress/sql中添加新的sql文件,如test1.sql,test2.sql, test3.sql
  2. 使用psql执行添加的sql文件,psql -X -a -q -d databasename > /regress/expected/test1.sql . 将其每个sql文件的执行结果放入expected目录下作为期望文件,用于下次回归进行比对。
  3. 可以在serial_schedule 和parallel_schedule 中添加自己新建的case ,也可以自己单独写一个schedule文件。
  4. 如果自己新建了schedule文件,在Makefile中添加对应的make 标签。serial_schedule 和parallel_schedule 中添加则不需要。

尽管我们已经掌握了regress test的基本原理,就是并行或者串行的执行sql和期望的执行结果进行比对。但是我们有时候会需要这种需求,并不完全并行或者执行一些sql去测试,比如我们需要两个以上的session,相互交互的以固定顺序执行sql,一般用来测试事务和lock以及隔离级别等。然而在regress下我们却无法满足这种schedule。所以下面我们就来介绍以下Postgres的另一个重要的测试工具pg_isolation_regress。

pg_isolation_regress

同样地,我们去了解一下这个工具如何使用,就需要进入它的源码目录一探究竟,目录位于src/test/isolation。幸运的是它提供了一个看起来相当丰富的README 。首段翻译如下:

该目录包含一组针对并发行为的PostgreSQL的的测试。这些测试需要运行多个交互事务,
这需要管理多个并发连接,因此无法使用正常的pg_regress程序进行测试。
名字“隔离”来自这个事实,原来的动机是测试可序列化隔离级别;
但测试其他类型的并发行为也被添加了。
  1. 其中介绍了pg_isolation_regress用法如下:
    • make installcheck #可以在已经安装的实例上运行所有case,可以通过PGPORT指定端口。
    • ./pg_isolation_regress case1 case2 #运行指定case1,case2。
  2. 添加一个新的测试,将casename.spec文件放在specs /子目录中,添加预期的输出在expected /子目录中,并将测试的名称添加到isolation_schedule文件,基本用法和pg_regress差不多。
  3. case写法可以参考specs /子目录中.sepc文件。每个case定义的格式如下:
setup { <SQL> }

  The given SQL block is executed once, in one session only, before running
  the test.  Create any test tables or other required objects here.  This
  part is optional.  Multiple setup blocks are allowed if needed; each is
  run separately, in the given order.  (The reason for allowing multiple
  setup blocks is that each block is run as a single PQexec submission,
  and some statements such as VACUUM cannot be combined with others in such
  a block.)

teardown { <SQL> }

  The teardown SQL block is executed once after the test is finished. Use
  this to clean up in preparation for the next permutation, e.g dropping
  any test tables created by setup. This part is optional.

session "<name>"

  There are normally several "session" parts in a spec file. Each
  session is executed in its own connection. A session part consists
  of three parts: setup, teardown and one or more "steps". The per-session
  setup and teardown parts have the same syntax as the per-test setup and
  teardown described above, but they are executed in each session. The
  setup part typically contains a "BEGIN" command to begin a transaction.

  Each step has the syntax

  step "<name>" { <SQL> }

  where <name> is a name identifying this step, and SQL is a SQL statement
  (or statements, separated by semicolons) that is executed in the step.
  Step names must be unique across the whole spec file.

permutation "<step name>" ...

  A permutation line specifies a list of steps that are run in that order.
  Any number of permutation lines can appear.  If no permutation lines are
  given, the test program automatically generates all possible orderings
  of the steps from each session (running the steps of any one session in
  order).  Note that the list of steps in a manually specified
  "permutation" line doesn't actually have to be a permutation of the
  available steps; it could for instance repeat some steps more than once,
  or leave others out.

读完了readme,同时又勾起了我们的好奇心,pg_isolation_regress其内部到底是如何实现的呢?

isolationtester

打开pg_isolation_regress源码我们就会看到isolation_main文件,在Makefile中isolation_main.c, isolationtester.c 被编译了成了一个叫做isolationtester的二进制。我们就从isolation_main 入口开看一看其逻辑。其main函数如下:

int
main(int argc, char *argv[])
{
	return regression_main(argc, argv, isolation_init, isolation_start_test);
}

咋一看,和pg_regress没什么区别呀? 仔细一看原来是,其传奇的的函数指针不同。前面我们了解了pg_regress的参数是psql_init 和psql_start_test ,pg_regress的原理对于给一个case启动以进程用psql来执行case中sql。 但是在pg_isolation_regress中实现了多个session交互的逻辑, 单单使用psql是无法做到的,所以原来,pg_isolation_regress自己实现了一个isolation_start_test来执行每个case,我们进入isolation_start_test 就会发现,其使用的一个和psql不一样的工具就叫做isolationtester。 其实现在isolationtester.c 中,有兴趣的同学可以自己去品尝。其中的主要逻辑如下:

  • 1.参数解析
  • 2.读取将要运行的case即 spec文件。
  • 3.解析spec文件,初始化多个session和对应step。
  • 4.检查step是否有重名。
  • 5.使用libpq建立每一个session连接。
  • 6.按步骤设置会话索引字段。
  • 7.运行规范中指定的排列,如果没有明确指定,则运行所有排列。
  • 8.将./results下的输出文件和./expected下的下相同文件名进行diff。
  • 9.相同ok,有差异这failed
  • 10.关闭所有连接,exit结束。

总结

本文介绍了如何使用Postgres的测试工具进行回归测试。其中主要介绍了两个工具分别为pg_regresss和 pg_isolation_regress。这两个工具都是可以用来回归的,pg_regresss的原理是使用psql来运行每一个case,可以串行的运行,也可以并行的运行。而pg_isolation_regress 是使用libpq来开启多个连接进行交互的运行sql,适用于并发锁的测试。