001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package de.softwareforge.testing.postgres.junit5;
015
016import static com.google.common.base.Preconditions.checkNotNull;
017import static com.google.common.base.Preconditions.checkState;
018
019import de.softwareforge.testing.postgres.embedded.DatabaseInfo;
020import de.softwareforge.testing.postgres.embedded.DatabaseManager;
021import de.softwareforge.testing.postgres.embedded.DatabaseManager.DatabaseManagerBuilder;
022import de.softwareforge.testing.postgres.embedded.EmbeddedPostgres;
023
024import java.lang.reflect.Type;
025import java.sql.SQLException;
026import java.util.UUID;
027import javax.sql.DataSource;
028
029import com.google.common.annotations.VisibleForTesting;
030import edu.umd.cs.findbugs.annotations.NonNull;
031import org.junit.jupiter.api.extension.AfterAllCallback;
032import org.junit.jupiter.api.extension.AfterEachCallback;
033import org.junit.jupiter.api.extension.BeforeAllCallback;
034import org.junit.jupiter.api.extension.BeforeEachCallback;
035import org.junit.jupiter.api.extension.ExtensionContext;
036import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
037import org.junit.jupiter.api.extension.ExtensionContext.Store;
038import org.junit.jupiter.api.extension.ParameterContext;
039import org.junit.jupiter.api.extension.ParameterResolutionException;
040import org.junit.jupiter.api.extension.ParameterResolver;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043
044/**
045 * A <a href="https://junit.org/junit5/docs/current/user-guide/#extensions">JUnit 5 extension</a> that manages an embedded PostgreSQL database server.
046 * <p>
047 * This extension can provide the {@link EmbeddedPostgres} instance, a {@link DatabaseInfo} or a {@link DataSource} object as test parameters.
048 *
049 * <ul>
050 * <li>Using a {@link DatabaseInfo} parameter is equivalent to calling {@link EmbeddedPgExtension#createDatabaseInfo()}.</li>
051 * <li>Using a {@link DataSource} parameter is equivalent to calling {@link EmbeddedPgExtension#createDataSource()}.</li>
052 * </ul>
053 */
054public final class EmbeddedPgExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver {
055
056    private static final Logger LOG = LoggerFactory.getLogger(EmbeddedPgExtension.class);
057
058    // multiple instances must use different namespaces
059    private final Namespace PG_NAMESPACE = Namespace.create(UUID.randomUUID());
060
061    private final DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder;
062
063    private volatile DatabaseManager databaseManager = null;
064
065    private EmbeddedPgExtension(DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder) {
066        this.databaseManagerBuilder = databaseManagerBuilder;
067    }
068
069    /**
070     * Creates a new {@link EmbeddedPgExtensionBuilder} that allows further customization of the {@link EmbeddedPgExtension}. The resulting extension manages
071     * the database server in multi-mode (creating multiple databases).
072     *
073     * @return A {@link EmbeddedPgExtensionBuilder} instance. Never null.
074     */
075    @NonNull
076    static EmbeddedPgExtensionBuilder multiDatabase() {
077        return new EmbeddedPgExtensionBuilder(true);
078    }
079
080    /**
081     * Creates a new {@link EmbeddedPgExtensionBuilder} that allows further customization of the {@link EmbeddedPgExtension}. The resulting extension manages
082     * the database server in single-mode (using a single database instance).
083     *
084     * @return A {@link EmbeddedPgExtensionBuilder} instance. Never null.
085     */
086    @NonNull
087    static EmbeddedPgExtensionBuilder singleDatabase() {
088        return new EmbeddedPgExtensionBuilder(false);
089    }
090
091    /**
092     * Default constructor which allows using this extension with the {@link org.junit.jupiter.api.extension.ExtendWith} annotation.
093     * <p>
094     * This is equivalent to using <pre>{@code
095     *     @RegisterExtension
096     *     public static EmbeddedPgExtension pg = MultiDatabaseBuilder.instanceWithDefaults().build();
097     *     }</pre>
098     *
099     * @since 3.0
100     */
101    public EmbeddedPgExtension() {
102        this(new DatabaseManagerBuilder(true).withInstancePreparer(EmbeddedPostgres.Builder::withDefaults));
103    }
104
105    /**
106     * Returns a data source. Depending on the mode it returns a datasource connected to the same database (single mode) or a new database (multi mode) on every
107     * call.
108     *
109     * @return A {@link DataSource} instance. This is never null.
110     * @throws SQLException If a problem connecting to the database occurs.
111     */
112    @NonNull
113    public DataSource createDataSource() throws SQLException {
114        return createDatabaseInfo().asDataSource();
115    }
116
117    @VisibleForTesting
118    EmbeddedPostgres getEmbeddedPostgres() {
119        return databaseManager.getEmbeddedPostgres();
120    }
121
122    /**
123     * Returns a new {@link DatabaseInfo} describing a database connection.
124     * <p>
125     * Depending on the mode, this either describes the same database (single mode) or a new database (multi mode).
126     *
127     * @return A {@link DatabaseInfo} instance. This is never null.
128     * @throws SQLException If a problem connecting to the database occurs.
129     */
130    @NonNull
131    public DatabaseInfo createDatabaseInfo() throws SQLException {
132        checkState(databaseManager != null, "no before method has been called!");
133
134        DatabaseInfo databaseInfo = databaseManager.getDatabaseInfo();
135        LOG.info("Connection to {}", databaseInfo.asJdbcUrl());
136        return databaseInfo;
137    }
138
139    @Override
140    public void beforeAll(@NonNull ExtensionContext extensionContext) throws Exception {
141        checkNotNull(extensionContext, "extensionContext is null");
142
143        Store pgStore = extensionContext.getStore(PG_NAMESPACE);
144
145        TestMode testMode = pgStore.getOrComputeIfAbsent(TestMode.TESTMODE_KEY,
146                k -> new TestMode(extensionContext.getUniqueId(), databaseManagerBuilder.build()),
147                TestMode.class);
148
149        this.databaseManager = testMode.start(extensionContext.getUniqueId());
150    }
151
152    @Override
153    public void afterAll(@NonNull ExtensionContext extensionContext) throws Exception {
154        checkNotNull(extensionContext, "extensionContext is null");
155
156        Store pgStore = extensionContext.getStore(PG_NAMESPACE);
157        TestMode testMode = pgStore.get(TestMode.TESTMODE_KEY, TestMode.class);
158
159        if (testMode != null) {
160            this.databaseManager = testMode.stop(extensionContext.getUniqueId());
161        }
162    }
163
164    @Override
165    public void beforeEach(@NonNull ExtensionContext extensionContext) throws Exception {
166        checkNotNull(extensionContext, "extensionContext is null");
167
168        Store pgStore = extensionContext.getStore(PG_NAMESPACE);
169        TestMode testMode = pgStore.getOrComputeIfAbsent(TestMode.TESTMODE_KEY,
170                k -> new TestMode(extensionContext.getUniqueId(), databaseManagerBuilder.build()),
171                TestMode.class);
172
173        this.databaseManager = testMode.start(extensionContext.getUniqueId());
174    }
175
176    @Override
177    public void afterEach(@NonNull ExtensionContext extensionContext) throws Exception {
178        checkNotNull(extensionContext, "extensionContext is null");
179
180        Store pgStore = extensionContext.getStore(PG_NAMESPACE);
181        TestMode testMode = pgStore.get(TestMode.TESTMODE_KEY, TestMode.class);
182
183        if (testMode != null) {
184            this.databaseManager = testMode.stop(extensionContext.getUniqueId());
185        }
186    }
187
188    @Override
189    public boolean supportsParameter(@NonNull ParameterContext parameterContext, ExtensionContext extensionContext) {
190        Type type = parameterContext.getParameter().getType();
191        return type == EmbeddedPostgres.class
192                || type == DatabaseInfo.class
193                || type == DataSource.class;
194    }
195
196    @Override
197    public Object resolveParameter(@NonNull ParameterContext parameterContext, ExtensionContext extensionContext) {
198        Type type = parameterContext.getParameter().getType();
199        try {
200            if (type == EmbeddedPostgres.class) {
201                return getEmbeddedPostgres();
202            } else if (type == DatabaseInfo.class) {
203                return createDatabaseInfo();
204
205            } else if (type == DataSource.class) {
206                return createDataSource();
207            }
208        } catch (SQLException e) {
209            throw new ParameterResolutionException("Could not create " + type.getTypeName() + " instance", e);
210        }
211        return null;
212    }
213
214    /**
215     * Builder for {@link EmbeddedPgExtension} customization.
216     */
217    public static final class EmbeddedPgExtensionBuilder extends DatabaseManager.Builder<EmbeddedPgExtension> {
218
219        private EmbeddedPgExtensionBuilder(boolean multiMode) {
220            super(multiMode);
221        }
222
223        /**
224         * Create a {@link EmbeddedPgExtension} instance.
225         *
226         * @return A {@link EmbeddedPgExtension} instance. Is never null.
227         */
228        @Override
229        @NonNull
230        public EmbeddedPgExtension build() {
231            DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder = new DatabaseManagerBuilder(multiMode)
232                    .withDatabasePreparers(databasePreparers.build())
233                    .withInstancePreparers(instancePreparers.build());
234
235            return new EmbeddedPgExtension(databaseManagerBuilder);
236        }
237    }
238
239    private static final class TestMode {
240
241        private static final Object TESTMODE_KEY = new Object();
242
243        private final String id;
244        private final DatabaseManager databaseManager;
245
246        private TestMode(String id, DatabaseManager databaseManager) {
247            this.id = id;
248            this.databaseManager = databaseManager;
249        }
250
251        private DatabaseManager start(String id) throws Exception {
252            if (this.id.equals(id)) {
253                databaseManager.start();
254            }
255
256            return databaseManager;
257        }
258
259        private DatabaseManager stop(String id) throws Exception {
260            if (this.id.equals(id)) {
261                databaseManager.close();
262                return null;
263            }
264
265            return databaseManager;
266        }
267    }
268}