/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ package org.voltdb.regressionsuites; import java.io.IOException; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.net.ConnectException; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.Random; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import org.apache.commons.lang3.StringUtils; import org.voltcore.utils.ssl.SSLConfiguration; import org.voltdb.CatalogContext; import org.voltdb.VoltDB; import org.voltdb.VoltTable; import org.voltdb.VoltType; import org.voltdb.catalog.Catalog; import org.voltdb.catalog.CatalogDiffEngine; import org.voltdb.client.Client; import org.voltdb.client.ClientAuthScheme; import org.voltdb.client.ClientConfig; import org.voltdb.client.ClientConfigForTest; import org.voltdb.client.ClientFactory; import org.voltdb.client.ClientResponse; import org.voltdb.client.ConnectionUtil; import org.voltdb.client.NoConnectionsException; import org.voltdb.client.ProcCallException; import org.voltdb.common.Constants; import org.voltdb.types.GeographyPointValue; import org.voltdb.types.GeographyValue; import org.voltdb.types.TimestampType; import org.voltdb.types.VoltDecimalHelper; import org.voltdb.utils.CatalogUtil; import org.voltdb.utils.Encoder; import org.voltdb.utils.InMemoryJarfile; import com.google_voltpatches.common.net.HostAndPort; import junit.framework.TestCase; /** * Base class for a set of JUnit tests that perform regression tests * on a running VoltDB server. It is assumed that all tests will access * a particular VoltDB server to do their work and check the output of * any procedures called. The main feature of this class is that the * backend instance of VoltDB is very flexible and the tests can be run * on multiple instances of VoltDB to test different VoltDB topologies. * */ public class RegressionSuite extends TestCase { protected final static double GEOGRAPHY_EPSILON = 1.0e-13; protected static int m_verboseDiagnosticRowCap = 40; protected VoltServerConfig m_config; protected String m_username = "default"; protected String m_password = "password"; private final ArrayList<Client> m_clients = new ArrayList<>(); private final ArrayList<SocketChannel> m_clientChannels = new ArrayList<>(); protected final String m_methodName; // If the current RegressionSuite instance is the last one in the current VoltServerConfig, // shutdown the cluster completely after finishing the test. protected boolean m_completeShutdown; /** * Trivial constructor that passes parameter on to superclass. * @param name The name of the method to run as a test. (JUnit magic) */ public RegressionSuite(final String name) { super(name); m_methodName = name; VoltServerConfig.setInstanceSet(new HashSet<>()); m_completeShutdown = false; } /** * JUnit special method called to setup the test. This instance will start * the VoltDB server using the VoltServerConfig instance provided. */ @Override public void setUp() throws Exception { //New tests means a new server thread that hasn't done a restore m_config.setCallingMethodName(m_methodName); m_config.startUp(true); } private static Catalog getCurrentCatalog() { CatalogContext context = VoltDB.instance().getCatalogContext(); if (context == null) { return null; } InMemoryJarfile currentCatalogJar = context.getCatalogJar(); String serializedCatalogString = CatalogUtil.getSerializedCatalogStringFromJar(currentCatalogJar); assertNotNull(serializedCatalogString); Catalog c = new Catalog(); c.execute(serializedCatalogString); return c; } /** * JUnit special method called to shutdown the test. This instance will * stop the VoltDB server using the VoltServerConfig instance provided. */ @Override public void tearDown() throws Exception { if (m_completeShutdown) { m_config.shutDown(); } else { Catalog currentCataog = getCurrentCatalog(); if (currentCataog != null) { CatalogDiffEngine diff = new CatalogDiffEngine(m_config.getInitialCatalog(), currentCataog); // All catalog changes will have a changed "set /clusters#cluster/databases#database schema" command. // If the diff command only has this line, it means something is changed first but restored later. // We will ignore this case. if (diff.commands().split("\n").length > 1) { fail("Catalog changed in test " + getName() + " while the regression suite optimization is on: \n" + diff.getDescriptionOfChanges(false)); } } Client client = getClient(); VoltTable tableList = client.callProcedure("@SystemCatalog", "TABLES").getResults()[0]; ArrayList<String> tableNames = new ArrayList<>(tableList.getRowCount()); int tableNameColIdx = tableList.getColumnIndex("TABLE_NAME"); int tableTypeColIdx = tableList.getColumnIndex("TABLE_TYPE"); while (tableList.advanceRow()) { String tableType = tableList.getString(tableTypeColIdx); if (! tableType.equalsIgnoreCase("EXPORT")) { tableNames.add(tableList.getString(tableNameColIdx)); } } for (String tableName : tableNames) { try { client.callProcedure("@AdHoc", "DELETE FROM " + tableName); } catch (ProcCallException pce) { if (! pce.getMessage().contains("Illegal to modify a materialized view.")) { fail("Hit an exception when cleaning up tables between tests: " + pce.getMessage()); } } } client.drain(); } for (final Client c : m_clients) { c.close(); } synchronized (m_clientChannels) { for (final SocketChannel sc : m_clientChannels) { try { ConnectionUtil.closeConnection(sc); } catch (final IOException e) { e.printStackTrace(); } } m_clientChannels.clear(); } m_clients.clear(); } /** * @return Is the underlying instance of VoltDB running HSQL? */ public boolean isHSQL() { return m_config.isHSQL(); } /** * @return The number of logical partitions in this configuration */ public int getLogicalPartitionCount() { return m_config.getLogicalPartitionCount(); } /** * @return Is the underlying instance of VoltDB running Valgrind with the IPC client? */ public boolean isValgrind() { return m_config.isValgrind(); } public boolean isLocalCluster() { return m_config instanceof LocalCluster; } public boolean isDebug() { return m_config.isDebug(); } /** * @return a reference to the associated VoltServerConfig */ public final VoltServerConfig getServerConfig() { return m_config; } public Client getAdminClient() throws IOException { return getClient(1000 * 60 * 10, ClientAuthScheme.HASH_SHA256, true); // 10 minute default } public Client getClient() throws IOException { return getClient(1000 * 60 * 10, ClientAuthScheme.HASH_SHA256); // 10 minute default } public Client getClient(ClientAuthScheme scheme) throws IOException { return getClient(1000 * 60 * 10, scheme); // 10 minute default } public Client getClientToHostId(int hostId) throws IOException { return getClientToHostId(hostId, 1000 * 60 * 10); // 10 minute default } public Client getFullyConnectedClient() throws IOException { return getFullyConnectedClient(1000 * 60 * 10); // 10 minute default } /** * Get a VoltClient instance connected to the server driven by the * VoltServerConfig instance. Just pick from the list of listeners * randomly. * * Only uses the time * * @return A VoltClient instance connected to the server driven by the * VoltServerConfig instance. */ public Client getClient(long timeout) throws IOException { return getClient(timeout, ClientAuthScheme.HASH_SHA256); } /** * Get a VoltClient instance connected to the server driven by the * VoltServerConfig instance. Just pick from the list of listeners * randomly. * * Only uses the time * * @return A VoltClient instance connected to the server driven by the * VoltServerConfig instance. */ public Client getClient(long timeout, ClientAuthScheme scheme) throws IOException { return getClient(timeout, scheme, false); } public Client getClient(long timeout, ClientAuthScheme scheme, boolean useAdmin) throws IOException { final Random r = new Random(); String listener = null; if (useAdmin) { listener = m_config.getAdminAddress(r.nextInt(m_config.getListenerCount())); } else { listener = m_config.getListenerAddress(r.nextInt(m_config.getListenerCount())); } ClientConfig config = new ClientConfigForTest(m_username, m_password, scheme); config.setConnectionResponseTimeout(timeout); config.setProcedureCallTimeout(timeout); final Client client = ClientFactory.createClient(config); // Use the port generated by LocalCluster if applicable try { client.createConnection(listener); } // retry once catch (ConnectException e) { if (useAdmin) { listener = m_config.getAdminAddress(r.nextInt(m_config.getListenerCount())); } else { listener = m_config.getListenerAddress(r.nextInt(m_config.getListenerCount())); } client.createConnection(listener); } m_clients.add(client); return client; } /** * Get a VoltClient instance connected to the server driven by the * VoltServerConfig instance. Just pick from the list of listeners * randomly. * * Only uses the time * * @return A VoltClient instance connected to the server driven by the * VoltServerConfig instance. */ public Client getClientSha1(long timeout) throws IOException { final List<String> listeners = m_config.getListenerAddresses(); final Random r = new Random(); String listener = listeners.get(r.nextInt(listeners.size())); ClientConfig config = new ClientConfigForTest(m_username, m_password, ClientAuthScheme.HASH_SHA1); config.setConnectionResponseTimeout(timeout); config.setProcedureCallTimeout(timeout); final Client client = ClientFactory.createClient(config); // Use the port generated by LocalCluster if applicable try { client.createConnection(listener); } // retry once catch (ConnectException e) { listener = listeners.get(r.nextInt(listeners.size())); client.createConnection(listener); } m_clients.add(client); return client; } /** * Get a VoltClient instance connected to a specific server driven by the * VoltServerConfig instance. Find the server by the config's HostId. * * @return A VoltClient instance connected to the server driven by the * VoltServerConfig instance. */ public Client getClientToHostId(int hostId, long timeout) throws IOException { final String listener = m_config.getListenerAddress(hostId); ClientConfig config = new ClientConfigForTest(m_username, m_password); config.setConnectionResponseTimeout(timeout); config.setProcedureCallTimeout(timeout); final Client client = ClientFactory.createClient(config); try { client.createConnection(listener); } // retry once catch (ConnectException e) { client.createConnection(listener); } m_clients.add(client); return client; } public Client getFullyConnectedClient(long timeout) throws IOException { final List<String> listeners = m_config.getListenerAddresses(); final Random r = new Random(); ClientConfig config = new ClientConfigForTest(m_username, m_password); config.setConnectionResponseTimeout(timeout); config.setProcedureCallTimeout(timeout); final Client client = ClientFactory.createClient(config); for (String listener : listeners) { // Use the port generated by LocalCluster if applicable try { client.createConnection(listener); } // retry once catch (ConnectException e) { listener = listeners.get(r.nextInt(listeners.size())); client.createConnection(listener); } } m_clients.add(client); return client; } /** * Release a client instance and any resources associated with it */ public void releaseClient(Client c) throws IOException, InterruptedException { boolean removed = m_clients.remove(c); assert(removed); c.close(); } /** * Get a SocketChannel that is an authenticated connection to a server driven by the * VoltServerConfig instance. Just pick from the list of listeners * randomly. * * @return A SocketChannel that is already authenticated with the server */ public SocketChannel getClientChannel() throws IOException { return getClientChannel(false); } public SocketChannel getClientChannel(final boolean noTearDown) throws IOException { final List<String> listeners = m_config.getListenerAddresses(); final Random r = new Random(); final String listener = listeners.get(r.nextInt(listeners.size())); byte[] hashedPassword = ConnectionUtil.getHashedPassword(m_password); HostAndPort hNp = HostAndPort.fromString(listener); int port = Constants.DEFAULT_PORT; if (hNp.hasPort()) { port = hNp.getPort(); } SSLEngine sslEngine = null; boolean sslEnabled = Boolean.valueOf(System.getenv("ENABLE_SSL") == null ? Boolean.toString(Boolean.getBoolean("ENABLE_SSL")) : System.getenv("ENABLE_SSL")); if (sslEnabled) { SSLContext sslContext = SSLConfiguration.createSslContext(new SSLConfiguration.SslConfig()); sslEngine = sslContext.createSSLEngine("client", port); sslEngine.setUseClientMode(true); } final SocketChannel channel = (SocketChannel) ConnectionUtil.getAuthenticatedConnection( hNp.getHostText(), m_username, hashedPassword, port, null, ClientAuthScheme.getByUnencodedLength(hashedPassword.length), sslEngine)[0]; channel.configureBlocking(true); if (!noTearDown) { synchronized (m_clientChannels) { m_clientChannels.add(channel); } } return channel; } /** * Protected method used by MultiConfigSuiteBuilder to set the VoltServerConfig * instance a particular test will run with. * * @param config An instance of VoltServerConfig to run tests with. */ void setConfig(final VoltServerConfig config) { m_config = config; } @Override public String getName() { // munge the test name with the VoltServerConfig instance name return super.getName() + "-" + m_config.getName(); } /** * Return appropriate port for hostId. Deal with LocalCluster providing non-default ports. * @param hostId zero-based host id * @return port number */ public int port(int hostId) { return isLocalCluster() ? ((LocalCluster)m_config).port(hostId) : VoltDB.DEFAULT_PORT+hostId; } /** * Return appropriate admin port for hostId. Deal with LocalCluster providing non-default ports. * @param hostId zero-based host id * @return admin port number */ public int adminPort(int hostId) { return isLocalCluster() ? ((LocalCluster)m_config).adminPort(hostId) : VoltDB.DEFAULT_ADMIN_PORT+hostId; } /** * Return appropriate internal port for hostId. Deal with LocalCluster providing non-default ports. * @param hostId zero-based host id * @return internal port number */ public int internalPort(int hostId) { return isLocalCluster() ? ((LocalCluster)m_config).internalPort(hostId) : org.voltcore.common.Constants.DEFAULT_INTERNAL_PORT+hostId; } static protected void validateDMLTupleCount(Client c, String sql, long modifiedTupleCount) throws NoConnectionsException, IOException, ProcCallException { VoltTable vt = c.callProcedure("@AdHoc", sql).getResults()[0]; validateTableOfLongs(sql, vt, new long[][] {{modifiedTupleCount}}); } static protected void validateTableOfLongs(Client c, String sql, long[][] expected) throws NoConnectionsException, IOException, ProcCallException { VoltTable vt = c.callProcedure("@AdHoc", sql).getResults()[0]; validateTableOfLongs(sql, vt, expected); } static protected void validateTableOfScalarLongs(VoltTable vt, long[] expected) { assertNotNull(expected); assertEquals("Different number of rows! ", expected.length, vt.getRowCount()); int len = expected.length; for (int i=0; i < len; i++) { validateRowOfLongs(vt, new long[] {expected[i]}); } } static protected void validateTableOfScalarLongs(Client client, String sql, long[] expected) throws NoConnectionsException, IOException, ProcCallException { assertNotNull(expected); VoltTable vt = client.callProcedure("@AdHoc", sql).getResults()[0]; validateTableOfScalarLongs(vt, expected); } static protected void validateTableOfScalarDecimals(Client client, String sql, BigDecimal[] expected) throws NoConnectionsException, IOException, ProcCallException { assertNotNull(expected); VoltTable vt = client.callProcedure("@AdHoc", sql).getResults()[0]; assertEquals("Different number of rows! ", expected.length, vt.getRowCount()); int len = expected.length; for (int i=0; i < len; i++) { assertTrue(vt.advanceRow()); String message = "at column 0,"; BigDecimal actual = new BigDecimal(-10000000); try { actual = vt.getDecimalAsBigDecimal(i); } catch (IllegalArgumentException ex) { ex.printStackTrace(); fail(message); } assertEquals(message, expected[i], actual); } } private static void dumpExpectedLongs(long[][] expected) { System.out.println("row count:" + expected.length); for (long[] row : expected) { String prefix = "{ "; for (long value : row) { System.out.print(prefix + value); prefix = ", "; } System.out.println(" }"); } } private static void validateTableOfLongs(String messagePrefix, VoltTable vt, long[][] expected) { assertNotNull(expected); if (expected.length != vt.getRowCount()) { if (vt.getRowCount() < m_verboseDiagnosticRowCap) { System.out.println("Diagnostic dump of unexpected result for " + messagePrefix + " : " + vt); System.out.println("VS. expected : "); dumpExpectedLongs(expected); } //* enable and set breakpoint to debug multiple row count mismatches and continue */ return; } assertEquals(messagePrefix + " returned wrong number of rows. ", expected.length, vt.getRowCount()); int len = expected.length; for (int i=0; i < len; i++) { validateRowOfLongs(messagePrefix + " at row " + (i+1) + ", ", vt, expected[i]); } } protected void validateRowCount(Client client, String query, int expected) throws NoConnectionsException, IOException, ProcCallException { VoltTable result = client.callProcedure("@AdHoc", query).getResults()[0]; int actual = result.getRowCount(); assertEquals("Wrong row count from query '" + query + "'", expected, actual); } public static void validateTableOfLongs(VoltTable vt, long[][] expected) { validateTableOfLongs("", vt, expected); } static protected void validateRowOfLongs(String messagePrefix, VoltTable vt, long [] expected) { int len = expected.length; assertTrue(vt.advanceRow()); for (int i=0; i < len; i++) { String message = messagePrefix + "at column " + (i+1) + ", "; long actual = -10000000; // ENG-4295: hsql bug: HSQLBackend sometimes returns wrong column type. try { actual = vt.getLong(i); } catch (IllegalArgumentException ex) { try { actual = (long) vt.getDouble(i); } catch (IllegalArgumentException newEx) { try { actual = vt.getTimestampAsLong(i); } catch (IllegalArgumentException exTm) { try { actual = vt.getDecimalAsBigDecimal(i).longValueExact(); } catch (IllegalArgumentException newerEx) { newerEx.printStackTrace(); fail(message); } } catch (ArithmeticException newestEx) { newestEx.printStackTrace(); fail(message); } } } // Long.MIN_VALUE is like a NULL if (expected[i] != Long.MIN_VALUE) { assertEquals(message, expected[i], actual); } else { VoltType type = vt.getColumnType(i); assertEquals(message + "expected null: ", Long.parseLong(type.getNullValue().toString()), actual); } } } static protected void validateRowOfLongs(VoltTable vt, long [] expected) { validateRowOfLongs("", vt, expected); } /** * Given a two dimensional array, randomly permute the rows, but * leave the columns alone. This is used to generate test cases for kinds * of sorts. * * @param input */ static protected <T> T[][] shuffleArray(T [][] input) { T[][] output = input.clone(); Integer [] indices = new Integer[input.length]; for (int idx = 0; idx < indices.length; idx += 1) { indices[idx] = Integer.valueOf(idx); } List<Integer> permutation = Arrays.asList(indices); Collections.shuffle(permutation); for (int idx = 0; idx < input.length; idx += 1) { output[idx] = input[permutation.get(idx)]; } return output; } static protected void validateTableColumnOfScalarLong(VoltTable vt, int col, long[] expected) { assertNotNull(expected); assertEquals(expected.length, vt.getRowCount()); int len = expected.length; for (int i=0; i < len; i++) { assertTrue(vt.advanceRow()); long actual = vt.getLong(col); if (expected[i] == Long.MIN_VALUE) { assertTrue(vt.wasNull()); assertEquals(null, actual); } else { assertEquals(expected[i], actual); } } } static protected void validateTableColumnOfScalarVarchar(Client client, String sql, String[] expected) throws NoConnectionsException, IOException, ProcCallException { VoltTable vt = client.callProcedure("@AdHoc", sql).getResults()[0]; validateTableColumnOfScalarVarchar(vt, 0, expected); } static protected void validateTableColumnOfScalarVarchar(VoltTable vt, String[] expected) { validateTableColumnOfScalarVarchar(vt, 0, expected); } static protected void validateTableColumnOfScalarVarchar(VoltTable vt, int col, String[] expected) { assertNotNull(expected); assertEquals(expected.length, vt.getRowCount()); int len = expected.length; for (int i=0; i < len; i++) { assertTrue(vt.advanceRow()); if (expected[i] == null) { String actual = vt.getString(col); assertTrue(vt.wasNull()); assertEquals(null, actual); } else { assertEquals(expected[i], vt.getString(col)); } } } static protected void validateTableColumnOfScalarVarbinary(Client client, String sql, String[] expected) throws NoConnectionsException, IOException, ProcCallException { VoltTable vt = client.callProcedure("@AdHoc", sql).getResults()[0]; validateTableColumnOfScalarVarbinary(vt, 0, expected); } static private void validateTableColumnOfScalarVarbinary(VoltTable vt, int col, String[] expected) { assertNotNull(expected); assertEquals(expected.length, vt.getRowCount()); int len = expected.length; for (int i=0; i < len; i++) { assertTrue(vt.advanceRow()); byte[] actual = vt.getVarbinary(col); if (expected[i] == null) { assertTrue(vt.wasNull()); assertEquals(null, actual); } else { assertEquals(expected[i], Encoder.hexEncode(actual)); } } } static protected void validateTableColumnOfScalarFloat(Client client, String sql, double[] expected) throws NoConnectionsException, IOException, ProcCallException { VoltTable vt = client.callProcedure("@AdHoc", sql).getResults()[0]; validateTableColumnOfScalarFloat(vt, 0, expected); } static protected void validateTableColumnOfScalarFloat(VoltTable vt, int col, double[] expected) { assertNotNull(expected); assertEquals(expected.length, vt.getRowCount()); int len = expected.length; for (int i=0; i < len; i++) { assertTrue(vt.advanceRow()); double actual = vt.getDouble(col); if (expected[i] == Double.MIN_VALUE) { assertTrue(vt.wasNull()); assertEquals(null, actual); } else { assertEquals(expected[i], actual, 0.00001); } } } private void validateRowOfDecimal(VoltTable vt, BigDecimal [] expected) { int len = expected.length; assertTrue(vt.advanceRow()); for (int i=0; i < len; i++) { BigDecimal actual = null; try { actual = vt.getDecimalAsBigDecimal(i); } catch (IllegalArgumentException ex) { ex.printStackTrace(); fail(); } if (expected[i] != null) { assertNotSame(null, actual); assertEquals(expected[i], actual); } else { if (isHSQL()) { // We don't actually use this with // HSQL. So, just assert failure here. fail("HSQL is not used to test the Volt DECIMAL type."); } else { assertTrue(vt.wasNull()); } } } } protected void validateTableOfDecimal(VoltTable vt, BigDecimal[][] expected) { assertNotNull(expected); assertEquals(expected.length, vt.getRowCount()); int len = expected.length; for (int i=0; i < len; i++) { validateRowOfDecimal(vt, expected[i]); } } protected static int m_defaultScale = 12; protected static String m_roundingEnabledProperty = "BIGDECIMAL_ROUND"; protected static String m_roundingModeProperty = "BIGDECIMAL_ROUND_POLICY"; protected static String m_defaultRoundingEnablement = "true"; protected static String m_defaultRoundingMode = "HALF_UP"; protected static String getRoundingString(String label) { return String.format("%sRounding %senabled, mode is %s", label == null ? (label + ": ") : "", VoltDecimalHelper.isRoundingEnabled() ? "is " : "is *NOT* ", VoltDecimalHelper.getRoundingMode().toString()); } /* * This little helper function converts a string to * a decimal, and, maybe, rounds it to the Volt default scale * using the given mode. If roundingEnabled is false, no * rounding is done. */ protected static final BigDecimal roundDecimalValue(String decimalValueString, boolean roundingEnabled, RoundingMode mode) { BigDecimal bd = new BigDecimal(decimalValueString); if (!roundingEnabled) { return bd; } int precision = bd.precision(); int scale = bd.scale(); int lostScale = scale - m_defaultScale ; if (lostScale <= 0) { return bd; } int newPrecision = precision - lostScale; MathContext mc = new MathContext(newPrecision, mode); BigDecimal nbd = bd.round(mc); assertTrue(nbd.scale() <= m_defaultScale); if (nbd.scale() != m_defaultScale) { nbd = nbd.setScale(m_defaultScale); } assertEquals(getRoundingString("Decimal Scale setting failure"), m_defaultScale, nbd.scale()); return nbd; } protected void validateTableOfDecimal(Client c, String sql, BigDecimal[][] expected) throws Exception, IOException, ProcCallException { assertNotNull(expected); VoltTable vt = c.callProcedure("@AdHoc", sql).getResults()[0]; validateTableOfDecimal(vt, expected); } static protected void validateTableColumnOfScalarDecimal(Client client, String sql, BigDecimal[] expected) throws NoConnectionsException, IOException, ProcCallException { VoltTable vt = client.callProcedure("@AdHoc", sql).getResults()[0]; validateTableColumnOfScalarDecimal(vt, 0, expected); } static protected void validateTableColumnOfScalarDecimal(VoltTable vt, int col, BigDecimal[] expected) { assertNotNull(expected); assertEquals(expected.length, vt.getRowCount()); int len = expected.length; for (int i=0; i < len; i++) { assertTrue(vt.advanceRow()); BigDecimal actual = vt.getDecimalAsBigDecimal(col); if (expected[i] == null) { assertTrue(vt.wasNull()); assertEquals(null, actual); } else { BigDecimal rounded = expected[i].setScale(m_defaultScale, RoundingMode.valueOf(m_defaultRoundingMode)); assertEquals(rounded, actual); } } } protected void assertTablesAreEqual(String prefix, VoltTable expectedRows, VoltTable actualRows) { assertTablesAreEqual(prefix, expectedRows, actualRows, null); } private static final long TOO_MUCH_INFO = 100; protected void assertTablesAreEqual(String prefix, VoltTable expectedRows, VoltTable actualRows, Double epsilon) { assertEquals(prefix + "column count mismatch. Expected: " + expectedRows.getColumnCount() + " actual: " + actualRows.getColumnCount(), expectedRows.getColumnCount(), actualRows.getColumnCount()); if (expectedRows.getRowCount() != actualRows.getRowCount()) { long expRowCount = expectedRows.getRowCount(); long actRowCount = actualRows.getRowCount(); if (expRowCount + actRowCount < TOO_MUCH_INFO) { System.out.println("Expected: " + expectedRows); System.out.println("Actual: " + actualRows); } else { System.out.println("Expected: " + expRowCount + " rows"); System.out.println("Actual: " + actRowCount + " rows"); } fail(prefix + "row count mismatch. Expected: " + expectedRows.getRowCount() + " actual: " + actualRows.getRowCount()); } int rowNum = 1; while (expectedRows.advanceRow()) { if (! actualRows.advanceRow()) { fail(prefix + "too few actual rows; expected more than " + rowNum); } for (int j = 0; j < actualRows.getColumnCount(); j++) { String columnName = actualRows.getColumnName(j); String colPrefix = prefix + "row " + rowNum + ": column: " + columnName + ": "; VoltType actualType = actualRows.getColumnType(j); VoltType expectedType = expectedRows.getColumnType(j); assertEquals(colPrefix + "type mismatch", expectedType, actualType); Object expectedObj = expectedRows.get(j, expectedType); Object actualObj = actualRows.get(j, actualType); if (expectedRows.wasNull()) { if (actualRows.wasNull()) { continue; } fail(colPrefix + "expected null, got non null value: " + actualObj); } else { assertFalse(colPrefix + "expected the value " + expectedObj + ", got a null value.", actualRows.wasNull()); String message = colPrefix + "values not equal: "; if (expectedType == VoltType.FLOAT) { if (epsilon != null) { assertEquals(message, (Double)expectedObj, (Double)actualObj, epsilon); continue; } // With no epsilon provided, fall through to take // a chance on an exact value match, but helpfully // annotate any false positive that results. message += ". NOTE: You may want to pass a" + " non-null epsilon value >= " + Math.abs((Double)expectedObj - (Double)actualObj) + " to the table comparison test " + " if nearly equal FLOAT values are " + " causing a false mismatch."; } assertEquals(message, expectedObj, actualObj); } } rowNum++; } } public static void assertEquals(String msg, GeographyPointValue expected, GeographyPointValue actual) { assertApproximatelyEquals(msg, expected, actual, GEOGRAPHY_EPSILON); } /** * Assert that two points are approximately equal. By this we mean the latitude and * longitude differ by at most epsilon. If epsilon is zero or negative this means * equality. * * @param msg * @param expected * @param actual */ public static void assertApproximatelyEquals(String msg, GeographyPointValue expected, GeographyPointValue actual, double epsilon) { if (epsilon > 0) { assertEquals(msg + " latitude: ", expected.getLatitude(), actual.getLatitude(), epsilon); assertEquals(msg + " longitude: ", expected.getLongitude(), actual.getLongitude(), epsilon); } else { assertEquals(msg + " latitude: ", expected.getLatitude(), actual.getLatitude()); assertEquals(msg + " longitude: ", expected.getLongitude(), actual.getLongitude()); } } public static void assertEquals(GeographyPointValue expected, GeographyPointValue actual) { assertEquals("Points not equal: ", expected, actual); } /** * Assert that two geography values are equal. This delegates to * assertApproximatelyEquals with epsilon equal to zero. * * @param msg * @param expected * @param actual */ public static void assertEquals(String msg, GeographyValue expected, GeographyValue actual) { assertApproximatelyEquals(msg, expected, actual, GEOGRAPHY_EPSILON); } /** * Assert that two geography values are approximately equal. By approximately * equal we mean that the vertices of the expected and actual values differ * by at most epsilon. If epsilon is not positive this means the values must * be exactly equal. * * @param msg * @param expected * @param actual * @param epsilon */ public static void assertApproximatelyEquals(String msg, GeographyValue expected, GeographyValue actual, double epsilon) { if (expected == actual) { return; } // caller checks for null in the expected value assert (expected != null); if (actual == null) { fail(msg + " found null value when non-null expected"); } List<List<GeographyPointValue>> expectedLoops = expected.getRings(); List<List<GeographyPointValue>> actualLoops = actual.getRings(); assertEquals(msg + "wrong number of loops, expected " + expectedLoops.size() + ", " + "got " + actualLoops.size(), expectedLoops.size(), actualLoops.size()); int loopCtr = 0; Iterator<List<GeographyPointValue>> expectedLoopIt = expectedLoops.iterator(); for (List<GeographyPointValue> actualLoop : actualLoops) { List<GeographyPointValue> expectedLoop = expectedLoopIt.next(); assertEquals(msg + loopCtr + "the loop should have " + expectedLoop.size() + " vertices, but has " + actualLoop.size(), expectedLoop.size(), actualLoop.size()); int vertexCtr = 0; Iterator<GeographyPointValue> expectedVertexIt = expectedLoop.iterator(); for (GeographyPointValue actualPt : actualLoop) { GeographyPointValue expectedPt = expectedVertexIt.next(); String prefix = msg + "at loop " + loopCtr + ", vertex " + vertexCtr; assertApproximatelyEquals(prefix, expectedPt, actualPt, epsilon); ++vertexCtr; } ++loopCtr; } } public static void assertEquals(GeographyValue expected, GeographyValue actual) { assertEquals("Geographies not equal: ", expected, actual); } private static void assertApproximateContentOfRow(int row, Object[] expectedRow, VoltTable actualRow, double epsilon) { assertEquals("Actual row has wrong number of columns", expectedRow.length, actualRow.getColumnCount()); for (int i = 0; i < expectedRow.length; ++i) { String msg = "Row " + row + ", col " + i + ": "; Object expectedObj = expectedRow[i]; if (expectedObj == null) { VoltType vt = actualRow.getColumnType(i); actualRow.get(i, vt); assertTrue(msg, actualRow.wasNull()); } else if (expectedObj instanceof GeographyPointValue) { assertApproximatelyEquals(msg, (GeographyPointValue) expectedObj, actualRow.getGeographyPointValue(i), epsilon); } else if (expectedObj instanceof GeographyValue) { assertApproximatelyEquals(msg, (GeographyValue) expectedObj, actualRow.getGeographyValue(i), epsilon); } else if (expectedObj instanceof Long) { long val = ((Long)expectedObj).longValue(); assertEquals(msg, val, actualRow.getLong(i)); } else if (expectedObj instanceof Integer) { long val = ((Integer)expectedObj).longValue(); assertEquals(msg, val, actualRow.getLong(i)); } else if (expectedObj instanceof Double) { double expectedValue = (Double)expectedObj; double actualValue = actualRow.getDouble(i); // check if the row value was evaluated as null. Looking // at return is not reliable way to do so; // for null values, convert value into double min if (actualRow.wasNull()) { actualValue = Double.MIN_VALUE; } if (epsilon <= 0) { String fullMsg = msg + String.format("Expected value %f != actual value %f", expectedValue, actualValue); assertEquals(fullMsg, expectedValue, actualValue); } else { String fullMsg = msg + String.format("abs(Expected Value - Actual Value) = %e >= %e", Math.abs(expectedValue - actualValue), epsilon); assertTrue(fullMsg, Math.abs(expectedValue - actualValue) < epsilon); } } else if (expectedObj instanceof BigDecimal) { BigDecimal exp = (BigDecimal)expectedObj; BigDecimal got = actualRow.getDecimalAsBigDecimal(i); // Either both are null or neither are null. assertEquals(exp == null, got == null); assertEquals(msg, exp.doubleValue(), got.doubleValue(), epsilon); } else if (expectedObj instanceof String) { String val = (String)expectedObj; assertEquals(msg, val, actualRow.getString(i)); } else if (expectedObj instanceof TimestampType) { TimestampType val = (TimestampType)expectedObj; assertEquals(msg, val, actualRow.getTimestampAsTimestamp(i)); } else { fail("Unexpected type in expected row: " + expectedObj.getClass().getSimpleName()); } } } /** * Accept expected table contents as 2-dimensional array of objects, to make it easy to write tests. * Currently only handles some data types. Feel free to add more as needed. */ public static void assertContentOfTable(Object[][] expectedTable, VoltTable actualTable) { assertApproximateContentOfTable(expectedTable, actualTable, 0.0d); } /** * Assert that the expected and actual valus are approximately equal. By * approximately equal we mean that non-floating point values are identical, * and floating point values differ by at most epsilon. If epsilon is zero or negative, * we require equality. * * @param expectedTable * @param actualTable * @param epsilon */ public static void assertApproximateContentOfTable(Object[][] expectedTable, VoltTable actualTable, double epsilon) { for (int i = 0; i < expectedTable.length; ++i) { assertTrue("Fewer rows than expected: " + "expected: " + expectedTable.length + ", " + "actual: " + i, actualTable.advanceRow()); assertApproximateContentOfRow(i, expectedTable[i], actualTable, epsilon); } assertFalse("More rows than expected: " + "expected " + expectedTable.length + ", " + "actual: " + actualTable.getRowCount(), actualTable.advanceRow()); } static protected void verifyStmtFails(Client client, String stmt, String expectedPattern) throws IOException { verifyProcFails(client, expectedPattern, "@AdHoc", stmt); } static protected void verifyAdHocFails(Client client, String expectedPattern, Object... args) throws IOException { verifyProcFails(client, expectedPattern, "@AdHoc", args); } static protected void verifyProcFails(Client client, String expectedPattern, String storedProc, Object... args) throws IOException { String what; if (storedProc.compareTo("@AdHoc") == 0) { what = "the statement \"" + args[0] + "\""; } else { what = "the stored procedure \"" + storedProc + "\""; } try { client.callProcedure(storedProc, args); } catch (ProcCallException pce) { String msg = pce.getMessage(); String diagnostic = "Expected " + what + " to throw an exception matching the pattern \"" + expectedPattern + "\", but instead it threw an exception containing \"" + msg + "\"."; Pattern pattern = Pattern.compile(expectedPattern, Pattern.MULTILINE); assertTrue(diagnostic, pattern.matcher(msg).find()); return; } String diagnostic = "Expected " + what + " to throw an exception matching the pattern \"" + expectedPattern + "\", but instead it threw nothing."; fail(diagnostic); } // ALL OF THE VALIDATION SCHEMAS IN THIS TEST ARE BASED OFF OF // THE VOLTDB DOCS, RATHER THAN REUSING THE CODE THAT GENERATES THEM. // IN SOME MAGICAL FUTURE MAYBE THEY ALL CAN BE GENERATED FROM THE // SAME METADATA. static protected void validateSchema(VoltTable result, VoltTable expected) { assertEquals(expected.getColumnCount(), result.getColumnCount()); for (int i = 0; i < result.getColumnCount(); i++) { assertEquals("Failed name column: " + i, expected.getColumnName(i), result.getColumnName(i)); assertEquals("Failed type column: " + i, expected.getColumnType(i), result.getColumnType(i)); } } static protected void validStatisticsForTableLimit(Client client, String tableName, long limit) throws Exception { validStatisticsForTableLimitAndPercentage(client, tableName, limit, -1); } static protected void validStatisticsForTableLimitAndPercentage(Client client, String tableName, long limit, long percentage) throws Exception { long start = System.currentTimeMillis(); while (true) { long lastLimit =-1, lastPercentage = -1; Thread.sleep(1000); if (System.currentTimeMillis() - start > 10000) { String percentageStr = ""; if (percentage >= 0) { percentageStr = ", last seen percentage: " + lastPercentage; } fail("Took too long or have wrong answers: last seen limit: " + lastLimit + percentageStr); } VoltTable[] results = client.callProcedure("@Statistics", "TABLE", 0).getResults(); for (VoltTable t: results) { System.out.println(t.toString()); } if (results[0].getRowCount() == 0) continue; boolean foundTargetTuple = false; boolean limitExpected = false; boolean percentageExpected = percentage < 0 ? true: false; for (VoltTable vt: results) { while(vt.advanceRow()) { String name = vt.getString("TABLE_NAME"); if (tableName.equals(name)) { foundTargetTuple = true; lastLimit = vt.getLong("TUPLE_LIMIT"); if (limit == lastLimit) { limitExpected = true; } if (percentageExpected || percentage == (lastPercentage = vt.getLong("PERCENT_FULL")) ) { percentageExpected = true; } if (limitExpected && percentageExpected) return; break; } } if (foundTargetTuple) break; } } } static protected void checkDeploymentPropertyValue(Client client, String key, String value) throws IOException, ProcCallException, InterruptedException { boolean found = false; VoltTable result = client.callProcedure("@SystemInformation", "DEPLOYMENT").getResults()[0]; while (result.advanceRow()) { if (result.getString("PROPERTY").equalsIgnoreCase(key)) { found = true; assertEquals(value, result.getString("VALUE")); break; } } assertTrue(found); } static protected void checkQueryPlan(Client client, String query, String...patterns) throws NoConnectionsException, IOException, ProcCallException { VoltTable vt; assert(patterns.length >= 1); vt = client.callProcedure("@Explain", query).getResults()[0]; String vtStr = vt.toString(); for (String pattern : patterns) { if (! vtStr.contains(pattern)) { fail("The explain plan \n" + vtStr + "\n is expected to contain pattern: " + pattern); } } } /** * Utility function to run queries and dump results to stdout. * @param client * @param queries one or more query strings to send in a batch * @throws IOException * @throws NoConnectionsException * @throws ProcCallException */ protected static void dumpQueryResults(Client client, String... queries) throws IOException, NoConnectionsException, ProcCallException { VoltTable vts[] = client.callProcedure("@AdHoc", StringUtils.join(queries, '\n')).getResults(); int ii = 0; for (VoltTable vtn : vts) { System.out.println("DEBUG: result for " + queries[ii] + "\n" + vtn + "\n"); ++ii; } } /** * Utility function to explain queries and dump results to stdout. * @param client * @param queries one or more query strings to send in a batch to @Explain. * @throws IOException * @throws NoConnectionsException * @throws ProcCallException */ protected static void dumpQueryPlans(Client client, String... queries) throws IOException, NoConnectionsException, ProcCallException { VoltTable vts[] = client.callProcedure("@Explain", StringUtils.join(queries, '\n')).getResults(); int ii = 0; for (VoltTable vtn : vts) { System.out.println("DEBUG: plan for " + queries[ii] + "\n" + vtn + "\n"); ++ii; } } protected static void truncateTables(Client client, String... tables) throws IOException, ProcCallException { for (String tb : tables) { truncateTable(client, tb); } } protected static void truncateTable(Client client, String tb) throws IOException, ProcCallException { client.callProcedure("@AdHoc", "Truncate table " + tb); validateTableOfScalarLongs(client, "select count(*) from " + tb, new long[]{0}); } protected static void truncateAllTables(Client client) throws Exception { ClientResponse cr; cr = client.callProcedure("@SystemCatalog", "TABLES"); assertEquals(cr.getStatus(), ClientResponse.SUCCESS); VoltTable vt = cr.getResults()[0]; String allTables[] = new String[vt.getRowCount()]; int idx = 0; while (vt.advanceRow()) { allTables[idx++] = vt.getString("TABLE_NAME"); } truncateTables(client, allTables); } /** * A convenience method to build a Properties object initialized with an * arbitrary number of property/value pairs. * This one method with its scalable argument list replaces the * calls to the constructor and to the ugly and nonscalable * putAll(ImmutableMap.<String, String> of(...) and/or * a verbose list of calls to setProperty. * @param alternatingKeysAndValues property-name-1, string-value-1, * property-name-2, string-value-2, ... * @return the new Properties object **/ public static Properties buildProperties(String... alternatingKeysAndValues) { Properties properties = new Properties(); int nStrings = alternatingKeysAndValues.length; // Each key should have a value, so the length should be even. assert nStrings % 2 == 0; for (int ii = 0; ii < nStrings; ii += 2) { // Initialize each key value pair from adjacent strings. properties.setProperty( alternatingKeysAndValues[ii], alternatingKeysAndValues[ii+1]); } return properties; } }