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 #createDatabaseInfo()}.</li>
051 * <li>Using a {@link DataSource} parameter is equivalent to calling {@link #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    public EmbeddedPgExtension() {
100        this(new DatabaseManagerBuilder(true).withInstancePreparer(EmbeddedPostgres.Builder::withDefaults));
101    }
102
103    /**
104     * 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
105     * call.
106     *
107     * @return A {@link DataSource} instance. This is never null.
108     * @throws SQLException If a problem connecting to the database occurs.
109     */
110    @NonNull
111    public DataSource createDataSource() throws SQLException {
112        return createDatabaseInfo().asDataSource();
113    }
114
115    @VisibleForTesting
116    EmbeddedPostgres getEmbeddedPostgres() {
117        return databaseManager.getEmbeddedPostgres();
118    }
119
120    /**
121     * Returns a new {@link DatabaseInfo} describing a database connection.
122     * <p>
123     * Depending on the mode, this either describes the same database (single mode) or a new database (multi mode).
124     *
125     * @return A {@link DatabaseInfo} instance. This is never null.
126     * @throws SQLException If a problem connecting to the database occurs.
127     */
128    @NonNull
129    public DatabaseInfo createDatabaseInfo() throws SQLException {
130        checkState(databaseManager != null, "no before method has been called!");
131
132        DatabaseInfo databaseInfo = databaseManager.getDatabaseInfo();
133        LOG.info("Connection to {}", databaseInfo.asJdbcUrl());
134        return databaseInfo;
135    }
136
137    @Override
138    public void beforeAll(@NonNull ExtensionContext extensionContext) throws Exception {
139        checkNotNull(extensionContext, "extensionContext is null");
140
141        Store pgStore = extensionContext.getStore(PG_NAMESPACE);
142
143        TestMode testMode = pgStore.getOrComputeIfAbsent(TestMode.TESTMODE_KEY,
144                k -> new TestMode(extensionContext.getUniqueId(), databaseManagerBuilder.build()),
145                TestMode.class);
146
147        this.databaseManager = testMode.start(extensionContext.getUniqueId());
148    }
149
150    @Override
151    public void afterAll(@NonNull ExtensionContext extensionContext) throws Exception {
152        checkNotNull(extensionContext, "extensionContext is null");
153
154        Store pgStore = extensionContext.getStore(PG_NAMESPACE);
155        TestMode testMode = pgStore.get(TestMode.TESTMODE_KEY, TestMode.class);
156
157        if (testMode != null) {
158            this.databaseManager = testMode.stop(extensionContext.getUniqueId());
159        }
160    }
161
162    @Override
163    public void beforeEach(@NonNull ExtensionContext extensionContext) throws Exception {
164        checkNotNull(extensionContext, "extensionContext is null");
165
166        Store pgStore = extensionContext.getStore(PG_NAMESPACE);
167        TestMode testMode = pgStore.getOrComputeIfAbsent(TestMode.TESTMODE_KEY,
168                k -> new TestMode(extensionContext.getUniqueId(), databaseManagerBuilder.build()),
169                TestMode.class);
170
171        this.databaseManager = testMode.start(extensionContext.getUniqueId());
172    }
173
174    @Override
175    public void afterEach(@NonNull ExtensionContext extensionContext) throws Exception {
176        checkNotNull(extensionContext, "extensionContext is null");
177
178        Store pgStore = extensionContext.getStore(PG_NAMESPACE);
179        TestMode testMode = pgStore.get(TestMode.TESTMODE_KEY, TestMode.class);
180
181        if (testMode != null) {
182            this.databaseManager = testMode.stop(extensionContext.getUniqueId());
183        }
184    }
185
186    @Override
187    public boolean supportsParameter(@NonNull ParameterContext parameterContext, ExtensionContext extensionContext) {
188        Type type = parameterContext.getParameter().getType();
189        return type == EmbeddedPostgres.class
190                || type == DatabaseInfo.class
191                || type == DataSource.class;
192    }
193
194    @Override
195    public Object resolveParameter(@NonNull ParameterContext parameterContext, ExtensionContext extensionContext) {
196        Type type = parameterContext.getParameter().getType();
197        try {
198            if (type == EmbeddedPostgres.class) {
199                return getEmbeddedPostgres();
200            } else if (type == DatabaseInfo.class) {
201                return createDatabaseInfo();
202
203            } else if (type == DataSource.class) {
204                return createDataSource();
205            }
206        } catch (SQLException e) {
207            throw new ParameterResolutionException("Could not create " + type.getTypeName() + " instance", e);
208        }
209        return null;
210    }
211
212    /**
213     * Builder for {@link EmbeddedPgExtension} customization.
214     */
215    public static final class EmbeddedPgExtensionBuilder extends DatabaseManager.Builder<EmbeddedPgExtension> {
216
217        private EmbeddedPgExtensionBuilder(boolean multiMode) {
218            super(multiMode);
219        }
220
221        /**
222         * Create a {@link EmbeddedPgExtension} instance.
223         *
224         * @return A {@link EmbeddedPgExtension} instance. Is never null.
225         */
226        @Override
227        @NonNull
228        public EmbeddedPgExtension build() {
229            DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder = new DatabaseManagerBuilder(multiMode)
230                    .withDatabasePreparers(databasePreparers.build())
231                    .withInstancePreparers(instancePreparers.build());
232
233            return new EmbeddedPgExtension(databaseManagerBuilder);
234        }
235    }
236
237    private static final class TestMode {
238
239        private static final Object TESTMODE_KEY = new Object();
240
241        private final String id;
242        private final DatabaseManager databaseManager;
243
244        private TestMode(String id, DatabaseManager databaseManager) {
245            this.id = id;
246            this.databaseManager = databaseManager;
247        }
248
249        private DatabaseManager start(String id) throws Exception {
250            if (this.id.equals(id)) {
251                databaseManager.start();
252            }
253
254            return databaseManager;
255        }
256
257        private DatabaseManager stop(String id) throws Exception {
258            if (this.id.equals(id)) {
259                databaseManager.close();
260                return null;
261            }
262
263            return databaseManager;
264        }
265    }
266}