[feature](test) Add regression test framework (#8125)

Add scalable regression testing framework(#7584)
contains
- Test framework making by groovy, and support built-in **readable DSL** named as `Action`
- Demo exists in `${DORIS_HOME}/regression-test/data/demo`
- Chinese doc exist in `${DORIS_HOME}/docs/zh-CN/developer-guide/regression-testing.md`

English document coming soon
This commit is contained in:
924060929
2022-02-19 12:05:50 +08:00
committed by GitHub
parent 5f50d9ae3b
commit 694e6433cc
39 changed files with 3584 additions and 0 deletions

3
.gitignore vendored
View File

@ -36,6 +36,9 @@ samples/**/.classpath
fe/fe-core/src/main/resources/static/
nohup.out
regression-test/framework/target
regression-test/framework/.flattened-pom.xml
regression-test/framework/dependency-reduced-pom.xml
# ignore eclipse project file & idea project file
.cproject

View File

@ -50,5 +50,7 @@ header:
- 'be/src/util/sse2neon.h'
- 'be/src/util/utf8_check.cpp'
- 'build-support/run_clang_format.py'
- 'regression-test/suites'
- 'regression-test/data'
comment: on-failure

View File

@ -713,6 +713,7 @@ module.exports = [
"How-to-Share-blogs",
"minidump",
"bitmap-hll-file-format",
"regression-testing",
],
},
{

View File

@ -0,0 +1,487 @@
---
{
"title": "回归测试",
"language": "zh-CN"
}
---
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# 回归测试
## 概念
1. `Suite`: 一个测试用例,目前仅用来指代测试用例文件名
2. `Group`: 一个测试集,目前仅用于指代测试用例所属的目录
3. `Action`: 一个封装好的具体测试行为,比如用于执行sql的`sql` Action,用于校验结果的`test` Action,用于导入数据的`streamLoad` Action等
## 测试步骤
1. 需要预先安装好集群
2. 修改配置文件`${DORIS_HOME}/conf/regression-conf.groovy`,设置jdbc url、用户等配置项
3. 创建测试用例文件并编写用例
4. 如果用例文件包含`qt` Action,则需要创建关联的的data文件,比如`suites/demo/qt_action.groovy`这个例子,需要用到`data/demo/qt_action.out`这个TSV文件来校验输出是否一致
5. 运行`${DORIS_HOME}/run-regression-test.sh`测试全部用例,或运行`${DORIS_HOME}/run-regression-test.sh --run <suiteName>` 测试若干用例,更多例子见"启动脚本例子"章节
## 目录结构
开发时需要关注的重要文件/目录
1. `run-regression-test.sh`: 启动脚本
2. `regression-conf.groovy`: 回归测试的默认配置
3. `data`: 存放输入数据和输出校验数据
4. `suites`: 存放用例
```
./${DORIS_HOME}
|-- **run-regression-test.sh** 回归测试启动脚本
|-- regression-test
| |-- conf
| | |-- logback.xml 日志配置文件
| | |-- **regression-conf.groovy** 默认配置文件
| |
| |-- framework 回归测试框架源码
| |-- **data** 用例的输入输出文件
| | |-- demo 存放demo的输入输出文件
| | |-- correctness 存放正确性测试用例的输入输出文件
| | |-- performance 存放性能测试用例的输入输出文件
| | |-- utils 存放其他工具的输入输出文件
| |
| |-- **suites** 回归测试用例
| |-- demo 存放测试用例的demo
| |-- correctness 存放正确性测试用例
| |-- performance 存放性能测试用例
| |-- utils 其他工具
|
|-- output
|-- regression-test
|-- log 回归测试日志
```
## 框架默认配置
测试时需要实际情况修改jdbc和fe的配置
```groovy
/* ============ 一般只需要关注下面这部分 ============ */
// 默认DB,如果未创建,则会尝试创建这个db
defaultDb = "regression_test"
// Jdbc配置
jdbcUrl = "jdbc:mysql://127.0.0.1:9030/?"
jdbcUser = "root"
jdbcPassword = ""
// fe地址配置, 用于stream load
feHttpAddress = "127.0.0.1:8030"
feHttpUser = "root"
feHttpPassword = ""
/* ============ 一般不需要修改下面的部分 ============ */
// DORIS_HOME变量是通过run-regression-test.sh传入的
// 即 java -DDORIS_HOME=./
// 设置回归测试用例的目录
suitePath = "${DORIS_HOME}/regression-test/suites"
// 设置输入输出数据的目录
dataPath = "${DORIS_HOME}/regression-test/data"
// 默认会读所有的组,读多个组可以用半角逗号隔开,如: "demo,performance"
// 一般不需要在配置文件中修改,而是通过run-regression-test.sh来动态指定和覆盖
testGroups = ""
// 默认会读所有的用例, 同样可以使用run-regression-test.sh来动态指定和覆盖
testSuites = ""
// 其他自定义配置
customConf1 = "test_custom_conf_value"
```
## 编写用例的步骤
1. 进入`${DORIS_HOME}/regression-test`目录
2. 根据测试的目的来选择用例的目录,正确性测试存在`suites/correctness`,而性能测试存在`suites/performance`
3. 新建一个groovy用例文件,增加若干`Action`用于测试,Action讲在后续章节具体说明
## Action
Action是一个测试框架默认提供的测试行为,使用DSL来定义。
### sql action
sql action用于提交sql并获取结果,如果查询失败则会抛出异常
参数如下
- String sql: 输入的sql字符串
- return List<List<Object>>: 查询结果,如果是DDL/DML,则返回一行一列,唯一的值是updateRowCount
下面的样例代码存放于`${DORIS_HOME}/regression-test/suites/demo/sql_action.groovy`:
```groovy
// execute sql and ignore result
sql "show databases"
// execute sql and get result, outer List denote rows, inner List denote columns in a single row
List<List<Object>> tables = sql "show tables"
// assertXxx() will invoke junit5's Assertions.assertXxx() dynamically
assertTrue(tables.size() >= 0) // test rowCount >= 0
// syntax error
try {
sql "a b c d e"
throw new IllegalStateException("Should be syntax error")
} catch (java.sql.SQLException t) {
assertTrue(true)
}
def testTable = "test_sql_action1"
try {
sql "DROP TABLE IF EXISTS ${testTable}"
// multi-line sql
def result1 = sql """
CREATE TABLE IF NOT EXISTS ${testTable} (
id int
)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
)
"""
// DDL/DML return 1 row and 1 column, the only value is update row count
assertTrue(result1.size() == 1)
assertTrue(result1[0].size() == 1)
assertTrue(result1[0][0] == 0, "Create table should update 0 rows")
def result2 = sql "INSERT INTO test_sql_action1 values(1), (2), (3)"
assertTrue(result2.size() == 1)
assertTrue(result2[0].size() == 1)
assertTrue(result2[0][0] == 3, "Insert should update 3 rows")
} finally {
/**
* try_xxx(args) means:
*
* try {
* return xxx(args)
* } catch (Throwable t) {
* // do nothing
* }
*/
try_sql("DROP TABLE IF EXISTS ${testTable}")
// you can see the error sql will not throw exception and return
try {
def errorSqlResult = try_sql("a b c d e f g")
assertTrue(errorSqlResult == null)
} catch (Throwable t) {
assertTrue(false, "Never catch exception")
}
}
// order_sql(sqlStr) equals to sql(sqlStr, isOrder=true)
// sort result by string dict
def list = order_sql """
select 2
union all
select 1
union all
select null
union all
select 15
union all
select 3
"""
assertEquals(null, list[0][0])
assertEquals(1, list[1][0])
assertEquals(15, list[2][0])
assertEquals(2, list[3][0])
assertEquals(3, list[4][0])
```
### qt action
qt action用于提交sql,并使用对应的.out TSV文件来校验结果
- String sql: 输入sql字符串
- return void
下面的样例代码存放于`${DORIS_HOME}/regression-test/suites/demo/qt_action.groovy`:
```groovy
/**
* qt_xxx sql equals to quickTest(xxx, sql) witch xxx is tag.
* the result will be compare to the relate file: ${DORIS_HOME}/regression_test/data/qt_action.out.
*
* if you want to generate .out tsv file for real execute result. you can run with -genOut or -forceGenOut option.
* e.g
* ${DORIS_HOME}/run-regression-test.sh --run qt_action -genOut
* ${DORIS_HOME}/run-regression-test.sh --run qt_action -forceGenOut
*/
qt_select "select 1, 'beijing' union all select 2, 'shanghai'"
qt_select2 "select 2"
// order result by string dict then compare to .out file.
// order_qt_xxx sql equals to quickTest(xxx, sql, true).
order_qt_union_all """
select 2
union all
select 1
union all
select null
union all
select 15
union all
select 3
"""
```
### test action
test action可以使用更复杂的校验规则来测试,比如验证行数、执行时间、是否抛出异常
可用参数
- String sql: 输入的sql字符串
- List<List<Object>> result: 提供一个List对象,用于校验真实查询结果对比是否相等
- String exception: 校验抛出的异常是否包含某些字符串
- long rowNum: 验证结果行数
- long time: 验证执行时间是否小于这个值,单位是毫秒
- Closure<List<List<Object>>, Throwable, Long, Long> check: 自定义回调校验,可传入结果、异常、时间。存在回调函数时,其他校验方式会失效。
下面的样例代码存放于`${DORIS_HOME}/regression-test/suites/demo/qt_action.groovy`:
```groovy
test {
sql "abcdefg"
// check exception message contains
exception "errCode = 2, detailMessage = Syntax error"
}
test {
sql """
select *
from (
select 1 id
union all
select 2
) a
order by id"""
// multi check condition
// check return 2 rows
rowNum 2
// execute time must <= 5000 millisecond
time 5000
// check result, must be 2 rows and 1 column, the first row is 1, second is 2
result(
[[1], [2]]
)
}
test {
sql "a b c d e f g"
// other check will not work because already declared a check callback
exception "aaaaaaaaa"
// callback
check { result, exception, startTime, endTime ->
// assertXxx() will invoke junit5's Assertions.assertXxx() dynamically
assertTrue(exception != null)
}
}
test {
sql """
select 2
union all
select 1
union all
select null
union all
select 15
union all
select 3
"""
check { result, ex, startTime, endTime ->
// same as order_sql(sqlStr)
result = sortRows(result)
assertEquals(null, result[0][0])
assertEquals(1, result[1][0])
assertEquals(15, result[2][0])
assertEquals(2, result[3][0])
assertEquals(3, result[4][0])
}
}
```
### explain action
explain action用来校验explain返回的字符串是否包含某些字符串
可用参数:
- String sql: 查询的sql,需要去掉sql中的explain
- String contains: 校验explain是否包含某些字符串,可多次调用校验同时多个结果
- String notContains: 校验explain是否不含某些字符串,可多次调用校验同时多个结果
- Closure<String> check: 自定义校验回调函数,可以获取返回的字符串,存在校验函数时,其他校验方式会失效
- Closure<String, Throwable, Long, Long> check: 自定义校验回调函数,可以额外获取异常和时间
下面的样例代码存放于`${DORIS_HOME}/regression-test/suites/demo/explain_action.groovy`:
```groovy
explain {
sql("select 100")
// contains("OUTPUT EXPRS:<slot 0> 100\n") && contains("PARTITION: UNPARTITIONED\n")
contains "OUTPUT EXPRS:<slot 0> 100\n"
contains "PARTITION: UNPARTITIONED\n"
}
explain {
sql("select 100")
// contains(" 100\n") && !contains("abcdefg") && !("1234567")
contains " 100\n"
notContains "abcdefg"
notContains "1234567"
}
explain {
sql("select 100")
// simple callback
check { explainStr -> explainStr.contains("abcdefg") || explainStr.contains(" 100\n") }
}
explain {
sql("a b c d e")
// callback with exception and time
check { explainStr, exception, startTime, endTime ->
// assertXxx() will invoke junit5's Assertions.assertXxx() dynamically
assertTrue(exception != null)
}
}
```
### streamLoad action
streamLoad action用于导入数据
可用参数为
- String db: db,默认值为regression-conf.groovy中的defaultDb
- String table: 表名
- String file: 要导入的文件路径,可以写data目录下的相对路径,或者写http url来导入网络文件
- Iterator<List<Object>> inputIterator: 要导入的迭代器
- String inputText: 要导入的文本, 较为少用
- InputStream inputStream: 要导入的字节流,较为少用
- long time: 验证执行时间是否小于这个值,单位是毫秒
- void set(String key, String value): 设置stream load的http请求的header,如label、columnSeparator
- Closure<String, Throwable, Long, Long> check: 自定义校验回调函数,可以获取返回结果、异常和超时时间。当存在回调函数时,其他校验项会失效。
下面的样例代码存放于`${DORIS_HOME}/regression-test/suites/demo/streamLoad_action.groovy`:
```groovy
def tableName = "test_streamload_action1"
sql """
CREATE TABLE IF NOT EXISTS ${tableName} (
id int,
name varchar(255)
)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
)
"""
streamLoad {
// you can skip declare db, because a default db already specify in ${DORIS_HOME}/conf/regression-conf.groovy
// db 'regression_test'
table tableName
// default label is UUID:
// set 'label' UUID.randomUUID().toString()
// default column_separator is specify in doris fe config, usually is '\t'.
// this line change to ','
set 'column_separator', ','
// relate to ${DORIS_HOME}/regression-test/data/demo/streamload_input.csv.
// also, you can stream load a http stream, e.g. http://xxx/some.csv
file 'streamload_input.csv'
time 10000 // limit inflight 10s
// stream load action will check result, include Success status, and NumberTotalRows == NumberLoadedRows
}
// stream load 100 rows
def rowCount = 100
def rowIt = java.util.stream.LongStream.range(0, rowCount) // [0, rowCount)
.mapToObj({i -> [i, "a_" + i]}) // change Long to List<Long, String>
.iterator()
streamLoad {
table tableName
// also, you can upload a memory iterator
inputIterator rowIt
// if declared a check callback, the default check condition will ignore.
// So you must check all condition
check { result, exception, startTime, endTime ->
if (exception != null) {
throw exception
}
log.info("Stream load result: ${result}".toString())
def json = parseJson(result)
assertEquals("success", json.Status.toLowerCase())
assertEquals(json.NumberTotalRows, json.NumberLoadedRows)
assertTrue(json.NumberLoadedRows > 0 && json.LoadBytes > 0)
}
}
```
## 启动脚本例子
```shell
# 查看脚本参数说明
./run-regression-test.sh h
# 查看框架参数说明
./run-regression-test.sh --run -h
# 测试所有用例
./run-regression-test.sh
# 删除测试框架编译结果和测试日志
./run-regression-test.sh --clean
# 测试suiteName为sql_action的用例, 目前suiteName等于文件名前缀,例子对应的用例文件是sql_action.groovy
./run-regression-test.sh --run sql_action
# 测试suiteName包含'sql'的用例,**注意需要用单引号括起来**
./run-regression-test.sh --run '*sql*'
# 测试demo和perfomance group
./run-regression-test.sh --run -g 'demo,performance'
# 测试demo group下的sql_action
./run-regression-test.sh --run -g demo -s sql_action
# 自定义配置
./run-regression-test.sh --run -conf a=b
```
## 使用查询结果自动生成.out文件
```shell
# 使用查询结果自动生成sql_action用例的.out文件,如果.out文件存在则忽略
./run-regression-test.sh --run sql_action -genOut
# 使用查询结果自动生成sql_action用例的.out文件,如果.out文件存在则覆盖
./run-regression-test.sh --run sql_action -forceGenOut
```

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<timestamp key="LOG_TIME" datePattern="yyyyMMdd.HHmmss"/>
<appender name="stdoutAppender" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread] \(%F:%L\) - %msg%n</pattern>
</encoder>
</appender>
<appender name="fileAppender" class="ch.qos.logback.core.FileAppender">
<!-- you can specify LOG_PATH by 'java -DLOG_PATH=xxx ...',
default LOG_PATH is './log'
-->
<file>${LOG_PATH:-./log}/doris-regression-test.${LOG_TIME}.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread] \(%F:%L\) - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="stdoutAppender" />
<appender-ref ref="fileAppender" />
</root>
</configuration>

