Skip to content

Commit 50923e8

Browse files
committed
Improve support for running tests against Docker
... and other "remote" systems. * Property to explicitly enable or disable availability of events * Property to provide mapped path of database files (e.g. Docker volume location or remote network share mount)
1 parent 6decdae commit 50923e8

13 files changed

+506
-198
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,15 @@ test {
235235
'test.user': project.'test.user',
236236
'test.password': project.'test.password',
237237
'test.db.dir': project.'test.db.dir',
238+
'test.db.mapped': project.findProperty('test.db.mapped'),
238239
'test.db.host': project.'test.db.host',
239240
'test.db.port': project.'test.db.port',
240241
'test.db.lc_ctype': project.'test.db.lc_ctype',
241242
'test.gds_type': project.'test.gds_type',
242243
'test.use_firebird_autocommit': project.'test.use.firebird.autocommit',
243244
'jdk.net.useFastTcpLoopback': project.findProperty('jdk.net.useFastTcpLoopback') ?: 'false',
244245
'test.db_on_docker': project.findProperty('test.dbondocker') ?: 'false',
246+
'test.event.available': project.findProperty('test.event.available'),
245247
'test.enableProtocol': project.'test.enableProtocol'
246248
)
247249
if (project.hasProperty('test.jna.library.path')) {

devdoc/build-documentation.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,27 @@ have privileges to create databases and users
8080

8181
Less important properties, but relevant for testing remotely:
8282

83-
- `test.db.host` - the hostname of Firebird (defaults to `localhost`)
83+
- `test.db.host` - the hostname of Firebird (defaults to `localhost`).
84+
Be aware that changing the `test.db.host` can result in test failures or
85+
ignored tests as some tests perform verifications against the local
86+
filesystem. Some of those tests can be disabled by using the `test.dbondocker`
87+
property set to true, or use `test.db.mapped` so these tests can read
88+
the files locally.
8489
- `test.db.port` - the port of Firebird (defaults to `3050`)
85-
- `test.db.dir` - the path to use for databases (defaults to `${module.output}/db`)
86-
87-
Be aware that changing the `test.db.host` can result in test failures as some
88-
tests perform verifications against the local filesystem. Some of those tests
89-
can be disabled by using the `test.dbondocker` property set to true.
90+
- `test.db.dir` - the server-side path to use for databases (defaults to `${module.output}/db`)
91+
- `test.db.mapped` - (optional) the test-local path that corresponds to
92+
the server-side path (`test.db.dir`). Some tests verify existence of
93+
server-side files or delete them (e.g. backups). Specify the host directory
94+
backing the Docker volume, or mounting the remote database directory.
95+
Tests need access to read, create, and delete files and directories.
96+
- `test.dbondocker` - Make tests aware the server is in a Docker container
97+
(defaults to `false`). This avoids assumptions that `test.db.dir` is directly
98+
accessible to the tests if `test.db.host` is localhost. Use `test.db.mapped`
99+
to specify the volume mount. When `true`, tests default to assuming the event
100+
port is not available. Use `test.event.available` to override.
101+
- `test.event.available` - Explicitly mark events as available (`true`) or
102+
unavailable (`false`) (defaults to not set). When this property is not set,
103+
tests assume the event port is available, unless `test.dbondocker` is `true`.
90104

91105
Properties for varying the type of connection tested:
92106

src/test/org/firebirdsql/common/FBTestProperties.java

Lines changed: 142 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-FileCopyrightText: Copyright 2003 Ryan Baldwin
22
// SPDX-FileCopyrightText: Copyright 2003-2010 Roman Rokytskyy
3-
// SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
3+
// SPDX-FileCopyrightText: Copyright 2012-2026 Mark Rotteveel
44
// SPDX-License-Identifier: LGPL-2.1-or-later
55
package org.firebirdsql.common;
66

@@ -37,17 +37,20 @@
3737
import java.sql.SQLException;
3838
import java.util.Map;
3939
import java.util.MissingResourceException;
40+
import java.util.Optional;
4041
import java.util.Properties;
4142
import java.util.ResourceBundle;
4243

44+
import static java.util.Objects.requireNonNullElse;
45+
import static org.firebirdsql.common.PathUtils.posixPathString;
4346
import static org.firebirdsql.common.matchers.GdsTypeMatchers.isEmbeddedType;
4447
import static org.firebirdsql.common.matchers.GdsTypeMatchers.isOtherNativeType;
4548
import static org.firebirdsql.common.matchers.GdsTypeMatchers.isPureJavaType;
4649
import static org.firebirdsql.jaybird.util.StringUtils.trimToNull;
4750
import static org.hamcrest.Matchers.not;
4851

4952
/**
50-
* Helper class for test properties (database user, password, paths etc)
53+
* Helper class for test properties (database user, password, paths etc).
5154
*/
5255
public final class FBTestProperties {
5356

@@ -83,10 +86,15 @@ public static String getProperty(String property, String defaultValue) {
8386
public static final String DB_USER = getProperty("test.user", "sysdba");
8487
public static final String DB_PASSWORD = getProperty("test.password", "masterkey");
8588
public static final String DB_PATH = getProperty("test.db.dir", "");
89+
private static final Path DB_DIR_PATH;
90+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
91+
private static final Optional<Path> DB_MAPPED_LOCAL_PATH =
92+
Optional.ofNullable(trimToNull(getProperty("test.db.mapped"))).map(Path::of);
8693
public static final String DB_SERVER_URL = getProperty("test.db.host", "localhost");
8794
public static final int DB_SERVER_PORT = Integer.parseInt(getProperty("test.db.port", "3050"));
8895
public static final String DB_LC_CTYPE = getProperty("test.db.lc_ctype", "NONE");
8996
public static final boolean DB_ON_DOCKER = Boolean.parseBoolean(getProperty("test.db_on_docker", "false"));
97+
private static final @Nullable Boolean EVENTS_AVAILABLE;
9098
public static final String GDS_TYPE = getProperty("test.gds_type", "PURE_JAVA");
9199
public static final boolean USE_FIREBIRD_AUTOCOMMIT =
92100
Boolean.parseBoolean(getProperty("test.use_firebird_autocommit", "false"));
@@ -96,39 +104,149 @@ public static String getProperty(String property, String defaultValue) {
96104
Boolean.parseBoolean(getProperty("test.native_legacy_auth_compat", "false"));
97105
private static final String NATIVE_LEGACY_AUTH_COMPAT_AUTH_PLUGINS = "Legacy_Auth";
98106

107+
static {
108+
var dbDirPath = Path.of(DB_PATH.isEmpty() ? "." : DB_PATH);
109+
if (isSameHostServer()) {
110+
dbDirPath = dbDirPath.toAbsolutePath();
111+
}
112+
DB_DIR_PATH = dbDirPath;
113+
114+
String eventsAvailable = trimToNull(getProperty("test.event.available"));
115+
EVENTS_AVAILABLE = eventsAvailable != null ? Boolean.valueOf(eventsAvailable) : null;
116+
}
117+
99118
public static boolean isLocalhost() {
100-
return "localhost".equals(DB_SERVER_URL) || "127.0.0.1".equals(DB_SERVER_URL);
119+
return "localhost".equalsIgnoreCase(DB_SERVER_URL)
120+
|| "127.0.0.1".equals(DB_SERVER_URL)
121+
|| "::1".equals(DB_SERVER_URL);
122+
}
123+
124+
public static boolean isDbOnDocker() {
125+
return DB_ON_DOCKER;
101126
}
102127

103128
public static boolean isDefaultPort() {
104129
return DB_SERVER_PORT == PropertyConstants.DEFAULT_PORT;
105130
}
106131

132+
public static boolean isEventPortAvailable() {
133+
return requireNonNullElse(EVENTS_AVAILABLE, !DB_ON_DOCKER);
134+
}
135+
107136
public static String getDatabasePath() {
108137
return getDatabasePath(DB_NAME);
109138
}
110139

111140
public static String getDatabasePath(String name) {
112-
if (not(isEmbeddedType()).matches(GDS_TYPE) && (!isLocalhost() || DB_ON_DOCKER)) {
113-
return DB_PATH + "/" + name;
114-
}
115-
return Path.of(DB_PATH, name).toAbsolutePath().toString();
141+
return getDatabasePath(Path.of(DB_PATH, name));
142+
}
143+
144+
public static String getDatabasePath(Path path) {
145+
return isSameHostServer() ? path.toString() : posixPathString(path);
146+
}
147+
148+
/**
149+
* Check if database paths and Firebird server (or embedded) is truly local.
150+
* <p>
151+
* Specifically, it means that the server accesses paths on the local filesystem of this machine and that the return
152+
* values of {@link #getDatabasePath()} and {@link #getDatabasePath(String)} are local to this machine.
153+
* </p>
154+
*
155+
* @return {@code true} if server has direct access to the filesystem of this host
156+
* @see #hasMappedDatabaseDirectory()
157+
*/
158+
public static boolean isSameHostServer() {
159+
return isEmbeddedType().matches(GDS_TYPE) || (isLocalhost() && !isDbOnDocker());
160+
}
161+
162+
/**
163+
* Check if database files are locally accessible to the tests.
164+
* <p>
165+
* Specifically, it means that either the server has direct filesystem access (i.e. {@link #isSameHostServer()}
166+
* returns {@code true}), or the directory specified in system property {@code test.db.dir} is mapped (e.g. as a
167+
* volume, or network share) in the directory specified in system property {@code test.db.mapped}, and
168+
* {@link #getMappedDatabaseDirectory()}, {@link #getMappedDatabasePath()} and {@link #getMappedDatabasePath(String)} return
169+
* non-empty.
170+
* </p>
171+
* <p>
172+
* Contrary to {@link #isSameHostServer()}, this method returning {@code true} does not necessarily mean that the
173+
* Firebird server has direct filesystem access to this machine, nor that it would recognize paths as returned by
174+
* the {@code getLocalDatabase...} methods.
175+
* </p>
176+
*
177+
* @return {@code true} if database files are locally accessible
178+
*/
179+
public static boolean hasMappedDatabaseDirectory() {
180+
return isSameHostServer() || DB_MAPPED_LOCAL_PATH.isPresent();
116181
}
117182

118183
/**
119184
* Builds a firebird database connection string for the supplied database file.
120-
*
121-
* @param name Database name
185+
*
186+
* @param name
187+
* Database name
122188
* @return URL or path for the gds type.
123189
*/
124190
public static String getdbpath(String name) {
191+
String databasePath = getDatabasePath(name);
125192
if (isEmbeddedType().matches(GDS_TYPE)) {
126-
return Path.of(DB_PATH, name).toAbsolutePath().toString();
193+
return databasePath;
127194
} else {
128-
return DB_SERVER_URL + "/" + DB_SERVER_PORT + ":" + getDatabasePath(name);
195+
return DB_SERVER_URL + "/" + DB_SERVER_PORT + ":" + databasePath;
129196
}
130197
}
131198

199+
/**
200+
* The mapped database directory.
201+
* <p>
202+
* For local databases, this is the normal database directory ({@code test.db.dir}). For remote databases, or
203+
* databases on Docker, this can be configured using the {@code test.db.mapped} system property. For remote
204+
* databases, this can be the local mount of a network share, or for databases on Docker, the host directory backing
205+
* the volume.
206+
* </p>
207+
*
208+
* @return mapped database directory, or empty
209+
*/
210+
public static Optional<Path> getMappedDatabaseDirectory() {
211+
return isSameHostServer() ? Optional.of(DB_DIR_PATH) : DB_MAPPED_LOCAL_PATH;
212+
}
213+
214+
/**
215+
* The database name {@code name} resolved against {@link #getMappedDatabaseDirectory()}.
216+
* <p>
217+
* The {@code name} expects a relative path; absolute paths will likely not work unless {@link #isSameHostServer()}
218+
* is {@code true}).
219+
* </p>
220+
*
221+
* @param name
222+
* database name (or other file)
223+
* @return mapped database file path, or empty ({@link #hasMappedDatabaseDirectory()} returns {@code false})
224+
*/
225+
public static Optional<Path> getMappedDatabasePath(String name) {
226+
return getMappedDatabaseDirectory().map(p -> p.resolve(name));
227+
}
228+
229+
/**
230+
* The database name of the default test database.
231+
*
232+
* @return mapped database file path, or empty ({@link #hasMappedDatabaseDirectory()} returns {@code false})
233+
*/
234+
public static Optional<Path> getMappedDatabasePath() {
235+
return getMappedDatabasePath(DB_NAME);
236+
}
237+
238+
/**
239+
* Transforms (remaps) the mapped database path to the server-side database path.
240+
*
241+
* @param mappedPath
242+
* mapped path (i.e. relative to {@link #getMappedDatabaseDirectory()})
243+
* @return database file path
244+
*/
245+
public static Optional<Path> transformMappedToDatabasePath(Path mappedPath) {
246+
return getMappedDatabaseDirectory()
247+
.map(mappedDir -> DB_DIR_PATH.resolve(mappedDir.relativize(mappedPath)));
248+
}
249+
132250
/**
133251
* @return Default database connection properties for this testrun
134252
*/
@@ -293,14 +411,25 @@ public static String getUrl(String dbPath) {
293411
}
294412

295413
/**
296-
* Convenience method equivalent to {@code getUrl(dbPath.toAbsolutePath().toString())}.
414+
* Convenience method equivalent to {@code getUrl(getDatabasePath(dbPath))}.
297415
*
298416
* @param dbPath
299417
* path of the database
300418
* @return JDBC URL (without parameters) for this test run
301419
*/
302420
public static String getUrl(Path dbPath) {
303-
return getUrl(dbPath.toAbsolutePath().toString());
421+
return getUrl(getDatabasePath(dbPath));
422+
}
423+
424+
/**
425+
* Convenience method equivalent to {@code getUrl(mappedPath.toServerPath())}.
426+
*
427+
* @param mappedPath
428+
* path of the database
429+
* @return JDBC URL (without parameters) for this test run
430+
*/
431+
public static String getUrl(MappedPath mappedPath) {
432+
return getUrl(mappedPath.toServerPath());
304433
}
305434

306435
// FACTORY METHODS
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Mark Rotteveel
2+
// SPDX-License-Identifier: LGPL-2.1-or-later
3+
package org.firebirdsql.common;
4+
5+
import java.nio.file.Path;
6+
7+
/**
8+
* A mapped path represents the pair of a mapped local filesystem path and its equivalent server-side path.
9+
*
10+
* @param local
11+
* local (mapped) path
12+
* @param server
13+
* server-side equivalent path
14+
* @see FBTestProperties#hasMappedDatabaseDirectory()
15+
*/
16+
public record MappedPath(Path local, Path server) {
17+
18+
/**
19+
* @return string form of {@link #server()} for use by Firebird
20+
*/
21+
public String toServerPath() {
22+
return FBTestProperties.getDatabasePath(server);
23+
}
24+
25+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Mark Rotteveel
2+
// SPDX-License-Identifier: LGPL-2.1-or-later
3+
package org.firebirdsql.common;
4+
5+
import org.jspecify.annotations.NullMarked;
6+
import org.junit.jupiter.api.extension.AnnotatedElementContext;
7+
import org.junit.jupiter.api.extension.ExtensionContext;
8+
import org.junit.jupiter.api.io.TempDirFactory;
9+
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.util.Optional;
13+
14+
/**
15+
* JUnit {@link TempDirFactory} that by default creates in the mapped database directory, or otherwise falls back
16+
* to the default temporary directory.
17+
*/
18+
@NullMarked
19+
public class MappedTempDirFactory implements TempDirFactory {
20+
21+
@Override
22+
public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
23+
throws Exception {
24+
Optional<Path> optMappedDatabaseDirectory = FBTestProperties.getMappedDatabaseDirectory();
25+
if (optMappedDatabaseDirectory.isPresent()) {
26+
return Files.createTempDirectory(optMappedDatabaseDirectory.get(), "junit-");
27+
}
28+
return Files.createTempDirectory("junit-");
29+
}
30+
31+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Mark Rotteveel
2+
// SPDX-License-Identifier: LGPL-2.1-or-later
3+
package org.firebirdsql.common;
4+
5+
import java.io.File;
6+
import java.nio.file.Path;
7+
8+
/**
9+
* Helpers and utilities for working with paths.
10+
*/
11+
public final class PathUtils {
12+
13+
private PathUtils() {
14+
// no instances
15+
}
16+
17+
/**
18+
* Returns the equivalent of {@link Path#toString()} with {@link File#separatorChar} replaced by forward slash.
19+
* <p>
20+
* This is a naive replacement, so there is no actual guarantee this a valid POSIX path. For example, if
21+
* {@code path} is a Windows absolute path including a drive letter, the result also contains the drive letter.
22+
* </p>
23+
*
24+
* @param path
25+
* path to convert to string
26+
* @return string representation of {@code path} with forward slash as the separator
27+
*/
28+
public static String posixPathString(Path path) {
29+
String pathString = path.toString();
30+
if (File.separatorChar != '/') {
31+
pathString = pathString.replace(File.separatorChar, '/');
32+
}
33+
return pathString;
34+
}
35+
}

0 commit comments

Comments
 (0)