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