View File

@ -0,0 +1,42 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/* ******* Do not commit this file unless you know what you are doing ******* */
// **Note**: default db will be create if not exist
defaultDb = "regression_test"
jdbcUrl = "jdbc:mysql://127.0.0.1:9030/?"
jdbcUser = "root"
jdbcPassword = ""
feHttpAddress = "127.0.0.1:8030"
feHttpUser = "root"
feHttpPassword = ""
// set DORIS_HOME by system properties
// e.g. java -DDORIS_HOME=./
suitePath = "${DORIS_HOME}/regression-test/suites"
dataPath = "${DORIS_HOME}/regression-test/data"
// will test <group>/<suite>.groovy
// empty group will test all group
testGroups = ""
// empty suite will test all suite
testSuites = ""
customConf1 = "test_custom_conf_value"

View File

@ -0,0 +1,4 @@
-- This file is automatically generated. You should know what you did if you want to edit this
-- !select1 --
100 test 2021-01-02

View File

@ -0,0 +1,15 @@
-- This file is automatically generated. You should know what you did if you want to edit this
-- !select --
1 beijing
2 shanghai
-- !select2 --
2
-- !union --
\N
1
15
2
3

View File

@ -0,0 +1,3 @@
1,BeiJing
2,ShangHai
3,GuangZhou
1 1 BeiJing
2 2 ShangHai
3 3 GuangZhou

View File

@ -0,0 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# framework

View File

@ -0,0 +1,16 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

View File

@ -0,0 +1,33 @@
<?xml version="1.0"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<!-- See https://checkstyle.org/ for details -->
<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="Header">
<property name="headerFile" value="checkstyle-apache-header.txt"/>
<property name="fileExtensions" value="java"/>
</module>
<module name="SuppressWarningsFilter"/>
<module name="TreeWalker">
<module name="AvoidStarImport"/>
<module name="UnusedImports"/>
</module>
</module>

View File

