ExpectedLifecycle.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.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import org.dbunit.IDatabaseTester;
import org.dbunit.PrepAndExpectedTestCase;
import org.dbunit.util.fileloader.DataFileLoader;

/**
 * The prep/expected path of one test's dbUnit lifecycle, driven by
 * {@link AnnotatedTestExecutor} when {@link AnnotatedTestConfiguration#isExpected()} is true:
 * resolve or construct the {@link PrepAndExpectedTestCase}, push the {@code @DbUnitConfig}
 * values it can only receive through its own API onto it (via {@link InjectedTestCaseConfigurer}),
 * set the setup/teardown operations, then run its {@code configureTest()}/{@code preTest()}
 * before the test and {@code postTest()} after.
 *
 * <p>The row count check on this path is the {@link PrepAndExpectedTestCase}'s own -
 * {@code DefaultPrepAndExpectedTestCase} captures and verifies it around its own steps - so
 * this class never touches it; {@code @DbUnitRowCountCheck} reaches it as an override through
 * {@link InjectedTestCaseConfigurer}.
 *
 * <p>The dataset and setup/teardown operations {@link #tester} carries coming into the test are
 * snapshotted at the start and restored at the end (see {@link TesterStateSnapshot}), so a
 * tester shared across methods - a {@code static @DbUnitTester} field, or one under
 * {@code @TestInstance(Lifecycle.PER_CLASS)} - carries no per-method state onto the next
 * method.
 *
 * @author Jeff Jensen
 * @since 3.6.0
 */
final class ExpectedLifecycle
{
    private final AnnotatedTestConfiguration configuration;
    private final IDatabaseTester tester;
    private final boolean testCaseInjected;

    private PrepAndExpectedTestCase testCase;
    private boolean configured;
    private TesterStateSnapshot incomingState;

    /**
     * Creates the prep/expected lifecycle for one test.
     *
     * @param configuration The resolved configuration.
     * @param tester The tester to construct a {@link PrepAndExpectedTestCase} around when one
     *            is not injected, and whose setup/teardown operations to set.
     * @param injectedTestCase An already-injected {@code @DbUnitTestCase} instance to drive, or
     *            {@code null} to construct one from
     *            {@link AnnotatedTestConfiguration#getPrepAndExpectedTestCaseClass()}.
     */
    ExpectedLifecycle(final AnnotatedTestConfiguration configuration,
            final IDatabaseTester tester, final PrepAndExpectedTestCase injectedTestCase)
    {
        this.configuration = configuration;
        this.tester = tester;
        this.testCase = injectedTestCase;
        this.testCaseInjected = injectedTestCase != null;
    }

    /**
     * Returns the {@link PrepAndExpectedTestCase} this lifecycle drives, or {@code null} when
     * nothing has constructed one yet - a binding resolving a parameter before {@link #before()}
     * runs sees {@code null} unless one was injected through the constructor, or an earlier
     * parameter resolution this same test already triggered {@link #ensureTestCase()}.
     *
     * @return The test case, or {@code null} if none exists yet.
     */
    PrepAndExpectedTestCase getPrepAndExpectedTestCase()
    {
        return testCase;
    }

    /**
     * Returns {@link #testCase}, constructing it via {@link #newTestCase()} first when it does
     * not already exist (no {@code @DbUnitTestCase} field supplied one, and nothing has
     * constructed one yet). Safe to call more than once - construction only happens once - and
     * safe to call before {@link #before()} does, e.g. from a binding resolving an early
     * {@code @BeforeEach} {@code Connection} parameter through
     * {@link PrepAndExpectedTestCase#getReusableConnection()}, since construction has no side
     * effect beyond reflectively instantiating the instance.
     *
     * @return The test case this lifecycle drives.
     * @throws Exception If constructing it fails.
     */
    PrepAndExpectedTestCase ensureTestCase() throws Exception
    {
        if (testCase == null)
        {
            testCase = newTestCase();
        }
        return testCase;
    }

    /**
     * Returns whether the connection came from an injected {@code @DbUnitTestCase} instance's
     * {@link PrepAndExpectedTestCase#getReusableConnection()} for a {@code @BeforeEach}
     * parameter, and {@link #before()} then failed before {@code configureTest()} - so
     * {@code postTest()}/{@code cleanupData()} never ran to close that connection the test
     * case's own way. It is the {@code borrowingLifecycleRan} input to
     * {@link AnnotatedTestExecutor}'s {@link org.dbunit.database.connection.ConnectionOwnership}:
     * {@code false} here means the executor's
     * {@link org.dbunit.database.connection.TestScopedConnection#release()} leaves the
     * connection alone, so a reused instance (e.g. a {@code static @DbUnitTestCase} field, or
     * any instance field under {@code @TestInstance(PER_CLASS)}) is not stranded holding a
     * closed connection for the next test method. The instance keeps ownership; its own next
     * lifecycle run, or the caller discarding it, releases the connection.
     *
     * <p>Only for an <em>injected</em> instance: a test case constructed here is single-use
     * and discarded after this test, so the executor still closes its connection rather than
     * leak it.
     *
     * @return True when a reused injected instance still owns the connection.
     */
    boolean isConnectionOwnedByUnconfiguredInjectedTestCase()
    {
        return testCaseInjected && !configured;
    }

