Package com.foundationdb.sql.pg

Source Code of com.foundationdb.sql.pg.YamlTester

/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.foundationdb.sql.pg;

import static com.foundationdb.util.AssertUtils.assertCollectionEquals;
import static com.foundationdb.util.FileTestUtils.printClickableFile;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.sql.Types;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;
import java.util.regex.Pattern;

import com.foundationdb.server.error.ErrorCode;
import com.foundationdb.util.Strings;
import org.junit.ComparisonFailure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.AbstractConstruct;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.error.Mark;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.Tag;

/** A utility for testing SQL access over a Postgres server connection based on the contents of a YAML file. */
/* Here's an overview of the syntax of the YAML file.

  General:

   - One or more YAML documents
   - Each document is a sequence whose first element is a map
   - Key of first element's map is a command: a string with an uppercase first
     character

   Commands:

   Include
   - Syntax:
     - Include: <file>
   - If the file is relative, it is parsed relative to the referring file
   - If the file argument is missing or null, the command will be ignored

   Properties
   - Syntax:
     - Properties: <framework engine>
     - <property>: <value>
   - The frame engines that apply to this engine are: "all" (all frameworks) and
     "it" (this integration test framework engine)
   - A new property definition overrides any previous ones

   CreateTable
   - Syntax:
     - CreateTable: <table name> <create table arguments>...
     - error: [<error code>, <error message>]
   - The error message is optional
   - If the error code is missing or null, then the attribute and its value
     will be ignored
   - This command has the same behavior as would specifying a CREATE TABLE
     statement with a Statement command, but it lets the test framework know
     which tables have been created, so it can drop them after the test is
     completed

   Statement
   - Syntax
     - Statement: <statement text>
     - params: [[<parameter value>, ...], ...]
     - param_types: [<column type>, ...]
     - output: [[<output value>, ...], ...]
     - output_already_ordered: [[<output value>, ...], ...]
     - row_count: <number of rows>
     - output_types: [<column type>, ...]
     - explain: <explain plan>
     - error: [<error code>, <error message>]
     - warnings_count: <count>
     - warnings: [[<warning code>, <warning message], ...]
   - If the statement text is missing or null, the command will be ignored
   - Attributes are optional and can appear at most once
   - If the value of any attribute is missing or null, then the attribute and
     its value will be ignored
   - Only one statement in statement text (not checked)
   - At least one row element in params, param_types, output, output_types
   - At least one row in params and output
   - Types for param_types and output_types listed in code below
   - param_types requires params
   - All rows same length for output and params
   - Same values for output row length, output_types length, and row_count
   - Same values for params row length and param_types length
   - The value of row_count is non-negative
   - The error message is optional
   - Can't have error with output or row_count
   - YAML null for null value
   - !dc dc for don't care value in output
   - !re regular-expression for regular expression patterns that should match
     output, error codes, error messages, warnings, or explain output
   - The statement text should not create a table -- use the CreateTable
     command for that purpose
   - output: does a sort on the expected and actual during comparison
   - Warnings include statement warnings followed by result set warnings for
     each output row
   - The warning message is optional
  
   - JMX: <objectName>   (i.e com.foundationdb:type=IndexStatistics)
   ** Only one allowed of the following three (3) per command set
   - set: <set method>
   - method: <method>
   - get: <get method>
  
   - params: [<parameter value>, ...]
   - output: [[<output value>, ...], ...]
*/
class YamlTester
{
    private static final Logger LOG = LoggerFactory.getLogger(YamlTester.class);

    private static final Map<String, Integer> typeNameToNumber = new HashMap<>();
    private static final Map<Integer, String> typeNumberToName = new HashMap<>();

    static {
        addTypeNameAndNumber("VARBINARY", Types.BINARY); // name to number overwritten below
        addTypeNameAndNumber("BIGINT", Types.BIGINT);
        addTypeNameAndNumber("BLOB", Types.BLOB);
        addTypeNameAndNumber("BOOLEAN", Types.BIT); // TODO: Postgres JDBC claims it is Types.BIT (-7) instead of Types.BOOLEAN (16)
        addTypeNameAndNumber("CHAR", Types.CHAR);
        addTypeNameAndNumber("CLOB", Types.CLOB);
        addTypeNameAndNumber("DATE", Types.DATE);
        addTypeNameAndNumber("DECIMAL", Types.DECIMAL);
        addTypeNameAndNumber("DOUBLE", Types.DOUBLE);
        addTypeNameAndNumber("FLOAT", Types.FLOAT);
        addTypeNameAndNumber("INTEGER", Types.INTEGER);
        addTypeNameAndNumber("LONGVARBINARY", Types.LONGVARBINARY);
        addTypeNameAndNumber("LONGVARCHAR", Types.LONGVARCHAR);
        addTypeNameAndNumber("NUMERIC", Types.NUMERIC);
        addTypeNameAndNumber("REAL", Types.REAL);
        addTypeNameAndNumber("SMALLINT", Types.SMALLINT);
        addTypeNameAndNumber("TIME", Types.TIME);
        addTypeNameAndNumber("TIMESTAMP", Types.TIMESTAMP);
        addTypeNameAndNumber("TINYINT", Types.TINYINT);
        addTypeNameAndNumber("VARBINARY", Types.VARBINARY);
        addTypeNameAndNumber("VARCHAR", Types.VARCHAR);
        addTypeNameAndNumber("OTHER", Types.OTHER);
    }

    /** Matches all engines. */
    private static final String ALL_ENGINE = "all";
    /** Matches the IT engine. */
    private static final String IT_ENGINE = "it";
    /** Matches the Random Cost engine. */
    private static final String RAND_COST_ENGINE = "random-cost";

    /**
     * Compare two Lists into a consistent, though not necessarily ordered, order.
     * Actual contents are expected to be compared more strictly after using this.
     */
    private static final Comparator<List<?>> SIMPLE_LIST_COMPARATOR = new Comparator<List<?>>()
    {
        public int compare(List<?> x, List<?> y) {
            assertEquals("list sizes", x.size(), y.size());
            for(int i = 0; i < x.size(); i++) {
                Object xObj = x.get(i);
                Object yObj = y.get(i);
                // Regex, etc are not safe sortable with respect to the the final order
                assertFalse("CompareExpected in unsorted -output: " + xObj, xObj instanceof CompareExpected);
                assertFalse("CompareExpected in unsorted -output: " + yObj, yObj instanceof CompareExpected);
                String xString = objectToString(xObj);
                String yString = objectToString(yObj);
                int cmp = xString.compareTo(yString);
                if(cmp != 0) {
                    return cmp;
                }
            }
            return 0;
        }
    };

    private final String filename;
    private final Reader in;
    private final Connection connection;
    private final Stack<String> includeStack = new Stack<>();
    private int commandNumber = 0;
    private String commandName = null;
    private boolean suppressed = false;
    protected boolean randomCost = false;
    private static final DateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    private static final DateFormat DEFAULT_DATETIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S z");
    private static final int DEFAULT_RETRY_COUNT = 5;
    private int lineNumber = 1;