@ -0,0 +1,219 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache</groupId>
<artifactId>apache</artifactId>
<version>23</version>
</parent>
<groupId>org.apache.doris</groupId>
<artifactId>regression-test</artifactId>
<version>${revision}</version>
<packaging>jar</packaging>
<name>Doris Regression Test Project</name>
<url>https://doris.apache.org/</url>
<licenses>
<license>
<name>Apache 2.0 License</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.html</url>
<distribution>repo</distribution>
</license>
</licenses>
<scm>
<connection>scm:git:https://git@github.com/apache/incubator-doris.git</connection>
<developerConnection>scm:git:https://git@github.com/apache/incubator-doris.git</developerConnection>
<url>scm:git:https://git@github.com/apache/incubator-doris.git</url>
<tag>HEAD</tag>
</scm>
<issueManagement>
<system>GitHub</system>
<url>https://github.com/apache/incubator-doris/issues</url>
</issueManagement>
<mailingLists>
<mailingList>
<name>Dev Mailing List</name>
<post>dev@doris.apache.org</post>
<subscribe>dev-subscribe@doris.apache.org</subscribe>
<unsubscribe>dev-unsubscribe@doris.apache.org</unsubscribe>
</mailingList>
<mailingList>
<name>Commits Mailing List</name>
<post>commits@doris.apache.org</post>
<subscribe>commits-subscribe@doris.apache.org</subscribe>
<unsubscribe>commits-unsubscribe@doris.apache.org</unsubscribe>
</mailingList>
</mailingLists>
<properties>
<doris.home>${basedir}/../../</doris.home>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<revision>1.0-SNAPSHOT</revision>
<project.scm.id>github</project.scm.id>
<groovy.version>3.0.7</groovy.version>
<groovy-eclipse-batch.version>3.0.7-01</groovy-eclipse-batch.version>
<groovy-eclipse-compiler.version>3.7.0</groovy-eclipse-compiler.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.2.5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.9.0</version>
<configuration>
<compilerId>groovy-eclipse-compiler</compilerId>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<fork>true</fork>
</configuration>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-eclipse-compiler</artifactId>
<version>${groovy-eclipse-compiler.version}</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-eclipse-batch</artifactId>
<version>${groovy-eclipse-batch.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-eclipse-compiler</artifactId>
<version>${groovy-eclipse-compiler.version}</version>
<extensions>true</extensions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.apache.doris.regression.RegressionTest</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.2.5</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>bom</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>org.jodd</groupId>
<artifactId>jodd-core</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,259 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import com.google.common.collect.Maps
import org.apache.commons.cli.CommandLine
import org.apache.doris.regression.util.FileUtils
import org.apache.doris.regression.util.JdbcUtils
import java.sql.Connection
import java.sql.DriverManager
import static org.apache.doris.regression.ConfigOptions.*
@Slf4j
@CompileStatic
class Config {
public String jdbcUrl
public String jdbcUser
public String jdbcPassword
public String defaultDb
public String feHttpAddress
public String feHttpUser
public String feHttpPassword
public String suitePath
public String dataPath
public String testGroups
public String testSuites
public boolean generateOutputFile
public boolean forceGenerateOutputFile
public Properties otherConfigs = new Properties()
public Set<String> suiteWildcard = new HashSet<>()
public Set<String> groups = new HashSet<>()
public InetSocketAddress feHttpInetSocketAddress
Config() {}
Config(String defaultDb, String jdbcUrl, String jdbcUser, String jdbcPassword,
String feHttpAddress, String feHttpUser, String feHttpPassword,
String suitePath, String dataPath, String testGroups, String testSuites) {
this.defaultDb = defaultDb
this.jdbcUrl = jdbcUrl
this.jdbcUser = jdbcUser
this.jdbcPassword = jdbcPassword
this.feHttpAddress = feHttpAddress
this.feHttpUser = feHttpUser
this.feHttpPassword = feHttpPassword
this.suitePath = suitePath
this.dataPath = dataPath
this.testGroups = testGroups
this.testSuites = testSuites
}
static Config fromCommandLine(CommandLine cmd) {
String confFilePath = cmd.getOptionValue(confFileOpt, "")
File confFile = new File(confFilePath)
Config config = new Config()
if (confFile.exists() && confFile.isFile()) {
log.info("Load config file ${confFilePath}".toString())
def configSlurper = new ConfigSlurper()
def systemProperties = Maps.newLinkedHashMap(System.getProperties())
configSlurper.setBinding(systemProperties)
ConfigObject configObj = configSlurper.parse(new File(confFilePath).toURI().toURL())
config = Config.fromConfigObject(configObj)
}
fillDefaultConfig(config)
config.suitePath = FileUtils.getCanonicalPath(cmd.getOptionValue(pathOpt, config.suitePath))
config.dataPath = FileUtils.getCanonicalPath(cmd.getOptionValue(dataOpt, config.dataPath))
config.suiteWildcard = cmd.getOptionValue(suiteOpt, config.testSuites)
.split(",")
.collect({g -> g.trim()})
.findAll({g -> g != null && g.length() > 0})
.toSet()
config.groups = cmd.getOptionValue(groupsOpt, config.testGroups)
.split(",")
.collect({g -> g.trim()})
.findAll({g -> g != null && g.length() > 0})
.toSet()
config.feHttpAddress = cmd.getOptionValue(feHttpAddressOpt, config.feHttpAddress)
try {
Inet4Address host = Inet4Address.getByName(config.feHttpAddress.split(":")[0]) as Inet4Address
int port = Integer.valueOf(config.feHttpAddress.split(":")[1])
config.feHttpInetSocketAddress = new InetSocketAddress(host, port)
} catch (Throwable t) {
throw new IllegalStateException("Can not parse stream load address: ${config.feHttpAddress}", t)
}
config.defaultDb = cmd.getOptionValue(jdbcOpt, config.defaultDb)
config.jdbcUrl = cmd.getOptionValue(jdbcOpt, config.jdbcUrl)
config.jdbcUser = cmd.getOptionValue(userOpt, config.jdbcUser)
config.jdbcPassword = cmd.getOptionValue(passwordOpt, config.jdbcPassword)
config.feHttpUser = cmd.getOptionValue(feHttpUserOpt, config.feHttpUser)
config.feHttpPassword = cmd.getOptionValue(feHttpPasswordOpt, config.feHttpPassword)
config.generateOutputFile = cmd.hasOption(genOutOpt)
config.forceGenerateOutputFile = cmd.hasOption(forceGenOutOpt)
Properties props = cmd.getOptionProperties("conf")
config.otherConfigs.putAll(props)
config.tryCreateDbIfNotExist()
config.buildUrlWithDefaultDb()
return config
}
static Config fromConfigObject(ConfigObject obj) {
def config = new Config(
configToString(obj.defaultDb),
configToString(obj.jdbcUrl),
configToString(obj.jdbcUser),
configToString(obj.jdbcPassword),
configToString(obj.feHttpAddress),
configToString(obj.feHttpUser),
configToString(obj.feHttpPassword),
configToString(obj.suitePath),
configToString(obj.dataPath),
configToString(obj.testGroups),
configToString(obj.testSuites)
)
def declareFileNames = config.getClass()
.getDeclaredFields()
.collect({f -> f.name})
.toSet()
for (def kv : obj.toProperties().entrySet()) {
String key = kv.getKey() as String
if (!declareFileNames.contains(key)) {
config.otherConfigs.put(key, kv.getValue())
}
}
return config
}
static void fillDefaultConfig(Config config) {
if (config.defaultDb == null) {
config.defaultDb = "regression_test"
log.info("Set defaultDb to '${config.defaultDb}' because not specify.".toString())
}
if (config.jdbcUrl == null) {
config.jdbcUrl = "jdbc:mysql://127.0.0.1:9030"
log.info("Set jdbcUrl to '${config.jdbcUrl}' because not specify.".toString())
}
if (config.jdbcUser == null) {
config.jdbcUser = "root"
log.info("Set jdbcUser to '${config.jdbcUser}' because not specify.".toString())
}
if (config.jdbcPassword == null) {
config.jdbcPassword = ""
log.info("Set jdbcPassword to empty because not specify.".toString())
}
if (config.feHttpAddress == null) {
config.feHttpAddress = "127.0.0.1:8030"
log.info("Set feHttpAddress to '${config.feHttpAddress}' because not specify.".toString())
}
if (config.feHttpUser == null) {
config.feHttpUser = "root"
log.info("Set feHttpUser to '${config.feHttpUser}' because not specify.".toString())
}
if (config.feHttpPassword == null) {
config.feHttpPassword = ""
log.info("Set feHttpPassword to empty because not specify.".toString())
}
if (config.suitePath == null) {
config.suitePath = "regression-test/suites"
log.info("Set suitePath to '${config.suitePath}' because not specify.".toString())
}
if (config.dataPath == null) {
config.dataPath = "regression-test/suites"
log.info("Set dataPath to '${config.dataPath}' because not specify.".toString())
}
if (config.testGroups == null) {
config.testGroups = "default"
log.info("Set testGroups to '${config.testGroups}' because not specify.".toString())
}
if (config.testSuites == null) {
config.testSuites = ""
log.info("Set testSuites to empty because not specify.".toString())
}
}
static String configToString(Object obj) {
return (obj instanceof String || obj instanceof GString) ? obj.toString() : null
}
void tryCreateDbIfNotExist() {
// connect without specify default db
try {
String sql = "CREATE DATABASE IF NOT EXISTS ${defaultDb}"
log.info("Try to create db, sql: ${sql}".toString())
getConnection().withCloseable { conn ->
JdbcUtils.executeToList(conn, sql)
}
} catch (Throwable t) {
throw new IllegalStateException("Create database failed, jdbcUrl: ${jdbcUrl}", t)
}
}
Connection getConnection() {
return DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword)
}
private void buildUrlWithDefaultDb() {
String urlWithDb = jdbcUrl
String urlWithoutSchema = jdbcUrl.substring(jdbcUrl.indexOf("://") + 3)
if (urlWithoutSchema.indexOf("/") >= 0) {
if (jdbcUrl.contains("?")) {
// e.g: jdbc:mysql://locahost:8080/?a=b
urlWithDb = jdbcUrl.substring(0, jdbcUrl.lastIndexOf("/"))
urlWithDb += ("/" + defaultDb) + jdbcUrl.substring(jdbcUrl.lastIndexOf("?"))
} else {
// e.g: jdbc:mysql://locahost:8080/
urlWithDb += defaultDb
}
} else {
// e.g: jdbc:mysql://locahost:8080
urlWithDb += ("/" + defaultDb)
}
this.jdbcUrl = urlWithDb
log.info("Reset jdbcUrl to ${jdbcUrl}".toString())
// check connection with default db
getConnection().close()
}
}

View File

@ -0,0 +1,200 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression
import groovy.transform.CompileStatic
import org.apache.commons.cli.CommandLine
import org.apache.commons.cli.DefaultParser
import org.apache.commons.cli.HelpFormatter
import org.apache.commons.cli.Option
import org.apache.commons.cli.Options
@CompileStatic
class ConfigOptions {
static Option helpOption
static Option confFileOpt
static Option defaultDbOpt
static Option jdbcOpt
static Option userOpt
static Option passwordOpt
static Option feHttpAddressOpt
static Option feHttpUserOpt
static Option feHttpPasswordOpt
static Option pathOpt
static Option dataOpt
static Option suiteOpt
static Option groupsOpt
static Option confOpt
static Option genOutOpt
static Option forceGenOutOpt
static CommandLine initCommands(String[] args) {
helpOption = Option.builder("h")
.required(false)
.hasArg(false)
.longOpt("help")
.desc("print this usage help")
.build()
confFileOpt = Option.builder("cf")
.argName("confFilePath")
.required(false)
.hasArg(true)
.type(String.class)
.longOpt("confFile")
.desc("the configure file path")
.build()
defaultDbOpt = Option.builder("db")
.argName("db")
.required(false)
.hasArg(true)
.type(String.class)
.longOpt("defaultDb")
.desc("default db")
.build()
jdbcOpt = Option.builder("c")
.argName("url")
.required(false)
.hasArg(true)
.type(String.class)
.longOpt("jdbc")
.desc("jdbc url")
.build()
userOpt = Option.builder("u")
.argName("user")
.required(false)
.hasArg(true)
.type(String.class)
.longOpt("user")
.desc("the username of jdbc connection")
.build()
passwordOpt = Option.builder("p")
.argName("password")
.required(false)
.hasArg(true)
.optionalArg(true)
.type(String.class)
.longOpt("password")
.desc("the password of jdbc connection")
.build()
pathOpt = Option.builder("P")
.argName("path")
.required(false)
.hasArg(true)
.type(String.class)
.longOpt("path")
.desc("the suite path")
.build()
dataOpt = Option.builder("D")
.argName("dataPath")
.required(false)
.hasArg(true)
.type(String.class)
.longOpt("dataPath")
.desc("the data path")
.build()
suiteOpt = Option.builder("s")
.argName("suiteName")
.required(false)
.hasArg(true)
.optionalArg(true)
.type(String.class)
.longOpt("suite")
.desc("the suite name wildcard to be test")
.build()
groupsOpt = Option.builder("g")
.argName("groups")
.required(false)
.hasArg(true)
.optionalArg(true)
.type(String.class)
.longOpt("groups")
.desc("the suite group to be test")
.build()
feHttpAddressOpt = Option.builder("ha")
.argName("address")
.required(false)
.hasArg(true)
.type(String.class)
.longOpt("feHttpAddress")
.desc("the fe http address, format is ip:port")
.build()
feHttpUserOpt = Option.builder("hu")
.argName("userName")
.required(false)
.hasArg(true)
.type(String.class)
.longOpt("feHttpUser")
.desc("the user of fe http server")
.build()
feHttpPasswordOpt = Option.builder("hp")
.argName("password")
.required(false)
.hasArg(true)
.type(String.class)
.longOpt("feHttpPassword")
.desc("the password of fe http server")
.build()
genOutOpt = Option.builder("genOut")
.required(false)
.hasArg(false)
.desc("generate qt .out file if not exist")
.build()
forceGenOutOpt = Option.builder("forceGenOut")
.required(false)
.hasArg(false)
.desc("delete and generate qt .out file")
.build()
confOpt = Option.builder("conf")
.argName("conf")
.required(false)
.hasArgs()
.valueSeparator(('=' as char))
.longOpt("configurations, format: key=value")
.desc("set addition context configurations")
.build()
Options options = new Options()
.addOption(helpOption)
.addOption(jdbcOpt)
.addOption(userOpt)
.addOption(passwordOpt)
.addOption(pathOpt)
.addOption(dataOpt)
.addOption(confOpt)
.addOption(suiteOpt)
.addOption(groupsOpt)
.addOption(feHttpAddressOpt)
.addOption(feHttpUserOpt)
.addOption(feHttpPasswordOpt)
.addOption(genOutOpt)
.addOption(confFileOpt)
.addOption(forceGenOutOpt)
CommandLine cmd = new DefaultParser().parse(options, args, true)
if (cmd.hasOption(helpOption)) {
printHelp(options)
return null
}
return cmd
}
static void printHelp(Options options) {
HelpFormatter hf = new HelpFormatter()
hf.printHelp("regression-test", options, true)
}
}

