diff --git a/pom.xml b/pom.xml
index e1ff256c..a57f3ff5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -45,6 +45,7 @@
nlpxgboostmixserv
+ systemtest
diff --git a/systemtest/README.md b/systemtest/README.md
new file mode 100644
index 00000000..2b1167e2
--- /dev/null
+++ b/systemtest/README.md
@@ -0,0 +1,211 @@
+
+## Usage
+
+### Initialization
+
+Define `CommonInfo`, `Runner` and `Team` in each your test class.
+
+#### `CommonInfo`
+
+* `SystemTestCommonInfo`
+
+`CommonInfo` holds common information of test class, for example,
+you can refer to auto-defined path to resources. This should be defined as `private static`.
+
+
+#### `Runner`
+
+* `HiveSystemTestRunner`
+* `TDSystemTestRunner`
+
+`Runner` represents a test environment and its configuration. This must be defined with `@ClassRule`
+as `public static` because of JUnit spec. You can add test class initializations by `#initBy(...)`
+with class methods of `HQ`, which are abstract domain-specific hive queries, in instance initializer
+of each `Runner`.
+
+
+#### `Team`
+
+* `SystemTestTeam`
+
+`Team` manages `Runner`s each test method. This must be defined with `@Rule` as `public` because of
+JUnit spec. You can set `Runner`s via constructor argument as common in class and via `#add(...)`
+as method-local and add test method initializations by `#initBy(...)` and test case by `#set(...)`
+with class methods of `HQ`. Finally, don't forget call `#run()` to enable set `HQ`s.
+As an alternative method, by `#set(HQ.autoMatchingByFileName(${filename}))` with queries predefined in
+`auto-defined/path/init/${filename}`, `auto-defined/path/case/${filename}` and
+`auto-defined/path/answer/${filename}`, you can do auto matching test.
+
+
+### External properties
+
+You can use external properties at `systemtest/src/test/resources/hivemall/*`, default is `hiverunner.properties`
+for `HiveSystemTestRunner` and `td.properties` for `TDSystemTestRunner`. Also user-defined properties file can
+be loaded via constructor of `Runner` by file name.
+
+
+## Notice
+
+* DDL and insert statement should be called via class methods of `HQ` because of wrapping hive queries
+and several runner-specific APIs, don't call them via string statement
+* Also you can use low-level API via an instance of `Runner`, independent of `Team`
+* You can use `IO.getFromResourcePath(...)` to get answer whose format is TSV
+* Table created in initialization of runner should be used as immutable, don't neither insert nor update
+* TD client configs in properties file prior to $HOME/.td/td.conf
+* Don't use insert w/ big data, use file upload instead
+
+## Quick example
+
+```java
+package hivemall;
+// here is several imports
+public class QuickExample {
+ private static SystemTestCommonInfo ci = new SystemTestCommonInfo(QuickExample.class);
+
+ @ClassRule
+ public static HiveSystemTestRunner hRunner = new HiveSystemTestRunner(ci) {
+ {
+ initBy(HQ.uploadByResourcePathAsNewTable("color", ci.initDir + "color.tsv",
+ new LinkedHashMap() {
+ {
+ put("name", "string");
+ put("red", "int");
+ put("green", "int");
+ put("blue", "int");
+ }
+ })); // create table `color`, which is marked as immutable, for this test class
+
+ // add function from hivemall class
+ initBy(HQ.fromStatement("CREATE TEMPORARY FUNCTION hivemall_version as 'hivemall.HivemallVersionUDF'"));
+ }
+ };
+
+ @ClassRule
+ public static TDSystemTestRunner tRunner = new TDSystemTestRunner(ci) {
+ {
+ initBy(HQ.uploadByResourcePathAsNewTable("color", ci.initDir + "color.tsv",
+ new LinkedHashMap() {
+ {
+ put("name", "string");
+ put("red", "int");
+ put("green", "int");
+ put("blue", "int");
+ }
+ })); // create table `color`, which is marked as immutable, for this test class
+ }
+ };
+
+ @Rule
+ public SystemTestTeam team = new SystemTestTeam(hRunner); // set hRunner as default runner
+
+ @Rule
+ public ExpectedException predictor = ExpectedException.none();
+
+
+ @Test
+ public void test0() throws Exception {
+ team.add(tRunner, hRunner); // test on HiveRunner -> TD -> HiveRunner (NOTE: state of DB is retained in each runner)
+ team.set(HQ.fromStatement("SELECT name FROM color WHERE blue = 255 ORDER BY name"), "azure\tblue\tmagenta", true); // ordered test
+ team.run(); // this call is required
+ }
+
+ @Test
+ public void test1() throws Exception {
+ // test on HiveRunner once only
+ String tableName = "users";
+ team.initBy(HQ.createTable(tableName, new LinkedHashMap() {
+ {
+ put("name", "string");
+ put("age", "int");
+ put("favorite_color", "string");
+ }
+ })); // create local table in this test method `users` for each set runner(only hRunner here)
+ team.initBy(HQ.insert(tableName, Arrays.asList("name", "age", "favorite_color"), Arrays.asList(
+ new Object[]{"Karen", 16, "orange"}, new Object[]{"Alice", 17, "pink"}))); // insert into `users`
+ team.set(HQ.fromStatement("SELECT CONCAT('rgb(', red, ',', green, ',', blue, ')') FROM "
+ + tableName + " u LEFT JOIN color c on u.favorite_color = c.name"), "rgb(255,165,0)\trgb(255,192,203)"); // unordered test
+ team.run(); // this call is required
+ }
+
+ @Test
+ public void test2() throws Exception {
+ // You can also use runner's raw API directly
+ for(RawHQ q: HQ.fromStatements("SELECT hivemall_version();SELECT hivemall_version();")) {
+ System.out.println(hRunner.exec(q).get(0));
+ }
+ // raw API doesn't require `SystemTestTeam#run()`
+ }
+
+ @Test
+ public void test3() throws Exception {
+ // test on HiveRunner once only
+ // auto matching by files which name is `test3` in `case/` and `answer/`
+ team.set(HQ.autoMatchingByFileName("test3"), ci); // unordered test
+ team.run(); // this call is required
+ }
+
+ @Test
+ public void test4() throws Exception {
+ // test on HiveRunner once only
+ predictor.expect(Throwable.class); // you can use systemtest w/ other rules
+ team.set(HQ.fromStatement("invalid queryyy"), "never used"); // this query throws an exception
+ team.run(); // this call is required
+ // thrown exception will be caught by `ExpectedException` rule
+ }
+}
+```
+
+The above requires following files
+
+* `systemtest/src/test/resources/hivemall/QuickExample/init/color.tsv` (`systemtest/src/test/resources/${path/to/package}/${className}/init/${fileName}`)
+
+```tsv
+blue 0 0 255
+lavender 230 230 250
+magenta 255 0 255
+violet 238 130 238
+purple 128 0 128
+azure 240 255 255
+lightseagreen 32 178 170
+orange 255 165 0
+orangered 255 69 0
+red 255 0 0
+pink 255 192 203
+```
+
+* `systemtest/src/test/resources/hivemall/QuickExample/case/test3` (`systemtest/src/test/resources/${path/to/package}/${className}/case/${fileName}`)
+
+```sql
+-- write your hive queries
+-- comments like this and multiple queries in one row are allowed
+SELECT blue FROM color WHERE name = 'lavender';
+SELECT green FROM color WHERE name LIKE 'orange%';
+SELECT name FROM color WHERE blue = 255;
+```
+
+* `systemtest/src/test/resources/hivemall/QuickExample/answer/test3` (`systemtest/src/test/resources/${path/to/package}/${className}/answer/${fileName}`)
+
+tsv format is required
+
+```tsv
+250
+165 69
+azure blue magenta
+```
diff --git a/systemtest/pom.xml b/systemtest/pom.xml
new file mode 100644
index 00000000..0debee03
--- /dev/null
+++ b/systemtest/pom.xml
@@ -0,0 +1,105 @@
+
+
+ 4.0.0
+
+
+ io.github.myui
+ hivemall
+ 0.4.2-rc.2
+ ../pom.xml
+
+
+ hivemall-systemtest
+ System test for Hivemall
+ jar
+
+
+
+ io.github.myui
+ hivemall-core
+ 0.4.2-rc.2
+
+
+ junit
+ junit
+ 4.12
+
+
+ com.klarna
+ hiverunner
+ 3.0.0
+
+
+ com.treasuredata.client
+ td-client
+ 0.7.25
+ jar-with-dependencies
+
+
+ org.apache.commons
+ commons-csv
+ 1.1
+
+
+ org.msgpack
+ msgpack-core
+ 0.8.9
+
+
+ org.hamcrest
+ hamcrest-library
+ 1.3
+
+
+
+
+ target
+ target/classes
+ ${project.artifactId}-${project.version}
+ target/test-classes
+
+
+
+ com.mycila
+ license-maven-plugin
+ 2.8
+
+ ${project.parent.basedir}/resources/license-header.txt
+
+ ${build.year}
+ ${project.organization.name}
+
+
+ src/main/**/*.java
+ src/test/**/*.java
+
+ UTF-8
+
+ ${project.parent.basedir}/resources/header-definition.xml
+
+
+
+
+
+
+
+
diff --git a/systemtest/src/main/java/com/klarna/hiverunner/Extractor.java b/systemtest/src/main/java/com/klarna/hiverunner/Extractor.java
new file mode 100644
index 00000000..f7f372ff
--- /dev/null
+++ b/systemtest/src/main/java/com/klarna/hiverunner/Extractor.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.klarna.hiverunner;
+
+import com.klarna.hiverunner.config.HiveRunnerConfig;
+import org.junit.rules.TemporaryFolder;
+
+public class Extractor {
+ public static StandaloneHiveServerContext getStandaloneHiveServerContext(
+ TemporaryFolder basedir, HiveRunnerConfig hiveRunnerConfig) {
+ return new StandaloneHiveServerContext(basedir, hiveRunnerConfig);
+ }
+
+ public static HiveServerContainer getHiveServerContainer(HiveServerContext context) {
+ return new HiveServerContainer(context);
+ }
+}
diff --git a/systemtest/src/main/java/hivemall/systemtest/MsgpackConverter.java b/systemtest/src/main/java/hivemall/systemtest/MsgpackConverter.java
new file mode 100644
index 00000000..b86c1cf8
--- /dev/null
+++ b/systemtest/src/main/java/hivemall/systemtest/MsgpackConverter.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package hivemall.systemtest;
+
+import hivemall.utils.lang.Preconditions;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
+import org.msgpack.core.MessagePack;
+import org.msgpack.core.MessagePacker;
+import org.msgpack.value.ValueFactory;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.util.List;
+import java.util.zip.GZIPOutputStream;
+
+public class MsgpackConverter {
+ @Nonnull
+ private final File file;
+ @Nonnull
+ private final List header;
+ @Nonnull
+ private final CSVFormat format;
+
+ public MsgpackConverter(@CheckForNull File file, @CheckForNull List header,
+ @CheckForNull CSVFormat format) {
+ Preconditions.checkNotNull(file);
+ Preconditions.checkNotNull(header);
+ Preconditions.checkNotNull(format);
+ Preconditions.checkArgument(file.exists(), "%s not found", file.getPath());
+
+ this.file = file;
+ this.header = header;
+ this.format = format;
+ }
+
+ public byte[] asByteArray(final boolean needTimeColumn) throws Exception {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ final MessagePacker packer = MessagePack.newDefaultPacker(new GZIPOutputStream(os));
+ final BufferedReader br = new BufferedReader(new FileReader(file));
+ try {
+ // always skip header, use user-defined or existing table's
+ final CSVParser parser = format.withSkipHeaderRecord().parse(br);
+ final long time = System.currentTimeMillis() / 1000;
+ for (CSVRecord record : parser.getRecords()) {
+ final ValueFactory.MapBuilder map = ValueFactory.newMapBuilder();
+
+ // add `time` column if needed && not exists
+ if (needTimeColumn && !header.contains("time")) {
+ map.put(ValueFactory.newString("time"), ValueFactory.newInteger(time));
+ }
+
+ // pack each value in row
+ int i = 0;
+ for (String val : record) {
+ map.put(ValueFactory.newString(header.get(i)), ValueFactory.newString(val));
+ i++;
+ }
+ packer.packValue(map.build());
+ }
+ } finally {
+ br.close();
+ packer.close();
+ }
+
+ return os.toByteArray();
+ }
+
+ public byte[] asByteArray() throws Exception {
+ return asByteArray(true);
+ }
+
+ public File asFile(@CheckForNull File to, final boolean needTimeColumn) throws Exception {
+ Preconditions.checkNotNull(to);
+ Preconditions.checkArgument(to.exists(), "%s not found", to.getPath());
+
+ FileOutputStream os = null;
+ try {
+ os = new FileOutputStream(to);
+ os.write(asByteArray(needTimeColumn));
+ return to;
+ } finally {
+ if (os != null) {
+ os.close();
+ }
+ }
+ }
+
+ public File asFile(File to) throws Exception {
+ return asFile(to, true);
+ }
+}
diff --git a/systemtest/src/main/java/hivemall/systemtest/exception/QueryExecutionException.java b/systemtest/src/main/java/hivemall/systemtest/exception/QueryExecutionException.java
new file mode 100644
index 00000000..c2b00344
--- /dev/null
+++ b/systemtest/src/main/java/hivemall/systemtest/exception/QueryExecutionException.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package hivemall.systemtest.exception;
+
+import javax.annotation.Nonnull;
+
+public class QueryExecutionException extends RuntimeException {
+ public QueryExecutionException(@Nonnull final String message) {
+ super(message);
+ }
+}
diff --git a/systemtest/src/main/java/hivemall/systemtest/model/CreateTableHQ.java b/systemtest/src/main/java/hivemall/systemtest/model/CreateTableHQ.java
new file mode 100644
index 00000000..e0047a65
--- /dev/null
+++ b/systemtest/src/main/java/hivemall/systemtest/model/CreateTableHQ.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package hivemall.systemtest.model;
+
+import javax.annotation.Nonnull;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class CreateTableHQ extends TableHQ {
+ @Nonnull
+ public final LinkedHashMap header;
+
+ CreateTableHQ(@Nonnull final String tableName,
+ @Nonnull final LinkedHashMap header) {
+ super(tableName);
+
+ this.header = header;
+ }
+
+ public String getTableDeclaration() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("(");
+ for (Map.Entry e : header.entrySet()) {
+ sb.append(e.getKey());
+ sb.append(" ");
+ sb.append(e.getValue());
+ sb.append(",");
+ }
+ sb.deleteCharAt(sb.length() - 1);
+ sb.append(")");
+ return sb.toString();
+ }
+}
diff --git a/systemtest/src/main/java/hivemall/systemtest/model/DropTableHQ.java b/systemtest/src/main/java/hivemall/systemtest/model/DropTableHQ.java
new file mode 100644
index 00000000..c09ae247
--- /dev/null
+++ b/systemtest/src/main/java/hivemall/systemtest/model/DropTableHQ.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package hivemall.systemtest.model;
+
+import javax.annotation.Nonnull;
+
+public class DropTableHQ extends TableHQ {
+ DropTableHQ(@Nonnull final String tableName) {
+ super(tableName);
+ }
+}
diff --git a/systemtest/src/main/java/hivemall/systemtest/model/HQ.java b/systemtest/src/main/java/hivemall/systemtest/model/HQ.java
new file mode 100644
index 00000000..05933a4c
--- /dev/null
+++ b/systemtest/src/main/java/hivemall/systemtest/model/HQ.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package hivemall.systemtest.model;
+
+import com.google.common.io.Resources;
+import com.klarna.hiverunner.CommandShellEmulation;
+import com.klarna.hiverunner.sql.StatementsSplitter;
+import hivemall.systemtest.model.lazy.LazyMatchingResource;
+import hivemall.utils.lang.Preconditions;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+/**
+ * Domain-specific Hive Query Factory
+ */
+public class HQ {
+
+ private HQ() {}
+
+ @Nonnull
+ public static RawHQ fromStatement(String query) {
+ Preconditions.checkNotNull(query);
+
+ final String formatted = CommandShellEmulation.HIVE_CLI.transformScript(query);
+ final List split = StatementsSplitter.splitStatements(formatted);
+
+ Preconditions.checkArgument(
+ 1 == split.size(),
+ "Detected %s queries, should be exactly one. Use `HQ.fromStatements` for multi queries.",
+ split.size());
+
+ return new RawHQ(split.get(0));
+ }
+
+ @Nonnull
+ public static List fromStatements(@CheckForNull final String queries) {
+ Preconditions.checkNotNull(queries);
+
+ final String formatted = CommandShellEmulation.HIVE_CLI.transformScript(queries);
+ final List split = StatementsSplitter.splitStatements(formatted);
+ final List results = new ArrayList();
+ for (String q : split) {
+ results.add(new RawHQ(q));
+ }
+ return results;
+ }
+
+ @Nonnull
+ public static LazyMatchingResource autoMatchingByFileName(@CheckForNull final String fileName,
+ @CheckForNull final Charset charset) {
+ Preconditions.checkNotNull(fileName);
+ Preconditions.checkNotNull(charset);
+
+ return new LazyMatchingResource(fileName, charset);
+ }
+
+ @Nonnull
+ public static LazyMatchingResource autoMatchingByFileName(final String fileName) {
+ return autoMatchingByFileName(fileName, Charset.defaultCharset());
+ }
+
+ @Nonnull
+ public static List fromResourcePath(final String resourcePath, final Charset charset) {
+ return autoMatchingByFileName(resourcePath, charset).toStrict("");
+ }
+
+ @Nonnull
+ public static List fromResourcePath(final String resourcePath) {
+ return fromResourcePath(resourcePath, Charset.defaultCharset());
+ }
+
+ @Nonnull
+ public static TableListHQ tableList() {
+ return new TableListHQ();
+ }
+
+ @Nonnull
+ public static CreateTableHQ createTable(@CheckForNull final String tableName,
+ @CheckForNull final LinkedHashMap header) {
+ Preconditions.checkNotNull(tableName);
+ Preconditions.checkNotNull(header);
+
+ return new CreateTableHQ(tableName, header);
+ }
+
+ @Nonnull
+ public static DropTableHQ dropTable(@CheckForNull final String tableName) {
+ Preconditions.checkNotNull(tableName);
+
+ return new DropTableHQ(tableName);
+ }
+
+ @Nonnull
+ public static InsertHQ insert(@CheckForNull final String tableName,
+ @CheckForNull final List header, @CheckForNull final List