Package com.github.youtube.vitess.jdbc.vtocc

Source Code of com.github.youtube.vitess.jdbc.vtocc.VtoccIntegrationTest$VtoccIntegrationSqlCasesTester

package com.github.youtube.vitess.jdbc.vtocc;

import com.google.common.collect.Maps;
import com.google.common.io.CharStreams;
import com.google.common.io.Files;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;

import com.github.youtube.vitess.jdbc.bson.Driver;

import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@RunWith(JUnit4.class)
public class VtoccIntegrationTest {

  public static final String VTOCC_PORT = System.getProperty("vtocc.port");
  public static final String VTROOT = Files.simplifyPath(System.getProperty("VTROOT"));
  public static final String VTDATAROOT = Files.simplifyPath(System.getProperty("VTDATA"));

  private static final String TARGET_TESTS_OUTPUT_DIRECTORY =
      VtoccIntegrationTest.class.getResource("/").getPath();
  private static Process vtoccProcess;

  @BeforeClass
  public static void setupJdbcDriver() throws Exception {
    DriverManager.registerDriver(new Driver());
  }

  @BeforeClass
  public static void setupVtoccServer() throws IOException, InterruptedException {
    ProcessBuilder pb = new ProcessBuilder(VTROOT + "/bin/vtocc",
        "-port", VTOCC_PORT,
        "-alsologtostderr",
        "-log_dir", VTDATAROOT + "/tmp",
        "-db-config-app-charset", "utf8",
        "-db-config-app-dbname", "vt_test_keyspace",
        "-db-config-app-host", "localhost",
        "-db-config-app-unixsocket", VTDATAROOT + "/mysql.sock",
        "-db-config-app-uname", "vt_dba",
        "-db-config-app-keyspace", "test_keyspace",
        "-db-config-app-shard", "0");
    pb.redirectOutput(Redirect.INHERIT);
    pb.redirectError(Redirect.INHERIT);
    vtoccProcess = pb.start();
    Thread.sleep(TimeUnit.SECONDS.toMillis(10));
  }

  @AfterClass
  public static void teardownVtoccServer() {
    vtoccProcess.destroy();
  }


  @Test
  public void testIntegrationTests() throws Exception {
    try (Connection connection = DriverManager.getConnection(
        "jdbc:vtocc://localhost:" + VTOCC_PORT + "/test_keyspace/0")) {

      VtoccIntegrationSqlCasesTester tester = new VtoccIntegrationSqlCasesTester(
          connection);

      tester.initializeDatabaseState();
      tester.verifyDatabaseState();
    }
  }

  /**
   * Reuses tests from python implementation of Vtocc.
   *
   * This allows us to use always up to date test suite of immense size with ease.
   *
   * Parses and executes tests generated in {@code sql_tests.json} and {@code test_schema.sql}.
   * Original files are in {@code $VTTOP/test/test_data/test_schema.sql} and in {@code
   * $VTTOP/test/queryservice_tests/nocache_cases.py}.
   */
  public static class VtoccIntegrationSqlCasesTester {

    public static final String TEST_SCHEMA_SQL = "test_schema.sql";
    public static final String SQL_TESTS_JSON = "sql_tests.json";
    private static final Logger logger = LoggerFactory
        .getLogger(VtoccIntegrationSqlCasesTester.class);
    private final Connection connection;

    public VtoccIntegrationSqlCasesTester(Connection connection) {
      this.connection = connection;
    }

    /**
     * Reads and executes {@code testdata/test_shcema.sql} on a provided connection.
     */
    public void initializeDatabaseState()
        throws SQLException, IOException {
      // workaround to support mysql driver which initializes in autocommit=true state
      connection.setAutoCommit(false);

      File schemaFile = getFile(TEST_SCHEMA_SQL);
      logger.info("Initializing database: " + schemaFile.getPath());
      List<String> sqlStatements = CharStreams.readLines(
          Files.newReader(schemaFile, StandardCharsets.UTF_8));
      for (String sql : sqlStatements) {
        executeSql(connection, sql);
      }
    }