View File

@ -0,0 +1,189 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression
import groovy.transform.CompileStatic
import jodd.util.Wildcard
import org.apache.doris.regression.suite.Suite
import org.apache.doris.regression.suite.SuiteContext
import org.apache.doris.regression.util.Recorder
import groovy.util.logging.Slf4j
import org.apache.commons.cli.*
import org.apache.doris.regression.util.SuiteInfo
import org.codehaus.groovy.control.CompilerConfiguration
import java.util.stream.Collectors
@Slf4j
@CompileStatic
class RegressionTest {
static ClassLoader classloader
static CompilerConfiguration compileConfig
static GroovyShell shell
static void main(String[] args) {
CommandLine cmd = ConfigOptions.initCommands(args)
if (cmd == null) {
return
}
Config config = Config.fromCommandLine(cmd)
initGroovyEnv(config)
Recorder recorder = runSuites(config)
printResult(config, recorder)
}
static void initGroovyEnv(Config config) {
classloader = new GroovyClassLoader()
compileConfig = new CompilerConfiguration()
compileConfig.setScriptBaseClass((Suite as Class).name)
shell = new GroovyShell(classloader, new Binding(), compileConfig)
}
static List<File> findSuiteFiles(String root) {
if (root == null) {
log.warn("Not specify suite path")
return new ArrayList<File>()
}
List<File> files = new ArrayList<>()
new File(root).eachFileRecurse { f ->
if (f.isFile() && f.name.endsWith(".groovy")) {
files.add(f)
}
}
return files
}
static String parseGroup(Config config, File suiteFile) {
String group = new File(config.suitePath).relativePath(suiteFile)
int separatorIndex = group.lastIndexOf(File.separator)
if (separatorIndex == -1) {
return ""
} else {
return group.substring(0, separatorIndex)
}
}
static Recorder runSuites(Config config) {
def files = findSuiteFiles(config.suitePath)
def recorder = new Recorder()
List<SuiteFile> runScripts = files.stream().map({ file ->
String suiteName = file.name.substring(0, file.name.lastIndexOf("."))
String group = parseGroup(config, file)
return new SuiteFile(file, suiteName, group)
}).filter({ sf ->
canRun(config, sf.suiteName, sf.group)
}).collect(Collectors.toList())
log.info("Start to run suites")
int totalFile = runScripts.size()
runScripts.eachWithIndex { sf, i ->
File file = sf.file
String suiteName = sf.suiteName
String group = sf.group
def suiteConn = config.getConnection()
new SuiteContext(file, suiteConn, config, recorder).withCloseable { context ->
try {
log.info("[${i + 1}/${totalFile}] Run ${suiteName} in $file".toString())
Suite suite = shell.parse(file) as Suite
suite.init(suiteName, group, context)
suite.run()
recorder.onSuccess(new SuiteInfo(file, group, suiteName))
log.info("Run ${suiteName} in ${file.absolutePath} succeed".toString())
} catch (Throwable t) {
recorder.onFailure(new SuiteInfo(file, group, suiteName))
log.error("Run ${suiteName} in ${file.absolutePath} failed".toString(), t)
}
}
}
return recorder
}
static boolean canRun(Config config, String suiteName, String group) {
Set<String> suiteGroups = group.split(",").collect {g -> g.trim()}.toSet()
if (config.suiteWildcard.size() == 0 ||
(suiteName != null && (config.suiteWildcard.any {
suiteWildcard -> Wildcard.match(suiteName, suiteWildcard)
}))) {
if (config.groups == null || config.groups.isEmpty()
|| !config.groups.intersect(suiteGroups).isEmpty()) {
return true
}
}
return false
}
static void printResult(Config config, Recorder recorder) {
int allSuiteNum = recorder.successList.size() + recorder.failureList.size()
int failedSuiteNum = recorder.failureList.size()
log.info("Test ${allSuiteNum} suites, failed ${failedSuiteNum} suites".toString())
// print success list
{
String successList = recorder.successList.collect { info ->
"${info.file.absolutePath}: group=${info.group}, name=${info.suiteName}"
}.join("\n")
log.info("successList suites:\n${successList}".toString())
}
// print failure list
if (!recorder.failureList.isEmpty()) {
def failureList = recorder.failureList.collect() { info ->
"${info.file.absolutePath}: group=${info.group}, name=${info.suiteName}"
}.join("\n")
log.info("Failure suites:\n${failureList}".toString())
printFailed()
throw new IllegalStateException("Test failed")
} else {
printPassed()
}
}
static void printPassed() {
log.info("""All suites success.
| ____ _ ____ ____ _____ ____
|| _ \\ / \\ / ___/ ___|| ____| _ \\
|| |_) / _ \\ \\___ \\___ \\| _| | | | |
|| __/ ___ \\ ___) |__) | |___| |_| |
||_| /_/ \\_\\____/____/|_____|____/
|""".stripMargin())
}
static void printFailed() {
log.info("""Some suites failed.
| _____ _ ___ _ _____ ____
|| ___/ \\ |_ _| | | ____| _ \\
|| |_ / _ \\ | || | | _| | | | |
|| _/ ___ \\ | || |___| |___| |_| |
||_|/_/ \\_\\___|_____|_____|____/
|""".stripMargin())
}
static class SuiteFile {
File file
String suiteName
String group
SuiteFile(File file, String suiteName, String group) {
this.file = file
this.suiteName = suiteName
this.group = group
}
}
}

View File

@ -0,0 +1,144 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.action
import org.apache.doris.regression.suite.SuiteContext
import org.apache.doris.regression.util.JdbcUtils
import groovy.util.logging.Slf4j
import java.util.stream.Collectors
@Slf4j
class ExplainAction implements SuiteAction {
private String sql
private SuiteContext context
private Set<String> containsStrings = new LinkedHashSet<>()
private Set<String> notContainsStrings = new LinkedHashSet<>()
private Closure checkFunction
ExplainAction(SuiteContext context) {
this.context = context
}
void sql(String sql) {
this.sql = sql
}
void sql(Closure<String> sqlSupplier) {
this.sql = sqlSupplier.call()
}
void contains(String subString) {
containsStrings.add(subString)
}
void notContains(String subString) {
notContainsStrings.add(subString)
}
void check(Closure<Boolean> checkFunction) {
this.checkFunction = checkFunction
}
@Override
void run() {
String explainSql = "explain\n" + sql
def result = doTest(explainSql)
String explainString = result.result
if (checkFunction != null) {
try {
Boolean checkResult = null
if (checkFunction.parameterTypes.size() == 1) {
if (result.exception == null) {
checkResult = checkFunction(explainString)
} else {
throw result.exception
}
} else {
checkResult = checkFunction(explainString, result.exception, result.startTime, result.endTime)
}
if (checkResult != null && checkResult.booleanValue() == false) {
String msg = "Explain and custom check failed, actual explain string is:\n${explainString}".toString()
throw new IllegalStateException(msg)
}
} catch (Throwable t) {
log.error("Explain and custom check failed", t)
List resList = [context.file.getName(), 'explain', sql, t]
context.recorder.reportDiffResult(resList)
throw t
}
} else if (result.exception != null) {
String msg = "Explain failed"
log.error(msg, result.exception)
List resList = [context.file.getName(), 'explain', sql, result.exception]
context.recorder.reportDiffResult(resList)
throw new IllegalStateException(msg, result.exception)
} else {
for (String string : containsStrings) {
if (!explainString.contains(string)) {
String msg = ("Explain and check failed, expect contains '${string}',"
+ "but actual explain string is:\n${explainString}").toString()
log.info(msg)
def t = new IllegalStateException(msg)
List resList = [context.file.getName(), 'explain', sql, t]
context.recorder.reportDiffResult(resList)
throw t
}
}
for (String string : notContainsStrings) {
if (explainString.contains(string)) {
String msg = ("Explain and check failed, expect not contains '${string}',"
+ "but actual explain string is:\n${explainString}").toString()
log.info(msg)
def t = new IllegalStateException(msg)
List resList = [context.file.getName(), 'explain', sql, t]
context.recorder.reportDiffResult(resList)
throw t
}
}
}
}
private ActionResult doTest(String explainSql) {
log.info("Execute sql:\n${explainSql}".toString())
long startTime = System.currentTimeMillis()
String explainString = null
try {
explainString = JdbcUtils.executeToList(context.conn, explainSql).stream()
.map({row -> row.get(0).toString()})
.collect(Collectors.joining("\n"))
return new ActionResult(explainString, null, startTime, System.currentTimeMillis())
} catch (Throwable t) {
return new ActionResult(explainString, t, startTime, System.currentTimeMillis())
}
}
class ActionResult {
String result
Throwable exception
long startTime
long endTime
ActionResult(String result, Throwable exception, long startTime, long endTime) {
this.result = result
this.exception = exception
this.startTime = startTime
this.endTime = endTime
}
}
}

View File

