AnnotatedTestExecutor.java

/*
 *
 * The DbUnit Database Testing Framework
 * Copyright (C)2002-2026, 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.annotation.runtime;

import java.util.Properties;

import org.dbunit.ConnectionPreservingOperationListener;
import org.dbunit.DatabaseUnitException;
import org.dbunit.DefaultPrepAndExpectedTestCase;
import org.dbunit.IDatabaseTester;
import org.dbunit.IOperationListener;
import org.dbunit.PrepAndExpectedTestCase;
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;

/**
 * Drives one test's dbUnit lifecycle from a resolved {@link AnnotatedTestConfiguration},
 * dispatching {@link #beforeTest()}/{@link #afterTest(boolean)} to one of two path objects -
 * {@link SetupTeardownLifecycle} when {@link AnnotatedTestConfiguration#isExpected()} is false,
 * {@link ExpectedLifecycle} when it is true - and owning what both share: the one
 * {@link TestScopedConnection} this test's steps reuse, its {@link ConnectionOwnership}
 * decision, the {@code @DbUnitProperty} application, and (for an annotation-driven test) the
 * {@link ExecutorOperationListener} installed on the tester.
 *
 * <p>Not intended for direct use by test code; this is machinery consumed by a binding such as
 * {@code DbUnitExtension}, which resolves the {@link IDatabaseTester} and any injected
 * {@link PrepAndExpectedTestCase} - field discovery is binding-specific - and hands them here
 * already resolved.
 *
 * <p>The binding tells this executor, through the constructor's {@code annotationDriven} flag,
 * whether the test opted into the {@code org.dbunit.annotation} family at all - any
 * {@code @DbUnit*} annotation, or a {@code @DbUnitTester}/{@code @DbUnitTestCase} field. When it
 * did not - a bare {@code @ExtendWith(DbUnitExtension.class)} class with one plain, unannotated
 * {@link IDatabaseTester} field, the 3.5.0 lifecycle-only style - this executor takes the
 * <em>classic path</em>: {@link #installOperationListener()} does not run, so the tester's
 * {@link IOperationListener} is left untouched, and {@link AnnotatedRowCountCheck} never
 * piggybacks (it captures its baseline eagerly and {@link SetupTeardownLifecycle} lets
 * {@code onSetup()}/{@code onTearDown()} manage their own connections) - exactly as
 * {@code DbUnitExtension} did before this class existed. The prep/expected path is always
 * annotation-driven (it needs {@code @DbUnitExpected}).
 *
 * <p>The connection {@link #getConnection()} memoizes - the tester's own on the setup/teardown
 * path, {@link PrepAndExpectedTestCase#getReusableConnection()}'s on the prep/expected path so a
 * {@code Connection}/{@code IDatabaseConnection} parameter shares the test case's own - is used
 * by the row count check and by a binding's parameter injection, and closed in
 * {@link #afterTest(boolean)} when {@link ConnectionOwnership#mayClose()} allows it. On an
 * annotation-driven test the {@link ExecutorOperationListener} additionally shields that one
 * connection, by identity, from a premature close by the tester's own {@code onSetup()}/
 * {@code onTearDown()} machinery while still forwarding a close for any other connection the
 * tester hands out.
 *
 * <p>The row count check (see {@code org.dbunit.database.rowcount.RowCountCheck}) runs, on the
 * setup/teardown path, through {@link AnnotatedRowCountCheck}; the prep/expected path already
 * has its own via {@link DefaultPrepAndExpectedTestCase#preTest()} and {@code cleanupData()}.
 * Either way, when {@code @DbUnitRowCountCheck} is declared its resolved {@code RowCountCheck}
 * overrides whichever of the two would otherwise resolve one from the connection's own
 * {@link DatabaseConfig} - for the prep/expected path via
 * {@link DefaultPrepAndExpectedTestCase#setRowCountCheckOverride(boolean, String[])} (applied
 * by {@link InjectedTestCaseConfigurer}), which resolves that connection lazily on its own so
 * this executor needs none just to build the override.
 *
 * @author Jeff Jensen
 * @since 3.6.0
 */
public class AnnotatedTestExecutor
{
    private final AnnotatedTestConfiguration configuration;
    private final IDatabaseTester tester;
    private final boolean annotationDriven;
    private final AutoCommitOffWarning autoCommitOffWarning = new AutoCommitOffWarning();
    private final TestScopedConnection testScopedConnection;
    private final ExpectedLifecycle expectedLifecycle;
    private final SetupTeardownLifecycle setupTeardownLifecycle;

