DefaultPrepAndExpectedTestCase.java

/*
 *
 * The DbUnit Database Testing Framework
 * Copyright (C)2002-2008, DbUnit.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
package org.dbunit;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

import org.dbunit.assertion.FailureHandler;
import org.dbunit.assertion.comparer.value.ValueComparer;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.connection.AutoCommitOffWarning;
import org.dbunit.database.connection.ConnectionOwnership;
import org.dbunit.database.connection.TestScopedConnection;
import org.dbunit.database.rowcount.RowCountCheck;
import org.dbunit.database.rowcount.RowCountChecker;
import org.dbunit.dataset.Column;
import org.dbunit.dataset.CompositeDataSet;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ITableMetaData;
import org.dbunit.dataset.SortedTable;
import org.dbunit.dataset.datatype.DataType;
import org.dbunit.dataset.filter.DefaultColumnFilter;
import org.dbunit.operation.DatabaseOperation;
import org.dbunit.util.TableFormatter;
import org.dbunit.util.fileloader.DataFileLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Test case base class supporting prep data and expected data. Prep data is the
 * data needed for the test to run. Expected data is the data needed to compare
 * if the test ran successfully.
 * <p>
 * configureTest(), setupData(), verifyData(), and cleanupData() share one
 * {@link org.dbunit.database.IDatabaseConnection} for a test's lifecycle,
 * acquired lazily on first use and closed once by cleanupData(), instead of
 * each acquiring (and often closing) its own. Calling any of those methods
 * without an eventual cleanupData() call - e.g. testing them individually
 * rather than through preTest()/postTest() - leaves that connection open.
 * <p>
 * If databaseTester is configured with a
 * {@link org.dbunit.database.CachingConnectionProvider} shared across test
 * methods, set {@link #setCloseConnectionAfterTest(boolean)} to false so
 * cleanupData() does not close a connection other tests still expect to
 * reuse; the provider's owner is then responsible for closing it once,
 * itself, when the whole run finishes.
 * <p>
 * The connection must be in autocommit mode. The setup and teardown
 * operations run here (CLEAN_INSERT, DELETE_ALL, ...) do not manage a
 * transaction of their own, and {@link org.dbunit.operation.TransactionOperation}
 * refuses a connection whose autocommit is already off, so on a
 * non-autocommit connection the prep and teardown writes are never
 * committed: invisible to any other connection, and - with
 * {@link #setCloseConnectionAfterTest(boolean)} false - held as locks on a
 * connection the database's idle-in-transaction timeout may terminate
 * mid-run. This class logs a warning when it detects such a connection.
 * <p>
 * The {@code verifyData()} method hands assertion failures to
 * {@link org.dbunit.assertion.DefaultFailureHandler} by default, which throws
 * on the first mismatch found. Set {@link #setFailureHandler(FailureHandler)}
 * to, for example, a {@link org.dbunit.assertion.DiffCollectingFailureHandler}
 * to collect every {@link org.dbunit.assertion.Difference} instead; that is
 * not the default since most tests want to keep failing fast.
 *
 * @see "org.dbunit.DefaultPrepAndExpectedTestCaseDiIT, a composition-based (DI) usage example in the test sources"
 * @see "org.dbunit.DefaultPrepAndExpectedTestCaseExtIT, an inheritance-based usage example in the test sources"
 *
 * @author Jeff Jensen jeffjensen AT users.sourceforge.net
 * @author Last changed by: $Author$
 * @version $Revision$ $Date$
 * @since 2.4.8
 */