@ -0,0 +1,297 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.action
import com.google.common.collect.Iterators
import org.apache.doris.regression.suite.SuiteContext
import org.apache.doris.regression.util.BytesInputStream
import org.apache.doris.regression.util.OutputUtils
import groovy.json.JsonSlurper
import groovy.util.logging.Slf4j
import org.apache.http.HttpEntity
import org.apache.http.client.methods.RequestBuilder
import org.apache.http.entity.FileEntity
import org.apache.http.entity.InputStreamEntity
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClients
import org.apache.http.util.EntityUtils
import org.junit.Assert
@Slf4j
class StreamLoadAction implements SuiteAction {
public final InetSocketAddress address
public final String user
public final String password
String db
String table
String file
InputStream inputStream
String inputText
Iterator<List<Object>> inputIterator
long time
Closure check
Map<String, String> headers
SuiteContext context
StreamLoadAction(SuiteContext context) {
this.address = context.config.feHttpInetSocketAddress
this.user = context.config.feHttpUser
this.password = context.config.feHttpPassword
this.db = context.config.defaultDb
this.context = context
this.headers = new LinkedHashMap<>()
this.headers.put('label', UUID.randomUUID().toString())
}
void db(String db) {
this.db = db
}
void db(Closure<String> db) {
this.db = db.call()
}
void table(String table) {
this.table = table
}
void table(Closure<String> table) {
this.table = table.call()
}
void inputStream(InputStream inputStream) {
this.inputStream = inputStream
}
void inputStream(Closure<InputStream> inputStream) {
this.inputStream = inputStream.call()
}
void inputIterator(Iterator<List<Object>> inputIterator) {
this.inputIterator = inputIterator
}
void inputIterator(Closure<Iterator<List<Object>>> inputIterator) {
this.inputIterator = inputIterator.call()
}
void inputText(String inputText) {
this.inputText = inputText
}
void inputText(Closure<String> inputText) {
this.inputText = inputText.call()
}
void file(String file) {
this.file = file
}
void file(Closure<String> file) {
this.file = file.call()
}
void time(long time) {
this.time = time
}
void time(Closure<Long> time) {
this.time = time.call()
}
void check(Closure check) {
this.check = check
}
void set(String key, String value) {
headers.put(key, value)
}
@Override
void run() {
String responseText = null
Throwable ex = null
long startTime = System.currentTimeMillis()
try {
def uri = "http://${address.hostString}:${address.port}/api/${db}/${table}/_stream_load"
HttpClients.createDefault().withCloseable { client ->
RequestBuilder requestBuilder = prepareRequestHeader(RequestBuilder.put(uri))
HttpEntity httpEntity = prepareHttpEntity(client)
String beLocation = streamLoadToFe(client, requestBuilder)
responseText = streamLoadToBe(client, requestBuilder, beLocation, httpEntity)
}
} catch (Throwable t) {
ex = t
}
long endTime = System.currentTimeMillis()
log.info("Stream load elapsed ${endTime - startTime} ms".toString())
checkResult(responseText, ex, startTime, endTime)
}
private String httpGetString(CloseableHttpClient client, String url) {
return client.execute(RequestBuilder.get(url).build()).withCloseable { resp ->
EntityUtils.toString(resp.getEntity())
}
}
private InputStream httpGetStream(CloseableHttpClient client, String url) {
return client.execute(RequestBuilder.get(url).build()).getEntity().getContent()
}
private RequestBuilder prepareRequestHeader(RequestBuilder requestBuilder) {
String encoding = Base64.getEncoder()
.encodeToString((user + ":" + (password == null ? "" : password)).getBytes("UTF-8"))
requestBuilder.setHeader("Authorization", "Basic ${encoding}")
for (Map.Entry<String, String> entry : headers.entrySet()) {
requestBuilder.setHeader(entry.key, entry.value)
}
requestBuilder.setHeader("Expect", "100-Continue")
return requestBuilder
}
private HttpEntity prepareHttpEntity(CloseableHttpClient client) {
HttpEntity entity = null
if (inputStream != null) {
entity = new InputStreamEntity(inputStream)
} else if (inputText != null) {
entity = new StringEntity(inputText)
} else if (inputIterator != null) {
def bytesIt = Iterators.transform(inputIterator,
{row -> (OutputUtils.toCsvString(row) + "\n").getBytes()})
entity = new InputStreamEntity(new BytesInputStream(bytesIt))
} else {
String fileName = this.file
if (fileName.startsWith("http://") || fileName.startsWith("https://")) {
log.info("Set stream load input: ${fileName}".toString())
entity = new InputStreamEntity(httpGetStream(client, fileName))
} else { // local file
if (!new File(fileName).isAbsolute()) {
fileName = new File(context.dataPath, fileName).getAbsolutePath()
}
def file = new File(fileName)
if (!file.exists()) {
log.warn("Stream load input file not exists: ${file}".toString())
}
log.info("Set stream load input: ${file.canonicalPath}".toString())
entity = new FileEntity(file)
}
}
return entity
}
private String streamLoadToFe(CloseableHttpClient client, RequestBuilder requestBuilder) {
log.info("Stream load to ${requestBuilder.uri}".toString())
String backendStreamLoadUri = null
client.execute(requestBuilder.build()).withCloseable { resp ->
resp.withCloseable {
String body = EntityUtils.toString(resp.getEntity())
def respCode = resp.getStatusLine().getStatusCode()
// should redirect to backend
if (respCode != 307) {
List resList = [context.file.getName(), 'streamLoad', '', "Expect frontend stream load response code is 307, " +
"but meet ${respCode}\nbody: ${body}"]
context.recorder.reportDiffResult(resList)
throw new IllegalStateException("Expect frontend stream load response code is 307, " +
"but meet ${respCode}\nbody: ${body}")
}
backendStreamLoadUri = resp.getFirstHeader("location").getValue()
}
}
return backendStreamLoadUri
}
private String streamLoadToBe(CloseableHttpClient client, RequestBuilder requestBuilder, String beLocation, HttpEntity httpEntity) {
log.info("Redirect stream load to ${beLocation}".toString())
requestBuilder.setUri(beLocation)
requestBuilder.setEntity(httpEntity)
String responseText
try{
client.execute(requestBuilder.build()).withCloseable { resp ->
resp.withCloseable {
String body = EntityUtils.toString(resp.getEntity())
def respCode = resp.getStatusLine().getStatusCode()
if (respCode != 200) {
List resList = [context.file.getName(), 'streamLoad', '', "Expect backend stream load response code is 200, " +
"but meet ${respCode}\nbody: ${body}"]
context.recorder.reportDiffResult(resList)
throw new IllegalStateException("Expect backend stream load response code is 200, " +
"but meet ${respCode}\nbody: ${body}")
}
responseText = body
}
}
} catch (Throwable t) {
log.info("StreamLoadAction Exception: ", t)
List resList = [context.file.getName(), 'streamLoad', '', t]
context.recorder.reportDiffResult(resList)
}
return responseText
}
private void checkResult(String responseText, Throwable ex, long startTime, long endTime) {
if (check != null) {
check.call(responseText, ex, startTime, endTime)
} else {
if (ex != null) {
List resList = [context.file.getName(), 'streamLoad', '', ex]
context.recorder.reportDiffResult(resList)
throw ex
}
def jsonSlurper = new JsonSlurper()
def result = jsonSlurper.parseText(responseText)
String status = result.Status
if (!"Success".equalsIgnoreCase(status)) {
String errorUrl = result.ErrorURL
if (errorUrl != null) {
String errorDetails = HttpClients.createDefault().withCloseable { client ->
httpGetString(client, errorUrl)
}
List resList = [context.file.getName(), 'streamLoad', '', "Stream load failed:\n${responseText}\n${errorDetails}"]
context.recorder.reportDiffResult(resList)
throw new IllegalStateException("Stream load failed:\n${responseText}\n${errorDetails}")
}
List resList = [context.file.getName(), 'streamLoad', '', "Stream load failed:\n${responseText}"]
context.recorder.reportDiffResult(resList)
throw new IllegalStateException("Stream load failed:\n${responseText}")
}
long numberTotalRows = result.NumberTotalRows.toLong()
long numberLoadedRows = result.NumberLoadedRows.toLong()
if (numberTotalRows != numberLoadedRows) {
List resList = [context.file.getName(), 'streamLoad', '', "Stream load rows mismatch:\n${responseText}"]
context.recorder.reportDiffResult(resList)
throw new IllegalStateException("Stream load rows mismatch:\n${responseText}")
}
if (time > 0) {
long elapsed = endTime - startTime
try{
Assert.assertTrue("Expect elapsed <= ${time}, but meet ${elapsed}", elapsed <= time)
} catch (Throwable t) {
List resList = [context.file.getName(), 'streamLoad', '', "Expect elapsed <= ${time}, but meet ${elapsed}"]
context.recorder.reportDiffResult(resList)
throw new IllegalStateException("Expect elapsed <= ${time}, but meet ${elapsed}")
}
}
}
}
}

View File

@ -0,0 +1,22 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.action
interface SuiteAction {
void run()
}

View File

@ -0,0 +1,151 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.action
import groovy.util.logging.Slf4j
import java.sql.Connection
import org.apache.doris.regression.suite.SuiteContext
import org.apache.doris.regression.util.JdbcUtils
import org.junit.Assert
@Slf4j
class TestAction implements SuiteAction {
private String sql
private Object result
private long time
private long rowNum = -1
private String exception
private Closure check
SuiteContext context
TestAction(SuiteContext context) {
this.context = context
}
@Override
void run() {
try{
def result = doRun(context.conn)
if (check != null) {
check.call(result.result, result.exception, result.startTime, result.endTime)
} else {
if (exception != null || result.exception != null) {
def msg = result.exception?.toString()
Assert.assertTrue("Expect exception msg contains '${exception}', but meet '${msg}'",
msg != null && exception != null && msg.contains(exception))
}
if (time > 0) {
long elapsed = result.endTime - result.startTime
Assert.assertTrue("Expect elapsed <= ${time}, but meet ${elapsed}", elapsed <= time)
}
if (rowNum >= 0) {
if (result.result instanceof Integer || result.result instanceof Long) {
def realRowNum = ((Number) result.result).longValue()
Assert.assertEquals("RowNum", rowNum, realRowNum)
} else if (result.result instanceof List) {
def realRowNum = ((List) result.result).size().longValue()
Assert.assertEquals("RowNum", rowNum, realRowNum)
} else {
log.warn("Unknown result: ${result.result}, can not check row num".toString())
}
}
if (this.result != null) {
Assert.assertEquals(this.result, result.result)
}
}
} catch (Throwable t) {
log.info("TestAction Exception: ", t)
List resList = [context.file.getName(), 'test', sql, t]
context.recorder.reportDiffResult(resList)
throw t
}
}
ActionResult doRun(Connection conn) {
Object result = null
Throwable ex = null
long startTime = System.currentTimeMillis()
try {
log.info("Execute sql:\n${sql}".toString())
result = JdbcUtils.executeToList(conn, sql)
} catch (Throwable t) {
ex = t
}
long endTime = System.currentTimeMillis()
return new ActionResult(result, ex, startTime, endTime)
}
void sql(String sql) {
this.sql = sql
}
void sql(Closure<String> sqlSupplier) {
this.sql = sqlSupplier.call()
}
void time(long time) {
this.time = time
}
void time(Closure<Long> timeSupplier) {
this.time = timeSupplier.call()
}
void rowNum(long rowNum) {
this.rowNum = rowNum
}
void rowNum(Closure<Long> rowNum) {
this.rowNum = rowNum.call()
}
void result(Object result) {
this.result = result
}
void result(Closure<Object> resultSupplier) {
this.result = resultSupplier.call()
}
void exception(String exceptionMsg) {
this.exception = exceptionMsg
}
void exception(Closure<String> exceptionMsgSupplier) {
this.exception = exceptionMsgSupplier.call()
}
void check(Closure check) {
this.check = check
}
class ActionResult {
Object result
Throwable exception
long startTime
long endTime
ActionResult(Object result, Throwable exception, long startTime, long endTime) {
this.result = result
this.exception = exception
this.startTime = startTime
this.endTime = endTime
}
}
}