    /**
     * Reads and executes tests in {@code testdata/sql_tests.json} on a provided connection.
     *
     * See {@link #verifySqlTestsArray(Connection, JsonArray)} for format description.
     */
    public void verifyDatabaseState()
        throws SQLException, IOException {
      // workaround to support mysql driver which initializes in autocommit=true state
      connection.setAutoCommit(false);

      File sqlTestsFile = getFile(SQL_TESTS_JSON);
      logger.info("Verifying database: " + sqlTestsFile.getPath());
      JsonArray testsArray = new JsonParser()
          .parse(Files.newReader(sqlTestsFile, StandardCharsets.UTF_8)).getAsJsonArray();
      verifySqlTestsArray(connection, testsArray);

      // verify that tests are failing/passing at least sometimes
      verifySqlTest(connection, new JsonParser().parse(
          "{'sql':'select id from vtocc_b limit 1', 'result':[[1]]}").getAsJsonObject());
      boolean testPassed = false;
      try {
        verifySqlTest(connection, new JsonParser().parse(
            "{'sql':'select id from vtocc_b limit 1', 'result':[[42]]}").getAsJsonObject());
        testPassed = true;
      } catch (AssertionError ignored) {
        // exception is expected to be thrown
      } finally {
        Assert.assertFalse(testPassed);
      }
    }

    /**
     * Helper to load data file from the same package.
     */
    private File getFile(String fileName) {
      return new File(TARGET_TESTS_OUTPUT_DIRECTORY + "/" + fileName);
    }

    /**
     * Verifies all the tests in array provided.
     *
     * JSON formats supported: Normal test, just like in {@link #verifySqlTest(Connection,
     * JsonObject)} or {@code "sqls_and_cases"} which is an array of normal tests.
     * <pre>
     * {
     *   "doc": "bind values",
     *   "sqls_and_cases": [
     *       "begin",
     *       {
     *           "sql": ...
     *           ...
     *       },
     *       "commit",
     *       ...
     * }
     * </pre>
     */
    private void verifySqlTestsArray(Connection connection, JsonArray testsArray)
        throws SQLException {
      for (JsonElement jsonElement : testsArray) {
        JsonObject testObject = jsonElement.getAsJsonObject();
        if (testObject.has("sql")) {
          verifySqlTest(connection, testObject);
        } else if (testObject.has("sqls_and_cases")) {
          JsonArray sqlsAndCasesArray = testObject.getAsJsonArray("sqls_and_cases");
          for (JsonElement sqlAndCaseElement : sqlsAndCasesArray) {
            if (sqlAndCaseElement.isJsonObject()) {
              verifySqlTest(connection, sqlAndCaseElement.getAsJsonObject());
            } else if (sqlAndCaseElement.isJsonPrimitive()
                && sqlAndCaseElement.getAsJsonPrimitive().isString()) {
              executeSql(connection, sqlAndCaseElement.getAsString());
            } else {
              throw new IllegalArgumentException("Can not parse subtest: " + sqlAndCaseElement);
            }
          }
        } else {
          throw new IllegalArgumentException("Can not parse type of test: " + testObject);
        }
      }
    }