    /**
     * Creates an executor for an annotation-driven test - equivalent to
     * {@link #AnnotatedTestExecutor(AnnotatedTestConfiguration, IDatabaseTester, PrepAndExpectedTestCase, boolean)}
     * with {@code annotationDriven} {@code true}.
     *
     * @param configuration The resolved configuration to execute.
     * @param tester The tester to drive the setup/teardown path with, or to construct a
     *            {@link PrepAndExpectedTestCase} around when {@code prepAndExpectedTestCase}
     *            is {@code null} and the prep/expected path is configured.
     * @param prepAndExpectedTestCase An already-injected test case to drive instead of
     *            constructing one, or {@code null} to have this executor construct one from
     *            {@link AnnotatedTestConfiguration#getPrepAndExpectedTestCaseClass()}.
     */
    public AnnotatedTestExecutor(final AnnotatedTestConfiguration configuration,
            final IDatabaseTester tester,
            final PrepAndExpectedTestCase prepAndExpectedTestCase)
    {
        this(configuration, tester, prepAndExpectedTestCase, true);
    }

    /**
     * Creates an executor for one test.
     *
     * <p>Mutates {@code tester} only when {@code annotationDriven} is true:
     * {@link #installOperationListener()} runs here, replacing {@code tester}'s
     * {@link IOperationListener} with an {@code ExecutorOperationListener} wrapping the previous
     * one. A binding constructs one executor per test method, so a {@code tester} shared across
     * methods (e.g. a {@code static @DbUnitTester} field) is re-wrapped each time -
     * {@link ConnectionPreservingOperationListener#unwrap(IOperationListener)} unwraps the prior
     * wrapper first, so the layers do not stack - and the last test's wrapper stays installed
     * on the tester after the class
     * finishes, holding a reference to that last executor until the tester is itself discarded
     * or given a new listener. When {@code annotationDriven} is false - the classic path - the
     * tester's listener is left untouched; see the class Javadoc.
     *
     * @param configuration The resolved configuration to execute.
     * @param tester The tester to drive the setup/teardown path with, or to construct a
     *            {@link PrepAndExpectedTestCase} around when {@code prepAndExpectedTestCase}
     *            is {@code null} and the prep/expected path is configured.
     * @param prepAndExpectedTestCase An already-injected test case to drive instead of
     *            constructing one, or {@code null} to have this executor construct one from
     *            {@link AnnotatedTestConfiguration#getPrepAndExpectedTestCaseClass()}.
     * @param annotationDriven Whether the test opted into the {@code org.dbunit.annotation}
     *            family - any {@code @DbUnit*} annotation, or a
     *            {@code @DbUnitTester}/{@code @DbUnitTestCase} field. False for a bare
     *            {@code @ExtendWith(DbUnitExtension.class)} class with one plain, unannotated
     *            {@link IDatabaseTester} field, whose tester listener and connection lifecycle
     *            are then left exactly as the 3.5.0 lifecycle. Always true for the prep/expected
     *            path, which needs {@code @DbUnitExpected}.
     */
    public AnnotatedTestExecutor(final AnnotatedTestConfiguration configuration,
            final IDatabaseTester tester,
            final PrepAndExpectedTestCase prepAndExpectedTestCase,
            final boolean annotationDriven)
    {
        this.configuration = configuration;
        this.tester = tester;
        this.annotationDriven = annotationDriven;
        this.expectedLifecycle =
                new ExpectedLifecycle(configuration, tester, prepAndExpectedTestCase);
        final ConnectionOwnership ownership = new ConnectionOwnership(
                configuration::isCloseConnectionAfterTest, tester::getOperationListener,
                this::borrowingLifecycleRan);
        this.testScopedConnection = new TestScopedConnection(this::acquireConnection, ownership,
                this::onConnectionAcquired);
        this.setupTeardownLifecycle = new SetupTeardownLifecycle(configuration, tester,
                testScopedConnection, annotationDriven);
        if (annotationDriven)
        {
            installOperationListener();
        }
    }

    /**
     * The {@code borrowingLifecycleRan} input to this executor's {@link ConnectionOwnership}:
     * true unless the connection is held by a reused injected {@code @DbUnitTestCase} instance
     * whose {@link ExpectedLifecycle#before()} failed before {@code configureTest()} - see
     * {@link ExpectedLifecycle#isConnectionOwnedByUnconfiguredInjectedTestCase()}.
     */
    private boolean borrowingLifecycleRan()
    {
        return !(configuration.isExpected()
                && expectedLifecycle.isConnectionOwnedByUnconfiguredInjectedTestCase());
    }

    private IDatabaseConnection acquireConnection() throws Exception
    {
        return configuration.isExpected()
                ? expectedLifecycle.ensureTestCase().getReusableConnection()
                : tester.getConnection();
    }