View File

@ -0,0 +1,196 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.suite
import groovy.json.JsonSlurper
import groovy.util.logging.Slf4j
import com.google.common.collect.ImmutableList
import org.apache.doris.regression.util.DataUtils
import org.apache.doris.regression.util.OutputUtils
import org.apache.doris.regression.action.ExplainAction
import org.apache.doris.regression.action.StreamLoadAction
import org.apache.doris.regression.action.SuiteAction
import org.apache.doris.regression.action.TestAction
import org.apache.doris.regression.util.JdbcUtils
import org.junit.jupiter.api.Assertions
import static org.apache.doris.regression.util.DataUtils.sortByToString
@Slf4j
abstract class Suite extends Script implements GroovyInterceptable {
SuiteContext context
String name
String group
void init(String name, String group, SuiteContext context) {
this.name = name
this.group = group
this.context = context
}
String getConf(String key, String defaultValue = null) {
String value = context.config.otherConfigs.get(key)
return value == null ? defaultValue : value
}
Properties getConfs(String prefix) {
Properties p = new Properties()
for (String name : context.config.otherConfigs.stringPropertyNames()) {
if (name.startsWith(prefix + ".")) {
p.put(name.substring(prefix.length() + 1), context.config.getProperty(name))
}
}
return p
}
String toCsv(List<Object> rows) {
StringBuilder sb = new StringBuilder()
for (int i = 0; i < rows.size(); ++i) {
Object row = rows.get(i)
if (!(row instanceof List)) {
row = ImmutableList.of(row)
}
sb.append(OutputUtils.toCsvString(row as List)).append("\n")
}
sb.toString()
}
Object parseJson(String str) {
def jsonSlurper = new JsonSlurper()
return jsonSlurper.parseText(str)
}
Object sql(String sqlStr, boolean isOrder = false) {
log.info("Execute sql: ${sqlStr}".toString())
def result = JdbcUtils.executeToList(context.conn, sqlStr)
if (isOrder) {
result = DataUtils.sortByToString(result)
}
return result
}
Object order_sql(String sqlStr) {
return sql(sqlStr, true)
}
List<List<Object>> sortRows(List<List<Object>> result) {
if (result == null) {
return null
}
return DataUtils.sortByToString(result)
}
void explain(Closure actionSupplier) {
runAction(new ExplainAction(context), actionSupplier)
}
void test(Closure actionSupplier) {
runAction(new TestAction(context), actionSupplier)
}
void streamLoad(Closure actionSupplier) {
runAction(new StreamLoadAction(context), actionSupplier)
}
void runAction(SuiteAction action, Closure actionSupplier) {
actionSupplier.setDelegate(action)
actionSupplier.setResolveStrategy(Closure.DELEGATE_FIRST)
actionSupplier.call()
action.run()
}
void quickTest(String tag, String sql, boolean order = false) {
log.info("Execute tag: ${tag}, sql: ${sql}".toString())
if (context.config.generateOutputFile || context.config.forceGenerateOutputFile) {
def result = JdbcUtils.executorToStringList(context.conn, sql)
if (order) {
result = sortByToString(result)
}
Iterator<List<Object>> realResults = result.iterator()
// generate and save to .out file
def writer = context.getOutputWriter(context.config.forceGenerateOutputFile)
writer.write(realResults, tag)
} else {
if (context.outputIterator == null) {
String res = "Missing outputFile: ${context.outputFile.getAbsolutePath()}"
List excelContentList = [context.file.getName(), context.file, context.file, res]
context.recorder.reportDiffResult(excelContentList)
throw new IllegalStateException("Missing outputFile: ${context.outputFile.getAbsolutePath()}")
}
if (!context.outputIterator.hasNext()) {
String res = "Missing output block for tag '${tag}': ${context.outputFile.getAbsolutePath()}"
List excelContentList = [context.file.getName(), tag, context.file, res]
context.recorder.reportDiffResult(excelContentList)
throw new IllegalStateException("Missing output block for tag '${tag}': ${context.outputFile.getAbsolutePath()}")
}
try {
Iterator<List<Object>> expectCsvResults = context.outputIterator.next() as Iterator
List<List<Object>> realResults = JdbcUtils.executorToStringList(context.conn, sql)
if (order) {
realResults = sortByToString(realResults)
}
def res = OutputUtils.assertEquals(expectCsvResults, realResults.iterator(), "Tag '${tag}' wrong")
if (res) {
List excelContentList = [context.file.getName(), tag, sql.trim(), res]
context.recorder.reportDiffResult(excelContentList)
throw new IllegalStateException("'${tag}' line not match . Detailed results is : '${res}'")
}
} catch (Throwable t) {
if (t.toString().contains('line not match . Detailed results is')) {
throw t
} else {
List excelContentList = [context.file.getName(), tag, sql.trim(), t]
context.recorder.reportDiffResult(excelContentList)
throw new IllegalStateException("'${tag}' run failed . Detailed failure information is : '${t}'", t)
}
}
}
}
@Override
Object invokeMethod(String name, Object args) {
// qt: quick test
if (name.startsWith("qt_")) {
return quickTest(name.substring("qt_".length()), (args as Object[])[0] as String)
} else if (name.startsWith("order_qt_")) {
return quickTest(name.substring("order_qt_".length()), (args as Object[])[0] as String, true)
} else if (name.startsWith("assert") && name.length() > "assert".length()) {
// delegate to junit Assertions dynamically
return Assertions."$name"(*args) // *args: spread-dot
} else if (name.startsWith("try_")) {
String realMethod = name.substring("try_".length())
try {
return this."$realMethod"(*args)
} catch (Throwable t) {
// do nothing
}
} else {
// invoke origin method
return metaClass.invokeMethod(this, name, args)
}
}
private Object invokeAssertions(String name, Object args) {
}
}

View File

@ -0,0 +1,107 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.suite
import groovy.transform.CompileStatic
import org.apache.doris.regression.Config
import org.apache.doris.regression.util.OutputUtils
import org.apache.doris.regression.util.Recorder
import groovy.util.logging.Slf4j
import org.apache.doris.regression.util.CloseableIterator
import java.sql.Connection
@Slf4j
@CompileStatic
class SuiteContext implements Closeable {
public final File file
public final Connection conn
public final Config config
public final File dataPath
public final File outputFile
public final Recorder recorder
// public final File tmpOutputPath
public final CloseableIterator<Iterator<List<String>>> outputIterator
private volatile OutputUtils.OutputBlocksWriter outputBlocksWriter
SuiteContext(File file, Connection conn, Config config, Recorder recorder) {
this.file = file
this.conn = conn
this.config = config
this.recorder = recorder
def path = new File(config.suitePath).relativePath(file)
def outputRelativePath = path.substring(0, path.lastIndexOf(".")) + ".out"
this.outputFile = new File(new File(config.dataPath), outputRelativePath)
this.dataPath = this.outputFile.getParentFile().getCanonicalFile()
if (!config.otherConfigs.getProperty("qt.generate.out", "false").toBoolean()
&& outputFile.exists()) {
this.outputIterator = OutputUtils.iterator(outputFile)
}
// def dataParentPath = new File(config.dataPath).parentFile.absolutePath
// def tmpOutputPath = "${dataParentPath}/tmp_output/${outputRelativePath}".toString()
// this.tmpOutputPath = new File(tmpOutputPath)
}
OutputUtils.OutputBlocksWriter getOutputWriter(boolean deleteIfExist) {
if (outputBlocksWriter != null) {
return outputBlocksWriter
}
synchronized (this) {
if (outputBlocksWriter != null) {
return outputBlocksWriter
} else if (outputFile.exists() && deleteIfExist) {
log.info("Delete ${outputFile}".toString())
outputFile.delete()
log.info("Generate ${outputFile}".toString())
outputFile.createNewFile()
outputBlocksWriter = OutputUtils.writer(outputFile)
} else if (!outputFile.exists()) {
outputFile.parentFile.mkdirs()
outputFile.createNewFile()
log.info("Generate ${outputFile}".toString())
outputBlocksWriter = OutputUtils.writer(outputFile)
} else {
log.info("Skip generate output file because exists: ${outputFile}".toString())
outputBlocksWriter = new OutputUtils.OutputBlocksWriter(null)
}
return outputBlocksWriter
}
}
@Override
void close() {
if (outputIterator != null) {
try {
outputIterator.close()
} catch (Throwable t) {
log.warn("Close outputFile failed", t)
}
}
if (outputBlocksWriter != null) {
outputBlocksWriter.close()
}
try {
conn.close()
} catch (Throwable t) {
log.warn("Close connection failed", t)
}
}
}

View File

@ -0,0 +1,66 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.util
import groovy.transform.CompileStatic
@CompileStatic
class BytesInputStream extends InputStream {
private Iterator<byte[]> bytesIt
private ByteArrayInputStream currentStream = new ByteArrayInputStream(new byte[0])
BytesInputStream(Iterator<byte[]> bytesIt) {
this.bytesIt = bytesIt
}
@Override
int read() throws IOException {
int byteValue = currentStream.read()
if (byteValue == -1) {
if (bytesIt.hasNext()) {
currentStream = new ByteArrayInputStream(bytesIt.next())
return read()
} else {
return -1
}
}
return byteValue
}
@Override
int read(byte[] b, int off, int len) throws IOException {
int readSize = 0
while (readSize < len) {
int read = currentStream.read(b, off + readSize, len - readSize)
if (read == -1) {
if (bytesIt.hasNext()) {
currentStream = new ByteArrayInputStream(bytesIt.next())
continue
} else {
return readSize > 0 ? readSize : -1
}
} else if (read > 0) {
readSize += read
} else if (read == 0) {
break
}
}
return readSize
}
}

View File

@ -0,0 +1,24 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.util
import groovy.transform.CompileStatic
@CompileStatic
interface CloseableIterator<T> extends Iterator<T>, Closeable {
}

View File

@ -0,0 +1,47 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.util
import groovy.transform.CompileStatic
@CompileStatic
class DataUtils {
// null first, order by column.toString asc
static List<List<Object>> sortByToString(List<List<Object>> originData) {
def comparator = Comparator.<String>naturalOrder()
originData.sort(false, { row1, row2 ->
for (int i = 0; i < row1.size(); ++i) {
Object column1 = row1[i]
Object column2 = row2[i]
if (column1 == column2) {
continue
}
if (column1 == null) {
return -1
} else if (column2 == null) {
return 1
}
int result = comparator.compare(column1.toString(), column2.toString())
if (result != 0) {
return result
}
}
return 0
})
}
}

