详解 Sqllogictest

关于 sqllogictest

数据库质量保证

测试维度和测试覆盖率是保证数据库质量的关键,测试维度包括 单元测试、模糊测试、功能测试(sqllogictest 在这里)、端到端(e2e)测试、性能测试等。数据库功能测试方案核心是通过执行 sql 语句获得返回值,将返回值与预期进行对比,通常存在几个需要考虑的问题:

  1. 如何设计用例的格式?
  2. 如何比对结果?多数方案直接保存结果文件,无法区分具体 sql 的执行结果,只能通过在用例之间增加输出的方式,导致用例不直观;
  3. 不同客户端或者数据库的差异如何解决?如不同的客户端对返回内容格式化方式有差异,不同的数据库对某些类型输出有差异;

发展简介

sqllogictest 最早是 sqlite 进行测试的工具,由 sqlite 的作者 D. Richard Hipp(理查德 希普)设计开发。关于相关的设计理念可以在 找到。

sqllogictest 的目标是保证数据库引擎执行结果是正确的。因此它不会关注其他方面的问题,诸如性能、索引优化、磁盘内存的使用情况、并发和锁等。

目前主流的数据库都有自己的 sqllogictest 测试工具和测试用例,测试用例的语法略有差异并且不能互相兼容,测试工具的实现方式也有所区别:

Databend 为何引入 sqllogictest

Databend 原来有一套功能测试工具,借鉴 clickhouse 的测试方法,将功能测试用例分为 stateless 测试和 stateful 测试。通过 Databend-test(python 实现)来执行,用例通过脚本的方式编写(或者一个 sql 文件),用例的预期结果写成同名不同后缀名的文件并将两者的输出进行 diff 对比。如果相同则认为结果正确。这种测试方法错误用例的编写和修改不友好外,此外 Databend 支持多套不同的 handler(如 MysqL、http、clickhouse)这些 handler 都有被测试的需求,有点像测试不同的数据库。但原来的测试方法没办法解决这个问题,因此我们开始寻找一种能解决这些问题的测试方法和工具。

Databend 如何实现 sqllogictest

虽然都叫 sqllogictest,但实现差异很大,这种差异不仅在用例语法的支持上,实现使用的技术栈及整个工具的实现程度区别也很大。导致不管是测试集还是工具本身,很难开箱即用。经过对不同实现方案的分析对比,我们发现 sqllogictest 的核心功能需求不多、整个开源社区实现分裂无法满意的直接用、本身随着测试工作的推进越来越多的需求会加入进来导致大量的定制化开发。最终我们选择使用 python 自己造轮子。

sqllogictest 包含多个不同的 Runner 负责与不同的数据库或者 handler 交互,每个 Runner 要实现基类 SuiteRunner 中的方法包括

  • execute_ok

  • execute_error

  • execute_query

  • batch_execute

这些方法是执行 sqllogictest 的核心,除此之外 SuiteRunner 类还会保存执行过程中的一些状态和控制变量。

以 Httprunner 的实现为例,实现了必要的接口 execute_ok 、execute_error、execute_query、batch_execute,除此之外还有两个函数  get_connection 和 reset_connection 主要用来重置连接和会话。

通过 Statement 类去解析用例文件,目前没有考虑实现一个解释器的方案,而采用简单的逐行读取文件通过正则匹配的方式实现语法解析。这么做的好处是可以快速实现;缺点是后续要添加语法支持比较麻烦。通过 LogicError 来输出错误信息,包含错误出现的 runner 名称错误的消息(包含出错的 statement 的详情)及错误的类型。此外还实现了一个 LogicTestStatistics 类,记录每一个 sql 执行的时间开销,最终输出统计信息还比较简单,后续可以补充完善。

如何编写 sqllogictest

基础功能

可以通过这个实例快速入门: github.com/datafuselab… 当前支持的执行器: MysqL handler, http handler, clickhouse handler。支持注释语法 ,使用 -- 来注释特定的行。statement类型:

  • ok

    • 语句正确执行,无错误返回
  • error

    • 语句执行错误,且返回的错误信息包含指定预期的内容,通常使用返回码,也可以使用消息文本(但不直观)
  • query

    • B Boolean             布尔类型
    • T text                 文本类型
    • F floating point     浮点类型
    • I integer               整形
    • 语句执行返回带有结果集, 通过 options 和 labels 区分结果集的对比方式
    • options由字符组成,每个字符代表结果集中的一个列,支持的字符有:
    • labels 不同的数据库(handler)对结果的处理存在差异通过 labels 区分开,对于存在多个差异的,通过逗号分隔开