    /**
     * Creates an instance of this class.
     *
     * @param filename   the file name of the YAML input
     * @param in         the YAML input
     * @param connection the JDBC connection
     */
    YamlTester(String filename, Reader in, Connection connection) {
        this.filename = filename;
        this.in = in;
        this.connection = connection;
    }

    YamlTester(String filename, Reader in, Connection connection, Boolean randomCost) {
        this.filename = filename;
        this.in = in;
        this.connection = connection;
        this.randomCost = randomCost;
    }

    /** Test the input specified in the constructor. */
    void test() {
        try {
            test(in);
        } catch (Throwable e) {
            if (filename != null) {
                System.err.println("Failed Yaml test (note: line number points to start of document)");
                printClickableFile(filename.substring(0, filename.length() - 5), "yaml", lineNumber);
            }
            throw e;
        }
    }

    private void test(Reader in) {
        List<?> sequence = null;
        try {
            Yaml yaml = new Yaml(new RegisterTags());
            for(Object yamlObject : yaml.loadAll(in)) {
                RegisterTags.LinedObject linedDocument = (RegisterTags.LinedObject) yamlObject;
                Object document = linedDocument.getObject();
                lineNumber = linedDocument.getStartMark().getLine();
                ++commandNumber;
                commandName = null;
                sequence = nonEmptySequence(document, "command document");
                Entry<?,?> firstEntry = firstEntry(sequence.get(0), "first element of the document");
                commandName = string(firstEntry.getKey(), "command name");
                Object value = firstEntry.getValue();
                if("Include".equals(commandName)) {
                    includeCommand(value, sequence);
                } else if("Properties".equals(commandName)) {
                    propertiesCommand(value, sequence);
                } else if("CreateTable".equals(commandName)) {
                    createTableCommand(value, sequence);
                } else if("DropTable".equals(commandName)) {
                    dropTableCommand(value, sequence);
                } else if("Statement".equals(commandName)) {
                    statementCommand(value, sequence);
                } else if("Message".equals(commandName)) {
                    messageCommand(value);
                } else if("JMX".equals(commandName)) {
                    jmxCommand(value, sequence);
                } else if ("UseContext".equals(commandName)) {
                    useContextCommand(value, sequence);
                } else if("Newtypes".equals(commandName)) {
                    fail("No longer supported Newtypes command");
                } else {
                    fail("Unknown command: " + commandName);
                }
                if(suppressed) {
                    LOG.debug("Test suppressed: {}", filename);
                    break;
                }
            }
            if(commandNumber == 0) {
                fail("Test file must not be empty");
            }
        } catch(ContextAssertionError e) {
            throw e;
        } catch (FullOutputAssertionError e) {
            throw e;
        } catch(Throwable e) {
            // Add context
            throw new ContextAssertionError(String.valueOf(sequence), e.toString(), e);
        }
    }

    private void includeCommand(Object value, List<?> sequence) {
        if(value == null) {
            return;
        }
        String includeValue = string(value, "Include value");
        File include = new File(includeValue);
        if(sequence.size() > 1) {
            throw new ContextAssertionError(
                includeValue, "The Include command does not support attributes" + "\nFound: " + sequence.get(1)
            );
        }
        if(!include.isAbsolute()) {
            String parent = filename;
            if(!includeStack.isEmpty()) {
                parent = includeStack.peek();
            }
            if(parent != null) {
                include = new File(new File(parent).getParent(), include.toString());
            }
        }
        try(Reader in = new InputStreamReader(new FileInputStream(include), "UTF-8")) {
            int originalCommandNumber = commandNumber;
            commandNumber = 0;
            String originalCommandName = commandName;
            commandName = null;
            try {
                includeStack.push(includeValue);
                test(in);
            } finally {
                includeStack.pop();
                commandNumber = originalCommandNumber;
                commandName = originalCommandName;
            }
        } catch(IOException e) {
            throw new ContextAssertionError(includeValue, "Problem accessing include file " + include + ": " + e, e);
        }
    }

    private void messageCommand(Object value) {
        String message = string(value, "Message");
        LOG.info("FTS Message: {}", message);
    }

    private void propertiesCommand(Object value, List<?> sequence) {
        String engine = string(value, "Properties framework engine");
        if(ALL_ENGINE.equals(engine) || IT_ENGINE.equals(engine)
                || (randomCost && RAND_COST_ENGINE.equals(engine))) {
            for(Object elem : sequence) {
                Entry<?,?> entry = onlyEntry(elem, "Properties entry");
                if("suppressed".equals(entry.getKey())) {
                    suppressed = bool(entry.getValue(), "suppressed value");
                }
            }
        }
    }

    /** Implements common behavior of commands that execute statements. */
    private abstract class AbstractStatementCommand
    {
        final String statement;
        boolean errorSpecified;
        Object errorCode;
        Object errorMessage;
        boolean doSortOutput;
        int retryCount = -1;
        int retriesPerformed = 0;

        /** Handle a statement with the specified statement text. */
        AbstractStatementCommand(String statement) {
            this.statement = statement;
        }

        /** Parse an error attribute with the specified value. */
        void parseError(Object value) {
            if(value == null) {
                return;
            }
            assertFalse(
                "The error attribute must not appear more than once", errorSpecified
            );
            errorSpecified = true;
            List<?> errorInfo = nonEmptyScalarSequence(value, "error value");
            errorCode = scalar(errorInfo.get(0), "error code");
            if(errorInfo.size() > 1) {
                errorMessage = scalar(errorInfo.get(1), "error message");
                assertTrue("The error attribute can have at most two elements", errorInfo.size() < 3);
            }
        }

        /**
         * Check the specified exception against the error attribute specified earlier, if any.
         *
         * @return <code>true</code> if the original statement should be retried, <code>false</code> otherwise
         */
        boolean checkFailure(SQLException sqlException) {
            LOG.debug("Generated error code: {}", sqlException.getSQLState(), sqlException);
            if(!errorSpecified) {
                // Retry rollback automatically if command did not give an explicit request
                if(retryCount == -1) {
                    try {
                        if(ErrorCode.valueOfCode(sqlException.getSQLState()).isRollbackClass()) {
                            retryCount = DEFAULT_RETRY_COUNT;
                        }
                    } catch(IllegalArgumentException e) {
                        // Ignore, wasn't an ErrorCode
                    }
                }
                if(retriesPerformed < retryCount) {
                    LOG.debug("Unexpected error, retrying statement.");
                    ++retriesPerformed;
                    try {
                        // Delay a little to try and avoid another immediate conflict
                        Thread.sleep(100 * retriesPerformed);
                    } catch(InterruptedException e) {
                        // Ignore
                    }
                    return true;
                }
                throw new ContextAssertionError(statement, "Unexpected statement execution failure", sqlException);
            }
            checkExpected("error code", errorCode, sqlException.getSQLState());
            if(errorMessage != null) {
                checkExpected("error message", errorMessage, sqlException.getMessage());
            }
            return false;
        }


    }