View File

@ -0,0 +1,33 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.util
import groovy.transform.CompileStatic
@CompileStatic
class FileUtils {
static String getCanonicalPath(String path) {
if (path == null) {
return null
}
if (!new File(path as String).isAbsolute()) {
path = new File(path).getCanonicalPath()
}
return path
}
}

View File

@ -0,0 +1,87 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.util
import com.google.common.collect.ImmutableList
import java.sql.Connection
import java.sql.ResultSet
class JdbcUtils {
static List<List<Object>> executeToList(Connection conn, String sql) {
conn.prepareStatement(sql).withCloseable { stmt ->
boolean hasResultSet = stmt.execute()
if (!hasResultSet) {
return ImmutableList.of(ImmutableList.of(stmt.getUpdateCount()))
} else {
toList(stmt.resultSet)
}
}
}
static List<List<Object>> executorToStringList(Connection conn, String sql) {
conn.prepareStatement(sql).withCloseable { stmt ->
boolean hasResultSet = stmt.execute()
if (!hasResultSet) {
return ImmutableList.of(ImmutableList.of(stmt.getUpdateCount()))
} else {
toStringList(stmt.resultSet)
}
}
}
static List<List<Object>> toList(ResultSet resultSet) {
resultSet.withCloseable {
List<List<Object>> rows = new ArrayList<>()
def columnCount = resultSet.metaData.columnCount
while (resultSet.next()) {
def row = new ArrayList<>()
for (int i = 1; i <= columnCount; ++i) {
row.add(resultSet.getObject(i))
}
rows.add(row)
}
return rows
}
}
static List<List<Object>> toStringList(ResultSet resultSet) {
resultSet.withCloseable {
List<List<Object>> rows = new ArrayList<>()
def columnCount = resultSet.metaData.columnCount
while (resultSet.next()) {
def row = new ArrayList<>()
for (int i = 1; i <= columnCount; ++i) {
// row.add(resultSet.getObject(i))
// row.add(resultSet.getString(i))
try {
row.add(resultSet.getObject(i))
} catch (Throwable t) {
if(resultSet.getBytes(i) != null){
row.add(new String(resultSet.getBytes(i)))
} else {
row.add(resultSet.getObject(i))
}
}
}
rows.add(row)
}
return rows
}
}
}

View File

@ -0,0 +1,275 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.util
import com.google.common.collect.ImmutableList
import groovy.transform.CompileStatic
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVPrinter
import org.apache.commons.csv.CSVRecord
import org.apache.commons.io.LineIterator
@CompileStatic
class OutputUtils {
static toCsvString(List<Object> row) {
StringWriter writer = new StringWriter()
def printer = new CSVPrinter(new PrintWriter(writer), CSVFormat.MYSQL)
for (int i = 0; i < row.size(); ++i) {
printer.print(row.get(i))
}
return writer.toString()
}
static assertEquals(Iterator<List<String>> expect, Iterator<List<Object>> real, String info) {
while (true) {
if (expect.hasNext() && !real.hasNext()) {
def res = "${info}, line not match, real line is empty, but expect is ${expect.next()}"
return res
// throw new IllegalStateException("${info}, line not match, real line is empty, but expect is ${expect.next()}")
}
if (!expect.hasNext() && real.hasNext()) {
def res = "${info}, line not match, expect line is empty, but real is ${toCsvString(real.next())}"
return res
// throw new IllegalStateException("${info}, line not match, expect line is empty, but real is ${toCsvString(real.next())}")
}
if (!expect.hasNext() && !real.hasNext()) {
break
}
def expectCsvString = toCsvString(expect.next() as List<Object>)
def realCsvString = toCsvString(real.next())
if (!expectCsvString.equals(realCsvString)) {
def res = "${info}, line not match.\nExpect line is: ${expectCsvString}\nBut real is : ${realCsvString}"
return res
// throw new IllegalStateException("${info}, line not match.\nExpect line is: ${expectCsvString}\nBut real is : ${realCsvString}")
}
}
}
static CloseableIterator<Iterator<List<String>>> iterator(File file) {
def it = new ReusableIterator<String>(new LineIteratorAdaptor(new LineIterator(new FileReader(file))))
return new OutputBlocksIterator(it)
}
static OutputBlocksWriter writer(File file) {
return new OutputBlocksWriter(file)
}
static class LineIteratorAdaptor implements CloseableIterator<String> {
LineIterator lineIt
LineIteratorAdaptor(LineIterator lineIt) {
this.lineIt = lineIt
}
@Override
void close() throws IOException {
lineIt.close()
}
@Override
boolean hasNext() {
return lineIt.hasNext()
}
@Override
String next() {
return lineIt.next()
}
}
static class OutputBlocksWriter {
private PrintWriter writer
OutputBlocksWriter(File file) {
if (file != null) {
writer = file.newPrintWriter()
writer.println("""-- This file is automatically generated. You should know what you did if you want to edit this""")
}
}
void write(Iterator<List<String>> real, String comment) {
if (writer != null) {
writer.println("-- !${comment} --")
while (real.hasNext()) {
writer.println(toCsvString(real.next() as List<Object>))
}
writer.println()
}
}
void close() {
if (writer != null) {
writer.close()
}
}
}
static class OutputBlocksIterator implements CloseableIterator<Iterator<List<String>>> {
private ReusableIterator<String> lineIt
private CsvParserIterator cache
private boolean cached
OutputBlocksIterator(ReusableIterator<String> lineIt) {
this.lineIt = lineIt
}
@Override
void close() throws IOException {
lineIt.close()
}
@Override
boolean hasNext() {
if (!cached) {
if (cache != null) {
while (cache.hasNext()) {
cache.next()
}
}
if (!lineIt.hasNext()) {
return false
}
// find next comment block
while (true) {
String blockComment = lineIt.next() // skip block comment, e.g. -- !qt_sql_1 --
if (blockComment.startsWith("-- !") && blockComment.endsWith(" --")) {
break
}
if (!lineIt.hasNext()) {
return false
}
}
cache = new CsvParserIterator(new SkipLastEmptyLineIterator(new OutputBlockIterator(lineIt)))
cached = true
return true
} else {
return true
}
}
@Override
Iterator<List<String>> next() {
if (hasNext()) {
cached = false
return cache
}
throw new NoSuchElementException()
}
}
static class CsvParserIterator implements Iterator<List<String>> {
private Iterator<String> it
CsvParserIterator(Iterator<String> it) {
this.it = it
}
@Override
boolean hasNext() {
return it.hasNext()
}
@Override
List<String> next() {
String line = it.next()
if (line.size() == 0) {
return ImmutableList.of(line)
}
CSVRecord record = CSVFormat.MYSQL.parse(new StringReader(line)).first()
List<String> row = new ArrayList(record.size())
for (int i = 0; i < record.size(); ++i) {
row.add(record.get(i))
}
return row
}
}
static class SkipLastEmptyLineIterator implements Iterator<String> {
private Iterator<String> it
private String cache
private boolean cached
SkipLastEmptyLineIterator(Iterator<String> it) {
this.it = it
}
@Override
boolean hasNext() {
if (!cached) {
if (!it.hasNext()) {
return false
}
String next = it.next()
if (next.length() == 0 && !it.hasNext()) {
return false
}
cache = next
cached = true
return true
} else {
return true
}
}
@Override
String next() {
if (hasNext()) {
cached = false
return cache
}
throw new NoSuchElementException()
}
}
static class OutputBlockIterator implements Iterator<String> {
private ReusableIterator<String> it
OutputBlockIterator(ReusableIterator<String> it) {
this.it = it
}
@Override
boolean hasNext() {
while (true) {
if (!it.hasNext()) {
return false
}
// predict next line
String line = it.preRead()
if (line.startsWith("-- !") && line.endsWith(" --")) {
return false
} else if (line.startsWith("-- ")) {
it.next()
continue
}
return true
}
return false
}
@Override
String next() {
if (hasNext()) {
return it.next()
}
throw new NoSuchElementException()
}
}
}

View File

@ -0,0 +1,38 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.util
import groovy.transform.CompileStatic
@CompileStatic
class Recorder {
public final List<SuiteInfo> successList = new ArrayList<>()
public final List<SuiteInfo> failureList = new ArrayList<>()
void onSuccess(SuiteInfo suiteInfo) {
successList.add(suiteInfo)
}
void onFailure(SuiteInfo suiteInfo) {
failureList.add(suiteInfo)
}
void reportDiffResult(List res) {
// TODO
}
}

View File

@ -0,0 +1,64 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.util
import groovy.transform.CompileStatic
@CompileStatic
class ReusableIterator<T> implements CloseableIterator<T> {
private CloseableIterator<T> it
private T next
private boolean cached
ReusableIterator(CloseableIterator<T> it) {
this.it = it
}
@Override
void close() throws IOException {
it.close()
}
@Override
boolean hasNext() {
if (!cached) {
if (it.hasNext()) {
next = it.next()
cached = true
return true
} else {
return false
}
} else {
return true
}
}
T preRead() {
return next
}
@Override
T next() {
if (hasNext()) {
cached = false
return next
}
throw new NoSuchElementException()
}
}

View File

@ -0,0 +1,33 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.regression.util
import groovy.transform.CompileStatic
@CompileStatic
class SuiteInfo {
File file
String group
String suiteName
SuiteInfo(File file, String group, String suiteName) {
this.file = file
this.group = group
this.suiteName = suiteName
}
}

View File

@ -0,0 +1 @@
qt_select1 'select 100, "test", date("2021-01-02")'

View File

@ -0,0 +1,31 @@
explain {
sql("select 100")
// contains("OUTPUT EXPRS:<slot 0> 100\n") && contains("PARTITION: UNPARTITIONED\n")
contains "OUTPUT EXPRS:<slot 0> 100\n"
contains "PARTITION: UNPARTITIONED\n"
}
explain {
sql("select 100")
// contains(" 100\n") && !contains("abcdefg") && !("1234567")
contains " 100\n"
notContains "abcdefg"
notContains "1234567"
}
explain {
sql("select 100")
// simple callback
check { explainStr -> explainStr.contains("abcdefg") || explainStr.contains(" 100\n") }
}
explain {
sql("a b c d e")
// callback with exception and time
check { explainStr, exception, startTime, endTime ->
// assertXxx() will invoke junit5's Assertions.assertXxx() dynamically
assertTrue(exception != null)
}
}

View File