    /**
     * Resolves or constructs the {@link PrepAndExpectedTestCase}, configures it, and runs its
     * {@code configureTest()} then {@code preTest()}.
     *
     * @throws Exception If any step fails.
     */
    void before() throws Exception
    {
        ensureTestCase();
        incomingState = TesterStateSnapshot.capture(tester);
        new InjectedTestCaseConfigurer(configuration, testCase).applyAll();
        applySetUpOperation();
        applyTearDownOperation();
        testCase.configureTest(configuration.getVerifyTableDefinitions(),
                configuration.getPrepDataFiles(), configuration.getExpectedDataFiles());
        configured = true;
        testCase.preTest();
    }

    /**
     * Calls {@code postTest(!testFailed)} on the test case, unless {@link #before()} never ran
     * or failed before {@code configureTest()} completed - in which case the instance, when
     * non-null, is either uninitialized or (for one reused across tests) still holding an
     * earlier test's prep/expected state, and {@code postTest()} would run cleanup against that
     * stale state instead of nothing. Either way the incoming tester state is restored
     * afterward - on a failure, with any restore failure attached as suppressed rather than
     * masking the primary.
     *
     * @param testFailed Whether the test method itself already threw.
     * @throws Exception If {@code postTest()} fails.
     */
    void after(final boolean testFailed) throws Exception
    {
        try
        {
            if (configured)
            {
                testCase.postTest(!testFailed);
            }
        } catch (final Throwable primaryFailure)
        {
            TesterStateSnapshot.restoreSuppressing(tester, incomingState, primaryFailure);
            throw primaryFailure;
        }
        TesterStateSnapshot.restore(tester, incomingState);
    }

    /**
     * Applies {@code @DbUnitSetup}'s operation to {@link #tester}, unconditionally:
     * {@link AnnotatedTestConfiguration#getSetUpOperation()} already defaults to
     * {@link org.dbunit.operation.DatabaseOperation#CLEAN_INSERT} - the same default the
     * setup/teardown path applies whenever it has a dataset to set up - and {@code setupData()}
     * always runs against a real, if possibly empty, prep dataset on this path, so there is no
     * null-dataset case to avoid touching.
     */
    private void applySetUpOperation()
    {
        tester.setSetUpOperation(configuration.getSetUpOperation());
    }

    /**
     * Applies {@code @DbUnitTearDown}'s operation to {@link #tester}, unconditionally:
     * {@code cleanupData()} always runs on this path and reads its teardown operation from the
     * tester lazily, so the default of {@link org.dbunit.operation.DatabaseOperation#NONE}
     * still has to be set explicitly to override whatever a reused test case's tester already
     * carries. Whatever this applies is undone by {@link #after(boolean)} restoring
     * {@link #incomingState} once the test finishes, so a shared tester carries no per-method
     * operation forward.
     */
    private void applyTearDownOperation()
    {
        tester.setTearDownOperation(configuration.getTearDownOperation());
    }

    private PrepAndExpectedTestCase newTestCase() throws Exception
    {
        final Class<? extends PrepAndExpectedTestCase> testCaseClass =
                configuration.getPrepAndExpectedTestCaseClass();
        final Constructor<? extends PrepAndExpectedTestCase> constructor;
        try
        {
            constructor = testCaseClass.getDeclaredConstructor(DataFileLoader.class,
                    IDatabaseTester.class, boolean.class);
        } catch (final NoSuchMethodException e)
        {
            throw new IllegalStateException("DbUnitConfig.prepAndExpectedTestCase class "
                    + testCaseClass.getName() + " has no (DataFileLoader, IDatabaseTester,"
                    + " boolean) constructor.", e);
        }
        constructor.setAccessible(true);
        try
        {
            return constructor.newInstance(configuration.getDataFileLoader(), tester,
                    configuration.isCloseConnectionAfterTest());
        } catch (final InvocationTargetException e)
        {
            throw new IllegalStateException("DbUnitConfig.prepAndExpectedTestCase class "
                    + testCaseClass.getName() + " threw from its (DataFileLoader,"
                    + " IDatabaseTester, boolean) constructor.", e.getCause());
        }
    }
}