相对而言 ok 和 error 比较好理解,query 相对复杂一些,以下是一个 query 类型用例的示例(仅供参考不代表实际结果):

statement query III label(MysqL)
select number, number + 1, number + 999 from numbers(10);

----
     0     1   999
     1     2  1000
     2     3  1001
     3     4  1002
     4     5  1003
     5     6  1004
     6     7  1005
     7     8  1006
     8     9  1007
     9    10  1008.0

----  MysqL
     0     1   999
     1     2  1000
     2     3  1001
     3     4  1002
     4     5  1003
     5     6  1004
     6     7  1005
     7     8  1006
     8     9  1007
     9    10  1008
复制代码

测试流程控制语法
1.支持 skipif  用于跳过指定的 runner

skipif clickhouse
statement query I
select 1;

----
1
复制代码

2.支持 onlyif 用于仅执行指定的 runner

onlyif MysqL
statement query I
select 1;

----
1
复制代码

3.如果遇到一些偶发的测试失败,无法短期解决的。可以通过 skipped 跳过这个用例,也可以选择注释掉。

statement query skipped I
select 1;

----
1
复制代码

执行输出

成功样例:

Logic Test Summary
Runner MysqL test 237 suites, avg time cost of suites is 822.25 ms
Runner MysqL test 4302 statements, avg time cost of statements is 45.3 ms
Runner http test 231 suites, avg time cost of suites is 341.56 ms
Runner http test 4222 statements, avg time cost of statements is 18.69 ms
Runner clickhouse test 231 suites, avg time cost of suites is 336.48 ms
Runner clickhouse test 4219 statements, avg time cost of statements is 18.42 ms
All tests pass! Logic test success!
复制代码

当前的 summary 中包含了对测试执行过程的简单统计包括执行的用例文件数、每个用例文件包含多少个语句、每个语句执行的平均时间及用例执行的平均时间。
失败样例 1:

ErrorType: statement query get result not equal to expected
Message:
 Expected:
1
 Actual:
 Statement:
Parsed Statement
        at_line: 4,
        s_type: Statement: query, type:I, query_type: I, retry: False,
        suite_name: base\15_query\alias\having_with_alias.test,
        text:
                select count(*) as count from (select * from numbers(1)) having count = 1;        
        results: [(<re.Match object; span=(0,4), match="------------------->>, 8, '1')],
        runs_on: {'MysqL", 'clickhouse", ‘http'}.
Start Line: 8, Result Label:

复制代码

可以看出失败的用例为 base\15_query\alias\having_with_alias.test 中的第四行 ,返回的内容预期为 1 但实际是空。
失败样例 2:

Failed to execute. Collected info: Orig exception: Code: 2302, displayText = Table 'strings_oct_sample_u8' already exists.
Parsed Statement
        at_line: 1,
        s_type: Statement: ok, type: None,
        suite_name: base\02_function\02_0017_function_strings_oct,
        text:
                CREATE TABLE strings_oct_sample_u8 (value UInt8 null) Engine = Fuse;
        results:[],
        runs_on: {'MysqL', 'clickhouse', ‘http'}.
复制代码

可以看出失败的用例为 base\02_function\02_0017_function_strings_oct 的第一行,返回的错误为表已存在。以上示例中我们发现从输出内容很容易就可以定位到具体的用例文件甚至哪一行哪个 sql,对于需要对比结果的,也会把结果的预期和实际返回值打印出来,轻松的找出错误的问题。极大的改善了开发人员的使用体验,提升了排查问题的效率。

相关文章

显卡天梯图2024最新版,显卡是电脑进行图形处理的重要设备,...
初始化电脑时出现问题怎么办,可以使用win系统的安装介质,连...
todesk远程开机怎么设置,两台电脑要在同一局域网内,然后需...
油猴谷歌插件怎么安装,可以通过谷歌应用商店进行安装,需要...
虚拟内存这个名词想必很多人都听说过,我们在使用电脑的时候...