@ -0,0 +1,26 @@
/**
* qt_xxx sql equals to quickTest(xxx, sql) witch xxx is tag.
* the result will be compare to the relate file: ${DORIS_HOME}/regression_test/data/qt_action.out.
*
* if you want to generate .out tsv file for real execute result. you can run with -genOut or -forceGenOut option.
* e.g
* ${DORIS_HOME}/run-regression-test.sh --run qt_action -genOut
* ${DORIS_HOME}/run-regression-test.sh --run qt_action -forceGenOut
*/
qt_select "select 1, 'beijing' union all select 2, 'shanghai'"
qt_select2 "select 2"
// order result by string dict then compare to .out file.
// order_qt_xxx sql equals to quickTest(xxx, sql, true).
order_qt_union_all """
select 2
union all
select 1
union all
select null
union all
select 15
union all
select 3
"""

View File

@ -0,0 +1,81 @@
// execute sql and ignore result
sql "show databases"
// execute sql and get result, outer List denote rows, inner List denote columns in a single row
List<List<Object>> tables = sql "show tables"
// assertXxx() will invoke junit5's Assertions.assertXxx() dynamically
assertTrue(tables.size() >= 0) // test rowCount >= 0
// syntax error
try {
sql "a b c d e"
throw new IllegalStateException("Should be syntax error")
} catch (java.sql.SQLException t) {
assertTrue(true)
}
def testTable = "test_sql_action1"
try {
sql "DROP TABLE IF EXISTS ${testTable}"
// multi-line sql
def result1 = sql """
CREATE TABLE IF NOT EXISTS ${testTable} (
id int
)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
)
"""
// DDL/DML return 1 row and 1 column, the only value is update row count
assertTrue(result1.size() == 1)
assertTrue(result1[0].size() == 1)
assertTrue(result1[0][0] == 0, "Create table should update 0 rows")
def result2 = sql "INSERT INTO test_sql_action1 values(1), (2), (3)"
assertTrue(result2.size() == 1)
assertTrue(result2[0].size() == 1)
assertTrue(result2[0][0] == 3, "Insert should update 3 rows")
} finally {
/**
* try_xxx(args) means:
*
* try {
* return xxx(args)
* } catch (Throwable t) {
* // do nothing
* }
*/
try_sql("DROP TABLE IF EXISTS ${testTable}")
// you can see the error sql will not throw exception and return
try {
def errorSqlResult = try_sql("a b c d e f g")
assertTrue(errorSqlResult == null)
} catch (Throwable t) {
assertTrue(false, "Never catch exception")
}
}
// order_sql(sqlStr) equals to sql(sqlStr, isOrder=true)
// sort result by string dict
def list = order_sql """
select 2
union all
select 1
union all
select null
union all
select 15
union all
select 3
"""
assertEquals(null, list[0][0])
assertEquals(1, list[1][0])
assertEquals(15, list[2][0])
assertEquals(2, list[3][0])
assertEquals(3, list[4][0])

View File

@ -0,0 +1,59 @@
def tableName = "test_streamload_action1"
sql """
CREATE TABLE IF NOT EXISTS ${tableName} (
id int,
name varchar(255)
)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
)
"""
streamLoad {
// you can skip declare db, because a default db already specify in ${DORIS_HOME}/conf/regression-conf.groovy
// db 'regression_test'
table tableName
// default label is UUID:
// set 'label' UUID.randomUUID().toString()
// default column_separator is specify in doris fe config, usually is '\t'.
// this line change to ','
set 'column_separator', ','
// relate to ${DORIS_HOME}/regression-test/data/demo/streamload_input.csv.
// also, you can stream load a http stream, e.g. http://xxx/some.csv
file 'streamload_input.csv'
time 10000 // limit inflight 10s
// stream load action will check result, include Success status, and NumberTotalRows == NumberLoadedRows
}
// stream load 100 rows
def rowCount = 100
def rowIt = java.util.stream.LongStream.range(0, rowCount) // [0, rowCount)
.mapToObj({i -> [i, "a_" + i]}) // change Long to List<Long, String>
.iterator()
streamLoad {
table tableName
// also, you can upload a memory iterator
inputIterator rowIt
// if declared a check callback, the default check condition will ignore.
// So you must check all condition
check { result, exception, startTime, endTime ->
if (exception != null) {
throw exception
}
log.info("Stream load result: ${result}".toString())
def json = parseJson(result)
assertEquals("success", json.Status.toLowerCase())
assertEquals(json.NumberTotalRows, json.NumberLoadedRows)
assertTrue(json.NumberLoadedRows > 0 && json.LoadBytes > 0)
}
}

View File

@ -0,0 +1,65 @@
test {
sql "abcdefg"
// check exception message contains
exception "errCode = 2, detailMessage = Syntax error"
}
test {
sql """
select *
from (
select 1 id
union all
select 2
) a
order by id"""
// multi check condition
// check return 2 rows
rowNum 2
// execute time must <= 5000 millisecond
time 5000
// check result, must be 2 rows and 1 column, the first row is 1, second is 2
result(
[[1], [2]]
)
}
test {
sql "a b c d e f g"
// other check will not work because already declared a check callback
exception "aaaaaaaaa"
// callback
check { result, exception, startTime, endTime ->
// assertXxx() will invoke junit5's Assertions.assertXxx() dynamically
assertTrue(exception != null)
}
}
test {
sql """
select 2
union all
select 1
union all
select null
union all
select 15
union all
select 3
"""
check { result, ex, startTime, endTime ->
// same as order_sql(sqlStr)
result = sortRows(result)
assertEquals(null, result[0][0])
assertEquals(1, result[1][0])
assertEquals(15, result[2][0])
assertEquals(2, result[3][0])
assertEquals(3, result[4][0])
}
}

View File

@ -0,0 +1,27 @@
def tableName = "test_streamload_performance1"
try {
sql """
CREATE TABLE IF NOT EXISTS ${tableName} (
id int,
name varchar(255)
)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
)
"""
def rowCount = 10000
def rowIt = java.util.stream.LongStream.range(0, rowCount)
.mapToObj({i -> [i, "a_" + i]})
.iterator()
streamLoad {
table tableName
time 5000
inputIterator rowIt
}
} finally {
try_sql "DROP TABLE IF EXISTS ${tableName}"
}

176
run-regression-test.sh Executable file
View File

@ -0,0 +1,176 @@
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#####################################################################
# This script is used to run regression test of Doris Backend
# Usage: $0 <shell_options> <framework_options>
# Optional shell_options:
# --clean clean output of regression test
# --run run regression test. build framework if necessary
#
# Optional framework_options
# -h print all other_options
# -s xxx suite name
# -g xxx group name
# -c xxx jdbc url
# -u xxx jdbc user
# -genOut generate .out file
# -forceGenOut delete and generate .out file
#
# log to ${DORIS_HOME}/output/regression/log
#####################################################################
set -eo pipefail
#set -x
ROOT=`dirname "$0"`
ROOT=`cd "$ROOT"; pwd`
DORIS_HOME=${ROOT}
# Check args
usage() {
echo "
Usage: $0 <shell_options> <framework_options>
Optional shell_options:
--clean clean output of regression test framework
--run run regression test. build framework if necessary
Optional framework_options:
-s run a specified suite
-g run a specified group
-h **print all framework options usage**
-genOut generate .out file if not exist
-forceGenOut delete and generate .out file if not exist
Eg.
$0 build regression test framework and run all suite which in default group
$0 --run test_select run a suite which named as test_select
$0 --run 'test*' run all suite which named start with 'test', note that you must quota with ''
$0 --run -s test_select run a suite which named as test_select
$0 --run test_select -genOut generate output file for test_select if not exist
$0 --run -g default run all suite in the group which named as default
$0 --clean clean output of regression test framework
$0 --clean --run test_select clean output and build regression test framework and run a suite which named as test_select
$0 --run -h print framework options
Log path: \${DORIS_HOME}/output/regression-test/log
Default config file: \${DORIS_HOME}/regression-test/conf/regression-conf.groovy
"
exit 1
}
CLEAN=
WRONG_CMD=
RUN=
if [ $# == 0 ] ; then
#default
CLEAN=0
WRONG_CMD=0
RUN=1
else
CLEAN=0
RUN=0
WRONG_CMD=0
while true; do
case "$1" in
--clean) CLEAN=1 ; shift ;;
--run) RUN=1 ; shift ;;
*)
if [ ${RUN} -eq 0 ] && [ ${CLEAN} -eq 0 ]; then
WRONG_CMD=1
fi
break ;;
esac
done
fi
if [ ${WRONG_CMD} -eq 1 ]; then
usage
exit 1
fi
# set maven
MVN_CMD=mvn
if [[ ! -z ${CUSTOM_MVN} ]]; then
MVN_CMD=${CUSTOM_MVN}
fi
if ! ${MVN_CMD} --version; then
echo "Error: mvn is not found"
exit 1
fi
export MVN_CMD
CONF_DIR=${DORIS_HOME}/regression-test/conf
CONFIG_FILE=${CONF_DIR}/regression-conf.groovy
LOG_CONFIG_FILE=${CONF_DIR}/logback.xml
FRAMEWORK_SOURCE_DIR=${DORIS_HOME}/regression-test/framework
REGRESSION_TEST_BUILD_DIR=${FRAMEWORK_SOURCE_DIR}/target
OUTPUT_DIR=${DORIS_HOME}/output/regression-test
LOG_OUTPUT_FILE=${OUTPUT_DIR}/log
RUN_JAR=${OUTPUT_DIR}/lib/regression-test-*.jar
if [ ${CLEAN} -eq 1 ]; then
rm -rf ${REGRESSION_TEST_BUILD_DIR}
rm -rf ${OUTPUT_DIR}
fi
if [ ${RUN} -ne 1 ]; then
echo "Finished"
exit 0
fi
if [ ! -f ${RUN_JAR} ]; then
echo "===== Build Regression Test Framework ====="
cd ${DORIS_HOME}/regression-test/framework
${MVN_CMD} package
cd ${DORIS_HOME}
mkdir -p ${OUTPUT_DIR}/{lib,log}
cp -r ${REGRESSION_TEST_BUILD_DIR}/regression-test-*.jar ${OUTPUT_DIR}/lib
fi
# check java home
if [[ -z ${JAVA_HOME} ]]; then
echo "Error: JAVA_HOME is not set"
exit 1
fi
# check java version
export JAVA=${JAVA_HOME}/bin/java
REGRESSION_OPTIONS_PREFIX=
# contains framework options and not start with -
# it should be suite name
if [ $# -ne 0 ] && [[ "$1" =~ ^[^-].* ]]; then
# specify suiteName
REGRESSION_OPTIONS_PREFIX='-s'
fi
echo "===== Run Regression Test ====="
$JAVA -DDORIS_HOME=$DORIS_HOME \
-DLOG_PATH=$LOG_OUTPUT_FILE \
-Dlogback.configurationFile=${LOG_CONFIG_FILE} \
-jar ${RUN_JAR} \
-cf ${CONFIG_FILE} \
${REGRESSION_OPTIONS_PREFIX} "$@"