    /**
     * Runs on each connection {@link #testScopedConnection} acquires: applies
     * {@code @DbUnitProperty} values, and - on the annotation-driven setup/teardown path, where
     * this executor's prep and teardown operations run through the tester rather than a
     * transaction-managed {@link org.dbunit.operation.TransactionOperation} - warns once if
     * that connection has autocommit off, the same check {@link DefaultPrepAndExpectedTestCase}
     * makes for the prep/expected path. The classic path resolves this connection only for the
     * read-only row count check baseline, so an autocommit-off connection there is not a
     * problem and is not warned about; the prep/expected path warns via
     * {@link DefaultPrepAndExpectedTestCase}'s own acquisition of the same connection.
     */
    private void onConnectionAcquired(final IDatabaseConnection connection)
    {
        applyProperties(connection, configuration.getDatabaseConfigProperties());
        if (annotationDriven && !configuration.isExpected())
        {
            autoCommitOffWarning.accept(connection);
        }
    }

    /**
     * Returns the tester this executor drives, so a binding can inject it as a parameter
     * without resolving a second, independent instance.
     *
     * @return The tester passed to the constructor.
     */
    public IDatabaseTester getTester()
    {
        return tester;
    }

    /**
     * Returns the {@link PrepAndExpectedTestCase} this executor drives, or {@code null} when
     * {@link AnnotatedTestConfiguration#isExpected()} is false, or it is true but nothing has
     * constructed one yet - a binding resolving a parameter before {@link #beforeTest()} runs
     * (e.g. a {@code @BeforeEach} parameter) sees {@code null} unless one was already injected
     * through the constructor, or an earlier parameter resolution this same test already
     * triggered {@link ExpectedLifecycle#ensureTestCase()} (e.g. a {@code Connection} parameter
     * resolved via {@link #getConnection()}).
     *
     * @return The test case this executor drives, or {@code null} if none exists yet.
     */
    public PrepAndExpectedTestCase getPrepAndExpectedTestCase()
    {
        return expectedLifecycle.getPrepAndExpectedTestCase();
    }

    /**
     * Returns the connection this test's steps use, resolved at most once per test and reused
     * by every caller instead of each asking independently - the row count check's own
     * baseline/verify calls on the setup/teardown path, and a binding injecting an
     * {@code IDatabaseConnection}/{@code Connection} parameter on either path. On the
     * prep/expected path ({@link AnnotatedTestConfiguration#isExpected()} true), delegates to
     * {@link PrepAndExpectedTestCase#getReusableConnection()} - constructing the test case first
     * via {@link ExpectedLifecycle#ensureTestCase()} if it does not exist yet - instead of
     * asking {@link #tester} directly, so a parameter injection shares that test case's own
     * connection; on the setup/teardown path, asks {@link #tester} directly. For a tester with
     * no connection caching of its own (e.g. a plain {@code JdbcDatabaseTester}), asking
     * independently on either path would otherwise open one new physical connection per call.
     * Closed in {@link #afterTest(boolean)} - see the class Javadoc.
     *
     * @return The connection to reuse, or {@code null} if none is available (e.g. a test
     *         double).
     * @throws Exception If resolving the connection fails.
     */
    public IDatabaseConnection getConnection() throws Exception
    {
        return testScopedConnection.getConnection();
    }

    /**
     * Returns the connection {@link #getConnection()} has already resolved this test, without
     * triggering resolution - {@code null} both before {@link #getConnection()} first runs and
     * when the tester has no connection to offer. Used only by
     * {@link ExecutorOperationListener} to recognize, at whatever later moment the tester's own
     * {@code onSetup()}/{@code onTearDown()} happens to ask, whether the connection they are
     * about to close is the one this executor is still holding onto.
     */
    private IDatabaseConnection peekResolvedConnection()
    {
        return testScopedConnection.peekConnection();
    }

    /**
     * Wraps the tester's operation listener in an {@link ExecutorOperationListener}, so that
     * regardless of whether {@code @DbUnitProperty} is configured, this executor's own memoized
     * connection (see the class Javadoc) is protected from a premature close by the tester's own
     * setup/teardown machinery, and so the row count check baseline can be captured from
     * whatever connection {@code onSetup()} retrieves on its own - see
     * {@link #onListenerFirstConnectionRetrieved(IDatabaseConnection)}. Called from the
     * constructor only for an {@code annotationDriven} test; the classic path leaves the
     * tester's listener untouched (see the class Javadoc).
     * {@link ConnectionPreservingOperationListener#unwrap(IOperationListener)} un-nests a prior
     * wrapper so re-wrapping a tester shared across tests does not stack layers.
     */
    private void installOperationListener()
    {
        final IOperationListener delegate =
                ConnectionPreservingOperationListener.unwrap(tester.getOperationListener());
        tester.setOperationListener(new ExecutorOperationListener(
                configuration.getDatabaseConfigProperties(), this::peekResolvedConnection,
                delegate, this::onListenerFirstConnectionRetrieved));
    }