public class DefaultPrepAndExpectedTestCase extends DBTestCase
        implements PrepAndExpectedTestCase
{
    private final Logger log =
            LoggerFactory.getLogger(DefaultPrepAndExpectedTestCase.class);

    private static final String DATABASE_TESTER_IS_NULL_MSG =
            "databaseTester is null; must configure or set it first";

    /** Message prefix used for wrapped test failures. */
    public static final String TEST_ERROR_MSG = "DbUnit test error.";

    private IDatabaseTester databaseTester;
    private DataFileLoader dataFileLoader;

    /**
     * Whether lookupFeatureValue() and cleanupData() close the connection
     * they are done with; false when databaseTester shares a
     * CachingConnectionProvider across test methods and this instance must
     * not close a connection other tests still expect to reuse.
     *
     * @since 3.4.0
     */
    private boolean closeConnectionAfterTest = true;

    // per test data
    private IDataSet prepDataSet = new DefaultDataSet();
    private IDataSet expectedDataSet = new DefaultDataSet();
    private VerifyTableDefinition[] verifyTableDefs = {};

    /**
     * Connection shared by setupData()/verifyData()/cleanupData() for one
     * test's lifecycle instead of each acquiring (and often closing) its own:
     * acquired lazily on first use from {@link #getConnection()}, closed once
     * by cleanupData() unless {@link #closeConnectionAfterTest} is false or
     * {@link #getOperationListener()} is the no-op listener, and re-acquired if
     * the pool or server closed it between reused test methods.
     *
     * @since 3.6.0
     */
    private final TestScopedConnection reusableConnectionHolder = newReusableConnectionHolder();

    /**
     * Builds {@link #reusableConnectionHolder}. Its {@link ConnectionOwnership}
     * reads {@link #closeConnectionAfterTest} and {@link #getOperationListener()}
     * fresh at release time; its third input - whether the borrowing lifecycle
     * ran - is always {@code true} here, since this class only ever releases the
     * connection from its own cleanupData(), which runs only after setupData()
     * already acquired it. Since 3.6.0 an {@link #getOperationListener()} that is
     * (or wraps) {@link IOperationListener#NO_OP_OPERATION_LISTENER} keeps
     * cleanupData() from closing the connection even when
     * {@link #closeConnectionAfterTest} is true - the established signal that the
     * connection is owned elsewhere, now honored on this path too and not only
     * by the annotation runtime.
     *
     * @return The holder.
     */
    private TestScopedConnection newReusableConnectionHolder()
    {
        final ConnectionOwnership ownership = new ConnectionOwnership(
                () -> closeConnectionAfterTest, this::getOperationListener, () -> true);
        return new TestScopedConnection(this::getConnection, ownership,
                new AutoCommitOffWarning());
    }

    /**
     * isCaseSensitiveTableNames as resolved by configureTest(), cached so
     * cleanupData() does not need a second connection just to re-read this
     * same DatabaseConfig feature flag.
     *
     * @since 3.4.0
     */
    private Boolean cachedIsCaseSensitiveTableNames;

    /**
     * Manages the row count check baseline used by preTest() and cleanupData() to detect a
     * table the test left dirty - either one it should have cleaned up and did not, or a
     * reference table it wrongly cleaned.
     *
     * @since 3.6.0
     */
    private final RowCountChecker rowCountChecker = new RowCountChecker();

    private ExpectedDataSetAndVerifyTableDefinitionVerifier expectedDataSetAndVerifyTableDefinitionVerifier =
            new DefaultExpectedDataSetAndVerifyTableDefinitionVerifier();

    /**
     * FailureHandler for verifyData()'s assertion failures. Null (the
     * default) leaves compareData() using
     * {@link org.dbunit.Assertion#assertWithValueComparer(ITable, ITable, Column[], ValueComparer, Map)}'s
     * own {@link org.dbunit.assertion.DefaultFailureHandler}, configured with
     * the additionalColumnInfo computed by {@link #makeAdditionalColumnInfo};
     * that default is intentional so most tests keep failing fast on the
     * first mismatch. Set this, for example to a
     * {@link org.dbunit.assertion.DiffCollectingFailureHandler}, only when a
     * test needs to collect every {@link org.dbunit.assertion.Difference}
     * instead.
     *
     * @since 3.5.0
     */
    private FailureHandler failureHandler;

    /**
     * DatabaseConfig property name/value pairs applied to the connection shared by
     * setupData(), verifyData() and cleanupData(), every time {@link #getConnection()}
     * resolves it - see {@link #setUpDatabaseConfig(DatabaseConfig)}. Null (the default)
     * applies none.
     *
     * @since 3.6.0
     */
    private Properties databaseConfigProperties;

    final TableFormatter tableFormatter = new TableFormatter();

    /** Create new instance. */
    public DefaultPrepAndExpectedTestCase()
    {
    }

    /**
     * Create new instance with specified dataFileLoader and databaseTester.
     *
     * @param dataFileLoader
     *            Load to use for loading the data files.
     * @param databaseTester
     *            Tester to use for database manipulation.
     */
    public DefaultPrepAndExpectedTestCase(final DataFileLoader dataFileLoader,
            final IDatabaseTester databaseTester)
    {
        this.dataFileLoader = dataFileLoader;
        this.databaseTester = databaseTester;
    }

    /**
     * Create new instance with specified dataFileLoader and databaseTester.
     *
     * @param dataFileLoader
     *            Load to use for loading the data files.
     * @param databaseTester
     *            Tester to use for database manipulation.
     * @param closeConnectionAfterTest
     *            Whether or not to close the database connection after each test.
     *
     * @since 3.4.0
     */
    public DefaultPrepAndExpectedTestCase(final DataFileLoader dataFileLoader,
            final IDatabaseTester databaseTester,
            final boolean closeConnectionAfterTest)
    {
        this.dataFileLoader = dataFileLoader;
        this.databaseTester = databaseTester;
        this.closeConnectionAfterTest = closeConnectionAfterTest;
    }

    /**
     * Create new instance with specified test case name.
     *
     * @param name
     *            The test case name.
     */
    public DefaultPrepAndExpectedTestCase(final String name)
    {
        super(name);
    }

    /**
     * {@inheritDoc} This implementation returns the databaseTester set by the
     * test.
     */
    @Override
    public IDatabaseTester newDatabaseTester() throws Exception
    {
        // questionable, but there is not a "setter" for any parent...
        return databaseTester;
    }

    /**
     * {@inheritDoc} Returns the prep dataset.
     */
    @Override
    public IDataSet getDataSet() throws Exception
    {
        return prepDataSet;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Executes against the connection shared with setupData(), verifyData()
     * and cleanupData() for this test's lifecycle rather than a fresh one,
     * and leaves it open; cleanupData() closes it. See #800.
     */
    @Override
    public void configureTest(
            final VerifyTableDefinition[] verifyTableDefinitions,
            final String[] prepDataFiles, final String[] expectedDataFiles)
            throws Exception
    {
        log.debug("configureTest: saving instance variables");

        final boolean isCaseSensitiveTableNames = lookupFeatureValue(
                DatabaseConfig.FEATURE_CASE_SENSITIVE_TABLE_NAMES);
        log.debug("configureTest: using case sensitive table names={}",
                isCaseSensitiveTableNames);
        this.cachedIsCaseSensitiveTableNames = isCaseSensitiveTableNames;

        this.prepDataSet = makeCompositeDataSet(prepDataFiles, "prep",
                isCaseSensitiveTableNames);
        this.expectedDataSet = makeCompositeDataSet(expectedDataFiles,
                "expected", isCaseSensitiveTableNames);

        this.verifyTableDefs = verifyTableDefinitions;
    }

    /**
     * {@inheritDoc} Applies {@link #databaseConfigProperties}, when set - the composition-based
     * equivalent of overriding this method, for a caller that cannot subclass to do so directly
     * (e.g. {@code org.dbunit.annotation}'s {@code @DbUnitProperty}).
     *
     * <p><strong>Note:</strong> a subclass overriding this method must call
     * {@code super.setUpDatabaseConfig(config)} to keep
     * {@link #setDatabaseConfigProperties(java.util.Properties)} - and therefore
     * {@code @DbUnitProperty} on the annotation-driven path - working.
     *
     * @throws IllegalStateException If a property value is invalid for its target
     *             {@link DatabaseConfig} entry.
     */
    @Override
    protected void setUpDatabaseConfig(final DatabaseConfig config)
    {
        if (databaseConfigProperties == null || databaseConfigProperties.isEmpty())
        {
            return;
        }
        try
        {
            config.setPropertiesByString(databaseConfigProperties);
        } catch (final DatabaseUnitException e)
        {
            throw new IllegalStateException("Failed to apply a databaseConfigProperties value.",
                    e);
        }
    }

    private boolean lookupFeatureValue(final String featureName)
            throws Exception
    {
        final boolean acquiredConnectionHere = reusableConnectionHolder.peekConnection() == null;
        try
        {
            final IDatabaseConnection reusableConnection =
                    getReusableConnection();
            final DatabaseConfig config = reusableConnection.getConfig();
            return config.getFeature(featureName);
        } catch (final Exception e)
        {
            if (acquiredConnectionHere)
            {
                closeReusableConnectionSuppressing(e);
            }
            throw e;
        }
    }

    /**
     * Return the connection shared by lookupFeatureValue(), setupData(),
     * verifyData() and cleanupData() for the current test's lifecycle,
     * acquiring it on first use instead of a fresh connection at each step.
     * <p>
     * When {@link #closeConnectionAfterTest} is false this connection is kept
     * across test methods, where the connection pool or the database server can
     * close it between tests - a pool max-lifetime or reap, a bounced
     * application context, a
     * {@link org.dbunit.database.CachingConnectionProvider#close()}. A closed
     * one is discarded before it is handed back, so the next call re-acquires
     * from databaseTester - letting a
     * {@link org.dbunit.database.CachingConnectionProvider} behind it supply a
     * live replacement - rather than this instance reusing a connection every
     * later lifecycle step would only fail on.
     *
     * @return The shared connection.
     * @throws Exception On dbUnit errors.
     * @since 3.4.0
     */
    @Override
    public IDatabaseConnection getReusableConnection() throws Exception
    {
        return reusableConnectionHolder.getConnection();
    }

    /**
     * Release the connection shared by lookupFeatureValue(), setupData(),
     * verifyData() and cleanupData(), if one was acquired: closes it and
     * forgets it when {@link #closeConnectionAfterTest} is true (the default)
     * and {@link #getOperationListener()} is not the no-op listener; otherwise
     * leaves it open for its real owner - a
     * {@link org.dbunit.database.CachingConnectionProvider}, an
     * externally-supplied fixed connection - and keeps it memoized so a later
     * lifecycle step reuses it rather than orphaning another. A kept connection
     * the pool or server has since closed is dropped and re-acquired on the
     * next {@link #getReusableConnection()}.
     *
     * @throws Exception On close errors.
     * @since 3.4.0
     */
    private void closeReusableConnection() throws Exception
    {
        reusableConnectionHolder.release();
    }

    /**
     * Close the reusable connection, attaching any close failure to the given
     * primary throwable via {@link Throwable#addSuppressed(Throwable)} rather
     * than letting it replace and hide the primary. Mirrors the exception
     * safety of {@link #runTest} and {@code DatabaseTestCase.tearDown(Throwable)}.
     * <p>
     * Only ever called from a lifecycle step that has already failed, so it also
     * forgets the shared connection even when {@link #closeConnectionAfterTest}
     * is false and {@link #closeReusableConnection()} therefore left it open: a
     * step that just threw may have broken it, so the next
     * {@link #getReusableConnection()} re-acquires rather than reusing it. Any
     * {@link org.dbunit.database.CachingConnectionProvider} behind
     * {@code databaseTester} still owns closing it.
     *
     * @param primary
     *            The exception already in flight to attach a close failure
     *            to.
     * @since 3.4.0
     */
    private void closeReusableConnectionSuppressing(final Throwable primary)
    {
        reusableConnectionHolder.releaseSuppressing(primary);
    }

    /**
     * Make a {@link ReusableConnectionDatabaseTester} configured with the
     * given dataset and databaseTester's setUpOperation and
     * tearDownOperation, for setupData() or cleanupData() to run
     * {@link IDatabaseTester#onSetup()} or {@link IDatabaseTester#onTearDown()}
     * on, respectively. Setting both operations regardless of which one the
     * caller uses is safe: {@link AbstractDatabaseTester#onSetup()} only
     * reads setUpOperation and {@link AbstractDatabaseTester#onTearDown()}
     * only reads tearDownOperation, so the other is simply never consulted.
     *
     * @param dataSet
     *            The dataset to run the operation against.
     * @return The configured tester.
     * @throws Exception On dbUnit errors.
     * @since 3.4.0
     */
    private IDatabaseTester makeReusableConnectionDatabaseTester(
            final IDataSet dataSet) throws Exception
    {
        final IDatabaseTester reusableTester =
                new ReusableConnectionDatabaseTester(
                        this::getReusableConnection);
        reusableTester.setSetUpOperation(getSetUpOperation());
        reusableTester.setTearDownOperation(getTearDownOperation());
        reusableTester.setDataSet(dataSet);
        // This instance, not the listener, owns the shared connection's lifecycle (see
        // getReusableConnection()/closeReusableConnection()), so a blanket
        // ConnectionPreservingOperationListener keeps operationSetUpFinished/
        // operationTearDownFinished from closing it while still forwarding connectionRetrieved
        // so a user-defined listener runs its connection-configuration logic.
        reusableTester.setOperationListener(
                new ConnectionPreservingOperationListener(getOperationListener()));
        return reusableTester;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Captures the row count check baseline, if enabled, before setting up the prep data - see
     * {@link RowCountCheck#capture(IDatabaseConnection)}.
     */
    @Override
    public void preTest() throws Exception
    {
        captureRowCountBaseline();
        setupData();
    }

    /**
     * Capture the row count check baseline, using the connection shared with the rest of this
     * test's lifecycle. A no-op that leaves no baseline captured when the check is disabled.
     *
     * @throws Exception On dbUnit errors.
     * @since 3.6.0
     */
    private void captureRowCountBaseline() throws Exception
    {
        final boolean acquiredConnectionHere = reusableConnectionHolder.peekConnection() == null;
        try
        {
            rowCountChecker.capture(getReusableConnection());
        } catch (final Exception e)
        {
            if (acquiredConnectionHere)
            {
                closeReusableConnectionSuppressing(e);
            }
            throw e;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void preTest(final VerifyTableDefinition[] tables,
            final String[] prepDataFiles, final String[] expectedDataFiles)
            throws Exception
    {
        configureTest(tables, prepDataFiles, expectedDataFiles);
        preTest();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object runTest(final VerifyTableDefinition[] verifyTables,
            final String[] prepDataFiles, final String[] expectedDataFiles,
            final PrepAndExpectedTestCaseSteps testSteps) throws Exception
    {
        final Object result;

        try
        {
            preTest(verifyTables, prepDataFiles, expectedDataFiles);
            log.info("runTest: running test steps");
            result = runTestSteps(testSteps);
        } catch (final Throwable e)
        {
            log.error(TEST_ERROR_MSG, e);
            // don't verify table data when test execution has errors as:
            // * a verify data failure masks the test error exception
            // * tables in unknown state and therefore probably not accurate
            try
            {
                postTest(false);
            } catch (final Throwable cleanupFailure)
            {
                // never let a cleanup failure replace and hide the real test
                // failure; keep e as the thrown exception, cleanupFailure alongside
                e.addSuppressed(cleanupFailure);
            }
            throw e;
        }

        postTest();

        return result;
    }

    /**
     * Run the provided test steps. Override as necessary for custom logic.
     *
     * @param testSteps
     *            The test steps to run.
     * @return the user-defined object returned by the test steps.
     * @throws Exception if the test steps fail.
     */
    protected Object runTestSteps(final PrepAndExpectedTestCaseSteps testSteps)
            throws Exception
    {
        return testSteps.run();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void postTest() throws Exception
    {
        postTest(true);
    }

    /**
     * {@inheritDoc}
     * <p>
     * When {@code verifyData} is false - the test steps already failed - discards any
     * captured row count check baseline, so cleanupData() skips that check too: the database
     * is in an unknown state, so a count difference would be noise around the real failure,
     * not a finding worth its own report.
     */
    @Override
    public void postTest(final boolean verifyData) throws Exception
    {
        Throwable verifyFailure = null;
        try
        {
            if (verifyData)
            {
                verifyData();
            } else
            {
                rowCountChecker.discardBaseline();
            }
        } catch (final Throwable t)
        {
            verifyFailure = t;
            throw t;
        } finally
        {
            // it is deliberate to have cleanup exceptions shadow verify
            // failures so user knows db is probably in unknown state (for
            // those not using an in-memory db or transaction rollback),
            // otherwise would mask probable cause of subsequent test
            // failures; the verify failure rides along as suppressed on the
            // cleanup exception so it is not lost entirely
            try
            {
                cleanupData();
            } catch (final Throwable cleanupFailure)
            {
                if (verifyFailure != null)
                {
                    cleanupFailure.addSuppressed(verifyFailure);
                }
                throw cleanupFailure;
            }
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Runs the tear down operation against the connection shared with
     * setupData() and verifyData() for this test's lifecycle, then closes it.
     * See #800.
     */
    @Override
    public void cleanupData() throws Exception
    {
        try
        {
            final boolean isCaseSensitiveTableNames;
            if (cachedIsCaseSensitiveTableNames == null)
            {
                isCaseSensitiveTableNames = lookupFeatureValue(
                        DatabaseConfig.FEATURE_CASE_SENSITIVE_TABLE_NAMES);
            } else
            {
                isCaseSensitiveTableNames = cachedIsCaseSensitiveTableNames;
            }
            log.debug("cleanupData: using case sensitive table names={}",
                    isCaseSensitiveTableNames);

            final IDataSet[] dataSets = {prepDataSet, expectedDataSet};
            final IDataSet dataset = new CompositeDataSet(dataSets, true,
                    isCaseSensitiveTableNames);
            final String[] tableNames = dataset.getTableNames();
            final int count = tableNames.length;
            log.info("cleanupData: about to clean up {} tables={}", count,
                    tableNames);

            if (databaseTester == null)
            {
                throw new IllegalStateException(DATABASE_TESTER_IS_NULL_MSG);
            }

            final IDatabaseTester reusableTester =
                    makeReusableConnectionDatabaseTester(dataset);
            reusableTester.onTearDown();
            log.debug("cleanupData: Clean up done");
            verifyRowCountUnchanged();
            closeReusableConnection();
        } catch (final Exception e)
        {
            log.error("cleanupData: Exception:", e);
            closeReusableConnectionSuppressing(e);
            throw e;
        }
    }

    /**
     * Verify the row count check baseline, if one was captured, using the connection shared
     * with the rest of this test's lifecycle. A no-op when no baseline was captured - the
     * check is disabled, capture never ran, or {@link #postTest(boolean)} discarded it
     * because the test steps already failed.
     *
     * @throws Exception On dbUnit errors, including {@link org.dbunit.database.rowcount.UnexpectedRowCountException}
     *             when a table's row count no longer matches the baseline.
     * @since 3.6.0
     */
    private void verifyRowCountUnchanged() throws Exception
    {
        rowCountChecker.verify(getReusableConnection());
    }

    /**
     * Legacy JUnit-3-era tear-down hook. Not invoked automatically under JUnit 5;
     * kept for subclasses that drive the lifecycle manually. Calling it after a
     * full {@link #runTest} or {@link #postTest} cycle cleans up a second time
     * (parent tearDown() re-runs the tear down operation on the prep dataset with
     * a fresh connection).
     */
    @Override
    protected void tearDown() throws Exception
    {
        // parent tearDown() only cleans up prep data
        cleanupData();
        super.tearDown();
    }

    /**
     * Use the provided databaseTester to prep the database with the provided
     * prep dataset. See {@link org.dbunit.IDatabaseTester#onSetup()}.
     * <p>
     * Executes against the connection shared with verifyData() and
     * cleanupData() for this test's lifecycle rather than a fresh one, and
     * leaves it open; cleanupData() closes it. See #800.
     *
     * @throws Exception if preparing the data fails.
     */
    public void setupData() throws Exception
    {
        log.info("setupData: setting prep dataset and inserting rows");
        if (databaseTester == null)
        {
            throw new IllegalStateException(DATABASE_TESTER_IS_NULL_MSG);
        }

        try
        {
            final IDatabaseTester reusableTester =
                    makeReusableConnectionDatabaseTester(getDataSet());
            reusableTester.onSetup();
        } catch (final Exception e)
        {
            log.error("setupData: Exception with setting up data:", e);
            throw e;
        }
    }

    @Override
    protected DatabaseOperation getSetUpOperation() throws Exception
    {
        if (databaseTester == null)
        {
            throw new IllegalStateException(DATABASE_TESTER_IS_NULL_MSG);
        }
        return databaseTester.getSetUpOperation();
    }

    @Override
    protected DatabaseOperation getTearDownOperation() throws Exception
    {
        if (databaseTester == null)
        {
            throw new IllegalStateException(DATABASE_TESTER_IS_NULL_MSG);
        }
        return databaseTester.getTearDownOperation();
    }

    /**
     * {@inheritDoc} Uses the connection from the provided databaseTester,
     * shared with setupData() and cleanupData() for this test's lifecycle.
     * Left open on return; cleanupData() closes it. See #800.
     */
    @Override
    public void verifyData() throws Exception
    {
        if (databaseTester == null)
        {
            throw new IllegalStateException(DATABASE_TESTER_IS_NULL_MSG);
        }

        final IDatabaseConnection reusableConnection = getReusableConnection();

        final DatabaseConfig config = reusableConnection.getConfig();
        expectedDataSetAndVerifyTableDefinitionVerifier.verify(verifyTableDefs,
                expectedDataSet, config);

        try
        {
            final int tableDefsCount = verifyTableDefs.length;
            if (tableDefsCount == 0)
            {
                log.info("verifyData: No tables to verify as"
                        + " no VerifyTableDefinitions specified");
            } else
            {
                log.info(
                        "verifyData: about to verify {} tables"
                                + " using verifyTableDefinitions={}",
                        tableDefsCount, verifyTableDefs);
            }

            for (int i = 0; i < tableDefsCount; i++)
            {
                final VerifyTableDefinition td = verifyTableDefs[i];
                verifyData(reusableConnection, td);
            }
        } catch (final Exception e)
        {
            log.error("verifyData: Exception:", e);
            throw e;
        }
    }

    /**
     * Verifies a single table's actual data against its expected data.
     *
     * @param connection the database connection to load the actual data from.
     * @param verifyTableDefinition the table definition to verify.
     * @throws Exception if verifying the table fails.
     */
    protected void verifyData(final IDatabaseConnection connection,
            final VerifyTableDefinition verifyTableDefinition) throws Exception
    {
        final String tableName = verifyTableDefinition.getTableName();
        log.debug("verifyData: Verifying table '{}'", tableName);

        final String[] excludeColumns =
                verifyTableDefinition.getColumnExclusionFilters();
        final String[] includeColumns =
                verifyTableDefinition.getColumnInclusionFilters();
        final Map<String, ValueComparer> columnValueComparers =
                verifyTableDefinition.getColumnValueComparers();
        final ValueComparer defaultValueComparer =
                verifyTableDefinition.getDefaultValueComparer();
        final boolean sortOnFilteredColumnsOnly =
                verifyTableDefinition.isSortOnFilteredColumnsOnly();

        final ITable expectedTable = loadTableDataFromDataSet(tableName);
        final ITable actualTable =
                loadTableDataFromDatabase(tableName, connection);

        if (sortOnFilteredColumnsOnly)
        {
            verifyData(expectedTable, actualTable, excludeColumns,
                    includeColumns, defaultValueComparer, columnValueComparers,
                    true);
        } else
        {
            verifyData(expectedTable, actualTable, excludeColumns,
                    includeColumns, defaultValueComparer,
                    columnValueComparers);
        }
    }

    /**
     * Loads the given table's expected data from the expected dataset.
     *
     * @param tableName the name of the table to load.
     * @return the table's expected data.
     * @throws DataSetException if loading the table fails.
     */
    public ITable loadTableDataFromDataSet(final String tableName)
            throws DataSetException
    {
        ITable table = null;

        final String methodName = "loadTableDataFromDataSet";

        log.debug("{}: Loading table {} from expected dataset", methodName,
                tableName);
        try
        {
            table = expectedDataSet.getTable(tableName);
        } catch (final Exception e)
        {
            final String msg = methodName + ": Problem obtaining table '"
                    + tableName + "' from expected dataset";
            log.error(msg, e);
            throw new DataSetException(msg, e);
        }
        return table;
    }

    /**
     * Loads the given table's actual data from the database.
     *
     * @param tableName the name of the table to load.
     * @param connection the database connection to load the data from.
     * @return the table's actual data.
     * @throws Exception if loading the table fails.
     */
    public ITable loadTableDataFromDatabase(final String tableName,
            final IDatabaseConnection connection) throws Exception
    {
        ITable table = null;

        final String methodName = "loadTableDataFromDatabase";

        log.debug("{}: Loading table {} from database", methodName, tableName);
        try
        {
            table = connection.createTable(tableName);
        } catch (final Exception e)
        {
            final String msg = methodName + ": Problem obtaining table '"
                    + tableName + "' from database";
            log.error(msg, e);
            throw new DataSetException(msg, e);
        }
        return table;
    }

    /**
     * For the specified expected and actual tables (and excluding and including
     * the specified columns), verify the actual data is as expected.
     *
     * @param expectedTable
     *            The expected table to compare the actual table to.
     * @param actualTable
     *            The actual table to compare to the expected table.
     * @param excludeColumns
     *            The column names to exclude from comparison. See
     *            {@link org.dbunit.dataset.filter.DefaultColumnFilter#excludeColumn(String)}
     *            .
     * @param includeColumns
     *            The column names to only include in comparison. See
     *            {@link org.dbunit.dataset.filter.DefaultColumnFilter#includeColumn(String)}
     *            .
     * @param defaultValueComparer
     *            {@link ValueComparer} to use with column value comparisons
     *            when the column name for the table is not in the
     *            columnValueComparers {@link Map}. Can be <code>null</code> and
     *            will default.
     * @param columnValueComparers
     *            {@link Map} of {@link ValueComparer}s to use for specific
     *            columns. Key is column name, value is the
     *            {@link ValueComparer}. Can be <code>null</code> and will
     *            default to defaultValueComparer for all columns in all tables.
     * @throws DatabaseUnitException if the tables' row counts, columns, or data do not match.
     * @see #verifyData(ITable, ITable, String[], String[], ValueComparer, Map, boolean)
     *      to also control whether sorting considers only the filtered
     *      columns; this overload always sorts by all native columns.
     */
    protected void verifyData(final ITable expectedTable,
            final ITable actualTable, final String[] excludeColumns,
            final String[] includeColumns,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers)
            throws DatabaseUnitException
    {
        verifyData(expectedTable, actualTable, excludeColumns, includeColumns,
                defaultValueComparer, columnValueComparers, false);
    }

    /**
     * For the specified expected and actual tables (and excluding and including
     * the specified columns), verify the actual data is as expected.
     *
     * @param expectedTable
     *            The expected table to compare the actual table to.
     * @param actualTable
     *            The actual table to compare to the expected table.
     * @param excludeColumns
     *            The column names to exclude from comparison. See
     *            {@link org.dbunit.dataset.filter.DefaultColumnFilter#excludeColumn(String)}
     *            .
     * @param includeColumns
     *            The column names to only include in comparison. See
     *            {@link org.dbunit.dataset.filter.DefaultColumnFilter#includeColumn(String)}
     *            .
     * @param defaultValueComparer
     *            {@link ValueComparer} to use with column value comparisons
     *            when the column name for the table is not in the
     *            columnValueComparers {@link Map}. Can be <code>null</code> and
     *            will default.
     * @param columnValueComparers
     *            {@link Map} of {@link ValueComparer}s to use for specific
     *            columns. Key is column name, value is the
     *            {@link ValueComparer}. Can be <code>null</code> and will
     *            default to defaultValueComparer for all columns in all tables.
     * @param sortOnFilteredColumnsOnly
     *            True to sort the expected and actual tables by only the
     *            columns that survive excludeColumns/includeColumns, instead
     *            of by all of the actual table's native columns; see
     *            {@link VerifyTableDefinition#isSortOnFilteredColumnsOnly()}.
     * @throws DatabaseUnitException if the tables' row counts, columns, or data do not match.
     * @since 3.5.0
     */
    protected void verifyData(final ITable expectedTable,
            final ITable actualTable, final String[] excludeColumns,
            final String[] includeColumns,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers,
            final boolean sortOnFilteredColumnsOnly)
            throws DatabaseUnitException
    {
        final String methodName = "verifyData";

        final ITableMetaData actualTableMetaData =
                actualTable.getTableMetaData();
        final ITableMetaData expectedTableMetaData =
                expectedTable.getTableMetaData();

        final Column[] actualTableColumns = actualTableMetaData.getColumns();
        final Column[] expectedTableColumns = makeExpectedTableColumns(
                actualTableColumns, expectedTableMetaData);

        final Column[] actualSortColumns;
        final Column[] expectedSortColumns;
        if (sortOnFilteredColumnsOnly)
        {
            log.debug("{}: Sorting using only filtered columns", methodName);
            final String tableName = actualTableMetaData.getTableName();
            actualSortColumns = makeSortColumns(actualTableColumns,
                    excludeColumns, includeColumns, tableName);
            expectedSortColumns = makeSortColumns(expectedTableColumns,
                    excludeColumns, includeColumns, tableName);
        } else
        {
            log.debug("{}: Sorting using all columns", methodName);
            actualSortColumns = actualTableColumns;
            expectedSortColumns = expectedTableColumns;
        }

        final SortedTable expectedSortedTable =
                new SortedTable(expectedTable, expectedSortColumns, true);
        expectedSortedTable.setUseComparable(true);
        log.trace("{}: Sorted expected table={}", methodName,
                expectedSortedTable);

        final SortedTable actualSortedTable =
                new SortedTable(actualTable, actualSortColumns);
        actualSortedTable.setUseComparable(true);
        log.trace("{}: Sorted actual table={}", methodName, actualSortedTable);

        // Filter out the columns from the expected and actual results
        log.debug(
                "{}: Applying column exclude and include filters to sorted expected table",
                methodName);
        final ITable expectedFilteredTable = applyColumnFilters(
                expectedSortedTable, excludeColumns, includeColumns);
        log.debug(
                "{}: Applying column exclude and include filters to sorted actual table",
                methodName);
        final ITable actualFilteredTable = applyColumnFilters(actualSortedTable,
                excludeColumns, includeColumns);

        log.debug("{}: Creating additionalColumnInfo for expected table",
                methodName);
        final Column[] additionalColumnInfo =
                makeAdditionalColumnInfo(expectedTable, excludeColumns);
        log.trace("{}: additionalColumnInfo={}", methodName,
                additionalColumnInfo);

        logSortedTables(expectedSortedTable, actualSortedTable);

        log.debug("{}: Comparing expected table to actual table", methodName);
        compareData(expectedFilteredTable, actualFilteredTable,
                additionalColumnInfo, defaultValueComparer,
                columnValueComparers);
    }

    /**
     * Reduces the given columns to those that survive the given exclude and
     * include column filters, using the same matching semantics - including
     * {@link DefaultColumnFilter}'s wildcard pattern support - as
     * {@link #applyColumnFilters(ITable, String[], String[])}, so the sort
     * key always matches the columns that end up compared.
     *
     * @param columns
     *            The columns to filter.
     * @param excludeColumns
     *            The column names to exclude; null or empty to exclude none.
     * @param includeColumns
     *            The column names to only include; null to include all.
     * @param tableName
     *            The table name; passed only to
     *            {@link DefaultColumnFilter#accept(String, Column)} for its
     *            debug logging.
     * @return The filtered columns, in columns' original order.
     */
    private Column[] makeSortColumns(final Column[] columns,
            final String[] excludeColumns, final String[] includeColumns,
            final String tableName)
    {
        final DefaultColumnFilter columnFilter = new DefaultColumnFilter();
        if (includeColumns != null)
        {
            for (final String includeColumn : includeColumns)
            {
                columnFilter.includeColumn(includeColumn);
            }
        }
        if (excludeColumns != null)
        {
            for (final String excludeColumn : excludeColumns)
            {
                columnFilter.excludeColumn(excludeColumn);
            }
        }

        final List<Column> sortColumns = new ArrayList<>();
        for (final Column column : columns)
        {
            if (columnFilter.accept(tableName, column))
            {
                sortColumns.add(column);
            }
        }
        return sortColumns.toArray(new Column[sortColumns.size()]);
    }

    /**
     * If expected column definitions exist and are {@link DataType.UNKNOWN},
     * make them from actual table column definitions.
     *
     * @throws DataSetException if the actual table's columns cannot be retrieved.
     */
    private Column[] makeExpectedTableColumns(final Column[] actualColumns,
            final ITableMetaData expectedTableMetaData) throws DataSetException
    {
        final Column[] expectedTableColumns;

        final Column[] expectedColumns = expectedTableMetaData.getColumns();
        if (expectedColumns.length > 0)
        {
            final DataType dataType = expectedColumns[0].getDataType();
            if (DataType.UNKNOWN.equals(dataType))
            {
                // all column definitions probably unknown, use actual's
                expectedTableColumns = makeExpectedTableColumns(actualColumns,
                        expectedColumns);
            } else
            {
                // all expected column definitions probably known, use them
                expectedTableColumns = expectedColumns;
            }
        } else
        {
            // no column definitions exist, so don't falsely add any
            expectedTableColumns = expectedColumns;
        }

        return expectedTableColumns;
    }

    /**
     * Make expected Column[] from actual table column definitions so expected
     * data comparisons use data types from database (and expected data columns
     * handled same as actual data in comparisons). Don't include columns from
     * actual that are not in expected.
     */
    private Column[] makeExpectedTableColumns(final Column[] actualColumns,
            final Column[] expectedColumns)
    {
        final Set<String> expectedColumnNames =
                Arrays.stream(expectedColumns).map(Column::getColumnName)
                        .map(name -> name.toLowerCase(Locale.ENGLISH))
                        .collect(Collectors.toSet());

        final List<Column> expectedColumnsList = Arrays.stream(actualColumns)
                .filter(col -> expectedColumnNames.contains(
                        col.getColumnName().toLowerCase(Locale.ENGLISH)))
                .collect(Collectors.toList());
        return expectedColumnsList
                .toArray(new Column[expectedColumnsList.size()]);
    }

    private void logSortedTables(final SortedTable expectedSortedTable,
            final SortedTable actualSortedTable)
    {
        if (log.isTraceEnabled())
        {
            logSortedTable("expectedSortedTable", expectedSortedTable);
            logSortedTable("actualSortedTable", actualSortedTable);
        }
    }

    private void logSortedTable(final String tableTypeName,
            final SortedTable table)
    {
        final String methodName = "logSortedTable:";
        final Column[] sortColumns = table.getSortColumns();
        log.trace("{} {} sortColumns={}", methodName, tableTypeName,
                sortColumns);
        try
        {
            final String tableContents = tableFormatter.format(table);
            log.trace("{} {} tableContents={}", methodName, tableTypeName,
                    tableContents);
        } catch (final DataSetException e)
        {
            log.error("{} Error trying to log table={}", methodName,
                    tableTypeName, e);
        }
    }

    /**
     * Compare the tables, enables easy overriding.
     * <p>
     * Uses {@link #failureHandler} when set; otherwise defers to
     * {@link Assertion#assertWithValueComparer(ITable, ITable, Column[], ValueComparer, Map)}'s
     * own {@link org.dbunit.assertion.DefaultFailureHandler}, configured with
     * additionalColumnInfo.
     *
     * @param expectedTable the table containing all expected results.
     * @param actualTable the table containing all actual results.
     * @param additionalColumnInfo the additional columns to include in failure messages.
     * @param defaultValueComparer the value comparer used when no more specific comparer is configured.
     * @param columnValueComparers the per-column value comparers to use.
     * @throws DatabaseUnitException if the tables' row counts, columns, or data do not match.
     */
    protected void compareData(final ITable expectedTable,
            final ITable actualTable, final Column[] additionalColumnInfo,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers)
            throws DatabaseUnitException
    {
        if (failureHandler == null)
        {
            Assertion.assertWithValueComparer(expectedTable, actualTable,
                    additionalColumnInfo, defaultValueComparer,
                    columnValueComparers);
        } else
        {
            Assertion.assertWithValueComparer(expectedTable, actualTable,
                    failureHandler, defaultValueComparer,
                    columnValueComparers);
        }
    }

    /**
     * Don't add excluded columns to additionalColumnInfo as they are not found
     * and generate a not found message in the fail message.
     *
     * @param expectedTable
     *            Not null.
     * @param excludeColumns
     *            Nullable.
     * @return the additional column info, excluding excludeColumns.
     * @throws DataSetException if the expected table's columns cannot be retrieved.
     */
    protected Column[] makeAdditionalColumnInfo(final ITable expectedTable,
            final String[] excludeColumns) throws DataSetException
    {
        final Column[] allColumns =
                expectedTable.getTableMetaData().getColumns();

        return excludeColumns == null ? allColumns
                : makeAdditionalColumnInfo(excludeColumns, allColumns);
    }

    /**
     * Don't add excluded columns to additionalColumnInfo as they are not found
     * and generate a not found message in the fail message.
     *
     * @param excludeColumns
     *            Not null.
     * @param allColumns
     *            Not null.
     * @return the additional column info, excluding excludeColumns.
     */
    protected Column[] makeAdditionalColumnInfo(final String[] excludeColumns,
            final Column[] allColumns)
    {
        final List<Column> keepColumnsList = new ArrayList<>();
        final List<String> excludeColumnsList = Arrays.asList(excludeColumns);

        for (final Column column : allColumns)
        {
            final String columnName = column.getColumnName();
            if (!excludeColumnsList.contains(columnName))
            {
                keepColumnsList.add(column);
            }
        }

        return keepColumnsList.toArray(new Column[keepColumnsList.size()]);
    }

    /**
     * Make a <code>IDataSet</code> from the specified files with case sensitive
     * table names as false.
     *
     * @param dataFiles
     *            Represents the array of dbUnit data files.
     * @param dataFilesName
     *            Concept name of the data files, e.g. prep, expected.
     * @return The composite dataset.
     * @throws DataSetException
     *             On dbUnit errors.
     */
    public IDataSet makeCompositeDataSet(final String[] dataFiles,
            final String dataFilesName) throws DataSetException
    {
        return makeCompositeDataSet(dataFiles, dataFilesName, false);
    }

    /**
     * Make a <code>IDataSet</code> from the specified files.
     *
     * @param dataFiles
     *            Represents the array of dbUnit data files.
     * @param dataFilesName
     *            Concept name of the data files, e.g. prep, expected.
     * @param isCaseSensitiveTableNames
     *            true if case sensitive table names is on.
     * @return The composite dataset.
     * @throws DataSetException
     *             On dbUnit errors.
     */
    public IDataSet makeCompositeDataSet(final String[] dataFiles,
            final String dataFilesName, final boolean isCaseSensitiveTableNames)
            throws DataSetException
    {
        if (dataFileLoader == null)
        {
            throw new IllegalStateException(
                    "dataFileLoader is null; must configure or set it first");
        }

        final int count = dataFiles.length;
        log.debug("makeCompositeDataSet: {} dataFiles count={}", dataFilesName,
                count);
        if (count == 0)
        {
            log.info("makeCompositeDataSet: Specified zero {} data files",
                    dataFilesName);
        }

        final List<IDataSet> list = new ArrayList<>();
        for (int i = 0; i < count; i++)
        {
            final IDataSet ds = dataFileLoader.load(dataFiles[i]);
            list.add(ds);
        }

        final IDataSet[] dataSet = list.toArray(new IDataSet[0]);
        return new CompositeDataSet(dataSet, true, isCaseSensitiveTableNames);
    }

    /**
     * Apply the specified exclude and include column filters to the specified
     * table.
     *
     * @param table
     *            The table to apply the filters to.
     * @param excludeColumns
     *            The exclude filters; use null or empty array to mean exclude
     *            none.
     * @param includeColumns
     *            The include filters; use null to mean include all.
     * @return The filtered table.
     * @throws DataSetException if applying the filters fails.
     */
    public ITable applyColumnFilters(final ITable table,
            final String[] excludeColumns, final String[] includeColumns)
            throws DataSetException
    {
        if (table == null)
        {
            throw new IllegalArgumentException("table is null");
        }

        ITable filteredTable = table;

        // note: dbunit interprets an empty inclusion filter array as one
        // not wanting to compare anything!
        if (includeColumns == null)
        {
            log.debug("applyColumnFilters: including columns=(all)");
        } else
        {
            log.debug("applyColumnFilters: including columns='{}'",
                    Arrays.toString(includeColumns));
            filteredTable = DefaultColumnFilter
                    .includedColumnsTable(filteredTable, includeColumns);
        }

        if (excludeColumns == null || excludeColumns.length == 0)
        {
            log.debug("applyColumnFilters: excluding columns=(none)");
        } else
        {
            log.debug("applyColumnFilters: excluding columns='{}'",
                    Arrays.toString(excludeColumns));
            filteredTable = DefaultColumnFilter
                    .excludedColumnsTable(filteredTable, excludeColumns);
        }

        return filteredTable;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IDataSet getPrepDataset()
    {
        return prepDataSet;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IDataSet getExpectedDataset()
    {
        return expectedDataSet;
    }

    /**
     * Get the databaseTester.
     *
     * @see #databaseTester
     *
     * @return The databaseTester.
     */
    @Override
    public IDatabaseTester getDatabaseTester()
    {
        return databaseTester;
    }

    /**
     * Set the databaseTester.
     *
     * @see #databaseTester
     *
     * @param databaseTester
     *            The databaseTester to set.
     */
    @Override
    public void setDatabaseTester(final IDatabaseTester databaseTester)
    {
        this.databaseTester = databaseTester;
    }

    /**
     * Get whether the connection lookupFeatureValue() and cleanupData() are
     * done with is closed.
     *
     * @see #closeConnectionAfterTest
     *
     * @return True if it is closed, false if not.
     * @since 3.4.0
     */
    public boolean isCloseConnectionAfterTest()
    {
        return closeConnectionAfterTest;
    }

    /**
     * Set whether the connection lookupFeatureValue() and cleanupData() are
     * done with is closed. Default is true. Set to false when databaseTester
     * shares a {@link org.dbunit.database.CachingConnectionProvider} across
     * test methods, so this instance does not close a connection other tests
     * still expect to reuse.
     *
     * @see #closeConnectionAfterTest
     *
     * @param closeConnectionAfterTest
     *            True to close it, false to leave it open.
     * @since 3.4.0
     */
    @Override
    public void setCloseConnectionAfterTest(
            final boolean closeConnectionAfterTest)
    {
        this.closeConnectionAfterTest = closeConnectionAfterTest;
    }

    /**
     * Get the dataFileLoader.
     *
     * @see #dataFileLoader
     *
     * @return The dataFileLoader.
     */
    public DataFileLoader getDataFileLoader()
    {
        return dataFileLoader;
    }

    /**
     * Set the dataFileLoader.
     *
     * @see #dataFileLoader
     *
     * @param dataFileLoader
     *            The dataFileLoader to set.
     */
    @Override
    public void setDataFileLoader(final DataFileLoader dataFileLoader)
    {
        this.dataFileLoader = dataFileLoader;
    }

    /**
     * Set the prepDs.
     *
     * @see #prepDataSet
     *
     * @param prepDataSet
     *            The prepDs to set.
     */
    public void setPrepDs(final IDataSet prepDataSet)
    {
        this.prepDataSet = prepDataSet;
    }

    /**
     * Set the expectedDs.
     *
     * @see #expectedDataSet
     *
     * @param expectedDataSet
     *            The expectedDs to set.
     */
    public void setExpectedDs(final IDataSet expectedDataSet)
    {
        this.expectedDataSet = expectedDataSet;
    }

    /**
     * Get the tableDefs.
     *
     * @see #verifyTableDefs
     *
     * @return The tableDefs.
     */
    public VerifyTableDefinition[] getVerifyTableDefs()
    {
        return verifyTableDefs;
    }

    /**
     * Set the tableDefs.
     *
     * @see #verifyTableDefs
     *
     * @param verifyTableDefs
     *            The tableDefs to set.
     */
    public void setVerifyTableDefs(
            final VerifyTableDefinition[] verifyTableDefs)
    {
        this.verifyTableDefs = verifyTableDefs;
    }

    /**
     * Returns the verifier used to check that verify table definitions and the expected dataset agree.
     *
     * @return the verifier used to check that verify table definitions and the expected dataset agree.
     */
    public ExpectedDataSetAndVerifyTableDefinitionVerifier getExpectedDataSetAndVerifyTableDefinitionVerifier()
    {
        return expectedDataSetAndVerifyTableDefinitionVerifier;
    }

    /**
     * Sets the verifier used to check that verify table definitions and the expected dataset agree.
     *
     * @param expectedDataSetAndVerifyTableDefinitionVerifier the verifier to use.
     */
    public void setExpectedDataSetAndVerifyTableDefinitionVerifier(
            final ExpectedDataSetAndVerifyTableDefinitionVerifier expectedDataSetAndVerifyTableDefinitionVerifier)
    {
        this.expectedDataSetAndVerifyTableDefinitionVerifier =
                expectedDataSetAndVerifyTableDefinitionVerifier;
    }

    /**
     * Get the failureHandler.
     *
     * @see #failureHandler
     *
     * @return The failureHandler.
     * @since 3.5.0
     */
    public FailureHandler getFailureHandler()
    {
        return failureHandler;
    }

    /**
     * Set the failureHandler.
     *
     * @see #failureHandler
     *
     * @param failureHandler
     *            The failureHandler to set.
     * @since 3.5.0
     */
    @Override
    public void setFailureHandler(final FailureHandler failureHandler)
    {
        this.failureHandler = failureHandler;
    }

    /**
     * Get the RowCountCheck in use.
     *
     * @see #rowCountChecker
     *
     * @return The RowCountCheck, or null if none has been resolved or set yet.
     * @since 3.6.0
     */
    public RowCountCheck getRowCountCheck()
    {
        return rowCountChecker.getRowCountCheck();
    }

    /**
     * Set the RowCountCheck, overriding the one otherwise lazily built from the shared
     * connection's DatabaseConfig.
     *
     * @see #rowCountChecker
     *
     * @param rowCountCheck
     *            The RowCountCheck to use.
     * @since 3.6.0
     */
    public void setRowCountCheck(final RowCountCheck rowCountCheck)
    {
        rowCountChecker.setRowCountCheck(rowCountCheck);
    }

    /**
     * Set the enabled flag and excluded table patterns to resolve a RowCountCheck from, instead
     * of the shared connection's DatabaseConfig - the values an annotation such as
     * {@code @DbUnitRowCountCheck} declares.
     *
     * @see #rowCountChecker
     *
     * @param enabled
     *            Whether the check is enabled.
     * @param exclude
     *            The excluded table patterns; null is treated as empty (excludes none).
     * @since 3.6.0
     */
    @Override
    public void setRowCountCheckOverride(final boolean enabled, final String[] exclude)
    {
        rowCountChecker.setEnabledOverride(enabled, exclude);
    }

    /**
     * Clear a previously set enabled flag and excluded table patterns override, returning to
     * resolving a RowCountCheck from the shared connection's DatabaseConfig.
     *
     * <p>A caller reusing one instance across several tests - e.g. one held by a
     * {@code @DbUnitTestCase} static field - must call this for a test that declares no
     * {@code @DbUnitRowCountCheck} of its own, so an earlier test's override does not
     * silently carry over onto this one.
     *
     * @see #rowCountChecker
     * @since 3.6.0
     */
    @Override
    public void clearRowCountCheckOverride()
    {
        rowCountChecker.clearEnabledOverride();
    }

    /**
     * Set DatabaseConfig property name/value pairs to apply to the connection shared by
     * setupData(), verifyData() and cleanupData(), every time {@link #getConnection()}
     * resolves it.
     *
     * @see #databaseConfigProperties
     *
     * @param databaseConfigProperties
     *            The properties to apply; null or empty applies none.
     * @since 3.6.0
     */
    @Override
    public void setDatabaseConfigProperties(final Properties databaseConfigProperties)
    {
        if (databaseConfigProperties == null)
        {
            this.databaseConfigProperties = null;
        } else
        {
            final Properties copy = new Properties();
            copy.putAll(databaseConfigProperties);
            this.databaseConfigProperties = copy;
        }
    }

    /**
     * {@link IDatabaseTester} that runs setUp/tearDown operations against a
     * connection supplied by the given {@link Callable} instead of calling
     * {@code getConnection()} on a wrapped {@link IDatabaseTester}, so
     * repeated {@link #onSetup()}/{@link #onTearDown()} calls driven through
     * this instance share whatever connection the supplier itself caches
     * (here, {@link DefaultPrepAndExpectedTestCase#getReusableConnection()})
     * rather than each opening a new one.
     *
     * @since 3.4.0
     */
    private static final class ReusableConnectionDatabaseTester
            extends AbstractDatabaseTester
    {
        private final Callable<IDatabaseConnection> connectionSupplier;

        /**
         * Create new instance with the specified connection supplier.
         *
         * @param connectionSupplier
         *            Supplies the connection to use; invoked once per
         *            {@link #getConnection()} call, so it is responsible for
         *            any caching of its own.
         */
        private ReusableConnectionDatabaseTester(
                final Callable<IDatabaseConnection> connectionSupplier)
        {
            this.connectionSupplier = connectionSupplier;
        }

        @Override
        public IDatabaseConnection getConnection() throws Exception
        {
            return connectionSupplier.call();
        }
    }
}