    /** Represents an SQL warning. */
    private static class Warning implements CompareExpected
    {
        /** The SQL state -- warning code */
        final Object code;
        /** The warning message */
        final Object message;

        Warning(Object code, Object message) {
            this.code = code;
            this.message = message;
        }

        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("[").append(code);
            if(message != null) {
                sb.append(", '").append(message).append("'");
            }
            sb.append("]");
            return sb.toString();
        }

        public boolean compareExpected(Object actual) {
            if(!(actual instanceof Warning)) {
                return false;
            }
            Warning warning = (Warning)actual;
            // Require the codes to match
            if(!expected(code, warning.code)) {
                return false;
            }
            // Only require the message to match if the pattern -- this object -- specifies a message
            return message == null || expected(message, warning.message);
        }
    }

    private void createTableCommand(Object value, List<?> sequence) throws SQLException {
        new CreateTableCommand(value, sequence).execute();
    }

    private class CreateTableCommand extends AbstractStatementCommand
    {
        private Object warningsCount;
        private List<Warning> warnings;

        CreateTableCommand(Object value, List<?> sequence) {
            super("CREATE TABLE " + string(value, "CreateTable argument"));
            for(int i = 1; i < sequence.size(); i++) {
                Entry<?,?> map = onlyEntry(sequence.get(i), "CreateTable attribute");
                String attribute = string(map.getKey(), "CreateTable attribute name");
                Object attributeValue = map.getValue();
                if("error".equals(attribute)) {
                    parseError(attributeValue);
                } else if("warnings_count".equals(attribute)) {
                    warningsCount = parseWarningsCount(attributeValue, warningsCount);
                } else if("warnings".equals(attribute)) {
                    warnings = parseWarnings(attributeValue, warnings);
                } else {
                    fail("The '" + attribute + "' attribute name was not" + " recognized");
                }

                if(warnings != null && warningsCount != null && !expected(warningsCount, warnings.size())) {
                    fail("Warnings count " + warningsCount + " does not match " + warnings.size() + ", which is the number of warnings");
                }
            }
        }

        void execute() throws SQLException {
            Statement stmt = connection.createStatement();
            LOG.debug("Executing statement: {}", statement);
            while(true) {
                try {
                    stmt.execute(statement);
                    LOG.debug("Statement executed successfully");
                    break;
                } catch(SQLException e) {
                    LOG.debug("Generated error code: {}", e.getSQLState(), e);
                    if(checkFailure(e)) {
                        continue;
                    }
                    return;
                }
            }
            checkSuccess(stmt, errorSpecified, warnings, warningsCount);
        }
    }


    private void dropTableCommand(Object value, List<?> sequence) throws SQLException {
        new DropTableCommand(value, sequence).execute();
    }


    private class DropTableCommand extends AbstractStatementCommand
    {

        private Object warningsCount;
        private List<Warning> warnings;

        DropTableCommand(Object value, List<?> sequence) {
            super("DROP TABLE " + string(value, "DropTable argument"));
            for(int i = 1; i < sequence.size(); i++) {
                Entry<?,?> map = onlyEntry(sequence.get(i), "DropTable attribute");
                String attribute = string(map.getKey(), "CreateTable attribute name");
                Object attributeValue = map.getValue();
                if("error".equals(attribute)) {
                    parseError(attributeValue);
                } else if("warnings_count".equals(attribute)) {
                    warningsCount = parseWarningsCount(attributeValue, warningsCount);
                } else if("warnings".equals(attribute)) {
                    warnings = parseWarnings(attributeValue, warnings);
                } else {
                    fail("The '" + attribute + "' attribute name was not" + " recognized");
                }

                if(warnings != null && warningsCount != null && !expected(warningsCount, warnings.size())) {
                    fail("Warnings count " + warningsCount + " does not match " + warnings.size() + ", which is the number of warnings");
                }
            }
        }

        void execute() throws SQLException {
            Statement stmt = connection.createStatement();
            LOG.debug("Executing statement: {}", statement);
            while(true) {
                try {
                    stmt.execute(statement);
                    LOG.debug("Statement executed successfully");
                    break;
                } catch(SQLException e) {
                    LOG.debug("Generated error code: {}", e.getSQLState(), e);
                    if(checkFailure(e)) {
                        continue;
                    }
                    return;
                }
            }
            checkSuccess(stmt, errorSpecified, warnings, warningsCount);
        }
    }

    private void useContextCommand(Object value, List<?> sequence) throws SQLException {
        new UseContextCommand(value, sequence).execute();
    }

    private class UseContextCommand extends AbstractStatementCommand
    {
        UseContextCommand (Object context, List<?>sequence) {
            super("Use Context: " + string(context, "UseContext Argument"));
        }
       
        void execute() throws SQLException {
        }
    }

   
    private void statementCommand(Object value, List<?> sequence) throws SQLException {
        assertNotNull("Statement value cannot be null (e.g. null, empty, no matching select-engine)", value);
        new StatementCommand(string(value, "Statement value"), sequence).execute();
    }

    private class StatementCommand extends AbstractStatementCommand
    {
        private List<List<?>> params;
        private List<Integer> paramTypes;
        private List<List<?>> output;
        private int rowCount = -1;
        private List<String> outputTypes;
        private Object explain;
        private Object warningsCount;
        private List<Warning> warnings;

        /** The 1-based index of the row of parameters being used for the current parameterized statement execution. */
        private int paramsRow = 1;

        /** The 0-based index of the row of the output being compared with the statement output. */
        private int outputRow = 0;

        StatementCommand(String statement, List<?> sequence) {
            super(statement);
            for(int i = 1; i < sequence.size(); i++) {
                Entry<?,?> map = onlyEntry(
                    sequence.get(i), "Statement attribute"
                );
                String attribute = string(
                    map.getKey(), "Statement attribute name"
                );
                Object attributeValue = map.getValue();
                if("params".equals(attribute)) {
                    parseParams(attributeValue);
                } else if("param_types".equals(attribute)) {
                    parseParamTypes(attributeValue);
                } else if("output_already_ordered".equals(attribute)) {
                    this.doSortOutput = false;
                    parseOutput(attributeValue);
                } else if("output".equals(attribute)) {
                    this.doSortOutput = true;
                    parseOutput(attributeValue);
                } else if("row_count".equals(attribute)) {
                    parseRowCount(attributeValue);
                } else if("retry_count".equals(attribute)) {
                    parseRetryCount(attributeValue);
                } else if("output_types".equals(attribute)) {
                    parseOutputTypes(attributeValue);
                } else if("error".equals(attribute)) {
                    parseError(attributeValue);
                } else if("explain".equals(attribute)) {
                    parseExplain(attributeValue);
                } else if("warnings_count".equals(attribute)) {
                    warningsCount = parseWarningsCount(attributeValue, warningsCount);
                } else if("warnings".equals(attribute)) {
                    warnings = parseWarnings(attributeValue, warnings);
                } else {
                    fail("The '" + attribute + "' attribute name was not" + " recognized");
                }
            }
            if(paramTypes != null) {
                if(params == null) {
                    fail("Cannot specify the param_types attribute without" + " params attribute");
                } else {
                    assertEquals(
                        "The params_types attribute must be the same" + " length as the row length of the params" + " attribute:",
                        params.get(0).size(),
                        paramTypes.size()
                    );
                }
            }
            if(rowCount != -1) {
                if(output != null) {
                    assertEquals(
                        "The row_count attribute must be the same" + " as the length of the rows in the output" + " attribute:",
                        output.size(),
                        rowCount
                    );
                } else if(outputTypes != null) {
                    assertEquals(
                        "The row_count attribute must be the same" + " as the length of the output_types" + " attribute:",
                        outputTypes.size(),
                        rowCount
                    );
                }
            }
            if(outputTypes != null) {
                if(output != null) {
                    assertEquals(
                        "The output_types attribute must be the same" + " length as the length of the rows in the" + " output attribute:",
                        output.get(0).size(),
                        outputTypes.size()
                    );
                }
            }
            if(errorSpecified && output != null) {
                fail("Cannot specify both error and output attributes");
            }
            if(errorSpecified && rowCount != -1) {
                fail("Cannot specify both error and row_count attributes");
            }
            if(warnings != null &&
                warningsCount != null &&
                !expected(warningsCount, warnings.size())) {
                fail("Warnings count " + warningsCount + " does not match " + warnings.size() + ", which is the number of warnings");
            }
        }

        private void parseParams(Object value) {
            if(value == null) {
                return;
            }
            assertNull("The params attribute must not appear more than once", params);
            params = rows(value, "params value");
            // TODO: -output needs to (optionally?) be another list nesting deep for clarity with multiple params
            assertEquals("params size", 1, params.size());
        }

        private void parseParamTypes(Object value) {
            if(value == null) {
                return;
            }
            assertNull("The param_types attribute must not appear more than once", paramTypes);
            List<String> paramTypeNames = nonEmptyStringSequence(
                value, "param_types value"
            );
            paramTypes = new ArrayList<>(paramTypeNames.size());
            for(String typeName : paramTypeNames) {
                Integer typeNumber = getTypeNumber(typeName);
                assertNotNull("Unknown type name in param_types: " + typeName, typeNumber);
                paramTypes.add(typeNumber);
            }
        }

        private void parseOutput(Object value) {
            if(value == null) {
                return;
            }
            assertNull("The output attribute must not appear more than once", output);
            output = rows(value, "output value");
        }

        private void parseRowCount(Object value) {
            if(value == null) {
                return;
            }
            assertTrue("The row_count attribute must not appear more than once", rowCount == -1);
            rowCount = integer(value, "row_count value");
            assertTrue("The row_count value must not be negative", rowCount >= 0);
        }

        private void parseRetryCount(Object value) {
            assertTrue("The retry_count attribute must not appear more than once", retryCount == -1);
            int count = DEFAULT_RETRY_COUNT;
            if(value != null) {
                count = integer(value, "retry_count value");
                assertTrue("The retry_count value must not be negative", count >= 0);
            }
            retryCount = count;
        }

        private void parseOutputTypes(Object value) {
            if(value == null) {
                return;
            }
            assertNull("The output_types attribute must not appear more than once", outputTypes);
            outputTypes = nonEmptyStringSequence(value, "output_types value");
            for(String typeName : outputTypes) {
                assertNotNull("Unknown type name in output_types: " + typeName, getTypeNumber(typeName));
            }
        }

        private void parseExplain(Object value) {
            if(value == null) {
                return;
            }
            assertNull("The explain attribute must not appear more than once", explain);
            explain = scalar(value, "explain value");
        }

        void execute() throws SQLException {
            if(explain != null) {
                checkExplain();
            }
            boolean done = false;
            while(!done) {
                if(params == null) {
                    try(Statement stmt = connection.createStatement()) {
                        LOG.debug("Executing statement: {}", statement);
                        try {
                            stmt.execute(statement);
                        } catch(SQLException e) {
                            if(checkFailure(e)) {
                                continue;
                            }
                            return;
                        }
                        checkSuccess(stmt, doSortOutput);
                    }
                } else {
                    try(PreparedStatement stmt = connection.prepareStatement(statement)) {
                        int numParams = params.get(0).size();
                        for(List<?> paramsList : params) {
                            if(params.size() > 1) {
                                commandName = "Statement, params list " + paramsRow;
                            }
                            for(int i = 0; i < numParams; i++) {
                                Object param = paramsList.get(i);
                                if(paramTypes != null) {
                                    stmt.setObject(i + 1, param, paramTypes.get(i));
                                } else {
                                    stmt.setObject(i + 1, param);
                                }
                            }
                            LOG.debug("Executing statement: {}   with Parameters: {}", statement, paramsList);
                            try {
                                stmt.execute();
                            } catch(SQLException e) {
                                if(checkFailure(e)) {
                                    continue;
                                }
                                continue;
                            }
                            checkSuccess(stmt, doSortOutput);
                            paramsRow++;
                        }
                        commandName = "Statement";
                    }
                }
                done = true;
            }
        }

        private void checkExplain() throws SQLException {
            try(Statement stmt = connection.createStatement()) {
                stmt.execute("EXPLAIN " + statement);
                ResultSet rs = stmt.getResultSet();
                StringBuilder sb = new StringBuilder();
                int numColumns = rs.getMetaData().getColumnCount();
                while(rs.next()) {
                    for(int i = 1; i <= numColumns; i++) {
                        if(i != 1) {
                            sb.append(", ");
                        }
                        sb.append(rs.getString(i));
                    }
                    sb.append('\n');
                }
                String got = sb.toString().trim();
                checkExpected("explain output", explain, got);
            }
        }

        private void checkSuccess(Statement stmt, boolean doSortOutput) throws SQLException {
            assertFalse("Statement execution succeeded, but was expected" + " to generate an error", errorSpecified);
            ResultSet rs = stmt.getResultSet();
            if(rs == null) {
                assertNull("Query did not produce results output", output);
                assertNull("Query did not produce results, so output_types" + " are not supported", outputTypes);
                if(rowCount != -1) {
                    int updateCount = stmt.getUpdateCount();
                    assertFalse("Query did not produce an update count", updateCount == -1);
                    outputRow += updateCount;
                    checkRowCount(rowCount, false);
                }
                List<Warning> reportedWarnings = new ArrayList<>();
                collectWarnings(stmt.getWarnings(), reportedWarnings);
                checkWarnings(reportedWarnings);
            } else {
                checkResults(rs, doSortOutput);
                assertFalse("Multiple result sets not supported", stmt.getMoreResults());
            }
        }

        /**
         * Add the warning message from the specified warning, as well as any additional warnings linked via the
         * getNextWarning method, to the list of messages.
         */
        private void collectWarnings(SQLWarning warning, List<Warning> messages) {
            while(warning != null) {
                messages.add(new Warning(warning.getSQLState(), warning.getMessage()));
                warning = warning.getNextWarning();
            }
        }

        private void checkWarnings(List<Warning> reportedWarnings) {
            if(!reportedWarnings.isEmpty()) {
                LOG.debug("Statement warnings: {}", reportedWarnings);
            }
            if(warningsCount != null) {
                checkExpected("warnings count", warningsCount, reportedWarnings.size());
            }
            if(warnings == null) {
                return;
            }
            if(reportedWarnings.isEmpty()) {
                if(!warnings.isEmpty()) {
                    fail("No warnings were reported, but expected warnings: " + warnings);
                }
            } else {
                if(warnings.isEmpty()) {
                    fail("Warnings were reported but none were expected: " + warnings);
                }
                checkExpectedList("Warnings", warnings, reportedWarnings);
            }
        }

        /**
         * Check if the number of rows of output seen, as measured by the outputRow field, is incorrect given the expected
         * number of rows.
         *
         * @param expected the number of rows expected
         * @param more     whether there are more result rows in the current result set
         */
        private void checkRowCount(int expected, boolean more) {
            int got = outputRow;
            if(more) {
                got++;
            }
            if(got > expected) {
                throw new ContextAssertionError(
                    statement, "Too many output rows:" + "\nExpected: " + expected + "\n     got: " + got
                );
            } else if(!more && (params == null || paramsRow == params.size()) && (got < expected)) {
                throw new ContextAssertionError(
                    statement, "Too few output rows:" + "\nExpected: " + expected + "\n     got: " + got
                );
            }
        }

        private void checkResults(ResultSet rs, boolean doSortOutput) throws SQLException {
            if(outputTypes != null && outputRow == 0) {
                checkOutputTypes(rs);
            }
            LOG.debug("Statement output:");
            List<Warning> reportedWarnings = new ArrayList<>();
            if(output != null) {
                ResultSetMetaData metaData = rs.getMetaData();
                int numColumns = metaData.getColumnCount();
                List<List<?>> resultsList = new ArrayList<>();
                Statement stmt = rs.getStatement();
                assert stmt != null;
                collectWarnings(stmt.getWarnings(), reportedWarnings);
                while (rs.next()) {
                    ArrayList<Object> actualRow = new ArrayList<>(metaData.getColumnCount());
                    resultsList.add(actualRow);
                    for (int i=0; i<metaData.getColumnCount(); i++) {
                        actualRow.add(rs.getObject(i+1));
                    }
                }
                try {
                    assertEquals("Unexpected number of columns in output:", output.get(0).size(), numColumns);
                    if (doSortOutput) {
                        Collections.sort(output, SIMPLE_LIST_COMPARATOR);
                        Collections.sort(resultsList, SIMPLE_LIST_COMPARATOR);
                    }
                    for (int i=0; outputRow < output.size() && i < resultsList.size(); outputRow++, i++) {
                        List<?> row = output.get(outputRow);
                        List<?> resultsRow = resultsList.get(i);
                        if (!rowsEqual(row, resultsRow)) {
                            throw new ContextAssertionError(
                                    statement,
                                    "Unexpected output in row " + (outputRow + 1) + ":" +
                                            " Expected: " + arrayString(row) + " got: " + arrayString(resultsRow)
                            );
                        }
                    }
                    assertEquals("Unexpected number of rows", output.size(), resultsList.size());
                } catch (ContextAssertionError e) {
                    throw new FullOutputAssertionError(output, resultsList, e);
                } catch (AssertionError e) {
                    throw new FullOutputAssertionError(output, resultsList, new ContextAssertionError(statement, e));
                }
            } else {
                Statement stmt = rs.getStatement();
                assert stmt != null;
                collectWarnings(stmt.getWarnings(), reportedWarnings);
                while(rs.next()) {
                    outputRow++;
                    collectWarnings(rs.getWarnings(), reportedWarnings);
                }
                if(rowCount != -1) {
                    checkRowCount(rowCount, false);
                }
            }
            checkWarnings(reportedWarnings);
        }

        private boolean rowsEqual(List<?> pattern, List<?> row) {
            int size = pattern.size();
            if(size != row.size()) {
                return false;
            }
            for(int i = 0; i < size; i++) {
                Object patternElem = pattern.get(i);
                Object rowElem = row.get(i);
                if(!expected(patternElem, rowElem)) {
                    return false;
                }
            }
            return true;
        }

        private void checkOutputTypes(ResultSet rs) throws SQLException {
            ResultSetMetaData metaData = rs.getMetaData();
            int numColumns = metaData.getColumnCount();
            assertEquals("Wrong number of output types:", outputTypes.size(), numColumns);
            for(int i = 1; i <= numColumns; i++) {
                int columnType = metaData.getColumnType(i);
                String columnTypeName = getTypeName(columnType);
                if(columnTypeName == null) {
                    columnTypeName = "<unknown " + metaData.getColumnTypeName(i) + " (" + columnType + ")>";
                }
                assertEquals("Wrong output type for column " + i + ":", outputTypes.get(i - 1), columnTypeName);
            }
        }
    }

    //----------- static helpers -----------------

    static Object stripWARN(Object msg) {
        if(msg instanceof String) {
            String st = (String)msg;
            if(st.startsWith("WARN:  ")) {
                return st.substring(7);
            }
        }
        return msg;
    }

    static void checkSuccess(Statement stmt,
                             boolean errorSpecified,
                             List<Warning> warnings,
                             Object warningsCount) throws SQLException {
        assertFalse("Statement execution succeeded, but was expected" + " to generate an error", errorSpecified);
        List<Warning> reportedWarnings = new ArrayList<>();
        collectWarnings(stmt.getWarnings(), reportedWarnings);
        checkWarnings(reportedWarnings, warnings, warningsCount);
    }

    static Object parseWarningsCount(Object value, Object warningsCount) {
        if(value == null) {
            return null;
        }
        assertNull("The warnings_count attribute must not appear more than once", warningsCount);
        return scalar(value, "warningsCount");
    }

    static List<Warning> parseWarnings(Object value, List<Warning> warnings) {
        if(value == null) {
            return null;
        }
        assertNull("The warnings attribute must not appear more than once", warnings);

        List<?> list = nonEmptySequence(value, "warnings");
        warnings = new ArrayList<>();
        for(int i = 0; i < list.size(); i++) {
            List<?> element = nonEmptyScalarSequence(list.get(i), "warnings element " + i);
            assertFalse("Warnings element " + i + " is empty", element.isEmpty());
            assertFalse("Warnings element " + i + " has more than two elements", element.size() > 2);
            warnings.add(new Warning(element.get(0), element.size() > 1 ? stripWARN(element.get(1)) : null));
        }
        return warnings;
    }

    private static void collectWarnings(SQLWarning warning, List<Warning> messages) {
        while(warning != null) {
            messages.add(new Warning(warning.getSQLState(), warning.getMessage()));
            warning = warning.getNextWarning();
        }
    }

    private static void checkWarnings(List<Warning> reportedWarnings, List<Warning> warnings, Object warningsCount) {
        if(!reportedWarnings.isEmpty()) {
            LOG.debug("Statement warnings: {}", reportedWarnings);
        }

        if(warningsCount != null) {
            checkExpected("warnings count", warningsCount, reportedWarnings.size());
        }
        if(warnings == null) {
            return;
        }
        if(reportedWarnings.isEmpty()) {
            if(!warnings.isEmpty()) {
                fail("No warnings were reported, but expected warnings: " + warnings);
            }
        } else {
            if(warnings.isEmpty()) {
                fail("Warnings were reported but none were expected: " + warnings);
            }
            checkExpectedList("Warnings", warnings, reportedWarnings);
        }
    }

    static void checkExpectedList(String description, List<?> expected, List<?> actual) {
        int expectedSize = expected.size();
        int actualSize = actual.size();
        if(expectedSize != actualSize) {
            fail(
                description + " should have " + expectedSize +
                    " elements, but has " + actualSize + ":" +
                    "\nExpected: " + expected +
                    "\n     got: " + actual
            );
        }
        for(int i = 0; i < expectedSize; i++) {
            Object expectedElement = expected.get(i);
            Object actualElement = actual.get(i);
            if(!expected(expectedElement, actualElement)) {
                fail(
                    "Incorrect value for element " + i + " of " +
                        description + ":" +
                        "\nExpected: " + expected +
                        "\n     got: " + actual
                );
            }
        }
    }

    static void checkExpected(String description, Object expected, Object actual) {
        if(!expected(expected, actual)) {
            fail("Incorrect " + description + ":" + "\nExpected: " + expected + "\n     got: " + actual);
        }
    }

    static boolean expected(Object expected, Object actual) {
        if(expected instanceof CompareExpected) {
            return ((CompareExpected)expected).compareExpected(actual);
        } else if(expected == null) {
            return actual == null;
        } else {
            String expectedString = objectToString(expected);
            String actualString = objectToString(actual);

            return expectedString.equals(actualString);
        }
    }

    static String arrayString(List<?> array) {
        if(array == null) {
            return "null";
        }
        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for(int i = 0; i < array.size(); i++) {
            Object elem = array.get(i);
            if(i != 0) {
                sb.append(", ");
            }
            sb.append(objectToString(elem));
        }
        sb.append(']');
        return sb.toString();
    }

    static String objectToString(Object object) {
        if(object == null) {
            return "null";
        } else {
            Class objectClass = object.getClass();
            if(!objectClass.isArray()) {
                return object.toString();
            } else if(objectClass == byte[].class) {
                return Arrays.toString((byte[])object);
            } else if(objectClass == short[].class) {
                return Arrays.toString((short[])object);
            } else if(objectClass == int[].class) {
                return Arrays.toString((int[])object);
            } else if(objectClass == long[].class) {
                return Arrays.toString((long[])object);
            } else if(objectClass == char[].class) {
                return Arrays.toString((char[])object);
            } else if(objectClass == float[].class) {
                return Arrays.toString((float[])object);
            } else if(objectClass == double[].class) {
                return Arrays.toString((double[])object);
            } else if(objectClass == boolean[].class) {
                return Arrays.toString((boolean[])object);
            } else {
                return Arrays.toString((Object[])object);
            }
        }
    }

    static Object scalar(Object object, String desc) {
        assertThat(
            "The " + desc + " must be a scalar", object, not(anyOf(instanceOf(Collection.class), instanceOf(Map.class)))
        );
        return object;
    }

    static String string(Object object, String desc) {
        assertThat("The " + desc + " must be a string", object, instanceOf(String.class));
        return (String)object;
    }

    static int integer(Object object, String desc) {
        assertThat("The " + desc + " must be an integer", object, instanceOf(Integer.class));
        return (Integer)object;
    }

    static boolean bool(Object object, String desc) {
        assertThat("The " + desc + " must be a boolean", object, instanceOf(Boolean.class));
        return (Boolean)object;
    }

    static Map<?,?> map(Object object, String desc) {
        assertThat("The " + desc + " must be a map", object, instanceOf(Map.class));
        return (Map<?,?>)object;
    }

    static Entry<?,?> firstEntry(Object object, String desc) {
        Map<?,?> map = map(object, desc);
        assertFalse("The " + desc + " must not be empty", map.isEmpty());
        return map.entrySet().iterator().next();
    }

    static Entry<?,?> onlyEntry(Object object, String desc) {
        Map<?,?> map = map(object, desc);
        assertEquals("The " + desc + " must contain exactly one entry:", 1, map.size());
        return map.entrySet().iterator().next();
    }

    static List<?> sequence(Object object, String desc) {
        assertThat("The " + desc + " must be a sequence", object, instanceOf(List.class));
        return (List<?>)object;
    }

    static List<?> nonEmptySequence(Object object, String desc) {
        List<?> list = sequence(object, desc);
        assertFalse("The " + desc + " must not be empty", list.isEmpty());
        return list;
    }

    static List<?> scalarSequence(Object object, String desc) {
        List<?> list = sequence(object, desc);
        for(Object elem : list) {
            assertThat(
                "The element of the " + desc + " must be a scalar",
                elem,
                not(anyOf(instanceOf(Collection.class), instanceOf(Map.class)))
            );
        }
        return list;
    }

    static List<?> nonEmptyScalarSequence(Object object, String desc) {
        List<?> list = scalarSequence(object, desc);
        assertFalse("The " + desc + " must not be empty", list.isEmpty());
        return list;
    }

    static List<String> stringSequence(Object object, String desc) {
        List<?> list = sequence(object, desc);
        List<String> strList = new ArrayList<>(list.size());
        for(Object elem : list) {
            assertThat("The element of the " + desc + " must be a string", elem, instanceOf(String.class));
            strList.add((String)elem);
        }
        return strList;
    }

    static List<String> nonEmptyStringSequence(Object object, String desc) {
        List<String> list = stringSequence(object, desc);
        assertFalse("The " + desc + " must not be empty", list.isEmpty());
        return list;
    }

    static List<List<?>> rows(Object object, String desc) {
        List<?> list = nonEmptySequence(object, desc);
        List<List<?>> rows = new ArrayList<>();
        int rowLength = -1;
        for(int i = 0; i < list.size(); i++) {
            List<?> row = nonEmptyScalarSequence(list.get(i), desc + " element");
            if(i == 0) {
                rowLength = row.size();
            } else {
                assertEquals(
                    desc + " row " + (i + 1) + " has a different" + " length than previous rows:", rowLength, row.size()
                );
            }
            rows.add(row);
        }
        return rows;
    }

    /** Support comparing an expected value to an actual value. */
    interface CompareExpected
    {
        /**
         * Compares an actual value with the expected value represented by this object.
         *
         * @param actual the actual value
         * @return whether the actual value matches the expected value
         */
        boolean compareExpected(Object actual);
    }

    /** An object that represents a don't care value specified as an expected value. */
    static class DontCare implements CompareExpected
    {
        static final DontCare INSTANCE = new DontCare();

        private DontCare() {
        }

        @Override
        public boolean compareExpected(Object actual) {
            return true;
        }

        @Override
        public String toString() {
            return "!dc";
        }
    }

    /** A class that represents a regular expression specified as an expected value. */
    static class Regexp implements CompareExpected
    {
        private final Pattern pattern;

        Regexp(String pattern) {
            this.pattern = Pattern.compile(convertPattern(pattern));
            LOG.debug("Regexp: '{}' => '{}'", pattern, this.pattern);
        }

        @Override
        public boolean compareExpected(Object actual) {
            boolean result = pattern.matcher(String.valueOf(actual)).matches();
            LOG.debug("Regexp.compareExpected pattern='{}', actual='{}' => '{}'", new Object[]{pattern, actual, result});
            return result;
        }

        @Override
        public String toString() {
            return "!re '" + pattern + "'";
        }
    }

    /** Convert a pattern from the input format, with {N} for captured groups, to the \N format used by Java regexps. */
    static String convertPattern(String pattern) {
        if(!pattern.contains("{")) {
            return pattern;
        } else {
            /*
             * Replace {N} with \N.  To make sure that the '{' is not escaped,
             * require that the brace is either at the beginning of the input,
             * right after the last match, that the previous character is not a
             * backslash, or that the previous two characters are backslashes,
             * for an escaped backslash.  Note that backslashes need to be
             * doubled to get them into the string, and then doubled again for
             * regexp processing to treat them as literals.
             */
            return pattern.replaceAll("(\\A|\\G|[^\\\\]|\\\\\\\\)[{]([0-9]+)[}]", "$1\\\\$2");
        }
    }

    /** A SnakeYAML constructor that converts dc tags to DontCare.INSTANCE and re tags to Regexp instances. */
    private static class RegisterTags extends SafeConstructor
    {
        private boolean recursing;

        RegisterTags() {
            yamlConstructors.put(new Tag("!dc"), new ConstructDontCare());
            yamlConstructors.put(new Tag("!re"), new ConstructRegexp());
            yamlConstructors.put(new Tag("!select-engine"), new ConstructSelectEngine());
            yamlConstructors.put(new Tag("!date"), new ConstructSystemDate());
            yamlConstructors.put(new Tag("!time"), new ConstructSystemTime());
            yamlConstructors.put(new Tag("!datetime"), new ConstructSystemDateTime());
            yamlConstructors.put(new Tag("!unicode"), new ConstructUnicode());
            yamlConstructors.put(new Tag("!utf8-bytes"), new ConstructUTF8Bytes());
        }

        @Override
        protected Object constructObject(Node node) {
            if (recursing)
                return super.constructObject(node);
            else {
                recursing = true;
                Object o = super.constructObject(node);
                recursing = false;
                return new LinedObject(o, node.getStartMark());
            }
        }

        private class LinedObject {
            private Object o;
            private Mark startMark;

            private LinedObject(Object o, Mark startMark) {
                this.o = o;
                this.startMark = startMark;
            }

            public Object getObject() {
                return o;
            }

            public Mark getStartMark() {
                return startMark;
            }
        }

        private static class ConstructDontCare extends AbstractConstruct
        {
            @Override
            public Object construct(Node node) {
                return DontCare.INSTANCE;
            }
        }

        private static class ConstructRegexp extends AbstractConstruct
        {
            @Override
            public Object construct(Node node) {
                if(!(node instanceof ScalarNode)) {
                    fail("The value of the regular expression (!re) tag must" + " be a scalar");
                }
                return new Regexp(((ScalarNode)node).getValue());
            }
        }

        private class ConstructSelectEngine extends AbstractConstruct
        {
            @Override
            public Object construct(Node node) {
                if(!(node instanceof MappingNode)) {
                    fail("The value of the !select-engine tag must be a map" + "\nGot: " + node);
                }
                String matchingKey = null;
                Object result = null;
                for(NodeTuple tuple : ((MappingNode)node).getValue()) {
                    Node keyNode = tuple.getKeyNode();
                    if(!(keyNode instanceof ScalarNode)) {
                        fail("The key in a !select-engine map must be a scalar" + "\nGot: " + constructObject(keyNode));
                    }
                    String key = ((ScalarNode)keyNode).getValue();
                    if(IT_ENGINE.equals(key) || (matchingKey == null && ALL_ENGINE.equals(key))) {
                        matchingKey = key;
                        result = constructObject(tuple.getValueNode());
                    }
                }
                if(matchingKey != null) {
                    LOG.debug("Select engine: '{}' => '{}'", matchingKey, result);
                    return result;
                } else {
                    LOG.debug("Select engine: no match");
                    return null;
                }
            }
        }

        private static class ConstructSystemDate extends AbstractConstruct
        {
            @Override
            public Object construct(Node node) {
                Date today = new Date();
                return DEFAULT_DATE_FORMAT.format(today);
            }

        }

        private static class ConstructSystemTime extends AbstractConstruct
        {
            @Override
            public Object construct(Node node) {
                return new TimeChecker();
            }
        }

        private static class ConstructSystemDateTime extends AbstractConstruct
        {
            @Override
            public Object construct(Node node) {
                return new DateTimeChecker();
            }
        }

        private static class ConstructUnicode extends AbstractConstruct
        {
            @Override
            public Object construct(Node node) {
                if(!(node instanceof ScalarNode)) {
                    fail("The value of the Unicode tag must be a scalar");
                }
                return ((ScalarNode)node).getValue();
            }
        }

        private static class ConstructUTF8Bytes extends AbstractConstruct
        {
            @Override
            public Object construct(Node node) {
                if(!(node instanceof ScalarNode)) {
                    fail("The value of the UTF8Bytes tag must be a scalar");
                }
                try {
                    return ((ScalarNode)node).getValue().toString().getBytes("UTF-8");
                }
                catch (UnsupportedEncodingException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }

    }

    /** A class that compares a time value allowing a 1 minute window */
    static class TimeChecker implements CompareExpected
    {

        private static final int MINUTES_IN_SECONDS = 60;
        private static final int HOURS_IN_MINUTES = 60;

        @Override
        public boolean compareExpected(Object actual) {
            String[] timeAsString = String.valueOf(actual).split(":");
            Calendar localCalendar = Calendar.getInstance();
            localCalendar.setTimeInMillis(System.currentTimeMillis());

            long localTimeInSeconds = localCalendar.get(Calendar.HOUR_OF_DAY) * MINUTES_IN_SECONDS * HOURS_IN_MINUTES;
            localTimeInSeconds += localCalendar.get(Calendar.MINUTE) * MINUTES_IN_SECONDS;
            localTimeInSeconds += localCalendar.get(Calendar.SECOND);
            long resultTime = Integer.parseInt(timeAsString[0]) * MINUTES_IN_SECONDS * HOURS_IN_MINUTES;
            resultTime += Integer.parseInt(timeAsString[1]) * MINUTES_IN_SECONDS;
            resultTime += Integer.parseInt(timeAsString[2]);
            return Math.abs(resultTime - localTimeInSeconds) < MINUTES_IN_SECONDS;
        }

    }

    /** A class that compares a datetime value allowing a 1 minute window */
    static class DateTimeChecker implements CompareExpected
    {

        private static final int MINUTES_IN_SECONDS = 60;
        private static final int SECONDS_IN_MILLISECONDS = 1000;

        @Override
        public boolean compareExpected(Object actual) {
            Date now = new Date();
            Date date;
            try {
                date = DEFAULT_DATETIME_FORMAT.parse(String.valueOf(actual) + " UTC");
            } catch(ParseException e) {
                fail(e.getMessage());
                throw new AssertionError();
            }
            long testResult = date.getTime() - now.getTime();
            return Math.abs(testResult) < (1 * MINUTES_IN_SECONDS * SECONDS_IN_MILLISECONDS);
        }

    }

    private static void addTypeNameAndNumber(String name, int number) {
        typeNameToNumber.put(name, number);
        typeNumberToName.put(number, name);
    }

    private static String getTypeName(int typeNumber) {
        return typeNumberToName.get(typeNumber);
    }

    private static Integer getTypeNumber(String typeName) {
        return typeNameToNumber.get(typeName);
    }

    /** An assertion error that includes context information. */
    private class ContextAssertionError extends AssertionError
    {
        ContextAssertionError(String failedStatement, String message) {
            super(context(failedStatement) + message);
        }

        ContextAssertionError(String failedStatement, String message, Throwable cause) {
            super(context(failedStatement) + message);
            initCause(cause);
        }

        public ContextAssertionError(String statement, AssertionError e) {
            super(context(statement) + e.getMessage());
            initCause(e);
        }
    }

    private class FullOutputAssertionError extends ComparisonFailure {
        FullOutputAssertionError(List<List<?>> expected, List<List<?>> actual, AssertionError e) {
            super(e.getMessage(), Strings.join(expected,"\n"), Strings.join(actual,"\n"));
        }

    }

    private String context(String failedStatement) {
        StringBuilder context = new StringBuilder();
        if(filename != null) {
            context.append(filename);
        }
        if(!includeStack.isEmpty()) {
            for(String include : includeStack) {
                if(context.length() != 0) {
                    context.append(", ");
                }
                context.append("Include ").append(include);
            }
        }
        if(commandNumber != 0) {
            if(context.length() != 0) {
                context.append(", ");
            }
            context.append("Command ").append(commandNumber);
            context.append(" at line ").append(lineNumber);
            if(commandName != null) {
                context.append(" (").append(commandName);
                if(failedStatement != null) {
                    context.append(": <").append(failedStatement).append('>');
                }
                context.append(')');
            }
        }
        if(context.length() != 0) {
            context.append(": ");
        }
        return context.toString();
    }

    private void jmxCommand(Object value, List<?> sequence) throws SQLException {
        if(value != null) {
            new JMXCommand(value, sequence).execute();
        }
    }

    private class JMXCommand extends AbstractStatementCommand
    {
        ArrayList<Object> output = null;
        ArrayList<Object> split_output = null;
        String objectName = null;
        String[] params = null;
        String method = null;
        String set = null;
        String get = null;

        public JMXCommand(Object value, List<?> sequence) {
            super(string(value, "JMX value"));
            if(value != null & String.valueOf(value).trim().length() > 1) {
                objectName = String.valueOf(value).trim();
            } else {
                fail("Must provide an Object name");
            }

            for(int i = 1; i < sequence.size(); i++) {
                Entry<?,?> map = onlyEntry(
                    sequence.get(i), "JMX attribute"
                );
                String attribute = string(map.getKey(), "JMX attribute name");
                Object attributeValue = map.getValue();
                if("params".equals(attribute)) {
                    parseParams(attributeValue);
                } else if("method".equals(attribute)) {
                    method = String.valueOf(attributeValue).trim();
                } else if("set".equals(attribute)) {
                    set = String.valueOf(attributeValue).trim();
                } else if("get".equals(attribute)) {
                    get = String.valueOf(attributeValue).trim();
                } else if("output".equals(attribute)) {
                    parseOutput(attributeValue);
                } else if("split_result".equals(attribute)) {
                    parseSplit(attributeValue);
                } else {
                    fail("The '" + attribute + "' attribute name was not" + " recognized");
                }
            }

        }

        private void parseParams(Object value) {
            if(value == null) {
                return;
            }
            assertNull("The params attribute must not appear more than once", params);
            ArrayList<String> list = new ArrayList<>(stringSequence(value, "params value"));
            params = list.toArray(new String[list.size()]);
        }

        private void parseSplit(Object value) {
            if(value == null) {
                split_output = null;
                return;
            }
            assertNull("The split_result attribute must not appear more than once", split_output);
            assertNull("The output and split_result attributes can not appear together", output);
            List<List<?>> rows = rows(value, "output split value");
            split_output = new ArrayList<>(rows.size());
            for(List<?> row : rows) {
                assertEquals("number of entries in row " + row, 1, row.size());
                split_output.add(row.get(0));
            }
        }

        private void parseOutput(Object value) {
            if(value == null) {
                output = null;
                return;
            }
            assertNull("The output attribute must not appear more than once", output);
            assertNull("The split_result and output attributes can not appear together", split_output);

            List<List<?>> rows = rows(value, "output value");
            output = new ArrayList<>(rows.size());
            for(List<?> row : rows) {
                assertEquals("number of entries in row " + row, 1, row.size());
                output.add(row.get(0));
            }
        }

        public void execute() {
            Object result = null;
            try(JMXInterpreter conn = new JMXInterpreter()) {
                if(method != null) {
                    result = conn.makeBeanCall("localhost", 8082, objectName, method, params, "method");
                }
                if(set != null) {
                    conn.makeBeanCall("localhost", 8082, objectName, set, params, "set");
                }
                if(get != null) {
                    result = conn.makeBeanCall("localhost", 8082, objectName, get, params, "get");
                }
            } catch(Exception e) {
                LOG.debug("Caught making JMX call", e);
                fail("Error: " + e.getMessage());
            }

            if(split_output != null) {
                if(result == null) {
                    fail("found null; expected: " + split_output);
                }
                List<Object> actual = new ArrayList<Object>(Arrays.asList(result.toString().split("\\n")));
                int highestCommon = Math.min(actual.size(), split_output.size());
                for(int i = 0; i < highestCommon; ++i) {
                    if(split_output.get(i) == DontCare.INSTANCE) {
                        actual.set(i, DontCare.INSTANCE);
                    }
                }
                assertCollectionEquals(split_output, actual);
            } else if(output != null) {
                if(result == null) {
                    fail("found null; expected: " + output);
                }
                List<Object> actual = new ArrayList<Object>(Arrays.asList(result.toString()));
                int highestCommon = Math.min(actual.size(), output.size());
                for(int i = 0; i < highestCommon; i++) {
                    if(output.get(i) == DontCare.INSTANCE) {
                        actual.set(i, DontCare.INSTANCE);
                    }
                }
                assertCollectionEquals(output, actual);
            }
        }
    }
}
TOP

Related Classes of com.foundationdb.sql.pg.YamlTester

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.