    /**
     * Runs specified SQL statement against database and checks its result.
     *
     * JSON format supported:
     * <pre>
     * {
     *   "sql": "select eid, id from vtocc_a union select eid, id from vtocc_b",
     *   "bindings": {
     *       "a": 1
     *   },
     *   "result": [
     *       [
     *           1,
     *           1
     *       ],
     *       [
     *           1,
     *           2
     *       ]
     *   ],
     *   ...
     * }
     * </pre>
     */
    private void verifySqlTest(Connection connection, JsonObject testObject)
        throws SQLException {
      String sql = testObject.get("sql").getAsString();
      logger.debug("Testing: {}", sql);
      switch (sql) {
        case "commit":
          connection.commit();
          return;
        case "begin":
          // we're always in a transaction
          return;
        case "rollback":
          throw new UnsupportedOperationException("Rollback is not supported in tests");
      }

      // Our driver only supports '?' as a param placeholder, all params in tests are named
      Map<String, Integer> bindingIndexes = Maps.newHashMap();
      Matcher bindingMatcher = Pattern.compile(":(\\w+)").matcher(sql);
      int bindingIndex = 0;
      while (bindingMatcher.find()) {
        bindingIndex++;
        bindingIndexes.put(bindingMatcher.group(1), bindingIndex);
      }
      bindingIndexes.put("v1", 1);
      bindingIndexes.put("v2", 2);
      bindingIndexes.put("v3", 3);
      bindingIndexes.put("v4", 4);
      bindingIndexes.put("v5", 5);
      sql = sql.replaceAll(":(\\w+)", "?");
      PreparedStatement preparedStatement = connection.prepareStatement(sql);

      // Bind parameters as an indexed params, not named ones
      if ((testObject.has("bindings") && testObject.get("bindings").isJsonObject())) {
        JsonObject bindingsObject = testObject.getAsJsonObject("bindings");
        for (Entry<String, JsonElement> keyValueElement : bindingsObject.entrySet()) {
          Integer parameterIndex = bindingIndexes.get(keyValueElement.getKey());
          Assert.assertNotNull(
              "Can not find variable binding for " + keyValueElement.getKey(), parameterIndex);
          Object value = parseJsonValue(keyValueElement.getValue());
          preparedStatement.setObject(parameterIndex, value);
          bindingIndex++;
        }
      }

      try {
        if (!testObject.has("result") || testObject.get("result").isJsonNull()) {
          // This is DML or INSERT statement
          preparedStatement.executeUpdate();
          return;
        }

        ResultSet resultSet = preparedStatement.executeQuery();

        // And verify that result is the one specified in the test
        JsonArray resultArrayArray = testObject.getAsJsonArray("result");
        int rowIndex = 0;
        for (JsonElement resultArrayElement : resultArrayArray) {
          rowIndex++;
          Assert.assertTrue("Not enough results, row: " + rowIndex, resultSet.next());
          JsonArray resultArray = resultArrayElement.getAsJsonArray();
          int columnIndex = 0;
          for (JsonElement resultElement : resultArray) {
            columnIndex++;
            Object expected = parseJsonValue(resultElement);
            Object actual = resultSet.getObject(columnIndex);
            try {
              if (expected instanceof Number && actual instanceof Number) {
                // Special case to compare all types of Numbers
                Assert.assertEquals(expected.toString(), actual.toString());
              } else if (expected instanceof String && actual instanceof byte[]) {
                // Byte arrays are specified as strings in a JSON
                Assert.assertEquals(expected, new String((byte[]) actual, StandardCharsets.UTF_8));
                Assert.assertArrayEquals((byte[]) actual, resultSet.getBytes(columnIndex));
              } else if (expected instanceof Number && actual instanceof Date) {
                // Dates are specified as only year, sometimes
                Assert.assertEquals(Date.valueOf(expected.toString() + "-01-01"), actual);
                Assert.assertEquals(actual, resultSet.getDate(columnIndex));
              } else if (expected instanceof Boolean && actual instanceof String) {
                // Driver returns Strings instead of Booleans for some reason
                Assert.assertEquals(expected, ((String) actual).charAt(0) != '0');
                Assert.assertEquals(expected, resultSet.getBoolean(columnIndex));
              } else {
                Assert.assertEquals(expected, actual);
              }
            } catch (AssertionError e) {
              throw new AssertionError("Expected JSON: " + resultElement
                  + "\nExpected value: " + expected + " (" + expected.getClass().getSimpleName()
                  + ")"
                  + "\nActual: '" + actual + " (" + actual.getClass().getSimpleName() + ")"
                  + "\nRow/Column: " + rowIndex + "/" + columnIndex, e);
            }
          }
          Assert.assertEquals(columnIndex, resultSet.getMetaData().getColumnCount());
        }
        Assert.assertFalse("Too many results", resultSet.next());
      } catch (AssertionError | RuntimeException | SQLException e) {
        throw new AssertionError("Test failed: " + testObject, e);
      }
    }