    /**
     * Runs when the tester's own {@code onSetup()}/{@code onTearDown()} retrieves a connection
     * and notifies the installed {@link ExecutorOperationListener}: on the setup/teardown path,
     * warns once on an autocommit-off connection (the single point every connection a prep or
     * teardown operation runs on passes through - see {@link AutoCommitOffWarning}), then offers
     * it to {@link AnnotatedRowCountCheck} for the piggybacked baseline capture. A no-op on the
     * prep/expected path: the row count check there is the {@link PrepAndExpectedTestCase}'s own,
     * and the connection identity is {@link PrepAndExpectedTestCase#getReusableConnection()}'s -
     * adopting a directly-retrieved one here would reintroduce, for a
     * {@link PrepAndExpectedTestCase} implementation that drives {@code tester.onSetup()}
     * itself, the connection-identity split {@link #getConnection()}'s path split exists to
     * prevent.
     */
    private void onListenerFirstConnectionRetrieved(final IDatabaseConnection connection)
    {
        if (configuration.isExpected())
        {
            return;
        }
        autoCommitOffWarning.accept(connection);
        setupTeardownLifecycle.adoptAndCaptureBaselineOnFirstConnection(connection);
    }

    /**
     * Applies {@code @DbUnitProperty} values to {@code connection}'s {@link DatabaseConfig}; a
     * no-op when {@code properties} is empty. Called from both {@link #getConnection()}, so the
     * row count check's baseline/verify/override calls always see property state already
     * applied before they read it, and from {@link ExecutorOperationListener}, since a tester's
     * {@code onSetup()}/{@code onTearDown()} may resolve their own connection independently of
     * {@link #getConnection()}'s memoized one.
     *
     * @param connection The connection whose {@link DatabaseConfig} to apply the values to.
     * @param properties The {@code @DbUnitProperty} values; empty applies none.
     */
    static void applyProperties(final IDatabaseConnection connection,
            final Properties properties)
    {
        if (properties.isEmpty())
        {
            return;
        }
        try
        {
            connection.getConfig().setPropertiesByString(properties);
        } catch (final DatabaseUnitException e)
        {
            throw new IllegalStateException("Failed to apply @DbUnitProperty values.", e);
        }
    }

    /**
     * Runs every before-test step by delegating to the path {@link AnnotatedTestConfiguration#isExpected()}
     * selects: {@link SetupTeardownLifecycle#before()} or {@link ExpectedLifecycle#before()}.
     *
     * @throws Exception If any step fails.
     */
    public void beforeTest() throws Exception
    {
        if (configuration.isExpected())
        {
            expectedLifecycle.before();
        } else
        {
            setupTeardownLifecycle.before();
        }
    }

    /**
     * Runs every after-test step by delegating to the path {@link AnnotatedTestConfiguration#isExpected()}
     * selects - {@link SetupTeardownLifecycle#after(boolean)} or
     * {@link ExpectedLifecycle#after(boolean)} - then closes the connection
     * {@link #testScopedConnection} memoized, if one was ever resolved, so a difference
     * reported by the row count check still leaves it closed.
     *
     * <p>When both the step and closing the connection fail, the step's failure is the one
     * thrown, with the close failure attached to it via
     * {@link Throwable#addSuppressed(Throwable)} rather than replacing it - a plain {@code
     * finally} block would otherwise let the close failure silently discard the more useful
     * diagnostic (e.g. which table the row count check found unexpectedly changed). The step's
     * failure is caught as {@link Throwable}, not just {@link Exception}, and rethrown with its
     * original static type preserved: a comparison mismatch on the prep/expected path fails via
     * {@link org.dbunit.assertion.DbComparisonFailure}, an {@link Error} subclass, not an
     * {@code Exception} - catching only {@code Exception} would skip closing the connection on
     * every ordinary verification failure, the single most common way this method's step throws
     * at all.
     *
     * @param testFailed Whether the test method itself already threw; when {@code true},
     *            verification is skipped so a difference does not mask the real failure.
     * @throws Exception If any step fails with a checked exception.
     * @throws Error If any step fails with an {@code Error}, e.g. a comparison mismatch.
     */
    public void afterTest(final boolean testFailed) throws Exception
    {
        try
        {
            if (configuration.isExpected())
            {
                expectedLifecycle.after(testFailed);
            } else
            {
                setupTeardownLifecycle.after(testFailed);
            }
        } catch (final Throwable primaryFailure)
        {
            try
            {
                testScopedConnection.release();
            } catch (final Exception closeFailure)
            {
                primaryFailure.addSuppressed(closeFailure);
            }
            throw primaryFailure;
        }
        testScopedConnection.release();
    }
}