This is an automated email from the ASF dual-hosted git repository.

huajianlan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 9db2a96  [test] support a lot of actions (#8632)
9db2a96 is described below

commit 9db2a96af19477c1269a86dc26ab2b647815cd3b
Author: 924060929 <924060...@qq.com>
AuthorDate: Thu Mar 24 20:22:24 2022 +0800

    [test] support a lot of actions (#8632)
    
    Support a lot of actions for regression testing framework.
    e.g. thread, lazyCheck, onSuccess, connect, selectUnionAll, timer
    
    Demo exists in ${DORIS_HOME}/regression-test/suites/demo
---
 docs/zh-CN/developer-guide/regression-testing.md   |   9 +-
 regression-test/data/demo/qt_action.out            |   2 +-
 .../{qt_action.out => select_union_all_action.out} |  17 +-
 .../data/demo/{qt_action.out => thread_action.out} |  16 +-
 .../org/apache/doris/regression/Config.groovy      |  13 +-
 .../apache/doris/regression/ConfigOptions.groovy   |  10 ++
 .../apache/doris/regression/RegressionTest.groovy  |  42 +++--
 .../doris/regression/action/ExplainAction.groovy   |   6 +-
 .../regression/action/StreamLoadAction.groovy      |   4 +-
 .../doris/regression/action/TestAction.groovy      |   6 +-
 .../org/apache/doris/regression/suite/Suite.groovy | 198 +++++++++++++++++----
 .../doris/regression/suite/SuiteContext.groovy     |  67 +++++--
 .../doris/regression/util/OutputUtils.groovy       |  73 ++++++--
 .../apache/doris/regression/util/Recorder.groovy   |   4 +-
 .../framework/src/main/groovy/suite.gdsl           |  83 +++++++++
 regression-test/suites/demo/connect_action.groovy  |  16 ++
 regression-test/suites/demo/event_action.groovy    |  40 +++++
 .../suites/demo/lazyCheck_action.groovy            |  33 ++++
 .../suites/demo/select_union_all_action.groovy     |  19 ++
 regression-test/suites/demo/sql_action.groovy      |   2 +
 .../suites/demo/streamLoad_action.groovy           |   4 +-
 regression-test/suites/demo/thread_action.groovy   |  48 +++++
 regression-test/suites/demo/timer_action.groovy    |   7 +
 23 files changed, 600 insertions(+), 119 deletions(-)

diff --git a/docs/zh-CN/developer-guide/regression-testing.md 
b/docs/zh-CN/developer-guide/regression-testing.md
index eff0cda..a962502 100644
--- a/docs/zh-CN/developer-guide/regression-testing.md
+++ b/docs/zh-CN/developer-guide/regression-testing.md
@@ -36,7 +36,7 @@ under the License.
 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文件来校验输出是否一致
+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>` 测试若干用例,更多例子见"启动脚本例子"章节
 
 ## 目录结构
@@ -114,7 +114,7 @@ customConf1 = "test_custom_conf_value"
 ## 编写用例的步骤
 1. 进入`${DORIS_HOME}/regression-test`目录
 2. 根据测试的目的来选择用例的目录,正确性测试存在`suites/correctness`,而性能测试存在`suites/performance`
-3. 新建一个groovy用例文件,增加若干`Action`用于测试,Action讲在后续章节具体说明
+3. 新建一个groovy用例文件,增加若干`Action`用于测试,Action将在后续章节具体说明
 
 ## Action
 Action是一个测试框架默认提供的测试行为,使用DSL来定义。
@@ -178,6 +178,7 @@ try {
      *    return xxx(args)
      * } catch (Throwable t) {
      *     // do nothing
+     *     return null
      * }
      */
     try_sql("DROP TABLE IF EXISTS ${testTable}")
@@ -447,6 +448,10 @@ streamLoad {
 }
 ```
 
+### 其他Action
+thread, lazyCheck, events, connect, selectUnionAll
+具体可以在这个目录找到例子: `${DORIS_HOME}/regression-test/suites/demo`
+
 ## 启动脚本例子
 ```shell
 # 查看脚本参数说明
diff --git a/regression-test/data/demo/qt_action.out 
b/regression-test/data/demo/qt_action.out
index ea3e7b4..c34a713 100644
--- a/regression-test/data/demo/qt_action.out
+++ b/regression-test/data/demo/qt_action.out
@@ -6,7 +6,7 @@
 -- !select2 --
 2
 
--- !union --
+-- !union_all --
 \N
 1
 15
diff --git a/regression-test/data/demo/qt_action.out 
b/regression-test/data/demo/select_union_all_action.out
similarity index 57%
copy from regression-test/data/demo/qt_action.out
copy to regression-test/data/demo/select_union_all_action.out
index ea3e7b4..7cdd5df 100644
--- a/regression-test/data/demo/qt_action.out
+++ b/regression-test/data/demo/select_union_all_action.out
@@ -1,15 +1,10 @@
 -- 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
+-- !select_union_all1 --
 1
-15
-2
+10
 3
 
+-- !select_union_all2 --
+0      abc
+1      123
+2      \N
diff --git a/regression-test/data/demo/qt_action.out 
b/regression-test/data/demo/thread_action.out
similarity index 55%
copy from regression-test/data/demo/qt_action.out
copy to regression-test/data/demo/thread_action.out
index ea3e7b4..4f14b39 100644
--- a/regression-test/data/demo/qt_action.out
+++ b/regression-test/data/demo/thread_action.out
@@ -1,15 +1,7 @@
 -- This file is automatically generated. You should know what you did if you 
want to edit this
--- !select --
-1      beijing
-2      shanghai
+-- !diffrent_tag1 --
+100
 
--- !select2 --
-2
-
--- !union --
-\N
-1
-15
-2
-3
+-- !diffrent_tag2 --
+100
 
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
index 3a6bcda..b96b0e2 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
@@ -57,6 +57,7 @@ class Config {
     public Set<String> groups = new HashSet<>()
     public InetSocketAddress feHttpInetSocketAddress
     public Integer parallel
+    public Integer actionParallel
     public Integer times
     public boolean withOutLoadData
 
@@ -124,6 +125,7 @@ class Config {
         config.generateOutputFile = cmd.hasOption(genOutOpt)
         config.forceGenerateOutputFile = cmd.hasOption(forceGenOutOpt)
         config.parallel = Integer.parseInt(cmd.getOptionValue(parallelOpt, 
"1"))
+        config.actionParallel = 
Integer.parseInt(cmd.getOptionValue(actionParallelOpt, "10"))
         config.times = Integer.parseInt(cmd.getOptionValue(timesOpt, "1"))
         config.randomOrder = cmd.hasOption(randomOrderOpt)
         config.withOutLoadData = cmd.hasOption(withOutLoadDataOpt)
@@ -226,6 +228,11 @@ class Config {
             log.info("Set parallel to 1 because not specify.".toString())
         }
 
+        if (config.actionParallel == null) {
+            config.actionParallel = 10
+            log.info("Set actionParallel to 10 because not 
specify.".toString())
+        }
+
         if (config.randomOrder == null) {
             config.randomOrder = false
             log.info("set randomOrder to false because not 
specify.".toString())
@@ -263,15 +270,15 @@ class Config {
         String urlWithoutSchema = jdbcUrl.substring(jdbcUrl.indexOf("://") + 3)
         if (urlWithoutSchema.indexOf("/") >= 0) {
             if (jdbcUrl.contains("?")) {
-                // e.g: jdbc:mysql://locahost:8080/?a=b
+                // e.g: jdbc:mysql://localhost:8080/?a=b
                 urlWithDb = jdbcUrl.substring(0, jdbcUrl.lastIndexOf("/"))
                 urlWithDb += ("/" + defaultDb) + 
jdbcUrl.substring(jdbcUrl.lastIndexOf("?"))
             } else {
-                // e.g: jdbc:mysql://locahost:8080/
+                // e.g: jdbc:mysql://localhost:8080/
                 urlWithDb += defaultDb
             }
         } else {
-            // e.g: jdbc:mysql://locahost:8080
+            // e.g: jdbc:mysql://localhost:8080
             urlWithDb += ("/" + defaultDb)
         }
         this.jdbcUrl = urlWithDb
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/ConfigOptions.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/ConfigOptions.groovy
index 41d2d18..4e89c19 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/ConfigOptions.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/ConfigOptions.groovy
@@ -43,6 +43,7 @@ class ConfigOptions {
     static Option genOutOpt
     static Option forceGenOutOpt
     static Option parallelOpt
+    static Option actionParallelOpt
     static Option randomOrderOpt
     static Option timesOpt
     static Option withOutLoadDataOpt
@@ -180,6 +181,14 @@ class ConfigOptions {
                 .longOpt("parallel")
                 .desc("the num of threads running test")
                 .build()
+        actionParallelOpt = Option.builder("actionParallel")
+                .argName("parallel")
+                .required(false)
+                .hasArg(true)
+                .type(String.class)
+                .longOpt("actionParallel")
+                .desc("the num of threads running for thread action")
+                .build()
         randomOrderOpt = Option.builder("randomOrder")
                 .required(false)
                 .hasArg(false)
@@ -219,6 +228,7 @@ class ConfigOptions {
                 .addOption(confFileOpt)
                 .addOption(forceGenOutOpt)
                 .addOption(parallelOpt)
+                .addOption(actionParallelOpt)
                 .addOption(randomOrderOpt)
                 .addOption(withOutLoadDataOpt)
 
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/RegressionTest.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/RegressionTest.groovy
index 2584e4b..627a6b7 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/RegressionTest.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/RegressionTest.groovy
@@ -38,7 +38,8 @@ class RegressionTest {
     static ClassLoader classloader
     static CompilerConfiguration compileConfig
     static GroovyShell shell
-    static ExecutorService executorService;
+    static ExecutorService executorService
+    static ExecutorService actionExecutorService
 
     static void main(String[] args) {
         CommandLine cmd = ConfigOptions.initCommands(args)
@@ -53,6 +54,7 @@ class RegressionTest {
             Recorder recorder = runSuites(config)
             printResult(config, recorder)
         }
+        actionExecutorService.shutdown()
         executorService.shutdown()
     }
 
@@ -63,7 +65,8 @@ class RegressionTest {
         compileConfig.setScriptBaseClass((Suite as Class).name)
         shell = new GroovyShell(classloader, new Binding(), compileConfig)
         log.info("starting ${config.parallel} threads")
-        executorService = Executors.newFixedThreadPool(config.parallel);
+        executorService = Executors.newFixedThreadPool(config.parallel)
+        actionExecutorService = 
Executors.newFixedThreadPool(config.actionParallel)
     }
 
     static List<File> findSuiteFiles(String root) {
@@ -117,23 +120,34 @@ class RegressionTest {
         return groups;
     }
 
-    static Integer runSuite(Config config, SuiteFile sf, Recorder recorder) {
+    static Integer runSuite(Config config, SuiteFile sf, ExecutorService 
executorService, Recorder recorder) {
         File file = sf.file
         String suiteName = sf.suiteName
         String group = sf.group
         def suiteConn = config.getConnection()
-        new SuiteContext(file, suiteConn, config, recorder).withCloseable { 
context ->
+        new SuiteContext(file, suiteConn, executorService, config, 
recorder).withCloseable { context ->
+            Suite suite = null
             try {
                 log.info("Run ${suiteName} in $file".toString())
-                Suite suite = shell.parse(file) as Suite
+                suite = shell.parse(file) as Suite
                 suite.init(suiteName, group, context)
                 suite.run()
+                suite.doLazyCheck()
+                suite.successCallbacks.each { it() }
                 recorder.onSuccess(new SuiteInfo(file, group, suiteName))
                 log.info("Run ${suiteName} in ${file.absolutePath} 
succeed".toString())
             } catch (Throwable t) {
+                if (suite != null) {
+                    suite.failCallbacks.each { it() }
+                }
                 recorder.onFailure(new SuiteInfo(file, group, suiteName))
                 log.error("Run ${suiteName} in ${file.absolutePath} 
failed".toString(), t)
+            } finally {
+                if (suite != null) {
+                    suite.finishCallbacks.each { it() }
+                }
             }
+            shell.resetLoadedClasses()
         }
 
         return 0
@@ -146,7 +160,7 @@ class RegressionTest {
             String group = parseGroup(config, file)
             return new SuiteFile(file, suiteName, group)
         }).filter({ sf ->
-            { suiteNameMatch(sf.suiteName) && canRun(config, sf.suiteName, 
sf.group) }
+            suiteNameMatch(sf.suiteName) && canRun(config, sf.suiteName, 
sf.group)
         }).collect(Collectors.toList())
 
         if (config.randomOrder) {
@@ -157,11 +171,9 @@ class RegressionTest {
         def futures = new ArrayList<Future>()
         runScripts.eachWithIndex { sf, i ->
             log.info("[${i + 1}/${totalFile}] Run ${sf.suiteName} in 
${sf.file}".toString())
-            Future future = executorService.submit(
-                             ()-> {
-                                runSuite(config, sf, recorder)
-                             }
-                             )
+            Future future = executorService.submit {
+                runSuite(config, sf, actionExecutorService, recorder)
+            }
             futures.add(future)
         }
 
@@ -178,9 +190,9 @@ class RegressionTest {
     static Recorder runSuites(Config config) {
         def recorder = new Recorder()
         if (!config.withOutLoadData) {
-            runSuites(config, recorder, suiteName -> { suiteName == "load" })
+            runSuites(config, recorder, {suiteName -> suiteName == "load" })
         }
-        runSuites(config, recorder, suiteName -> { suiteName != "load" })
+        runSuites(config, recorder, {suiteName -> suiteName != "load" })
 
         return recorder
     }
@@ -189,9 +201,7 @@ class RegressionTest {
         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)
-                    }
+                suiteWildcard -> Wildcard.match(suiteName, suiteWildcard)
                 }))) {
             if (config.groups == null || config.groups.isEmpty()
                     || !config.groups.intersect(suiteGroups).isEmpty()) {
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/ExplainAction.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/ExplainAction.groovy
index 9617b79..01eddff 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/ExplainAction.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/ExplainAction.groovy
@@ -17,6 +17,8 @@
 
 package org.apache.doris.regression.action
 
+import groovy.transform.stc.ClosureParams
+import groovy.transform.stc.FromString
 import org.apache.doris.regression.suite.SuiteContext
 import org.apache.doris.regression.util.JdbcUtils
 import groovy.util.logging.Slf4j
@@ -51,7 +53,7 @@ class ExplainAction implements SuiteAction {
         notContainsStrings.add(subString)
     }
 
-    void check(Closure<Boolean> checkFunction) {
+    void check(@ClosureParams(value = FromString, options = ["String", 
"String,Throwable,Long,Long"]) Closure<Boolean> checkFunction) {
         this.checkFunction = checkFunction
     }
 
@@ -119,7 +121,7 @@ class ExplainAction implements SuiteAction {
         long startTime = System.currentTimeMillis()
         String explainString = null
         try {
-            explainString = JdbcUtils.executeToList(context.conn, 
explainSql).stream()
+            explainString = JdbcUtils.executeToList(context.getConnection(), 
explainSql).stream()
                     .map({row -> row.get(0).toString()})
                     .collect(Collectors.joining("\n"))
             return new ActionResult(explainString, null, startTime, 
System.currentTimeMillis())
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/StreamLoadAction.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/StreamLoadAction.groovy
index 54953c9..2b4348d 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/StreamLoadAction.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/StreamLoadAction.groovy
@@ -18,6 +18,8 @@
 package org.apache.doris.regression.action
 
 import com.google.common.collect.Iterators
+import groovy.transform.stc.ClosureParams
+import groovy.transform.stc.FromString
 import org.apache.doris.regression.suite.SuiteContext
 import org.apache.doris.regression.util.BytesInputStream
 import org.apache.doris.regression.util.OutputUtils
@@ -115,7 +117,7 @@ class StreamLoadAction implements SuiteAction {
         this.time = time.call()
     }
 
-    void check(Closure check) {
+    void check(@ClosureParams(value = FromString, options = 
["String,Throwable,Long,Long"]) Closure check) {
         this.check = check
     }
 
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/TestAction.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/TestAction.groovy
index 7b0e3cd..f9f629b 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/TestAction.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/TestAction.groovy
@@ -17,6 +17,8 @@
 
 package org.apache.doris.regression.action
 
+import groovy.transform.stc.ClosureParams
+import groovy.transform.stc.FromString
 import groovy.util.logging.Slf4j
 import java.sql.Connection
 
@@ -41,7 +43,7 @@ class TestAction implements SuiteAction {
     @Override
     void run() {
         try{
-            def result = doRun(context.conn)
+            def result = doRun(context.getConnection())
             if (check != null) {
                 check.call(result.result, result.exception, result.startTime, 
result.endTime)
             } else {
@@ -131,7 +133,7 @@ class TestAction implements SuiteAction {
         this.exception = exceptionMsgSupplier.call()
     }
 
-    void check(Closure check) {
+    void check(@ClosureParams(value = FromString, options = 
["String,Throwable,Long,Long"]) Closure check) {
         this.check = check
     }
 
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
index 371c895..8a36eb5 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
@@ -17,9 +17,10 @@
 
 package org.apache.doris.regression.suite
 
+import com.google.common.util.concurrent.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.common.util.concurrent.MoreExecutors
 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
@@ -29,14 +30,28 @@ 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 org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.util.concurrent.Callable
+import java.util.concurrent.Future
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.stream.Collectors
+import java.util.stream.LongStream
 
 import static org.apache.doris.regression.util.DataUtils.sortByToString
 
-@Slf4j
 abstract class Suite extends Script implements GroovyInterceptable {
     SuiteContext context
     String name
     String group
+    final Logger logger = LoggerFactory.getLogger(getClass())
+
+    final List<Closure> successCallbacks = new Vector<>()
+    final List<Closure> failCallbacks = new Vector<>()
+    final List<Closure> finishCallbacks = new Vector<>()
+    final List<Throwable> lazyCheckExceptions = new Vector<>()
+    final List<Future> lazyCheckFutures = new Vector<>()
 
     void init(String name, String group, SuiteContext context) {
         this.name = name
@@ -59,6 +74,26 @@ abstract class Suite extends Script implements 
GroovyInterceptable {
         return p
     }
 
+    void onSuccess(Closure callback) {
+        successCallbacks.add(callback)
+    }
+
+    void onFail(Closure callback) {
+        failCallbacks.add(callback)
+    }
+
+    void onFinish(Closure callback) {
+        finishCallbacks.add(callback)
+    }
+
+    LongStream range(long startInclusive, long endExclusive) {
+        return LongStream.range(startInclusive, endExclusive)
+    }
+
+    LongStream rangeClosed(long startInclusive, long endInclusive) {
+        return LongStream.rangeClosed(startInclusive, endInclusive)
+    }
+
     String toCsv(List<Object> rows) {
         StringBuilder sb = new StringBuilder()
         for (int i = 0; i < rows.size(); ++i) {
@@ -76,16 +111,79 @@ abstract class Suite extends Script implements 
GroovyInterceptable {
         return jsonSlurper.parseText(str)
     }
 
-    Object sql(String sqlStr, boolean isOrder = false) {
-        log.info("Execute sql: ${sqlStr}".toString())
-        def result = JdbcUtils.executeToList(context.conn, sqlStr)
+    public <T> T lazyCheck(Closure<T> closure) {
+        try {
+            T result = closure.call()
+            if (result instanceof Future) {
+                lazyCheckFutures.add(result)
+            }
+            return result
+        } catch (Throwable t) {
+            lazyCheckExceptions.add(t)
+            return null
+        }
+    }
+
+    void doLazyCheck() {
+        if (!lazyCheckExceptions.isEmpty()) {
+            throw lazyCheckExceptions.get(0)
+        }
+        lazyCheckFutures.forEach { it.get() }
+    }
+
+    public <T> Tuple2<T, Long> timer(Closure<T> actionSupplier) {
+        long startTime = System.currentTimeMillis()
+        T result = actionSupplier.call()
+        long endTime = System.currentTimeMillis()
+        return [result, endTime - startTime]
+    }
+
+    public <T> ListenableFuture<T> thread(String threadName = null, Closure<T> 
actionSupplier) {
+        return 
MoreExecutors.listeningDecorator(context.executorService).submit((Callable<T>) {
+            def originThreadName = Thread.currentThread().name
+            try {
+                Thread.currentThread().setName(threadName == null ? 
originThreadName : threadName)
+                return actionSupplier.call()
+            } finally {
+                try {
+                    context.closeThreadLocal()
+                } catch (Throwable t) {
+                    logger.warn("Close thread local context failed", t)
+                }
+                Thread.currentThread().setName(originThreadName)
+            }
+        })
+    }
+
+    public <T> ListenableFuture<T> lazyCheckThread(String threadName = null, 
Closure<T> actionSupplier) {
+        return lazyCheck {
+            thread(threadName, actionSupplier)
+        }
+    }
+
+    public <T> ListenableFuture<T> combineFutures(ListenableFuture<T> ... 
futures) {
+        return Futures.allAsList(futures)
+    }
+
+    public <T> ListenableFuture<List<T>> combineFutures(Iterable<? extends 
ListenableFuture<? extends T>> futures) {
+        return Futures.allAsList(futures)
+    }
+
+    public <T> T connect(String user = context.config.jdbcUser, String 
password = context.config.jdbcPassword,
+                         String url = context.config.jdbcUrl, Closure<T> 
actionSupplier) {
+        return context.connect(user, password, url, actionSupplier)
+    }
+
+    List<List<Object>> sql(String sqlStr, boolean isOrder = false) {
+        logger.info("Execute sql: ${sqlStr}".toString())
+        def result = JdbcUtils.executeToList(context.getConnection(), sqlStr)
         if (isOrder) {
             result = DataUtils.sortByToString(result)
         }
         return result
     }
 
-    Object order_sql(String sqlStr) {
+    List<List<Object>> order_sql(String sqlStr) {
         return sql(sqlStr,  true)
     }
 
@@ -96,6 +194,46 @@ abstract class Suite extends Script implements 
GroovyInterceptable {
         return DataUtils.sortByToString(result)
     }
 
+    String selectUnionAll(List list) {
+        def toSelectString = { Object value ->
+            if (value == null) {
+                return "null"
+            } else if (value instanceof Number) {
+                return value.toString()
+            } else {
+                return "'${value.toString()}'".toString()
+            }
+        }
+        AtomicBoolean isFirst = new AtomicBoolean(true)
+        String sql = list.stream()
+            .map({ row ->
+                StringBuilder sb = new StringBuilder("SELECT ")
+                if (row instanceof List) {
+                    if (isFirst.get()) {
+                        String columns = row.withIndex().collect({ column, 
index ->
+                            "${toSelectString(column)} AS c${index + 1}"
+                        }).join(", ")
+                        sb.append(columns)
+                        isFirst.set(false)
+                    } else {
+                        String columns = row.collect({ column ->
+                            "${toSelectString(column)}"
+                        }).join(", ")
+                        sb.append(columns)
+                    }
+                } else {
+                    if (isFirst.get()) {
+                        sb.append(toSelectString(row)).append(" AS c1")
+                        isFirst.set(false)
+                    } else {
+                        sb.append(toSelectString(row))
+                    }
+                }
+                return sb.toString()
+            }).collect(Collectors.joining("\nUNION ALL\n"))
+        return sql
+    }
+
     void explain(Closure actionSupplier) {
         runAction(new ExplainAction(context), actionSupplier)
     }
@@ -116,10 +254,10 @@ abstract class Suite extends Script implements 
GroovyInterceptable {
     }
 
     void quickTest(String tag, String sql, boolean order = false) {
-        log.info("Execute tag: ${tag}, sql: ${sql}".toString())
+        logger.info("Execute tag: ${tag}, sql: ${sql}".toString())
 
         if (context.config.generateOutputFile || 
context.config.forceGenerateOutputFile) {
-            def result = JdbcUtils.executorToStringList(context.conn, sql)
+            def result = 
JdbcUtils.executorToStringList(context.getConnection(), sql)
             if (order) {
                 result = sortByToString(result)
             }
@@ -128,40 +266,37 @@ abstract class Suite extends Script implements 
GroovyInterceptable {
             def writer = 
context.getOutputWriter(context.config.forceGenerateOutputFile)
             writer.write(realResults, tag)
         } else {
-            if (context.outputIterator == null) {
+            if (!context.outputFile.exists()) {
                 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()) {
+            if (!context.getOutputIterator().hasNextTagBlock(tag)) {
                 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()}")
             }
 
+            OutputUtils.TagBlockIterator expectCsvResults = 
context.getOutputIterator().next()
+            List<List<Object>> realResults = 
JdbcUtils.executorToStringList(context.getConnection(), sql)
+            if (order) {
+                realResults = sortByToString(realResults)
+            }
+            String errorMsg = null
             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}'")
-                }
+                errorMsg = OutputUtils.checkOutput(expectCsvResults, 
realResults.iterator(), "Check tag '${tag}' failed")
             } 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)
-                }
+                List excelContentList = [context.file.getName(), tag, 
sql.trim(), t]
+                context.recorder.reportDiffResult(excelContentList)
+                throw new IllegalStateException("Check tag '${tag}' failed", t)
+            }
+            if (errorMsg != null) {
+                List excelContentList = [context.file.getName(), tag, 
sql.trim(), errorMsg]
+                context.recorder.reportDiffResult(excelContentList)
+                throw new IllegalStateException(errorMsg)
             }
         }
     }
@@ -182,15 +317,12 @@ abstract class Suite extends Script implements 
GroovyInterceptable {
                 return this."$realMethod"(*args)
             } catch (Throwable t) {
                 // do nothing
+                return null
             }
         } else {
             // invoke origin method
             return metaClass.invokeMethod(this, name, args)
         }
     }
-
-    private Object invokeAssertions(String name, Object args) {
-
-    }
 }
 
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteContext.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteContext.groovy
index 41e7c69..320a718 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteContext.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteContext.groovy
@@ -22,42 +22,77 @@ 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
+import java.sql.DriverManager
+import java.util.concurrent.ExecutorService
 
 @Slf4j
 @CompileStatic
 class SuiteContext implements Closeable {
     public final File file
-    public final Connection conn
+    private final Connection conn
+    public final ThreadLocal<Connection> threadLocalConn = new ThreadLocal<>()
     public final Config config
     public final File dataPath
     public final File outputFile
+    public final ThreadLocal<OutputUtils.OutputBlocksIterator> 
threadLocalOutputIterator = new ThreadLocal<>()
+    public final ExecutorService executorService
     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) 
{
+    SuiteContext(File file, Connection conn, ExecutorService executorService, 
Config config, Recorder recorder) {
         this.file = file
         this.conn = conn
         this.config = config
+        this.executorService = executorService
         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)
     }
 
+    Connection getConnection() {
+        def threadConn = threadLocalConn.get()
+        if (threadConn != null) {
+            return threadConn
+        }
+        return this.conn
+    }
+
+    public <T> T connect(String user, String password, String url, Closure<T> 
actionSupplier) {
+        def originConnection = threadLocalConn.get()
+        try {
+            log.info("Create new connection for user '${user}'")
+            return DriverManager.getConnection(url, user, 
password).withCloseable { newConn ->
+                threadLocalConn.set(newConn)
+                return actionSupplier.call()
+            }
+        } finally {
+            log.info("Recover original connection")
+            if (originConnection == null) {
+                threadLocalConn.remove()
+            } else {
+                threadLocalConn.set(originConnection)
+            }
+        }
+    }
+
+    OutputUtils.OutputBlocksIterator getOutputIterator() {
+        def outputIt = threadLocalOutputIterator.get()
+        if (outputIt == null) {
+            outputIt = OutputUtils.iterator(outputFile)
+            threadLocalOutputIterator.set(outputIt)
+        }
+        return outputIt
+    }
+
     OutputUtils.OutputBlocksWriter getOutputWriter(boolean deleteIfExist) {
         if (outputBlocksWriter != null) {
             return outputBlocksWriter
@@ -84,15 +119,17 @@ class SuiteContext implements Closeable {
         }
     }
 
-    @Override
-    void close() {
+    void closeThreadLocal() {
+        def outputIterator = threadLocalOutputIterator.get()
         if (outputIterator != null) {
-            try {
-                outputIterator.close()
-            } catch (Throwable t) {
-                log.warn("Close outputFile failed", t)
-            }
+            outputIterator.close()
+            threadLocalOutputIterator.remove()
         }
+    }
+
+    @Override
+    void close() {
+        closeThreadLocal()
 
         if (outputBlocksWriter != null) {
             outputBlocksWriter.close()
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/OutputUtils.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/OutputUtils.groovy
index d5ecb51..ba37e33 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/OutputUtils.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/OutputUtils.groovy
@@ -26,7 +26,7 @@ import org.apache.commons.io.LineIterator
 
 @CompileStatic
 class OutputUtils {
-    static toCsvString(List<Object> row) {
+    static String 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) {
@@ -35,17 +35,13 @@ class OutputUtils {
         return writer.toString()
     }
 
-    static assertEquals(Iterator<List<String>> expect, Iterator<List<Object>> 
real, String info) {
+    static String checkOutput(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()}")
+                return "${info}, result mismatch, 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())}")
+                return "${info}, result mismatch, expect line is empty, but 
real is ${toCsvString(real.next())}"
             }
             if (!expect.hasNext() && !real.hasNext()) {
                 break
@@ -54,14 +50,12 @@ class OutputUtils {
             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}")
+                return "${info}, result mismatch.\nExpect line is: 
${expectCsvString}\nBut real is   : ${realCsvString}"
             }
         }
     }
 
-    static CloseableIterator<Iterator<List<String>>> iterator(File file) {
+    static OutputBlocksIterator iterator(File file) {
         def it = new ReusableIterator<String>(new LineIteratorAdaptor(new 
LineIterator(new FileReader(file))))
         return new OutputBlocksIterator(it)
     }
@@ -103,7 +97,7 @@ class OutputUtils {
             }
         }
 
-        void write(Iterator<List<String>> real, String comment) {
+        synchronized void write(Iterator<List<String>> real, String comment) {
             if (writer != null) {
                 writer.println("-- !${comment} --")
                 while (real.hasNext()) {
@@ -113,16 +107,40 @@ class OutputUtils {
             }
         }
 
-        void close() {
+        synchronized void close() {
             if (writer != null) {
                 writer.close()
             }
         }
     }
 
-    static class OutputBlocksIterator implements 
CloseableIterator<Iterator<List<String>>> {
+    static class TagBlockIterator implements Iterator<List<String>> {
+        private final String tag
+        private Iterator<List<String>> it
+
+        TagBlockIterator(String tag, Iterator<List<String>> it) {
+            this.tag = tag
+            this.it = it
+        }
+
+        String getTag() {
+            return tag
+        }
+
+        @Override
+        boolean hasNext() {
+            return it.hasNext()
+        }
+
+        @Override
+        List<String> next() {
+            return it.next()
+        }
+    }
+
+    static class OutputBlocksIterator implements 
CloseableIterator<TagBlockIterator> {
         private ReusableIterator<String> lineIt
-        private CsvParserIterator cache
+        private TagBlockIterator cache
         private boolean cached
 
         OutputBlocksIterator(ReusableIterator<String> lineIt) {
@@ -146,17 +164,21 @@ class OutputUtils {
                     return false
                 }
 
+                String tag = null
                 // find next comment block
                 while (true) {
                     String blockComment = lineIt.next() // skip block comment, 
e.g. -- !qt_sql_1 --
                     if (blockComment.startsWith("-- !") && 
blockComment.endsWith(" --")) {
+                        if (blockComment.startsWith("-- !")) {
+                            tag = blockComment.substring("-- !".length(), 
blockComment.length() - " --".length()).trim()
+                        }
                         break
                     }
                     if (!lineIt.hasNext()) {
                         return false
                     }
                 }
-                cache = new CsvParserIterator(new 
SkipLastEmptyLineIterator(new OutputBlockIterator(lineIt)))
+                cache = new TagBlockIterator(tag, new CsvParserIterator(new 
SkipLastEmptyLineIterator(new OutputBlockIterator(lineIt))))
                 cached = true
                 return true
             } else {
@@ -164,8 +186,23 @@ class OutputUtils {
             }
         }
 
+        boolean hasNextTagBlock(String tag) {
+            while (hasNext()) {
+                if (Objects.equals(tag, cache.tag)) {
+                    return true
+                }
+
+                // drain out
+                def it = next()
+                while (it.hasNext()) {
+                    it.next()
+                }
+            }
+            return false
+        }
+
         @Override
-        Iterator<List<String>> next() {
+        TagBlockIterator next() {
             if (hasNext()) {
                 cached = false
                 return cache
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Recorder.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Recorder.groovy
index b1cb460..34f3e82 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Recorder.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Recorder.groovy
@@ -21,8 +21,8 @@ import groovy.transform.CompileStatic
 
 @CompileStatic
 class Recorder {
-    public final List<SuiteInfo> successList = new ArrayList<>()
-    public final List<SuiteInfo> failureList = new ArrayList<>()
+    public final List<SuiteInfo> successList = new Vector<>()
+    public final List<SuiteInfo> failureList = new Vector<>()
 
     void onSuccess(SuiteInfo suiteInfo) {
         successList.add(suiteInfo)
diff --git a/regression-test/framework/src/main/groovy/suite.gdsl 
b/regression-test/framework/src/main/groovy/suite.gdsl
new file mode 100644
index 0000000..f39d661
--- /dev/null
+++ b/regression-test/framework/src/main/groovy/suite.gdsl
@@ -0,0 +1,83 @@
+// 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.
+
+// GDSL guide: 
https://confluence.jetbrains.com/display/GRVY/Scripting+IDE+for+DSL+awareness
+
+def suiteContext = context(
+    pathRegexp: ".*/regression-test/suites/.*",
+    filetypes: ["groovy"]
+)
+
+def suiteClassName = "org.apache.doris.regression.suite.Suite"
+
+def bindAction = { actionName, actionClassName ->
+    def closureBody = context(scope: closureScope(isArg: false))
+    contributor([closureBody]) {
+        if (enclosingCall(actionName)) {
+            def actionClass = findClass(actionClassName)
+            delegatesTo(actionClass)
+        }
+    }
+}
+
+bindAction("test", "org.apache.doris.regression.action.TestAction")
+bindAction("explain", "org.apache.doris.regression.action.ExplainAction")
+bindAction("streamLoad", "org.apache.doris.regression.action.StreamLoadAction")
+
+// bind qt_xxx and order_qt_xxx methods
+contributor([suiteContext]) {
+    def place = getPlace()
+    if (place == null || 
!place.getClass().getName().contains("GrReferenceExpressionImpl")) {
+        return
+    }
+    def invokeMethodName = place.getQualifiedReferenceName()
+    if (invokeMethodName == null) {
+        return
+    }
+    if (invokeMethodName.startsWith("qt_") || 
invokeMethodName.startsWith("order_qt_")) {
+        def suiteClass = findClass(suiteClassName)
+        def quickTestMethods = suiteClass.findMethodsByName("quickTest")
+        method(name: invokeMethodName, bindsTo: quickTestMethods[0])
+    }
+}
+
+contributor([suiteContext]) {
+    // bind assertXxx
+    def assertionsClass = findClass("org.junit.jupiter.api.Assertions")
+    delegatesTo(assertionsClass)
+
+    if (enclosingCall("check") ||
+            (!enclosingCall("test") &&
+            !enclosingCall("explain") &&
+            !enclosingCall("streamLoad"))) {
+        // bind other suite method and field
+        def suiteClass = findClass(suiteClassName)
+        delegatesTo(suiteClass)
+
+        // bind try_xxx
+        suiteClass.methods.each { m ->
+            if (m.isConstructor()) {
+                return
+            }
+            def parameters = 
m.getParameterList().getParameters().collectEntries { p ->
+                [p.name, p.getType().getPresentableText()]
+            }
+            def returnType = m.returnType.getPresentableText()
+            method(name: "try_${m.name}", bindsTo: m, params: parameters, 
type: returnType)
+        }
+    }
+}
\ No newline at end of file
diff --git a/regression-test/suites/demo/connect_action.groovy 
b/regression-test/suites/demo/connect_action.groovy
new file mode 100644
index 0000000..0db0372
--- /dev/null
+++ b/regression-test/suites/demo/connect_action.groovy
@@ -0,0 +1,16 @@
+def result1 = connect(user = 'admin', password = context.config.jdbcPassword, 
url = context.config.jdbcUrl) {
+    // execute sql with admin user
+    sql 'select 99 + 1'
+}
+
+// if not specify <user, password, url>, it will be set to 
context.config.jdbc<User, Password, Url>
+//
+// user: 'root'
+// password: context.config.jdbcPassword
+// url: context.config.jdbcUrl
+def result2 = connect('root') {
+    // execute sql with root user
+    sql 'select 50 + 50'
+}
+
+assertEquals(result1, result2)
\ No newline at end of file
diff --git a/regression-test/suites/demo/event_action.groovy 
b/regression-test/suites/demo/event_action.groovy
new file mode 100644
index 0000000..722ac6a
--- /dev/null
+++ b/regression-test/suites/demo/event_action.groovy
@@ -0,0 +1,40 @@
+def createTable = { tableName ->
+    sql """
+        create table ${tableName}
+        (id int)
+        distributed by hash(id)
+        properties
+        (
+          "replication_num"="1"
+        )
+        """
+}
+
+def tableName = "test_events_table1"
+createTable(tableName)
+
+// lazy drop table when execute this suite finished
+onFinish {
+    try_sql "drop table if exists ${tableName}"
+}
+
+
+
+// all event: success, fail, finish
+// and you can listen event multiple times
+
+onSuccess {
+    try_sql "drop table if exists ${tableName}"
+}
+
+onSuccess {
+    try_sql "drop table if exists ${tableName}_not_exist"
+}
+
+onFail {
+    try_sql "drop table if exists ${tableName}"
+}
+
+onFail {
+    try_sql "drop table if exists ${tableName}_not_exist"
+}
\ No newline at end of file
diff --git a/regression-test/suites/demo/lazyCheck_action.groovy 
b/regression-test/suites/demo/lazyCheck_action.groovy
new file mode 100644
index 0000000..2902147
--- /dev/null
+++ b/regression-test/suites/demo/lazyCheck_action.groovy
@@ -0,0 +1,33 @@
+/***** 1. lazy check exceptions *****/
+
+// will not throw exception immediately
+def result = lazyCheck {
+    sql "a b c d e d" // syntax error
+}
+assertTrue(result == null)
+
+result = lazyCheck {
+    sql "select 100"
+}
+assertEquals(result[0][0], 100)
+
+logger.info("You will see this log")
+
+// if you not clear the lazyCheckExceptions, and then,
+// after this suite execute finished, the syntax error in the lazyCheck action 
will be thrown.
+lazyCheckExceptions.clear()
+
+
+/***** 2. lazy check futures *****/
+
+// start new thread and lazy check future
+def futureResult = lazyCheckThread {
+    sql "a b c d e d"
+}
+assertTrue(futureResult instanceof java.util.concurrent.Future)
+
+logger.info("You will see this log too")
+
+// if you not clear the lazyCheckFutures, and then,
+// after this suite execute finished, the syntax error in the lazyCheckThread 
action will be thrown.
+lazyCheckFutures.clear()
diff --git a/regression-test/suites/demo/select_union_all_action.groovy 
b/regression-test/suites/demo/select_union_all_action.groovy
new file mode 100644
index 0000000..d4915a7
--- /dev/null
+++ b/regression-test/suites/demo/select_union_all_action.groovy
@@ -0,0 +1,19 @@
+// 3 rows and 1 column
+def rows = [3, 1, 10]
+order_qt_select_union_all1 """
+            select c1
+            from
+            (
+              ${selectUnionAll(rows)}
+            ) a
+            """
+
+// 3 rows and 2 columns
+rows = [[1, "123"], [2, null], [0, "abc"]]
+order_qt_select_union_all2 """
+             select c1, c2
+             from
+             (
+                ${selectUnionAll(rows)}
+             ) b
+             """
\ No newline at end of file
diff --git a/regression-test/suites/demo/sql_action.groovy 
b/regression-test/suites/demo/sql_action.groovy
index 9c737bd..9ba6716 100644
--- a/regression-test/suites/demo/sql_action.groovy
+++ b/regression-test/suites/demo/sql_action.groovy
@@ -48,6 +48,7 @@ try {
      *    return xxx(args)
      * } catch (Throwable t) {
      *     // do nothing
+     *     return null
      * }
      */
     try_sql("DROP TABLE IF EXISTS ${testTable}")
@@ -74,6 +75,7 @@ def list = order_sql """
                 union all
                 select 3
                 """
+
 assertEquals(null, list[0][0])
 assertEquals(1, list[1][0])
 assertEquals(15, list[2][0])
diff --git a/regression-test/suites/demo/streamLoad_action.groovy 
b/regression-test/suites/demo/streamLoad_action.groovy
index b2881ae..f9ae5bf 100644
--- a/regression-test/suites/demo/streamLoad_action.groovy
+++ b/regression-test/suites/demo/streamLoad_action.groovy
@@ -35,7 +35,9 @@ streamLoad {
 
 // stream load 100 rows
 def rowCount = 100
-def rowIt = java.util.stream.LongStream.range(0, rowCount) // [0, rowCount)
+// range: [0, rowCount)
+// or rangeClosed: [0, rowCount]
+def rowIt = range(0, rowCount)
         .mapToObj({i -> [i, "a_" + i]}) // change Long to List<Long, String>
         .iterator()
 
diff --git a/regression-test/suites/demo/thread_action.groovy 
b/regression-test/suites/demo/thread_action.groovy
new file mode 100644
index 0000000..7af7ff8
--- /dev/null
+++ b/regression-test/suites/demo/thread_action.groovy
@@ -0,0 +1,48 @@
+def (_, elapsedMillis) = timer {
+    /**
+     * the default max thread num is 10, you can specify by 'actionParallel' 
param.
+     * e.g. ./run-regression-test.sh --run someSuite -actionParallel 10
+     */
+    def future1 = thread("threadName1") {
+        sleep(200)
+        sql"select 1"
+    }
+
+    // create new thread but not specify name
+    def future2 = thread {
+        sleep(200)
+        sql "select 2"
+    }
+
+    def future3 = thread("threadName3") {
+        sleep(200)
+        sql "select 3"
+    }
+
+    def future4 = thread {
+        sleep(200)
+        sql "select 4"
+    }
+
+    // equals to combineFutures([future1, future2, future3, future4]), which 
[] is a Iterable<ListenableFuture>
+    def combineFuture = combineFutures(future1, future2, future3, future4)
+    // or you can use lazyCheckThread action(see lazyCheck_action.groovy), and 
not have to check exception from futures.
+    List<List<List<Object>>> result = combineFuture.get()
+    assertEquals(result[0][0][0], 1)
+    assertEquals(result[1][0][0], 2)
+    assertEquals(result[2][0][0], 3)
+    assertEquals(result[3][0][0], 4)
+}
+assertTrue(elapsedMillis < 600)
+
+
+// you can use qt action in thread action, and you **MUST** specify different 
tag,
+// testing framework can compare different qt result in different order.
+lazyCheckThread {
+    sleep(100)
+    qt_diffrent_tag1 "select 100"
+}
+
+lazyCheckThread("lazyCheckThread2") {
+    qt_diffrent_tag2 "select 100"
+}
\ No newline at end of file
diff --git a/regression-test/suites/demo/timer_action.groovy 
b/regression-test/suites/demo/timer_action.groovy
new file mode 100644
index 0000000..14b84f5
--- /dev/null
+++ b/regression-test/suites/demo/timer_action.groovy
@@ -0,0 +1,7 @@
+def (sumResult, elapsedMillis) = timer {
+    long sum = 0
+    (1..10000).each {sum += it}
+    sum // return value
+}
+
+logger.info("sum: ${sumResult}, elapsed: ${elapsedMillis} ms")

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@doris.apache.org
For additional commands, e-mail: commits-h...@doris.apache.org

Reply via email to