    /**
     * Executes SQL including transaction language: begin, commit and rollback.
     */
    private void executeSql(Connection connection, String sql) throws SQLException {
      logger.debug("Executing: {}", sql);
      switch (sql) {
        case "commit":
          connection.commit();
          return;
        case "begin":
          // we're always in a transaction
          return;
        case "rollback":
          throw new UnsupportedOperationException("Rollback is not supported in tests");
      }
      connection.prepareStatement(sql).execute();
    }

    /**
     * Parse expected value out of JSON. Turns out that Vtocc test cases contain peculiar values
     * that should be treated as {@link java.sql.Timestamp} as an example. Therefore no standard
     * library is good enough for us.
     *
     * Supported value formats:
     * <pre>
     * {'decimal':'1.00'} - BigDecimal
     * {'date':'2012-01-01'} - java.sql.Date
     * {'datetime':'2012-01-01 00:00'} - java.sql.Timestamp
     * {'timedelta':'00:00'} - java.sql.Time
     * 99 - Byte
     * 9999 - Short
     * 999999999 - Integer
     * 999999999999999999 - Long
     * 9999999999999999999... - BigDecimal
     * 1.0 - Double
     * "\u0001" - True
     * "\u0000" - False
     * "..." - String
     * </pre>
     */
    private Object parseJsonValue(JsonElement element) {
      if (element.isJsonObject()) {
        JsonObject object = element.getAsJsonObject();
        try {
          if (object.has("decimal")) {
            return new BigDecimal(object.get("decimal").getAsString());
          } else if (object.has("date")) {
            return Date.valueOf(object.get("date").getAsString());
          } else if (object.has("datetime")) {
            return Timestamp.valueOf(object.get("datetime").getAsString());
          } else if (object.has("timedelta")) {
            return Time.valueOf(object.get("timedelta").getAsString());
          } else {
            throw new IllegalArgumentException("Can not parse value object: " + object);
          }
        } catch (RuntimeException e) {
          throw new IllegalArgumentException("Can not parse value object: " + object, e);
        }
      }
      if (!element.isJsonPrimitive()) {
        throw new IllegalArgumentException("Can not parse value: " + element);
      }
      JsonPrimitive primitive = element.getAsJsonPrimitive();
      String resultString = primitive.getAsString();
      if (primitive.isNumber() && !resultString.contains(".")) {
        if (resultString.length() <= 2) {
          return primitive.getAsByte();
        } else if (resultString.length() <= 4) {
          return primitive.getAsShort();
        } else if (resultString.length() <= 9) {
          return primitive.getAsInt();
        } else if (resultString.length() <= 18) {
          return primitive.getAsLong();
        } else {
          // BigInteger is not supported by PreparedStatement
          return primitive.getAsBigDecimal();
        }
      } else if (primitive.isNumber() && resultString.contains(".")) {
        return primitive.getAsDouble();
      } else if (primitive.isString() && resultString.equals("\u0000")) {
        return false;
      } else if (primitive.isString() && resultString.equals("\u0001")) {
        return true;
      } else if (primitive.isString()) {
        return resultString;
      } else {
        throw new IllegalArgumentException("Can not parse value primitive: " + primitive);
      }
    }
  }

}
TOP

Related Classes of com.github.youtube.vitess.jdbc.vtocc.VtoccIntegrationTest$VtoccIntegrationSqlCasesTester

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.