/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.tajo; import com.google.protobuf.ServiceException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.tajo.algebra.*; import org.apache.tajo.annotation.Nullable; import org.apache.tajo.catalog.CatalogService; import org.apache.tajo.catalog.CatalogUtil; import org.apache.tajo.catalog.TableDesc; import org.apache.tajo.cli.ParsedResult; import org.apache.tajo.cli.SimpleParser; import org.apache.tajo.client.TajoClient; import org.apache.tajo.conf.TajoConf; import org.apache.tajo.engine.parser.SQLAnalyzer; import org.apache.tajo.storage.StorageUtil; import org.apache.tajo.util.FileUtil; import org.junit.*; import org.junit.rules.TestName; import java.io.File; import java.io.IOException; import java.net.URL; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.*; import static org.junit.Assert.*; /** * (Note that this class is not thread safe. Do not execute maven test in any parallel mode.) * <br /> * <code>QueryTestCaseBase</code> provides useful methods to easily execute queries and verify their results. * * This class basically uses four resource directories: * <ul> * <li>src/test/resources/dataset - contains a set of data files. It contains sub directories, each of which * corresponds each test class. All data files in each sub directory can be used in the corresponding test class.</li> * * <li>src/test/resources/queries - This is the query directory. It contains sub directories, each of which * corresponds each test class. All query files in each sub directory can be used in the corresponding test * class.</li> * * <li>src/test/resources/results - This is the result directory. It contains sub directories, each of which * corresponds each test class. All result files in each sub directory can be used in the corresponding test class. * </li> * </ul> * * For example, if you create a test class named <code>TestJoinQuery</code>, you should create a pair of query and * result set directories as follows: * * <pre> * src-| * |- resources * |- dataset * | |- TestJoinQuery * | |- table1.tbl * | |- table2.tbl * | * |- queries * | |- TestJoinQuery * | |- TestInnerJoin.sql * | |- table1_ddl.sql * | |- table2_ddl.sql * | * |- results * |- TestJoinQuery * |- TestInnerJoin.result * </pre> * * <code>QueryTestCaseBase</code> basically provides the following methods: * <ul> * <li><code>{@link #executeQuery()}</code> - executes a corresponding query and returns an ResultSet instance</li> * <li><code>{@link #executeFile(String)}</code> - executes a given query file included in the corresponding query * file in the current class's query directory</li> * <li><code>assertResultSet()</code> - check if the query result is equivalent to the expected result included * in the corresponding result file in the current class's result directory.</li> * <li><code>cleanQuery()</code> - clean up all resources</li> * <li><code>executeDDL()</code> - execute a DDL query like create or drop table.</li> * </ul> * * In order to make use of the above methods, query files and results file must be as follows: * <ul> * <li>Each query file must be located on the subdirectory whose structure must be src/resources/queries/${ClassName}, * where ${ClassName} indicates an actual test class's simple name.</li> * <li>Each result file must be located on the subdirectory whose structure must be src/resources/results/${ClassName}, * where ${ClassName} indicates an actual test class's simple name.</li> * </ul> * * Especially, {@link #executeQuery() and {@link #assertResultSet(java.sql.ResultSet)} methods automatically finds * a query file to be executed and a result to be compared, which are corresponding to the running class and method. * For them, query and result files additionally must be follows as: * <ul> * <li>Each result file must have the file extension '.result'</li> * <li>Each query file must have the file extension '.sql'.</li> * </ul> */ public class QueryTestCaseBase { private static final Log LOG = LogFactory.getLog(QueryTestCaseBase.class); protected static final TpchTestBase testBase; protected static final TajoTestingCluster testingCluster; protected static TajoConf conf; protected static TajoClient client; protected static final CatalogService catalog; protected static final SQLAnalyzer sqlParser = new SQLAnalyzer(); /** the base path of dataset directories */ protected static final Path datasetBasePath; /** the base path of query directories */ protected static final Path queryBasePath; /** the base path of result directories */ protected static final Path resultBasePath; static { testBase = TpchTestBase.getInstance(); testingCluster = testBase.getTestingCluster(); conf = testBase.getTestingCluster().getConfiguration(); catalog = testBase.getTestingCluster().getMaster().getCatalog(); URL datasetBaseURL = ClassLoader.getSystemResource("dataset"); datasetBasePath = new Path(datasetBaseURL.toString()); URL queryBaseURL = ClassLoader.getSystemResource("queries"); queryBasePath = new Path(queryBaseURL.toString()); URL resultBaseURL = ClassLoader.getSystemResource("results"); resultBasePath = new Path(resultBaseURL.toString()); } /** It transiently contains created tables for the running test class. */ private static String currentDatabase; private static Set<String> createdTableGlobalSet = new HashSet<String>(); // queries and results directory corresponding to subclass class. private Path currentQueryPath; private Path currentResultPath; private Path currentDatasetPath; // for getting a method name @Rule public TestName name = new TestName(); @BeforeClass public static void setUpClass() throws IOException { conf = testBase.getTestingCluster().getConfiguration(); client = new TajoClient(conf); } @AfterClass public static void tearDownClass() throws ServiceException { for (String tableName : createdTableGlobalSet) { client.updateQuery("DROP TABLE IF EXISTS " + CatalogUtil.denormalizeIdentifier(tableName)); } createdTableGlobalSet.clear(); // if the current database is "default", shouldn't drop it. if (!currentDatabase.equals(TajoConstants.DEFAULT_DATABASE_NAME)) { for (String tableName : catalog.getAllTableNames(currentDatabase)) { client.updateQuery("DROP TABLE IF EXISTS " + tableName); } client.selectDatabase(TajoConstants.DEFAULT_DATABASE_NAME); client.dropDatabase(currentDatabase); } client.close(); } public QueryTestCaseBase() { // hive 0.12 does not support quoted identifier. // So, we use lower case database names when Tajo uses HCatalogStore. if (testingCluster.isHCatalogStoreRunning()) { this.currentDatabase = getClass().getSimpleName().toLowerCase(); } else { this.currentDatabase = getClass().getSimpleName(); } init(); } public QueryTestCaseBase(String currentDatabase) { this.currentDatabase = currentDatabase; init(); } private void init() { String className = getClass().getSimpleName(); currentQueryPath = new Path(queryBasePath, className); currentResultPath = new Path(resultBasePath, className); currentDatasetPath = new Path(datasetBasePath, className); try { // if the current database is "default", we don't need create it because it is already prepated at startup time. if (!currentDatabase.equals(TajoConstants.DEFAULT_DATABASE_NAME)) { client.updateQuery("CREATE DATABASE IF NOT EXISTS " + CatalogUtil.denormalizeIdentifier(currentDatabase)); } client.selectDatabase(currentDatabase); } catch (ServiceException e) { e.printStackTrace(); } testingCluster.setAllTajoDaemonConfValue(TajoConf.ConfVars.DIST_QUERY_BROADCAST_JOIN_AUTO.varname, "false"); } protected TajoClient getClient() { return client; } public String getCurrentDatabase() { return currentDatabase; } protected ResultSet executeString(String sql) throws Exception { return testBase.execute(sql); } /** * Execute a query contained in the file located in src/test/resources/results/<i>ClassName</i>/<i>MethodName</i>. * <i>ClassName</i> and <i>MethodName</i> will be replaced by actual executed class and methods. * * @return ResultSet of query execution. */ public ResultSet executeQuery() throws Exception { return executeFile(name.getMethodName() + ".sql"); } /** * Execute a query contained in the given named file. This methods tries to find the given file within the directory * src/test/resources/results/<i>ClassName</i>. * * @param queryFileName The file name to be used to execute a query. * @return ResultSet of query execution. */ public ResultSet executeFile(String queryFileName) throws Exception { Path queryFilePath = getQueryFilePath(queryFileName); FileSystem fs = currentQueryPath.getFileSystem(testBase.getTestingCluster().getConfiguration()); assertTrue(queryFilePath.toString() + " existence check", fs.exists(queryFilePath)); List<ParsedResult> parsedResults = SimpleParser.parseScript(FileUtil.readTextFile(new File(queryFilePath.toUri()))); if (parsedResults.size() > 1) { assertNotNull("This script \"" + queryFileName + "\" includes two or more queries"); } ResultSet result = client.executeQueryAndGetResult(parsedResults.get(0).getStatement()); assertNotNull("Query succeeded test", result); return result; } /** * Assert the equivalence between the expected result and an actual query result. * If it isn't it throws an AssertionError. * * @param result Query result to be compared. */ public final void assertResultSet(ResultSet result) throws IOException { assertResultSet("Result Verification", result, name.getMethodName() + ".result"); } /** * Assert the equivalence between the expected result and an actual query result. * If it isn't it throws an AssertionError. * * @param result Query result to be compared. * @param resultFileName The file name containing the result to be compared */ public final void assertResultSet(ResultSet result, String resultFileName) throws IOException { assertResultSet("Result Verification", result, resultFileName); } /** * Assert the equivalence between the expected result and an actual query result. * If it isn't it throws an AssertionError with the given message. * * @param message message The message to printed if the assertion is failed. * @param result Query result to be compared. */ public final void assertResultSet(String message, ResultSet result, String resultFileName) throws IOException { FileSystem fs = currentQueryPath.getFileSystem(testBase.getTestingCluster().getConfiguration()); Path resultFile = getResultFile(resultFileName); assertTrue(resultFile.toString() + " existence check", fs.exists(resultFile)); try { verifyResultText(message, result, resultFile); } catch (SQLException e) { throw new IOException(e); } } public final void assertStrings(String actual) throws IOException { assertStrings(actual, name.getMethodName() + ".result"); } public final void assertStrings(String actual, String resultFileName) throws IOException { assertStrings("Result Verification", actual, resultFileName); } public final void assertStrings(String message, String actual, String resultFileName) throws IOException { FileSystem fs = currentQueryPath.getFileSystem(testBase.getTestingCluster().getConfiguration()); Path resultFile = getResultFile(resultFileName); assertTrue(resultFile.toString() + " existence check", fs.exists(resultFile)); String expectedResult = FileUtil.readTextFile(new File(resultFile.toUri())); assertEquals(message, expectedResult, actual); } /** * Release all resources * * @param resultSet ResultSet */ public final void cleanupQuery(ResultSet resultSet) throws IOException { if (resultSet == null) { return; } try { resultSet.close(); } catch (SQLException e) { throw new IOException(e); } } /** * Assert that the database exists. * @param databaseName The database name to be checked. This name is case sensitive. */ public void assertDatabaseExists(String databaseName) throws ServiceException { assertTrue(client.existDatabase(databaseName)); } /** * Assert that the database does not exists. * @param databaseName The database name to be checked. This name is case sensitive. */ public void assertDatabaseNotExists(String databaseName) throws ServiceException { assertTrue(!client.existDatabase(databaseName)); } /** * Assert that the table exists. * * @param tableName The table name to be checked. This name is case sensitive. * @throws ServiceException */ public void assertTableExists(String tableName) throws ServiceException { assertTrue(client.existTable(tableName)); } /** * Assert that the table does not exist. * * @param tableName The table name to be checked. This name is case sensitive. */ public void assertTableNotExists(String tableName) throws ServiceException { assertTrue(!client.existTable(tableName)); } public void assertColumnExists(String tableName,String columnName) throws ServiceException { TableDesc tableDesc = fetchTableMetaData(tableName); assertTrue(tableDesc.getSchema().containsByName(columnName)); } private TableDesc fetchTableMetaData(String tableName) throws ServiceException { return client.getTableDesc(tableName); } /** * It transforms a ResultSet instance to rows represented as strings. * * @param resultSet ResultSet that contains a query result * @return String * @throws SQLException */ public String resultSetToString(ResultSet resultSet) throws SQLException { StringBuilder sb = new StringBuilder(); ResultSetMetaData rsmd = resultSet.getMetaData(); int numOfColumns = rsmd.getColumnCount(); for (int i = 1; i <= numOfColumns; i++) { if (i > 1) sb.append(","); String columnName = rsmd.getColumnName(i); sb.append(columnName); } sb.append("\n-------------------------------\n"); while (resultSet.next()) { for (int i = 1; i <= numOfColumns; i++) { if (i > 1) sb.append(","); String columnValue = resultSet.getObject(i).toString(); sb.append(columnValue); } sb.append("\n"); } return sb.toString(); } private void verifyResultText(String message, ResultSet res, Path resultFile) throws SQLException, IOException { String actualResult = resultSetToString(res); String expectedResult = FileUtil.readTextFile(new File(resultFile.toUri())); assertEquals(message, expectedResult.trim(), actualResult.trim()); } private Path getQueryFilePath(String fileName) { return StorageUtil.concatPath(currentQueryPath, fileName); } private Path getResultFile(String fileName) { return StorageUtil.concatPath(currentResultPath, fileName); } private Path getDataSetFile(String fileName) { return StorageUtil.concatPath(currentDatasetPath, fileName); } public List<String> executeDDL(String ddlFileName, @Nullable String [] args) throws Exception { return executeDDL(ddlFileName, null, true, args); } /** * * Execute a data definition language (DDL) template. A general SQL DDL statement can be included in this file. But, * for user-specified table name or exact external table path, you must use some format string to indicate them. * The format string will be replaced by the corresponding arguments. * * The below is predefined format strings: * <ul> * <li>${table.path} - It is replaced by the absolute file path that <code>dataFileName</code> points. </li> * <li>${i} - It is replaced by the corresponding element of <code>args</code>. For example, ${0} and ${1} are * replaced by the first and second elements of <code>args</code> respectively</li>. It uses zero-based index. * </ul> * * @param ddlFileName A file name, containing a data definition statement. * @param dataFileName A file name, containing data rows, which columns have to be separated by vertical bar '|'. * This file name is used for replacing some format string indicating an external table location. * @param args A list of arguments, each of which is used to replace corresponding variable which has a form of ${i}. * @return The table names created */ public List<String> executeDDL(String ddlFileName, @Nullable String dataFileName, @Nullable String ... args) throws Exception { return executeDDL(ddlFileName, dataFileName, true, args); } private List<String> executeDDL(String ddlFileName, @Nullable String dataFileName, boolean isLocalTable, @Nullable String[] args) throws Exception { Path ddlFilePath = new Path(currentQueryPath, ddlFileName); FileSystem fs = ddlFilePath.getFileSystem(conf); assertTrue(ddlFilePath + " existence check", fs.exists(ddlFilePath)); String template = FileUtil.readTextFile(new File(ddlFilePath.toUri())); String dataFilePath = null; if (dataFileName != null) { dataFilePath = getDataSetFile(dataFileName).toString(); } String compiled = compileTemplate(template, dataFilePath, args); List<ParsedResult> parsedResults = SimpleParser.parseScript(compiled); List<String> createdTableNames = new ArrayList<String>(); for (ParsedResult parsedResult : parsedResults) { // parse a statement Expr expr = sqlParser.parse(parsedResult.getStatement()); assertNotNull(ddlFilePath + " cannot be parsed", expr); if (expr.getType() == OpType.CreateTable) { CreateTable createTable = (CreateTable) expr; String tableName = createTable.getTableName(); assertTrue("Table [" + tableName + "] creation is failed.", client.updateQuery(parsedResult.getStatement())); TableDesc createdTable = client.getTableDesc(tableName); String createdTableName = createdTable.getName(); assertTrue("table '" + createdTableName + "' creation check", client.existTable(createdTableName)); if (isLocalTable) { createdTableGlobalSet.add(createdTableName); createdTableNames.add(tableName); } } else if (expr.getType() == OpType.DropTable) { DropTable dropTable = (DropTable) expr; String tableName = dropTable.getTableName(); assertTrue("table '" + tableName + "' existence check", client.existTable(CatalogUtil.buildFQName(currentDatabase, tableName))); assertTrue("table drop is failed.", client.updateQuery(parsedResult.getStatement())); assertFalse("table '" + tableName + "' dropped check", client.existTable(CatalogUtil.buildFQName(currentDatabase, tableName))); if (isLocalTable) { createdTableGlobalSet.remove(tableName); } } else if (expr.getType() == OpType.AlterTable) { AlterTable alterTable = (AlterTable) expr; String tableName = alterTable.getTableName(); assertTrue("table '" + tableName + "' existence check", client.existTable(tableName)); client.updateQuery(compiled); if (isLocalTable) { createdTableGlobalSet.remove(tableName); } } else { assertTrue(ddlFilePath + " is not a Create or Drop Table statement", false); } } return createdTableNames; } /** * Replace format strings by a given parameters. * * @param template * @param dataFileName The data file name to replace <code>${table.path}</code> * @param args The list argument to replace each corresponding format string ${i}. ${i} uses zero-based index. * @return A string compiled */ private String compileTemplate(String template, @Nullable String dataFileName, @Nullable String ... args) { String result; if (dataFileName != null) { result = template.replace("${table.path}", "\'" + dataFileName + "'"); } else { result = template; } if (args != null) { for (int i = 0; i < args.length; i++) { result = result.replace("${" + i + "}", args[i]); } } return result; } }